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