##// END OF EJS Templates
vcs: sanitize diff context values (Issue #306)...
Branko Majic -
r7049:55d2b08d stable
parent child Browse files
Show More
@@ -1,734 +1,756 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 errno
15 import errno
16 import urllib
16 import urllib
17 import urllib2
17 import urllib2
18 import logging
18 import logging
19 import posixpath
19 import posixpath
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 from dulwich.config import ConfigFile
23 from dulwich.config import ConfigFile
24
24
25 from kallithea.lib.vcs import subprocessio
25 from kallithea.lib.vcs import subprocessio
26 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
26 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
27 from kallithea.lib.vcs.conf import settings
27 from kallithea.lib.vcs.conf import settings
28
28
29 from kallithea.lib.vcs.exceptions import (
29 from kallithea.lib.vcs.exceptions import (
30 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
30 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
31 RepositoryError, TagAlreadyExistError, TagDoesNotExistError
31 RepositoryError, TagAlreadyExistError, TagDoesNotExistError
32 )
32 )
33 from kallithea.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 from kallithea.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
34 from kallithea.lib.vcs.utils.lazy import LazyProperty
34 from kallithea.lib.vcs.utils.lazy import LazyProperty
35 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
35 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
36 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
36 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
37
37
38 from kallithea.lib.vcs.utils.hgcompat import (
38 from kallithea.lib.vcs.utils.hgcompat import (
39 hg_url, httpbasicauthhandler, httpdigestauthhandler
39 hg_url, httpbasicauthhandler, httpdigestauthhandler
40 )
40 )
41
41
42 from .changeset import GitChangeset
42 from .changeset import GitChangeset
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
84 @property
85 def _empty(self):
85 def _empty(self):
86 """
86 """
87 Checks if repository is empty ie. without any changesets
87 Checks if repository is empty ie. without any changesets
88 """
88 """
89
89
90 try:
90 try:
91 self.revisions[0]
91 self.revisions[0]
92 except (KeyError, IndexError):
92 except (KeyError, IndexError):
93 return True
93 return True
94 return False
94 return False
95
95
96 @LazyProperty
96 @LazyProperty
97 def revisions(self):
97 def revisions(self):
98 """
98 """
99 Returns list of revisions' ids, in ascending order. Being lazy
99 Returns list of revisions' ids, in ascending order. Being lazy
100 attribute allows external tools to inject shas from cache.
100 attribute allows external tools to inject shas from cache.
101 """
101 """
102 return self._get_all_revisions()
102 return self._get_all_revisions()
103
103
104 @classmethod
104 @classmethod
105 def _run_git_command(cls, cmd, **opts):
105 def _run_git_command(cls, cmd, **opts):
106 """
106 """
107 Runs given ``cmd`` as git command and returns tuple
107 Runs given ``cmd`` as git command and returns tuple
108 (stdout, stderr).
108 (stdout, stderr).
109
109
110 :param cmd: git command to be executed
110 :param cmd: git command to be executed
111 :param opts: env options to pass into Subprocess command
111 :param opts: env options to pass into Subprocess command
112 """
112 """
113
113
114 if '_bare' in opts:
114 if '_bare' in opts:
115 _copts = []
115 _copts = []
116 del opts['_bare']
116 del opts['_bare']
117 else:
117 else:
118 _copts = ['-c', 'core.quotepath=false', ]
118 _copts = ['-c', 'core.quotepath=false', ]
119 safe_call = False
119 safe_call = False
120 if '_safe' in opts:
120 if '_safe' in opts:
121 #no exc on failure
121 #no exc on failure
122 del opts['_safe']
122 del opts['_safe']
123 safe_call = True
123 safe_call = True
124
124
125 assert isinstance(cmd, list), cmd
125 assert isinstance(cmd, list), cmd
126
126
127 gitenv = os.environ
127 gitenv = os.environ
128 # need to clean fix GIT_DIR !
128 # need to clean fix GIT_DIR !
129 if 'GIT_DIR' in gitenv:
129 if 'GIT_DIR' in gitenv:
130 del gitenv['GIT_DIR']
130 del gitenv['GIT_DIR']
131 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
131 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
132
132
133 _git_path = settings.GIT_EXECUTABLE_PATH
133 _git_path = settings.GIT_EXECUTABLE_PATH
134 cmd = [_git_path] + _copts + cmd
134 cmd = [_git_path] + _copts + cmd
135
135
136 try:
136 try:
137 _opts = dict(
137 _opts = dict(
138 env=gitenv,
138 env=gitenv,
139 shell=False,
139 shell=False,
140 )
140 )
141 _opts.update(opts)
141 _opts.update(opts)
142 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
142 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
143 except (EnvironmentError, OSError) as err:
143 except (EnvironmentError, OSError) as err:
144 tb_err = ("Couldn't run git command (%s).\n"
144 tb_err = ("Couldn't run git command (%s).\n"
145 "Original error was:%s\n" % (cmd, err))
145 "Original error was:%s\n" % (cmd, err))
146 log.error(tb_err)
146 log.error(tb_err)
147 if safe_call:
147 if safe_call:
148 return '', err
148 return '', err
149 else:
149 else:
150 raise RepositoryError(tb_err)
150 raise RepositoryError(tb_err)
151
151
152 return ''.join(p.output), ''.join(p.error)
152 return ''.join(p.output), ''.join(p.error)
153
153
154 def run_git_command(self, cmd):
154 def run_git_command(self, cmd):
155 opts = {}
155 opts = {}
156 if os.path.isdir(self.path):
156 if os.path.isdir(self.path):
157 opts['cwd'] = self.path
157 opts['cwd'] = self.path
158 return self._run_git_command(cmd, **opts)
158 return self._run_git_command(cmd, **opts)
159
159
160 @classmethod
160 @classmethod
161 def _check_url(cls, url):
161 def _check_url(cls, url):
162 """
162 """
163 Function will check given url and try to verify if it's a valid
163 Function will check given url and try to verify if it's a valid
164 link. Sometimes it may happened that git will issue basic
164 link. Sometimes it may happened that git will issue basic
165 auth request that can cause whole API to hang when used from python
165 auth request that can cause whole API to hang when used from python
166 or other external calls.
166 or other external calls.
167
167
168 On failures it'll raise urllib2.HTTPError, exception is also thrown
168 On failures it'll raise urllib2.HTTPError, exception is also thrown
169 when the return code is non 200
169 when the return code is non 200
170 """
170 """
171
171
172 # check first if it's not an local url
172 # check first if it's not an local url
173 if os.path.isdir(url) or url.startswith('file:'):
173 if os.path.isdir(url) or url.startswith('file:'):
174 return True
174 return True
175
175
176 if url.startswith('git://'):
176 if url.startswith('git://'):
177 return True
177 return True
178
178
179 if '+' in url[:url.find('://')]:
179 if '+' in url[:url.find('://')]:
180 url = url[url.find('+') + 1:]
180 url = url[url.find('+') + 1:]
181
181
182 handlers = []
182 handlers = []
183 url_obj = hg_url(url)
183 url_obj = hg_url(url)
184 test_uri, authinfo = url_obj.authinfo()
184 test_uri, authinfo = url_obj.authinfo()
185 url_obj.passwd = '*****'
185 url_obj.passwd = '*****'
186 cleaned_uri = str(url_obj)
186 cleaned_uri = str(url_obj)
187
187
188 if not test_uri.endswith('info/refs'):
188 if not test_uri.endswith('info/refs'):
189 test_uri = test_uri.rstrip('/') + '/info/refs'
189 test_uri = test_uri.rstrip('/') + '/info/refs'
190
190
191 if authinfo:
191 if authinfo:
192 #create a password manager
192 #create a password manager
193 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
193 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
194 passmgr.add_password(*authinfo)
194 passmgr.add_password(*authinfo)
195
195
196 handlers.extend((httpbasicauthhandler(passmgr),
196 handlers.extend((httpbasicauthhandler(passmgr),
197 httpdigestauthhandler(passmgr)))
197 httpdigestauthhandler(passmgr)))
198
198
199 o = urllib2.build_opener(*handlers)
199 o = urllib2.build_opener(*handlers)
200 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
200 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
201
201
202 q = {"service": 'git-upload-pack'}
202 q = {"service": 'git-upload-pack'}
203 qs = '?%s' % urllib.urlencode(q)
203 qs = '?%s' % urllib.urlencode(q)
204 cu = "%s%s" % (test_uri, qs)
204 cu = "%s%s" % (test_uri, qs)
205 req = urllib2.Request(cu, None, {})
205 req = urllib2.Request(cu, None, {})
206
206
207 try:
207 try:
208 resp = o.open(req)
208 resp = o.open(req)
209 if resp.code != 200:
209 if resp.code != 200:
210 raise Exception('Return Code is not 200')
210 raise Exception('Return Code is not 200')
211 except Exception as e:
211 except Exception as e:
212 # means it cannot be cloned
212 # means it cannot be cloned
213 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
213 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
214
214
215 # now detect if it's proper git repo
215 # now detect if it's proper git repo
216 gitdata = resp.read()
216 gitdata = resp.read()
217 if not 'service=git-upload-pack' in gitdata:
217 if not 'service=git-upload-pack' in gitdata:
218 raise urllib2.URLError(
218 raise urllib2.URLError(
219 "url [%s] does not look like an git" % (cleaned_uri))
219 "url [%s] does not look like an git" % (cleaned_uri))
220
220
221 return True
221 return True
222
222
223 def _get_repo(self, create, src_url=None, update_after_clone=False,
223 def _get_repo(self, create, src_url=None, update_after_clone=False,
224 bare=False):
224 bare=False):
225 if create and os.path.exists(self.path):
225 if create and os.path.exists(self.path):
226 raise RepositoryError("Location already exist")
226 raise RepositoryError("Location already exist")
227 if src_url and not create:
227 if src_url and not create:
228 raise RepositoryError("Create should be set to True if src_url is "
228 raise RepositoryError("Create should be set to True if src_url is "
229 "given (clone operation creates repository)")
229 "given (clone operation creates repository)")
230 try:
230 try:
231 if create and src_url:
231 if create and src_url:
232 GitRepository._check_url(src_url)
232 GitRepository._check_url(src_url)
233 self.clone(src_url, update_after_clone, bare)
233 self.clone(src_url, update_after_clone, bare)
234 return Repo(self.path)
234 return Repo(self.path)
235 elif create:
235 elif create:
236 os.makedirs(self.path)
236 os.makedirs(self.path)
237 if bare:
237 if bare:
238 return Repo.init_bare(self.path)
238 return Repo.init_bare(self.path)
239 else:
239 else:
240 return Repo.init(self.path)
240 return Repo.init(self.path)
241 else:
241 else:
242 return self._repo
242 return self._repo
243 except (NotGitRepository, OSError) as err:
243 except (NotGitRepository, OSError) as err:
244 raise RepositoryError(err)
244 raise RepositoryError(err)
245
245
246 def _get_all_revisions(self):
246 def _get_all_revisions(self):
247 # we must check if this repo is not empty, since later command
247 # we must check if this repo is not empty, since later command
248 # fails if it is. And it's cheaper to ask than throw the subprocess
248 # fails if it is. And it's cheaper to ask than throw the subprocess
249 # errors
249 # errors
250 try:
250 try:
251 self._repo.head()
251 self._repo.head()
252 except KeyError:
252 except KeyError:
253 return []
253 return []
254
254
255 rev_filter = settings.GIT_REV_FILTER
255 rev_filter = settings.GIT_REV_FILTER
256 cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
256 cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
257 try:
257 try:
258 so, se = self.run_git_command(cmd)
258 so, se = self.run_git_command(cmd)
259 except RepositoryError:
259 except RepositoryError:
260 # Can be raised for empty repositories
260 # Can be raised for empty repositories
261 return []
261 return []
262 return so.splitlines()
262 return so.splitlines()
263
263
264 def _get_all_revisions2(self):
264 def _get_all_revisions2(self):
265 #alternate implementation using dulwich
265 #alternate implementation using dulwich
266 includes = [x[1][0] for x in self._parsed_refs.iteritems()
266 includes = [x[1][0] for x in self._parsed_refs.iteritems()
267 if x[1][1] != 'T']
267 if x[1][1] != 'T']
268 return [c.commit.id for c in self._repo.get_walker(include=includes)]
268 return [c.commit.id for c in self._repo.get_walker(include=includes)]
269
269
270 def _get_revision(self, revision):
270 def _get_revision(self, revision):
271 """
271 """
272 For git backend we always return integer here. This way we ensure
272 For git backend we always return integer here. This way we ensure
273 that changeset's revision attribute would become integer.
273 that changeset's revision attribute would become integer.
274 """
274 """
275
275
276 is_null = lambda o: len(o) == revision.count('0')
276 is_null = lambda o: len(o) == revision.count('0')
277
277
278 if self._empty:
278 if self._empty:
279 raise EmptyRepositoryError("There are no changesets yet")
279 raise EmptyRepositoryError("There are no changesets yet")
280
280
281 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
281 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
282 return self.revisions[-1]
282 return self.revisions[-1]
283
283
284 is_bstr = isinstance(revision, (str, unicode))
284 is_bstr = isinstance(revision, (str, unicode))
285 if ((is_bstr and revision.isdigit() and len(revision) < 12)
285 if ((is_bstr and revision.isdigit() and len(revision) < 12)
286 or isinstance(revision, int) or is_null(revision)):
286 or isinstance(revision, int) or is_null(revision)):
287 try:
287 try:
288 revision = self.revisions[int(revision)]
288 revision = self.revisions[int(revision)]
289 except IndexError:
289 except IndexError:
290 msg = ("Revision %s does not exist for %s" % (revision, self))
290 msg = ("Revision %s does not exist for %s" % (revision, self))
291 raise ChangesetDoesNotExistError(msg)
291 raise ChangesetDoesNotExistError(msg)
292
292
293 elif is_bstr:
293 elif is_bstr:
294 # get by branch/tag name
294 # get by branch/tag name
295 _ref_revision = self._parsed_refs.get(revision)
295 _ref_revision = self._parsed_refs.get(revision)
296 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
296 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
297 return _ref_revision[0]
297 return _ref_revision[0]
298
298
299 _tags_shas = self.tags.values()
299 _tags_shas = self.tags.values()
300 # maybe it's a tag ? we don't have them in self.revisions
300 # maybe it's a tag ? we don't have them in self.revisions
301 if revision in _tags_shas:
301 if revision in _tags_shas:
302 return _tags_shas[_tags_shas.index(revision)]
302 return _tags_shas[_tags_shas.index(revision)]
303
303
304 elif not SHA_PATTERN.match(revision) or revision not in self.revisions:
304 elif not SHA_PATTERN.match(revision) or revision not in self.revisions:
305 msg = ("Revision %s does not exist for %s" % (revision, self))
305 msg = ("Revision %s does not exist for %s" % (revision, self))
306 raise ChangesetDoesNotExistError(msg)
306 raise ChangesetDoesNotExistError(msg)
307
307
308 # Ensure we return full id
308 # Ensure we return full id
309 if not SHA_PATTERN.match(str(revision)):
309 if not SHA_PATTERN.match(str(revision)):
310 raise ChangesetDoesNotExistError("Given revision %s not recognized"
310 raise ChangesetDoesNotExistError("Given revision %s not recognized"
311 % revision)
311 % revision)
312 return revision
312 return revision
313
313
314 def get_ref_revision(self, ref_type, ref_name):
314 def get_ref_revision(self, ref_type, ref_name):
315 """
315 """
316 Returns ``MercurialChangeset`` object representing repository's
316 Returns ``MercurialChangeset`` object representing repository's
317 changeset at the given ``revision``.
317 changeset at the given ``revision``.
318 """
318 """
319 return self._get_revision(ref_name)
319 return self._get_revision(ref_name)
320
320
321 def _get_archives(self, archive_name='tip'):
321 def _get_archives(self, archive_name='tip'):
322
322
323 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
323 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
324 yield {"type": i[0], "extension": i[1], "node": archive_name}
324 yield {"type": i[0], "extension": i[1], "node": archive_name}
325
325
326 def _get_url(self, url):
326 def _get_url(self, url):
327 """
327 """
328 Returns normalized url. If schema is not given, would fall to
328 Returns normalized url. If schema is not given, would fall to
329 filesystem (``file:///``) schema.
329 filesystem (``file:///``) schema.
330 """
330 """
331 url = str(url)
331 url = str(url)
332 if url != 'default' and not '://' in url:
332 if url != 'default' and not '://' in url:
333 url = ':///'.join(('file', url))
333 url = ':///'.join(('file', url))
334 return url
334 return url
335
335
336 def get_hook_location(self):
336 def get_hook_location(self):
337 """
337 """
338 returns absolute path to location where hooks are stored
338 returns absolute path to location where hooks are stored
339 """
339 """
340 loc = os.path.join(self.path, 'hooks')
340 loc = os.path.join(self.path, 'hooks')
341 if not self.bare:
341 if not self.bare:
342 loc = os.path.join(self.path, '.git', 'hooks')
342 loc = os.path.join(self.path, '.git', 'hooks')
343 return loc
343 return loc
344
344
345 @LazyProperty
345 @LazyProperty
346 def name(self):
346 def name(self):
347 return os.path.basename(self.path)
347 return os.path.basename(self.path)
348
348
349 @LazyProperty
349 @LazyProperty
350 def last_change(self):
350 def last_change(self):
351 """
351 """
352 Returns last change made on this repository as datetime object
352 Returns last change made on this repository as datetime object
353 """
353 """
354 return date_fromtimestamp(self._get_mtime(), makedate()[1])
354 return date_fromtimestamp(self._get_mtime(), makedate()[1])
355
355
356 def _get_mtime(self):
356 def _get_mtime(self):
357 try:
357 try:
358 return time.mktime(self.get_changeset().date.timetuple())
358 return time.mktime(self.get_changeset().date.timetuple())
359 except RepositoryError:
359 except RepositoryError:
360 idx_loc = '' if self.bare else '.git'
360 idx_loc = '' if self.bare else '.git'
361 # fallback to filesystem
361 # fallback to filesystem
362 in_path = os.path.join(self.path, idx_loc, "index")
362 in_path = os.path.join(self.path, idx_loc, "index")
363 he_path = os.path.join(self.path, idx_loc, "HEAD")
363 he_path = os.path.join(self.path, idx_loc, "HEAD")
364 if os.path.exists(in_path):
364 if os.path.exists(in_path):
365 return os.stat(in_path).st_mtime
365 return os.stat(in_path).st_mtime
366 else:
366 else:
367 return os.stat(he_path).st_mtime
367 return os.stat(he_path).st_mtime
368
368
369 @LazyProperty
369 @LazyProperty
370 def description(self):
370 def description(self):
371 undefined_description = u'unknown'
371 undefined_description = u'unknown'
372 _desc = self._repo.get_description()
372 _desc = self._repo.get_description()
373 return safe_unicode(_desc or undefined_description)
373 return safe_unicode(_desc or undefined_description)
374
374
375 @LazyProperty
375 @LazyProperty
376 def contact(self):
376 def contact(self):
377 undefined_contact = u'Unknown'
377 undefined_contact = u'Unknown'
378 return undefined_contact
378 return undefined_contact
379
379
380 @property
380 @property
381 def branches(self):
381 def branches(self):
382 if not self.revisions:
382 if not self.revisions:
383 return {}
383 return {}
384 sortkey = lambda ctx: ctx[0]
384 sortkey = lambda ctx: ctx[0]
385 _branches = [(x[0], x[1][0])
385 _branches = [(x[0], x[1][0])
386 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
386 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
387 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
387 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
388
388
389 @LazyProperty
389 @LazyProperty
390 def closed_branches(self):
390 def closed_branches(self):
391 return {}
391 return {}
392
392
393 @LazyProperty
393 @LazyProperty
394 def tags(self):
394 def tags(self):
395 return self._get_tags()
395 return self._get_tags()
396
396
397 def _get_tags(self):
397 def _get_tags(self):
398 if not self.revisions:
398 if not self.revisions:
399 return {}
399 return {}
400
400
401 sortkey = lambda ctx: ctx[0]
401 sortkey = lambda ctx: ctx[0]
402 _tags = [(x[0], x[1][0])
402 _tags = [(x[0], x[1][0])
403 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
403 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
404 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
404 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
405
405
406 def tag(self, name, user, revision=None, message=None, date=None,
406 def tag(self, name, user, revision=None, message=None, date=None,
407 **kwargs):
407 **kwargs):
408 """
408 """
409 Creates and returns a tag for the given ``revision``.
409 Creates and returns a tag for the given ``revision``.
410
410
411 :param name: name for new tag
411 :param name: name for new tag
412 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
412 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
413 :param revision: changeset id for which new tag would be created
413 :param revision: changeset id for which new tag would be created
414 :param message: message of the tag's commit
414 :param message: message of the tag's commit
415 :param date: date of tag's commit
415 :param date: date of tag's commit
416
416
417 :raises TagAlreadyExistError: if tag with same name already exists
417 :raises TagAlreadyExistError: if tag with same name already exists
418 """
418 """
419 if name in self.tags:
419 if name in self.tags:
420 raise TagAlreadyExistError("Tag %s already exists" % name)
420 raise TagAlreadyExistError("Tag %s already exists" % name)
421 changeset = self.get_changeset(revision)
421 changeset = self.get_changeset(revision)
422 message = message or "Added tag %s for commit %s" % (name,
422 message = message or "Added tag %s for commit %s" % (name,
423 changeset.raw_id)
423 changeset.raw_id)
424 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
424 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
425
425
426 self._parsed_refs = self._get_parsed_refs()
426 self._parsed_refs = self._get_parsed_refs()
427 self.tags = self._get_tags()
427 self.tags = self._get_tags()
428 return changeset
428 return changeset
429
429
430 def remove_tag(self, name, user, message=None, date=None):
430 def remove_tag(self, name, user, message=None, date=None):
431 """
431 """
432 Removes tag with the given ``name``.
432 Removes tag with the given ``name``.
433
433
434 :param name: name of the tag to be removed
434 :param name: name of the tag to be removed
435 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
435 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
436 :param message: message of the tag's removal commit
436 :param message: message of the tag's removal commit
437 :param date: date of tag's removal commit
437 :param date: date of tag's removal commit
438
438
439 :raises TagDoesNotExistError: if tag with given name does not exists
439 :raises TagDoesNotExistError: if tag with given name does not exists
440 """
440 """
441 if name not in self.tags:
441 if name not in self.tags:
442 raise TagDoesNotExistError("Tag %s does not exist" % name)
442 raise TagDoesNotExistError("Tag %s does not exist" % name)
443 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
443 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
444 try:
444 try:
445 os.remove(tagpath)
445 os.remove(tagpath)
446 self._parsed_refs = self._get_parsed_refs()
446 self._parsed_refs = self._get_parsed_refs()
447 self.tags = self._get_tags()
447 self.tags = self._get_tags()
448 except OSError as e:
448 except OSError as e:
449 raise RepositoryError(e.strerror)
449 raise RepositoryError(e.strerror)
450
450
451 @LazyProperty
451 @LazyProperty
452 def bookmarks(self):
452 def bookmarks(self):
453 """
453 """
454 Gets bookmarks for this repository
454 Gets bookmarks for this repository
455 """
455 """
456 return {}
456 return {}
457
457
458 @LazyProperty
458 @LazyProperty
459 def _parsed_refs(self):
459 def _parsed_refs(self):
460 return self._get_parsed_refs()
460 return self._get_parsed_refs()
461
461
462 def _get_parsed_refs(self):
462 def _get_parsed_refs(self):
463 # cache the property
463 # cache the property
464 _repo = self._repo
464 _repo = self._repo
465 refs = _repo.get_refs()
465 refs = _repo.get_refs()
466 keys = [('refs/heads/', 'H'),
466 keys = [('refs/heads/', 'H'),
467 ('refs/remotes/origin/', 'RH'),
467 ('refs/remotes/origin/', 'RH'),
468 ('refs/tags/', 'T')]
468 ('refs/tags/', 'T')]
469 _refs = {}
469 _refs = {}
470 for ref, sha in refs.iteritems():
470 for ref, sha in refs.iteritems():
471 for k, type_ in keys:
471 for k, type_ in keys:
472 if ref.startswith(k):
472 if ref.startswith(k):
473 _key = ref[len(k):]
473 _key = ref[len(k):]
474 if type_ == 'T':
474 if type_ == 'T':
475 obj = _repo.get_object(sha)
475 obj = _repo.get_object(sha)
476 if isinstance(obj, Tag):
476 if isinstance(obj, Tag):
477 sha = _repo.get_object(sha).object[1]
477 sha = _repo.get_object(sha).object[1]
478 _refs[_key] = [sha, type_]
478 _refs[_key] = [sha, type_]
479 break
479 break
480 return _refs
480 return _refs
481
481
482 def _heads(self, reverse=False):
482 def _heads(self, reverse=False):
483 refs = self._repo.get_refs()
483 refs = self._repo.get_refs()
484 heads = {}
484 heads = {}
485
485
486 for key, val in refs.items():
486 for key, val in refs.items():
487 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
487 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
488 if key.startswith(ref_key):
488 if key.startswith(ref_key):
489 n = key[len(ref_key):]
489 n = key[len(ref_key):]
490 if n not in ['HEAD']:
490 if n not in ['HEAD']:
491 heads[n] = val
491 heads[n] = val
492
492
493 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
493 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
494
494
495 def get_changeset(self, revision=None):
495 def get_changeset(self, revision=None):
496 """
496 """
497 Returns ``GitChangeset`` object representing commit from git repository
497 Returns ``GitChangeset`` object representing commit from git repository
498 at the given revision or head (most recent commit) if None given.
498 at the given revision or head (most recent commit) if None given.
499 """
499 """
500 if isinstance(revision, GitChangeset):
500 if isinstance(revision, GitChangeset):
501 return revision
501 return revision
502 revision = self._get_revision(revision)
502 revision = self._get_revision(revision)
503 changeset = GitChangeset(repository=self, revision=revision)
503 changeset = GitChangeset(repository=self, revision=revision)
504 return changeset
504 return changeset
505
505
506 def get_changesets(self, start=None, end=None, start_date=None,
506 def get_changesets(self, start=None, end=None, start_date=None,
507 end_date=None, branch_name=None, reverse=False):
507 end_date=None, branch_name=None, reverse=False):
508 """
508 """
509 Returns iterator of ``GitChangeset`` objects from start to end (both
509 Returns iterator of ``GitChangeset`` objects from start to end (both
510 are inclusive), in ascending date order (unless ``reverse`` is set).
510 are inclusive), in ascending date order (unless ``reverse`` is set).
511
511
512 :param start: changeset ID, as str; first returned changeset
512 :param start: changeset ID, as str; first returned changeset
513 :param end: changeset ID, as str; last returned changeset
513 :param end: changeset ID, as str; last returned changeset
514 :param start_date: if specified, changesets with commit date less than
514 :param start_date: if specified, changesets with commit date less than
515 ``start_date`` would be filtered out from returned set
515 ``start_date`` would be filtered out from returned set
516 :param end_date: if specified, changesets with commit date greater than
516 :param end_date: if specified, changesets with commit date greater than
517 ``end_date`` would be filtered out from returned set
517 ``end_date`` would be filtered out from returned set
518 :param branch_name: if specified, changesets not reachable from given
518 :param branch_name: if specified, changesets not reachable from given
519 branch would be filtered out from returned set
519 branch would be filtered out from returned set
520 :param reverse: if ``True``, returned generator would be reversed
520 :param reverse: if ``True``, returned generator would be reversed
521 (meaning that returned changesets would have descending date order)
521 (meaning that returned changesets would have descending date order)
522
522
523 :raise BranchDoesNotExistError: If given ``branch_name`` does not
523 :raise BranchDoesNotExistError: If given ``branch_name`` does not
524 exist.
524 exist.
525 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
525 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
526 ``end`` could not be found.
526 ``end`` could not be found.
527
527
528 """
528 """
529 if branch_name and branch_name not in self.branches:
529 if branch_name and branch_name not in self.branches:
530 raise BranchDoesNotExistError("Branch '%s' not found" \
530 raise BranchDoesNotExistError("Branch '%s' not found" \
531 % branch_name)
531 % branch_name)
532 # actually we should check now if it's not an empty repo to not spaw
532 # actually we should check now if it's not an empty repo to not spaw
533 # subprocess commands
533 # subprocess commands
534 if self._empty:
534 if self._empty:
535 raise EmptyRepositoryError("There are no changesets yet")
535 raise EmptyRepositoryError("There are no changesets yet")
536
536
537 # %H at format means (full) commit hash, initial hashes are retrieved
537 # %H at format means (full) commit hash, initial hashes are retrieved
538 # in ascending date order
538 # in ascending date order
539 cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H']
539 cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H']
540 if start_date:
540 if start_date:
541 cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')]
541 cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')]
542 if end_date:
542 if end_date:
543 cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')]
543 cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')]
544 if branch_name:
544 if branch_name:
545 cmd.append(branch_name)
545 cmd.append(branch_name)
546 else:
546 else:
547 cmd.append(settings.GIT_REV_FILTER)
547 cmd.append(settings.GIT_REV_FILTER)
548
548
549 revs = self.run_git_command(cmd)[0].splitlines()
549 revs = self.run_git_command(cmd)[0].splitlines()
550 start_pos = 0
550 start_pos = 0
551 end_pos = len(revs)
551 end_pos = len(revs)
552 if start:
552 if start:
553 _start = self._get_revision(start)
553 _start = self._get_revision(start)
554 try:
554 try:
555 start_pos = revs.index(_start)
555 start_pos = revs.index(_start)
556 except ValueError:
556 except ValueError:
557 pass
557 pass
558
558
559 if end is not None:
559 if end is not None:
560 _end = self._get_revision(end)
560 _end = self._get_revision(end)
561 try:
561 try:
562 end_pos = revs.index(_end)
562 end_pos = revs.index(_end)
563 except ValueError:
563 except ValueError:
564 pass
564 pass
565
565
566 if None not in [start, end] and start_pos > end_pos:
566 if None not in [start, end] and start_pos > end_pos:
567 raise RepositoryError('start cannot be after end')
567 raise RepositoryError('start cannot be after end')
568
568
569 if end_pos is not None:
569 if end_pos is not None:
570 end_pos += 1
570 end_pos += 1
571
571
572 revs = revs[start_pos:end_pos]
572 revs = revs[start_pos:end_pos]
573 if reverse:
573 if reverse:
574 revs = reversed(revs)
574 revs = reversed(revs)
575 return CollectionGenerator(self, revs)
575 return CollectionGenerator(self, revs)
576
576
577 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
577 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
578 context=3):
578 context=3):
579 """
579 """
580 Returns (git like) *diff*, as plain text. Shows changes introduced by
580 Returns (git like) *diff*, as plain text. Shows changes introduced by
581 ``rev2`` since ``rev1``.
581 ``rev2`` since ``rev1``.
582
582
583 :param rev1: Entry point from which diff is shown. Can be
583 :param rev1: Entry point from which diff is shown. Can be
584 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
584 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
585 the changes since empty state of the repository until ``rev2``
585 the changes since empty state of the repository until ``rev2``
586 :param rev2: Until which revision changes should be shown.
586 :param rev2: Until which revision changes should be shown.
587 :param ignore_whitespace: If set to ``True``, would not show whitespace
587 :param ignore_whitespace: If set to ``True``, would not show whitespace
588 changes. Defaults to ``False``.
588 changes. Defaults to ``False``.
589 :param context: How many lines before/after changed lines should be
589 :param context: How many lines before/after changed lines should be
590 shown. Defaults to ``3``.
590 shown. Defaults to ``3``. Due to limitations in Git, if
591 value passed-in is greater than ``2**31-1``
592 (``2147483647``), it will be set to ``2147483647``
593 instead. If negative value is passed-in, it will be set to
594 ``0`` instead.
591 """
595 """
596
597 # Git internally uses a signed long int for storing context
598 # size (number of lines to show before and after the
599 # differences). This can result in integer overflow, so we
600 # ensure the requested context is smaller by one than the
601 # number that would cause the overflow. It is highly unlikely
602 # that a single file will contain that many lines, so this
603 # kind of change should not cause any realistic consequences.
604 overflowed_long_int = 2**31
605
606 if context >= overflowed_long_int:
607 context = overflowed_long_int-1
608
609 # Negative context values make no sense, and will result in
610 # errors. Ensure this does not happen.
611 if context < 0:
612 context = 0
613
592 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
614 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
593 if ignore_whitespace:
615 if ignore_whitespace:
594 flags.append('-w')
616 flags.append('-w')
595
617
596 if hasattr(rev1, 'raw_id'):
618 if hasattr(rev1, 'raw_id'):
597 rev1 = getattr(rev1, 'raw_id')
619 rev1 = getattr(rev1, 'raw_id')
598
620
599 if hasattr(rev2, 'raw_id'):
621 if hasattr(rev2, 'raw_id'):
600 rev2 = getattr(rev2, 'raw_id')
622 rev2 = getattr(rev2, 'raw_id')
601
623
602 if rev1 == self.EMPTY_CHANGESET:
624 if rev1 == self.EMPTY_CHANGESET:
603 rev2 = self.get_changeset(rev2).raw_id
625 rev2 = self.get_changeset(rev2).raw_id
604 cmd = ['show'] + flags + [rev2]
626 cmd = ['show'] + flags + [rev2]
605 else:
627 else:
606 rev1 = self.get_changeset(rev1).raw_id
628 rev1 = self.get_changeset(rev1).raw_id
607 rev2 = self.get_changeset(rev2).raw_id
629 rev2 = self.get_changeset(rev2).raw_id
608 cmd = ['diff'] + flags + [rev1, rev2]
630 cmd = ['diff'] + flags + [rev1, rev2]
609
631
610 if path:
632 if path:
611 cmd += ['--', path]
633 cmd += ['--', path]
612
634
613 stdout, stderr = self.run_git_command(cmd)
635 stdout, stderr = self.run_git_command(cmd)
614 # TODO: don't ignore stderr
636 # TODO: don't ignore stderr
615 # If we used 'show' command, strip first few lines (until actual diff
637 # If we used 'show' command, strip first few lines (until actual diff
616 # starts)
638 # starts)
617 if rev1 == self.EMPTY_CHANGESET:
639 if rev1 == self.EMPTY_CHANGESET:
618 parts = stdout.split('\ndiff ', 1)
640 parts = stdout.split('\ndiff ', 1)
619 if len(parts) > 1:
641 if len(parts) > 1:
620 stdout = 'diff ' + parts[1]
642 stdout = 'diff ' + parts[1]
621 return stdout
643 return stdout
622
644
623 @LazyProperty
645 @LazyProperty
624 def in_memory_changeset(self):
646 def in_memory_changeset(self):
625 """
647 """
626 Returns ``GitInMemoryChangeset`` object for this repository.
648 Returns ``GitInMemoryChangeset`` object for this repository.
627 """
649 """
628 return GitInMemoryChangeset(self)
650 return GitInMemoryChangeset(self)
629
651
630 def clone(self, url, update_after_clone=True, bare=False):
652 def clone(self, url, update_after_clone=True, bare=False):
631 """
653 """
632 Tries to clone changes from external location.
654 Tries to clone changes from external location.
633
655
634 :param update_after_clone: If set to ``False``, git won't checkout
656 :param update_after_clone: If set to ``False``, git won't checkout
635 working directory
657 working directory
636 :param bare: If set to ``True``, repository would be cloned into
658 :param bare: If set to ``True``, repository would be cloned into
637 *bare* git repository (no working directory at all).
659 *bare* git repository (no working directory at all).
638 """
660 """
639 url = self._get_url(url)
661 url = self._get_url(url)
640 cmd = ['clone', '-q']
662 cmd = ['clone', '-q']
641 if bare:
663 if bare:
642 cmd.append('--bare')
664 cmd.append('--bare')
643 elif not update_after_clone:
665 elif not update_after_clone:
644 cmd.append('--no-checkout')
666 cmd.append('--no-checkout')
645 cmd += ['--', url, self.path]
667 cmd += ['--', url, self.path]
646 # If error occurs run_git_command raises RepositoryError already
668 # If error occurs run_git_command raises RepositoryError already
647 self.run_git_command(cmd)
669 self.run_git_command(cmd)
648
670
649 def pull(self, url):
671 def pull(self, url):
650 """
672 """
651 Tries to pull changes from external location.
673 Tries to pull changes from external location.
652 """
674 """
653 url = self._get_url(url)
675 url = self._get_url(url)
654 cmd = ['pull', '--ff-only', url]
676 cmd = ['pull', '--ff-only', url]
655 # If error occurs run_git_command raises RepositoryError already
677 # If error occurs run_git_command raises RepositoryError already
656 self.run_git_command(cmd)
678 self.run_git_command(cmd)
657
679
658 def fetch(self, url):
680 def fetch(self, url):
659 """
681 """
660 Tries to pull changes from external location.
682 Tries to pull changes from external location.
661 """
683 """
662 url = self._get_url(url)
684 url = self._get_url(url)
663 so, se = self.run_git_command(['ls-remote', '-h', url])
685 so, se = self.run_git_command(['ls-remote', '-h', url])
664 cmd = ['fetch', url, '--']
686 cmd = ['fetch', url, '--']
665 for line in (x for x in so.splitlines()):
687 for line in (x for x in so.splitlines()):
666 sha, ref = line.split('\t')
688 sha, ref = line.split('\t')
667 cmd.append('+%s:%s' % (ref, ref))
689 cmd.append('+%s:%s' % (ref, ref))
668 self.run_git_command(cmd)
690 self.run_git_command(cmd)
669
691
670 def _update_server_info(self):
692 def _update_server_info(self):
671 """
693 """
672 runs gits update-server-info command in this repo instance
694 runs gits update-server-info command in this repo instance
673 """
695 """
674 from dulwich.server import update_server_info
696 from dulwich.server import update_server_info
675 try:
697 try:
676 update_server_info(self._repo)
698 update_server_info(self._repo)
677 except OSError as e:
699 except OSError as e:
678 if e.errno not in [errno.ENOENT, errno.EROFS]:
700 if e.errno not in [errno.ENOENT, errno.EROFS]:
679 raise
701 raise
680 # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock
702 # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock
681 log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e)
703 log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e)
682
704
683 @LazyProperty
705 @LazyProperty
684 def workdir(self):
706 def workdir(self):
685 """
707 """
686 Returns ``Workdir`` instance for this repository.
708 Returns ``Workdir`` instance for this repository.
687 """
709 """
688 return GitWorkdir(self)
710 return GitWorkdir(self)
689
711
690 def get_config_value(self, section, name, config_file=None):
712 def get_config_value(self, section, name, config_file=None):
691 """
713 """
692 Returns configuration value for a given [``section``] and ``name``.
714 Returns configuration value for a given [``section``] and ``name``.
693
715
694 :param section: Section we want to retrieve value from
716 :param section: Section we want to retrieve value from
695 :param name: Name of configuration we want to retrieve
717 :param name: Name of configuration we want to retrieve
696 :param config_file: A path to file which should be used to retrieve
718 :param config_file: A path to file which should be used to retrieve
697 configuration from (might also be a list of file paths)
719 configuration from (might also be a list of file paths)
698 """
720 """
699 if config_file is None:
721 if config_file is None:
700 config_file = []
722 config_file = []
701 elif isinstance(config_file, basestring):
723 elif isinstance(config_file, basestring):
702 config_file = [config_file]
724 config_file = [config_file]
703
725
704 def gen_configs():
726 def gen_configs():
705 for path in config_file + self._config_files:
727 for path in config_file + self._config_files:
706 try:
728 try:
707 yield ConfigFile.from_path(path)
729 yield ConfigFile.from_path(path)
708 except (IOError, OSError, ValueError):
730 except (IOError, OSError, ValueError):
709 continue
731 continue
710
732
711 for config in gen_configs():
733 for config in gen_configs():
712 try:
734 try:
713 return config.get(section, name)
735 return config.get(section, name)
714 except KeyError:
736 except KeyError:
715 continue
737 continue
716 return None
738 return None
717
739
718 def get_user_name(self, config_file=None):
740 def get_user_name(self, config_file=None):
719 """
741 """
720 Returns user's name from global configuration file.
742 Returns user's name from global configuration file.
721
743
722 :param config_file: A path to file which should be used to retrieve
744 :param config_file: A path to file which should be used to retrieve
723 configuration from (might also be a list of file paths)
745 configuration from (might also be a list of file paths)
724 """
746 """
725 return self.get_config_value('user', 'name', config_file)
747 return self.get_config_value('user', 'name', config_file)
726
748
727 def get_user_email(self, config_file=None):
749 def get_user_email(self, config_file=None):
728 """
750 """
729 Returns user's email from global configuration file.
751 Returns user's email from global configuration file.
730
752
731 :param config_file: A path to file which should be used to retrieve
753 :param config_file: A path to file which should be used to retrieve
732 configuration from (might also be a list of file paths)
754 configuration from (might also be a list of file paths)
733 """
755 """
734 return self.get_config_value('user', 'email', config_file)
756 return self.get_config_value('user', 'email', config_file)
@@ -1,620 +1,627 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 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
19 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
20
20
21 from kallithea.lib.vcs.exceptions import (
21 from kallithea.lib.vcs.exceptions import (
22 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
22 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
23 RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
23 RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
24 )
24 )
25 from kallithea.lib.vcs.utils import (
25 from kallithea.lib.vcs.utils import (
26 author_email, author_name, date_fromtimestamp, makedate, safe_unicode, safe_str,
26 author_email, author_name, date_fromtimestamp, makedate, safe_unicode, safe_str,
27 )
27 )
28 from kallithea.lib.vcs.utils.lazy import LazyProperty
28 from kallithea.lib.vcs.utils.lazy import LazyProperty
29 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
29 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
30 from kallithea.lib.vcs.utils.paths import abspath
30 from kallithea.lib.vcs.utils.paths import abspath
31 from kallithea.lib.vcs.utils.hgcompat import (
31 from kallithea.lib.vcs.utils.hgcompat import (
32 ui, nullid, match, patch, diffopts, clone, get_contact,
32 ui, nullid, match, patch, diffopts, clone, get_contact,
33 localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
33 localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
34 httpbasicauthhandler, httpdigestauthhandler, peer, httppeer, sshpeer
34 httpbasicauthhandler, httpdigestauthhandler, peer, httppeer, sshpeer
35 )
35 )
36
36
37 from .changeset import MercurialChangeset
37 from .changeset import MercurialChangeset
38 from .inmemory import MercurialInMemoryChangeset
38 from .inmemory import MercurialInMemoryChangeset
39 from .workdir import MercurialWorkdir
39 from .workdir import MercurialWorkdir
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class MercurialRepository(BaseRepository):
44 class MercurialRepository(BaseRepository):
45 """
45 """
46 Mercurial repository backend
46 Mercurial repository backend
47 """
47 """
48 DEFAULT_BRANCH_NAME = 'default'
48 DEFAULT_BRANCH_NAME = 'default'
49 scm = 'hg'
49 scm = 'hg'
50
50
51 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
51 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
52 update_after_clone=False):
52 update_after_clone=False):
53 """
53 """
54 Raises RepositoryError if repository could not be find at the given
54 Raises RepositoryError if repository could not be find at the given
55 ``repo_path``.
55 ``repo_path``.
56
56
57 :param repo_path: local path of the repository
57 :param repo_path: local path of the repository
58 :param create=False: if set to True, would try to create repository if
58 :param create=False: if set to True, would try to create repository if
59 it does not exist rather than raising exception
59 it does not exist rather than raising exception
60 :param baseui=None: user data
60 :param baseui=None: user data
61 :param src_url=None: would try to clone repository from given location
61 :param src_url=None: would try to clone repository from given location
62 :param update_after_clone=False: sets update of working copy after
62 :param update_after_clone=False: sets update of working copy after
63 making a clone
63 making a clone
64 """
64 """
65
65
66 if not isinstance(repo_path, str):
66 if not isinstance(repo_path, str):
67 raise VCSError('Mercurial backend requires repository path to '
67 raise VCSError('Mercurial backend requires repository path to '
68 'be instance of <str> got %s instead' %
68 'be instance of <str> got %s instead' %
69 type(repo_path))
69 type(repo_path))
70
70
71 self.path = abspath(repo_path)
71 self.path = abspath(repo_path)
72 self.baseui = baseui or ui.ui()
72 self.baseui = baseui or ui.ui()
73 # We've set path and ui, now we can set _repo itself
73 # We've set path and ui, now we can set _repo itself
74 self._repo = self._get_repo(create, src_url, update_after_clone)
74 self._repo = self._get_repo(create, src_url, update_after_clone)
75
75
76 @property
76 @property
77 def _empty(self):
77 def _empty(self):
78 """
78 """
79 Checks if repository is empty ie. without any changesets
79 Checks if repository is empty ie. without any changesets
80 """
80 """
81 # TODO: Following raises errors when using InMemoryChangeset...
81 # TODO: Following raises errors when using InMemoryChangeset...
82 # return len(self._repo.changelog) == 0
82 # return len(self._repo.changelog) == 0
83 return len(self.revisions) == 0
83 return len(self.revisions) == 0
84
84
85 @LazyProperty
85 @LazyProperty
86 def revisions(self):
86 def revisions(self):
87 """
87 """
88 Returns list of revisions' ids, in ascending order. Being lazy
88 Returns list of revisions' ids, in ascending order. Being lazy
89 attribute allows external tools to inject shas from cache.
89 attribute allows external tools to inject shas from cache.
90 """
90 """
91 return self._get_all_revisions()
91 return self._get_all_revisions()
92
92
93 @LazyProperty
93 @LazyProperty
94 def name(self):
94 def name(self):
95 return os.path.basename(self.path)
95 return os.path.basename(self.path)
96
96
97 @LazyProperty
97 @LazyProperty
98 def branches(self):
98 def branches(self):
99 return self._get_branches()
99 return self._get_branches()
100
100
101 @LazyProperty
101 @LazyProperty
102 def closed_branches(self):
102 def closed_branches(self):
103 return self._get_branches(normal=False, closed=True)
103 return self._get_branches(normal=False, closed=True)
104
104
105 @LazyProperty
105 @LazyProperty
106 def allbranches(self):
106 def allbranches(self):
107 """
107 """
108 List all branches, including closed branches.
108 List all branches, including closed branches.
109 """
109 """
110 return self._get_branches(closed=True)
110 return self._get_branches(closed=True)
111
111
112 def _get_branches(self, normal=True, closed=False):
112 def _get_branches(self, normal=True, closed=False):
113 """
113 """
114 Gets branches for this repository
114 Gets branches for this repository
115 Returns only not closed branches by default
115 Returns only not closed branches by default
116
116
117 :param closed: return also closed branches for mercurial
117 :param closed: return also closed branches for mercurial
118 :param normal: return also normal branches
118 :param normal: return also normal branches
119 """
119 """
120
120
121 if self._empty:
121 if self._empty:
122 return {}
122 return {}
123
123
124 bt = OrderedDict()
124 bt = OrderedDict()
125 for bn, _heads, tip, isclosed in sorted(self._repo.branchmap().iterbranches()):
125 for bn, _heads, tip, isclosed in sorted(self._repo.branchmap().iterbranches()):
126 if isclosed:
126 if isclosed:
127 if closed:
127 if closed:
128 bt[safe_unicode(bn)] = hex(tip)
128 bt[safe_unicode(bn)] = hex(tip)
129 else:
129 else:
130 if normal:
130 if normal:
131 bt[safe_unicode(bn)] = hex(tip)
131 bt[safe_unicode(bn)] = hex(tip)
132
132
133 return bt
133 return bt
134
134
135 @LazyProperty
135 @LazyProperty
136 def tags(self):
136 def tags(self):
137 """
137 """
138 Gets tags for this repository
138 Gets tags for this repository
139 """
139 """
140 return self._get_tags()
140 return self._get_tags()
141
141
142 def _get_tags(self):
142 def _get_tags(self):
143 if self._empty:
143 if self._empty:
144 return {}
144 return {}
145
145
146 sortkey = lambda ctx: ctx[0] # sort by name
146 sortkey = lambda ctx: ctx[0] # sort by name
147 _tags = [(safe_unicode(n), hex(h),) for n, h in
147 _tags = [(safe_unicode(n), hex(h),) for n, h in
148 self._repo.tags().items()]
148 self._repo.tags().items()]
149
149
150 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
150 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
151
151
152 def tag(self, name, user, revision=None, message=None, date=None,
152 def tag(self, name, user, revision=None, message=None, date=None,
153 **kwargs):
153 **kwargs):
154 """
154 """
155 Creates and returns a tag for the given ``revision``.
155 Creates and returns a tag for the given ``revision``.
156
156
157 :param name: name for new tag
157 :param name: name for new tag
158 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
158 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
159 :param revision: changeset id for which new tag would be created
159 :param revision: changeset id for which new tag would be created
160 :param message: message of the tag's commit
160 :param message: message of the tag's commit
161 :param date: date of tag's commit
161 :param date: date of tag's commit
162
162
163 :raises TagAlreadyExistError: if tag with same name already exists
163 :raises TagAlreadyExistError: if tag with same name already exists
164 """
164 """
165 if name in self.tags:
165 if name in self.tags:
166 raise TagAlreadyExistError("Tag %s already exists" % name)
166 raise TagAlreadyExistError("Tag %s already exists" % name)
167 changeset = self.get_changeset(revision)
167 changeset = self.get_changeset(revision)
168 local = kwargs.setdefault('local', False)
168 local = kwargs.setdefault('local', False)
169
169
170 if message is None:
170 if message is None:
171 message = "Added tag %s for changeset %s" % (name,
171 message = "Added tag %s for changeset %s" % (name,
172 changeset.short_id)
172 changeset.short_id)
173
173
174 if date is None:
174 if date is None:
175 date = datetime.datetime.now().ctime()
175 date = datetime.datetime.now().ctime()
176
176
177 try:
177 try:
178 self._repo.tag(name, changeset._ctx.node(), message, local, user,
178 self._repo.tag(name, changeset._ctx.node(), message, local, user,
179 date)
179 date)
180 except Abort as e:
180 except Abort as e:
181 raise RepositoryError(e.message)
181 raise RepositoryError(e.message)
182
182
183 # Reinitialize tags
183 # Reinitialize tags
184 self.tags = self._get_tags()
184 self.tags = self._get_tags()
185 tag_id = self.tags[name]
185 tag_id = self.tags[name]
186
186
187 return self.get_changeset(revision=tag_id)
187 return self.get_changeset(revision=tag_id)
188
188
189 def remove_tag(self, name, user, message=None, date=None):
189 def remove_tag(self, name, user, message=None, date=None):
190 """
190 """
191 Removes tag with the given ``name``.
191 Removes tag with the given ``name``.
192
192
193 :param name: name of the tag to be removed
193 :param name: name of the tag to be removed
194 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
194 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
195 :param message: message of the tag's removal commit
195 :param message: message of the tag's removal commit
196 :param date: date of tag's removal commit
196 :param date: date of tag's removal commit
197
197
198 :raises TagDoesNotExistError: if tag with given name does not exists
198 :raises TagDoesNotExistError: if tag with given name does not exists
199 """
199 """
200 if name not in self.tags:
200 if name not in self.tags:
201 raise TagDoesNotExistError("Tag %s does not exist" % name)
201 raise TagDoesNotExistError("Tag %s does not exist" % name)
202 if message is None:
202 if message is None:
203 message = "Removed tag %s" % name
203 message = "Removed tag %s" % name
204 if date is None:
204 if date is None:
205 date = datetime.datetime.now().ctime()
205 date = datetime.datetime.now().ctime()
206 local = False
206 local = False
207
207
208 try:
208 try:
209 self._repo.tag(name, nullid, message, local, user, date)
209 self._repo.tag(name, nullid, message, local, user, date)
210 self.tags = self._get_tags()
210 self.tags = self._get_tags()
211 except Abort as e:
211 except Abort as e:
212 raise RepositoryError(e.message)
212 raise RepositoryError(e.message)
213
213
214 @LazyProperty
214 @LazyProperty
215 def bookmarks(self):
215 def bookmarks(self):
216 """
216 """
217 Gets bookmarks for this repository
217 Gets bookmarks for this repository
218 """
218 """
219 return self._get_bookmarks()
219 return self._get_bookmarks()
220
220
221 def _get_bookmarks(self):
221 def _get_bookmarks(self):
222 if self._empty:
222 if self._empty:
223 return {}
223 return {}
224
224
225 sortkey = lambda ctx: ctx[0] # sort by name
225 sortkey = lambda ctx: ctx[0] # sort by name
226 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
226 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
227 self._repo._bookmarks.items()]
227 self._repo._bookmarks.items()]
228 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
228 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
229
229
230 def _get_all_revisions(self):
230 def _get_all_revisions(self):
231
231
232 return [self._repo[x].hex() for x in self._repo.filtered('visible').changelog.revs()]
232 return [self._repo[x].hex() for x in self._repo.filtered('visible').changelog.revs()]
233
233
234 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
234 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
235 context=3):
235 context=3):
236 """
236 """
237 Returns (git like) *diff*, as plain text. Shows changes introduced by
237 Returns (git like) *diff*, as plain text. Shows changes introduced by
238 ``rev2`` since ``rev1``.
238 ``rev2`` since ``rev1``.
239
239
240 :param rev1: Entry point from which diff is shown. Can be
240 :param rev1: Entry point from which diff is shown. Can be
241 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
241 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
242 the changes since empty state of the repository until ``rev2``
242 the changes since empty state of the repository until ``rev2``
243 :param rev2: Until which revision changes should be shown.
243 :param rev2: Until which revision changes should be shown.
244 :param ignore_whitespace: If set to ``True``, would not show whitespace
244 :param ignore_whitespace: If set to ``True``, would not show whitespace
245 changes. Defaults to ``False``.
245 changes. Defaults to ``False``.
246 :param context: How many lines before/after changed lines should be
246 :param context: How many lines before/after changed lines should be
247 shown. Defaults to ``3``.
247 shown. Defaults to ``3``. If negative value is passed-in, it will be
248 set to ``0`` instead.
248 """
249 """
250
251 # Negative context values make no sense, and will result in
252 # errors. Ensure this does not happen.
253 if context < 0:
254 context = 0
255
249 if hasattr(rev1, 'raw_id'):
256 if hasattr(rev1, 'raw_id'):
250 rev1 = getattr(rev1, 'raw_id')
257 rev1 = getattr(rev1, 'raw_id')
251
258
252 if hasattr(rev2, 'raw_id'):
259 if hasattr(rev2, 'raw_id'):
253 rev2 = getattr(rev2, 'raw_id')
260 rev2 = getattr(rev2, 'raw_id')
254
261
255 # Check if given revisions are present at repository (may raise
262 # Check if given revisions are present at repository (may raise
256 # ChangesetDoesNotExistError)
263 # ChangesetDoesNotExistError)
257 if rev1 != self.EMPTY_CHANGESET:
264 if rev1 != self.EMPTY_CHANGESET:
258 self.get_changeset(rev1)
265 self.get_changeset(rev1)
259 self.get_changeset(rev2)
266 self.get_changeset(rev2)
260 if path:
267 if path:
261 file_filter = match(self.path, '', [path])
268 file_filter = match(self.path, '', [path])
262 else:
269 else:
263 file_filter = None
270 file_filter = None
264
271
265 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
272 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
266 opts=diffopts(git=True,
273 opts=diffopts(git=True,
267 showfunc=True,
274 showfunc=True,
268 ignorews=ignore_whitespace,
275 ignorews=ignore_whitespace,
269 context=context)))
276 context=context)))
270
277
271 @classmethod
278 @classmethod
272 def _check_url(cls, url, repoui=None):
279 def _check_url(cls, url, repoui=None):
273 """
280 """
274 Function will check given url and try to verify if it's a valid
281 Function will check given url and try to verify if it's a valid
275 link. Sometimes it may happened that mercurial will issue basic
282 link. Sometimes it may happened that mercurial will issue basic
276 auth request that can cause whole API to hang when used from python
283 auth request that can cause whole API to hang when used from python
277 or other external calls.
284 or other external calls.
278
285
279 On failures it'll raise urllib2.HTTPError, exception is also thrown
286 On failures it'll raise urllib2.HTTPError, exception is also thrown
280 when the return code is non 200
287 when the return code is non 200
281 """
288 """
282 # check first if it's not an local url
289 # check first if it's not an local url
283 if os.path.isdir(url) or url.startswith('file:'):
290 if os.path.isdir(url) or url.startswith('file:'):
284 return True
291 return True
285
292
286 if url.startswith('ssh:'):
293 if url.startswith('ssh:'):
287 # in case of invalid uri or authentication issues, sshpeer will
294 # in case of invalid uri or authentication issues, sshpeer will
288 # throw an exception.
295 # throw an exception.
289 sshpeer(repoui or ui.ui(), url).lookup('tip')
296 sshpeer(repoui or ui.ui(), url).lookup('tip')
290 return True
297 return True
291
298
292 url_prefix = None
299 url_prefix = None
293 if '+' in url[:url.find('://')]:
300 if '+' in url[:url.find('://')]:
294 url_prefix, url = url.split('+', 1)
301 url_prefix, url = url.split('+', 1)
295
302
296 handlers = []
303 handlers = []
297 url_obj = hg_url(url)
304 url_obj = hg_url(url)
298 test_uri, authinfo = url_obj.authinfo()
305 test_uri, authinfo = url_obj.authinfo()
299 url_obj.passwd = '*****'
306 url_obj.passwd = '*****'
300 cleaned_uri = str(url_obj)
307 cleaned_uri = str(url_obj)
301
308
302 if authinfo:
309 if authinfo:
303 #create a password manager
310 #create a password manager
304 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
311 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
305 passmgr.add_password(*authinfo)
312 passmgr.add_password(*authinfo)
306
313
307 handlers.extend((httpbasicauthhandler(passmgr),
314 handlers.extend((httpbasicauthhandler(passmgr),
308 httpdigestauthhandler(passmgr)))
315 httpdigestauthhandler(passmgr)))
309
316
310 o = urllib2.build_opener(*handlers)
317 o = urllib2.build_opener(*handlers)
311 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
318 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
312 ('Accept', 'application/mercurial-0.1')]
319 ('Accept', 'application/mercurial-0.1')]
313
320
314 q = {"cmd": 'between'}
321 q = {"cmd": 'between'}
315 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
322 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
316 qs = '?%s' % urllib.urlencode(q)
323 qs = '?%s' % urllib.urlencode(q)
317 cu = "%s%s" % (test_uri, qs)
324 cu = "%s%s" % (test_uri, qs)
318 req = urllib2.Request(cu, None, {})
325 req = urllib2.Request(cu, None, {})
319
326
320 try:
327 try:
321 resp = o.open(req)
328 resp = o.open(req)
322 if resp.code != 200:
329 if resp.code != 200:
323 raise Exception('Return Code is not 200')
330 raise Exception('Return Code is not 200')
324 except Exception as e:
331 except Exception as e:
325 # means it cannot be cloned
332 # means it cannot be cloned
326 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
333 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
327
334
328 if not url_prefix: # skip svn+http://... (and git+... too)
335 if not url_prefix: # skip svn+http://... (and git+... too)
329 # now check if it's a proper hg repo
336 # now check if it's a proper hg repo
330 try:
337 try:
331 httppeer(repoui or ui.ui(), url).lookup('tip')
338 httppeer(repoui or ui.ui(), url).lookup('tip')
332 except Exception as e:
339 except Exception as e:
333 raise urllib2.URLError(
340 raise urllib2.URLError(
334 "url [%s] does not look like an hg repo org_exc: %s"
341 "url [%s] does not look like an hg repo org_exc: %s"
335 % (cleaned_uri, e))
342 % (cleaned_uri, e))
336
343
337 return True
344 return True
338
345
339 def _get_repo(self, create, src_url=None, update_after_clone=False):
346 def _get_repo(self, create, src_url=None, update_after_clone=False):
340 """
347 """
341 Function will check for mercurial repository in given path and return
348 Function will check for mercurial repository in given path and return
342 a localrepo object. If there is no repository in that path it will
349 a localrepo object. If there is no repository in that path it will
343 raise an exception unless ``create`` parameter is set to True - in
350 raise an exception unless ``create`` parameter is set to True - in
344 that case repository would be created and returned.
351 that case repository would be created and returned.
345 If ``src_url`` is given, would try to clone repository from the
352 If ``src_url`` is given, would try to clone repository from the
346 location at given clone_point. Additionally it'll make update to
353 location at given clone_point. Additionally it'll make update to
347 working copy accordingly to ``update_after_clone`` flag
354 working copy accordingly to ``update_after_clone`` flag
348 """
355 """
349
356
350 try:
357 try:
351 if src_url:
358 if src_url:
352 url = str(self._get_url(src_url))
359 url = str(self._get_url(src_url))
353 opts = {}
360 opts = {}
354 if not update_after_clone:
361 if not update_after_clone:
355 opts.update({'noupdate': True})
362 opts.update({'noupdate': True})
356 MercurialRepository._check_url(url, self.baseui)
363 MercurialRepository._check_url(url, self.baseui)
357 clone(self.baseui, url, self.path, **opts)
364 clone(self.baseui, url, self.path, **opts)
358
365
359 # Don't try to create if we've already cloned repo
366 # Don't try to create if we've already cloned repo
360 create = False
367 create = False
361 return localrepository(self.baseui, self.path, create=create)
368 return localrepository(self.baseui, self.path, create=create)
362 except (Abort, RepoError) as err:
369 except (Abort, RepoError) as err:
363 if create:
370 if create:
364 msg = "Cannot create repository at %s. Original error was %s"\
371 msg = "Cannot create repository at %s. Original error was %s"\
365 % (self.path, err)
372 % (self.path, err)
366 else:
373 else:
367 msg = "Not valid repository at %s. Original error was %s"\
374 msg = "Not valid repository at %s. Original error was %s"\
368 % (self.path, err)
375 % (self.path, err)
369 raise RepositoryError(msg)
376 raise RepositoryError(msg)
370
377
371 @LazyProperty
378 @LazyProperty
372 def in_memory_changeset(self):
379 def in_memory_changeset(self):
373 return MercurialInMemoryChangeset(self)
380 return MercurialInMemoryChangeset(self)
374
381
375 @LazyProperty
382 @LazyProperty
376 def description(self):
383 def description(self):
377 undefined_description = u'unknown'
384 undefined_description = u'unknown'
378 _desc = self._repo.ui.config('web', 'description', None, untrusted=True)
385 _desc = self._repo.ui.config('web', 'description', None, untrusted=True)
379 return safe_unicode(_desc or undefined_description)
386 return safe_unicode(_desc or undefined_description)
380
387
381 @LazyProperty
388 @LazyProperty
382 def contact(self):
389 def contact(self):
383 undefined_contact = u'Unknown'
390 undefined_contact = u'Unknown'
384 return safe_unicode(get_contact(self._repo.ui.config)
391 return safe_unicode(get_contact(self._repo.ui.config)
385 or undefined_contact)
392 or undefined_contact)
386
393
387 @LazyProperty
394 @LazyProperty
388 def last_change(self):
395 def last_change(self):
389 """
396 """
390 Returns last change made on this repository as datetime object
397 Returns last change made on this repository as datetime object
391 """
398 """
392 return date_fromtimestamp(self._get_mtime(), makedate()[1])
399 return date_fromtimestamp(self._get_mtime(), makedate()[1])
393
400
394 def _get_mtime(self):
401 def _get_mtime(self):
395 try:
402 try:
396 return time.mktime(self.get_changeset().date.timetuple())
403 return time.mktime(self.get_changeset().date.timetuple())
397 except RepositoryError:
404 except RepositoryError:
398 #fallback to filesystem
405 #fallback to filesystem
399 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
406 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
400 st_path = os.path.join(self.path, '.hg', "store")
407 st_path = os.path.join(self.path, '.hg', "store")
401 if os.path.exists(cl_path):
408 if os.path.exists(cl_path):
402 return os.stat(cl_path).st_mtime
409 return os.stat(cl_path).st_mtime
403 else:
410 else:
404 return os.stat(st_path).st_mtime
411 return os.stat(st_path).st_mtime
405
412
406 def _get_revision(self, revision):
413 def _get_revision(self, revision):
407 """
414 """
408 Gets an ID revision given as str. This will always return a fill
415 Gets an ID revision given as str. This will always return a fill
409 40 char revision number
416 40 char revision number
410
417
411 :param revision: str or int or None
418 :param revision: str or int or None
412 """
419 """
413 if isinstance(revision, unicode):
420 if isinstance(revision, unicode):
414 revision = safe_str(revision)
421 revision = safe_str(revision)
415
422
416 if self._empty:
423 if self._empty:
417 raise EmptyRepositoryError("There are no changesets yet")
424 raise EmptyRepositoryError("There are no changesets yet")
418
425
419 if revision in [-1, 'tip', None]:
426 if revision in [-1, 'tip', None]:
420 revision = 'tip'
427 revision = 'tip'
421
428
422 try:
429 try:
423 revision = hex(self._repo.lookup(revision))
430 revision = hex(self._repo.lookup(revision))
424 except (LookupError, ):
431 except (LookupError, ):
425 msg = ("Ambiguous identifier `%s` for %s" % (revision, self))
432 msg = ("Ambiguous identifier `%s` for %s" % (revision, self))
426 raise ChangesetDoesNotExistError(msg)
433 raise ChangesetDoesNotExistError(msg)
427 except (IndexError, ValueError, RepoLookupError, TypeError):
434 except (IndexError, ValueError, RepoLookupError, TypeError):
428 msg = ("Revision %s does not exist for %s" % (revision, self))
435 msg = ("Revision %s does not exist for %s" % (revision, self))
429 raise ChangesetDoesNotExistError(msg)
436 raise ChangesetDoesNotExistError(msg)
430
437
431 return revision
438 return revision
432
439
433 def get_ref_revision(self, ref_type, ref_name):
440 def get_ref_revision(self, ref_type, ref_name):
434 """
441 """
435 Returns revision number for the given reference.
442 Returns revision number for the given reference.
436 """
443 """
437 ref_name = safe_str(ref_name)
444 ref_name = safe_str(ref_name)
438 if ref_type == 'rev' and not ref_name.strip('0'):
445 if ref_type == 'rev' and not ref_name.strip('0'):
439 return self.EMPTY_CHANGESET
446 return self.EMPTY_CHANGESET
440 # lookup up the exact node id
447 # lookup up the exact node id
441 _revset_predicates = {
448 _revset_predicates = {
442 'branch': 'branch',
449 'branch': 'branch',
443 'book': 'bookmark',
450 'book': 'bookmark',
444 'tag': 'tag',
451 'tag': 'tag',
445 'rev': 'id',
452 'rev': 'id',
446 }
453 }
447 # avoid expensive branch(x) iteration over whole repo
454 # avoid expensive branch(x) iteration over whole repo
448 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
455 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
449 try:
456 try:
450 revs = self._repo.revs(rev_spec, ref_name, ref_name)
457 revs = self._repo.revs(rev_spec, ref_name, ref_name)
451 except LookupError:
458 except LookupError:
452 msg = ("Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name))
459 msg = ("Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name))
453 raise ChangesetDoesNotExistError(msg)
460 raise ChangesetDoesNotExistError(msg)
454 except RepoLookupError:
461 except RepoLookupError:
455 msg = ("Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name))
462 msg = ("Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name))
456 raise ChangesetDoesNotExistError(msg)
463 raise ChangesetDoesNotExistError(msg)
457 if revs:
464 if revs:
458 try:
465 try:
459 revision = revs.last()
466 revision = revs.last()
460 except AttributeError:
467 except AttributeError:
461 # removed in hg 3.2
468 # removed in hg 3.2
462 revision = revs[-1]
469 revision = revs[-1]
463 else:
470 else:
464 # TODO: just report 'not found'?
471 # TODO: just report 'not found'?
465 revision = ref_name
472 revision = ref_name
466
473
467 return self._get_revision(revision)
474 return self._get_revision(revision)
468
475
469 def _get_archives(self, archive_name='tip'):
476 def _get_archives(self, archive_name='tip'):
470 allowed = self.baseui.configlist("web", "allow_archive",
477 allowed = self.baseui.configlist("web", "allow_archive",
471 untrusted=True)
478 untrusted=True)
472 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
479 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
473 if i[0] in allowed or self._repo.ui.configbool("web",
480 if i[0] in allowed or self._repo.ui.configbool("web",
474 "allow" + i[0],
481 "allow" + i[0],
475 untrusted=True):
482 untrusted=True):
476 yield {"type": i[0], "extension": i[1], "node": archive_name}
483 yield {"type": i[0], "extension": i[1], "node": archive_name}
477
484
478 def _get_url(self, url):
485 def _get_url(self, url):
479 """
486 """
480 Returns normalized url. If schema is not given, would fall
487 Returns normalized url. If schema is not given, would fall
481 to filesystem
488 to filesystem
482 (``file:///``) schema.
489 (``file:///``) schema.
483 """
490 """
484 url = str(url)
491 url = str(url)
485 if url != 'default' and not '://' in url:
492 if url != 'default' and not '://' in url:
486 url = "file:" + urllib.pathname2url(url)
493 url = "file:" + urllib.pathname2url(url)
487 return url
494 return url
488
495
489 def get_hook_location(self):
496 def get_hook_location(self):
490 """
497 """
491 returns absolute path to location where hooks are stored
498 returns absolute path to location where hooks are stored
492 """
499 """
493 return os.path.join(self.path, '.hg', '.hgrc')
500 return os.path.join(self.path, '.hg', '.hgrc')
494
501
495 def get_changeset(self, revision=None):
502 def get_changeset(self, revision=None):
496 """
503 """
497 Returns ``MercurialChangeset`` object representing repository's
504 Returns ``MercurialChangeset`` object representing repository's
498 changeset at the given ``revision``.
505 changeset at the given ``revision``.
499 """
506 """
500 revision = self._get_revision(revision)
507 revision = self._get_revision(revision)
501 changeset = MercurialChangeset(repository=self, revision=revision)
508 changeset = MercurialChangeset(repository=self, revision=revision)
502 return changeset
509 return changeset
503
510
504 def get_changesets(self, start=None, end=None, start_date=None,
511 def get_changesets(self, start=None, end=None, start_date=None,
505 end_date=None, branch_name=None, reverse=False):
512 end_date=None, branch_name=None, reverse=False):
506 """
513 """
507 Returns iterator of ``MercurialChangeset`` objects from start to end
514 Returns iterator of ``MercurialChangeset`` objects from start to end
508 (both are inclusive)
515 (both are inclusive)
509
516
510 :param start: None, str, int or mercurial lookup format
517 :param start: None, str, int or mercurial lookup format
511 :param end: None, str, int or mercurial lookup format
518 :param end: None, str, int or mercurial lookup format
512 :param start_date:
519 :param start_date:
513 :param end_date:
520 :param end_date:
514 :param branch_name:
521 :param branch_name:
515 :param reversed: return changesets in reversed order
522 :param reversed: return changesets in reversed order
516 """
523 """
517
524
518 start_raw_id = self._get_revision(start)
525 start_raw_id = self._get_revision(start)
519 start_pos = self.revisions.index(start_raw_id) if start else None
526 start_pos = self.revisions.index(start_raw_id) if start else None
520 end_raw_id = self._get_revision(end)
527 end_raw_id = self._get_revision(end)
521 end_pos = self.revisions.index(end_raw_id) if end else None
528 end_pos = self.revisions.index(end_raw_id) if end else None
522
529
523 if None not in [start, end] and start_pos > end_pos:
530 if None not in [start, end] and start_pos > end_pos:
524 raise RepositoryError("Start revision '%s' cannot be "
531 raise RepositoryError("Start revision '%s' cannot be "
525 "after end revision '%s'" % (start, end))
532 "after end revision '%s'" % (start, end))
526
533
527 if branch_name and branch_name not in self.allbranches.keys():
534 if branch_name and branch_name not in self.allbranches.keys():
528 msg = ("Branch %s not found in %s" % (branch_name, self))
535 msg = ("Branch %s not found in %s" % (branch_name, self))
529 raise BranchDoesNotExistError(msg)
536 raise BranchDoesNotExistError(msg)
530 if end_pos is not None:
537 if end_pos is not None:
531 end_pos += 1
538 end_pos += 1
532 #filter branches
539 #filter branches
533 filter_ = []
540 filter_ = []
534 if branch_name:
541 if branch_name:
535 filter_.append('branch("%s")' % (branch_name))
542 filter_.append('branch("%s")' % (branch_name))
536
543
537 if start_date and not end_date:
544 if start_date and not end_date:
538 filter_.append('date(">%s")' % start_date)
545 filter_.append('date(">%s")' % start_date)
539 if end_date and not start_date:
546 if end_date and not start_date:
540 filter_.append('date("<%s")' % end_date)
547 filter_.append('date("<%s")' % end_date)
541 if start_date and end_date:
548 if start_date and end_date:
542 filter_.append('date(">%s") and date("<%s")' % (start_date, end_date))
549 filter_.append('date(">%s") and date("<%s")' % (start_date, end_date))
543 if filter_:
550 if filter_:
544 revisions = scmutil.revrange(self._repo, filter_)
551 revisions = scmutil.revrange(self._repo, filter_)
545 else:
552 else:
546 revisions = self.revisions
553 revisions = self.revisions
547
554
548 # this is very much a hack to turn this into a list; a better solution
555 # this is very much a hack to turn this into a list; a better solution
549 # would be to get rid of this function entirely and use revsets
556 # would be to get rid of this function entirely and use revsets
550 revs = list(revisions)[start_pos:end_pos]
557 revs = list(revisions)[start_pos:end_pos]
551 if reverse:
558 if reverse:
552 revs = reversed(revs)
559 revs = reversed(revs)
553
560
554 return CollectionGenerator(self, revs)
561 return CollectionGenerator(self, revs)
555
562
556 def pull(self, url):
563 def pull(self, url):
557 """
564 """
558 Tries to pull changes from external location.
565 Tries to pull changes from external location.
559 """
566 """
560 url = self._get_url(url)
567 url = self._get_url(url)
561 other = peer(self._repo, {}, url)
568 other = peer(self._repo, {}, url)
562 try:
569 try:
563 # hg 3.2 moved push / pull to exchange module
570 # hg 3.2 moved push / pull to exchange module
564 from mercurial import exchange
571 from mercurial import exchange
565 exchange.pull(self._repo, other, heads=None, force=None)
572 exchange.pull(self._repo, other, heads=None, force=None)
566 except ImportError:
573 except ImportError:
567 self._repo.pull(other, heads=None, force=None)
574 self._repo.pull(other, heads=None, force=None)
568 except Abort as err:
575 except Abort as err:
569 # Propagate error but with vcs's type
576 # Propagate error but with vcs's type
570 raise RepositoryError(str(err))
577 raise RepositoryError(str(err))
571
578
572 @LazyProperty
579 @LazyProperty
573 def workdir(self):
580 def workdir(self):
574 """
581 """
575 Returns ``Workdir`` instance for this repository.
582 Returns ``Workdir`` instance for this repository.
576 """
583 """
577 return MercurialWorkdir(self)
584 return MercurialWorkdir(self)
578
585
579 def get_config_value(self, section, name=None, config_file=None):
586 def get_config_value(self, section, name=None, config_file=None):
580 """
587 """
581 Returns configuration value for a given [``section``] and ``name``.
588 Returns configuration value for a given [``section``] and ``name``.
582
589
583 :param section: Section we want to retrieve value from
590 :param section: Section we want to retrieve value from
584 :param name: Name of configuration we want to retrieve
591 :param name: Name of configuration we want to retrieve
585 :param config_file: A path to file which should be used to retrieve
592 :param config_file: A path to file which should be used to retrieve
586 configuration from (might also be a list of file paths)
593 configuration from (might also be a list of file paths)
587 """
594 """
588 if config_file is None:
595 if config_file is None:
589 config_file = []
596 config_file = []
590 elif isinstance(config_file, basestring):
597 elif isinstance(config_file, basestring):
591 config_file = [config_file]
598 config_file = [config_file]
592
599
593 config = self._repo.ui
600 config = self._repo.ui
594 for path in config_file:
601 for path in config_file:
595 config.readconfig(path)
602 config.readconfig(path)
596 return config.config(section, name)
603 return config.config(section, name)
597
604
598 def get_user_name(self, config_file=None):
605 def get_user_name(self, config_file=None):
599 """
606 """
600 Returns user's name from global configuration file.
607 Returns user's name from global configuration file.
601
608
602 :param config_file: A path to file which should be used to retrieve
609 :param config_file: A path to file which should be used to retrieve
603 configuration from (might also be a list of file paths)
610 configuration from (might also be a list of file paths)
604 """
611 """
605 username = self.get_config_value('ui', 'username')
612 username = self.get_config_value('ui', 'username')
606 if username:
613 if username:
607 return author_name(username)
614 return author_name(username)
608 return None
615 return None
609
616
610 def get_user_email(self, config_file=None):
617 def get_user_email(self, config_file=None):
611 """
618 """
612 Returns user's email from global configuration file.
619 Returns user's email from global configuration file.
613
620
614 :param config_file: A path to file which should be used to retrieve
621 :param config_file: A path to file which should be used to retrieve
615 configuration from (might also be a list of file paths)
622 configuration from (might also be a list of file paths)
616 """
623 """
617 username = self.get_config_value('ui', 'username')
624 username = self.get_config_value('ui', 'username')
618 if username:
625 if username:
619 return author_email(username)
626 return author_email(username)
620 return None
627 return None
@@ -1,850 +1,890 b''
1
1
2 import os
2 import os
3 import sys
3 import sys
4 import mock
4 import mock
5 import datetime
5 import datetime
6 import urllib2
6 import urllib2
7 from kallithea.lib.vcs.backends.git import GitRepository, GitChangeset
7 from kallithea.lib.vcs.backends.git import GitRepository, GitChangeset
8 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
8 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
9 from kallithea.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState
9 from kallithea.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState
10 from kallithea.lib.vcs.utils.compat import unittest
10 from kallithea.lib.vcs.utils.compat import unittest
11 from kallithea.model.scm import ScmModel
11 from kallithea.model.scm import ScmModel
12 from kallithea.tests.vcs.base import _BackendTestMixin
12 from kallithea.tests.vcs.base import _BackendTestMixin
13 from kallithea.tests.vcs.conf import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir
13 from kallithea.tests.vcs.conf import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir
14
14
15
15
16 class GitRepositoryTest(unittest.TestCase):
16 class GitRepositoryTest(unittest.TestCase):
17
17
18 def __check_for_existing_repo(self):
18 def __check_for_existing_repo(self):
19 if os.path.exists(TEST_GIT_REPO_CLONE):
19 if os.path.exists(TEST_GIT_REPO_CLONE):
20 self.fail('Cannot test git clone repo as location %s already '
20 self.fail('Cannot test git clone repo as location %s already '
21 'exists. You should manually remove it first.'
21 'exists. You should manually remove it first.'
22 % TEST_GIT_REPO_CLONE)
22 % TEST_GIT_REPO_CLONE)
23
23
24 def setUp(self):
24 def setUp(self):
25 self.repo = GitRepository(TEST_GIT_REPO)
25 self.repo = GitRepository(TEST_GIT_REPO)
26
26
27 def test_wrong_repo_path(self):
27 def test_wrong_repo_path(self):
28 wrong_repo_path = '/tmp/errorrepo'
28 wrong_repo_path = '/tmp/errorrepo'
29 self.assertRaises(RepositoryError, GitRepository, wrong_repo_path)
29 self.assertRaises(RepositoryError, GitRepository, wrong_repo_path)
30
30
31 def test_git_cmd_injection(self):
31 def test_git_cmd_injection(self):
32 repo_inject_path = TEST_GIT_REPO + '; echo "Cake";'
32 repo_inject_path = TEST_GIT_REPO + '; echo "Cake";'
33 with self.assertRaises(urllib2.URLError):
33 with self.assertRaises(urllib2.URLError):
34 # Should fail because URL will contain the parts after ; too
34 # Should fail because URL will contain the parts after ; too
35 urlerror_fail_repo = GitRepository(get_new_dir('injection-repo'), src_url=repo_inject_path, update_after_clone=True, create=True)
35 urlerror_fail_repo = GitRepository(get_new_dir('injection-repo'), src_url=repo_inject_path, update_after_clone=True, create=True)
36
36
37 with self.assertRaises(RepositoryError):
37 with self.assertRaises(RepositoryError):
38 # Should fail on direct clone call, which as of this writing does not happen outside of class
38 # Should fail on direct clone call, which as of this writing does not happen outside of class
39 clone_fail_repo = GitRepository(get_new_dir('injection-repo'), create=True)
39 clone_fail_repo = GitRepository(get_new_dir('injection-repo'), create=True)
40 clone_fail_repo.clone(repo_inject_path, update_after_clone=True,)
40 clone_fail_repo.clone(repo_inject_path, update_after_clone=True,)
41
41
42 # Verify correct quoting of evil characters that should work on posix file systems
42 # Verify correct quoting of evil characters that should work on posix file systems
43 if sys.platform == 'win32':
43 if sys.platform == 'win32':
44 # windows does not allow '"' in dir names
44 # windows does not allow '"' in dir names
45 tricky_path = get_new_dir("tricky-path-repo-$'`")
45 tricky_path = get_new_dir("tricky-path-repo-$'`")
46 else:
46 else:
47 tricky_path = get_new_dir("tricky-path-repo-$'\"`")
47 tricky_path = get_new_dir("tricky-path-repo-$'\"`")
48 successfully_cloned = GitRepository(tricky_path, src_url=TEST_GIT_REPO, update_after_clone=True, create=True)
48 successfully_cloned = GitRepository(tricky_path, src_url=TEST_GIT_REPO, update_after_clone=True, create=True)
49 # Repo should have been created
49 # Repo should have been created
50 self.assertFalse(successfully_cloned._repo.bare)
50 self.assertFalse(successfully_cloned._repo.bare)
51
51
52 if sys.platform == 'win32':
52 if sys.platform == 'win32':
53 # windows does not allow '"' in dir names
53 # windows does not allow '"' in dir names
54 tricky_path_2 = get_new_dir("tricky-path-2-repo-$'`")
54 tricky_path_2 = get_new_dir("tricky-path-2-repo-$'`")
55 else:
55 else:
56 tricky_path_2 = get_new_dir("tricky-path-2-repo-$'\"`")
56 tricky_path_2 = get_new_dir("tricky-path-2-repo-$'\"`")
57 successfully_cloned2 = GitRepository(tricky_path_2, src_url=tricky_path, bare=True, create=True)
57 successfully_cloned2 = GitRepository(tricky_path_2, src_url=tricky_path, bare=True, create=True)
58 # Repo should have been created and thus used correct quoting for clone
58 # Repo should have been created and thus used correct quoting for clone
59 self.assertTrue(successfully_cloned2._repo.bare)
59 self.assertTrue(successfully_cloned2._repo.bare)
60
60
61 # Should pass because URL has been properly quoted
61 # Should pass because URL has been properly quoted
62 successfully_cloned.pull(tricky_path_2)
62 successfully_cloned.pull(tricky_path_2)
63 successfully_cloned2.fetch(tricky_path)
63 successfully_cloned2.fetch(tricky_path)
64
64
65 def test_repo_create_with_spaces_in_path(self):
65 def test_repo_create_with_spaces_in_path(self):
66 repo_path = get_new_dir("path with spaces")
66 repo_path = get_new_dir("path with spaces")
67 repo = GitRepository(repo_path, src_url=None, bare=True, create=True)
67 repo = GitRepository(repo_path, src_url=None, bare=True, create=True)
68 # Repo should have been created
68 # Repo should have been created
69 self.assertTrue(repo._repo.bare)
69 self.assertTrue(repo._repo.bare)
70
70
71 def test_repo_clone(self):
71 def test_repo_clone(self):
72 self.__check_for_existing_repo()
72 self.__check_for_existing_repo()
73 repo = GitRepository(TEST_GIT_REPO)
73 repo = GitRepository(TEST_GIT_REPO)
74 repo_clone = GitRepository(TEST_GIT_REPO_CLONE,
74 repo_clone = GitRepository(TEST_GIT_REPO_CLONE,
75 src_url=TEST_GIT_REPO, create=True, update_after_clone=True)
75 src_url=TEST_GIT_REPO, create=True, update_after_clone=True)
76 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
76 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
77 # Checking hashes of changesets should be enough
77 # Checking hashes of changesets should be enough
78 for changeset in repo.get_changesets():
78 for changeset in repo.get_changesets():
79 raw_id = changeset.raw_id
79 raw_id = changeset.raw_id
80 self.assertEqual(raw_id, repo_clone.get_changeset(raw_id).raw_id)
80 self.assertEqual(raw_id, repo_clone.get_changeset(raw_id).raw_id)
81
81
82 def test_repo_clone_with_spaces_in_path(self):
82 def test_repo_clone_with_spaces_in_path(self):
83 repo_path = get_new_dir("path with spaces")
83 repo_path = get_new_dir("path with spaces")
84 successfully_cloned = GitRepository(repo_path, src_url=TEST_GIT_REPO, update_after_clone=True, create=True)
84 successfully_cloned = GitRepository(repo_path, src_url=TEST_GIT_REPO, update_after_clone=True, create=True)
85 # Repo should have been created
85 # Repo should have been created
86 self.assertFalse(successfully_cloned._repo.bare)
86 self.assertFalse(successfully_cloned._repo.bare)
87
87
88 successfully_cloned.pull(TEST_GIT_REPO)
88 successfully_cloned.pull(TEST_GIT_REPO)
89 self.repo.fetch(repo_path)
89 self.repo.fetch(repo_path)
90
90
91 def test_repo_clone_without_create(self):
91 def test_repo_clone_without_create(self):
92 self.assertRaises(RepositoryError, GitRepository,
92 self.assertRaises(RepositoryError, GitRepository,
93 TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO)
93 TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO)
94
94
95 def test_repo_clone_with_update(self):
95 def test_repo_clone_with_update(self):
96 repo = GitRepository(TEST_GIT_REPO)
96 repo = GitRepository(TEST_GIT_REPO)
97 clone_path = TEST_GIT_REPO_CLONE + '_with_update'
97 clone_path = TEST_GIT_REPO_CLONE + '_with_update'
98 repo_clone = GitRepository(clone_path,
98 repo_clone = GitRepository(clone_path,
99 create=True, src_url=TEST_GIT_REPO, update_after_clone=True)
99 create=True, src_url=TEST_GIT_REPO, update_after_clone=True)
100 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
100 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
101
101
102 #check if current workdir was updated
102 #check if current workdir was updated
103 fpath = os.path.join(clone_path, 'MANIFEST.in')
103 fpath = os.path.join(clone_path, 'MANIFEST.in')
104 self.assertEqual(True, os.path.isfile(fpath),
104 self.assertEqual(True, os.path.isfile(fpath),
105 'Repo was cloned and updated but file %s could not be found'
105 'Repo was cloned and updated but file %s could not be found'
106 % fpath)
106 % fpath)
107
107
108 def test_repo_clone_without_update(self):
108 def test_repo_clone_without_update(self):
109 repo = GitRepository(TEST_GIT_REPO)
109 repo = GitRepository(TEST_GIT_REPO)
110 clone_path = TEST_GIT_REPO_CLONE + '_without_update'
110 clone_path = TEST_GIT_REPO_CLONE + '_without_update'
111 repo_clone = GitRepository(clone_path,
111 repo_clone = GitRepository(clone_path,
112 create=True, src_url=TEST_GIT_REPO, update_after_clone=False)
112 create=True, src_url=TEST_GIT_REPO, update_after_clone=False)
113 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
113 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
114 #check if current workdir was *NOT* updated
114 #check if current workdir was *NOT* updated
115 fpath = os.path.join(clone_path, 'MANIFEST.in')
115 fpath = os.path.join(clone_path, 'MANIFEST.in')
116 # Make sure it's not bare repo
116 # Make sure it's not bare repo
117 self.assertFalse(repo_clone._repo.bare)
117 self.assertFalse(repo_clone._repo.bare)
118 self.assertEqual(False, os.path.isfile(fpath),
118 self.assertEqual(False, os.path.isfile(fpath),
119 'Repo was cloned and updated but file %s was found'
119 'Repo was cloned and updated but file %s was found'
120 % fpath)
120 % fpath)
121
121
122 def test_repo_clone_into_bare_repo(self):
122 def test_repo_clone_into_bare_repo(self):
123 repo = GitRepository(TEST_GIT_REPO)
123 repo = GitRepository(TEST_GIT_REPO)
124 clone_path = TEST_GIT_REPO_CLONE + '_bare.git'
124 clone_path = TEST_GIT_REPO_CLONE + '_bare.git'
125 repo_clone = GitRepository(clone_path, create=True,
125 repo_clone = GitRepository(clone_path, create=True,
126 src_url=repo.path, bare=True)
126 src_url=repo.path, bare=True)
127 self.assertTrue(repo_clone._repo.bare)
127 self.assertTrue(repo_clone._repo.bare)
128
128
129 def test_create_repo_is_not_bare_by_default(self):
129 def test_create_repo_is_not_bare_by_default(self):
130 repo = GitRepository(get_new_dir('not-bare-by-default'), create=True)
130 repo = GitRepository(get_new_dir('not-bare-by-default'), create=True)
131 self.assertFalse(repo._repo.bare)
131 self.assertFalse(repo._repo.bare)
132
132
133 def test_create_bare_repo(self):
133 def test_create_bare_repo(self):
134 repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True)
134 repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True)
135 self.assertTrue(repo._repo.bare)
135 self.assertTrue(repo._repo.bare)
136
136
137 def test_revisions(self):
137 def test_revisions(self):
138 # there are 112 revisions (by now)
138 # there are 112 revisions (by now)
139 # so we can assume they would be available from now on
139 # so we can assume they would be available from now on
140 subset = set([
140 subset = set([
141 'c1214f7e79e02fc37156ff215cd71275450cffc3',
141 'c1214f7e79e02fc37156ff215cd71275450cffc3',
142 '38b5fe81f109cb111f549bfe9bb6b267e10bc557',
142 '38b5fe81f109cb111f549bfe9bb6b267e10bc557',
143 'fa6600f6848800641328adbf7811fd2372c02ab2',
143 'fa6600f6848800641328adbf7811fd2372c02ab2',
144 '102607b09cdd60e2793929c4f90478be29f85a17',
144 '102607b09cdd60e2793929c4f90478be29f85a17',
145 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
145 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
146 '2d1028c054665b962fa3d307adfc923ddd528038',
146 '2d1028c054665b962fa3d307adfc923ddd528038',
147 'd7e0d30fbcae12c90680eb095a4f5f02505ce501',
147 'd7e0d30fbcae12c90680eb095a4f5f02505ce501',
148 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
148 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
149 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
149 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
150 '8430a588b43b5d6da365400117c89400326e7992',
150 '8430a588b43b5d6da365400117c89400326e7992',
151 'd955cd312c17b02143c04fa1099a352b04368118',
151 'd955cd312c17b02143c04fa1099a352b04368118',
152 'f67b87e5c629c2ee0ba58f85197e423ff28d735b',
152 'f67b87e5c629c2ee0ba58f85197e423ff28d735b',
153 'add63e382e4aabc9e1afdc4bdc24506c269b7618',
153 'add63e382e4aabc9e1afdc4bdc24506c269b7618',
154 'f298fe1189f1b69779a4423f40b48edf92a703fc',
154 'f298fe1189f1b69779a4423f40b48edf92a703fc',
155 'bd9b619eb41994cac43d67cf4ccc8399c1125808',
155 'bd9b619eb41994cac43d67cf4ccc8399c1125808',
156 '6e125e7c890379446e98980d8ed60fba87d0f6d1',
156 '6e125e7c890379446e98980d8ed60fba87d0f6d1',
157 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd',
157 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd',
158 '0b05e4ed56c802098dfc813cbe779b2f49e92500',
158 '0b05e4ed56c802098dfc813cbe779b2f49e92500',
159 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
159 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
160 '45223f8f114c64bf4d6f853e3c35a369a6305520',
160 '45223f8f114c64bf4d6f853e3c35a369a6305520',
161 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
161 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
162 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68',
162 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68',
163 '27d48942240f5b91dfda77accd2caac94708cc7d',
163 '27d48942240f5b91dfda77accd2caac94708cc7d',
164 '622f0eb0bafd619d2560c26f80f09e3b0b0d78af',
164 '622f0eb0bafd619d2560c26f80f09e3b0b0d78af',
165 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'])
165 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'])
166 self.assertTrue(subset.issubset(set(self.repo.revisions)))
166 self.assertTrue(subset.issubset(set(self.repo.revisions)))
167
167
168
168
169
169
170 def test_slicing(self):
170 def test_slicing(self):
171 #4 1 5 10 95
171 #4 1 5 10 95
172 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
172 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
173 (10, 20, 10), (5, 100, 95)]:
173 (10, 20, 10), (5, 100, 95)]:
174 revs = list(self.repo[sfrom:sto])
174 revs = list(self.repo[sfrom:sto])
175 self.assertEqual(len(revs), size)
175 self.assertEqual(len(revs), size)
176 self.assertEqual(revs[0], self.repo.get_changeset(sfrom))
176 self.assertEqual(revs[0], self.repo.get_changeset(sfrom))
177 self.assertEqual(revs[-1], self.repo.get_changeset(sto - 1))
177 self.assertEqual(revs[-1], self.repo.get_changeset(sto - 1))
178
178
179
179
180 def test_branches(self):
180 def test_branches(self):
181 # TODO: Need more tests here
181 # TODO: Need more tests here
182 # Removed (those are 'remotes' branches for cloned repo)
182 # Removed (those are 'remotes' branches for cloned repo)
183 #self.assertTrue('master' in self.repo.branches)
183 #self.assertTrue('master' in self.repo.branches)
184 #self.assertTrue('gittree' in self.repo.branches)
184 #self.assertTrue('gittree' in self.repo.branches)
185 #self.assertTrue('web-branch' in self.repo.branches)
185 #self.assertTrue('web-branch' in self.repo.branches)
186 for name, id in self.repo.branches.items():
186 for name, id in self.repo.branches.items():
187 self.assertTrue(isinstance(
187 self.assertTrue(isinstance(
188 self.repo.get_changeset(id), GitChangeset))
188 self.repo.get_changeset(id), GitChangeset))
189
189
190 def test_tags(self):
190 def test_tags(self):
191 # TODO: Need more tests here
191 # TODO: Need more tests here
192 self.assertTrue('v0.1.1' in self.repo.tags)
192 self.assertTrue('v0.1.1' in self.repo.tags)
193 self.assertTrue('v0.1.2' in self.repo.tags)
193 self.assertTrue('v0.1.2' in self.repo.tags)
194 for name, id in self.repo.tags.items():
194 for name, id in self.repo.tags.items():
195 self.assertTrue(isinstance(
195 self.assertTrue(isinstance(
196 self.repo.get_changeset(id), GitChangeset))
196 self.repo.get_changeset(id), GitChangeset))
197
197
198 def _test_single_changeset_cache(self, revision):
198 def _test_single_changeset_cache(self, revision):
199 chset = self.repo.get_changeset(revision)
199 chset = self.repo.get_changeset(revision)
200 self.assertTrue(revision in self.repo.changesets)
200 self.assertTrue(revision in self.repo.changesets)
201 self.assertTrue(chset is self.repo.changesets[revision])
201 self.assertTrue(chset is self.repo.changesets[revision])
202
202
203 def test_initial_changeset(self):
203 def test_initial_changeset(self):
204 id = self.repo.revisions[0]
204 id = self.repo.revisions[0]
205 init_chset = self.repo.get_changeset(id)
205 init_chset = self.repo.get_changeset(id)
206 self.assertEqual(init_chset.message, 'initial import\n')
206 self.assertEqual(init_chset.message, 'initial import\n')
207 self.assertEqual(init_chset.author,
207 self.assertEqual(init_chset.author,
208 'Marcin Kuzminski <marcin@python-blog.com>')
208 'Marcin Kuzminski <marcin@python-blog.com>')
209 for path in ('vcs/__init__.py',
209 for path in ('vcs/__init__.py',
210 'vcs/backends/BaseRepository.py',
210 'vcs/backends/BaseRepository.py',
211 'vcs/backends/__init__.py'):
211 'vcs/backends/__init__.py'):
212 self.assertTrue(isinstance(init_chset.get_node(path), FileNode))
212 self.assertTrue(isinstance(init_chset.get_node(path), FileNode))
213 for path in ('', 'vcs', 'vcs/backends'):
213 for path in ('', 'vcs', 'vcs/backends'):
214 self.assertTrue(isinstance(init_chset.get_node(path), DirNode))
214 self.assertTrue(isinstance(init_chset.get_node(path), DirNode))
215
215
216 self.assertRaises(NodeDoesNotExistError, init_chset.get_node, path='foobar')
216 self.assertRaises(NodeDoesNotExistError, init_chset.get_node, path='foobar')
217
217
218 node = init_chset.get_node('vcs/')
218 node = init_chset.get_node('vcs/')
219 self.assertTrue(hasattr(node, 'kind'))
219 self.assertTrue(hasattr(node, 'kind'))
220 self.assertEqual(node.kind, NodeKind.DIR)
220 self.assertEqual(node.kind, NodeKind.DIR)
221
221
222 node = init_chset.get_node('vcs')
222 node = init_chset.get_node('vcs')
223 self.assertTrue(hasattr(node, 'kind'))
223 self.assertTrue(hasattr(node, 'kind'))
224 self.assertEqual(node.kind, NodeKind.DIR)
224 self.assertEqual(node.kind, NodeKind.DIR)
225
225
226 node = init_chset.get_node('vcs/__init__.py')
226 node = init_chset.get_node('vcs/__init__.py')
227 self.assertTrue(hasattr(node, 'kind'))
227 self.assertTrue(hasattr(node, 'kind'))
228 self.assertEqual(node.kind, NodeKind.FILE)
228 self.assertEqual(node.kind, NodeKind.FILE)
229
229
230 def test_not_existing_changeset(self):
230 def test_not_existing_changeset(self):
231 self.assertRaises(RepositoryError, self.repo.get_changeset,
231 self.assertRaises(RepositoryError, self.repo.get_changeset,
232 'f' * 40)
232 'f' * 40)
233
233
234 def test_changeset10(self):
234 def test_changeset10(self):
235
235
236 chset10 = self.repo.get_changeset(self.repo.revisions[9])
236 chset10 = self.repo.get_changeset(self.repo.revisions[9])
237 README = """===
237 README = """===
238 VCS
238 VCS
239 ===
239 ===
240
240
241 Various Version Control System management abstraction layer for Python.
241 Various Version Control System management abstraction layer for Python.
242
242
243 Introduction
243 Introduction
244 ------------
244 ------------
245
245
246 TODO: To be written...
246 TODO: To be written...
247
247
248 """
248 """
249 node = chset10.get_node('README.rst')
249 node = chset10.get_node('README.rst')
250 self.assertEqual(node.kind, NodeKind.FILE)
250 self.assertEqual(node.kind, NodeKind.FILE)
251 self.assertEqual(node.content, README)
251 self.assertEqual(node.content, README)
252
252
253
253
254 class GitChangesetTest(unittest.TestCase):
254 class GitChangesetTest(unittest.TestCase):
255
255
256 def setUp(self):
256 def setUp(self):
257 self.repo = GitRepository(TEST_GIT_REPO)
257 self.repo = GitRepository(TEST_GIT_REPO)
258
258
259 def test_default_changeset(self):
259 def test_default_changeset(self):
260 tip = self.repo.get_changeset()
260 tip = self.repo.get_changeset()
261 self.assertEqual(tip, self.repo.get_changeset(None))
261 self.assertEqual(tip, self.repo.get_changeset(None))
262 self.assertEqual(tip, self.repo.get_changeset('tip'))
262 self.assertEqual(tip, self.repo.get_changeset('tip'))
263
263
264 def test_root_node(self):
264 def test_root_node(self):
265 tip = self.repo.get_changeset()
265 tip = self.repo.get_changeset()
266 self.assertTrue(tip.root is tip.get_node(''))
266 self.assertTrue(tip.root is tip.get_node(''))
267
267
268 def test_lazy_fetch(self):
268 def test_lazy_fetch(self):
269 """
269 """
270 Test if changeset's nodes expands and are cached as we walk through
270 Test if changeset's nodes expands and are cached as we walk through
271 the revision. This test is somewhat hard to write as order of tests
271 the revision. This test is somewhat hard to write as order of tests
272 is a key here. Written by running command after command in a shell.
272 is a key here. Written by running command after command in a shell.
273 """
273 """
274 hex = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
274 hex = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
275 self.assertTrue(hex in self.repo.revisions)
275 self.assertTrue(hex in self.repo.revisions)
276 chset = self.repo.get_changeset(hex)
276 chset = self.repo.get_changeset(hex)
277 self.assertTrue(len(chset.nodes) == 0)
277 self.assertTrue(len(chset.nodes) == 0)
278 root = chset.root
278 root = chset.root
279 self.assertTrue(len(chset.nodes) == 1)
279 self.assertTrue(len(chset.nodes) == 1)
280 self.assertTrue(len(root.nodes) == 8)
280 self.assertTrue(len(root.nodes) == 8)
281 # accessing root.nodes updates chset.nodes
281 # accessing root.nodes updates chset.nodes
282 self.assertTrue(len(chset.nodes) == 9)
282 self.assertTrue(len(chset.nodes) == 9)
283
283
284 docs = root.get_node('docs')
284 docs = root.get_node('docs')
285 # we haven't yet accessed anything new as docs dir was already cached
285 # we haven't yet accessed anything new as docs dir was already cached
286 self.assertTrue(len(chset.nodes) == 9)
286 self.assertTrue(len(chset.nodes) == 9)
287 self.assertTrue(len(docs.nodes) == 8)
287 self.assertTrue(len(docs.nodes) == 8)
288 # accessing docs.nodes updates chset.nodes
288 # accessing docs.nodes updates chset.nodes
289 self.assertTrue(len(chset.nodes) == 17)
289 self.assertTrue(len(chset.nodes) == 17)
290
290
291 self.assertTrue(docs is chset.get_node('docs'))
291 self.assertTrue(docs is chset.get_node('docs'))
292 self.assertTrue(docs is root.nodes[0])
292 self.assertTrue(docs is root.nodes[0])
293 self.assertTrue(docs is root.dirs[0])
293 self.assertTrue(docs is root.dirs[0])
294 self.assertTrue(docs is chset.get_node('docs'))
294 self.assertTrue(docs is chset.get_node('docs'))
295
295
296 def test_nodes_with_changeset(self):
296 def test_nodes_with_changeset(self):
297 hex = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
297 hex = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
298 chset = self.repo.get_changeset(hex)
298 chset = self.repo.get_changeset(hex)
299 root = chset.root
299 root = chset.root
300 docs = root.get_node('docs')
300 docs = root.get_node('docs')
301 self.assertTrue(docs is chset.get_node('docs'))
301 self.assertTrue(docs is chset.get_node('docs'))
302 api = docs.get_node('api')
302 api = docs.get_node('api')
303 self.assertTrue(api is chset.get_node('docs/api'))
303 self.assertTrue(api is chset.get_node('docs/api'))
304 index = api.get_node('index.rst')
304 index = api.get_node('index.rst')
305 self.assertTrue(index is chset.get_node('docs/api/index.rst'))
305 self.assertTrue(index is chset.get_node('docs/api/index.rst'))
306 self.assertTrue(index is chset.get_node('docs')\
306 self.assertTrue(index is chset.get_node('docs')\
307 .get_node('api')\
307 .get_node('api')\
308 .get_node('index.rst'))
308 .get_node('index.rst'))
309
309
310 def test_branch_and_tags(self):
310 def test_branch_and_tags(self):
311 """
311 """
312 rev0 = self.repo.revisions[0]
312 rev0 = self.repo.revisions[0]
313 chset0 = self.repo.get_changeset(rev0)
313 chset0 = self.repo.get_changeset(rev0)
314 self.assertEqual(chset0.branch, 'master')
314 self.assertEqual(chset0.branch, 'master')
315 self.assertEqual(chset0.tags, [])
315 self.assertEqual(chset0.tags, [])
316
316
317 rev10 = self.repo.revisions[10]
317 rev10 = self.repo.revisions[10]
318 chset10 = self.repo.get_changeset(rev10)
318 chset10 = self.repo.get_changeset(rev10)
319 self.assertEqual(chset10.branch, 'master')
319 self.assertEqual(chset10.branch, 'master')
320 self.assertEqual(chset10.tags, [])
320 self.assertEqual(chset10.tags, [])
321
321
322 rev44 = self.repo.revisions[44]
322 rev44 = self.repo.revisions[44]
323 chset44 = self.repo.get_changeset(rev44)
323 chset44 = self.repo.get_changeset(rev44)
324 self.assertEqual(chset44.branch, 'web-branch')
324 self.assertEqual(chset44.branch, 'web-branch')
325
325
326 tip = self.repo.get_changeset('tip')
326 tip = self.repo.get_changeset('tip')
327 self.assertTrue('tip' in tip.tags)
327 self.assertTrue('tip' in tip.tags)
328 """
328 """
329 # Those tests would fail - branches are now going
329 # Those tests would fail - branches are now going
330 # to be changed at main API in order to support git backend
330 # to be changed at main API in order to support git backend
331 pass
331 pass
332
332
333 def _test_slices(self, limit, offset):
333 def _test_slices(self, limit, offset):
334 count = self.repo.count()
334 count = self.repo.count()
335 changesets = self.repo.get_changesets(limit=limit, offset=offset)
335 changesets = self.repo.get_changesets(limit=limit, offset=offset)
336 idx = 0
336 idx = 0
337 for changeset in changesets:
337 for changeset in changesets:
338 rev = offset + idx
338 rev = offset + idx
339 idx += 1
339 idx += 1
340 rev_id = self.repo.revisions[rev]
340 rev_id = self.repo.revisions[rev]
341 if idx > limit:
341 if idx > limit:
342 self.fail("Exceeded limit already (getting revision %s, "
342 self.fail("Exceeded limit already (getting revision %s, "
343 "there are %s total revisions, offset=%s, limit=%s)"
343 "there are %s total revisions, offset=%s, limit=%s)"
344 % (rev_id, count, offset, limit))
344 % (rev_id, count, offset, limit))
345 self.assertEqual(changeset, self.repo.get_changeset(rev_id))
345 self.assertEqual(changeset, self.repo.get_changeset(rev_id))
346 result = list(self.repo.get_changesets(limit=limit, offset=offset))
346 result = list(self.repo.get_changesets(limit=limit, offset=offset))
347 start = offset
347 start = offset
348 end = limit and offset + limit or None
348 end = limit and offset + limit or None
349 sliced = list(self.repo[start:end])
349 sliced = list(self.repo[start:end])
350 self.failUnlessEqual(result, sliced,
350 self.failUnlessEqual(result, sliced,
351 msg="Comparison failed for limit=%s, offset=%s"
351 msg="Comparison failed for limit=%s, offset=%s"
352 "(get_changeset returned: %s and sliced: %s"
352 "(get_changeset returned: %s and sliced: %s"
353 % (limit, offset, result, sliced))
353 % (limit, offset, result, sliced))
354
354
355 def _test_file_size(self, revision, path, size):
355 def _test_file_size(self, revision, path, size):
356 node = self.repo.get_changeset(revision).get_node(path)
356 node = self.repo.get_changeset(revision).get_node(path)
357 self.assertTrue(node.is_file())
357 self.assertTrue(node.is_file())
358 self.assertEqual(node.size, size)
358 self.assertEqual(node.size, size)
359
359
360 def test_file_size(self):
360 def test_file_size(self):
361 to_check = (
361 to_check = (
362 ('c1214f7e79e02fc37156ff215cd71275450cffc3',
362 ('c1214f7e79e02fc37156ff215cd71275450cffc3',
363 'vcs/backends/BaseRepository.py', 502),
363 'vcs/backends/BaseRepository.py', 502),
364 ('d7e0d30fbcae12c90680eb095a4f5f02505ce501',
364 ('d7e0d30fbcae12c90680eb095a4f5f02505ce501',
365 'vcs/backends/hg.py', 854),
365 'vcs/backends/hg.py', 854),
366 ('6e125e7c890379446e98980d8ed60fba87d0f6d1',
366 ('6e125e7c890379446e98980d8ed60fba87d0f6d1',
367 'setup.py', 1068),
367 'setup.py', 1068),
368
368
369 ('d955cd312c17b02143c04fa1099a352b04368118',
369 ('d955cd312c17b02143c04fa1099a352b04368118',
370 'vcs/backends/base.py', 2921),
370 'vcs/backends/base.py', 2921),
371 ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
371 ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
372 'vcs/backends/base.py', 3936),
372 'vcs/backends/base.py', 3936),
373 ('f50f42baeed5af6518ef4b0cb2f1423f3851a941',
373 ('f50f42baeed5af6518ef4b0cb2f1423f3851a941',
374 'vcs/backends/base.py', 6189),
374 'vcs/backends/base.py', 6189),
375 )
375 )
376 for revision, path, size in to_check:
376 for revision, path, size in to_check:
377 self._test_file_size(revision, path, size)
377 self._test_file_size(revision, path, size)
378
378
379 def _test_dir_size(self, revision, path, size):
379 def _test_dir_size(self, revision, path, size):
380 node = self.repo.get_changeset(revision).get_node(path)
380 node = self.repo.get_changeset(revision).get_node(path)
381 self.assertEqual(node.size, size)
381 self.assertEqual(node.size, size)
382
382
383 def test_dir_size(self):
383 def test_dir_size(self):
384 to_check = (
384 to_check = (
385 ('5f2c6ee195929b0be80749243c18121c9864a3b3', '/', 674076),
385 ('5f2c6ee195929b0be80749243c18121c9864a3b3', '/', 674076),
386 ('7ab37bc680b4aa72c34d07b230c866c28e9fc204', '/', 674049),
386 ('7ab37bc680b4aa72c34d07b230c866c28e9fc204', '/', 674049),
387 ('6892503fb8f2a552cef5f4d4cc2cdbd13ae1cd2f', '/', 671830),
387 ('6892503fb8f2a552cef5f4d4cc2cdbd13ae1cd2f', '/', 671830),
388 )
388 )
389 for revision, path, size in to_check:
389 for revision, path, size in to_check:
390 self._test_dir_size(revision, path, size)
390 self._test_dir_size(revision, path, size)
391
391
392 def test_repo_size(self):
392 def test_repo_size(self):
393 self.assertEqual(self.repo.size, 674076)
393 self.assertEqual(self.repo.size, 674076)
394
394
395 def test_file_history(self):
395 def test_file_history(self):
396 # we can only check if those revisions are present in the history
396 # we can only check if those revisions are present in the history
397 # as we cannot update this test every time file is changed
397 # as we cannot update this test every time file is changed
398 files = {
398 files = {
399 'setup.py': [
399 'setup.py': [
400 '54386793436c938cff89326944d4c2702340037d',
400 '54386793436c938cff89326944d4c2702340037d',
401 '51d254f0ecf5df2ce50c0b115741f4cf13985dab',
401 '51d254f0ecf5df2ce50c0b115741f4cf13985dab',
402 '998ed409c795fec2012b1c0ca054d99888b22090',
402 '998ed409c795fec2012b1c0ca054d99888b22090',
403 '5e0eb4c47f56564395f76333f319d26c79e2fb09',
403 '5e0eb4c47f56564395f76333f319d26c79e2fb09',
404 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
404 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
405 '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e',
405 '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e',
406 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
406 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
407 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
407 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
408 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
408 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
409 ],
409 ],
410 'vcs/nodes.py': [
410 'vcs/nodes.py': [
411 '33fa3223355104431402a888fa77a4e9956feb3e',
411 '33fa3223355104431402a888fa77a4e9956feb3e',
412 'fa014c12c26d10ba682fadb78f2a11c24c8118e1',
412 'fa014c12c26d10ba682fadb78f2a11c24c8118e1',
413 'e686b958768ee96af8029fe19c6050b1a8dd3b2b',
413 'e686b958768ee96af8029fe19c6050b1a8dd3b2b',
414 'ab5721ca0a081f26bf43d9051e615af2cc99952f',
414 'ab5721ca0a081f26bf43d9051e615af2cc99952f',
415 'c877b68d18e792a66b7f4c529ea02c8f80801542',
415 'c877b68d18e792a66b7f4c529ea02c8f80801542',
416 '4313566d2e417cb382948f8d9d7c765330356054',
416 '4313566d2e417cb382948f8d9d7c765330356054',
417 '6c2303a793671e807d1cfc70134c9ca0767d98c2',
417 '6c2303a793671e807d1cfc70134c9ca0767d98c2',
418 '54386793436c938cff89326944d4c2702340037d',
418 '54386793436c938cff89326944d4c2702340037d',
419 '54000345d2e78b03a99d561399e8e548de3f3203',
419 '54000345d2e78b03a99d561399e8e548de3f3203',
420 '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b',
420 '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b',
421 '2d03ca750a44440fb5ea8b751176d1f36f8e8f46',
421 '2d03ca750a44440fb5ea8b751176d1f36f8e8f46',
422 '2a08b128c206db48c2f0b8f70df060e6db0ae4f8',
422 '2a08b128c206db48c2f0b8f70df060e6db0ae4f8',
423 '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b',
423 '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b',
424 'ac71e9503c2ca95542839af0ce7b64011b72ea7c',
424 'ac71e9503c2ca95542839af0ce7b64011b72ea7c',
425 '12669288fd13adba2a9b7dd5b870cc23ffab92d2',
425 '12669288fd13adba2a9b7dd5b870cc23ffab92d2',
426 '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382',
426 '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382',
427 '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5',
427 '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5',
428 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
428 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
429 'f50f42baeed5af6518ef4b0cb2f1423f3851a941',
429 'f50f42baeed5af6518ef4b0cb2f1423f3851a941',
430 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25',
430 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25',
431 'f15c21f97864b4f071cddfbf2750ec2e23859414',
431 'f15c21f97864b4f071cddfbf2750ec2e23859414',
432 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade',
432 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade',
433 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b',
433 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b',
434 '84dec09632a4458f79f50ddbbd155506c460b4f9',
434 '84dec09632a4458f79f50ddbbd155506c460b4f9',
435 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
435 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
436 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
436 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
437 '3bf1c5868e570e39569d094f922d33ced2fa3b2b',
437 '3bf1c5868e570e39569d094f922d33ced2fa3b2b',
438 'b8d04012574729d2c29886e53b1a43ef16dd00a1',
438 'b8d04012574729d2c29886e53b1a43ef16dd00a1',
439 '6970b057cffe4aab0a792aa634c89f4bebf01441',
439 '6970b057cffe4aab0a792aa634c89f4bebf01441',
440 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
440 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
441 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
441 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
442 ],
442 ],
443 'vcs/backends/git.py': [
443 'vcs/backends/git.py': [
444 '4cf116ad5a457530381135e2f4c453e68a1b0105',
444 '4cf116ad5a457530381135e2f4c453e68a1b0105',
445 '9a751d84d8e9408e736329767387f41b36935153',
445 '9a751d84d8e9408e736329767387f41b36935153',
446 'cb681fb539c3faaedbcdf5ca71ca413425c18f01',
446 'cb681fb539c3faaedbcdf5ca71ca413425c18f01',
447 '428f81bb652bcba8d631bce926e8834ff49bdcc6',
447 '428f81bb652bcba8d631bce926e8834ff49bdcc6',
448 '180ab15aebf26f98f714d8c68715e0f05fa6e1c7',
448 '180ab15aebf26f98f714d8c68715e0f05fa6e1c7',
449 '2b8e07312a2e89e92b90426ab97f349f4bce2a3a',
449 '2b8e07312a2e89e92b90426ab97f349f4bce2a3a',
450 '50e08c506174d8645a4bb517dd122ac946a0f3bf',
450 '50e08c506174d8645a4bb517dd122ac946a0f3bf',
451 '54000345d2e78b03a99d561399e8e548de3f3203',
451 '54000345d2e78b03a99d561399e8e548de3f3203',
452 ],
452 ],
453 }
453 }
454 for path, revs in files.items():
454 for path, revs in files.items():
455 node = self.repo.get_changeset(revs[0]).get_node(path)
455 node = self.repo.get_changeset(revs[0]).get_node(path)
456 node_revs = [chset.raw_id for chset in node.history]
456 node_revs = [chset.raw_id for chset in node.history]
457 self.assertTrue(set(revs).issubset(set(node_revs)),
457 self.assertTrue(set(revs).issubset(set(node_revs)),
458 "We assumed that %s is subset of revisions for which file %s "
458 "We assumed that %s is subset of revisions for which file %s "
459 "has been changed, and history of that node returned: %s"
459 "has been changed, and history of that node returned: %s"
460 % (revs, path, node_revs))
460 % (revs, path, node_revs))
461
461
462 def test_file_annotate(self):
462 def test_file_annotate(self):
463 files = {
463 files = {
464 'vcs/backends/__init__.py': {
464 'vcs/backends/__init__.py': {
465 'c1214f7e79e02fc37156ff215cd71275450cffc3': {
465 'c1214f7e79e02fc37156ff215cd71275450cffc3': {
466 'lines_no': 1,
466 'lines_no': 1,
467 'changesets': [
467 'changesets': [
468 'c1214f7e79e02fc37156ff215cd71275450cffc3',
468 'c1214f7e79e02fc37156ff215cd71275450cffc3',
469 ],
469 ],
470 },
470 },
471 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': {
471 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': {
472 'lines_no': 21,
472 'lines_no': 21,
473 'changesets': [
473 'changesets': [
474 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
474 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
475 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
475 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
476 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
476 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
477 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
477 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
478 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
478 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
479 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
479 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
480 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
480 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
481 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
481 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
482 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
482 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
483 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
483 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
484 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
484 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
485 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
485 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
486 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
486 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
487 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
487 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
488 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
488 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
489 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
489 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
490 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
490 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
491 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
491 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
492 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
492 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
493 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
493 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
494 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
494 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
495 ],
495 ],
496 },
496 },
497 'e29b67bd158580fc90fc5e9111240b90e6e86064': {
497 'e29b67bd158580fc90fc5e9111240b90e6e86064': {
498 'lines_no': 32,
498 'lines_no': 32,
499 'changesets': [
499 'changesets': [
500 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
500 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
501 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
501 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
502 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
502 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
503 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
503 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
504 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
504 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
505 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
505 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
506 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
506 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
507 '54000345d2e78b03a99d561399e8e548de3f3203',
507 '54000345d2e78b03a99d561399e8e548de3f3203',
508 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
508 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
509 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
509 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
510 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
510 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
511 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
511 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
512 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
512 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
513 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
513 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
514 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
514 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
515 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
515 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
516 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
516 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
517 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
517 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
518 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
518 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
519 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
519 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
520 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
520 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
521 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
521 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
522 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
522 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
523 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
523 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
524 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
524 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
525 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
525 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
526 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
526 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
527 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
527 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
528 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
528 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
529 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
529 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
530 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
530 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
531 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
531 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
532 ],
532 ],
533 },
533 },
534 },
534 },
535 }
535 }
536
536
537 for fname, revision_dict in files.items():
537 for fname, revision_dict in files.items():
538 for rev, data in revision_dict.items():
538 for rev, data in revision_dict.items():
539 cs = self.repo.get_changeset(rev)
539 cs = self.repo.get_changeset(rev)
540
540
541 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
541 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
542 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
542 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
543 self.assertEqual(l1_1, l1_2)
543 self.assertEqual(l1_1, l1_2)
544 l1 = l1_1
544 l1 = l1_1
545 l2 = files[fname][rev]['changesets']
545 l2 = files[fname][rev]['changesets']
546 self.assertTrue(l1 == l2 , "The lists of revision for %s@rev %s"
546 self.assertTrue(l1 == l2 , "The lists of revision for %s@rev %s"
547 "from annotation list should match each other, "
547 "from annotation list should match each other, "
548 "got \n%s \nvs \n%s " % (fname, rev, l1, l2))
548 "got \n%s \nvs \n%s " % (fname, rev, l1, l2))
549
549
550 def test_files_state(self):
550 def test_files_state(self):
551 """
551 """
552 Tests state of FileNodes.
552 Tests state of FileNodes.
553 """
553 """
554 node = self.repo\
554 node = self.repo\
555 .get_changeset('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0')\
555 .get_changeset('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0')\
556 .get_node('vcs/utils/diffs.py')
556 .get_node('vcs/utils/diffs.py')
557 self.assertTrue(node.state, NodeState.ADDED)
557 self.assertTrue(node.state, NodeState.ADDED)
558 self.assertTrue(node.added)
558 self.assertTrue(node.added)
559 self.assertFalse(node.changed)
559 self.assertFalse(node.changed)
560 self.assertFalse(node.not_changed)
560 self.assertFalse(node.not_changed)
561 self.assertFalse(node.removed)
561 self.assertFalse(node.removed)
562
562
563 node = self.repo\
563 node = self.repo\
564 .get_changeset('33fa3223355104431402a888fa77a4e9956feb3e')\
564 .get_changeset('33fa3223355104431402a888fa77a4e9956feb3e')\
565 .get_node('.hgignore')
565 .get_node('.hgignore')
566 self.assertTrue(node.state, NodeState.CHANGED)
566 self.assertTrue(node.state, NodeState.CHANGED)
567 self.assertFalse(node.added)
567 self.assertFalse(node.added)
568 self.assertTrue(node.changed)
568 self.assertTrue(node.changed)
569 self.assertFalse(node.not_changed)
569 self.assertFalse(node.not_changed)
570 self.assertFalse(node.removed)
570 self.assertFalse(node.removed)
571
571
572 node = self.repo\
572 node = self.repo\
573 .get_changeset('e29b67bd158580fc90fc5e9111240b90e6e86064')\
573 .get_changeset('e29b67bd158580fc90fc5e9111240b90e6e86064')\
574 .get_node('setup.py')
574 .get_node('setup.py')
575 self.assertTrue(node.state, NodeState.NOT_CHANGED)
575 self.assertTrue(node.state, NodeState.NOT_CHANGED)
576 self.assertFalse(node.added)
576 self.assertFalse(node.added)
577 self.assertFalse(node.changed)
577 self.assertFalse(node.changed)
578 self.assertTrue(node.not_changed)
578 self.assertTrue(node.not_changed)
579 self.assertFalse(node.removed)
579 self.assertFalse(node.removed)
580
580
581 # If node has REMOVED state then trying to fetch it would raise
581 # If node has REMOVED state then trying to fetch it would raise
582 # ChangesetError exception
582 # ChangesetError exception
583 chset = self.repo.get_changeset(
583 chset = self.repo.get_changeset(
584 'fa6600f6848800641328adbf7811fd2372c02ab2')
584 'fa6600f6848800641328adbf7811fd2372c02ab2')
585 path = 'vcs/backends/BaseRepository.py'
585 path = 'vcs/backends/BaseRepository.py'
586 self.assertRaises(NodeDoesNotExistError, chset.get_node, path)
586 self.assertRaises(NodeDoesNotExistError, chset.get_node, path)
587 # but it would be one of ``removed`` (changeset's attribute)
587 # but it would be one of ``removed`` (changeset's attribute)
588 self.assertTrue(path in [rf.path for rf in chset.removed])
588 self.assertTrue(path in [rf.path for rf in chset.removed])
589
589
590 chset = self.repo.get_changeset(
590 chset = self.repo.get_changeset(
591 '54386793436c938cff89326944d4c2702340037d')
591 '54386793436c938cff89326944d4c2702340037d')
592 changed = ['setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py',
592 changed = ['setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py',
593 'vcs/nodes.py']
593 'vcs/nodes.py']
594 self.assertEqual(set(changed), set([f.path for f in chset.changed]))
594 self.assertEqual(set(changed), set([f.path for f in chset.changed]))
595
595
596 def test_commit_message_is_unicode(self):
596 def test_commit_message_is_unicode(self):
597 for cs in self.repo:
597 for cs in self.repo:
598 self.assertEqual(type(cs.message), unicode)
598 self.assertEqual(type(cs.message), unicode)
599
599
600 def test_changeset_author_is_unicode(self):
600 def test_changeset_author_is_unicode(self):
601 for cs in self.repo:
601 for cs in self.repo:
602 self.assertEqual(type(cs.author), unicode)
602 self.assertEqual(type(cs.author), unicode)
603
603
604 def test_repo_files_content_is_unicode(self):
604 def test_repo_files_content_is_unicode(self):
605 changeset = self.repo.get_changeset()
605 changeset = self.repo.get_changeset()
606 for node in changeset.get_node('/'):
606 for node in changeset.get_node('/'):
607 if node.is_file():
607 if node.is_file():
608 self.assertEqual(type(node.content), unicode)
608 self.assertEqual(type(node.content), unicode)
609
609
610 def test_wrong_path(self):
610 def test_wrong_path(self):
611 # There is 'setup.py' in the root dir but not there:
611 # There is 'setup.py' in the root dir but not there:
612 path = 'foo/bar/setup.py'
612 path = 'foo/bar/setup.py'
613 tip = self.repo.get_changeset()
613 tip = self.repo.get_changeset()
614 self.assertRaises(VCSError, tip.get_node, path)
614 self.assertRaises(VCSError, tip.get_node, path)
615
615
616 def test_author_email(self):
616 def test_author_email(self):
617 self.assertEqual('marcin@python-blog.com',
617 self.assertEqual('marcin@python-blog.com',
618 self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3')\
618 self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3')\
619 .author_email)
619 .author_email)
620 self.assertEqual('lukasz.balcerzak@python-center.pl',
620 self.assertEqual('lukasz.balcerzak@python-center.pl',
621 self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b')\
621 self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b')\
622 .author_email)
622 .author_email)
623 self.assertEqual('none@none',
623 self.assertEqual('none@none',
624 self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992')\
624 self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992')\
625 .author_email)
625 .author_email)
626
626
627 def test_author_username(self):
627 def test_author_username(self):
628 self.assertEqual('Marcin Kuzminski',
628 self.assertEqual('Marcin Kuzminski',
629 self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3')\
629 self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3')\
630 .author_name)
630 .author_name)
631 self.assertEqual('Lukasz Balcerzak',
631 self.assertEqual('Lukasz Balcerzak',
632 self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b')\
632 self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b')\
633 .author_name)
633 .author_name)
634 self.assertEqual('marcink',
634 self.assertEqual('marcink',
635 self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992')\
635 self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992')\
636 .author_name)
636 .author_name)
637
637
638
638
639 class GitSpecificTest(unittest.TestCase):
639 class GitSpecificTest(unittest.TestCase):
640
640
641 def test_error_is_raised_for_added_if_diff_name_status_is_wrong(self):
641 def test_error_is_raised_for_added_if_diff_name_status_is_wrong(self):
642 repo = mock.MagicMock()
642 repo = mock.MagicMock()
643 changeset = GitChangeset(repo, 'foobar')
643 changeset = GitChangeset(repo, 'foobar')
644 changeset._diff_name_status = 'foobar'
644 changeset._diff_name_status = 'foobar'
645 with self.assertRaises(VCSError):
645 with self.assertRaises(VCSError):
646 changeset.added
646 changeset.added
647
647
648 def test_error_is_raised_for_changed_if_diff_name_status_is_wrong(self):
648 def test_error_is_raised_for_changed_if_diff_name_status_is_wrong(self):
649 repo = mock.MagicMock()
649 repo = mock.MagicMock()
650 changeset = GitChangeset(repo, 'foobar')
650 changeset = GitChangeset(repo, 'foobar')
651 changeset._diff_name_status = 'foobar'
651 changeset._diff_name_status = 'foobar'
652 with self.assertRaises(VCSError):
652 with self.assertRaises(VCSError):
653 changeset.added
653 changeset.added
654
654
655 def test_error_is_raised_for_removed_if_diff_name_status_is_wrong(self):
655 def test_error_is_raised_for_removed_if_diff_name_status_is_wrong(self):
656 repo = mock.MagicMock()
656 repo = mock.MagicMock()
657 changeset = GitChangeset(repo, 'foobar')
657 changeset = GitChangeset(repo, 'foobar')
658 changeset._diff_name_status = 'foobar'
658 changeset._diff_name_status = 'foobar'
659 with self.assertRaises(VCSError):
659 with self.assertRaises(VCSError):
660 changeset.added
660 changeset.added
661
661
662
662
663 class GitSpecificWithRepoTest(_BackendTestMixin, unittest.TestCase):
663 class GitSpecificWithRepoTest(_BackendTestMixin, unittest.TestCase):
664 backend_alias = 'git'
664 backend_alias = 'git'
665
665
666 @classmethod
666 @classmethod
667 def _get_commits(cls):
667 def _get_commits(cls):
668 return [
668 return [
669 {
669 {
670 'message': 'Initial',
670 'message': 'Initial',
671 'author': 'Joe Doe <joe.doe@example.com>',
671 'author': 'Joe Doe <joe.doe@example.com>',
672 'date': datetime.datetime(2010, 1, 1, 20),
672 'date': datetime.datetime(2010, 1, 1, 20),
673 'added': [
673 'added': [
674 FileNode('foobar/static/js/admin/base.js', content='base'),
674 FileNode('foobar/static/js/admin/base.js', content='base'),
675 FileNode('foobar/static/admin', content='admin',
675 FileNode('foobar/static/admin', content='admin',
676 mode=0120000), # this is a link
676 mode=0120000), # this is a link
677 FileNode('foo', content='foo'),
677 FileNode('foo', content='foo'),
678 ],
678 ],
679 },
679 },
680 {
680 {
681 'message': 'Second',
681 'message': 'Second',
682 'author': 'Joe Doe <joe.doe@example.com>',
682 'author': 'Joe Doe <joe.doe@example.com>',
683 'date': datetime.datetime(2010, 1, 1, 22),
683 'date': datetime.datetime(2010, 1, 1, 22),
684 'added': [
684 'added': [
685 FileNode('foo2', content='foo2'),
685 FileNode('foo2', content='foo2'),
686 ],
686 ],
687 },
687 },
688 ]
688 ]
689
689
690 def test_paths_slow_traversing(self):
690 def test_paths_slow_traversing(self):
691 cs = self.repo.get_changeset()
691 cs = self.repo.get_changeset()
692 self.assertEqual(cs.get_node('foobar').get_node('static').get_node('js')
692 self.assertEqual(cs.get_node('foobar').get_node('static').get_node('js')
693 .get_node('admin').get_node('base.js').content, 'base')
693 .get_node('admin').get_node('base.js').content, 'base')
694
694
695 def test_paths_fast_traversing(self):
695 def test_paths_fast_traversing(self):
696 cs = self.repo.get_changeset()
696 cs = self.repo.get_changeset()
697 self.assertEqual(cs.get_node('foobar/static/js/admin/base.js').content,
697 self.assertEqual(cs.get_node('foobar/static/js/admin/base.js').content,
698 'base')
698 'base')
699
699
700 def test_workdir_get_branch(self):
700 def test_workdir_get_branch(self):
701 self.repo.run_git_command(['checkout', '-b', 'production'])
701 self.repo.run_git_command(['checkout', '-b', 'production'])
702 # Regression test: one of following would fail if we don't check
702 # Regression test: one of following would fail if we don't check
703 # .git/HEAD file
703 # .git/HEAD file
704 self.repo.run_git_command(['checkout', 'production'])
704 self.repo.run_git_command(['checkout', 'production'])
705 self.assertEqual(self.repo.workdir.get_branch(), 'production')
705 self.assertEqual(self.repo.workdir.get_branch(), 'production')
706 self.repo.run_git_command(['checkout', 'master'])
706 self.repo.run_git_command(['checkout', 'master'])
707 self.assertEqual(self.repo.workdir.get_branch(), 'master')
707 self.assertEqual(self.repo.workdir.get_branch(), 'master')
708
708
709 def test_get_diff_runs_git_command_with_hashes(self):
709 def test_get_diff_runs_git_command_with_hashes(self):
710 self.repo.run_git_command = mock.Mock(return_value=['', ''])
710 self.repo.run_git_command = mock.Mock(return_value=['', ''])
711 self.repo.get_diff(0, 1)
711 self.repo.get_diff(0, 1)
712 self.repo.run_git_command.assert_called_once_with(
712 self.repo.run_git_command.assert_called_once_with(
713 ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
713 ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
714 self.repo._get_revision(0), self.repo._get_revision(1)])
714 self.repo._get_revision(0), self.repo._get_revision(1)])
715
715
716 def test_get_diff_runs_git_command_with_str_hashes(self):
716 def test_get_diff_runs_git_command_with_str_hashes(self):
717 self.repo.run_git_command = mock.Mock(return_value=['', ''])
717 self.repo.run_git_command = mock.Mock(return_value=['', ''])
718 self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1)
718 self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1)
719 self.repo.run_git_command.assert_called_once_with(
719 self.repo.run_git_command.assert_called_once_with(
720 ['show', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
720 ['show', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
721 self.repo._get_revision(1)])
721 self.repo._get_revision(1)])
722
722
723 def test_get_diff_runs_git_command_with_path_if_its_given(self):
723 def test_get_diff_runs_git_command_with_path_if_its_given(self):
724 self.repo.run_git_command = mock.Mock(return_value=['', ''])
724 self.repo.run_git_command = mock.Mock(return_value=['', ''])
725 self.repo.get_diff(0, 1, 'foo')
725 self.repo.get_diff(0, 1, 'foo')
726 self.repo.run_git_command.assert_called_once_with(
726 self.repo.run_git_command.assert_called_once_with(
727 ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
727 ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
728 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
728 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
729
729
730 def test_get_diff_does_not_sanitize_valid_context(self):
731 almost_overflowed_long_int = 2**31-1
732
733 self.repo.run_git_command = mock.Mock(return_value=['', ''])
734 self.repo.get_diff(0, 1, 'foo', context=almost_overflowed_long_int)
735 self.repo.run_git_command.assert_called_once_with(
736 ['diff', '-U' + str(almost_overflowed_long_int), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
737 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
738
739 def test_get_diff_sanitizes_overflowing_context(self):
740 overflowed_long_int = 2**31
741 sanitized_overflowed_long_int = overflowed_long_int-1
742
743 self.repo.run_git_command = mock.Mock(return_value=['', ''])
744 self.repo.get_diff(0, 1, 'foo', context=overflowed_long_int)
745
746 self.repo.run_git_command.assert_called_once_with(
747 ['diff', '-U' + str(sanitized_overflowed_long_int), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
748 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
749
750 def test_get_diff_does_not_sanitize_zero_context(self):
751 zero_context = 0
752
753 self.repo.run_git_command = mock.Mock(return_value=['', ''])
754 self.repo.get_diff(0, 1, 'foo', context=zero_context)
755
756 self.repo.run_git_command.assert_called_once_with(
757 ['diff', '-U' + str(zero_context), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
758 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
759
760 def test_get_diff_sanitizes_negative_context(self):
761 negative_context = -10
762
763 self.repo.run_git_command = mock.Mock(return_value=['', ''])
764 self.repo.get_diff(0, 1, 'foo', context=negative_context)
765
766 self.repo.run_git_command.assert_called_once_with(
767 ['diff', '-U0', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
768 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
769
730
770
731 class GitRegressionTest(_BackendTestMixin, unittest.TestCase):
771 class GitRegressionTest(_BackendTestMixin, unittest.TestCase):
732 backend_alias = 'git'
772 backend_alias = 'git'
733
773
734 @classmethod
774 @classmethod
735 def _get_commits(cls):
775 def _get_commits(cls):
736 return [
776 return [
737 {
777 {
738 'message': 'Initial',
778 'message': 'Initial',
739 'author': 'Joe Doe <joe.doe@example.com>',
779 'author': 'Joe Doe <joe.doe@example.com>',
740 'date': datetime.datetime(2010, 1, 1, 20),
780 'date': datetime.datetime(2010, 1, 1, 20),
741 'added': [
781 'added': [
742 FileNode('bot/__init__.py', content='base'),
782 FileNode('bot/__init__.py', content='base'),
743 FileNode('bot/templates/404.html', content='base'),
783 FileNode('bot/templates/404.html', content='base'),
744 FileNode('bot/templates/500.html', content='base'),
784 FileNode('bot/templates/500.html', content='base'),
745 ],
785 ],
746 },
786 },
747 {
787 {
748 'message': 'Second',
788 'message': 'Second',
749 'author': 'Joe Doe <joe.doe@example.com>',
789 'author': 'Joe Doe <joe.doe@example.com>',
750 'date': datetime.datetime(2010, 1, 1, 22),
790 'date': datetime.datetime(2010, 1, 1, 22),
751 'added': [
791 'added': [
752 FileNode('bot/build/migrations/1.py', content='foo2'),
792 FileNode('bot/build/migrations/1.py', content='foo2'),
753 FileNode('bot/build/migrations/2.py', content='foo2'),
793 FileNode('bot/build/migrations/2.py', content='foo2'),
754 FileNode('bot/build/static/templates/f.html', content='foo2'),
794 FileNode('bot/build/static/templates/f.html', content='foo2'),
755 FileNode('bot/build/static/templates/f1.html', content='foo2'),
795 FileNode('bot/build/static/templates/f1.html', content='foo2'),
756 FileNode('bot/build/templates/err.html', content='foo2'),
796 FileNode('bot/build/templates/err.html', content='foo2'),
757 FileNode('bot/build/templates/err2.html', content='foo2'),
797 FileNode('bot/build/templates/err2.html', content='foo2'),
758 ],
798 ],
759 },
799 },
760 ]
800 ]
761
801
762 def test_similar_paths(self):
802 def test_similar_paths(self):
763 cs = self.repo.get_changeset()
803 cs = self.repo.get_changeset()
764 paths = lambda *n:[x.path for x in n]
804 paths = lambda *n:[x.path for x in n]
765 self.assertEqual(paths(*cs.get_nodes('bot')), ['bot/build', 'bot/templates', 'bot/__init__.py'])
805 self.assertEqual(paths(*cs.get_nodes('bot')), ['bot/build', 'bot/templates', 'bot/__init__.py'])
766 self.assertEqual(paths(*cs.get_nodes('bot/build')), ['bot/build/migrations', 'bot/build/static', 'bot/build/templates'])
806 self.assertEqual(paths(*cs.get_nodes('bot/build')), ['bot/build/migrations', 'bot/build/static', 'bot/build/templates'])
767 self.assertEqual(paths(*cs.get_nodes('bot/build/static')), ['bot/build/static/templates'])
807 self.assertEqual(paths(*cs.get_nodes('bot/build/static')), ['bot/build/static/templates'])
768 # this get_nodes below causes troubles !
808 # this get_nodes below causes troubles !
769 self.assertEqual(paths(*cs.get_nodes('bot/build/static/templates')), ['bot/build/static/templates/f.html', 'bot/build/static/templates/f1.html'])
809 self.assertEqual(paths(*cs.get_nodes('bot/build/static/templates')), ['bot/build/static/templates/f.html', 'bot/build/static/templates/f1.html'])
770 self.assertEqual(paths(*cs.get_nodes('bot/build/templates')), ['bot/build/templates/err.html', 'bot/build/templates/err2.html'])
810 self.assertEqual(paths(*cs.get_nodes('bot/build/templates')), ['bot/build/templates/err.html', 'bot/build/templates/err2.html'])
771 self.assertEqual(paths(*cs.get_nodes('bot/templates/')), ['bot/templates/404.html', 'bot/templates/500.html'])
811 self.assertEqual(paths(*cs.get_nodes('bot/templates/')), ['bot/templates/404.html', 'bot/templates/500.html'])
772
812
773
813
774 class GitHooksTest(unittest.TestCase):
814 class GitHooksTest(unittest.TestCase):
775 """
815 """
776 Tests related to hook functionality of Git repositories.
816 Tests related to hook functionality of Git repositories.
777 """
817 """
778
818
779 def setUp(self):
819 def setUp(self):
780 # For each run we want a fresh repo.
820 # For each run we want a fresh repo.
781 self.repo_directory = get_new_dir("githookrepo")
821 self.repo_directory = get_new_dir("githookrepo")
782 self.repo = GitRepository(self.repo_directory, create=True)
822 self.repo = GitRepository(self.repo_directory, create=True)
783
823
784 # Create a dictionary where keys are hook names, and values are paths to
824 # Create a dictionary where keys are hook names, and values are paths to
785 # them. Deduplicates code in tests a bit.
825 # them. Deduplicates code in tests a bit.
786 self.hook_directory = self.repo.get_hook_location()
826 self.hook_directory = self.repo.get_hook_location()
787 self.kallithea_hooks = {h: os.path.join(self.hook_directory, h) for h in ("pre-receive", "post-receive")}
827 self.kallithea_hooks = {h: os.path.join(self.hook_directory, h) for h in ("pre-receive", "post-receive")}
788
828
789 def test_hooks_created_if_missing(self):
829 def test_hooks_created_if_missing(self):
790 """
830 """
791 Tests if hooks are installed in repository if they are missing.
831 Tests if hooks are installed in repository if they are missing.
792 """
832 """
793
833
794 for hook, hook_path in self.kallithea_hooks.iteritems():
834 for hook, hook_path in self.kallithea_hooks.iteritems():
795 if os.path.exists(hook_path):
835 if os.path.exists(hook_path):
796 os.remove(hook_path)
836 os.remove(hook_path)
797
837
798 ScmModel().install_git_hooks(repo=self.repo)
838 ScmModel().install_git_hooks(repo=self.repo)
799
839
800 for hook, hook_path in self.kallithea_hooks.iteritems():
840 for hook, hook_path in self.kallithea_hooks.iteritems():
801 self.assertTrue(os.path.exists(hook_path))
841 self.assertTrue(os.path.exists(hook_path))
802
842
803 def test_kallithea_hooks_updated(self):
843 def test_kallithea_hooks_updated(self):
804 """
844 """
805 Tests if hooks are updated if they are Kallithea hooks already.
845 Tests if hooks are updated if they are Kallithea hooks already.
806 """
846 """
807
847
808 for hook, hook_path in self.kallithea_hooks.iteritems():
848 for hook, hook_path in self.kallithea_hooks.iteritems():
809 with open(hook_path, "w") as f:
849 with open(hook_path, "w") as f:
810 f.write("KALLITHEA_HOOK_VER=0.0.0\nJUST_BOGUS")
850 f.write("KALLITHEA_HOOK_VER=0.0.0\nJUST_BOGUS")
811
851
812 ScmModel().install_git_hooks(repo=self.repo)
852 ScmModel().install_git_hooks(repo=self.repo)
813
853
814 for hook, hook_path in self.kallithea_hooks.iteritems():
854 for hook, hook_path in self.kallithea_hooks.iteritems():
815 with open(hook_path) as f:
855 with open(hook_path) as f:
816 self.assertNotIn("JUST_BOGUS", f.read())
856 self.assertNotIn("JUST_BOGUS", f.read())
817
857
818 def test_custom_hooks_untouched(self):
858 def test_custom_hooks_untouched(self):
819 """
859 """
820 Tests if hooks are left untouched if they are not Kallithea hooks.
860 Tests if hooks are left untouched if they are not Kallithea hooks.
821 """
861 """
822
862
823 for hook, hook_path in self.kallithea_hooks.iteritems():
863 for hook, hook_path in self.kallithea_hooks.iteritems():
824 with open(hook_path, "w") as f:
864 with open(hook_path, "w") as f:
825 f.write("#!/bin/bash\n#CUSTOM_HOOK")
865 f.write("#!/bin/bash\n#CUSTOM_HOOK")
826
866
827 ScmModel().install_git_hooks(repo=self.repo)
867 ScmModel().install_git_hooks(repo=self.repo)
828
868
829 for hook, hook_path in self.kallithea_hooks.iteritems():
869 for hook, hook_path in self.kallithea_hooks.iteritems():
830 with open(hook_path) as f:
870 with open(hook_path) as f:
831 self.assertIn("CUSTOM_HOOK", f.read())
871 self.assertIn("CUSTOM_HOOK", f.read())
832
872
833 def test_custom_hooks_forced_update(self):
873 def test_custom_hooks_forced_update(self):
834 """
874 """
835 Tests if hooks are forcefully updated even though they are custom hooks.
875 Tests if hooks are forcefully updated even though they are custom hooks.
836 """
876 """
837
877
838 for hook, hook_path in self.kallithea_hooks.iteritems():
878 for hook, hook_path in self.kallithea_hooks.iteritems():
839 with open(hook_path, "w") as f:
879 with open(hook_path, "w") as f:
840 f.write("#!/bin/bash\n#CUSTOM_HOOK")
880 f.write("#!/bin/bash\n#CUSTOM_HOOK")
841
881
842 ScmModel().install_git_hooks(repo=self.repo, force_create=True)
882 ScmModel().install_git_hooks(repo=self.repo, force_create=True)
843
883
844 for hook, hook_path in self.kallithea_hooks.iteritems():
884 for hook, hook_path in self.kallithea_hooks.iteritems():
845 with open(hook_path) as f:
885 with open(hook_path) as f:
846 self.assertIn("KALLITHEA_HOOK_VER", f.read())
886 self.assertIn("KALLITHEA_HOOK_VER", f.read())
847
887
848
888
849 if __name__ == '__main__':
889 if __name__ == '__main__':
850 unittest.main()
890 unittest.main()
@@ -1,567 +1,587 b''
1
1
2 import os
2 import os
3
4 import mock
5
3 from kallithea.lib.vcs.backends.hg import MercurialRepository, MercurialChangeset
6 from kallithea.lib.vcs.backends.hg import MercurialRepository, MercurialChangeset
4 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
7 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
5 from kallithea.lib.vcs.nodes import NodeKind, NodeState
8 from kallithea.lib.vcs.nodes import NodeKind, NodeState
6 from kallithea.tests.vcs.conf import TEST_HG_REPO, TEST_HG_REPO_CLONE, \
9 from kallithea.tests.vcs.conf import TEST_HG_REPO, TEST_HG_REPO_CLONE, \
7 TEST_HG_REPO_PULL
10 TEST_HG_REPO_PULL
8 from kallithea.lib.vcs.utils.compat import unittest
11 from kallithea.lib.vcs.utils.compat import unittest
9
12
10
13
11 class MercurialRepositoryTest(unittest.TestCase):
14 class MercurialRepositoryTest(unittest.TestCase):
12
15
13 def __check_for_existing_repo(self):
16 def __check_for_existing_repo(self):
14 if os.path.exists(TEST_HG_REPO_CLONE):
17 if os.path.exists(TEST_HG_REPO_CLONE):
15 self.fail('Cannot test mercurial clone repo as location %s already '
18 self.fail('Cannot test mercurial clone repo as location %s already '
16 'exists. You should manually remove it first.'
19 'exists. You should manually remove it first.'
17 % TEST_HG_REPO_CLONE)
20 % TEST_HG_REPO_CLONE)
18
21
19 def setUp(self):
22 def setUp(self):
20 self.repo = MercurialRepository(TEST_HG_REPO)
23 self.repo = MercurialRepository(TEST_HG_REPO)
21
24
22 def test_wrong_repo_path(self):
25 def test_wrong_repo_path(self):
23 wrong_repo_path = '/tmp/errorrepo'
26 wrong_repo_path = '/tmp/errorrepo'
24 self.assertRaises(RepositoryError, MercurialRepository, wrong_repo_path)
27 self.assertRaises(RepositoryError, MercurialRepository, wrong_repo_path)
25
28
26 def test_unicode_path_repo(self):
29 def test_unicode_path_repo(self):
27 self.assertRaises(VCSError,lambda:MercurialRepository(u'iShouldFail'))
30 self.assertRaises(VCSError,lambda:MercurialRepository(u'iShouldFail'))
28
31
29 def test_repo_clone(self):
32 def test_repo_clone(self):
30 self.__check_for_existing_repo()
33 self.__check_for_existing_repo()
31 repo = MercurialRepository(TEST_HG_REPO)
34 repo = MercurialRepository(TEST_HG_REPO)
32 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE,
35 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE,
33 src_url=TEST_HG_REPO, update_after_clone=True)
36 src_url=TEST_HG_REPO, update_after_clone=True)
34 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
37 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
35 # Checking hashes of changesets should be enough
38 # Checking hashes of changesets should be enough
36 for changeset in repo.get_changesets():
39 for changeset in repo.get_changesets():
37 raw_id = changeset.raw_id
40 raw_id = changeset.raw_id
38 self.assertEqual(raw_id, repo_clone.get_changeset(raw_id).raw_id)
41 self.assertEqual(raw_id, repo_clone.get_changeset(raw_id).raw_id)
39
42
40 def test_repo_clone_with_update(self):
43 def test_repo_clone_with_update(self):
41 repo = MercurialRepository(TEST_HG_REPO)
44 repo = MercurialRepository(TEST_HG_REPO)
42 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_w_update',
45 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_w_update',
43 src_url=TEST_HG_REPO, update_after_clone=True)
46 src_url=TEST_HG_REPO, update_after_clone=True)
44 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
47 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
45
48
46 #check if current workdir was updated
49 #check if current workdir was updated
47 self.assertEqual(os.path.isfile(os.path.join(TEST_HG_REPO_CLONE \
50 self.assertEqual(os.path.isfile(os.path.join(TEST_HG_REPO_CLONE \
48 + '_w_update',
51 + '_w_update',
49 'MANIFEST.in')), True,)
52 'MANIFEST.in')), True,)
50
53
51 def test_repo_clone_without_update(self):
54 def test_repo_clone_without_update(self):
52 repo = MercurialRepository(TEST_HG_REPO)
55 repo = MercurialRepository(TEST_HG_REPO)
53 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_wo_update',
56 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_wo_update',
54 src_url=TEST_HG_REPO, update_after_clone=False)
57 src_url=TEST_HG_REPO, update_after_clone=False)
55 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
58 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
56 self.assertEqual(os.path.isfile(os.path.join(TEST_HG_REPO_CLONE \
59 self.assertEqual(os.path.isfile(os.path.join(TEST_HG_REPO_CLONE \
57 + '_wo_update',
60 + '_wo_update',
58 'MANIFEST.in')), False,)
61 'MANIFEST.in')), False,)
59
62
60 def test_pull(self):
63 def test_pull(self):
61 if os.path.exists(TEST_HG_REPO_PULL):
64 if os.path.exists(TEST_HG_REPO_PULL):
62 self.fail('Cannot test mercurial pull command as location %s '
65 self.fail('Cannot test mercurial pull command as location %s '
63 'already exists. You should manually remove it first'
66 'already exists. You should manually remove it first'
64 % TEST_HG_REPO_PULL)
67 % TEST_HG_REPO_PULL)
65 repo_new = MercurialRepository(TEST_HG_REPO_PULL, create=True)
68 repo_new = MercurialRepository(TEST_HG_REPO_PULL, create=True)
66 self.assertTrue(len(self.repo.revisions) > len(repo_new.revisions))
69 self.assertTrue(len(self.repo.revisions) > len(repo_new.revisions))
67
70
68 repo_new.pull(self.repo.path)
71 repo_new.pull(self.repo.path)
69 repo_new = MercurialRepository(TEST_HG_REPO_PULL)
72 repo_new = MercurialRepository(TEST_HG_REPO_PULL)
70 self.assertTrue(len(self.repo.revisions) == len(repo_new.revisions))
73 self.assertTrue(len(self.repo.revisions) == len(repo_new.revisions))
71
74
72 def test_revisions(self):
75 def test_revisions(self):
73 # there are 21 revisions at bitbucket now
76 # there are 21 revisions at bitbucket now
74 # so we can assume they would be available from now on
77 # so we can assume they would be available from now on
75 subset = set(['b986218ba1c9b0d6a259fac9b050b1724ed8e545',
78 subset = set(['b986218ba1c9b0d6a259fac9b050b1724ed8e545',
76 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
79 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
77 '6cba7170863a2411822803fa77a0a264f1310b35',
80 '6cba7170863a2411822803fa77a0a264f1310b35',
78 '56349e29c2af3ac913b28bde9a2c6154436e615b',
81 '56349e29c2af3ac913b28bde9a2c6154436e615b',
79 '2dda4e345facb0ccff1a191052dd1606dba6781d',
82 '2dda4e345facb0ccff1a191052dd1606dba6781d',
80 '6fff84722075f1607a30f436523403845f84cd9e',
83 '6fff84722075f1607a30f436523403845f84cd9e',
81 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
84 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
82 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
85 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
83 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
86 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
84 'be90031137367893f1c406e0a8683010fd115b79',
87 'be90031137367893f1c406e0a8683010fd115b79',
85 'db8e58be770518cbb2b1cdfa69146e47cd481481',
88 'db8e58be770518cbb2b1cdfa69146e47cd481481',
86 '84478366594b424af694a6c784cb991a16b87c21',
89 '84478366594b424af694a6c784cb991a16b87c21',
87 '17f8e105dddb9f339600389c6dc7175d395a535c',
90 '17f8e105dddb9f339600389c6dc7175d395a535c',
88 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
91 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
89 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
92 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
90 '786facd2c61deb9cf91e9534735124fb8fc11842',
93 '786facd2c61deb9cf91e9534735124fb8fc11842',
91 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
94 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
92 'aa6a0de05b7612707db567078e130a6cd114a9a7',
95 'aa6a0de05b7612707db567078e130a6cd114a9a7',
93 'eada5a770da98ab0dd7325e29d00e0714f228d09'
96 'eada5a770da98ab0dd7325e29d00e0714f228d09'
94 ])
97 ])
95 self.assertTrue(subset.issubset(set(self.repo.revisions)))
98 self.assertTrue(subset.issubset(set(self.repo.revisions)))
96
99
97
100
98 # check if we have the proper order of revisions
101 # check if we have the proper order of revisions
99 org = ['b986218ba1c9b0d6a259fac9b050b1724ed8e545',
102 org = ['b986218ba1c9b0d6a259fac9b050b1724ed8e545',
100 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
103 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
101 '6cba7170863a2411822803fa77a0a264f1310b35',
104 '6cba7170863a2411822803fa77a0a264f1310b35',
102 '56349e29c2af3ac913b28bde9a2c6154436e615b',
105 '56349e29c2af3ac913b28bde9a2c6154436e615b',
103 '2dda4e345facb0ccff1a191052dd1606dba6781d',
106 '2dda4e345facb0ccff1a191052dd1606dba6781d',
104 '6fff84722075f1607a30f436523403845f84cd9e',
107 '6fff84722075f1607a30f436523403845f84cd9e',
105 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
108 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
106 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
109 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
107 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
110 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
108 'be90031137367893f1c406e0a8683010fd115b79',
111 'be90031137367893f1c406e0a8683010fd115b79',
109 'db8e58be770518cbb2b1cdfa69146e47cd481481',
112 'db8e58be770518cbb2b1cdfa69146e47cd481481',
110 '84478366594b424af694a6c784cb991a16b87c21',
113 '84478366594b424af694a6c784cb991a16b87c21',
111 '17f8e105dddb9f339600389c6dc7175d395a535c',
114 '17f8e105dddb9f339600389c6dc7175d395a535c',
112 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
115 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
113 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
116 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
114 '786facd2c61deb9cf91e9534735124fb8fc11842',
117 '786facd2c61deb9cf91e9534735124fb8fc11842',
115 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
118 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
116 'aa6a0de05b7612707db567078e130a6cd114a9a7',
119 'aa6a0de05b7612707db567078e130a6cd114a9a7',
117 'eada5a770da98ab0dd7325e29d00e0714f228d09',
120 'eada5a770da98ab0dd7325e29d00e0714f228d09',
118 '2c1885c735575ca478bf9e17b0029dca68824458',
121 '2c1885c735575ca478bf9e17b0029dca68824458',
119 'd9bcd465040bf869799b09ad732c04e0eea99fe9',
122 'd9bcd465040bf869799b09ad732c04e0eea99fe9',
120 '469e9c847fe1f6f7a697b8b25b4bc5b48780c1a7',
123 '469e9c847fe1f6f7a697b8b25b4bc5b48780c1a7',
121 '4fb8326d78e5120da2c7468dcf7098997be385da',
124 '4fb8326d78e5120da2c7468dcf7098997be385da',
122 '62b4a097164940bd66030c4db51687f3ec035eed',
125 '62b4a097164940bd66030c4db51687f3ec035eed',
123 '536c1a19428381cfea92ac44985304f6a8049569',
126 '536c1a19428381cfea92ac44985304f6a8049569',
124 '965e8ab3c44b070cdaa5bf727ddef0ada980ecc4',
127 '965e8ab3c44b070cdaa5bf727ddef0ada980ecc4',
125 '9bb326a04ae5d98d437dece54be04f830cf1edd9',
128 '9bb326a04ae5d98d437dece54be04f830cf1edd9',
126 'f8940bcb890a98c4702319fbe36db75ea309b475',
129 'f8940bcb890a98c4702319fbe36db75ea309b475',
127 'ff5ab059786ebc7411e559a2cc309dfae3625a3b',
130 'ff5ab059786ebc7411e559a2cc309dfae3625a3b',
128 '6b6ad5f82ad5bb6190037671bd254bd4e1f4bf08',
131 '6b6ad5f82ad5bb6190037671bd254bd4e1f4bf08',
129 'ee87846a61c12153b51543bf860e1026c6d3dcba', ]
132 'ee87846a61c12153b51543bf860e1026c6d3dcba', ]
130 self.assertEqual(org, self.repo.revisions[:31])
133 self.assertEqual(org, self.repo.revisions[:31])
131
134
132 def test_iter_slice(self):
135 def test_iter_slice(self):
133 sliced = list(self.repo[:10])
136 sliced = list(self.repo[:10])
134 itered = list(self.repo)[:10]
137 itered = list(self.repo)[:10]
135 self.assertEqual(sliced, itered)
138 self.assertEqual(sliced, itered)
136
139
137 def test_slicing(self):
140 def test_slicing(self):
138 #4 1 5 10 95
141 #4 1 5 10 95
139 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
142 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
140 (10, 20, 10), (5, 100, 95)]:
143 (10, 20, 10), (5, 100, 95)]:
141 revs = list(self.repo[sfrom:sto])
144 revs = list(self.repo[sfrom:sto])
142 self.assertEqual(len(revs), size)
145 self.assertEqual(len(revs), size)
143 self.assertEqual(revs[0], self.repo.get_changeset(sfrom))
146 self.assertEqual(revs[0], self.repo.get_changeset(sfrom))
144 self.assertEqual(revs[-1], self.repo.get_changeset(sto - 1))
147 self.assertEqual(revs[-1], self.repo.get_changeset(sto - 1))
145
148
146 def test_branches(self):
149 def test_branches(self):
147 # TODO: Need more tests here
150 # TODO: Need more tests here
148
151
149 #active branches
152 #active branches
150 self.assertTrue('default' in self.repo.branches)
153 self.assertTrue('default' in self.repo.branches)
151 self.assertTrue('stable' in self.repo.branches)
154 self.assertTrue('stable' in self.repo.branches)
152
155
153 # closed
156 # closed
154 self.assertTrue('git' in self.repo._get_branches(closed=True))
157 self.assertTrue('git' in self.repo._get_branches(closed=True))
155 self.assertTrue('web' in self.repo._get_branches(closed=True))
158 self.assertTrue('web' in self.repo._get_branches(closed=True))
156
159
157 for name, id in self.repo.branches.items():
160 for name, id in self.repo.branches.items():
158 self.assertTrue(isinstance(
161 self.assertTrue(isinstance(
159 self.repo.get_changeset(id), MercurialChangeset))
162 self.repo.get_changeset(id), MercurialChangeset))
160
163
161 def test_tip_in_tags(self):
164 def test_tip_in_tags(self):
162 # tip is always a tag
165 # tip is always a tag
163 self.assertIn('tip', self.repo.tags)
166 self.assertIn('tip', self.repo.tags)
164
167
165 def test_tip_changeset_in_tags(self):
168 def test_tip_changeset_in_tags(self):
166 tip = self.repo.get_changeset()
169 tip = self.repo.get_changeset()
167 self.assertEqual(self.repo.tags['tip'], tip.raw_id)
170 self.assertEqual(self.repo.tags['tip'], tip.raw_id)
168
171
169 def test_initial_changeset(self):
172 def test_initial_changeset(self):
170
173
171 init_chset = self.repo.get_changeset(0)
174 init_chset = self.repo.get_changeset(0)
172 self.assertEqual(init_chset.message, 'initial import')
175 self.assertEqual(init_chset.message, 'initial import')
173 self.assertEqual(init_chset.author,
176 self.assertEqual(init_chset.author,
174 'Marcin Kuzminski <marcin@python-blog.com>')
177 'Marcin Kuzminski <marcin@python-blog.com>')
175 self.assertEqual(sorted(init_chset._file_paths),
178 self.assertEqual(sorted(init_chset._file_paths),
176 sorted([
179 sorted([
177 'vcs/__init__.py',
180 'vcs/__init__.py',
178 'vcs/backends/BaseRepository.py',
181 'vcs/backends/BaseRepository.py',
179 'vcs/backends/__init__.py',
182 'vcs/backends/__init__.py',
180 ])
183 ])
181 )
184 )
182 self.assertEqual(sorted(init_chset._dir_paths),
185 self.assertEqual(sorted(init_chset._dir_paths),
183 sorted(['', 'vcs', 'vcs/backends']))
186 sorted(['', 'vcs', 'vcs/backends']))
184
187
185 self.assertRaises(NodeDoesNotExistError, init_chset.get_node, path='foobar')
188 self.assertRaises(NodeDoesNotExistError, init_chset.get_node, path='foobar')
186
189
187 node = init_chset.get_node('vcs/')
190 node = init_chset.get_node('vcs/')
188 self.assertTrue(hasattr(node, 'kind'))
191 self.assertTrue(hasattr(node, 'kind'))
189 self.assertEqual(node.kind, NodeKind.DIR)
192 self.assertEqual(node.kind, NodeKind.DIR)
190
193
191 node = init_chset.get_node('vcs')
194 node = init_chset.get_node('vcs')
192 self.assertTrue(hasattr(node, 'kind'))
195 self.assertTrue(hasattr(node, 'kind'))
193 self.assertEqual(node.kind, NodeKind.DIR)
196 self.assertEqual(node.kind, NodeKind.DIR)
194
197
195 node = init_chset.get_node('vcs/__init__.py')
198 node = init_chset.get_node('vcs/__init__.py')
196 self.assertTrue(hasattr(node, 'kind'))
199 self.assertTrue(hasattr(node, 'kind'))
197 self.assertEqual(node.kind, NodeKind.FILE)
200 self.assertEqual(node.kind, NodeKind.FILE)
198
201
199 def test_not_existing_changeset(self):
202 def test_not_existing_changeset(self):
200 #rawid
203 #rawid
201 self.assertRaises(RepositoryError, self.repo.get_changeset,
204 self.assertRaises(RepositoryError, self.repo.get_changeset,
202 'abcd' * 10)
205 'abcd' * 10)
203 #shortid
206 #shortid
204 self.assertRaises(RepositoryError, self.repo.get_changeset,
207 self.assertRaises(RepositoryError, self.repo.get_changeset,
205 'erro' * 4)
208 'erro' * 4)
206 #numeric
209 #numeric
207 self.assertRaises(RepositoryError, self.repo.get_changeset,
210 self.assertRaises(RepositoryError, self.repo.get_changeset,
208 self.repo.count() + 1)
211 self.repo.count() + 1)
209
212
210
213
211 # Small chance we ever get to this one
214 # Small chance we ever get to this one
212 revision = pow(2, 30)
215 revision = pow(2, 30)
213 self.assertRaises(RepositoryError, self.repo.get_changeset, revision)
216 self.assertRaises(RepositoryError, self.repo.get_changeset, revision)
214
217
215 def test_changeset10(self):
218 def test_changeset10(self):
216
219
217 chset10 = self.repo.get_changeset(10)
220 chset10 = self.repo.get_changeset(10)
218 README = """===
221 README = """===
219 VCS
222 VCS
220 ===
223 ===
221
224
222 Various Version Control System management abstraction layer for Python.
225 Various Version Control System management abstraction layer for Python.
223
226
224 Introduction
227 Introduction
225 ------------
228 ------------
226
229
227 TODO: To be written...
230 TODO: To be written...
228
231
229 """
232 """
230 node = chset10.get_node('README.rst')
233 node = chset10.get_node('README.rst')
231 self.assertEqual(node.kind, NodeKind.FILE)
234 self.assertEqual(node.kind, NodeKind.FILE)
232 self.assertEqual(node.content, README)
235 self.assertEqual(node.content, README)
233
236
237 @mock.patch('kallithea.lib.vcs.backends.hg.repository.diffopts')
238 def test_get_diff_does_not_sanitize_zero_context(self, mock_diffopts):
239 zero_context = 0
240
241 self.repo.get_diff(0, 1, 'foo', context=zero_context)
242
243 mock_diffopts.assert_called_once_with(git=True, showfunc=True, ignorews=False, context=zero_context)
244
245 @mock.patch('kallithea.lib.vcs.backends.hg.repository.diffopts')
246 def test_get_diff_sanitizes_negative_context(self, mock_diffopts):
247 negative_context = -10
248 zero_context = 0
249
250 self.repo.get_diff(0, 1, 'foo', context=negative_context)
251
252 mock_diffopts.assert_called_once_with(git=True, showfunc=True, ignorews=False, context=zero_context)
253
234
254
235 class MercurialChangesetTest(unittest.TestCase):
255 class MercurialChangesetTest(unittest.TestCase):
236
256
237 def setUp(self):
257 def setUp(self):
238 self.repo = MercurialRepository(TEST_HG_REPO)
258 self.repo = MercurialRepository(TEST_HG_REPO)
239
259
240 def _test_equality(self, changeset):
260 def _test_equality(self, changeset):
241 revision = changeset.revision
261 revision = changeset.revision
242 self.assertEqual(changeset, self.repo.get_changeset(revision))
262 self.assertEqual(changeset, self.repo.get_changeset(revision))
243
263
244 def test_equality(self):
264 def test_equality(self):
245 self.setUp()
265 self.setUp()
246 revs = [0, 10, 20]
266 revs = [0, 10, 20]
247 changesets = [self.repo.get_changeset(rev) for rev in revs]
267 changesets = [self.repo.get_changeset(rev) for rev in revs]
248 for changeset in changesets:
268 for changeset in changesets:
249 self._test_equality(changeset)
269 self._test_equality(changeset)
250
270
251 def test_default_changeset(self):
271 def test_default_changeset(self):
252 tip = self.repo.get_changeset('tip')
272 tip = self.repo.get_changeset('tip')
253 self.assertEqual(tip, self.repo.get_changeset())
273 self.assertEqual(tip, self.repo.get_changeset())
254 self.assertEqual(tip, self.repo.get_changeset(revision=None))
274 self.assertEqual(tip, self.repo.get_changeset(revision=None))
255 self.assertEqual(tip, list(self.repo[-1:])[0])
275 self.assertEqual(tip, list(self.repo[-1:])[0])
256
276
257 def test_root_node(self):
277 def test_root_node(self):
258 tip = self.repo.get_changeset('tip')
278 tip = self.repo.get_changeset('tip')
259 self.assertTrue(tip.root is tip.get_node(''))
279 self.assertTrue(tip.root is tip.get_node(''))
260
280
261 def test_lazy_fetch(self):
281 def test_lazy_fetch(self):
262 """
282 """
263 Test if changeset's nodes expands and are cached as we walk through
283 Test if changeset's nodes expands and are cached as we walk through
264 the revision. This test is somewhat hard to write as order of tests
284 the revision. This test is somewhat hard to write as order of tests
265 is a key here. Written by running command after command in a shell.
285 is a key here. Written by running command after command in a shell.
266 """
286 """
267 self.setUp()
287 self.setUp()
268 chset = self.repo.get_changeset(45)
288 chset = self.repo.get_changeset(45)
269 self.assertTrue(len(chset.nodes) == 0)
289 self.assertTrue(len(chset.nodes) == 0)
270 root = chset.root
290 root = chset.root
271 self.assertTrue(len(chset.nodes) == 1)
291 self.assertTrue(len(chset.nodes) == 1)
272 self.assertTrue(len(root.nodes) == 8)
292 self.assertTrue(len(root.nodes) == 8)
273 # accessing root.nodes updates chset.nodes
293 # accessing root.nodes updates chset.nodes
274 self.assertTrue(len(chset.nodes) == 9)
294 self.assertTrue(len(chset.nodes) == 9)
275
295
276 docs = root.get_node('docs')
296 docs = root.get_node('docs')
277 # we haven't yet accessed anything new as docs dir was already cached
297 # we haven't yet accessed anything new as docs dir was already cached
278 self.assertTrue(len(chset.nodes) == 9)
298 self.assertTrue(len(chset.nodes) == 9)
279 self.assertTrue(len(docs.nodes) == 8)
299 self.assertTrue(len(docs.nodes) == 8)
280 # accessing docs.nodes updates chset.nodes
300 # accessing docs.nodes updates chset.nodes
281 self.assertTrue(len(chset.nodes) == 17)
301 self.assertTrue(len(chset.nodes) == 17)
282
302
283 self.assertTrue(docs is chset.get_node('docs'))
303 self.assertTrue(docs is chset.get_node('docs'))
284 self.assertTrue(docs is root.nodes[0])
304 self.assertTrue(docs is root.nodes[0])
285 self.assertTrue(docs is root.dirs[0])
305 self.assertTrue(docs is root.dirs[0])
286 self.assertTrue(docs is chset.get_node('docs'))
306 self.assertTrue(docs is chset.get_node('docs'))
287
307
288 def test_nodes_with_changeset(self):
308 def test_nodes_with_changeset(self):
289 self.setUp()
309 self.setUp()
290 chset = self.repo.get_changeset(45)
310 chset = self.repo.get_changeset(45)
291 root = chset.root
311 root = chset.root
292 docs = root.get_node('docs')
312 docs = root.get_node('docs')
293 self.assertTrue(docs is chset.get_node('docs'))
313 self.assertTrue(docs is chset.get_node('docs'))
294 api = docs.get_node('api')
314 api = docs.get_node('api')
295 self.assertTrue(api is chset.get_node('docs/api'))
315 self.assertTrue(api is chset.get_node('docs/api'))
296 index = api.get_node('index.rst')
316 index = api.get_node('index.rst')
297 self.assertTrue(index is chset.get_node('docs/api/index.rst'))
317 self.assertTrue(index is chset.get_node('docs/api/index.rst'))
298 self.assertTrue(index is chset.get_node('docs')\
318 self.assertTrue(index is chset.get_node('docs')\
299 .get_node('api')\
319 .get_node('api')\
300 .get_node('index.rst'))
320 .get_node('index.rst'))
301
321
302 def test_branch_and_tags(self):
322 def test_branch_and_tags(self):
303 chset0 = self.repo.get_changeset(0)
323 chset0 = self.repo.get_changeset(0)
304 self.assertEqual(chset0.branch, 'default')
324 self.assertEqual(chset0.branch, 'default')
305 self.assertEqual(chset0.tags, [])
325 self.assertEqual(chset0.tags, [])
306
326
307 chset10 = self.repo.get_changeset(10)
327 chset10 = self.repo.get_changeset(10)
308 self.assertEqual(chset10.branch, 'default')
328 self.assertEqual(chset10.branch, 'default')
309 self.assertEqual(chset10.tags, [])
329 self.assertEqual(chset10.tags, [])
310
330
311 chset44 = self.repo.get_changeset(44)
331 chset44 = self.repo.get_changeset(44)
312 self.assertEqual(chset44.branch, 'web')
332 self.assertEqual(chset44.branch, 'web')
313
333
314 tip = self.repo.get_changeset('tip')
334 tip = self.repo.get_changeset('tip')
315 self.assertTrue('tip' in tip.tags)
335 self.assertTrue('tip' in tip.tags)
316
336
317 def _test_file_size(self, revision, path, size):
337 def _test_file_size(self, revision, path, size):
318 node = self.repo.get_changeset(revision).get_node(path)
338 node = self.repo.get_changeset(revision).get_node(path)
319 self.assertTrue(node.is_file())
339 self.assertTrue(node.is_file())
320 self.assertEqual(node.size, size)
340 self.assertEqual(node.size, size)
321
341
322 def test_file_size(self):
342 def test_file_size(self):
323 to_check = (
343 to_check = (
324 (10, 'setup.py', 1068),
344 (10, 'setup.py', 1068),
325 (20, 'setup.py', 1106),
345 (20, 'setup.py', 1106),
326 (60, 'setup.py', 1074),
346 (60, 'setup.py', 1074),
327
347
328 (10, 'vcs/backends/base.py', 2921),
348 (10, 'vcs/backends/base.py', 2921),
329 (20, 'vcs/backends/base.py', 3936),
349 (20, 'vcs/backends/base.py', 3936),
330 (60, 'vcs/backends/base.py', 6189),
350 (60, 'vcs/backends/base.py', 6189),
331 )
351 )
332 for revision, path, size in to_check:
352 for revision, path, size in to_check:
333 self._test_file_size(revision, path, size)
353 self._test_file_size(revision, path, size)
334
354
335 def _test_dir_size(self, revision, path, size):
355 def _test_dir_size(self, revision, path, size):
336 node = self.repo.get_changeset(revision).get_node(path)
356 node = self.repo.get_changeset(revision).get_node(path)
337 self.assertFalse(node.is_file())
357 self.assertFalse(node.is_file())
338 self.assertEqual(node.size, size)
358 self.assertEqual(node.size, size)
339
359
340 def test_dir_size(self):
360 def test_dir_size(self):
341 to_check = (
361 to_check = (
342 ('96507bd11ecc', '/', 682421),
362 ('96507bd11ecc', '/', 682421),
343 ('a53d9201d4bc', '/', 682410),
363 ('a53d9201d4bc', '/', 682410),
344 ('90243de06161', '/', 682006),
364 ('90243de06161', '/', 682006),
345 )
365 )
346 for revision, path, size in to_check:
366 for revision, path, size in to_check:
347 self._test_dir_size(revision, path, size)
367 self._test_dir_size(revision, path, size)
348
368
349 def test_repo_size(self):
369 def test_repo_size(self):
350 self.assertEqual(self.repo.size, 682421)
370 self.assertEqual(self.repo.size, 682421)
351
371
352 def test_file_history(self):
372 def test_file_history(self):
353 # we can only check if those revisions are present in the history
373 # we can only check if those revisions are present in the history
354 # as we cannot update this test every time file is changed
374 # as we cannot update this test every time file is changed
355 files = {
375 files = {
356 'setup.py': [7, 18, 45, 46, 47, 69, 77],
376 'setup.py': [7, 18, 45, 46, 47, 69, 77],
357 'vcs/nodes.py': [7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60,
377 'vcs/nodes.py': [7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60,
358 61, 73, 76],
378 61, 73, 76],
359 'vcs/backends/hg.py': [4, 5, 6, 11, 12, 13, 14, 15, 16, 21, 22, 23,
379 'vcs/backends/hg.py': [4, 5, 6, 11, 12, 13, 14, 15, 16, 21, 22, 23,
360 26, 27, 28, 30, 31, 33, 35, 36, 37, 38, 39, 40, 41, 44, 45, 47,
380 26, 27, 28, 30, 31, 33, 35, 36, 37, 38, 39, 40, 41, 44, 45, 47,
361 48, 49, 53, 54, 55, 58, 60, 61, 67, 68, 69, 70, 73, 77, 78, 79,
381 48, 49, 53, 54, 55, 58, 60, 61, 67, 68, 69, 70, 73, 77, 78, 79,
362 82],
382 82],
363 }
383 }
364 for path, revs in files.items():
384 for path, revs in files.items():
365 tip = self.repo.get_changeset(revs[-1])
385 tip = self.repo.get_changeset(revs[-1])
366 node = tip.get_node(path)
386 node = tip.get_node(path)
367 node_revs = [chset.revision for chset in node.history]
387 node_revs = [chset.revision for chset in node.history]
368 self.assertTrue(set(revs).issubset(set(node_revs)),
388 self.assertTrue(set(revs).issubset(set(node_revs)),
369 "We assumed that %s is subset of revisions for which file %s "
389 "We assumed that %s is subset of revisions for which file %s "
370 "has been changed, and history of that node returned: %s"
390 "has been changed, and history of that node returned: %s"
371 % (revs, path, node_revs))
391 % (revs, path, node_revs))
372
392
373 def test_file_annotate(self):
393 def test_file_annotate(self):
374 files = {
394 files = {
375 'vcs/backends/__init__.py':
395 'vcs/backends/__init__.py':
376 {89: {'lines_no': 31,
396 {89: {'lines_no': 31,
377 'changesets': [32, 32, 61, 32, 32, 37, 32, 32, 32, 44,
397 'changesets': [32, 32, 61, 32, 32, 37, 32, 32, 32, 44,
378 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
398 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
379 32, 32, 32, 32, 37, 32, 37, 37, 32,
399 32, 32, 32, 32, 37, 32, 37, 37, 32,
380 32, 32]},
400 32, 32]},
381 20: {'lines_no': 1,
401 20: {'lines_no': 1,
382 'changesets': [4]},
402 'changesets': [4]},
383 55: {'lines_no': 31,
403 55: {'lines_no': 31,
384 'changesets': [32, 32, 45, 32, 32, 37, 32, 32, 32, 44,
404 'changesets': [32, 32, 45, 32, 32, 37, 32, 32, 32, 44,
385 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
405 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
386 32, 32, 32, 32, 37, 32, 37, 37, 32,
406 32, 32, 32, 32, 37, 32, 37, 37, 32,
387 32, 32]}},
407 32, 32]}},
388 'vcs/exceptions.py':
408 'vcs/exceptions.py':
389 {89: {'lines_no': 18,
409 {89: {'lines_no': 18,
390 'changesets': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
410 'changesets': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
391 16, 16, 17, 16, 16, 18, 18, 18]},
411 16, 16, 17, 16, 16, 18, 18, 18]},
392 20: {'lines_no': 18,
412 20: {'lines_no': 18,
393 'changesets': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
413 'changesets': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
394 16, 16, 17, 16, 16, 18, 18, 18]},
414 16, 16, 17, 16, 16, 18, 18, 18]},
395 55: {'lines_no': 18, 'changesets': [16, 16, 16, 16, 16, 16,
415 55: {'lines_no': 18, 'changesets': [16, 16, 16, 16, 16, 16,
396 16, 16, 16, 16, 16, 16,
416 16, 16, 16, 16, 16, 16,
397 17, 16, 16, 18, 18, 18]}},
417 17, 16, 16, 18, 18, 18]}},
398 'MANIFEST.in': {89: {'lines_no': 5,
418 'MANIFEST.in': {89: {'lines_no': 5,
399 'changesets': [7, 7, 7, 71, 71]},
419 'changesets': [7, 7, 7, 71, 71]},
400 20: {'lines_no': 3,
420 20: {'lines_no': 3,
401 'changesets': [7, 7, 7]},
421 'changesets': [7, 7, 7]},
402 55: {'lines_no': 3,
422 55: {'lines_no': 3,
403 'changesets': [7, 7, 7]}}}
423 'changesets': [7, 7, 7]}}}
404
424
405 for fname, revision_dict in files.items():
425 for fname, revision_dict in files.items():
406 for rev, data in revision_dict.items():
426 for rev, data in revision_dict.items():
407 cs = self.repo.get_changeset(rev)
427 cs = self.repo.get_changeset(rev)
408 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
428 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
409 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
429 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
410 self.assertEqual(l1_1, l1_2)
430 self.assertEqual(l1_1, l1_2)
411 l1 = l1_2 = [x[2]().revision for x in cs.get_file_annotate(fname)]
431 l1 = l1_2 = [x[2]().revision for x in cs.get_file_annotate(fname)]
412 l2 = files[fname][rev]['changesets']
432 l2 = files[fname][rev]['changesets']
413 self.assertTrue(l1 == l2 , "The lists of revision for %s@rev%s"
433 self.assertTrue(l1 == l2 , "The lists of revision for %s@rev%s"
414 "from annotation list should match each other,"
434 "from annotation list should match each other,"
415 "got \n%s \nvs \n%s " % (fname, rev, l1, l2))
435 "got \n%s \nvs \n%s " % (fname, rev, l1, l2))
416
436
417 def test_changeset_state(self):
437 def test_changeset_state(self):
418 """
438 """
419 Tests which files have been added/changed/removed at particular revision
439 Tests which files have been added/changed/removed at particular revision
420 """
440 """
421
441
422 # rev 46ad32a4f974:
442 # rev 46ad32a4f974:
423 # hg st --rev 46ad32a4f974
443 # hg st --rev 46ad32a4f974
424 # changed: 13
444 # changed: 13
425 # added: 20
445 # added: 20
426 # removed: 1
446 # removed: 1
427 changed = set(['.hgignore'
447 changed = set(['.hgignore'
428 , 'README.rst' , 'docs/conf.py' , 'docs/index.rst' , 'setup.py'
448 , 'README.rst' , 'docs/conf.py' , 'docs/index.rst' , 'setup.py'
429 , 'tests/test_hg.py' , 'tests/test_nodes.py' , 'vcs/__init__.py'
449 , 'tests/test_hg.py' , 'tests/test_nodes.py' , 'vcs/__init__.py'
430 , 'vcs/backends/__init__.py' , 'vcs/backends/base.py'
450 , 'vcs/backends/__init__.py' , 'vcs/backends/base.py'
431 , 'vcs/backends/hg.py' , 'vcs/nodes.py' , 'vcs/utils/__init__.py'])
451 , 'vcs/backends/hg.py' , 'vcs/nodes.py' , 'vcs/utils/__init__.py'])
432
452
433 added = set(['docs/api/backends/hg.rst'
453 added = set(['docs/api/backends/hg.rst'
434 , 'docs/api/backends/index.rst' , 'docs/api/index.rst'
454 , 'docs/api/backends/index.rst' , 'docs/api/index.rst'
435 , 'docs/api/nodes.rst' , 'docs/api/web/index.rst'
455 , 'docs/api/nodes.rst' , 'docs/api/web/index.rst'
436 , 'docs/api/web/simplevcs.rst' , 'docs/installation.rst'
456 , 'docs/api/web/simplevcs.rst' , 'docs/installation.rst'
437 , 'docs/quickstart.rst' , 'setup.cfg' , 'vcs/utils/baseui_config.py'
457 , 'docs/quickstart.rst' , 'setup.cfg' , 'vcs/utils/baseui_config.py'
438 , 'vcs/utils/web.py' , 'vcs/web/__init__.py' , 'vcs/web/exceptions.py'
458 , 'vcs/utils/web.py' , 'vcs/web/__init__.py' , 'vcs/web/exceptions.py'
439 , 'vcs/web/simplevcs/__init__.py' , 'vcs/web/simplevcs/exceptions.py'
459 , 'vcs/web/simplevcs/__init__.py' , 'vcs/web/simplevcs/exceptions.py'
440 , 'vcs/web/simplevcs/middleware.py' , 'vcs/web/simplevcs/models.py'
460 , 'vcs/web/simplevcs/middleware.py' , 'vcs/web/simplevcs/models.py'
441 , 'vcs/web/simplevcs/settings.py' , 'vcs/web/simplevcs/utils.py'
461 , 'vcs/web/simplevcs/settings.py' , 'vcs/web/simplevcs/utils.py'
442 , 'vcs/web/simplevcs/views.py'])
462 , 'vcs/web/simplevcs/views.py'])
443
463
444 removed = set(['docs/api.rst'])
464 removed = set(['docs/api.rst'])
445
465
446 chset64 = self.repo.get_changeset('46ad32a4f974')
466 chset64 = self.repo.get_changeset('46ad32a4f974')
447 self.assertEqual(set((node.path for node in chset64.added)), added)
467 self.assertEqual(set((node.path for node in chset64.added)), added)
448 self.assertEqual(set((node.path for node in chset64.changed)), changed)
468 self.assertEqual(set((node.path for node in chset64.changed)), changed)
449 self.assertEqual(set((node.path for node in chset64.removed)), removed)
469 self.assertEqual(set((node.path for node in chset64.removed)), removed)
450
470
451 # rev b090f22d27d6:
471 # rev b090f22d27d6:
452 # hg st --rev b090f22d27d6
472 # hg st --rev b090f22d27d6
453 # changed: 13
473 # changed: 13
454 # added: 20
474 # added: 20
455 # removed: 1
475 # removed: 1
456 chset88 = self.repo.get_changeset('b090f22d27d6')
476 chset88 = self.repo.get_changeset('b090f22d27d6')
457 self.assertEqual(set((node.path for node in chset88.added)), set())
477 self.assertEqual(set((node.path for node in chset88.added)), set())
458 self.assertEqual(set((node.path for node in chset88.changed)),
478 self.assertEqual(set((node.path for node in chset88.changed)),
459 set(['.hgignore']))
479 set(['.hgignore']))
460 self.assertEqual(set((node.path for node in chset88.removed)), set())
480 self.assertEqual(set((node.path for node in chset88.removed)), set())
461 #
481 #
462 # 85:
482 # 85:
463 # added: 2 ['vcs/utils/diffs.py', 'vcs/web/simplevcs/views/diffs.py']
483 # added: 2 ['vcs/utils/diffs.py', 'vcs/web/simplevcs/views/diffs.py']
464 # changed: 4 ['vcs/web/simplevcs/models.py', ...]
484 # changed: 4 ['vcs/web/simplevcs/models.py', ...]
465 # removed: 1 ['vcs/utils/web.py']
485 # removed: 1 ['vcs/utils/web.py']
466 chset85 = self.repo.get_changeset(85)
486 chset85 = self.repo.get_changeset(85)
467 self.assertEqual(set((node.path for node in chset85.added)), set([
487 self.assertEqual(set((node.path for node in chset85.added)), set([
468 'vcs/utils/diffs.py',
488 'vcs/utils/diffs.py',
469 'vcs/web/simplevcs/views/diffs.py']))
489 'vcs/web/simplevcs/views/diffs.py']))
470 self.assertEqual(set((node.path for node in chset85.changed)), set([
490 self.assertEqual(set((node.path for node in chset85.changed)), set([
471 'vcs/web/simplevcs/models.py',
491 'vcs/web/simplevcs/models.py',
472 'vcs/web/simplevcs/utils.py',
492 'vcs/web/simplevcs/utils.py',
473 'vcs/web/simplevcs/views/__init__.py',
493 'vcs/web/simplevcs/views/__init__.py',
474 'vcs/web/simplevcs/views/repository.py',
494 'vcs/web/simplevcs/views/repository.py',
475 ]))
495 ]))
476 self.assertEqual(set((node.path for node in chset85.removed)),
496 self.assertEqual(set((node.path for node in chset85.removed)),
477 set(['vcs/utils/web.py']))
497 set(['vcs/utils/web.py']))
478
498
479
499
480 def test_files_state(self):
500 def test_files_state(self):
481 """
501 """
482 Tests state of FileNodes.
502 Tests state of FileNodes.
483 """
503 """
484 chset = self.repo.get_changeset(85)
504 chset = self.repo.get_changeset(85)
485 node = chset.get_node('vcs/utils/diffs.py')
505 node = chset.get_node('vcs/utils/diffs.py')
486 self.assertTrue(node.state, NodeState.ADDED)
506 self.assertTrue(node.state, NodeState.ADDED)
487 self.assertTrue(node.added)
507 self.assertTrue(node.added)
488 self.assertFalse(node.changed)
508 self.assertFalse(node.changed)
489 self.assertFalse(node.not_changed)
509 self.assertFalse(node.not_changed)
490 self.assertFalse(node.removed)
510 self.assertFalse(node.removed)
491
511
492 chset = self.repo.get_changeset(88)
512 chset = self.repo.get_changeset(88)
493 node = chset.get_node('.hgignore')
513 node = chset.get_node('.hgignore')
494 self.assertTrue(node.state, NodeState.CHANGED)
514 self.assertTrue(node.state, NodeState.CHANGED)
495 self.assertFalse(node.added)
515 self.assertFalse(node.added)
496 self.assertTrue(node.changed)
516 self.assertTrue(node.changed)
497 self.assertFalse(node.not_changed)
517 self.assertFalse(node.not_changed)
498 self.assertFalse(node.removed)
518 self.assertFalse(node.removed)
499
519
500 chset = self.repo.get_changeset(85)
520 chset = self.repo.get_changeset(85)
501 node = chset.get_node('setup.py')
521 node = chset.get_node('setup.py')
502 self.assertTrue(node.state, NodeState.NOT_CHANGED)
522 self.assertTrue(node.state, NodeState.NOT_CHANGED)
503 self.assertFalse(node.added)
523 self.assertFalse(node.added)
504 self.assertFalse(node.changed)
524 self.assertFalse(node.changed)
505 self.assertTrue(node.not_changed)
525 self.assertTrue(node.not_changed)
506 self.assertFalse(node.removed)
526 self.assertFalse(node.removed)
507
527
508 # If node has REMOVED state then trying to fetch it would raise
528 # If node has REMOVED state then trying to fetch it would raise
509 # ChangesetError exception
529 # ChangesetError exception
510 chset = self.repo.get_changeset(2)
530 chset = self.repo.get_changeset(2)
511 path = 'vcs/backends/BaseRepository.py'
531 path = 'vcs/backends/BaseRepository.py'
512 self.assertRaises(NodeDoesNotExistError, chset.get_node, path)
532 self.assertRaises(NodeDoesNotExistError, chset.get_node, path)
513 # but it would be one of ``removed`` (changeset's attribute)
533 # but it would be one of ``removed`` (changeset's attribute)
514 self.assertTrue(path in [rf.path for rf in chset.removed])
534 self.assertTrue(path in [rf.path for rf in chset.removed])
515
535
516 def test_commit_message_is_unicode(self):
536 def test_commit_message_is_unicode(self):
517 for cm in self.repo:
537 for cm in self.repo:
518 self.assertEqual(type(cm.message), unicode)
538 self.assertEqual(type(cm.message), unicode)
519
539
520 def test_changeset_author_is_unicode(self):
540 def test_changeset_author_is_unicode(self):
521 for cm in self.repo:
541 for cm in self.repo:
522 self.assertEqual(type(cm.author), unicode)
542 self.assertEqual(type(cm.author), unicode)
523
543
524 def test_repo_files_content_is_unicode(self):
544 def test_repo_files_content_is_unicode(self):
525 test_changeset = self.repo.get_changeset(100)
545 test_changeset = self.repo.get_changeset(100)
526 for node in test_changeset.get_node('/'):
546 for node in test_changeset.get_node('/'):
527 if node.is_file():
547 if node.is_file():
528 self.assertEqual(type(node.content), unicode)
548 self.assertEqual(type(node.content), unicode)
529
549
530 def test_wrong_path(self):
550 def test_wrong_path(self):
531 # There is 'setup.py' in the root dir but not there:
551 # There is 'setup.py' in the root dir but not there:
532 path = 'foo/bar/setup.py'
552 path = 'foo/bar/setup.py'
533 self.assertRaises(VCSError, self.repo.get_changeset().get_node, path)
553 self.assertRaises(VCSError, self.repo.get_changeset().get_node, path)
534
554
535
555
536 def test_archival_file(self):
556 def test_archival_file(self):
537 #TODO:
557 #TODO:
538 pass
558 pass
539
559
540 def test_archival_as_generator(self):
560 def test_archival_as_generator(self):
541 #TODO:
561 #TODO:
542 pass
562 pass
543
563
544 def test_archival_wrong_kind(self):
564 def test_archival_wrong_kind(self):
545 tip = self.repo.get_changeset()
565 tip = self.repo.get_changeset()
546 self.assertRaises(VCSError, tip.fill_archive, kind='error')
566 self.assertRaises(VCSError, tip.fill_archive, kind='error')
547
567
548 def test_archival_empty_prefix(self):
568 def test_archival_empty_prefix(self):
549 #TODO:
569 #TODO:
550 pass
570 pass
551
571
552
572
553 def test_author_email(self):
573 def test_author_email(self):
554 self.assertEqual('marcin@python-blog.com',
574 self.assertEqual('marcin@python-blog.com',
555 self.repo.get_changeset('b986218ba1c9').author_email)
575 self.repo.get_changeset('b986218ba1c9').author_email)
556 self.assertEqual('lukasz.balcerzak@python-center.pl',
576 self.assertEqual('lukasz.balcerzak@python-center.pl',
557 self.repo.get_changeset('3803844fdbd3').author_email)
577 self.repo.get_changeset('3803844fdbd3').author_email)
558 self.assertEqual('',
578 self.assertEqual('',
559 self.repo.get_changeset('84478366594b').author_email)
579 self.repo.get_changeset('84478366594b').author_email)
560
580
561 def test_author_username(self):
581 def test_author_username(self):
562 self.assertEqual('Marcin Kuzminski',
582 self.assertEqual('Marcin Kuzminski',
563 self.repo.get_changeset('b986218ba1c9').author_name)
583 self.repo.get_changeset('b986218ba1c9').author_name)
564 self.assertEqual('Lukasz Balcerzak',
584 self.assertEqual('Lukasz Balcerzak',
565 self.repo.get_changeset('3803844fdbd3').author_name)
585 self.repo.get_changeset('3803844fdbd3').author_name)
566 self.assertEqual('marcink',
586 self.assertEqual('marcink',
567 self.repo.get_changeset('84478366594b').author_name)
587 self.repo.get_changeset('84478366594b').author_name)
General Comments 0
You need to be logged in to leave comments. Login now