##// END OF EJS Templates
Escape shell command parts for Git backend...
Daniel Anderson -
r4395:a6dfd14d default
parent child Browse files
Show More
@@ -1,744 +1,750 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git.repository
3 vcs.backends.git.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Git repository implementation.
6 Git repository implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import os
12 import os
13 import re
13 import re
14 import time
14 import time
15 import urllib
15 import urllib
16 import urllib2
16 import urllib2
17 import logging
17 import logging
18 import posixpath
18 import posixpath
19 import string
19 import string
20 try:
21 # Python <=2.7
22 from pipes import quote
23 except ImportError:
24 # Python 3.3+
25 from shlex import quote
20
26
21 from dulwich.objects import Tag
27 from dulwich.objects import Tag
22 from dulwich.repo import Repo, NotGitRepository
28 from dulwich.repo import Repo, NotGitRepository
23 from dulwich.config import ConfigFile
29 from dulwich.config import ConfigFile
24
30
25 from kallithea.lib.vcs import subprocessio
31 from kallithea.lib.vcs import subprocessio
26 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
32 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
27 from kallithea.lib.vcs.conf import settings
33 from kallithea.lib.vcs.conf import settings
28
34
29 from kallithea.lib.vcs.exceptions import (
35 from kallithea.lib.vcs.exceptions import (
30 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
36 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
31 RepositoryError, TagAlreadyExistError, TagDoesNotExistError
37 RepositoryError, TagAlreadyExistError, TagDoesNotExistError
32 )
38 )
33 from kallithea.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
39 from kallithea.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
34 from kallithea.lib.vcs.utils.lazy import LazyProperty
40 from kallithea.lib.vcs.utils.lazy import LazyProperty
35 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
41 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
36 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
42 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
37
43
38 from kallithea.lib.vcs.utils.hgcompat import (
44 from kallithea.lib.vcs.utils.hgcompat import (
39 hg_url, httpbasicauthhandler, httpdigestauthhandler
45 hg_url, httpbasicauthhandler, httpdigestauthhandler
40 )
46 )
41
47
42 from .changeset import GitChangeset
48 from .changeset import GitChangeset
43 from .inmemory import GitInMemoryChangeset
49 from .inmemory import GitInMemoryChangeset
44 from .workdir import GitWorkdir
50 from .workdir import GitWorkdir
45
51
46 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
52 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
47
53
48 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
49
55
50
56
51 class GitRepository(BaseRepository):
57 class GitRepository(BaseRepository):
52 """
58 """
53 Git repository backend.
59 Git repository backend.
54 """
60 """
55 DEFAULT_BRANCH_NAME = 'master'
61 DEFAULT_BRANCH_NAME = 'master'
56 scm = 'git'
62 scm = 'git'
57
63
58 def __init__(self, repo_path, create=False, src_url=None,
64 def __init__(self, repo_path, create=False, src_url=None,
59 update_after_clone=False, bare=False):
65 update_after_clone=False, bare=False):
60
66
61 self.path = abspath(repo_path)
67 self.path = abspath(repo_path)
62 repo = self._get_repo(create, src_url, update_after_clone, bare)
68 repo = self._get_repo(create, src_url, update_after_clone, bare)
63 self.bare = repo.bare
69 self.bare = repo.bare
64
70
65 @property
71 @property
66 def _config_files(self):
72 def _config_files(self):
67 return [
73 return [
68 self.bare and abspath(self.path, 'config')
74 self.bare and abspath(self.path, 'config')
69 or abspath(self.path, '.git', 'config'),
75 or abspath(self.path, '.git', 'config'),
70 abspath(get_user_home(), '.gitconfig'),
76 abspath(get_user_home(), '.gitconfig'),
71 ]
77 ]
72
78
73 @property
79 @property
74 def _repo(self):
80 def _repo(self):
75 return Repo(self.path)
81 return Repo(self.path)
76
82
77 @property
83 @property
78 def head(self):
84 def head(self):
79 try:
85 try:
80 return self._repo.head()
86 return self._repo.head()
81 except KeyError:
87 except KeyError:
82 return None
88 return None
83
89
84 @property
90 @property
85 def _empty(self):
91 def _empty(self):
86 """
92 """
87 Checks if repository is empty ie. without any changesets
93 Checks if repository is empty ie. without any changesets
88 """
94 """
89
95
90 try:
96 try:
91 self.revisions[0]
97 self.revisions[0]
92 except (KeyError, IndexError):
98 except (KeyError, IndexError):
93 return True
99 return True
94 return False
100 return False
95
101
96 @LazyProperty
102 @LazyProperty
97 def revisions(self):
103 def revisions(self):
98 """
104 """
99 Returns list of revisions' ids, in ascending order. Being lazy
105 Returns list of revisions' ids, in ascending order. Being lazy
100 attribute allows external tools to inject shas from cache.
106 attribute allows external tools to inject shas from cache.
101 """
107 """
102 return self._get_all_revisions()
108 return self._get_all_revisions()
103
109
104 @classmethod
110 @classmethod
105 def _run_git_command(cls, cmd, **opts):
111 def _run_git_command(cls, cmd, **opts):
106 """
112 """
107 Runs given ``cmd`` as git command and returns tuple
113 Runs given ``cmd`` as git command and returns tuple
108 (stdout, stderr).
114 (stdout, stderr).
109
115
110 :param cmd: git command to be executed
116 :param cmd: git command to be executed
111 :param opts: env options to pass into Subprocess command
117 :param opts: env options to pass into Subprocess command
112 """
118 """
113
119
114 if '_bare' in opts:
120 if '_bare' in opts:
115 _copts = []
121 _copts = []
116 del opts['_bare']
122 del opts['_bare']
117 else:
123 else:
118 _copts = ['-c', 'core.quotepath=false', ]
124 _copts = ['-c', 'core.quotepath=false', ]
119 safe_call = False
125 safe_call = False
120 if '_safe' in opts:
126 if '_safe' in opts:
121 #no exc on failure
127 #no exc on failure
122 del opts['_safe']
128 del opts['_safe']
123 safe_call = True
129 safe_call = True
124
130
125 _str_cmd = False
131 _str_cmd = False
126 if isinstance(cmd, basestring):
132 if isinstance(cmd, basestring):
127 cmd = [cmd]
133 cmd = [cmd]
128 _str_cmd = True
134 _str_cmd = True
129
135
130 gitenv = os.environ
136 gitenv = os.environ
131 # need to clean fix GIT_DIR !
137 # need to clean fix GIT_DIR !
132 if 'GIT_DIR' in gitenv:
138 if 'GIT_DIR' in gitenv:
133 del gitenv['GIT_DIR']
139 del gitenv['GIT_DIR']
134 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
140 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
135
141
136 _git_path = settings.GIT_EXECUTABLE_PATH
142 _git_path = settings.GIT_EXECUTABLE_PATH
137 cmd = [_git_path] + _copts + cmd
143 cmd = [_git_path] + _copts + cmd
138 if _str_cmd:
144 if _str_cmd:
139 cmd = ' '.join(cmd)
145 cmd = ' '.join(cmd)
140
146
141 try:
147 try:
142 _opts = dict(
148 _opts = dict(
143 env=gitenv,
149 env=gitenv,
144 shell=True,
150 shell=True,
145 )
151 )
146 _opts.update(opts)
152 _opts.update(opts)
147 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
153 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
148 except (EnvironmentError, OSError), err:
154 except (EnvironmentError, OSError), err:
149 tb_err = ("Couldn't run git command (%s).\n"
155 tb_err = ("Couldn't run git command (%s).\n"
150 "Original error was:%s\n" % (cmd, err))
156 "Original error was:%s\n" % (cmd, err))
151 log.error(tb_err)
157 log.error(tb_err)
152 if safe_call:
158 if safe_call:
153 return '', err
159 return '', err
154 else:
160 else:
155 raise RepositoryError(tb_err)
161 raise RepositoryError(tb_err)
156
162
157 return ''.join(p.output), ''.join(p.error)
163 return ''.join(p.output), ''.join(p.error)
158
164
159 def run_git_command(self, cmd):
165 def run_git_command(self, cmd):
160 opts = {}
166 opts = {}
161 if os.path.isdir(self.path):
167 if os.path.isdir(self.path):
162 opts['cwd'] = self.path
168 opts['cwd'] = self.path
163 return self._run_git_command(cmd, **opts)
169 return self._run_git_command(cmd, **opts)
164
170
165 @classmethod
171 @classmethod
166 def _check_url(cls, url):
172 def _check_url(cls, url):
167 """
173 """
168 Function will check given url and try to verify if it's a valid
174 Function will check given url and try to verify if it's a valid
169 link. Sometimes it may happened that git will issue basic
175 link. Sometimes it may happened that git will issue basic
170 auth request that can cause whole API to hang when used from python
176 auth request that can cause whole API to hang when used from python
171 or other external calls.
177 or other external calls.
172
178
173 On failures it'll raise urllib2.HTTPError, exception is also thrown
179 On failures it'll raise urllib2.HTTPError, exception is also thrown
174 when the return code is non 200
180 when the return code is non 200
175 """
181 """
176
182
177 # check first if it's not an local url
183 # check first if it's not an local url
178 if os.path.isdir(url) or url.startswith('file:'):
184 if os.path.isdir(url) or url.startswith('file:'):
179 return True
185 return True
180
186
181 if '+' in url[:url.find('://')]:
187 if '+' in url[:url.find('://')]:
182 url = url[url.find('+') + 1:]
188 url = url[url.find('+') + 1:]
183
189
184 handlers = []
190 handlers = []
185 url_obj = hg_url(url)
191 url_obj = hg_url(url)
186 test_uri, authinfo = url_obj.authinfo()
192 test_uri, authinfo = url_obj.authinfo()
187 url_obj.passwd = '*****'
193 url_obj.passwd = '*****'
188 cleaned_uri = str(url_obj)
194 cleaned_uri = str(url_obj)
189
195
190 if not test_uri.endswith('info/refs'):
196 if not test_uri.endswith('info/refs'):
191 test_uri = test_uri.rstrip('/') + '/info/refs'
197 test_uri = test_uri.rstrip('/') + '/info/refs'
192
198
193 if authinfo:
199 if authinfo:
194 #create a password manager
200 #create a password manager
195 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
201 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
196 passmgr.add_password(*authinfo)
202 passmgr.add_password(*authinfo)
197
203
198 handlers.extend((httpbasicauthhandler(passmgr),
204 handlers.extend((httpbasicauthhandler(passmgr),
199 httpdigestauthhandler(passmgr)))
205 httpdigestauthhandler(passmgr)))
200
206
201 o = urllib2.build_opener(*handlers)
207 o = urllib2.build_opener(*handlers)
202 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
208 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
203
209
204 q = {"service": 'git-upload-pack'}
210 q = {"service": 'git-upload-pack'}
205 qs = '?%s' % urllib.urlencode(q)
211 qs = '?%s' % urllib.urlencode(q)
206 cu = "%s%s" % (test_uri, qs)
212 cu = "%s%s" % (test_uri, qs)
207 req = urllib2.Request(cu, None, {})
213 req = urllib2.Request(cu, None, {})
208
214
209 try:
215 try:
210 resp = o.open(req)
216 resp = o.open(req)
211 if resp.code != 200:
217 if resp.code != 200:
212 raise Exception('Return Code is not 200')
218 raise Exception('Return Code is not 200')
213 except Exception, e:
219 except Exception, e:
214 # means it cannot be cloned
220 # means it cannot be cloned
215 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
221 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
216
222
217 # now detect if it's proper git repo
223 # now detect if it's proper git repo
218 gitdata = resp.read()
224 gitdata = resp.read()
219 if not 'service=git-upload-pack' in gitdata:
225 if not 'service=git-upload-pack' in gitdata:
220 raise urllib2.URLError(
226 raise urllib2.URLError(
221 "url [%s] does not look like an git" % (cleaned_uri))
227 "url [%s] does not look like an git" % (cleaned_uri))
222
228
223 return True
229 return True
224
230
225 def _get_repo(self, create, src_url=None, update_after_clone=False,
231 def _get_repo(self, create, src_url=None, update_after_clone=False,
226 bare=False):
232 bare=False):
227 if create and os.path.exists(self.path):
233 if create and os.path.exists(self.path):
228 raise RepositoryError("Location already exist")
234 raise RepositoryError("Location already exist")
229 if src_url and not create:
235 if src_url and not create:
230 raise RepositoryError("Create should be set to True if src_url is "
236 raise RepositoryError("Create should be set to True if src_url is "
231 "given (clone operation creates repository)")
237 "given (clone operation creates repository)")
232 try:
238 try:
233 if create and src_url:
239 if create and src_url:
234 GitRepository._check_url(src_url)
240 GitRepository._check_url(src_url)
235 self.clone(src_url, update_after_clone, bare)
241 self.clone(src_url, update_after_clone, bare)
236 return Repo(self.path)
242 return Repo(self.path)
237 elif create:
243 elif create:
238 os.makedirs(self.path)
244 os.makedirs(self.path)
239 if bare:
245 if bare:
240 return Repo.init_bare(self.path)
246 return Repo.init_bare(self.path)
241 else:
247 else:
242 return Repo.init(self.path)
248 return Repo.init(self.path)
243 else:
249 else:
244 return self._repo
250 return self._repo
245 except (NotGitRepository, OSError), err:
251 except (NotGitRepository, OSError), err:
246 raise RepositoryError(err)
252 raise RepositoryError(err)
247
253
248 def _get_all_revisions(self):
254 def _get_all_revisions(self):
249 # we must check if this repo is not empty, since later command
255 # we must check if this repo is not empty, since later command
250 # fails if it is. And it's cheaper to ask than throw the subprocess
256 # fails if it is. And it's cheaper to ask than throw the subprocess
251 # errors
257 # errors
252 try:
258 try:
253 self._repo.head()
259 self._repo.head()
254 except KeyError:
260 except KeyError:
255 return []
261 return []
256
262
257 rev_filter = _git_path = settings.GIT_REV_FILTER
263 rev_filter = _git_path = settings.GIT_REV_FILTER
258 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
264 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
259 try:
265 try:
260 so, se = self.run_git_command(cmd)
266 so, se = self.run_git_command(cmd)
261 except RepositoryError:
267 except RepositoryError:
262 # Can be raised for empty repositories
268 # Can be raised for empty repositories
263 return []
269 return []
264 return so.splitlines()
270 return so.splitlines()
265
271
266 def _get_all_revisions2(self):
272 def _get_all_revisions2(self):
267 #alternate implementation using dulwich
273 #alternate implementation using dulwich
268 includes = [x[1][0] for x in self._parsed_refs.iteritems()
274 includes = [x[1][0] for x in self._parsed_refs.iteritems()
269 if x[1][1] != 'T']
275 if x[1][1] != 'T']
270 return [c.commit.id for c in self._repo.get_walker(include=includes)]
276 return [c.commit.id for c in self._repo.get_walker(include=includes)]
271
277
272 def _get_revision(self, revision):
278 def _get_revision(self, revision):
273 """
279 """
274 For git backend we always return integer here. This way we ensure
280 For git backend we always return integer here. This way we ensure
275 that changset's revision attribute would become integer.
281 that changset's revision attribute would become integer.
276 """
282 """
277
283
278 is_null = lambda o: len(o) == revision.count('0')
284 is_null = lambda o: len(o) == revision.count('0')
279
285
280 if self._empty:
286 if self._empty:
281 raise EmptyRepositoryError("There are no changesets yet")
287 raise EmptyRepositoryError("There are no changesets yet")
282
288
283 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
289 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
284 return self.revisions[-1]
290 return self.revisions[-1]
285
291
286 is_bstr = isinstance(revision, (str, unicode))
292 is_bstr = isinstance(revision, (str, unicode))
287 if ((is_bstr and revision.isdigit() and len(revision) < 12)
293 if ((is_bstr and revision.isdigit() and len(revision) < 12)
288 or isinstance(revision, int) or is_null(revision)):
294 or isinstance(revision, int) or is_null(revision)):
289 try:
295 try:
290 revision = self.revisions[int(revision)]
296 revision = self.revisions[int(revision)]
291 except Exception:
297 except Exception:
292 msg = ("Revision %s does not exist for %s" % (revision, self))
298 msg = ("Revision %s does not exist for %s" % (revision, self))
293 raise ChangesetDoesNotExistError(msg)
299 raise ChangesetDoesNotExistError(msg)
294
300
295 elif is_bstr:
301 elif is_bstr:
296 # get by branch/tag name
302 # get by branch/tag name
297 _ref_revision = self._parsed_refs.get(revision)
303 _ref_revision = self._parsed_refs.get(revision)
298 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
304 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
299 return _ref_revision[0]
305 return _ref_revision[0]
300
306
301 _tags_shas = self.tags.values()
307 _tags_shas = self.tags.values()
302 # maybe it's a tag ? we don't have them in self.revisions
308 # maybe it's a tag ? we don't have them in self.revisions
303 if revision in _tags_shas:
309 if revision in _tags_shas:
304 return _tags_shas[_tags_shas.index(revision)]
310 return _tags_shas[_tags_shas.index(revision)]
305
311
306 elif not SHA_PATTERN.match(revision) or revision not in self.revisions:
312 elif not SHA_PATTERN.match(revision) or revision not in self.revisions:
307 msg = ("Revision %s does not exist for %s" % (revision, self))
313 msg = ("Revision %s does not exist for %s" % (revision, self))
308 raise ChangesetDoesNotExistError(msg)
314 raise ChangesetDoesNotExistError(msg)
309
315
310 # Ensure we return full id
316 # Ensure we return full id
311 if not SHA_PATTERN.match(str(revision)):
317 if not SHA_PATTERN.match(str(revision)):
312 raise ChangesetDoesNotExistError("Given revision %s not recognized"
318 raise ChangesetDoesNotExistError("Given revision %s not recognized"
313 % revision)
319 % revision)
314 return revision
320 return revision
315
321
316 def get_ref_revision(self, ref_type, ref_name):
322 def get_ref_revision(self, ref_type, ref_name):
317 """
323 """
318 Returns ``MercurialChangeset`` object representing repository's
324 Returns ``MercurialChangeset`` object representing repository's
319 changeset at the given ``revision``.
325 changeset at the given ``revision``.
320 """
326 """
321 return self._get_revision(ref_name)
327 return self._get_revision(ref_name)
322
328
323 def _get_archives(self, archive_name='tip'):
329 def _get_archives(self, archive_name='tip'):
324
330
325 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
331 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
326 yield {"type": i[0], "extension": i[1], "node": archive_name}
332 yield {"type": i[0], "extension": i[1], "node": archive_name}
327
333
328 def _get_url(self, url):
334 def _get_url(self, url):
329 """
335 """
330 Returns normalized url. If schema is not given, would fall to
336 Returns normalized url. If schema is not given, would fall to
331 filesystem (``file:///``) schema.
337 filesystem (``file:///``) schema.
332 """
338 """
333 url = str(url)
339 url = str(url)
334 if url != 'default' and not '://' in url:
340 if url != 'default' and not '://' in url:
335 url = ':///'.join(('file', url))
341 url = ':///'.join(('file', url))
336 return url
342 return url
337
343
338 def get_hook_location(self):
344 def get_hook_location(self):
339 """
345 """
340 returns absolute path to location where hooks are stored
346 returns absolute path to location where hooks are stored
341 """
347 """
342 loc = os.path.join(self.path, 'hooks')
348 loc = os.path.join(self.path, 'hooks')
343 if not self.bare:
349 if not self.bare:
344 loc = os.path.join(self.path, '.git', 'hooks')
350 loc = os.path.join(self.path, '.git', 'hooks')
345 return loc
351 return loc
346
352
347 @LazyProperty
353 @LazyProperty
348 def name(self):
354 def name(self):
349 return os.path.basename(self.path)
355 return os.path.basename(self.path)
350
356
351 @LazyProperty
357 @LazyProperty
352 def last_change(self):
358 def last_change(self):
353 """
359 """
354 Returns last change made on this repository as datetime object
360 Returns last change made on this repository as datetime object
355 """
361 """
356 return date_fromtimestamp(self._get_mtime(), makedate()[1])
362 return date_fromtimestamp(self._get_mtime(), makedate()[1])
357
363
358 def _get_mtime(self):
364 def _get_mtime(self):
359 try:
365 try:
360 return time.mktime(self.get_changeset().date.timetuple())
366 return time.mktime(self.get_changeset().date.timetuple())
361 except RepositoryError:
367 except RepositoryError:
362 idx_loc = '' if self.bare else '.git'
368 idx_loc = '' if self.bare else '.git'
363 # fallback to filesystem
369 # fallback to filesystem
364 in_path = os.path.join(self.path, idx_loc, "index")
370 in_path = os.path.join(self.path, idx_loc, "index")
365 he_path = os.path.join(self.path, idx_loc, "HEAD")
371 he_path = os.path.join(self.path, idx_loc, "HEAD")
366 if os.path.exists(in_path):
372 if os.path.exists(in_path):
367 return os.stat(in_path).st_mtime
373 return os.stat(in_path).st_mtime
368 else:
374 else:
369 return os.stat(he_path).st_mtime
375 return os.stat(he_path).st_mtime
370
376
371 @LazyProperty
377 @LazyProperty
372 def description(self):
378 def description(self):
373 undefined_description = u'unknown'
379 undefined_description = u'unknown'
374 _desc = self._repo.get_description()
380 _desc = self._repo.get_description()
375 return safe_unicode(_desc or undefined_description)
381 return safe_unicode(_desc or undefined_description)
376
382
377 @LazyProperty
383 @LazyProperty
378 def contact(self):
384 def contact(self):
379 undefined_contact = u'Unknown'
385 undefined_contact = u'Unknown'
380 return undefined_contact
386 return undefined_contact
381
387
382 @property
388 @property
383 def branches(self):
389 def branches(self):
384 if not self.revisions:
390 if not self.revisions:
385 return {}
391 return {}
386 sortkey = lambda ctx: ctx[0]
392 sortkey = lambda ctx: ctx[0]
387 _branches = [(x[0], x[1][0])
393 _branches = [(x[0], x[1][0])
388 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
394 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
389 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
395 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
390
396
391 @LazyProperty
397 @LazyProperty
392 def closed_branches(self):
398 def closed_branches(self):
393 return {}
399 return {}
394
400
395 @LazyProperty
401 @LazyProperty
396 def tags(self):
402 def tags(self):
397 return self._get_tags()
403 return self._get_tags()
398
404
399 def _get_tags(self):
405 def _get_tags(self):
400 if not self.revisions:
406 if not self.revisions:
401 return {}
407 return {}
402
408
403 sortkey = lambda ctx: ctx[0]
409 sortkey = lambda ctx: ctx[0]
404 _tags = [(x[0], x[1][0])
410 _tags = [(x[0], x[1][0])
405 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
411 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
406 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
412 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
407
413
408 def tag(self, name, user, revision=None, message=None, date=None,
414 def tag(self, name, user, revision=None, message=None, date=None,
409 **kwargs):
415 **kwargs):
410 """
416 """
411 Creates and returns a tag for the given ``revision``.
417 Creates and returns a tag for the given ``revision``.
412
418
413 :param name: name for new tag
419 :param name: name for new tag
414 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
420 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
415 :param revision: changeset id for which new tag would be created
421 :param revision: changeset id for which new tag would be created
416 :param message: message of the tag's commit
422 :param message: message of the tag's commit
417 :param date: date of tag's commit
423 :param date: date of tag's commit
418
424
419 :raises TagAlreadyExistError: if tag with same name already exists
425 :raises TagAlreadyExistError: if tag with same name already exists
420 """
426 """
421 if name in self.tags:
427 if name in self.tags:
422 raise TagAlreadyExistError("Tag %s already exists" % name)
428 raise TagAlreadyExistError("Tag %s already exists" % name)
423 changeset = self.get_changeset(revision)
429 changeset = self.get_changeset(revision)
424 message = message or "Added tag %s for commit %s" % (name,
430 message = message or "Added tag %s for commit %s" % (name,
425 changeset.raw_id)
431 changeset.raw_id)
426 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
432 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
427
433
428 self._parsed_refs = self._get_parsed_refs()
434 self._parsed_refs = self._get_parsed_refs()
429 self.tags = self._get_tags()
435 self.tags = self._get_tags()
430 return changeset
436 return changeset
431
437
432 def remove_tag(self, name, user, message=None, date=None):
438 def remove_tag(self, name, user, message=None, date=None):
433 """
439 """
434 Removes tag with the given ``name``.
440 Removes tag with the given ``name``.
435
441
436 :param name: name of the tag to be removed
442 :param name: name of the tag to be removed
437 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
443 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
438 :param message: message of the tag's removal commit
444 :param message: message of the tag's removal commit
439 :param date: date of tag's removal commit
445 :param date: date of tag's removal commit
440
446
441 :raises TagDoesNotExistError: if tag with given name does not exists
447 :raises TagDoesNotExistError: if tag with given name does not exists
442 """
448 """
443 if name not in self.tags:
449 if name not in self.tags:
444 raise TagDoesNotExistError("Tag %s does not exist" % name)
450 raise TagDoesNotExistError("Tag %s does not exist" % name)
445 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
451 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
446 try:
452 try:
447 os.remove(tagpath)
453 os.remove(tagpath)
448 self._parsed_refs = self._get_parsed_refs()
454 self._parsed_refs = self._get_parsed_refs()
449 self.tags = self._get_tags()
455 self.tags = self._get_tags()
450 except OSError, e:
456 except OSError, e:
451 raise RepositoryError(e.strerror)
457 raise RepositoryError(e.strerror)
452
458
453 @LazyProperty
459 @LazyProperty
454 def bookmarks(self):
460 def bookmarks(self):
455 """
461 """
456 Gets bookmarks for this repository
462 Gets bookmarks for this repository
457 """
463 """
458 return {}
464 return {}
459
465
460 @LazyProperty
466 @LazyProperty
461 def _parsed_refs(self):
467 def _parsed_refs(self):
462 return self._get_parsed_refs()
468 return self._get_parsed_refs()
463
469
464 def _get_parsed_refs(self):
470 def _get_parsed_refs(self):
465 # cache the property
471 # cache the property
466 _repo = self._repo
472 _repo = self._repo
467 refs = _repo.get_refs()
473 refs = _repo.get_refs()
468 keys = [('refs/heads/', 'H'),
474 keys = [('refs/heads/', 'H'),
469 ('refs/remotes/origin/', 'RH'),
475 ('refs/remotes/origin/', 'RH'),
470 ('refs/tags/', 'T')]
476 ('refs/tags/', 'T')]
471 _refs = {}
477 _refs = {}
472 for ref, sha in refs.iteritems():
478 for ref, sha in refs.iteritems():
473 for k, type_ in keys:
479 for k, type_ in keys:
474 if ref.startswith(k):
480 if ref.startswith(k):
475 _key = ref[len(k):]
481 _key = ref[len(k):]
476 if type_ == 'T':
482 if type_ == 'T':
477 obj = _repo.get_object(sha)
483 obj = _repo.get_object(sha)
478 if isinstance(obj, Tag):
484 if isinstance(obj, Tag):
479 sha = _repo.get_object(sha).object[1]
485 sha = _repo.get_object(sha).object[1]
480 _refs[_key] = [sha, type_]
486 _refs[_key] = [sha, type_]
481 break
487 break
482 return _refs
488 return _refs
483
489
484 def _heads(self, reverse=False):
490 def _heads(self, reverse=False):
485 refs = self._repo.get_refs()
491 refs = self._repo.get_refs()
486 heads = {}
492 heads = {}
487
493
488 for key, val in refs.items():
494 for key, val in refs.items():
489 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
495 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
490 if key.startswith(ref_key):
496 if key.startswith(ref_key):
491 n = key[len(ref_key):]
497 n = key[len(ref_key):]
492 if n not in ['HEAD']:
498 if n not in ['HEAD']:
493 heads[n] = val
499 heads[n] = val
494
500
495 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
501 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
496
502
497 def get_changeset(self, revision=None):
503 def get_changeset(self, revision=None):
498 """
504 """
499 Returns ``GitChangeset`` object representing commit from git repository
505 Returns ``GitChangeset`` object representing commit from git repository
500 at the given revision or head (most recent commit) if None given.
506 at the given revision or head (most recent commit) if None given.
501 """
507 """
502 if isinstance(revision, GitChangeset):
508 if isinstance(revision, GitChangeset):
503 return revision
509 return revision
504 revision = self._get_revision(revision)
510 revision = self._get_revision(revision)
505 changeset = GitChangeset(repository=self, revision=revision)
511 changeset = GitChangeset(repository=self, revision=revision)
506 return changeset
512 return changeset
507
513
508 def get_changesets(self, start=None, end=None, start_date=None,
514 def get_changesets(self, start=None, end=None, start_date=None,
509 end_date=None, branch_name=None, reverse=False):
515 end_date=None, branch_name=None, reverse=False):
510 """
516 """
511 Returns iterator of ``GitChangeset`` objects from start to end (both
517 Returns iterator of ``GitChangeset`` objects from start to end (both
512 are inclusive), in ascending date order (unless ``reverse`` is set).
518 are inclusive), in ascending date order (unless ``reverse`` is set).
513
519
514 :param start: changeset ID, as str; first returned changeset
520 :param start: changeset ID, as str; first returned changeset
515 :param end: changeset ID, as str; last returned changeset
521 :param end: changeset ID, as str; last returned changeset
516 :param start_date: if specified, changesets with commit date less than
522 :param start_date: if specified, changesets with commit date less than
517 ``start_date`` would be filtered out from returned set
523 ``start_date`` would be filtered out from returned set
518 :param end_date: if specified, changesets with commit date greater than
524 :param end_date: if specified, changesets with commit date greater than
519 ``end_date`` would be filtered out from returned set
525 ``end_date`` would be filtered out from returned set
520 :param branch_name: if specified, changesets not reachable from given
526 :param branch_name: if specified, changesets not reachable from given
521 branch would be filtered out from returned set
527 branch would be filtered out from returned set
522 :param reverse: if ``True``, returned generator would be reversed
528 :param reverse: if ``True``, returned generator would be reversed
523 (meaning that returned changesets would have descending date order)
529 (meaning that returned changesets would have descending date order)
524
530
525 :raise BranchDoesNotExistError: If given ``branch_name`` does not
531 :raise BranchDoesNotExistError: If given ``branch_name`` does not
526 exist.
532 exist.
527 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
533 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
528 ``end`` could not be found.
534 ``end`` could not be found.
529
535
530 """
536 """
531 if branch_name and branch_name not in self.branches:
537 if branch_name and branch_name not in self.branches:
532 raise BranchDoesNotExistError("Branch '%s' not found" \
538 raise BranchDoesNotExistError("Branch '%s' not found" \
533 % branch_name)
539 % branch_name)
534 # actually we should check now if it's not an empty repo to not spaw
540 # actually we should check now if it's not an empty repo to not spaw
535 # subprocess commands
541 # subprocess commands
536 if self._empty:
542 if self._empty:
537 raise EmptyRepositoryError("There are no changesets yet")
543 raise EmptyRepositoryError("There are no changesets yet")
538
544
539 # %H at format means (full) commit hash, initial hashes are retrieved
545 # %H at format means (full) commit hash, initial hashes are retrieved
540 # in ascending date order
546 # in ascending date order
541 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
547 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
542 cmd_params = {}
548 cmd_params = {}
543 if start_date:
549 if start_date:
544 cmd_template += ' --since "$since"'
550 cmd_template += ' --since "$since"'
545 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
551 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
546 if end_date:
552 if end_date:
547 cmd_template += ' --until "$until"'
553 cmd_template += ' --until "$until"'
548 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
554 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
549 if branch_name:
555 if branch_name:
550 cmd_template += ' $branch_name'
556 cmd_template += ' $branch_name'
551 cmd_params['branch_name'] = branch_name
557 cmd_params['branch_name'] = branch_name
552 else:
558 else:
553 rev_filter = _git_path = settings.GIT_REV_FILTER
559 rev_filter = _git_path = settings.GIT_REV_FILTER
554 cmd_template += ' %s' % (rev_filter)
560 cmd_template += ' %s' % (rev_filter)
555
561
556 cmd = string.Template(cmd_template).safe_substitute(**cmd_params)
562 cmd = string.Template(cmd_template).safe_substitute(**cmd_params)
557 revs = self.run_git_command(cmd)[0].splitlines()
563 revs = self.run_git_command(cmd)[0].splitlines()
558 start_pos = 0
564 start_pos = 0
559 end_pos = len(revs)
565 end_pos = len(revs)
560 if start:
566 if start:
561 _start = self._get_revision(start)
567 _start = self._get_revision(start)
562 try:
568 try:
563 start_pos = revs.index(_start)
569 start_pos = revs.index(_start)
564 except ValueError:
570 except ValueError:
565 pass
571 pass
566
572
567 if end is not None:
573 if end is not None:
568 _end = self._get_revision(end)
574 _end = self._get_revision(end)
569 try:
575 try:
570 end_pos = revs.index(_end)
576 end_pos = revs.index(_end)
571 except ValueError:
577 except ValueError:
572 pass
578 pass
573
579
574 if None not in [start, end] and start_pos > end_pos:
580 if None not in [start, end] and start_pos > end_pos:
575 raise RepositoryError('start cannot be after end')
581 raise RepositoryError('start cannot be after end')
576
582
577 if end_pos is not None:
583 if end_pos is not None:
578 end_pos += 1
584 end_pos += 1
579
585
580 revs = revs[start_pos:end_pos]
586 revs = revs[start_pos:end_pos]
581 if reverse:
587 if reverse:
582 revs = reversed(revs)
588 revs = reversed(revs)
583 return CollectionGenerator(self, revs)
589 return CollectionGenerator(self, revs)
584
590
585 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
591 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
586 context=3):
592 context=3):
587 """
593 """
588 Returns (git like) *diff*, as plain text. Shows changes introduced by
594 Returns (git like) *diff*, as plain text. Shows changes introduced by
589 ``rev2`` since ``rev1``.
595 ``rev2`` since ``rev1``.
590
596
591 :param rev1: Entry point from which diff is shown. Can be
597 :param rev1: Entry point from which diff is shown. Can be
592 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
598 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
593 the changes since empty state of the repository until ``rev2``
599 the changes since empty state of the repository until ``rev2``
594 :param rev2: Until which revision changes should be shown.
600 :param rev2: Until which revision changes should be shown.
595 :param ignore_whitespace: If set to ``True``, would not show whitespace
601 :param ignore_whitespace: If set to ``True``, would not show whitespace
596 changes. Defaults to ``False``.
602 changes. Defaults to ``False``.
597 :param context: How many lines before/after changed lines should be
603 :param context: How many lines before/after changed lines should be
598 shown. Defaults to ``3``.
604 shown. Defaults to ``3``.
599 """
605 """
600 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
606 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
601 if ignore_whitespace:
607 if ignore_whitespace:
602 flags.append('-w')
608 flags.append('-w')
603
609
604 if hasattr(rev1, 'raw_id'):
610 if hasattr(rev1, 'raw_id'):
605 rev1 = getattr(rev1, 'raw_id')
611 rev1 = getattr(rev1, 'raw_id')
606
612
607 if hasattr(rev2, 'raw_id'):
613 if hasattr(rev2, 'raw_id'):
608 rev2 = getattr(rev2, 'raw_id')
614 rev2 = getattr(rev2, 'raw_id')
609
615
610 if rev1 == self.EMPTY_CHANGESET:
616 if rev1 == self.EMPTY_CHANGESET:
611 rev2 = self.get_changeset(rev2).raw_id
617 rev2 = self.get_changeset(rev2).raw_id
612 cmd = ' '.join(['show'] + flags + [rev2])
618 cmd = ' '.join(['show'] + flags + [rev2])
613 else:
619 else:
614 rev1 = self.get_changeset(rev1).raw_id
620 rev1 = self.get_changeset(rev1).raw_id
615 rev2 = self.get_changeset(rev2).raw_id
621 rev2 = self.get_changeset(rev2).raw_id
616 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
622 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
617
623
618 if path:
624 if path:
619 cmd += ' -- "%s"' % path
625 cmd += ' -- "%s"' % path
620
626
621 stdout, stderr = self.run_git_command(cmd)
627 stdout, stderr = self.run_git_command(cmd)
622 # If we used 'show' command, strip first few lines (until actual diff
628 # If we used 'show' command, strip first few lines (until actual diff
623 # starts)
629 # starts)
624 if rev1 == self.EMPTY_CHANGESET:
630 if rev1 == self.EMPTY_CHANGESET:
625 lines = stdout.splitlines()
631 lines = stdout.splitlines()
626 x = 0
632 x = 0
627 for line in lines:
633 for line in lines:
628 if line.startswith('diff'):
634 if line.startswith('diff'):
629 break
635 break
630 x += 1
636 x += 1
631 # Append new line just like 'diff' command do
637 # Append new line just like 'diff' command do
632 stdout = '\n'.join(lines[x:]) + '\n'
638 stdout = '\n'.join(lines[x:]) + '\n'
633 return stdout
639 return stdout
634
640
635 @LazyProperty
641 @LazyProperty
636 def in_memory_changeset(self):
642 def in_memory_changeset(self):
637 """
643 """
638 Returns ``GitInMemoryChangeset`` object for this repository.
644 Returns ``GitInMemoryChangeset`` object for this repository.
639 """
645 """
640 return GitInMemoryChangeset(self)
646 return GitInMemoryChangeset(self)
641
647
642 def clone(self, url, update_after_clone=True, bare=False):
648 def clone(self, url, update_after_clone=True, bare=False):
643 """
649 """
644 Tries to clone changes from external location.
650 Tries to clone changes from external location.
645
651
646 :param update_after_clone: If set to ``False``, git won't checkout
652 :param update_after_clone: If set to ``False``, git won't checkout
647 working directory
653 working directory
648 :param bare: If set to ``True``, repository would be cloned into
654 :param bare: If set to ``True``, repository would be cloned into
649 *bare* git repository (no working directory at all).
655 *bare* git repository (no working directory at all).
650 """
656 """
651 url = self._get_url(url)
657 url = self._get_url(url)
652 cmd = ['clone', '-q']
658 cmd = ['clone', '-q']
653 if bare:
659 if bare:
654 cmd.append('--bare')
660 cmd.append('--bare')
655 elif not update_after_clone:
661 elif not update_after_clone:
656 cmd.append('--no-checkout')
662 cmd.append('--no-checkout')
657 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
663 cmd += ['--', quote(url), self.path]
658 cmd = ' '.join(cmd)
664 cmd = ' '.join(cmd)
659 # If error occurs run_git_command raises RepositoryError already
665 # If error occurs run_git_command raises RepositoryError already
660 self.run_git_command(cmd)
666 self.run_git_command(cmd)
661
667
662 def pull(self, url):
668 def pull(self, url):
663 """
669 """
664 Tries to pull changes from external location.
670 Tries to pull changes from external location.
665 """
671 """
666 url = self._get_url(url)
672 url = self._get_url(url)
667 cmd = ['pull', "--ff-only", url]
673 cmd = ['pull', "--ff-only", quote(url)]
668 cmd = ' '.join(cmd)
674 cmd = ' '.join(cmd)
669 # If error occurs run_git_command raises RepositoryError already
675 # If error occurs run_git_command raises RepositoryError already
670 self.run_git_command(cmd)
676 self.run_git_command(cmd)
671
677
672 def fetch(self, url):
678 def fetch(self, url):
673 """
679 """
674 Tries to pull changes from external location.
680 Tries to pull changes from external location.
675 """
681 """
676 url = self._get_url(url)
682 url = self._get_url(url)
677 so, se = self.run_git_command('ls-remote -h %s' % url)
683 so, se = self.run_git_command('ls-remote -h %s' % quote(url))
678 refs = []
684 refs = []
679 for line in (x for x in so.splitlines()):
685 for line in (x for x in so.splitlines()):
680 sha, ref = line.split('\t')
686 sha, ref = line.split('\t')
681 refs.append(ref)
687 refs.append(ref)
682 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
688 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
683 cmd = '''fetch %s -- %s''' % (url, refs)
689 cmd = '''fetch %s -- %s''' % (quote(url), refs)
684 self.run_git_command(cmd)
690 self.run_git_command(cmd)
685
691
686 def _update_server_info(self):
692 def _update_server_info(self):
687 """
693 """
688 runs gits update-server-info command in this repo instance
694 runs gits update-server-info command in this repo instance
689 """
695 """
690 from dulwich.server import update_server_info
696 from dulwich.server import update_server_info
691 update_server_info(self._repo)
697 update_server_info(self._repo)
692
698
693 @LazyProperty
699 @LazyProperty
694 def workdir(self):
700 def workdir(self):
695 """
701 """
696 Returns ``Workdir`` instance for this repository.
702 Returns ``Workdir`` instance for this repository.
697 """
703 """
698 return GitWorkdir(self)
704 return GitWorkdir(self)
699
705
700 def get_config_value(self, section, name, config_file=None):
706 def get_config_value(self, section, name, config_file=None):
701 """
707 """
702 Returns configuration value for a given [``section``] and ``name``.
708 Returns configuration value for a given [``section``] and ``name``.
703
709
704 :param section: Section we want to retrieve value from
710 :param section: Section we want to retrieve value from
705 :param name: Name of configuration we want to retrieve
711 :param name: Name of configuration we want to retrieve
706 :param config_file: A path to file which should be used to retrieve
712 :param config_file: A path to file which should be used to retrieve
707 configuration from (might also be a list of file paths)
713 configuration from (might also be a list of file paths)
708 """
714 """
709 if config_file is None:
715 if config_file is None:
710 config_file = []
716 config_file = []
711 elif isinstance(config_file, basestring):
717 elif isinstance(config_file, basestring):
712 config_file = [config_file]
718 config_file = [config_file]
713
719
714 def gen_configs():
720 def gen_configs():
715 for path in config_file + self._config_files:
721 for path in config_file + self._config_files:
716 try:
722 try:
717 yield ConfigFile.from_path(path)
723 yield ConfigFile.from_path(path)
718 except (IOError, OSError, ValueError):
724 except (IOError, OSError, ValueError):
719 continue
725 continue
720
726
721 for config in gen_configs():
727 for config in gen_configs():
722 try:
728 try:
723 return config.get(section, name)
729 return config.get(section, name)
724 except KeyError:
730 except KeyError:
725 continue
731 continue
726 return None
732 return None
727
733
728 def get_user_name(self, config_file=None):
734 def get_user_name(self, config_file=None):
729 """
735 """
730 Returns user's name from global configuration file.
736 Returns user's name from global configuration file.
731
737
732 :param config_file: A path to file which should be used to retrieve
738 :param config_file: A path to file which should be used to retrieve
733 configuration from (might also be a list of file paths)
739 configuration from (might also be a list of file paths)
734 """
740 """
735 return self.get_config_value('user', 'name', config_file)
741 return self.get_config_value('user', 'name', config_file)
736
742
737 def get_user_email(self, config_file=None):
743 def get_user_email(self, config_file=None):
738 """
744 """
739 Returns user's email from global configuration file.
745 Returns user's email from global configuration file.
740
746
741 :param config_file: A path to file which should be used to retrieve
747 :param config_file: A path to file which should be used to retrieve
742 configuration from (might also be a list of file paths)
748 configuration from (might also be a list of file paths)
743 """
749 """
744 return self.get_config_value('user', 'email', config_file)
750 return self.get_config_value('user', 'email', config_file)
@@ -1,707 +1,734 b''
1 from __future__ import with_statement
1 from __future__ import with_statement
2
2
3 import os
3 import os
4 import mock
4 import mock
5 import datetime
5 import datetime
6 import urllib2
6 from kallithea.lib.vcs.backends.git import GitRepository, GitChangeset
7 from kallithea.lib.vcs.backends.git import GitRepository, GitChangeset
7 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
8 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
8 from kallithea.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState
9 from kallithea.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState
9 from kallithea.lib.vcs.utils.compat import unittest
10 from kallithea.lib.vcs.utils.compat import unittest
10 from kallithea.tests.vcs.base import BackendTestMixin
11 from kallithea.tests.vcs.base import BackendTestMixin
11 from kallithea.tests.vcs.conf import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir
12 from kallithea.tests.vcs.conf import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir
12
13
13
14
14 class GitRepositoryTest(unittest.TestCase):
15 class GitRepositoryTest(unittest.TestCase):
15
16
16 def __check_for_existing_repo(self):
17 def __check_for_existing_repo(self):
17 if os.path.exists(TEST_GIT_REPO_CLONE):
18 if os.path.exists(TEST_GIT_REPO_CLONE):
18 self.fail('Cannot test git clone repo as location %s already '
19 self.fail('Cannot test git clone repo as location %s already '
19 'exists. You should manually remove it first.'
20 'exists. You should manually remove it first.'
20 % TEST_GIT_REPO_CLONE)
21 % TEST_GIT_REPO_CLONE)
21
22
22 def setUp(self):
23 def setUp(self):
23 self.repo = GitRepository(TEST_GIT_REPO)
24 self.repo = GitRepository(TEST_GIT_REPO)
24
25
25 def test_wrong_repo_path(self):
26 def test_wrong_repo_path(self):
26 wrong_repo_path = '/tmp/errorrepo'
27 wrong_repo_path = '/tmp/errorrepo'
27 self.assertRaises(RepositoryError, GitRepository, wrong_repo_path)
28 self.assertRaises(RepositoryError, GitRepository, wrong_repo_path)
28
29
30 def test_git_cmd_injection(self):
31 remote_repo_url = 'https://github.com/codeinn/vcs.git'
32 inject_remote = '%s;%s' % (remote_repo_url, '; echo "Cake";')
33 with self.assertRaises(urllib2.URLError):
34 # Should fail because URL will be: https://github.com/codeinn/vcs.git%3B%3B%20echo%20%22Cake%22%3B
35 urlerror_fail_repo = GitRepository(get_new_dir('injection-repo'), src_url=inject_remote, update_after_clone=True, create=True)
36
37 with self.assertRaises(RepositoryError):
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)
40 clone_fail_repo.clone(inject_remote, update_after_clone=True,)
41
42 successfully_cloned = GitRepository(get_new_dir('injection-repo'), src_url=remote_repo_url, update_after_clone=True, create=True)
43 # Repo should have been created
44 self.assertFalse(successfully_cloned._repo.bare)
45
46 with self.assertRaises(RepositoryError):
47 # Should fail because URL will be invalid repo
48 inject_remote_var = '%s;%s' % (remote_repo_url, '; echo $PATH;')
49 successfully_cloned.fetch(inject_remote_var)
50
51 with self.assertRaises(RepositoryError):
52 # Should fail because URL will be invalid repo
53 inject_remote_ls = '%s;%s' % (remote_repo_url, '; ls -1 ~;')
54 successfully_cloned.pull(inject_remote_ls)
55
29 def test_repo_clone(self):
56 def test_repo_clone(self):
30 self.__check_for_existing_repo()
57 self.__check_for_existing_repo()
31 repo = GitRepository(TEST_GIT_REPO)
58 repo = GitRepository(TEST_GIT_REPO)
32 repo_clone = GitRepository(TEST_GIT_REPO_CLONE,
59 repo_clone = GitRepository(TEST_GIT_REPO_CLONE,
33 src_url=TEST_GIT_REPO, create=True, update_after_clone=True)
60 src_url=TEST_GIT_REPO, create=True, update_after_clone=True)
34 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
61 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
35 # Checking hashes of changesets should be enough
62 # Checking hashes of changesets should be enough
36 for changeset in repo.get_changesets():
63 for changeset in repo.get_changesets():
37 raw_id = changeset.raw_id
64 raw_id = changeset.raw_id
38 self.assertEqual(raw_id, repo_clone.get_changeset(raw_id).raw_id)
65 self.assertEqual(raw_id, repo_clone.get_changeset(raw_id).raw_id)
39
66
40 def test_repo_clone_without_create(self):
67 def test_repo_clone_without_create(self):
41 self.assertRaises(RepositoryError, GitRepository,
68 self.assertRaises(RepositoryError, GitRepository,
42 TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO)
69 TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO)
43
70
44 def test_repo_clone_with_update(self):
71 def test_repo_clone_with_update(self):
45 repo = GitRepository(TEST_GIT_REPO)
72 repo = GitRepository(TEST_GIT_REPO)
46 clone_path = TEST_GIT_REPO_CLONE + '_with_update'
73 clone_path = TEST_GIT_REPO_CLONE + '_with_update'
47 repo_clone = GitRepository(clone_path,
74 repo_clone = GitRepository(clone_path,
48 create=True, src_url=TEST_GIT_REPO, update_after_clone=True)
75 create=True, src_url=TEST_GIT_REPO, update_after_clone=True)
49 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
76 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
50
77
51 #check if current workdir was updated
78 #check if current workdir was updated
52 fpath = os.path.join(clone_path, 'MANIFEST.in')
79 fpath = os.path.join(clone_path, 'MANIFEST.in')
53 self.assertEqual(True, os.path.isfile(fpath),
80 self.assertEqual(True, os.path.isfile(fpath),
54 'Repo was cloned and updated but file %s could not be found'
81 'Repo was cloned and updated but file %s could not be found'
55 % fpath)
82 % fpath)
56
83
57 def test_repo_clone_without_update(self):
84 def test_repo_clone_without_update(self):
58 repo = GitRepository(TEST_GIT_REPO)
85 repo = GitRepository(TEST_GIT_REPO)
59 clone_path = TEST_GIT_REPO_CLONE + '_without_update'
86 clone_path = TEST_GIT_REPO_CLONE + '_without_update'
60 repo_clone = GitRepository(clone_path,
87 repo_clone = GitRepository(clone_path,
61 create=True, src_url=TEST_GIT_REPO, update_after_clone=False)
88 create=True, src_url=TEST_GIT_REPO, update_after_clone=False)
62 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
89 self.assertEqual(len(repo.revisions), len(repo_clone.revisions))
63 #check if current workdir was *NOT* updated
90 #check if current workdir was *NOT* updated
64 fpath = os.path.join(clone_path, 'MANIFEST.in')
91 fpath = os.path.join(clone_path, 'MANIFEST.in')
65 # Make sure it's not bare repo
92 # Make sure it's not bare repo
66 self.assertFalse(repo_clone._repo.bare)
93 self.assertFalse(repo_clone._repo.bare)
67 self.assertEqual(False, os.path.isfile(fpath),
94 self.assertEqual(False, os.path.isfile(fpath),
68 'Repo was cloned and updated but file %s was found'
95 'Repo was cloned and updated but file %s was found'
69 % fpath)
96 % fpath)
70
97
71 def test_repo_clone_into_bare_repo(self):
98 def test_repo_clone_into_bare_repo(self):
72 repo = GitRepository(TEST_GIT_REPO)
99 repo = GitRepository(TEST_GIT_REPO)
73 clone_path = TEST_GIT_REPO_CLONE + '_bare.git'
100 clone_path = TEST_GIT_REPO_CLONE + '_bare.git'
74 repo_clone = GitRepository(clone_path, create=True,
101 repo_clone = GitRepository(clone_path, create=True,
75 src_url=repo.path, bare=True)
102 src_url=repo.path, bare=True)
76 self.assertTrue(repo_clone._repo.bare)
103 self.assertTrue(repo_clone._repo.bare)
77
104
78 def test_create_repo_is_not_bare_by_default(self):
105 def test_create_repo_is_not_bare_by_default(self):
79 repo = GitRepository(get_new_dir('not-bare-by-default'), create=True)
106 repo = GitRepository(get_new_dir('not-bare-by-default'), create=True)
80 self.assertFalse(repo._repo.bare)
107 self.assertFalse(repo._repo.bare)
81
108
82 def test_create_bare_repo(self):
109 def test_create_bare_repo(self):
83 repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True)
110 repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True)
84 self.assertTrue(repo._repo.bare)
111 self.assertTrue(repo._repo.bare)
85
112
86 def test_revisions(self):
113 def test_revisions(self):
87 # there are 112 revisions (by now)
114 # there are 112 revisions (by now)
88 # so we can assume they would be available from now on
115 # so we can assume they would be available from now on
89 subset = set([
116 subset = set([
90 'c1214f7e79e02fc37156ff215cd71275450cffc3',
117 'c1214f7e79e02fc37156ff215cd71275450cffc3',
91 '38b5fe81f109cb111f549bfe9bb6b267e10bc557',
118 '38b5fe81f109cb111f549bfe9bb6b267e10bc557',
92 'fa6600f6848800641328adbf7811fd2372c02ab2',
119 'fa6600f6848800641328adbf7811fd2372c02ab2',
93 '102607b09cdd60e2793929c4f90478be29f85a17',
120 '102607b09cdd60e2793929c4f90478be29f85a17',
94 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
121 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
95 '2d1028c054665b962fa3d307adfc923ddd528038',
122 '2d1028c054665b962fa3d307adfc923ddd528038',
96 'd7e0d30fbcae12c90680eb095a4f5f02505ce501',
123 'd7e0d30fbcae12c90680eb095a4f5f02505ce501',
97 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
124 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
98 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
125 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
99 '8430a588b43b5d6da365400117c89400326e7992',
126 '8430a588b43b5d6da365400117c89400326e7992',
100 'd955cd312c17b02143c04fa1099a352b04368118',
127 'd955cd312c17b02143c04fa1099a352b04368118',
101 'f67b87e5c629c2ee0ba58f85197e423ff28d735b',
128 'f67b87e5c629c2ee0ba58f85197e423ff28d735b',
102 'add63e382e4aabc9e1afdc4bdc24506c269b7618',
129 'add63e382e4aabc9e1afdc4bdc24506c269b7618',
103 'f298fe1189f1b69779a4423f40b48edf92a703fc',
130 'f298fe1189f1b69779a4423f40b48edf92a703fc',
104 'bd9b619eb41994cac43d67cf4ccc8399c1125808',
131 'bd9b619eb41994cac43d67cf4ccc8399c1125808',
105 '6e125e7c890379446e98980d8ed60fba87d0f6d1',
132 '6e125e7c890379446e98980d8ed60fba87d0f6d1',
106 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd',
133 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd',
107 '0b05e4ed56c802098dfc813cbe779b2f49e92500',
134 '0b05e4ed56c802098dfc813cbe779b2f49e92500',
108 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
135 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
109 '45223f8f114c64bf4d6f853e3c35a369a6305520',
136 '45223f8f114c64bf4d6f853e3c35a369a6305520',
110 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
137 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
111 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68',
138 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68',
112 '27d48942240f5b91dfda77accd2caac94708cc7d',
139 '27d48942240f5b91dfda77accd2caac94708cc7d',
113 '622f0eb0bafd619d2560c26f80f09e3b0b0d78af',
140 '622f0eb0bafd619d2560c26f80f09e3b0b0d78af',
114 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'])
141 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'])
115 self.assertTrue(subset.issubset(set(self.repo.revisions)))
142 self.assertTrue(subset.issubset(set(self.repo.revisions)))
116
143
117
144
118
145
119 def test_slicing(self):
146 def test_slicing(self):
120 #4 1 5 10 95
147 #4 1 5 10 95
121 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
148 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
122 (10, 20, 10), (5, 100, 95)]:
149 (10, 20, 10), (5, 100, 95)]:
123 revs = list(self.repo[sfrom:sto])
150 revs = list(self.repo[sfrom:sto])
124 self.assertEqual(len(revs), size)
151 self.assertEqual(len(revs), size)
125 self.assertEqual(revs[0], self.repo.get_changeset(sfrom))
152 self.assertEqual(revs[0], self.repo.get_changeset(sfrom))
126 self.assertEqual(revs[-1], self.repo.get_changeset(sto - 1))
153 self.assertEqual(revs[-1], self.repo.get_changeset(sto - 1))
127
154
128
155
129 def test_branches(self):
156 def test_branches(self):
130 # TODO: Need more tests here
157 # TODO: Need more tests here
131 # Removed (those are 'remotes' branches for cloned repo)
158 # Removed (those are 'remotes' branches for cloned repo)
132 #self.assertTrue('master' in self.repo.branches)
159 #self.assertTrue('master' in self.repo.branches)
133 #self.assertTrue('gittree' in self.repo.branches)
160 #self.assertTrue('gittree' in self.repo.branches)
134 #self.assertTrue('web-branch' in self.repo.branches)
161 #self.assertTrue('web-branch' in self.repo.branches)
135 for name, id in self.repo.branches.items():
162 for name, id in self.repo.branches.items():
136 self.assertTrue(isinstance(
163 self.assertTrue(isinstance(
137 self.repo.get_changeset(id), GitChangeset))
164 self.repo.get_changeset(id), GitChangeset))
138
165
139 def test_tags(self):
166 def test_tags(self):
140 # TODO: Need more tests here
167 # TODO: Need more tests here
141 self.assertTrue('v0.1.1' in self.repo.tags)
168 self.assertTrue('v0.1.1' in self.repo.tags)
142 self.assertTrue('v0.1.2' in self.repo.tags)
169 self.assertTrue('v0.1.2' in self.repo.tags)
143 for name, id in self.repo.tags.items():
170 for name, id in self.repo.tags.items():
144 self.assertTrue(isinstance(
171 self.assertTrue(isinstance(
145 self.repo.get_changeset(id), GitChangeset))
172 self.repo.get_changeset(id), GitChangeset))
146
173
147 def _test_single_changeset_cache(self, revision):
174 def _test_single_changeset_cache(self, revision):
148 chset = self.repo.get_changeset(revision)
175 chset = self.repo.get_changeset(revision)
149 self.assertTrue(revision in self.repo.changesets)
176 self.assertTrue(revision in self.repo.changesets)
150 self.assertTrue(chset is self.repo.changesets[revision])
177 self.assertTrue(chset is self.repo.changesets[revision])
151
178
152 def test_initial_changeset(self):
179 def test_initial_changeset(self):
153 id = self.repo.revisions[0]
180 id = self.repo.revisions[0]
154 init_chset = self.repo.get_changeset(id)
181 init_chset = self.repo.get_changeset(id)
155 self.assertEqual(init_chset.message, 'initial import\n')
182 self.assertEqual(init_chset.message, 'initial import\n')
156 self.assertEqual(init_chset.author,
183 self.assertEqual(init_chset.author,
157 'Marcin Kuzminski <marcin@python-blog.com>')
184 'Marcin Kuzminski <marcin@python-blog.com>')
158 for path in ('vcs/__init__.py',
185 for path in ('vcs/__init__.py',
159 'vcs/backends/BaseRepository.py',
186 'vcs/backends/BaseRepository.py',
160 'vcs/backends/__init__.py'):
187 'vcs/backends/__init__.py'):
161 self.assertTrue(isinstance(init_chset.get_node(path), FileNode))
188 self.assertTrue(isinstance(init_chset.get_node(path), FileNode))
162 for path in ('', 'vcs', 'vcs/backends'):
189 for path in ('', 'vcs', 'vcs/backends'):
163 self.assertTrue(isinstance(init_chset.get_node(path), DirNode))
190 self.assertTrue(isinstance(init_chset.get_node(path), DirNode))
164
191
165 self.assertRaises(NodeDoesNotExistError, init_chset.get_node, path='foobar')
192 self.assertRaises(NodeDoesNotExistError, init_chset.get_node, path='foobar')
166
193
167 node = init_chset.get_node('vcs/')
194 node = init_chset.get_node('vcs/')
168 self.assertTrue(hasattr(node, 'kind'))
195 self.assertTrue(hasattr(node, 'kind'))
169 self.assertEqual(node.kind, NodeKind.DIR)
196 self.assertEqual(node.kind, NodeKind.DIR)
170
197
171 node = init_chset.get_node('vcs')
198 node = init_chset.get_node('vcs')
172 self.assertTrue(hasattr(node, 'kind'))
199 self.assertTrue(hasattr(node, 'kind'))
173 self.assertEqual(node.kind, NodeKind.DIR)
200 self.assertEqual(node.kind, NodeKind.DIR)
174
201
175 node = init_chset.get_node('vcs/__init__.py')
202 node = init_chset.get_node('vcs/__init__.py')
176 self.assertTrue(hasattr(node, 'kind'))
203 self.assertTrue(hasattr(node, 'kind'))
177 self.assertEqual(node.kind, NodeKind.FILE)
204 self.assertEqual(node.kind, NodeKind.FILE)
178
205
179 def test_not_existing_changeset(self):
206 def test_not_existing_changeset(self):
180 self.assertRaises(RepositoryError, self.repo.get_changeset,
207 self.assertRaises(RepositoryError, self.repo.get_changeset,
181 'f' * 40)
208 'f' * 40)
182
209
183 def test_changeset10(self):
210 def test_changeset10(self):
184
211
185 chset10 = self.repo.get_changeset(self.repo.revisions[9])
212 chset10 = self.repo.get_changeset(self.repo.revisions[9])
186 README = """===
213 README = """===
187 VCS
214 VCS
188 ===
215 ===
189
216
190 Various Version Control System management abstraction layer for Python.
217 Various Version Control System management abstraction layer for Python.
191
218
192 Introduction
219 Introduction
193 ------------
220 ------------
194
221
195 TODO: To be written...
222 TODO: To be written...
196
223
197 """
224 """
198 node = chset10.get_node('README.rst')
225 node = chset10.get_node('README.rst')
199 self.assertEqual(node.kind, NodeKind.FILE)
226 self.assertEqual(node.kind, NodeKind.FILE)
200 self.assertEqual(node.content, README)
227 self.assertEqual(node.content, README)
201
228
202
229
203 class GitChangesetTest(unittest.TestCase):
230 class GitChangesetTest(unittest.TestCase):
204
231
205 def setUp(self):
232 def setUp(self):
206 self.repo = GitRepository(TEST_GIT_REPO)
233 self.repo = GitRepository(TEST_GIT_REPO)
207
234
208 def test_default_changeset(self):
235 def test_default_changeset(self):
209 tip = self.repo.get_changeset()
236 tip = self.repo.get_changeset()
210 self.assertEqual(tip, self.repo.get_changeset(None))
237 self.assertEqual(tip, self.repo.get_changeset(None))
211 self.assertEqual(tip, self.repo.get_changeset('tip'))
238 self.assertEqual(tip, self.repo.get_changeset('tip'))
212
239
213 def test_root_node(self):
240 def test_root_node(self):
214 tip = self.repo.get_changeset()
241 tip = self.repo.get_changeset()
215 self.assertTrue(tip.root is tip.get_node(''))
242 self.assertTrue(tip.root is tip.get_node(''))
216
243
217 def test_lazy_fetch(self):
244 def test_lazy_fetch(self):
218 """
245 """
219 Test if changeset's nodes expands and are cached as we walk through
246 Test if changeset's nodes expands and are cached as we walk through
220 the revision. This test is somewhat hard to write as order of tests
247 the revision. This test is somewhat hard to write as order of tests
221 is a key here. Written by running command after command in a shell.
248 is a key here. Written by running command after command in a shell.
222 """
249 """
223 hex = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
250 hex = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
224 self.assertTrue(hex in self.repo.revisions)
251 self.assertTrue(hex in self.repo.revisions)
225 chset = self.repo.get_changeset(hex)
252 chset = self.repo.get_changeset(hex)
226 self.assertTrue(len(chset.nodes) == 0)
253 self.assertTrue(len(chset.nodes) == 0)
227 root = chset.root
254 root = chset.root
228 self.assertTrue(len(chset.nodes) == 1)
255 self.assertTrue(len(chset.nodes) == 1)
229 self.assertTrue(len(root.nodes) == 8)
256 self.assertTrue(len(root.nodes) == 8)
230 # accessing root.nodes updates chset.nodes
257 # accessing root.nodes updates chset.nodes
231 self.assertTrue(len(chset.nodes) == 9)
258 self.assertTrue(len(chset.nodes) == 9)
232
259
233 docs = root.get_node('docs')
260 docs = root.get_node('docs')
234 # we haven't yet accessed anything new as docs dir was already cached
261 # we haven't yet accessed anything new as docs dir was already cached
235 self.assertTrue(len(chset.nodes) == 9)
262 self.assertTrue(len(chset.nodes) == 9)
236 self.assertTrue(len(docs.nodes) == 8)
263 self.assertTrue(len(docs.nodes) == 8)
237 # accessing docs.nodes updates chset.nodes
264 # accessing docs.nodes updates chset.nodes
238 self.assertTrue(len(chset.nodes) == 17)
265 self.assertTrue(len(chset.nodes) == 17)
239
266
240 self.assertTrue(docs is chset.get_node('docs'))
267 self.assertTrue(docs is chset.get_node('docs'))
241 self.assertTrue(docs is root.nodes[0])
268 self.assertTrue(docs is root.nodes[0])
242 self.assertTrue(docs is root.dirs[0])
269 self.assertTrue(docs is root.dirs[0])
243 self.assertTrue(docs is chset.get_node('docs'))
270 self.assertTrue(docs is chset.get_node('docs'))
244
271
245 def test_nodes_with_changeset(self):
272 def test_nodes_with_changeset(self):
246 hex = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
273 hex = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
247 chset = self.repo.get_changeset(hex)
274 chset = self.repo.get_changeset(hex)
248 root = chset.root
275 root = chset.root
249 docs = root.get_node('docs')
276 docs = root.get_node('docs')
250 self.assertTrue(docs is chset.get_node('docs'))
277 self.assertTrue(docs is chset.get_node('docs'))
251 api = docs.get_node('api')
278 api = docs.get_node('api')
252 self.assertTrue(api is chset.get_node('docs/api'))
279 self.assertTrue(api is chset.get_node('docs/api'))
253 index = api.get_node('index.rst')
280 index = api.get_node('index.rst')
254 self.assertTrue(index is chset.get_node('docs/api/index.rst'))
281 self.assertTrue(index is chset.get_node('docs/api/index.rst'))
255 self.assertTrue(index is chset.get_node('docs')\
282 self.assertTrue(index is chset.get_node('docs')\
256 .get_node('api')\
283 .get_node('api')\
257 .get_node('index.rst'))
284 .get_node('index.rst'))
258
285
259 def test_branch_and_tags(self):
286 def test_branch_and_tags(self):
260 """
287 """
261 rev0 = self.repo.revisions[0]
288 rev0 = self.repo.revisions[0]
262 chset0 = self.repo.get_changeset(rev0)
289 chset0 = self.repo.get_changeset(rev0)
263 self.assertEqual(chset0.branch, 'master')
290 self.assertEqual(chset0.branch, 'master')
264 self.assertEqual(chset0.tags, [])
291 self.assertEqual(chset0.tags, [])
265
292
266 rev10 = self.repo.revisions[10]
293 rev10 = self.repo.revisions[10]
267 chset10 = self.repo.get_changeset(rev10)
294 chset10 = self.repo.get_changeset(rev10)
268 self.assertEqual(chset10.branch, 'master')
295 self.assertEqual(chset10.branch, 'master')
269 self.assertEqual(chset10.tags, [])
296 self.assertEqual(chset10.tags, [])
270
297
271 rev44 = self.repo.revisions[44]
298 rev44 = self.repo.revisions[44]
272 chset44 = self.repo.get_changeset(rev44)
299 chset44 = self.repo.get_changeset(rev44)
273 self.assertEqual(chset44.branch, 'web-branch')
300 self.assertEqual(chset44.branch, 'web-branch')
274
301
275 tip = self.repo.get_changeset('tip')
302 tip = self.repo.get_changeset('tip')
276 self.assertTrue('tip' in tip.tags)
303 self.assertTrue('tip' in tip.tags)
277 """
304 """
278 # Those tests would fail - branches are now going
305 # Those tests would fail - branches are now going
279 # to be changed at main API in order to support git backend
306 # to be changed at main API in order to support git backend
280 pass
307 pass
281
308
282 def _test_slices(self, limit, offset):
309 def _test_slices(self, limit, offset):
283 count = self.repo.count()
310 count = self.repo.count()
284 changesets = self.repo.get_changesets(limit=limit, offset=offset)
311 changesets = self.repo.get_changesets(limit=limit, offset=offset)
285 idx = 0
312 idx = 0
286 for changeset in changesets:
313 for changeset in changesets:
287 rev = offset + idx
314 rev = offset + idx
288 idx += 1
315 idx += 1
289 rev_id = self.repo.revisions[rev]
316 rev_id = self.repo.revisions[rev]
290 if idx > limit:
317 if idx > limit:
291 self.fail("Exceeded limit already (getting revision %s, "
318 self.fail("Exceeded limit already (getting revision %s, "
292 "there are %s total revisions, offset=%s, limit=%s)"
319 "there are %s total revisions, offset=%s, limit=%s)"
293 % (rev_id, count, offset, limit))
320 % (rev_id, count, offset, limit))
294 self.assertEqual(changeset, self.repo.get_changeset(rev_id))
321 self.assertEqual(changeset, self.repo.get_changeset(rev_id))
295 result = list(self.repo.get_changesets(limit=limit, offset=offset))
322 result = list(self.repo.get_changesets(limit=limit, offset=offset))
296 start = offset
323 start = offset
297 end = limit and offset + limit or None
324 end = limit and offset + limit or None
298 sliced = list(self.repo[start:end])
325 sliced = list(self.repo[start:end])
299 self.failUnlessEqual(result, sliced,
326 self.failUnlessEqual(result, sliced,
300 msg="Comparison failed for limit=%s, offset=%s"
327 msg="Comparison failed for limit=%s, offset=%s"
301 "(get_changeset returned: %s and sliced: %s"
328 "(get_changeset returned: %s and sliced: %s"
302 % (limit, offset, result, sliced))
329 % (limit, offset, result, sliced))
303
330
304 def _test_file_size(self, revision, path, size):
331 def _test_file_size(self, revision, path, size):
305 node = self.repo.get_changeset(revision).get_node(path)
332 node = self.repo.get_changeset(revision).get_node(path)
306 self.assertTrue(node.is_file())
333 self.assertTrue(node.is_file())
307 self.assertEqual(node.size, size)
334 self.assertEqual(node.size, size)
308
335
309 def test_file_size(self):
336 def test_file_size(self):
310 to_check = (
337 to_check = (
311 ('c1214f7e79e02fc37156ff215cd71275450cffc3',
338 ('c1214f7e79e02fc37156ff215cd71275450cffc3',
312 'vcs/backends/BaseRepository.py', 502),
339 'vcs/backends/BaseRepository.py', 502),
313 ('d7e0d30fbcae12c90680eb095a4f5f02505ce501',
340 ('d7e0d30fbcae12c90680eb095a4f5f02505ce501',
314 'vcs/backends/hg.py', 854),
341 'vcs/backends/hg.py', 854),
315 ('6e125e7c890379446e98980d8ed60fba87d0f6d1',
342 ('6e125e7c890379446e98980d8ed60fba87d0f6d1',
316 'setup.py', 1068),
343 'setup.py', 1068),
317
344
318 ('d955cd312c17b02143c04fa1099a352b04368118',
345 ('d955cd312c17b02143c04fa1099a352b04368118',
319 'vcs/backends/base.py', 2921),
346 'vcs/backends/base.py', 2921),
320 ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
347 ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
321 'vcs/backends/base.py', 3936),
348 'vcs/backends/base.py', 3936),
322 ('f50f42baeed5af6518ef4b0cb2f1423f3851a941',
349 ('f50f42baeed5af6518ef4b0cb2f1423f3851a941',
323 'vcs/backends/base.py', 6189),
350 'vcs/backends/base.py', 6189),
324 )
351 )
325 for revision, path, size in to_check:
352 for revision, path, size in to_check:
326 self._test_file_size(revision, path, size)
353 self._test_file_size(revision, path, size)
327
354
328 def test_file_history(self):
355 def test_file_history(self):
329 # we can only check if those revisions are present in the history
356 # we can only check if those revisions are present in the history
330 # as we cannot update this test every time file is changed
357 # as we cannot update this test every time file is changed
331 files = {
358 files = {
332 'setup.py': [
359 'setup.py': [
333 '54386793436c938cff89326944d4c2702340037d',
360 '54386793436c938cff89326944d4c2702340037d',
334 '51d254f0ecf5df2ce50c0b115741f4cf13985dab',
361 '51d254f0ecf5df2ce50c0b115741f4cf13985dab',
335 '998ed409c795fec2012b1c0ca054d99888b22090',
362 '998ed409c795fec2012b1c0ca054d99888b22090',
336 '5e0eb4c47f56564395f76333f319d26c79e2fb09',
363 '5e0eb4c47f56564395f76333f319d26c79e2fb09',
337 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
364 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
338 '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e',
365 '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e',
339 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
366 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
340 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
367 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
341 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
368 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
342 ],
369 ],
343 'vcs/nodes.py': [
370 'vcs/nodes.py': [
344 '33fa3223355104431402a888fa77a4e9956feb3e',
371 '33fa3223355104431402a888fa77a4e9956feb3e',
345 'fa014c12c26d10ba682fadb78f2a11c24c8118e1',
372 'fa014c12c26d10ba682fadb78f2a11c24c8118e1',
346 'e686b958768ee96af8029fe19c6050b1a8dd3b2b',
373 'e686b958768ee96af8029fe19c6050b1a8dd3b2b',
347 'ab5721ca0a081f26bf43d9051e615af2cc99952f',
374 'ab5721ca0a081f26bf43d9051e615af2cc99952f',
348 'c877b68d18e792a66b7f4c529ea02c8f80801542',
375 'c877b68d18e792a66b7f4c529ea02c8f80801542',
349 '4313566d2e417cb382948f8d9d7c765330356054',
376 '4313566d2e417cb382948f8d9d7c765330356054',
350 '6c2303a793671e807d1cfc70134c9ca0767d98c2',
377 '6c2303a793671e807d1cfc70134c9ca0767d98c2',
351 '54386793436c938cff89326944d4c2702340037d',
378 '54386793436c938cff89326944d4c2702340037d',
352 '54000345d2e78b03a99d561399e8e548de3f3203',
379 '54000345d2e78b03a99d561399e8e548de3f3203',
353 '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b',
380 '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b',
354 '2d03ca750a44440fb5ea8b751176d1f36f8e8f46',
381 '2d03ca750a44440fb5ea8b751176d1f36f8e8f46',
355 '2a08b128c206db48c2f0b8f70df060e6db0ae4f8',
382 '2a08b128c206db48c2f0b8f70df060e6db0ae4f8',
356 '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b',
383 '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b',
357 'ac71e9503c2ca95542839af0ce7b64011b72ea7c',
384 'ac71e9503c2ca95542839af0ce7b64011b72ea7c',
358 '12669288fd13adba2a9b7dd5b870cc23ffab92d2',
385 '12669288fd13adba2a9b7dd5b870cc23ffab92d2',
359 '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382',
386 '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382',
360 '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5',
387 '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5',
361 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
388 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
362 'f50f42baeed5af6518ef4b0cb2f1423f3851a941',
389 'f50f42baeed5af6518ef4b0cb2f1423f3851a941',
363 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25',
390 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25',
364 'f15c21f97864b4f071cddfbf2750ec2e23859414',
391 'f15c21f97864b4f071cddfbf2750ec2e23859414',
365 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade',
392 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade',
366 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b',
393 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b',
367 '84dec09632a4458f79f50ddbbd155506c460b4f9',
394 '84dec09632a4458f79f50ddbbd155506c460b4f9',
368 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
395 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
369 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
396 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
370 '3bf1c5868e570e39569d094f922d33ced2fa3b2b',
397 '3bf1c5868e570e39569d094f922d33ced2fa3b2b',
371 'b8d04012574729d2c29886e53b1a43ef16dd00a1',
398 'b8d04012574729d2c29886e53b1a43ef16dd00a1',
372 '6970b057cffe4aab0a792aa634c89f4bebf01441',
399 '6970b057cffe4aab0a792aa634c89f4bebf01441',
373 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
400 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
374 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
401 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
375 ],
402 ],
376 'vcs/backends/git.py': [
403 'vcs/backends/git.py': [
377 '4cf116ad5a457530381135e2f4c453e68a1b0105',
404 '4cf116ad5a457530381135e2f4c453e68a1b0105',
378 '9a751d84d8e9408e736329767387f41b36935153',
405 '9a751d84d8e9408e736329767387f41b36935153',
379 'cb681fb539c3faaedbcdf5ca71ca413425c18f01',
406 'cb681fb539c3faaedbcdf5ca71ca413425c18f01',
380 '428f81bb652bcba8d631bce926e8834ff49bdcc6',
407 '428f81bb652bcba8d631bce926e8834ff49bdcc6',
381 '180ab15aebf26f98f714d8c68715e0f05fa6e1c7',
408 '180ab15aebf26f98f714d8c68715e0f05fa6e1c7',
382 '2b8e07312a2e89e92b90426ab97f349f4bce2a3a',
409 '2b8e07312a2e89e92b90426ab97f349f4bce2a3a',
383 '50e08c506174d8645a4bb517dd122ac946a0f3bf',
410 '50e08c506174d8645a4bb517dd122ac946a0f3bf',
384 '54000345d2e78b03a99d561399e8e548de3f3203',
411 '54000345d2e78b03a99d561399e8e548de3f3203',
385 ],
412 ],
386 }
413 }
387 for path, revs in files.items():
414 for path, revs in files.items():
388 node = self.repo.get_changeset(revs[0]).get_node(path)
415 node = self.repo.get_changeset(revs[0]).get_node(path)
389 node_revs = [chset.raw_id for chset in node.history]
416 node_revs = [chset.raw_id for chset in node.history]
390 self.assertTrue(set(revs).issubset(set(node_revs)),
417 self.assertTrue(set(revs).issubset(set(node_revs)),
391 "We assumed that %s is subset of revisions for which file %s "
418 "We assumed that %s is subset of revisions for which file %s "
392 "has been changed, and history of that node returned: %s"
419 "has been changed, and history of that node returned: %s"
393 % (revs, path, node_revs))
420 % (revs, path, node_revs))
394
421
395 def test_file_annotate(self):
422 def test_file_annotate(self):
396 files = {
423 files = {
397 'vcs/backends/__init__.py': {
424 'vcs/backends/__init__.py': {
398 'c1214f7e79e02fc37156ff215cd71275450cffc3': {
425 'c1214f7e79e02fc37156ff215cd71275450cffc3': {
399 'lines_no': 1,
426 'lines_no': 1,
400 'changesets': [
427 'changesets': [
401 'c1214f7e79e02fc37156ff215cd71275450cffc3',
428 'c1214f7e79e02fc37156ff215cd71275450cffc3',
402 ],
429 ],
403 },
430 },
404 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': {
431 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': {
405 'lines_no': 21,
432 'lines_no': 21,
406 'changesets': [
433 'changesets': [
407 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
434 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
408 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
435 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
409 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
436 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
410 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
437 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
411 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
438 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
412 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
439 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
413 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
440 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
414 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
441 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
415 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
442 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
416 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
443 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
417 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
444 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
418 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
445 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
419 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
446 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
420 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
447 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
421 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
448 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
422 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
449 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
423 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
450 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
424 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
451 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
425 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
452 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
426 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
453 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
427 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
454 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
428 ],
455 ],
429 },
456 },
430 'e29b67bd158580fc90fc5e9111240b90e6e86064': {
457 'e29b67bd158580fc90fc5e9111240b90e6e86064': {
431 'lines_no': 32,
458 'lines_no': 32,
432 'changesets': [
459 'changesets': [
433 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
460 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
434 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
461 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
435 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
462 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
436 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
463 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
437 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
464 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
438 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
465 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
439 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
466 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
440 '54000345d2e78b03a99d561399e8e548de3f3203',
467 '54000345d2e78b03a99d561399e8e548de3f3203',
441 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
468 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
442 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
469 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
443 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
470 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
444 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
471 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
445 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
472 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
446 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
473 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
447 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
474 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
448 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
475 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
449 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
476 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
450 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
477 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
451 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
478 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
452 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
479 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
453 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
480 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
454 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
481 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
455 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
482 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
456 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
483 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
457 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
484 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
458 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
485 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
459 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
486 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
460 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
487 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
461 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
488 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
462 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
489 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
463 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
490 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
464 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
491 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
465 ],
492 ],
466 },
493 },
467 },
494 },
468 }
495 }
469
496
470 for fname, revision_dict in files.items():
497 for fname, revision_dict in files.items():
471 for rev, data in revision_dict.items():
498 for rev, data in revision_dict.items():
472 cs = self.repo.get_changeset(rev)
499 cs = self.repo.get_changeset(rev)
473
500
474 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
501 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
475 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
502 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
476 self.assertEqual(l1_1, l1_2)
503 self.assertEqual(l1_1, l1_2)
477 l1 = l1_1
504 l1 = l1_1
478 l2 = files[fname][rev]['changesets']
505 l2 = files[fname][rev]['changesets']
479 self.assertTrue(l1 == l2 , "The lists of revision for %s@rev %s"
506 self.assertTrue(l1 == l2 , "The lists of revision for %s@rev %s"
480 "from annotation list should match each other, "
507 "from annotation list should match each other, "
481 "got \n%s \nvs \n%s " % (fname, rev, l1, l2))
508 "got \n%s \nvs \n%s " % (fname, rev, l1, l2))
482
509
483 def test_files_state(self):
510 def test_files_state(self):
484 """
511 """
485 Tests state of FileNodes.
512 Tests state of FileNodes.
486 """
513 """
487 node = self.repo\
514 node = self.repo\
488 .get_changeset('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0')\
515 .get_changeset('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0')\
489 .get_node('vcs/utils/diffs.py')
516 .get_node('vcs/utils/diffs.py')
490 self.assertTrue(node.state, NodeState.ADDED)
517 self.assertTrue(node.state, NodeState.ADDED)
491 self.assertTrue(node.added)
518 self.assertTrue(node.added)
492 self.assertFalse(node.changed)
519 self.assertFalse(node.changed)
493 self.assertFalse(node.not_changed)
520 self.assertFalse(node.not_changed)
494 self.assertFalse(node.removed)
521 self.assertFalse(node.removed)
495
522
496 node = self.repo\
523 node = self.repo\
497 .get_changeset('33fa3223355104431402a888fa77a4e9956feb3e')\
524 .get_changeset('33fa3223355104431402a888fa77a4e9956feb3e')\
498 .get_node('.hgignore')
525 .get_node('.hgignore')
499 self.assertTrue(node.state, NodeState.CHANGED)
526 self.assertTrue(node.state, NodeState.CHANGED)
500 self.assertFalse(node.added)
527 self.assertFalse(node.added)
501 self.assertTrue(node.changed)
528 self.assertTrue(node.changed)
502 self.assertFalse(node.not_changed)
529 self.assertFalse(node.not_changed)
503 self.assertFalse(node.removed)
530 self.assertFalse(node.removed)
504
531
505 node = self.repo\
532 node = self.repo\
506 .get_changeset('e29b67bd158580fc90fc5e9111240b90e6e86064')\
533 .get_changeset('e29b67bd158580fc90fc5e9111240b90e6e86064')\
507 .get_node('setup.py')
534 .get_node('setup.py')
508 self.assertTrue(node.state, NodeState.NOT_CHANGED)
535 self.assertTrue(node.state, NodeState.NOT_CHANGED)
509 self.assertFalse(node.added)
536 self.assertFalse(node.added)
510 self.assertFalse(node.changed)
537 self.assertFalse(node.changed)
511 self.assertTrue(node.not_changed)
538 self.assertTrue(node.not_changed)
512 self.assertFalse(node.removed)
539 self.assertFalse(node.removed)
513
540
514 # If node has REMOVED state then trying to fetch it would raise
541 # If node has REMOVED state then trying to fetch it would raise
515 # ChangesetError exception
542 # ChangesetError exception
516 chset = self.repo.get_changeset(
543 chset = self.repo.get_changeset(
517 'fa6600f6848800641328adbf7811fd2372c02ab2')
544 'fa6600f6848800641328adbf7811fd2372c02ab2')
518 path = 'vcs/backends/BaseRepository.py'
545 path = 'vcs/backends/BaseRepository.py'
519 self.assertRaises(NodeDoesNotExistError, chset.get_node, path)
546 self.assertRaises(NodeDoesNotExistError, chset.get_node, path)
520 # but it would be one of ``removed`` (changeset's attribute)
547 # but it would be one of ``removed`` (changeset's attribute)
521 self.assertTrue(path in [rf.path for rf in chset.removed])
548 self.assertTrue(path in [rf.path for rf in chset.removed])
522
549
523 chset = self.repo.get_changeset(
550 chset = self.repo.get_changeset(
524 '54386793436c938cff89326944d4c2702340037d')
551 '54386793436c938cff89326944d4c2702340037d')
525 changed = ['setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py',
552 changed = ['setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py',
526 'vcs/nodes.py']
553 'vcs/nodes.py']
527 self.assertEqual(set(changed), set([f.path for f in chset.changed]))
554 self.assertEqual(set(changed), set([f.path for f in chset.changed]))
528
555
529 def test_commit_message_is_unicode(self):
556 def test_commit_message_is_unicode(self):
530 for cs in self.repo:
557 for cs in self.repo:
531 self.assertEqual(type(cs.message), unicode)
558 self.assertEqual(type(cs.message), unicode)
532
559
533 def test_changeset_author_is_unicode(self):
560 def test_changeset_author_is_unicode(self):
534 for cs in self.repo:
561 for cs in self.repo:
535 self.assertEqual(type(cs.author), unicode)
562 self.assertEqual(type(cs.author), unicode)
536
563
537 def test_repo_files_content_is_unicode(self):
564 def test_repo_files_content_is_unicode(self):
538 changeset = self.repo.get_changeset()
565 changeset = self.repo.get_changeset()
539 for node in changeset.get_node('/'):
566 for node in changeset.get_node('/'):
540 if node.is_file():
567 if node.is_file():
541 self.assertEqual(type(node.content), unicode)
568 self.assertEqual(type(node.content), unicode)
542
569
543 def test_wrong_path(self):
570 def test_wrong_path(self):
544 # There is 'setup.py' in the root dir but not there:
571 # There is 'setup.py' in the root dir but not there:
545 path = 'foo/bar/setup.py'
572 path = 'foo/bar/setup.py'
546 tip = self.repo.get_changeset()
573 tip = self.repo.get_changeset()
547 self.assertRaises(VCSError, tip.get_node, path)
574 self.assertRaises(VCSError, tip.get_node, path)
548
575
549 def test_author_email(self):
576 def test_author_email(self):
550 self.assertEqual('marcin@python-blog.com',
577 self.assertEqual('marcin@python-blog.com',
551 self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3')\
578 self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3')\
552 .author_email)
579 .author_email)
553 self.assertEqual('lukasz.balcerzak@python-center.pl',
580 self.assertEqual('lukasz.balcerzak@python-center.pl',
554 self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b')\
581 self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b')\
555 .author_email)
582 .author_email)
556 self.assertEqual('none@none',
583 self.assertEqual('none@none',
557 self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992')\
584 self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992')\
558 .author_email)
585 .author_email)
559
586
560 def test_author_username(self):
587 def test_author_username(self):
561 self.assertEqual('Marcin Kuzminski',
588 self.assertEqual('Marcin Kuzminski',
562 self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3')\
589 self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3')\
563 .author_name)
590 .author_name)
564 self.assertEqual('Lukasz Balcerzak',
591 self.assertEqual('Lukasz Balcerzak',
565 self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b')\
592 self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b')\
566 .author_name)
593 .author_name)
567 self.assertEqual('marcink',
594 self.assertEqual('marcink',
568 self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992')\
595 self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992')\
569 .author_name)
596 .author_name)
570
597
571
598
572 class GitSpecificTest(unittest.TestCase):
599 class GitSpecificTest(unittest.TestCase):
573
600
574 def test_error_is_raised_for_added_if_diff_name_status_is_wrong(self):
601 def test_error_is_raised_for_added_if_diff_name_status_is_wrong(self):
575 repo = mock.MagicMock()
602 repo = mock.MagicMock()
576 changeset = GitChangeset(repo, 'foobar')
603 changeset = GitChangeset(repo, 'foobar')
577 changeset._diff_name_status = 'foobar'
604 changeset._diff_name_status = 'foobar'
578 with self.assertRaises(VCSError):
605 with self.assertRaises(VCSError):
579 changeset.added
606 changeset.added
580
607
581 def test_error_is_raised_for_changed_if_diff_name_status_is_wrong(self):
608 def test_error_is_raised_for_changed_if_diff_name_status_is_wrong(self):
582 repo = mock.MagicMock()
609 repo = mock.MagicMock()
583 changeset = GitChangeset(repo, 'foobar')
610 changeset = GitChangeset(repo, 'foobar')
584 changeset._diff_name_status = 'foobar'
611 changeset._diff_name_status = 'foobar'
585 with self.assertRaises(VCSError):
612 with self.assertRaises(VCSError):
586 changeset.added
613 changeset.added
587
614
588 def test_error_is_raised_for_removed_if_diff_name_status_is_wrong(self):
615 def test_error_is_raised_for_removed_if_diff_name_status_is_wrong(self):
589 repo = mock.MagicMock()
616 repo = mock.MagicMock()
590 changeset = GitChangeset(repo, 'foobar')
617 changeset = GitChangeset(repo, 'foobar')
591 changeset._diff_name_status = 'foobar'
618 changeset._diff_name_status = 'foobar'
592 with self.assertRaises(VCSError):
619 with self.assertRaises(VCSError):
593 changeset.added
620 changeset.added
594
621
595
622
596 class GitSpecificWithRepoTest(BackendTestMixin, unittest.TestCase):
623 class GitSpecificWithRepoTest(BackendTestMixin, unittest.TestCase):
597 backend_alias = 'git'
624 backend_alias = 'git'
598
625
599 @classmethod
626 @classmethod
600 def _get_commits(cls):
627 def _get_commits(cls):
601 return [
628 return [
602 {
629 {
603 'message': 'Initial',
630 'message': 'Initial',
604 'author': 'Joe Doe <joe.doe@example.com>',
631 'author': 'Joe Doe <joe.doe@example.com>',
605 'date': datetime.datetime(2010, 1, 1, 20),
632 'date': datetime.datetime(2010, 1, 1, 20),
606 'added': [
633 'added': [
607 FileNode('foobar/static/js/admin/base.js', content='base'),
634 FileNode('foobar/static/js/admin/base.js', content='base'),
608 FileNode('foobar/static/admin', content='admin',
635 FileNode('foobar/static/admin', content='admin',
609 mode=0120000), # this is a link
636 mode=0120000), # this is a link
610 FileNode('foo', content='foo'),
637 FileNode('foo', content='foo'),
611 ],
638 ],
612 },
639 },
613 {
640 {
614 'message': 'Second',
641 'message': 'Second',
615 'author': 'Joe Doe <joe.doe@example.com>',
642 'author': 'Joe Doe <joe.doe@example.com>',
616 'date': datetime.datetime(2010, 1, 1, 22),
643 'date': datetime.datetime(2010, 1, 1, 22),
617 'added': [
644 'added': [
618 FileNode('foo2', content='foo2'),
645 FileNode('foo2', content='foo2'),
619 ],
646 ],
620 },
647 },
621 ]
648 ]
622
649
623 def test_paths_slow_traversing(self):
650 def test_paths_slow_traversing(self):
624 cs = self.repo.get_changeset()
651 cs = self.repo.get_changeset()
625 self.assertEqual(cs.get_node('foobar').get_node('static').get_node('js')
652 self.assertEqual(cs.get_node('foobar').get_node('static').get_node('js')
626 .get_node('admin').get_node('base.js').content, 'base')
653 .get_node('admin').get_node('base.js').content, 'base')
627
654
628 def test_paths_fast_traversing(self):
655 def test_paths_fast_traversing(self):
629 cs = self.repo.get_changeset()
656 cs = self.repo.get_changeset()
630 self.assertEqual(cs.get_node('foobar/static/js/admin/base.js').content,
657 self.assertEqual(cs.get_node('foobar/static/js/admin/base.js').content,
631 'base')
658 'base')
632
659
633 def test_workdir_get_branch(self):
660 def test_workdir_get_branch(self):
634 self.repo.run_git_command('checkout -b production')
661 self.repo.run_git_command('checkout -b production')
635 # Regression test: one of following would fail if we don't check
662 # Regression test: one of following would fail if we don't check
636 # .git/HEAD file
663 # .git/HEAD file
637 self.repo.run_git_command('checkout production')
664 self.repo.run_git_command('checkout production')
638 self.assertEqual(self.repo.workdir.get_branch(), 'production')
665 self.assertEqual(self.repo.workdir.get_branch(), 'production')
639 self.repo.run_git_command('checkout master')
666 self.repo.run_git_command('checkout master')
640 self.assertEqual(self.repo.workdir.get_branch(), 'master')
667 self.assertEqual(self.repo.workdir.get_branch(), 'master')
641
668
642 def test_get_diff_runs_git_command_with_hashes(self):
669 def test_get_diff_runs_git_command_with_hashes(self):
643 self.repo.run_git_command = mock.Mock(return_value=['', ''])
670 self.repo.run_git_command = mock.Mock(return_value=['', ''])
644 self.repo.get_diff(0, 1)
671 self.repo.get_diff(0, 1)
645 self.repo.run_git_command.assert_called_once_with(
672 self.repo.run_git_command.assert_called_once_with(
646 'diff -U%s --full-index --binary -p -M --abbrev=40 %s %s' %
673 'diff -U%s --full-index --binary -p -M --abbrev=40 %s %s' %
647 (3, self.repo._get_revision(0), self.repo._get_revision(1)))
674 (3, self.repo._get_revision(0), self.repo._get_revision(1)))
648
675
649 def test_get_diff_runs_git_command_with_str_hashes(self):
676 def test_get_diff_runs_git_command_with_str_hashes(self):
650 self.repo.run_git_command = mock.Mock(return_value=['', ''])
677 self.repo.run_git_command = mock.Mock(return_value=['', ''])
651 self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1)
678 self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1)
652 self.repo.run_git_command.assert_called_once_with(
679 self.repo.run_git_command.assert_called_once_with(
653 'show -U%s --full-index --binary -p -M --abbrev=40 %s' %
680 'show -U%s --full-index --binary -p -M --abbrev=40 %s' %
654 (3, self.repo._get_revision(1)))
681 (3, self.repo._get_revision(1)))
655
682
656 def test_get_diff_runs_git_command_with_path_if_its_given(self):
683 def test_get_diff_runs_git_command_with_path_if_its_given(self):
657 self.repo.run_git_command = mock.Mock(return_value=['', ''])
684 self.repo.run_git_command = mock.Mock(return_value=['', ''])
658 self.repo.get_diff(0, 1, 'foo')
685 self.repo.get_diff(0, 1, 'foo')
659 self.repo.run_git_command.assert_called_once_with(
686 self.repo.run_git_command.assert_called_once_with(
660 'diff -U%s --full-index --binary -p -M --abbrev=40 %s %s -- "foo"'
687 'diff -U%s --full-index --binary -p -M --abbrev=40 %s %s -- "foo"'
661 % (3, self.repo._get_revision(0), self.repo._get_revision(1)))
688 % (3, self.repo._get_revision(0), self.repo._get_revision(1)))
662
689
663
690
664 class GitRegressionTest(BackendTestMixin, unittest.TestCase):
691 class GitRegressionTest(BackendTestMixin, unittest.TestCase):
665 backend_alias = 'git'
692 backend_alias = 'git'
666
693
667 @classmethod
694 @classmethod
668 def _get_commits(cls):
695 def _get_commits(cls):
669 return [
696 return [
670 {
697 {
671 'message': 'Initial',
698 'message': 'Initial',
672 'author': 'Joe Doe <joe.doe@example.com>',
699 'author': 'Joe Doe <joe.doe@example.com>',
673 'date': datetime.datetime(2010, 1, 1, 20),
700 'date': datetime.datetime(2010, 1, 1, 20),
674 'added': [
701 'added': [
675 FileNode('bot/__init__.py', content='base'),
702 FileNode('bot/__init__.py', content='base'),
676 FileNode('bot/templates/404.html', content='base'),
703 FileNode('bot/templates/404.html', content='base'),
677 FileNode('bot/templates/500.html', content='base'),
704 FileNode('bot/templates/500.html', content='base'),
678 ],
705 ],
679 },
706 },
680 {
707 {
681 'message': 'Second',
708 'message': 'Second',
682 'author': 'Joe Doe <joe.doe@example.com>',
709 'author': 'Joe Doe <joe.doe@example.com>',
683 'date': datetime.datetime(2010, 1, 1, 22),
710 'date': datetime.datetime(2010, 1, 1, 22),
684 'added': [
711 'added': [
685 FileNode('bot/build/migrations/1.py', content='foo2'),
712 FileNode('bot/build/migrations/1.py', content='foo2'),
686 FileNode('bot/build/migrations/2.py', content='foo2'),
713 FileNode('bot/build/migrations/2.py', content='foo2'),
687 FileNode('bot/build/static/templates/f.html', content='foo2'),
714 FileNode('bot/build/static/templates/f.html', content='foo2'),
688 FileNode('bot/build/static/templates/f1.html', content='foo2'),
715 FileNode('bot/build/static/templates/f1.html', content='foo2'),
689 FileNode('bot/build/templates/err.html', content='foo2'),
716 FileNode('bot/build/templates/err.html', content='foo2'),
690 FileNode('bot/build/templates/err2.html', content='foo2'),
717 FileNode('bot/build/templates/err2.html', content='foo2'),
691 ],
718 ],
692 },
719 },
693 ]
720 ]
694
721
695 def test_similar_paths(self):
722 def test_similar_paths(self):
696 cs = self.repo.get_changeset()
723 cs = self.repo.get_changeset()
697 paths = lambda *n:[x.path for x in n]
724 paths = lambda *n:[x.path for x in n]
698 self.assertEqual(paths(*cs.get_nodes('bot')), ['bot/build', 'bot/templates', 'bot/__init__.py'])
725 self.assertEqual(paths(*cs.get_nodes('bot')), ['bot/build', 'bot/templates', 'bot/__init__.py'])
699 self.assertEqual(paths(*cs.get_nodes('bot/build')), ['bot/build/migrations', 'bot/build/static', 'bot/build/templates'])
726 self.assertEqual(paths(*cs.get_nodes('bot/build')), ['bot/build/migrations', 'bot/build/static', 'bot/build/templates'])
700 self.assertEqual(paths(*cs.get_nodes('bot/build/static')), ['bot/build/static/templates'])
727 self.assertEqual(paths(*cs.get_nodes('bot/build/static')), ['bot/build/static/templates'])
701 # this get_nodes below causes troubles !
728 # this get_nodes below causes troubles !
702 self.assertEqual(paths(*cs.get_nodes('bot/build/static/templates')), ['bot/build/static/templates/f.html', 'bot/build/static/templates/f1.html'])
729 self.assertEqual(paths(*cs.get_nodes('bot/build/static/templates')), ['bot/build/static/templates/f.html', 'bot/build/static/templates/f1.html'])
703 self.assertEqual(paths(*cs.get_nodes('bot/build/templates')), ['bot/build/templates/err.html', 'bot/build/templates/err2.html'])
730 self.assertEqual(paths(*cs.get_nodes('bot/build/templates')), ['bot/build/templates/err.html', 'bot/build/templates/err2.html'])
704 self.assertEqual(paths(*cs.get_nodes('bot/templates/')), ['bot/templates/404.html', 'bot/templates/500.html'])
731 self.assertEqual(paths(*cs.get_nodes('bot/templates/')), ['bot/templates/404.html', 'bot/templates/500.html'])
705
732
706 if __name__ == '__main__':
733 if __name__ == '__main__':
707 unittest.main()
734 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now