##// END OF EJS Templates
vcs: fixed issues with calling get_changesets method doesn't...
marcink -
r3835:42981614 beta
parent child Browse files
Show More
@@ -1,695 +1,710 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git.repository
3 vcs.backends.git.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Git repository implementation.
6 Git repository implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import os
12 import os
13 import re
13 import re
14 import time
14 import time
15 import urllib
15 import urllib
16 import urllib2
16 import urllib2
17 import logging
17 import logging
18 import posixpath
18 import posixpath
19 import string
19 import string
20
20
21 from dulwich.objects import Tag
21 from dulwich.objects import Tag
22 from dulwich.repo import Repo, NotGitRepository
22 from dulwich.repo import Repo, NotGitRepository
23
23
24 from rhodecode.lib.vcs import subprocessio
24 from rhodecode.lib.vcs import subprocessio
25 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
25 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
26 from rhodecode.lib.vcs.conf import settings
26 from rhodecode.lib.vcs.conf import settings
27
27
28 from rhodecode.lib.vcs.exceptions import (
28 from rhodecode.lib.vcs.exceptions import (
29 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
29 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
30 RepositoryError, TagAlreadyExistError, TagDoesNotExistError
30 RepositoryError, TagAlreadyExistError, TagDoesNotExistError
31 )
31 )
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 from rhodecode.lib.vcs.utils.paths import abspath, get_user_home
35 from rhodecode.lib.vcs.utils.paths import abspath, get_user_home
36
36
37 from rhodecode.lib.vcs.utils.hgcompat import (
37 from rhodecode.lib.vcs.utils.hgcompat import (
38 hg_url, httpbasicauthhandler, httpdigestauthhandler
38 hg_url, httpbasicauthhandler, httpdigestauthhandler
39 )
39 )
40
40
41 from .changeset import GitChangeset
41 from .changeset import GitChangeset
42 from .config import ConfigFile
42 from .config import ConfigFile
43 from .inmemory import GitInMemoryChangeset
43 from .inmemory import GitInMemoryChangeset
44 from .workdir import GitWorkdir
44 from .workdir import GitWorkdir
45
45
46 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
46 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class GitRepository(BaseRepository):
51 class GitRepository(BaseRepository):
52 """
52 """
53 Git repository backend.
53 Git repository backend.
54 """
54 """
55 DEFAULT_BRANCH_NAME = 'master'
55 DEFAULT_BRANCH_NAME = 'master'
56 scm = 'git'
56 scm = 'git'
57
57
58 def __init__(self, repo_path, create=False, src_url=None,
58 def __init__(self, repo_path, create=False, src_url=None,
59 update_after_clone=False, bare=False):
59 update_after_clone=False, bare=False):
60
60
61 self.path = abspath(repo_path)
61 self.path = abspath(repo_path)
62 repo = self._get_repo(create, src_url, update_after_clone, bare)
62 repo = self._get_repo(create, src_url, update_after_clone, bare)
63 self.bare = repo.bare
63 self.bare = repo.bare
64
64
65 @property
65 @property
66 def _config_files(self):
66 def _config_files(self):
67 return [
67 return [
68 self.bare and abspath(self.path, 'config')
68 self.bare and abspath(self.path, 'config')
69 or abspath(self.path, '.git', 'config'),
69 or abspath(self.path, '.git', 'config'),
70 abspath(get_user_home(), '.gitconfig'),
70 abspath(get_user_home(), '.gitconfig'),
71 ]
71 ]
72
72
73 @property
73 @property
74 def _repo(self):
74 def _repo(self):
75 return Repo(self.path)
75 return Repo(self.path)
76
76
77 @property
77 @property
78 def head(self):
78 def head(self):
79 try:
79 try:
80 return self._repo.head()
80 return self._repo.head()
81 except KeyError:
81 except KeyError:
82 return None
82 return None
83
83
84 @property
85 def _empty(self):
86 """
87 Checks if repository is empty ie. without any changesets
88 """
89
90 try:
91 self.revisions[0]
92 except (KeyError, IndexError):
93 return True
94 return False
95
84 @LazyProperty
96 @LazyProperty
85 def revisions(self):
97 def revisions(self):
86 """
98 """
87 Returns list of revisions' ids, in ascending order. Being lazy
99 Returns list of revisions' ids, in ascending order. Being lazy
88 attribute allows external tools to inject shas from cache.
100 attribute allows external tools to inject shas from cache.
89 """
101 """
90 return self._get_all_revisions()
102 return self._get_all_revisions()
91
103
92 @classmethod
104 @classmethod
93 def _run_git_command(cls, cmd, **opts):
105 def _run_git_command(cls, cmd, **opts):
94 """
106 """
95 Runs given ``cmd`` as git command and returns tuple
107 Runs given ``cmd`` as git command and returns tuple
96 (stdout, stderr).
108 (stdout, stderr).
97
109
98 :param cmd: git command to be executed
110 :param cmd: git command to be executed
99 :param opts: env options to pass into Subprocess command
111 :param opts: env options to pass into Subprocess command
100 """
112 """
101
113
102 if '_bare' in opts:
114 if '_bare' in opts:
103 _copts = []
115 _copts = []
104 del opts['_bare']
116 del opts['_bare']
105 else:
117 else:
106 _copts = ['-c', 'core.quotepath=false', ]
118 _copts = ['-c', 'core.quotepath=false', ]
107 safe_call = False
119 safe_call = False
108 if '_safe' in opts:
120 if '_safe' in opts:
109 #no exc on failure
121 #no exc on failure
110 del opts['_safe']
122 del opts['_safe']
111 safe_call = True
123 safe_call = True
112
124
113 _str_cmd = False
125 _str_cmd = False
114 if isinstance(cmd, basestring):
126 if isinstance(cmd, basestring):
115 cmd = [cmd]
127 cmd = [cmd]
116 _str_cmd = True
128 _str_cmd = True
117
129
118 gitenv = os.environ
130 gitenv = os.environ
119 # need to clean fix GIT_DIR !
131 # need to clean fix GIT_DIR !
120 if 'GIT_DIR' in gitenv:
132 if 'GIT_DIR' in gitenv:
121 del gitenv['GIT_DIR']
133 del gitenv['GIT_DIR']
122 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
134 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
123
135
124 _git_path = settings.GIT_EXECUTABLE_PATH
136 _git_path = settings.GIT_EXECUTABLE_PATH
125 cmd = [_git_path] + _copts + cmd
137 cmd = [_git_path] + _copts + cmd
126 if _str_cmd:
138 if _str_cmd:
127 cmd = ' '.join(cmd)
139 cmd = ' '.join(cmd)
128
140
129 try:
141 try:
130 _opts = dict(
142 _opts = dict(
131 env=gitenv,
143 env=gitenv,
132 shell=True,
144 shell=True,
133 )
145 )
134 _opts.update(opts)
146 _opts.update(opts)
135 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
147 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
136 except (EnvironmentError, OSError), err:
148 except (EnvironmentError, OSError), err:
137 tb_err = ("Couldn't run git command (%s).\n"
149 tb_err = ("Couldn't run git command (%s).\n"
138 "Original error was:%s\n" % (cmd, err))
150 "Original error was:%s\n" % (cmd, err))
139 log.error(tb_err)
151 log.error(tb_err)
140 if safe_call:
152 if safe_call:
141 return '', err
153 return '', err
142 else:
154 else:
143 raise RepositoryError(tb_err)
155 raise RepositoryError(tb_err)
144
156
145 return ''.join(p.output), ''.join(p.error)
157 return ''.join(p.output), ''.join(p.error)
146
158
147 def run_git_command(self, cmd):
159 def run_git_command(self, cmd):
148 opts = {}
160 opts = {}
149 if os.path.isdir(self.path):
161 if os.path.isdir(self.path):
150 opts['cwd'] = self.path
162 opts['cwd'] = self.path
151 return self._run_git_command(cmd, **opts)
163 return self._run_git_command(cmd, **opts)
152
164
153 @classmethod
165 @classmethod
154 def _check_url(cls, url):
166 def _check_url(cls, url):
155 """
167 """
156 Functon will check given url and try to verify if it's a valid
168 Functon will check given url and try to verify if it's a valid
157 link. Sometimes it may happened that mercurial will issue basic
169 link. Sometimes it may happened that mercurial will issue basic
158 auth request that can cause whole API to hang when used from python
170 auth request that can cause whole API to hang when used from python
159 or other external calls.
171 or other external calls.
160
172
161 On failures it'll raise urllib2.HTTPError
173 On failures it'll raise urllib2.HTTPError
162 """
174 """
163
175
164 # check first if it's not an local url
176 # check first if it's not an local url
165 if os.path.isdir(url) or url.startswith('file:'):
177 if os.path.isdir(url) or url.startswith('file:'):
166 return True
178 return True
167
179
168 if('+' in url[:url.find('://')]):
180 if('+' in url[:url.find('://')]):
169 url = url[url.find('+') + 1:]
181 url = url[url.find('+') + 1:]
170
182
171 handlers = []
183 handlers = []
172 test_uri, authinfo = hg_url(url).authinfo()
184 test_uri, authinfo = hg_url(url).authinfo()
173 if not test_uri.endswith('info/refs'):
185 if not test_uri.endswith('info/refs'):
174 test_uri = test_uri.rstrip('/') + '/info/refs'
186 test_uri = test_uri.rstrip('/') + '/info/refs'
175 if authinfo:
187 if authinfo:
176 #create a password manager
188 #create a password manager
177 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
189 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
178 passmgr.add_password(*authinfo)
190 passmgr.add_password(*authinfo)
179
191
180 handlers.extend((httpbasicauthhandler(passmgr),
192 handlers.extend((httpbasicauthhandler(passmgr),
181 httpdigestauthhandler(passmgr)))
193 httpdigestauthhandler(passmgr)))
182
194
183 o = urllib2.build_opener(*handlers)
195 o = urllib2.build_opener(*handlers)
184 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
196 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
185
197
186 q = {"service": 'git-upload-pack'}
198 q = {"service": 'git-upload-pack'}
187 qs = '?%s' % urllib.urlencode(q)
199 qs = '?%s' % urllib.urlencode(q)
188 cu = "%s%s" % (test_uri, qs)
200 cu = "%s%s" % (test_uri, qs)
189 req = urllib2.Request(cu, None, {})
201 req = urllib2.Request(cu, None, {})
190
202
191 try:
203 try:
192 resp = o.open(req)
204 resp = o.open(req)
193 return resp.code == 200
205 return resp.code == 200
194 except Exception, e:
206 except Exception, e:
195 # means it cannot be cloned
207 # means it cannot be cloned
196 raise urllib2.URLError("[%s] %s" % (url, e))
208 raise urllib2.URLError("[%s] %s" % (url, e))
197
209
198 def _get_repo(self, create, src_url=None, update_after_clone=False,
210 def _get_repo(self, create, src_url=None, update_after_clone=False,
199 bare=False):
211 bare=False):
200 if create and os.path.exists(self.path):
212 if create and os.path.exists(self.path):
201 raise RepositoryError("Location already exist")
213 raise RepositoryError("Location already exist")
202 if src_url and not create:
214 if src_url and not create:
203 raise RepositoryError("Create should be set to True if src_url is "
215 raise RepositoryError("Create should be set to True if src_url is "
204 "given (clone operation creates repository)")
216 "given (clone operation creates repository)")
205 try:
217 try:
206 if create and src_url:
218 if create and src_url:
207 GitRepository._check_url(src_url)
219 GitRepository._check_url(src_url)
208 self.clone(src_url, update_after_clone, bare)
220 self.clone(src_url, update_after_clone, bare)
209 return Repo(self.path)
221 return Repo(self.path)
210 elif create:
222 elif create:
211 os.mkdir(self.path)
223 os.mkdir(self.path)
212 if bare:
224 if bare:
213 return Repo.init_bare(self.path)
225 return Repo.init_bare(self.path)
214 else:
226 else:
215 return Repo.init(self.path)
227 return Repo.init(self.path)
216 else:
228 else:
217 return self._repo
229 return self._repo
218 except (NotGitRepository, OSError), err:
230 except (NotGitRepository, OSError), err:
219 raise RepositoryError(err)
231 raise RepositoryError(err)
220
232
221 def _get_all_revisions(self):
233 def _get_all_revisions(self):
222 # we must check if this repo is not empty, since later command
234 # we must check if this repo is not empty, since later command
223 # fails if it is. And it's cheaper to ask than throw the subprocess
235 # fails if it is. And it's cheaper to ask than throw the subprocess
224 # errors
236 # errors
225 try:
237 try:
226 self._repo.head()
238 self._repo.head()
227 except KeyError:
239 except KeyError:
228 return []
240 return []
229
241
230 rev_filter = _git_path = settings.GIT_REV_FILTER
242 rev_filter = _git_path = settings.GIT_REV_FILTER
231 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
243 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
232 try:
244 try:
233 so, se = self.run_git_command(cmd)
245 so, se = self.run_git_command(cmd)
234 except RepositoryError:
246 except RepositoryError:
235 # Can be raised for empty repositories
247 # Can be raised for empty repositories
236 return []
248 return []
237 return so.splitlines()
249 return so.splitlines()
238
250
239 def _get_all_revisions2(self):
251 def _get_all_revisions2(self):
240 #alternate implementation using dulwich
252 #alternate implementation using dulwich
241 includes = [x[1][0] for x in self._parsed_refs.iteritems()
253 includes = [x[1][0] for x in self._parsed_refs.iteritems()
242 if x[1][1] != 'T']
254 if x[1][1] != 'T']
243 return [c.commit.id for c in self._repo.get_walker(include=includes)]
255 return [c.commit.id for c in self._repo.get_walker(include=includes)]
244
256
245 def _get_revision(self, revision):
257 def _get_revision(self, revision):
246 """
258 """
247 For git backend we always return integer here. This way we ensure
259 For git backend we always return integer here. This way we ensure
248 that changset's revision attribute would become integer.
260 that changset's revision attribute would become integer.
249 """
261 """
250
262
251 is_null = lambda o: len(o) == revision.count('0')
263 is_null = lambda o: len(o) == revision.count('0')
252
264
253 try:
265 if self._empty:
254 self.revisions[0]
255 except (KeyError, IndexError):
256 raise EmptyRepositoryError("There are no changesets yet")
266 raise EmptyRepositoryError("There are no changesets yet")
257
267
258 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
268 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
259 return self.revisions[-1]
269 return self.revisions[-1]
260
270
261 is_bstr = isinstance(revision, (str, unicode))
271 is_bstr = isinstance(revision, (str, unicode))
262 if ((is_bstr and revision.isdigit() and len(revision) < 12)
272 if ((is_bstr and revision.isdigit() and len(revision) < 12)
263 or isinstance(revision, int) or is_null(revision)):
273 or isinstance(revision, int) or is_null(revision)):
264 try:
274 try:
265 revision = self.revisions[int(revision)]
275 revision = self.revisions[int(revision)]
266 except Exception:
276 except Exception:
267 raise ChangesetDoesNotExistError("Revision %s does not exist "
277 raise ChangesetDoesNotExistError("Revision %s does not exist "
268 "for this repository" % (revision))
278 "for this repository" % (revision))
269
279
270 elif is_bstr:
280 elif is_bstr:
271 # get by branch/tag name
281 # get by branch/tag name
272 _ref_revision = self._parsed_refs.get(revision)
282 _ref_revision = self._parsed_refs.get(revision)
273 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
283 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
274 return _ref_revision[0]
284 return _ref_revision[0]
275
285
276 _tags_shas = self.tags.values()
286 _tags_shas = self.tags.values()
277 # maybe it's a tag ? we don't have them in self.revisions
287 # maybe it's a tag ? we don't have them in self.revisions
278 if revision in _tags_shas:
288 if revision in _tags_shas:
279 return _tags_shas[_tags_shas.index(revision)]
289 return _tags_shas[_tags_shas.index(revision)]
280
290
281 elif not SHA_PATTERN.match(revision) or revision not in self.revisions:
291 elif not SHA_PATTERN.match(revision) or revision not in self.revisions:
282 raise ChangesetDoesNotExistError("Revision %s does not exist "
292 raise ChangesetDoesNotExistError("Revision %s does not exist "
283 "for this repository" % (revision))
293 "for this repository" % (revision))
284
294
285 # Ensure we return full id
295 # Ensure we return full id
286 if not SHA_PATTERN.match(str(revision)):
296 if not SHA_PATTERN.match(str(revision)):
287 raise ChangesetDoesNotExistError("Given revision %s not recognized"
297 raise ChangesetDoesNotExistError("Given revision %s not recognized"
288 % revision)
298 % revision)
289 return revision
299 return revision
290
300
291 def _get_archives(self, archive_name='tip'):
301 def _get_archives(self, archive_name='tip'):
292
302
293 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
303 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
294 yield {"type": i[0], "extension": i[1], "node": archive_name}
304 yield {"type": i[0], "extension": i[1], "node": archive_name}
295
305
296 def _get_url(self, url):
306 def _get_url(self, url):
297 """
307 """
298 Returns normalized url. If schema is not given, would fall to
308 Returns normalized url. If schema is not given, would fall to
299 filesystem (``file:///``) schema.
309 filesystem (``file:///``) schema.
300 """
310 """
301 url = str(url)
311 url = str(url)
302 if url != 'default' and not '://' in url:
312 if url != 'default' and not '://' in url:
303 url = ':///'.join(('file', url))
313 url = ':///'.join(('file', url))
304 return url
314 return url
305
315
306 def get_hook_location(self):
316 def get_hook_location(self):
307 """
317 """
308 returns absolute path to location where hooks are stored
318 returns absolute path to location where hooks are stored
309 """
319 """
310 loc = os.path.join(self.path, 'hooks')
320 loc = os.path.join(self.path, 'hooks')
311 if not self.bare:
321 if not self.bare:
312 loc = os.path.join(self.path, '.git', 'hooks')
322 loc = os.path.join(self.path, '.git', 'hooks')
313 return loc
323 return loc
314
324
315 @LazyProperty
325 @LazyProperty
316 def name(self):
326 def name(self):
317 return os.path.basename(self.path)
327 return os.path.basename(self.path)
318
328
319 @LazyProperty
329 @LazyProperty
320 def last_change(self):
330 def last_change(self):
321 """
331 """
322 Returns last change made on this repository as datetime object
332 Returns last change made on this repository as datetime object
323 """
333 """
324 return date_fromtimestamp(self._get_mtime(), makedate()[1])
334 return date_fromtimestamp(self._get_mtime(), makedate()[1])
325
335
326 def _get_mtime(self):
336 def _get_mtime(self):
327 try:
337 try:
328 return time.mktime(self.get_changeset().date.timetuple())
338 return time.mktime(self.get_changeset().date.timetuple())
329 except RepositoryError:
339 except RepositoryError:
330 idx_loc = '' if self.bare else '.git'
340 idx_loc = '' if self.bare else '.git'
331 # fallback to filesystem
341 # fallback to filesystem
332 in_path = os.path.join(self.path, idx_loc, "index")
342 in_path = os.path.join(self.path, idx_loc, "index")
333 he_path = os.path.join(self.path, idx_loc, "HEAD")
343 he_path = os.path.join(self.path, idx_loc, "HEAD")
334 if os.path.exists(in_path):
344 if os.path.exists(in_path):
335 return os.stat(in_path).st_mtime
345 return os.stat(in_path).st_mtime
336 else:
346 else:
337 return os.stat(he_path).st_mtime
347 return os.stat(he_path).st_mtime
338
348
339 @LazyProperty
349 @LazyProperty
340 def description(self):
350 def description(self):
341 idx_loc = '' if self.bare else '.git'
351 idx_loc = '' if self.bare else '.git'
342 undefined_description = u'unknown'
352 undefined_description = u'unknown'
343 description_path = os.path.join(self.path, idx_loc, 'description')
353 description_path = os.path.join(self.path, idx_loc, 'description')
344 if os.path.isfile(description_path):
354 if os.path.isfile(description_path):
345 return safe_unicode(open(description_path).read())
355 return safe_unicode(open(description_path).read())
346 else:
356 else:
347 return undefined_description
357 return undefined_description
348
358
349 @LazyProperty
359 @LazyProperty
350 def contact(self):
360 def contact(self):
351 undefined_contact = u'Unknown'
361 undefined_contact = u'Unknown'
352 return undefined_contact
362 return undefined_contact
353
363
354 @property
364 @property
355 def branches(self):
365 def branches(self):
356 if not self.revisions:
366 if not self.revisions:
357 return {}
367 return {}
358 sortkey = lambda ctx: ctx[0]
368 sortkey = lambda ctx: ctx[0]
359 _branches = [(x[0], x[1][0])
369 _branches = [(x[0], x[1][0])
360 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
370 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
361 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
371 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
362
372
363 @LazyProperty
373 @LazyProperty
364 def tags(self):
374 def tags(self):
365 return self._get_tags()
375 return self._get_tags()
366
376
367 def _get_tags(self):
377 def _get_tags(self):
368 if not self.revisions:
378 if not self.revisions:
369 return {}
379 return {}
370
380
371 sortkey = lambda ctx: ctx[0]
381 sortkey = lambda ctx: ctx[0]
372 _tags = [(x[0], x[1][0])
382 _tags = [(x[0], x[1][0])
373 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
383 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
374 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
384 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
375
385
376 def tag(self, name, user, revision=None, message=None, date=None,
386 def tag(self, name, user, revision=None, message=None, date=None,
377 **kwargs):
387 **kwargs):
378 """
388 """
379 Creates and returns a tag for the given ``revision``.
389 Creates and returns a tag for the given ``revision``.
380
390
381 :param name: name for new tag
391 :param name: name for new tag
382 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
392 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
383 :param revision: changeset id for which new tag would be created
393 :param revision: changeset id for which new tag would be created
384 :param message: message of the tag's commit
394 :param message: message of the tag's commit
385 :param date: date of tag's commit
395 :param date: date of tag's commit
386
396
387 :raises TagAlreadyExistError: if tag with same name already exists
397 :raises TagAlreadyExistError: if tag with same name already exists
388 """
398 """
389 if name in self.tags:
399 if name in self.tags:
390 raise TagAlreadyExistError("Tag %s already exists" % name)
400 raise TagAlreadyExistError("Tag %s already exists" % name)
391 changeset = self.get_changeset(revision)
401 changeset = self.get_changeset(revision)
392 message = message or "Added tag %s for commit %s" % (name,
402 message = message or "Added tag %s for commit %s" % (name,
393 changeset.raw_id)
403 changeset.raw_id)
394 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
404 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
395
405
396 self._parsed_refs = self._get_parsed_refs()
406 self._parsed_refs = self._get_parsed_refs()
397 self.tags = self._get_tags()
407 self.tags = self._get_tags()
398 return changeset
408 return changeset
399
409
400 def remove_tag(self, name, user, message=None, date=None):
410 def remove_tag(self, name, user, message=None, date=None):
401 """
411 """
402 Removes tag with the given ``name``.
412 Removes tag with the given ``name``.
403
413
404 :param name: name of the tag to be removed
414 :param name: name of the tag to be removed
405 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
415 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
406 :param message: message of the tag's removal commit
416 :param message: message of the tag's removal commit
407 :param date: date of tag's removal commit
417 :param date: date of tag's removal commit
408
418
409 :raises TagDoesNotExistError: if tag with given name does not exists
419 :raises TagDoesNotExistError: if tag with given name does not exists
410 """
420 """
411 if name not in self.tags:
421 if name not in self.tags:
412 raise TagDoesNotExistError("Tag %s does not exist" % name)
422 raise TagDoesNotExistError("Tag %s does not exist" % name)
413 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
423 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
414 try:
424 try:
415 os.remove(tagpath)
425 os.remove(tagpath)
416 self._parsed_refs = self._get_parsed_refs()
426 self._parsed_refs = self._get_parsed_refs()
417 self.tags = self._get_tags()
427 self.tags = self._get_tags()
418 except OSError, e:
428 except OSError, e:
419 raise RepositoryError(e.strerror)
429 raise RepositoryError(e.strerror)
420
430
421 @LazyProperty
431 @LazyProperty
422 def _parsed_refs(self):
432 def _parsed_refs(self):
423 return self._get_parsed_refs()
433 return self._get_parsed_refs()
424
434
425 def _get_parsed_refs(self):
435 def _get_parsed_refs(self):
426 # cache the property
436 # cache the property
427 _repo = self._repo
437 _repo = self._repo
428 refs = _repo.get_refs()
438 refs = _repo.get_refs()
429 keys = [('refs/heads/', 'H'),
439 keys = [('refs/heads/', 'H'),
430 ('refs/remotes/origin/', 'RH'),
440 ('refs/remotes/origin/', 'RH'),
431 ('refs/tags/', 'T')]
441 ('refs/tags/', 'T')]
432 _refs = {}
442 _refs = {}
433 for ref, sha in refs.iteritems():
443 for ref, sha in refs.iteritems():
434 for k, type_ in keys:
444 for k, type_ in keys:
435 if ref.startswith(k):
445 if ref.startswith(k):
436 _key = ref[len(k):]
446 _key = ref[len(k):]
437 if type_ == 'T':
447 if type_ == 'T':
438 obj = _repo.get_object(sha)
448 obj = _repo.get_object(sha)
439 if isinstance(obj, Tag):
449 if isinstance(obj, Tag):
440 sha = _repo.get_object(sha).object[1]
450 sha = _repo.get_object(sha).object[1]
441 _refs[_key] = [sha, type_]
451 _refs[_key] = [sha, type_]
442 break
452 break
443 return _refs
453 return _refs
444
454
445 def _heads(self, reverse=False):
455 def _heads(self, reverse=False):
446 refs = self._repo.get_refs()
456 refs = self._repo.get_refs()
447 heads = {}
457 heads = {}
448
458
449 for key, val in refs.items():
459 for key, val in refs.items():
450 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
460 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
451 if key.startswith(ref_key):
461 if key.startswith(ref_key):
452 n = key[len(ref_key):]
462 n = key[len(ref_key):]
453 if n not in ['HEAD']:
463 if n not in ['HEAD']:
454 heads[n] = val
464 heads[n] = val
455
465
456 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
466 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
457
467
458 def get_changeset(self, revision=None):
468 def get_changeset(self, revision=None):
459 """
469 """
460 Returns ``GitChangeset`` object representing commit from git repository
470 Returns ``GitChangeset`` object representing commit from git repository
461 at the given revision or head (most recent commit) if None given.
471 at the given revision or head (most recent commit) if None given.
462 """
472 """
463 if isinstance(revision, GitChangeset):
473 if isinstance(revision, GitChangeset):
464 return revision
474 return revision
465 revision = self._get_revision(revision)
475 revision = self._get_revision(revision)
466 changeset = GitChangeset(repository=self, revision=revision)
476 changeset = GitChangeset(repository=self, revision=revision)
467 return changeset
477 return changeset
468
478
469 def get_changesets(self, start=None, end=None, start_date=None,
479 def get_changesets(self, start=None, end=None, start_date=None,
470 end_date=None, branch_name=None, reverse=False):
480 end_date=None, branch_name=None, reverse=False):
471 """
481 """
472 Returns iterator of ``GitChangeset`` objects from start to end (both
482 Returns iterator of ``GitChangeset`` objects from start to end (both
473 are inclusive), in ascending date order (unless ``reverse`` is set).
483 are inclusive), in ascending date order (unless ``reverse`` is set).
474
484
475 :param start: changeset ID, as str; first returned changeset
485 :param start: changeset ID, as str; first returned changeset
476 :param end: changeset ID, as str; last returned changeset
486 :param end: changeset ID, as str; last returned changeset
477 :param start_date: if specified, changesets with commit date less than
487 :param start_date: if specified, changesets with commit date less than
478 ``start_date`` would be filtered out from returned set
488 ``start_date`` would be filtered out from returned set
479 :param end_date: if specified, changesets with commit date greater than
489 :param end_date: if specified, changesets with commit date greater than
480 ``end_date`` would be filtered out from returned set
490 ``end_date`` would be filtered out from returned set
481 :param branch_name: if specified, changesets not reachable from given
491 :param branch_name: if specified, changesets not reachable from given
482 branch would be filtered out from returned set
492 branch would be filtered out from returned set
483 :param reverse: if ``True``, returned generator would be reversed
493 :param reverse: if ``True``, returned generator would be reversed
484 (meaning that returned changesets would have descending date order)
494 (meaning that returned changesets would have descending date order)
485
495
486 :raise BranchDoesNotExistError: If given ``branch_name`` does not
496 :raise BranchDoesNotExistError: If given ``branch_name`` does not
487 exist.
497 exist.
488 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
498 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
489 ``end`` could not be found.
499 ``end`` could not be found.
490
500
491 """
501 """
492 if branch_name and branch_name not in self.branches:
502 if branch_name and branch_name not in self.branches:
493 raise BranchDoesNotExistError("Branch '%s' not found" \
503 raise BranchDoesNotExistError("Branch '%s' not found" \
494 % branch_name)
504 % branch_name)
505 # actually we should check now if it's not an empty repo to not spaw
506 # subprocess commands
507 if self._empty:
508 raise EmptyRepositoryError("There are no changesets yet")
509
495 # %H at format means (full) commit hash, initial hashes are retrieved
510 # %H at format means (full) commit hash, initial hashes are retrieved
496 # in ascending date order
511 # in ascending date order
497 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
512 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
498 cmd_params = {}
513 cmd_params = {}
499 if start_date:
514 if start_date:
500 cmd_template += ' --since "$since"'
515 cmd_template += ' --since "$since"'
501 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
516 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
502 if end_date:
517 if end_date:
503 cmd_template += ' --until "$until"'
518 cmd_template += ' --until "$until"'
504 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
519 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
505 if branch_name:
520 if branch_name:
506 cmd_template += ' $branch_name'
521 cmd_template += ' $branch_name'
507 cmd_params['branch_name'] = branch_name
522 cmd_params['branch_name'] = branch_name
508 else:
523 else:
509 rev_filter = _git_path = settings.GIT_REV_FILTER
524 rev_filter = _git_path = settings.GIT_REV_FILTER
510 cmd_template += ' %s' % (rev_filter)
525 cmd_template += ' %s' % (rev_filter)
511
526
512 cmd = string.Template(cmd_template).safe_substitute(**cmd_params)
527 cmd = string.Template(cmd_template).safe_substitute(**cmd_params)
513 revs = self.run_git_command(cmd)[0].splitlines()
528 revs = self.run_git_command(cmd)[0].splitlines()
514 start_pos = 0
529 start_pos = 0
515 end_pos = len(revs)
530 end_pos = len(revs)
516 if start:
531 if start:
517 _start = self._get_revision(start)
532 _start = self._get_revision(start)
518 try:
533 try:
519 start_pos = revs.index(_start)
534 start_pos = revs.index(_start)
520 except ValueError:
535 except ValueError:
521 pass
536 pass
522
537
523 if end is not None:
538 if end is not None:
524 _end = self._get_revision(end)
539 _end = self._get_revision(end)
525 try:
540 try:
526 end_pos = revs.index(_end)
541 end_pos = revs.index(_end)
527 except ValueError:
542 except ValueError:
528 pass
543 pass
529
544
530 if None not in [start, end] and start_pos > end_pos:
545 if None not in [start, end] and start_pos > end_pos:
531 raise RepositoryError('start cannot be after end')
546 raise RepositoryError('start cannot be after end')
532
547
533 if end_pos is not None:
548 if end_pos is not None:
534 end_pos += 1
549 end_pos += 1
535
550
536 revs = revs[start_pos:end_pos]
551 revs = revs[start_pos:end_pos]
537 if reverse:
552 if reverse:
538 revs = reversed(revs)
553 revs = reversed(revs)
539 return CollectionGenerator(self, revs)
554 return CollectionGenerator(self, revs)
540
555
541 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
556 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
542 context=3):
557 context=3):
543 """
558 """
544 Returns (git like) *diff*, as plain text. Shows changes introduced by
559 Returns (git like) *diff*, as plain text. Shows changes introduced by
545 ``rev2`` since ``rev1``.
560 ``rev2`` since ``rev1``.
546
561
547 :param rev1: Entry point from which diff is shown. Can be
562 :param rev1: Entry point from which diff is shown. Can be
548 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
563 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
549 the changes since empty state of the repository until ``rev2``
564 the changes since empty state of the repository until ``rev2``
550 :param rev2: Until which revision changes should be shown.
565 :param rev2: Until which revision changes should be shown.
551 :param ignore_whitespace: If set to ``True``, would not show whitespace
566 :param ignore_whitespace: If set to ``True``, would not show whitespace
552 changes. Defaults to ``False``.
567 changes. Defaults to ``False``.
553 :param context: How many lines before/after changed lines should be
568 :param context: How many lines before/after changed lines should be
554 shown. Defaults to ``3``.
569 shown. Defaults to ``3``.
555 """
570 """
556 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
571 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
557 if ignore_whitespace:
572 if ignore_whitespace:
558 flags.append('-w')
573 flags.append('-w')
559
574
560 if hasattr(rev1, 'raw_id'):
575 if hasattr(rev1, 'raw_id'):
561 rev1 = getattr(rev1, 'raw_id')
576 rev1 = getattr(rev1, 'raw_id')
562
577
563 if hasattr(rev2, 'raw_id'):
578 if hasattr(rev2, 'raw_id'):
564 rev2 = getattr(rev2, 'raw_id')
579 rev2 = getattr(rev2, 'raw_id')
565
580
566 if rev1 == self.EMPTY_CHANGESET:
581 if rev1 == self.EMPTY_CHANGESET:
567 rev2 = self.get_changeset(rev2).raw_id
582 rev2 = self.get_changeset(rev2).raw_id
568 cmd = ' '.join(['show'] + flags + [rev2])
583 cmd = ' '.join(['show'] + flags + [rev2])
569 else:
584 else:
570 rev1 = self.get_changeset(rev1).raw_id
585 rev1 = self.get_changeset(rev1).raw_id
571 rev2 = self.get_changeset(rev2).raw_id
586 rev2 = self.get_changeset(rev2).raw_id
572 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
587 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
573
588
574 if path:
589 if path:
575 cmd += ' -- "%s"' % path
590 cmd += ' -- "%s"' % path
576
591
577 stdout, stderr = self.run_git_command(cmd)
592 stdout, stderr = self.run_git_command(cmd)
578 # If we used 'show' command, strip first few lines (until actual diff
593 # If we used 'show' command, strip first few lines (until actual diff
579 # starts)
594 # starts)
580 if rev1 == self.EMPTY_CHANGESET:
595 if rev1 == self.EMPTY_CHANGESET:
581 lines = stdout.splitlines()
596 lines = stdout.splitlines()
582 x = 0
597 x = 0
583 for line in lines:
598 for line in lines:
584 if line.startswith('diff'):
599 if line.startswith('diff'):
585 break
600 break
586 x += 1
601 x += 1
587 # Append new line just like 'diff' command do
602 # Append new line just like 'diff' command do
588 stdout = '\n'.join(lines[x:]) + '\n'
603 stdout = '\n'.join(lines[x:]) + '\n'
589 return stdout
604 return stdout
590
605
591 @LazyProperty
606 @LazyProperty
592 def in_memory_changeset(self):
607 def in_memory_changeset(self):
593 """
608 """
594 Returns ``GitInMemoryChangeset`` object for this repository.
609 Returns ``GitInMemoryChangeset`` object for this repository.
595 """
610 """
596 return GitInMemoryChangeset(self)
611 return GitInMemoryChangeset(self)
597
612
598 def clone(self, url, update_after_clone=True, bare=False):
613 def clone(self, url, update_after_clone=True, bare=False):
599 """
614 """
600 Tries to clone changes from external location.
615 Tries to clone changes from external location.
601
616
602 :param update_after_clone: If set to ``False``, git won't checkout
617 :param update_after_clone: If set to ``False``, git won't checkout
603 working directory
618 working directory
604 :param bare: If set to ``True``, repository would be cloned into
619 :param bare: If set to ``True``, repository would be cloned into
605 *bare* git repository (no working directory at all).
620 *bare* git repository (no working directory at all).
606 """
621 """
607 url = self._get_url(url)
622 url = self._get_url(url)
608 cmd = ['clone']
623 cmd = ['clone']
609 if bare:
624 if bare:
610 cmd.append('--bare')
625 cmd.append('--bare')
611 elif not update_after_clone:
626 elif not update_after_clone:
612 cmd.append('--no-checkout')
627 cmd.append('--no-checkout')
613 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
628 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
614 cmd = ' '.join(cmd)
629 cmd = ' '.join(cmd)
615 # If error occurs run_git_command raises RepositoryError already
630 # If error occurs run_git_command raises RepositoryError already
616 self.run_git_command(cmd)
631 self.run_git_command(cmd)
617
632
618 def pull(self, url):
633 def pull(self, url):
619 """
634 """
620 Tries to pull changes from external location.
635 Tries to pull changes from external location.
621 """
636 """
622 url = self._get_url(url)
637 url = self._get_url(url)
623 cmd = ['pull']
638 cmd = ['pull']
624 cmd.append("--ff-only")
639 cmd.append("--ff-only")
625 cmd.append(url)
640 cmd.append(url)
626 cmd = ' '.join(cmd)
641 cmd = ' '.join(cmd)
627 # If error occurs run_git_command raises RepositoryError already
642 # If error occurs run_git_command raises RepositoryError already
628 self.run_git_command(cmd)
643 self.run_git_command(cmd)
629
644
630 def fetch(self, url):
645 def fetch(self, url):
631 """
646 """
632 Tries to pull changes from external location.
647 Tries to pull changes from external location.
633 """
648 """
634 url = self._get_url(url)
649 url = self._get_url(url)
635 so, se = self.run_git_command('ls-remote -h %s' % url)
650 so, se = self.run_git_command('ls-remote -h %s' % url)
636 refs = []
651 refs = []
637 for line in (x for x in so.splitlines()):
652 for line in (x for x in so.splitlines()):
638 sha, ref = line.split('\t')
653 sha, ref = line.split('\t')
639 refs.append(ref)
654 refs.append(ref)
640 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
655 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
641 cmd = '''fetch %s -- %s''' % (url, refs)
656 cmd = '''fetch %s -- %s''' % (url, refs)
642 self.run_git_command(cmd)
657 self.run_git_command(cmd)
643
658
644 @LazyProperty
659 @LazyProperty
645 def workdir(self):
660 def workdir(self):
646 """
661 """
647 Returns ``Workdir`` instance for this repository.
662 Returns ``Workdir`` instance for this repository.
648 """
663 """
649 return GitWorkdir(self)
664 return GitWorkdir(self)
650
665
651 def get_config_value(self, section, name, config_file=None):
666 def get_config_value(self, section, name, config_file=None):
652 """
667 """
653 Returns configuration value for a given [``section``] and ``name``.
668 Returns configuration value for a given [``section``] and ``name``.
654
669
655 :param section: Section we want to retrieve value from
670 :param section: Section we want to retrieve value from
656 :param name: Name of configuration we want to retrieve
671 :param name: Name of configuration we want to retrieve
657 :param config_file: A path to file which should be used to retrieve
672 :param config_file: A path to file which should be used to retrieve
658 configuration from (might also be a list of file paths)
673 configuration from (might also be a list of file paths)
659 """
674 """
660 if config_file is None:
675 if config_file is None:
661 config_file = []
676 config_file = []
662 elif isinstance(config_file, basestring):
677 elif isinstance(config_file, basestring):
663 config_file = [config_file]
678 config_file = [config_file]
664
679
665 def gen_configs():
680 def gen_configs():
666 for path in config_file + self._config_files:
681 for path in config_file + self._config_files:
667 try:
682 try:
668 yield ConfigFile.from_path(path)
683 yield ConfigFile.from_path(path)
669 except (IOError, OSError, ValueError):
684 except (IOError, OSError, ValueError):
670 continue
685 continue
671
686
672 for config in gen_configs():
687 for config in gen_configs():
673 try:
688 try:
674 return config.get(section, name)
689 return config.get(section, name)
675 except KeyError:
690 except KeyError:
676 continue
691 continue
677 return None
692 return None
678
693
679 def get_user_name(self, config_file=None):
694 def get_user_name(self, config_file=None):
680 """
695 """
681 Returns user's name from global configuration file.
696 Returns user's name from global configuration file.
682
697
683 :param config_file: A path to file which should be used to retrieve
698 :param config_file: A path to file which should be used to retrieve
684 configuration from (might also be a list of file paths)
699 configuration from (might also be a list of file paths)
685 """
700 """
686 return self.get_config_value('user', 'name', config_file)
701 return self.get_config_value('user', 'name', config_file)
687
702
688 def get_user_email(self, config_file=None):
703 def get_user_email(self, config_file=None):
689 """
704 """
690 Returns user's email from global configuration file.
705 Returns user's email from global configuration file.
691
706
692 :param config_file: A path to file which should be used to retrieve
707 :param config_file: A path to file which should be used to retrieve
693 configuration from (might also be a list of file paths)
708 configuration from (might also be a list of file paths)
694 """
709 """
695 return self.get_config_value('user', 'email', config_file)
710 return self.get_config_value('user', 'email', config_file)
@@ -1,571 +1,571 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.hg.repository
3 vcs.backends.hg.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Mercurial repository implementation.
6 Mercurial repository implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import os
12 import os
13 import time
13 import time
14 import urllib
14 import urllib
15 import urllib2
15 import urllib2
16 import logging
16 import logging
17 import datetime
17 import datetime
18
18
19
19
20 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
20 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
21 from rhodecode.lib.vcs.conf import settings
21 from rhodecode.lib.vcs.conf import settings
22
22
23 from rhodecode.lib.vcs.exceptions import (
23 from rhodecode.lib.vcs.exceptions import (
24 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
24 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
25 RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
25 RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
26 )
26 )
27 from rhodecode.lib.vcs.utils import (
27 from rhodecode.lib.vcs.utils import (
28 author_email, author_name, date_fromtimestamp, makedate, safe_unicode
28 author_email, author_name, date_fromtimestamp, makedate, safe_unicode
29 )
29 )
30 from rhodecode.lib.vcs.utils.lazy import LazyProperty
30 from rhodecode.lib.vcs.utils.lazy import LazyProperty
31 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
31 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
32 from rhodecode.lib.vcs.utils.paths import abspath
32 from rhodecode.lib.vcs.utils.paths import abspath
33 from rhodecode.lib.vcs.utils.hgcompat import (
33 from rhodecode.lib.vcs.utils.hgcompat import (
34 ui, nullid, match, patch, diffopts, clone, get_contact, pull,
34 ui, nullid, match, patch, diffopts, clone, get_contact, pull,
35 localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
35 localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
36 httpbasicauthhandler, httpdigestauthhandler
36 httpbasicauthhandler, httpdigestauthhandler
37 )
37 )
38
38
39 from .changeset import MercurialChangeset
39 from .changeset import MercurialChangeset
40 from .inmemory import MercurialInMemoryChangeset
40 from .inmemory import MercurialInMemoryChangeset
41 from .workdir import MercurialWorkdir
41 from .workdir import MercurialWorkdir
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class MercurialRepository(BaseRepository):
46 class MercurialRepository(BaseRepository):
47 """
47 """
48 Mercurial repository backend
48 Mercurial repository backend
49 """
49 """
50 DEFAULT_BRANCH_NAME = 'default'
50 DEFAULT_BRANCH_NAME = 'default'
51 scm = 'hg'
51 scm = 'hg'
52
52
53 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
53 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
54 update_after_clone=False):
54 update_after_clone=False):
55 """
55 """
56 Raises RepositoryError if repository could not be find at the given
56 Raises RepositoryError if repository could not be find at the given
57 ``repo_path``.
57 ``repo_path``.
58
58
59 :param repo_path: local path of the repository
59 :param repo_path: local path of the repository
60 :param create=False: if set to True, would try to create repository if
60 :param create=False: if set to True, would try to create repository if
61 it does not exist rather than raising exception
61 it does not exist rather than raising exception
62 :param baseui=None: user data
62 :param baseui=None: user data
63 :param src_url=None: would try to clone repository from given location
63 :param src_url=None: would try to clone repository from given location
64 :param update_after_clone=False: sets update of working copy after
64 :param update_after_clone=False: sets update of working copy after
65 making a clone
65 making a clone
66 """
66 """
67
67
68 if not isinstance(repo_path, str):
68 if not isinstance(repo_path, str):
69 raise VCSError('Mercurial backend requires repository path to '
69 raise VCSError('Mercurial backend requires repository path to '
70 'be instance of <str> got %s instead' %
70 'be instance of <str> got %s instead' %
71 type(repo_path))
71 type(repo_path))
72
72
73 self.path = abspath(repo_path)
73 self.path = abspath(repo_path)
74 self.baseui = baseui or ui.ui()
74 self.baseui = baseui or ui.ui()
75 # We've set path and ui, now we can set _repo itself
75 # We've set path and ui, now we can set _repo itself
76 self._repo = self._get_repo(create, src_url, update_after_clone)
76 self._repo = self._get_repo(create, src_url, update_after_clone)
77
77
78 @property
78 @property
79 def _empty(self):
79 def _empty(self):
80 """
80 """
81 Checks if repository is empty without any changesets
81 Checks if repository is empty ie. without any changesets
82 """
82 """
83 # TODO: Following raises errors when using InMemoryChangeset...
83 # TODO: Following raises errors when using InMemoryChangeset...
84 # return len(self._repo.changelog) == 0
84 # return len(self._repo.changelog) == 0
85 return len(self.revisions) == 0
85 return len(self.revisions) == 0
86
86
87 @LazyProperty
87 @LazyProperty
88 def revisions(self):
88 def revisions(self):
89 """
89 """
90 Returns list of revisions' ids, in ascending order. Being lazy
90 Returns list of revisions' ids, in ascending order. Being lazy
91 attribute allows external tools to inject shas from cache.
91 attribute allows external tools to inject shas from cache.
92 """
92 """
93 return self._get_all_revisions()
93 return self._get_all_revisions()
94
94
95 @LazyProperty
95 @LazyProperty
96 def name(self):
96 def name(self):
97 return os.path.basename(self.path)
97 return os.path.basename(self.path)
98
98
99 @LazyProperty
99 @LazyProperty
100 def branches(self):
100 def branches(self):
101 return self._get_branches()
101 return self._get_branches()
102
102
103 @LazyProperty
103 @LazyProperty
104 def allbranches(self):
104 def allbranches(self):
105 """
105 """
106 List all branches, including closed branches.
106 List all branches, including closed branches.
107 """
107 """
108 return self._get_branches(closed=True)
108 return self._get_branches(closed=True)
109
109
110 def _get_branches(self, closed=False):
110 def _get_branches(self, closed=False):
111 """
111 """
112 Get's branches for this repository
112 Get's branches for this repository
113 Returns only not closed branches by default
113 Returns only not closed branches by default
114
114
115 :param closed: return also closed branches for mercurial
115 :param closed: return also closed branches for mercurial
116 """
116 """
117
117
118 if self._empty:
118 if self._empty:
119 return {}
119 return {}
120
120
121 def _branchtags(localrepo):
121 def _branchtags(localrepo):
122 """
122 """
123 Patched version of mercurial branchtags to not return the closed
123 Patched version of mercurial branchtags to not return the closed
124 branches
124 branches
125
125
126 :param localrepo: locarepository instance
126 :param localrepo: locarepository instance
127 """
127 """
128
128
129 bt = {}
129 bt = {}
130 bt_closed = {}
130 bt_closed = {}
131 for bn, heads in localrepo.branchmap().iteritems():
131 for bn, heads in localrepo.branchmap().iteritems():
132 tip = heads[-1]
132 tip = heads[-1]
133 if 'close' in localrepo.changelog.read(tip)[5]:
133 if 'close' in localrepo.changelog.read(tip)[5]:
134 bt_closed[bn] = tip
134 bt_closed[bn] = tip
135 else:
135 else:
136 bt[bn] = tip
136 bt[bn] = tip
137
137
138 if closed:
138 if closed:
139 bt.update(bt_closed)
139 bt.update(bt_closed)
140 return bt
140 return bt
141
141
142 sortkey = lambda ctx: ctx[0] # sort by name
142 sortkey = lambda ctx: ctx[0] # sort by name
143 _branches = [(safe_unicode(n), hex(h),) for n, h in
143 _branches = [(safe_unicode(n), hex(h),) for n, h in
144 _branchtags(self._repo).items()]
144 _branchtags(self._repo).items()]
145
145
146 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
146 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
147
147
148 @LazyProperty
148 @LazyProperty
149 def tags(self):
149 def tags(self):
150 """
150 """
151 Get's tags for this repository
151 Get's tags for this repository
152 """
152 """
153 return self._get_tags()
153 return self._get_tags()
154
154
155 def _get_tags(self):
155 def _get_tags(self):
156 if self._empty:
156 if self._empty:
157 return {}
157 return {}
158
158
159 sortkey = lambda ctx: ctx[0] # sort by name
159 sortkey = lambda ctx: ctx[0] # sort by name
160 _tags = [(safe_unicode(n), hex(h),) for n, h in
160 _tags = [(safe_unicode(n), hex(h),) for n, h in
161 self._repo.tags().items()]
161 self._repo.tags().items()]
162
162
163 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
163 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
164
164
165 def tag(self, name, user, revision=None, message=None, date=None,
165 def tag(self, name, user, revision=None, message=None, date=None,
166 **kwargs):
166 **kwargs):
167 """
167 """
168 Creates and returns a tag for the given ``revision``.
168 Creates and returns a tag for the given ``revision``.
169
169
170 :param name: name for new tag
170 :param name: name for new tag
171 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
171 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
172 :param revision: changeset id for which new tag would be created
172 :param revision: changeset id for which new tag would be created
173 :param message: message of the tag's commit
173 :param message: message of the tag's commit
174 :param date: date of tag's commit
174 :param date: date of tag's commit
175
175
176 :raises TagAlreadyExistError: if tag with same name already exists
176 :raises TagAlreadyExistError: if tag with same name already exists
177 """
177 """
178 if name in self.tags:
178 if name in self.tags:
179 raise TagAlreadyExistError("Tag %s already exists" % name)
179 raise TagAlreadyExistError("Tag %s already exists" % name)
180 changeset = self.get_changeset(revision)
180 changeset = self.get_changeset(revision)
181 local = kwargs.setdefault('local', False)
181 local = kwargs.setdefault('local', False)
182
182
183 if message is None:
183 if message is None:
184 message = "Added tag %s for changeset %s" % (name,
184 message = "Added tag %s for changeset %s" % (name,
185 changeset.short_id)
185 changeset.short_id)
186
186
187 if date is None:
187 if date is None:
188 date = datetime.datetime.now().ctime()
188 date = datetime.datetime.now().ctime()
189
189
190 try:
190 try:
191 self._repo.tag(name, changeset._ctx.node(), message, local, user,
191 self._repo.tag(name, changeset._ctx.node(), message, local, user,
192 date)
192 date)
193 except Abort, e:
193 except Abort, e:
194 raise RepositoryError(e.message)
194 raise RepositoryError(e.message)
195
195
196 # Reinitialize tags
196 # Reinitialize tags
197 self.tags = self._get_tags()
197 self.tags = self._get_tags()
198 tag_id = self.tags[name]
198 tag_id = self.tags[name]
199
199
200 return self.get_changeset(revision=tag_id)
200 return self.get_changeset(revision=tag_id)
201
201
202 def remove_tag(self, name, user, message=None, date=None):
202 def remove_tag(self, name, user, message=None, date=None):
203 """
203 """
204 Removes tag with the given ``name``.
204 Removes tag with the given ``name``.
205
205
206 :param name: name of the tag to be removed
206 :param name: name of the tag to be removed
207 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
207 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
208 :param message: message of the tag's removal commit
208 :param message: message of the tag's removal commit
209 :param date: date of tag's removal commit
209 :param date: date of tag's removal commit
210
210
211 :raises TagDoesNotExistError: if tag with given name does not exists
211 :raises TagDoesNotExistError: if tag with given name does not exists
212 """
212 """
213 if name not in self.tags:
213 if name not in self.tags:
214 raise TagDoesNotExistError("Tag %s does not exist" % name)
214 raise TagDoesNotExistError("Tag %s does not exist" % name)
215 if message is None:
215 if message is None:
216 message = "Removed tag %s" % name
216 message = "Removed tag %s" % name
217 if date is None:
217 if date is None:
218 date = datetime.datetime.now().ctime()
218 date = datetime.datetime.now().ctime()
219 local = False
219 local = False
220
220
221 try:
221 try:
222 self._repo.tag(name, nullid, message, local, user, date)
222 self._repo.tag(name, nullid, message, local, user, date)
223 self.tags = self._get_tags()
223 self.tags = self._get_tags()
224 except Abort, e:
224 except Abort, e:
225 raise RepositoryError(e.message)
225 raise RepositoryError(e.message)
226
226
227 @LazyProperty
227 @LazyProperty
228 def bookmarks(self):
228 def bookmarks(self):
229 """
229 """
230 Get's bookmarks for this repository
230 Get's bookmarks for this repository
231 """
231 """
232 return self._get_bookmarks()
232 return self._get_bookmarks()
233
233
234 def _get_bookmarks(self):
234 def _get_bookmarks(self):
235 if self._empty:
235 if self._empty:
236 return {}
236 return {}
237
237
238 sortkey = lambda ctx: ctx[0] # sort by name
238 sortkey = lambda ctx: ctx[0] # sort by name
239 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
239 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
240 self._repo._bookmarks.items()]
240 self._repo._bookmarks.items()]
241 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
241 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
242
242
243 def _get_all_revisions(self):
243 def _get_all_revisions(self):
244
244
245 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
245 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
246
246
247 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
247 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
248 context=3):
248 context=3):
249 """
249 """
250 Returns (git like) *diff*, as plain text. Shows changes introduced by
250 Returns (git like) *diff*, as plain text. Shows changes introduced by
251 ``rev2`` since ``rev1``.
251 ``rev2`` since ``rev1``.
252
252
253 :param rev1: Entry point from which diff is shown. Can be
253 :param rev1: Entry point from which diff is shown. Can be
254 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
254 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
255 the changes since empty state of the repository until ``rev2``
255 the changes since empty state of the repository until ``rev2``
256 :param rev2: Until which revision changes should be shown.
256 :param rev2: Until which revision changes should be shown.
257 :param ignore_whitespace: If set to ``True``, would not show whitespace
257 :param ignore_whitespace: If set to ``True``, would not show whitespace
258 changes. Defaults to ``False``.
258 changes. Defaults to ``False``.
259 :param context: How many lines before/after changed lines should be
259 :param context: How many lines before/after changed lines should be
260 shown. Defaults to ``3``.
260 shown. Defaults to ``3``.
261 """
261 """
262 if hasattr(rev1, 'raw_id'):
262 if hasattr(rev1, 'raw_id'):
263 rev1 = getattr(rev1, 'raw_id')
263 rev1 = getattr(rev1, 'raw_id')
264
264
265 if hasattr(rev2, 'raw_id'):
265 if hasattr(rev2, 'raw_id'):
266 rev2 = getattr(rev2, 'raw_id')
266 rev2 = getattr(rev2, 'raw_id')
267
267
268 # Check if given revisions are present at repository (may raise
268 # Check if given revisions are present at repository (may raise
269 # ChangesetDoesNotExistError)
269 # ChangesetDoesNotExistError)
270 if rev1 != self.EMPTY_CHANGESET:
270 if rev1 != self.EMPTY_CHANGESET:
271 self.get_changeset(rev1)
271 self.get_changeset(rev1)
272 self.get_changeset(rev2)
272 self.get_changeset(rev2)
273 if path:
273 if path:
274 file_filter = match(self.path, '', [path])
274 file_filter = match(self.path, '', [path])
275 else:
275 else:
276 file_filter = None
276 file_filter = None
277
277
278 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
278 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
279 opts=diffopts(git=True,
279 opts=diffopts(git=True,
280 ignorews=ignore_whitespace,
280 ignorews=ignore_whitespace,
281 context=context)))
281 context=context)))
282
282
283 @classmethod
283 @classmethod
284 def _check_url(cls, url):
284 def _check_url(cls, url):
285 """
285 """
286 Function will check given url and try to verify if it's a valid
286 Function will check given url and try to verify if it's a valid
287 link. Sometimes it may happened that mercurial will issue basic
287 link. Sometimes it may happened that mercurial will issue basic
288 auth request that can cause whole API to hang when used from python
288 auth request that can cause whole API to hang when used from python
289 or other external calls.
289 or other external calls.
290
290
291 On failures it'll raise urllib2.HTTPError, return code 200 if url
291 On failures it'll raise urllib2.HTTPError, return code 200 if url
292 is valid or True if it's a local path
292 is valid or True if it's a local path
293 """
293 """
294
294
295 # check first if it's not an local url
295 # check first if it's not an local url
296 if os.path.isdir(url) or url.startswith('file:'):
296 if os.path.isdir(url) or url.startswith('file:'):
297 return True
297 return True
298
298
299 if('+' in url[:url.find('://')]):
299 if('+' in url[:url.find('://')]):
300 url = url[url.find('+') + 1:]
300 url = url[url.find('+') + 1:]
301
301
302 handlers = []
302 handlers = []
303 test_uri, authinfo = hg_url(url).authinfo()
303 test_uri, authinfo = hg_url(url).authinfo()
304
304
305 if authinfo:
305 if authinfo:
306 #create a password manager
306 #create a password manager
307 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
307 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
308 passmgr.add_password(*authinfo)
308 passmgr.add_password(*authinfo)
309
309
310 handlers.extend((httpbasicauthhandler(passmgr),
310 handlers.extend((httpbasicauthhandler(passmgr),
311 httpdigestauthhandler(passmgr)))
311 httpdigestauthhandler(passmgr)))
312
312
313 o = urllib2.build_opener(*handlers)
313 o = urllib2.build_opener(*handlers)
314 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
314 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
315 ('Accept', 'application/mercurial-0.1')]
315 ('Accept', 'application/mercurial-0.1')]
316
316
317 q = {"cmd": 'between'}
317 q = {"cmd": 'between'}
318 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
318 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
319 qs = '?%s' % urllib.urlencode(q)
319 qs = '?%s' % urllib.urlencode(q)
320 cu = "%s%s" % (test_uri, qs)
320 cu = "%s%s" % (test_uri, qs)
321 req = urllib2.Request(cu, None, {})
321 req = urllib2.Request(cu, None, {})
322
322
323 try:
323 try:
324 resp = o.open(req)
324 resp = o.open(req)
325 return resp.code == 200
325 return resp.code == 200
326 except Exception, e:
326 except Exception, e:
327 # means it cannot be cloned
327 # means it cannot be cloned
328 raise urllib2.URLError("[%s] %s" % (url, e))
328 raise urllib2.URLError("[%s] %s" % (url, e))
329
329
330 def _get_repo(self, create, src_url=None, update_after_clone=False):
330 def _get_repo(self, create, src_url=None, update_after_clone=False):
331 """
331 """
332 Function will check for mercurial repository in given path and return
332 Function will check for mercurial repository in given path and return
333 a localrepo object. If there is no repository in that path it will
333 a localrepo object. If there is no repository in that path it will
334 raise an exception unless ``create`` parameter is set to True - in
334 raise an exception unless ``create`` parameter is set to True - in
335 that case repository would be created and returned.
335 that case repository would be created and returned.
336 If ``src_url`` is given, would try to clone repository from the
336 If ``src_url`` is given, would try to clone repository from the
337 location at given clone_point. Additionally it'll make update to
337 location at given clone_point. Additionally it'll make update to
338 working copy accordingly to ``update_after_clone`` flag
338 working copy accordingly to ``update_after_clone`` flag
339 """
339 """
340
340
341 try:
341 try:
342 if src_url:
342 if src_url:
343 url = str(self._get_url(src_url))
343 url = str(self._get_url(src_url))
344 opts = {}
344 opts = {}
345 if not update_after_clone:
345 if not update_after_clone:
346 opts.update({'noupdate': True})
346 opts.update({'noupdate': True})
347 try:
347 try:
348 MercurialRepository._check_url(url)
348 MercurialRepository._check_url(url)
349 clone(self.baseui, url, self.path, **opts)
349 clone(self.baseui, url, self.path, **opts)
350 # except urllib2.URLError:
350 # except urllib2.URLError:
351 # raise Abort("Got HTTP 404 error")
351 # raise Abort("Got HTTP 404 error")
352 except Exception:
352 except Exception:
353 raise
353 raise
354
354
355 # Don't try to create if we've already cloned repo
355 # Don't try to create if we've already cloned repo
356 create = False
356 create = False
357 return localrepository(self.baseui, self.path, create=create)
357 return localrepository(self.baseui, self.path, create=create)
358 except (Abort, RepoError), err:
358 except (Abort, RepoError), err:
359 if create:
359 if create:
360 msg = "Cannot create repository at %s. Original error was %s"\
360 msg = "Cannot create repository at %s. Original error was %s"\
361 % (self.path, err)
361 % (self.path, err)
362 else:
362 else:
363 msg = "Not valid repository at %s. Original error was %s"\
363 msg = "Not valid repository at %s. Original error was %s"\
364 % (self.path, err)
364 % (self.path, err)
365 raise RepositoryError(msg)
365 raise RepositoryError(msg)
366
366
367 @LazyProperty
367 @LazyProperty
368 def in_memory_changeset(self):
368 def in_memory_changeset(self):
369 return MercurialInMemoryChangeset(self)
369 return MercurialInMemoryChangeset(self)
370
370
371 @LazyProperty
371 @LazyProperty
372 def description(self):
372 def description(self):
373 undefined_description = u'unknown'
373 undefined_description = u'unknown'
374 return safe_unicode(self._repo.ui.config('web', 'description',
374 return safe_unicode(self._repo.ui.config('web', 'description',
375 undefined_description, untrusted=True))
375 undefined_description, untrusted=True))
376
376
377 @LazyProperty
377 @LazyProperty
378 def contact(self):
378 def contact(self):
379 undefined_contact = u'Unknown'
379 undefined_contact = u'Unknown'
380 return safe_unicode(get_contact(self._repo.ui.config)
380 return safe_unicode(get_contact(self._repo.ui.config)
381 or undefined_contact)
381 or undefined_contact)
382
382
383 @LazyProperty
383 @LazyProperty
384 def last_change(self):
384 def last_change(self):
385 """
385 """
386 Returns last change made on this repository as datetime object
386 Returns last change made on this repository as datetime object
387 """
387 """
388 return date_fromtimestamp(self._get_mtime(), makedate()[1])
388 return date_fromtimestamp(self._get_mtime(), makedate()[1])
389
389
390 def _get_mtime(self):
390 def _get_mtime(self):
391 try:
391 try:
392 return time.mktime(self.get_changeset().date.timetuple())
392 return time.mktime(self.get_changeset().date.timetuple())
393 except RepositoryError:
393 except RepositoryError:
394 #fallback to filesystem
394 #fallback to filesystem
395 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
395 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
396 st_path = os.path.join(self.path, '.hg', "store")
396 st_path = os.path.join(self.path, '.hg', "store")
397 if os.path.exists(cl_path):
397 if os.path.exists(cl_path):
398 return os.stat(cl_path).st_mtime
398 return os.stat(cl_path).st_mtime
399 else:
399 else:
400 return os.stat(st_path).st_mtime
400 return os.stat(st_path).st_mtime
401
401
402 def _get_hidden(self):
402 def _get_hidden(self):
403 return self._repo.ui.configbool("web", "hidden", untrusted=True)
403 return self._repo.ui.configbool("web", "hidden", untrusted=True)
404
404
405 def _get_revision(self, revision):
405 def _get_revision(self, revision):
406 """
406 """
407 Get's an ID revision given as str. This will always return a fill
407 Get's an ID revision given as str. This will always return a fill
408 40 char revision number
408 40 char revision number
409
409
410 :param revision: str or int or None
410 :param revision: str or int or None
411 """
411 """
412
412
413 if self._empty:
413 if self._empty:
414 raise EmptyRepositoryError("There are no changesets yet")
414 raise EmptyRepositoryError("There are no changesets yet")
415
415
416 if revision in [-1, 'tip', None]:
416 if revision in [-1, 'tip', None]:
417 revision = 'tip'
417 revision = 'tip'
418
418
419 try:
419 try:
420 revision = hex(self._repo.lookup(revision))
420 revision = hex(self._repo.lookup(revision))
421 except (IndexError, ValueError, RepoLookupError, TypeError):
421 except (IndexError, ValueError, RepoLookupError, TypeError):
422 raise ChangesetDoesNotExistError("Revision %s does not "
422 raise ChangesetDoesNotExistError("Revision %s does not "
423 "exist for this repository"
423 "exist for this repository"
424 % (revision))
424 % (revision))
425 return revision
425 return revision
426
426
427 def _get_archives(self, archive_name='tip'):
427 def _get_archives(self, archive_name='tip'):
428 allowed = self.baseui.configlist("web", "allow_archive",
428 allowed = self.baseui.configlist("web", "allow_archive",
429 untrusted=True)
429 untrusted=True)
430 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
430 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
431 if i[0] in allowed or self._repo.ui.configbool("web",
431 if i[0] in allowed or self._repo.ui.configbool("web",
432 "allow" + i[0],
432 "allow" + i[0],
433 untrusted=True):
433 untrusted=True):
434 yield {"type": i[0], "extension": i[1], "node": archive_name}
434 yield {"type": i[0], "extension": i[1], "node": archive_name}
435
435
436 def _get_url(self, url):
436 def _get_url(self, url):
437 """
437 """
438 Returns normalized url. If schema is not given, would fall
438 Returns normalized url. If schema is not given, would fall
439 to filesystem
439 to filesystem
440 (``file:///``) schema.
440 (``file:///``) schema.
441 """
441 """
442 url = str(url)
442 url = str(url)
443 if url != 'default' and not '://' in url:
443 if url != 'default' and not '://' in url:
444 url = "file:" + urllib.pathname2url(url)
444 url = "file:" + urllib.pathname2url(url)
445 return url
445 return url
446
446
447 def get_hook_location(self):
447 def get_hook_location(self):
448 """
448 """
449 returns absolute path to location where hooks are stored
449 returns absolute path to location where hooks are stored
450 """
450 """
451 return os.path.join(self.path, '.hg', '.hgrc')
451 return os.path.join(self.path, '.hg', '.hgrc')
452
452
453 def get_changeset(self, revision=None):
453 def get_changeset(self, revision=None):
454 """
454 """
455 Returns ``MercurialChangeset`` object representing repository's
455 Returns ``MercurialChangeset`` object representing repository's
456 changeset at the given ``revision``.
456 changeset at the given ``revision``.
457 """
457 """
458 revision = self._get_revision(revision)
458 revision = self._get_revision(revision)
459 changeset = MercurialChangeset(repository=self, revision=revision)
459 changeset = MercurialChangeset(repository=self, revision=revision)
460 return changeset
460 return changeset
461
461
462 def get_changesets(self, start=None, end=None, start_date=None,
462 def get_changesets(self, start=None, end=None, start_date=None,
463 end_date=None, branch_name=None, reverse=False):
463 end_date=None, branch_name=None, reverse=False):
464 """
464 """
465 Returns iterator of ``MercurialChangeset`` objects from start to end
465 Returns iterator of ``MercurialChangeset`` objects from start to end
466 (both are inclusive)
466 (both are inclusive)
467
467
468 :param start: None, str, int or mercurial lookup format
468 :param start: None, str, int or mercurial lookup format
469 :param end: None, str, int or mercurial lookup format
469 :param end: None, str, int or mercurial lookup format
470 :param start_date:
470 :param start_date:
471 :param end_date:
471 :param end_date:
472 :param branch_name:
472 :param branch_name:
473 :param reversed: return changesets in reversed order
473 :param reversed: return changesets in reversed order
474 """
474 """
475
475
476 start_raw_id = self._get_revision(start)
476 start_raw_id = self._get_revision(start)
477 start_pos = self.revisions.index(start_raw_id) if start else None
477 start_pos = self.revisions.index(start_raw_id) if start else None
478 end_raw_id = self._get_revision(end)
478 end_raw_id = self._get_revision(end)
479 end_pos = self.revisions.index(end_raw_id) if end else None
479 end_pos = self.revisions.index(end_raw_id) if end else None
480
480
481 if None not in [start, end] and start_pos > end_pos:
481 if None not in [start, end] and start_pos > end_pos:
482 raise RepositoryError("Start revision '%s' cannot be "
482 raise RepositoryError("Start revision '%s' cannot be "
483 "after end revision '%s'" % (start, end))
483 "after end revision '%s'" % (start, end))
484
484
485 if branch_name and branch_name not in self.allbranches.keys():
485 if branch_name and branch_name not in self.allbranches.keys():
486 raise BranchDoesNotExistError('Branch %s not found in'
486 raise BranchDoesNotExistError('Branch %s not found in'
487 ' this repository' % branch_name)
487 ' this repository' % branch_name)
488 if end_pos is not None:
488 if end_pos is not None:
489 end_pos += 1
489 end_pos += 1
490 #filter branches
490 #filter branches
491 filter_ = []
491 filter_ = []
492 if branch_name:
492 if branch_name:
493 filter_.append('branch("%s")' % (branch_name))
493 filter_.append('branch("%s")' % (branch_name))
494
494
495 if start_date and not end_date:
495 if start_date and not end_date:
496 filter_.append('date(">%s")' % start_date)
496 filter_.append('date(">%s")' % start_date)
497 if end_date and not start_date:
497 if end_date and not start_date:
498 filter_.append('date("<%s")' % end_date)
498 filter_.append('date("<%s")' % end_date)
499 if start_date and end_date:
499 if start_date and end_date:
500 filter_.append('date(">%s") and date("<%s")' % (start_date, end_date))
500 filter_.append('date(">%s") and date("<%s")' % (start_date, end_date))
501 if filter_:
501 if filter_:
502 revisions = scmutil.revrange(self._repo, filter_)
502 revisions = scmutil.revrange(self._repo, filter_)
503 else:
503 else:
504 revisions = self.revisions
504 revisions = self.revisions
505
505
506 revs = revisions[start_pos:end_pos]
506 revs = revisions[start_pos:end_pos]
507 if reverse:
507 if reverse:
508 revs = reversed(revs)
508 revs = reversed(revs)
509
509
510 return CollectionGenerator(self, revs)
510 return CollectionGenerator(self, revs)
511
511
512 def pull(self, url):
512 def pull(self, url):
513 """
513 """
514 Tries to pull changes from external location.
514 Tries to pull changes from external location.
515 """
515 """
516 url = self._get_url(url)
516 url = self._get_url(url)
517 try:
517 try:
518 pull(self.baseui, self._repo, url)
518 pull(self.baseui, self._repo, url)
519 except Abort, err:
519 except Abort, err:
520 # Propagate error but with vcs's type
520 # Propagate error but with vcs's type
521 raise RepositoryError(str(err))
521 raise RepositoryError(str(err))
522
522
523 @LazyProperty
523 @LazyProperty
524 def workdir(self):
524 def workdir(self):
525 """
525 """
526 Returns ``Workdir`` instance for this repository.
526 Returns ``Workdir`` instance for this repository.
527 """
527 """
528 return MercurialWorkdir(self)
528 return MercurialWorkdir(self)
529
529
530 def get_config_value(self, section, name=None, config_file=None):
530 def get_config_value(self, section, name=None, config_file=None):
531 """
531 """
532 Returns configuration value for a given [``section``] and ``name``.
532 Returns configuration value for a given [``section``] and ``name``.
533
533
534 :param section: Section we want to retrieve value from
534 :param section: Section we want to retrieve value from
535 :param name: Name of configuration we want to retrieve
535 :param name: Name of configuration we want to retrieve
536 :param config_file: A path to file which should be used to retrieve
536 :param config_file: A path to file which should be used to retrieve
537 configuration from (might also be a list of file paths)
537 configuration from (might also be a list of file paths)
538 """
538 """
539 if config_file is None:
539 if config_file is None:
540 config_file = []
540 config_file = []
541 elif isinstance(config_file, basestring):
541 elif isinstance(config_file, basestring):
542 config_file = [config_file]
542 config_file = [config_file]
543
543
544 config = self._repo.ui
544 config = self._repo.ui
545 for path in config_file:
545 for path in config_file:
546 config.readconfig(path)
546 config.readconfig(path)
547 return config.config(section, name)
547 return config.config(section, name)
548
548
549 def get_user_name(self, config_file=None):
549 def get_user_name(self, config_file=None):
550 """
550 """
551 Returns user's name from global configuration file.
551 Returns user's name from global configuration file.
552
552
553 :param config_file: A path to file which should be used to retrieve
553 :param config_file: A path to file which should be used to retrieve
554 configuration from (might also be a list of file paths)
554 configuration from (might also be a list of file paths)
555 """
555 """
556 username = self.get_config_value('ui', 'username')
556 username = self.get_config_value('ui', 'username')
557 if username:
557 if username:
558 return author_name(username)
558 return author_name(username)
559 return None
559 return None
560
560
561 def get_user_email(self, config_file=None):
561 def get_user_email(self, config_file=None):
562 """
562 """
563 Returns user's email from global configuration file.
563 Returns user's email from global configuration file.
564
564
565 :param config_file: A path to file which should be used to retrieve
565 :param config_file: A path to file which should be used to retrieve
566 configuration from (might also be a list of file paths)
566 configuration from (might also be a list of file paths)
567 """
567 """
568 username = self.get_config_value('ui', 'username')
568 username = self.get_config_value('ui', 'username')
569 if username:
569 if username:
570 return author_email(username)
570 return author_email(username)
571 return None
571 return None
@@ -1,360 +1,370 b''
1 from __future__ import with_statement
1 from __future__ import with_statement
2
2
3 import time
3 import datetime
4 import datetime
4 from rhodecode.lib import vcs
5 from rhodecode.lib import vcs
5 from rhodecode.tests.vcs.base import BackendTestMixin
6 from rhodecode.tests.vcs.base import BackendTestMixin
6 from rhodecode.tests.vcs.conf import SCM_TESTS
7 from rhodecode.tests.vcs.conf import SCM_TESTS
7
8
8 from rhodecode.lib.vcs.backends.base import BaseChangeset
9 from rhodecode.lib.vcs.backends.base import BaseChangeset
9 from rhodecode.lib.vcs.nodes import (
10 from rhodecode.lib.vcs.nodes import (
10 FileNode, AddedFileNodesGenerator,
11 FileNode, AddedFileNodesGenerator,
11 ChangedFileNodesGenerator, RemovedFileNodesGenerator
12 ChangedFileNodesGenerator, RemovedFileNodesGenerator
12 )
13 )
13 from rhodecode.lib.vcs.exceptions import (
14 from rhodecode.lib.vcs.exceptions import (
14 BranchDoesNotExistError, ChangesetDoesNotExistError,
15 BranchDoesNotExistError, ChangesetDoesNotExistError,
15 RepositoryError
16 RepositoryError, EmptyRepositoryError
16 )
17 )
17 from rhodecode.lib.vcs.utils.compat import unittest
18 from rhodecode.lib.vcs.utils.compat import unittest
19 from rhodecode.tests.vcs.conf import get_new_dir
18
20
19
21
20 class TestBaseChangeset(unittest.TestCase):
22 class TestBaseChangeset(unittest.TestCase):
21
23
22 def test_as_dict(self):
24 def test_as_dict(self):
23 changeset = BaseChangeset()
25 changeset = BaseChangeset()
24 changeset.id = 'ID'
26 changeset.id = 'ID'
25 changeset.raw_id = 'RAW_ID'
27 changeset.raw_id = 'RAW_ID'
26 changeset.short_id = 'SHORT_ID'
28 changeset.short_id = 'SHORT_ID'
27 changeset.revision = 1009
29 changeset.revision = 1009
28 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
30 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
29 changeset.message = 'Message of a commit'
31 changeset.message = 'Message of a commit'
30 changeset.author = 'Joe Doe <joe.doe@example.com>'
32 changeset.author = 'Joe Doe <joe.doe@example.com>'
31 changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar')]
33 changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar')]
32 changeset.changed = []
34 changeset.changed = []
33 changeset.removed = []
35 changeset.removed = []
34 self.assertEqual(changeset.as_dict(), {
36 self.assertEqual(changeset.as_dict(), {
35 'id': 'ID',
37 'id': 'ID',
36 'raw_id': 'RAW_ID',
38 'raw_id': 'RAW_ID',
37 'short_id': 'SHORT_ID',
39 'short_id': 'SHORT_ID',
38 'revision': 1009,
40 'revision': 1009,
39 'date': datetime.datetime(2011, 1, 30, 1, 45),
41 'date': datetime.datetime(2011, 1, 30, 1, 45),
40 'message': 'Message of a commit',
42 'message': 'Message of a commit',
41 'author': {
43 'author': {
42 'name': 'Joe Doe',
44 'name': 'Joe Doe',
43 'email': 'joe.doe@example.com',
45 'email': 'joe.doe@example.com',
44 },
46 },
45 'added': ['foo/bar/baz', 'foobar'],
47 'added': ['foo/bar/baz', 'foobar'],
46 'changed': [],
48 'changed': [],
47 'removed': [],
49 'removed': [],
48 })
50 })
49
51
50 class ChangesetsWithCommitsTestCaseixin(BackendTestMixin):
52 class ChangesetsWithCommitsTestCaseixin(BackendTestMixin):
51 recreate_repo_per_test = True
53 recreate_repo_per_test = True
52
54
53 @classmethod
55 @classmethod
54 def _get_commits(cls):
56 def _get_commits(cls):
55 start_date = datetime.datetime(2010, 1, 1, 20)
57 start_date = datetime.datetime(2010, 1, 1, 20)
56 for x in xrange(5):
58 for x in xrange(5):
57 yield {
59 yield {
58 'message': 'Commit %d' % x,
60 'message': 'Commit %d' % x,
59 'author': 'Joe Doe <joe.doe@example.com>',
61 'author': 'Joe Doe <joe.doe@example.com>',
60 'date': start_date + datetime.timedelta(hours=12 * x),
62 'date': start_date + datetime.timedelta(hours=12 * x),
61 'added': [
63 'added': [
62 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
64 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
63 ],
65 ],
64 }
66 }
65
67
66 def test_new_branch(self):
68 def test_new_branch(self):
67 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
69 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
68 content='Documentation\n'))
70 content='Documentation\n'))
69 foobar_tip = self.imc.commit(
71 foobar_tip = self.imc.commit(
70 message=u'New branch: foobar',
72 message=u'New branch: foobar',
71 author=u'joe',
73 author=u'joe',
72 branch='foobar',
74 branch='foobar',
73 )
75 )
74 self.assertTrue('foobar' in self.repo.branches)
76 self.assertTrue('foobar' in self.repo.branches)
75 self.assertEqual(foobar_tip.branch, 'foobar')
77 self.assertEqual(foobar_tip.branch, 'foobar')
76 # 'foobar' should be the only branch that contains the new commit
78 # 'foobar' should be the only branch that contains the new commit
77 self.assertNotEqual(*self.repo.branches.values())
79 self.assertNotEqual(*self.repo.branches.values())
78
80
79 def test_new_head_in_default_branch(self):
81 def test_new_head_in_default_branch(self):
80 tip = self.repo.get_changeset()
82 tip = self.repo.get_changeset()
81 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
83 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
82 content='Documentation\n'))
84 content='Documentation\n'))
83 foobar_tip = self.imc.commit(
85 foobar_tip = self.imc.commit(
84 message=u'New branch: foobar',
86 message=u'New branch: foobar',
85 author=u'joe',
87 author=u'joe',
86 branch='foobar',
88 branch='foobar',
87 parents=[tip],
89 parents=[tip],
88 )
90 )
89 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
91 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
90 content='Documentation\nand more...\n'))
92 content='Documentation\nand more...\n'))
91 newtip = self.imc.commit(
93 newtip = self.imc.commit(
92 message=u'At default branch',
94 message=u'At default branch',
93 author=u'joe',
95 author=u'joe',
94 branch=foobar_tip.branch,
96 branch=foobar_tip.branch,
95 parents=[foobar_tip],
97 parents=[foobar_tip],
96 )
98 )
97
99
98 newest_tip = self.imc.commit(
100 newest_tip = self.imc.commit(
99 message=u'Merged with %s' % foobar_tip.raw_id,
101 message=u'Merged with %s' % foobar_tip.raw_id,
100 author=u'joe',
102 author=u'joe',
101 branch=self.backend_class.DEFAULT_BRANCH_NAME,
103 branch=self.backend_class.DEFAULT_BRANCH_NAME,
102 parents=[newtip, foobar_tip],
104 parents=[newtip, foobar_tip],
103 )
105 )
104
106
105 self.assertEqual(newest_tip.branch,
107 self.assertEqual(newest_tip.branch,
106 self.backend_class.DEFAULT_BRANCH_NAME)
108 self.backend_class.DEFAULT_BRANCH_NAME)
107
109
108 def test_get_changesets_respects_branch_name(self):
110 def test_get_changesets_respects_branch_name(self):
109 tip = self.repo.get_changeset()
111 tip = self.repo.get_changeset()
110 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
112 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
111 content='Documentation\n'))
113 content='Documentation\n'))
112 doc_changeset = self.imc.commit(
114 doc_changeset = self.imc.commit(
113 message=u'New branch: docs',
115 message=u'New branch: docs',
114 author=u'joe',
116 author=u'joe',
115 branch='docs',
117 branch='docs',
116 )
118 )
117 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
119 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
118 self.imc.commit(
120 self.imc.commit(
119 message=u'Back in default branch',
121 message=u'Back in default branch',
120 author=u'joe',
122 author=u'joe',
121 parents=[tip],
123 parents=[tip],
122 )
124 )
123 default_branch_changesets = self.repo.get_changesets(
125 default_branch_changesets = self.repo.get_changesets(
124 branch_name=self.repo.DEFAULT_BRANCH_NAME)
126 branch_name=self.repo.DEFAULT_BRANCH_NAME)
125 self.assertNotIn(doc_changeset, default_branch_changesets)
127 self.assertNotIn(doc_changeset, default_branch_changesets)
126
128
127 def test_get_changeset_by_branch(self):
129 def test_get_changeset_by_branch(self):
128 for branch, sha in self.repo.branches.iteritems():
130 for branch, sha in self.repo.branches.iteritems():
129 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
131 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
130
132
131 def test_get_changeset_by_tag(self):
133 def test_get_changeset_by_tag(self):
132 for tag, sha in self.repo.tags.iteritems():
134 for tag, sha in self.repo.tags.iteritems():
133 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
135 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
134
136
135
137
136 class ChangesetsTestCaseMixin(BackendTestMixin):
138 class ChangesetsTestCaseMixin(BackendTestMixin):
137 recreate_repo_per_test = False
139 recreate_repo_per_test = False
138
140
139 @classmethod
141 @classmethod
140 def _get_commits(cls):
142 def _get_commits(cls):
141 start_date = datetime.datetime(2010, 1, 1, 20)
143 start_date = datetime.datetime(2010, 1, 1, 20)
142 for x in xrange(5):
144 for x in xrange(5):
143 yield {
145 yield {
144 'message': u'Commit %d' % x,
146 'message': u'Commit %d' % x,
145 'author': u'Joe Doe <joe.doe@example.com>',
147 'author': u'Joe Doe <joe.doe@example.com>',
146 'date': start_date + datetime.timedelta(hours=12 * x),
148 'date': start_date + datetime.timedelta(hours=12 * x),
147 'added': [
149 'added': [
148 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
150 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
149 ],
151 ],
150 }
152 }
151
153
152 def test_simple(self):
154 def test_simple(self):
153 tip = self.repo.get_changeset()
155 tip = self.repo.get_changeset()
154 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
156 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
155
157
156 def test_get_changesets_is_ordered_by_date(self):
158 def test_get_changesets_is_ordered_by_date(self):
157 changesets = list(self.repo.get_changesets())
159 changesets = list(self.repo.get_changesets())
158 ordered_by_date = sorted(changesets,
160 ordered_by_date = sorted(changesets,
159 key=lambda cs: cs.date)
161 key=lambda cs: cs.date)
160 self.assertItemsEqual(changesets, ordered_by_date)
162 self.assertItemsEqual(changesets, ordered_by_date)
161
163
162 def test_get_changesets_respects_start(self):
164 def test_get_changesets_respects_start(self):
163 second_id = self.repo.revisions[1]
165 second_id = self.repo.revisions[1]
164 changesets = list(self.repo.get_changesets(start=second_id))
166 changesets = list(self.repo.get_changesets(start=second_id))
165 self.assertEqual(len(changesets), 4)
167 self.assertEqual(len(changesets), 4)
166
168
167 def test_get_changesets_numerical_id_respects_start(self):
169 def test_get_changesets_numerical_id_respects_start(self):
168 second_id = 1
170 second_id = 1
169 changesets = list(self.repo.get_changesets(start=second_id))
171 changesets = list(self.repo.get_changesets(start=second_id))
170 self.assertEqual(len(changesets), 4)
172 self.assertEqual(len(changesets), 4)
171
173
172 def test_get_changesets_includes_start_changeset(self):
174 def test_get_changesets_includes_start_changeset(self):
173 second_id = self.repo.revisions[1]
175 second_id = self.repo.revisions[1]
174 changesets = list(self.repo.get_changesets(start=second_id))
176 changesets = list(self.repo.get_changesets(start=second_id))
175 self.assertEqual(changesets[0].raw_id, second_id)
177 self.assertEqual(changesets[0].raw_id, second_id)
176
178
177 def test_get_changesets_respects_end(self):
179 def test_get_changesets_respects_end(self):
178 second_id = self.repo.revisions[1]
180 second_id = self.repo.revisions[1]
179 changesets = list(self.repo.get_changesets(end=second_id))
181 changesets = list(self.repo.get_changesets(end=second_id))
180 self.assertEqual(changesets[-1].raw_id, second_id)
182 self.assertEqual(changesets[-1].raw_id, second_id)
181 self.assertEqual(len(changesets), 2)
183 self.assertEqual(len(changesets), 2)
182
184
183 def test_get_changesets_numerical_id_respects_end(self):
185 def test_get_changesets_numerical_id_respects_end(self):
184 second_id = 1
186 second_id = 1
185 changesets = list(self.repo.get_changesets(end=second_id))
187 changesets = list(self.repo.get_changesets(end=second_id))
186 self.assertEqual(changesets.index(changesets[-1]), second_id)
188 self.assertEqual(changesets.index(changesets[-1]), second_id)
187 self.assertEqual(len(changesets), 2)
189 self.assertEqual(len(changesets), 2)
188
190
189 def test_get_changesets_respects_both_start_and_end(self):
191 def test_get_changesets_respects_both_start_and_end(self):
190 second_id = self.repo.revisions[1]
192 second_id = self.repo.revisions[1]
191 third_id = self.repo.revisions[2]
193 third_id = self.repo.revisions[2]
192 changesets = list(self.repo.get_changesets(start=second_id,
194 changesets = list(self.repo.get_changesets(start=second_id,
193 end=third_id))
195 end=third_id))
194 self.assertEqual(len(changesets), 2)
196 self.assertEqual(len(changesets), 2)
195
197
196 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
198 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
197 changesets = list(self.repo.get_changesets(start=2, end=3))
199 changesets = list(self.repo.get_changesets(start=2, end=3))
198 self.assertEqual(len(changesets), 2)
200 self.assertEqual(len(changesets), 2)
199
201
202 def test_get_changesets_on_empty_repo_raises_EmptyRepository_error(self):
203 Backend = self.get_backend()
204 repo_path = get_new_dir(str(time.time()))
205 repo = Backend(repo_path, create=True)
206
207 with self.assertRaises(EmptyRepositoryError):
208 list(repo.get_changesets(start='foobar'))
209
200 def test_get_changesets_includes_end_changeset(self):
210 def test_get_changesets_includes_end_changeset(self):
201 second_id = self.repo.revisions[1]
211 second_id = self.repo.revisions[1]
202 changesets = list(self.repo.get_changesets(end=second_id))
212 changesets = list(self.repo.get_changesets(end=second_id))
203 self.assertEqual(changesets[-1].raw_id, second_id)
213 self.assertEqual(changesets[-1].raw_id, second_id)
204
214
205 def test_get_changesets_respects_start_date(self):
215 def test_get_changesets_respects_start_date(self):
206 start_date = datetime.datetime(2010, 2, 1)
216 start_date = datetime.datetime(2010, 2, 1)
207 for cs in self.repo.get_changesets(start_date=start_date):
217 for cs in self.repo.get_changesets(start_date=start_date):
208 self.assertGreaterEqual(cs.date, start_date)
218 self.assertGreaterEqual(cs.date, start_date)
209
219
210 def test_get_changesets_respects_end_date(self):
220 def test_get_changesets_respects_end_date(self):
211 start_date = datetime.datetime(2010, 1, 1)
221 start_date = datetime.datetime(2010, 1, 1)
212 end_date = datetime.datetime(2010, 2, 1)
222 end_date = datetime.datetime(2010, 2, 1)
213 for cs in self.repo.get_changesets(start_date=start_date,
223 for cs in self.repo.get_changesets(start_date=start_date,
214 end_date=end_date):
224 end_date=end_date):
215 self.assertGreaterEqual(cs.date, start_date)
225 self.assertGreaterEqual(cs.date, start_date)
216 self.assertLessEqual(cs.date, end_date)
226 self.assertLessEqual(cs.date, end_date)
217
227
218 def test_get_changesets_respects_start_date_and_end_date(self):
228 def test_get_changesets_respects_start_date_and_end_date(self):
219 end_date = datetime.datetime(2010, 2, 1)
229 end_date = datetime.datetime(2010, 2, 1)
220 for cs in self.repo.get_changesets(end_date=end_date):
230 for cs in self.repo.get_changesets(end_date=end_date):
221 self.assertLessEqual(cs.date, end_date)
231 self.assertLessEqual(cs.date, end_date)
222
232
223 def test_get_changesets_respects_reverse(self):
233 def test_get_changesets_respects_reverse(self):
224 changesets_id_list = [cs.raw_id for cs in
234 changesets_id_list = [cs.raw_id for cs in
225 self.repo.get_changesets(reverse=True)]
235 self.repo.get_changesets(reverse=True)]
226 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
236 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
227
237
228 def test_get_filenodes_generator(self):
238 def test_get_filenodes_generator(self):
229 tip = self.repo.get_changeset()
239 tip = self.repo.get_changeset()
230 filepaths = [node.path for node in tip.get_filenodes_generator()]
240 filepaths = [node.path for node in tip.get_filenodes_generator()]
231 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
241 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
232
242
233 def test_size(self):
243 def test_size(self):
234 tip = self.repo.get_changeset()
244 tip = self.repo.get_changeset()
235 size = 5 * len('Foobar N') # Size of 5 files
245 size = 5 * len('Foobar N') # Size of 5 files
236 self.assertEqual(tip.size, size)
246 self.assertEqual(tip.size, size)
237
247
238 def test_author(self):
248 def test_author(self):
239 tip = self.repo.get_changeset()
249 tip = self.repo.get_changeset()
240 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
250 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
241
251
242 def test_author_name(self):
252 def test_author_name(self):
243 tip = self.repo.get_changeset()
253 tip = self.repo.get_changeset()
244 self.assertEqual(tip.author_name, u'Joe Doe')
254 self.assertEqual(tip.author_name, u'Joe Doe')
245
255
246 def test_author_email(self):
256 def test_author_email(self):
247 tip = self.repo.get_changeset()
257 tip = self.repo.get_changeset()
248 self.assertEqual(tip.author_email, u'joe.doe@example.com')
258 self.assertEqual(tip.author_email, u'joe.doe@example.com')
249
259
250 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
260 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
251 with self.assertRaises(ChangesetDoesNotExistError):
261 with self.assertRaises(ChangesetDoesNotExistError):
252 list(self.repo.get_changesets(start='foobar'))
262 list(self.repo.get_changesets(start='foobar'))
253
263
254 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
264 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
255 with self.assertRaises(ChangesetDoesNotExistError):
265 with self.assertRaises(ChangesetDoesNotExistError):
256 list(self.repo.get_changesets(end='foobar'))
266 list(self.repo.get_changesets(end='foobar'))
257
267
258 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
268 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
259 with self.assertRaises(BranchDoesNotExistError):
269 with self.assertRaises(BranchDoesNotExistError):
260 list(self.repo.get_changesets(branch_name='foobar'))
270 list(self.repo.get_changesets(branch_name='foobar'))
261
271
262 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
272 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
263 start = self.repo.revisions[-1]
273 start = self.repo.revisions[-1]
264 end = self.repo.revisions[0]
274 end = self.repo.revisions[0]
265 with self.assertRaises(RepositoryError):
275 with self.assertRaises(RepositoryError):
266 list(self.repo.get_changesets(start=start, end=end))
276 list(self.repo.get_changesets(start=start, end=end))
267
277
268 def test_get_changesets_numerical_id_reversed(self):
278 def test_get_changesets_numerical_id_reversed(self):
269 with self.assertRaises(RepositoryError):
279 with self.assertRaises(RepositoryError):
270 [x for x in self.repo.get_changesets(start=3, end=2)]
280 [x for x in self.repo.get_changesets(start=3, end=2)]
271
281
272 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
282 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
273 with self.assertRaises(RepositoryError):
283 with self.assertRaises(RepositoryError):
274 last = len(self.repo.revisions)
284 last = len(self.repo.revisions)
275 list(self.repo.get_changesets(start=last-1, end=last-2))
285 list(self.repo.get_changesets(start=last-1, end=last-2))
276
286
277 def test_get_changesets_numerical_id_last_zero_error(self):
287 def test_get_changesets_numerical_id_last_zero_error(self):
278 with self.assertRaises(RepositoryError):
288 with self.assertRaises(RepositoryError):
279 last = len(self.repo.revisions)
289 last = len(self.repo.revisions)
280 list(self.repo.get_changesets(start=last-1, end=0))
290 list(self.repo.get_changesets(start=last-1, end=0))
281
291
282
292
283 class ChangesetsChangesTestCaseMixin(BackendTestMixin):
293 class ChangesetsChangesTestCaseMixin(BackendTestMixin):
284 recreate_repo_per_test = False
294 recreate_repo_per_test = False
285
295
286 @classmethod
296 @classmethod
287 def _get_commits(cls):
297 def _get_commits(cls):
288 return [
298 return [
289 {
299 {
290 'message': u'Initial',
300 'message': u'Initial',
291 'author': u'Joe Doe <joe.doe@example.com>',
301 'author': u'Joe Doe <joe.doe@example.com>',
292 'date': datetime.datetime(2010, 1, 1, 20),
302 'date': datetime.datetime(2010, 1, 1, 20),
293 'added': [
303 'added': [
294 FileNode('foo/bar', content='foo'),
304 FileNode('foo/bar', content='foo'),
295 FileNode('foobar', content='foo'),
305 FileNode('foobar', content='foo'),
296 FileNode('qwe', content='foo'),
306 FileNode('qwe', content='foo'),
297 ],
307 ],
298 },
308 },
299 {
309 {
300 'message': u'Massive changes',
310 'message': u'Massive changes',
301 'author': u'Joe Doe <joe.doe@example.com>',
311 'author': u'Joe Doe <joe.doe@example.com>',
302 'date': datetime.datetime(2010, 1, 1, 22),
312 'date': datetime.datetime(2010, 1, 1, 22),
303 'added': [FileNode('fallout', content='War never changes')],
313 'added': [FileNode('fallout', content='War never changes')],
304 'changed': [
314 'changed': [
305 FileNode('foo/bar', content='baz'),
315 FileNode('foo/bar', content='baz'),
306 FileNode('foobar', content='baz'),
316 FileNode('foobar', content='baz'),
307 ],
317 ],
308 'removed': [FileNode('qwe')],
318 'removed': [FileNode('qwe')],
309 },
319 },
310 ]
320 ]
311
321
312 def test_initial_commit(self):
322 def test_initial_commit(self):
313 changeset = self.repo.get_changeset(0)
323 changeset = self.repo.get_changeset(0)
314 self.assertItemsEqual(changeset.added, [
324 self.assertItemsEqual(changeset.added, [
315 changeset.get_node('foo/bar'),
325 changeset.get_node('foo/bar'),
316 changeset.get_node('foobar'),
326 changeset.get_node('foobar'),
317 changeset.get_node('qwe'),
327 changeset.get_node('qwe'),
318 ])
328 ])
319 self.assertItemsEqual(changeset.changed, [])
329 self.assertItemsEqual(changeset.changed, [])
320 self.assertItemsEqual(changeset.removed, [])
330 self.assertItemsEqual(changeset.removed, [])
321
331
322 def test_head_added(self):
332 def test_head_added(self):
323 changeset = self.repo.get_changeset()
333 changeset = self.repo.get_changeset()
324 self.assertTrue(isinstance(changeset.added, AddedFileNodesGenerator))
334 self.assertTrue(isinstance(changeset.added, AddedFileNodesGenerator))
325 self.assertItemsEqual(changeset.added, [
335 self.assertItemsEqual(changeset.added, [
326 changeset.get_node('fallout'),
336 changeset.get_node('fallout'),
327 ])
337 ])
328 self.assertTrue(isinstance(changeset.changed, ChangedFileNodesGenerator))
338 self.assertTrue(isinstance(changeset.changed, ChangedFileNodesGenerator))
329 self.assertItemsEqual(changeset.changed, [
339 self.assertItemsEqual(changeset.changed, [
330 changeset.get_node('foo/bar'),
340 changeset.get_node('foo/bar'),
331 changeset.get_node('foobar'),
341 changeset.get_node('foobar'),
332 ])
342 ])
333 self.assertTrue(isinstance(changeset.removed, RemovedFileNodesGenerator))
343 self.assertTrue(isinstance(changeset.removed, RemovedFileNodesGenerator))
334 self.assertEqual(len(changeset.removed), 1)
344 self.assertEqual(len(changeset.removed), 1)
335 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
345 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
336
346
337
347
338 # For each backend create test case class
348 # For each backend create test case class
339 for alias in SCM_TESTS:
349 for alias in SCM_TESTS:
340 attrs = {
350 attrs = {
341 'backend_alias': alias,
351 'backend_alias': alias,
342 }
352 }
343 # tests with additional commits
353 # tests with additional commits
344 cls_name = ''.join(('%s changesets with commits test' % alias).title().split())
354 cls_name = ''.join(('%s changesets with commits test' % alias).title().split())
345 bases = (ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
355 bases = (ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
346 globals()[cls_name] = type(cls_name, bases, attrs)
356 globals()[cls_name] = type(cls_name, bases, attrs)
347
357
348 # tests without additional commits
358 # tests without additional commits
349 cls_name = ''.join(('%s changesets test' % alias).title().split())
359 cls_name = ''.join(('%s changesets test' % alias).title().split())
350 bases = (ChangesetsTestCaseMixin, unittest.TestCase)
360 bases = (ChangesetsTestCaseMixin, unittest.TestCase)
351 globals()[cls_name] = type(cls_name, bases, attrs)
361 globals()[cls_name] = type(cls_name, bases, attrs)
352
362
353 # tests changes
363 # tests changes
354 cls_name = ''.join(('%s changesets changes test' % alias).title().split())
364 cls_name = ''.join(('%s changesets changes test' % alias).title().split())
355 bases = (ChangesetsChangesTestCaseMixin, unittest.TestCase)
365 bases = (ChangesetsChangesTestCaseMixin, unittest.TestCase)
356 globals()[cls_name] = type(cls_name, bases, attrs)
366 globals()[cls_name] = type(cls_name, bases, attrs)
357
367
358
368
359 if __name__ == '__main__':
369 if __name__ == '__main__':
360 unittest.main()
370 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now