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