##// END OF EJS Templates
vcs: add equality testing for commits against non commits to allow...
dan -
r985:6ce2682d default
parent child Browse files
Show More
@@ -1,1507 +1,1508 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Base module for all VCS systems
23 23 """
24 24
25 25 import collections
26 26 import datetime
27 27 import itertools
28 28 import logging
29 29 import os
30 30 import time
31 31 import warnings
32 32
33 33 from zope.cachedescriptors.property import Lazy as LazyProperty
34 34
35 35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 36 from rhodecode.lib.vcs import connection
37 37 from rhodecode.lib.vcs.utils import author_name, author_email
38 38 from rhodecode.lib.vcs.conf import settings
39 39 from rhodecode.lib.vcs.exceptions import (
40 40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 44 RepositoryError)
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 FILEMODE_DEFAULT = 0100644
51 51 FILEMODE_EXECUTABLE = 0100755
52 52
53 53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 54 MergeResponse = collections.namedtuple(
55 55 'MergeResponse',
56 56 ('possible', 'executed', 'merge_commit_id', 'failure_reason'))
57 57
58 58
59 59 class MergeFailureReason(object):
60 60 """
61 61 Enumeration with all the reasons why the server side merge could fail.
62 62
63 63 DO NOT change the number of the reasons, as they may be stored in the
64 64 database.
65 65
66 66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 67 reasons.
68 68 """
69 69
70 70 # Everything went well.
71 71 NONE = 0
72 72
73 73 # An unexpected exception was raised. Check the logs for more details.
74 74 UNKNOWN = 1
75 75
76 76 # The merge was not successful, there are conflicts.
77 77 MERGE_FAILED = 2
78 78
79 79 # The merge succeeded but we could not push it to the target repository.
80 80 PUSH_FAILED = 3
81 81
82 82 # The specified target is not a head in the target repository.
83 83 TARGET_IS_NOT_HEAD = 4
84 84
85 85 # The source repository contains more branches than the target. Pushing
86 86 # the merge will create additional branches in the target.
87 87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88 88
89 89 # The target reference has multiple heads. That does not allow to correctly
90 90 # identify the target location. This could only happen for mercurial
91 91 # branches.
92 92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93 93
94 94 # The target repository is locked
95 95 TARGET_IS_LOCKED = 7
96 96
97 97 # A involved commit could not be found.
98 98 MISSING_COMMIT = 8
99 99
100 100
101 101 class BaseRepository(object):
102 102 """
103 103 Base Repository for final backends
104 104
105 105 .. attribute:: DEFAULT_BRANCH_NAME
106 106
107 107 name of default branch (i.e. "trunk" for svn, "master" for git etc.
108 108
109 109 .. attribute:: commit_ids
110 110
111 111 list of all available commit ids, in ascending order
112 112
113 113 .. attribute:: path
114 114
115 115 absolute path to the repository
116 116
117 117 .. attribute:: bookmarks
118 118
119 119 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
120 120 there are no bookmarks or the backend implementation does not support
121 121 bookmarks.
122 122
123 123 .. attribute:: tags
124 124
125 125 Mapping from name to :term:`Commit ID` of the tag.
126 126
127 127 """
128 128
129 129 DEFAULT_BRANCH_NAME = None
130 130 DEFAULT_CONTACT = u"Unknown"
131 131 DEFAULT_DESCRIPTION = u"unknown"
132 132 EMPTY_COMMIT_ID = '0' * 40
133 133
134 134 path = None
135 135
136 136 def __init__(self, repo_path, config=None, create=False, **kwargs):
137 137 """
138 138 Initializes repository. Raises RepositoryError if repository could
139 139 not be find at the given ``repo_path`` or directory at ``repo_path``
140 140 exists and ``create`` is set to True.
141 141
142 142 :param repo_path: local path of the repository
143 143 :param config: repository configuration
144 144 :param create=False: if set to True, would try to create repository.
145 145 :param src_url=None: if set, should be proper url from which repository
146 146 would be cloned; requires ``create`` parameter to be set to True -
147 147 raises RepositoryError if src_url is set and create evaluates to
148 148 False
149 149 """
150 150 raise NotImplementedError
151 151
152 152 def __repr__(self):
153 153 return '<%s at %s>' % (self.__class__.__name__, self.path)
154 154
155 155 def __len__(self):
156 156 return self.count()
157 157
158 158 def __eq__(self, other):
159 159 same_instance = isinstance(other, self.__class__)
160 160 return same_instance and other.path == self.path
161 161
162 162 def __ne__(self, other):
163 163 return not self.__eq__(other)
164 164
165 165 @LazyProperty
166 166 def EMPTY_COMMIT(self):
167 167 return EmptyCommit(self.EMPTY_COMMIT_ID)
168 168
169 169 @LazyProperty
170 170 def alias(self):
171 171 for k, v in settings.BACKENDS.items():
172 172 if v.split('.')[-1] == str(self.__class__.__name__):
173 173 return k
174 174
175 175 @LazyProperty
176 176 def name(self):
177 177 return safe_unicode(os.path.basename(self.path))
178 178
179 179 @LazyProperty
180 180 def description(self):
181 181 raise NotImplementedError
182 182
183 183 def refs(self):
184 184 """
185 185 returns a `dict` with branches, bookmarks, tags, and closed_branches
186 186 for this repository
187 187 """
188 188 raise NotImplementedError
189 189
190 190 @LazyProperty
191 191 def branches(self):
192 192 """
193 193 A `dict` which maps branch names to commit ids.
194 194 """
195 195 raise NotImplementedError
196 196
197 197 @LazyProperty
198 198 def size(self):
199 199 """
200 200 Returns combined size in bytes for all repository files
201 201 """
202 202 tip = self.get_commit()
203 203 return tip.size
204 204
205 205 def size_at_commit(self, commit_id):
206 206 commit = self.get_commit(commit_id)
207 207 return commit.size
208 208
209 209 def is_empty(self):
210 210 return not bool(self.commit_ids)
211 211
212 212 @staticmethod
213 213 def check_url(url, config):
214 214 """
215 215 Function will check given url and try to verify if it's a valid
216 216 link.
217 217 """
218 218 raise NotImplementedError
219 219
220 220 @staticmethod
221 221 def is_valid_repository(path):
222 222 """
223 223 Check if given `path` contains a valid repository of this backend
224 224 """
225 225 raise NotImplementedError
226 226
227 227 # ==========================================================================
228 228 # COMMITS
229 229 # ==========================================================================
230 230
231 231 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
232 232 """
233 233 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
234 234 are both None, most recent commit is returned.
235 235
236 236 :param pre_load: Optional. List of commit attributes to load.
237 237
238 238 :raises ``EmptyRepositoryError``: if there are no commits
239 239 """
240 240 raise NotImplementedError
241 241
242 242 def __iter__(self):
243 243 for commit_id in self.commit_ids:
244 244 yield self.get_commit(commit_id=commit_id)
245 245
246 246 def get_commits(
247 247 self, start_id=None, end_id=None, start_date=None, end_date=None,
248 248 branch_name=None, pre_load=None):
249 249 """
250 250 Returns iterator of `BaseCommit` objects from start to end
251 251 not inclusive. This should behave just like a list, ie. end is not
252 252 inclusive.
253 253
254 254 :param start_id: None or str, must be a valid commit id
255 255 :param end_id: None or str, must be a valid commit id
256 256 :param start_date:
257 257 :param end_date:
258 258 :param branch_name:
259 259 :param pre_load:
260 260 """
261 261 raise NotImplementedError
262 262
263 263 def __getitem__(self, key):
264 264 """
265 265 Allows index based access to the commit objects of this repository.
266 266 """
267 267 pre_load = ["author", "branch", "date", "message", "parents"]
268 268 if isinstance(key, slice):
269 269 return self._get_range(key, pre_load)
270 270 return self.get_commit(commit_idx=key, pre_load=pre_load)
271 271
272 272 def _get_range(self, slice_obj, pre_load):
273 273 for commit_id in self.commit_ids.__getitem__(slice_obj):
274 274 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
275 275
276 276 def count(self):
277 277 return len(self.commit_ids)
278 278
279 279 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
280 280 """
281 281 Creates and returns a tag for the given ``commit_id``.
282 282
283 283 :param name: name for new tag
284 284 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
285 285 :param commit_id: commit id for which new tag would be created
286 286 :param message: message of the tag's commit
287 287 :param date: date of tag's commit
288 288
289 289 :raises TagAlreadyExistError: if tag with same name already exists
290 290 """
291 291 raise NotImplementedError
292 292
293 293 def remove_tag(self, name, user, message=None, date=None):
294 294 """
295 295 Removes tag with the given ``name``.
296 296
297 297 :param name: name of the tag to be removed
298 298 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
299 299 :param message: message of the tag's removal commit
300 300 :param date: date of tag's removal commit
301 301
302 302 :raises TagDoesNotExistError: if tag with given name does not exists
303 303 """
304 304 raise NotImplementedError
305 305
306 306 def get_diff(
307 307 self, commit1, commit2, path=None, ignore_whitespace=False,
308 308 context=3, path1=None):
309 309 """
310 310 Returns (git like) *diff*, as plain text. Shows changes introduced by
311 311 `commit2` since `commit1`.
312 312
313 313 :param commit1: Entry point from which diff is shown. Can be
314 314 ``self.EMPTY_COMMIT`` - in this case, patch showing all
315 315 the changes since empty state of the repository until `commit2`
316 316 :param commit2: Until which commit changes should be shown.
317 317 :param path: Can be set to a path of a file to create a diff of that
318 318 file. If `path1` is also set, this value is only associated to
319 319 `commit2`.
320 320 :param ignore_whitespace: If set to ``True``, would not show whitespace
321 321 changes. Defaults to ``False``.
322 322 :param context: How many lines before/after changed lines should be
323 323 shown. Defaults to ``3``.
324 324 :param path1: Can be set to a path to associate with `commit1`. This
325 325 parameter works only for backends which support diff generation for
326 326 different paths. Other backends will raise a `ValueError` if `path1`
327 327 is set and has a different value than `path`.
328 328 """
329 329 raise NotImplementedError
330 330
331 331 def strip(self, commit_id, branch=None):
332 332 """
333 333 Strip given commit_id from the repository
334 334 """
335 335 raise NotImplementedError
336 336
337 337 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
338 338 """
339 339 Return a latest common ancestor commit if one exists for this repo
340 340 `commit_id1` vs `commit_id2` from `repo2`.
341 341
342 342 :param commit_id1: Commit it from this repository to use as a
343 343 target for the comparison.
344 344 :param commit_id2: Source commit id to use for comparison.
345 345 :param repo2: Source repository to use for comparison.
346 346 """
347 347 raise NotImplementedError
348 348
349 349 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
350 350 """
351 351 Compare this repository's revision `commit_id1` with `commit_id2`.
352 352
353 353 Returns a tuple(commits, ancestor) that would be merged from
354 354 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
355 355 will be returned as ancestor.
356 356
357 357 :param commit_id1: Commit it from this repository to use as a
358 358 target for the comparison.
359 359 :param commit_id2: Source commit id to use for comparison.
360 360 :param repo2: Source repository to use for comparison.
361 361 :param merge: If set to ``True`` will do a merge compare which also
362 362 returns the common ancestor.
363 363 :param pre_load: Optional. List of commit attributes to load.
364 364 """
365 365 raise NotImplementedError
366 366
367 367 def merge(self, target_ref, source_repo, source_ref, workspace_id,
368 368 user_name='', user_email='', message='', dry_run=False,
369 369 use_rebase=False):
370 370 """
371 371 Merge the revisions specified in `source_ref` from `source_repo`
372 372 onto the `target_ref` of this repository.
373 373
374 374 `source_ref` and `target_ref` are named tupls with the following
375 375 fields `type`, `name` and `commit_id`.
376 376
377 377 Returns a MergeResponse named tuple with the following fields
378 378 'possible', 'executed', 'source_commit', 'target_commit',
379 379 'merge_commit'.
380 380
381 381 :param target_ref: `target_ref` points to the commit on top of which
382 382 the `source_ref` should be merged.
383 383 :param source_repo: The repository that contains the commits to be
384 384 merged.
385 385 :param source_ref: `source_ref` points to the topmost commit from
386 386 the `source_repo` which should be merged.
387 387 :param workspace_id: `workspace_id` unique identifier.
388 388 :param user_name: Merge commit `user_name`.
389 389 :param user_email: Merge commit `user_email`.
390 390 :param message: Merge commit `message`.
391 391 :param dry_run: If `True` the merge will not take place.
392 392 :param use_rebase: If `True` commits from the source will be rebased
393 393 on top of the target instead of being merged.
394 394 """
395 395 if dry_run:
396 396 message = message or 'dry_run_merge_message'
397 397 user_email = user_email or 'dry-run-merge@rhodecode.com'
398 398 user_name = user_name or 'Dry-Run User'
399 399 else:
400 400 if not user_name:
401 401 raise ValueError('user_name cannot be empty')
402 402 if not user_email:
403 403 raise ValueError('user_email cannot be empty')
404 404 if not message:
405 405 raise ValueError('message cannot be empty')
406 406
407 407 shadow_repository_path = self._maybe_prepare_merge_workspace(
408 408 workspace_id, target_ref)
409 409
410 410 try:
411 411 return self._merge_repo(
412 412 shadow_repository_path, target_ref, source_repo,
413 413 source_ref, message, user_name, user_email, dry_run=dry_run,
414 414 use_rebase=use_rebase)
415 415 except RepositoryError:
416 416 log.exception(
417 417 'Unexpected failure when running merge, dry-run=%s',
418 418 dry_run)
419 419 return MergeResponse(
420 420 False, False, None, MergeFailureReason.UNKNOWN)
421 421
422 422 def _merge_repo(self, shadow_repository_path, target_ref,
423 423 source_repo, source_ref, merge_message,
424 424 merger_name, merger_email, dry_run=False, use_rebase=False):
425 425 """Internal implementation of merge."""
426 426 raise NotImplementedError
427 427
428 428 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
429 429 """
430 430 Create the merge workspace.
431 431
432 432 :param workspace_id: `workspace_id` unique identifier.
433 433 """
434 434 raise NotImplementedError
435 435
436 436 def cleanup_merge_workspace(self, workspace_id):
437 437 """
438 438 Remove merge workspace.
439 439
440 440 This function MUST not fail in case there is no workspace associated to
441 441 the given `workspace_id`.
442 442
443 443 :param workspace_id: `workspace_id` unique identifier.
444 444 """
445 445 raise NotImplementedError
446 446
447 447 # ========== #
448 448 # COMMIT API #
449 449 # ========== #
450 450
451 451 @LazyProperty
452 452 def in_memory_commit(self):
453 453 """
454 454 Returns :class:`InMemoryCommit` object for this repository.
455 455 """
456 456 raise NotImplementedError
457 457
458 458 # ======================== #
459 459 # UTILITIES FOR SUBCLASSES #
460 460 # ======================== #
461 461
462 462 def _validate_diff_commits(self, commit1, commit2):
463 463 """
464 464 Validates that the given commits are related to this repository.
465 465
466 466 Intended as a utility for sub classes to have a consistent validation
467 467 of input parameters in methods like :meth:`get_diff`.
468 468 """
469 469 self._validate_commit(commit1)
470 470 self._validate_commit(commit2)
471 471 if (isinstance(commit1, EmptyCommit) and
472 472 isinstance(commit2, EmptyCommit)):
473 473 raise ValueError("Cannot compare two empty commits")
474 474
475 475 def _validate_commit(self, commit):
476 476 if not isinstance(commit, BaseCommit):
477 477 raise TypeError(
478 478 "%s is not of type BaseCommit" % repr(commit))
479 479 if commit.repository != self and not isinstance(commit, EmptyCommit):
480 480 raise ValueError(
481 481 "Commit %s must be a valid commit from this repository %s, "
482 482 "related to this repository instead %s." %
483 483 (commit, self, commit.repository))
484 484
485 485 def _validate_commit_id(self, commit_id):
486 486 if not isinstance(commit_id, basestring):
487 487 raise TypeError("commit_id must be a string value")
488 488
489 489 def _validate_commit_idx(self, commit_idx):
490 490 if not isinstance(commit_idx, (int, long)):
491 491 raise TypeError("commit_idx must be a numeric value")
492 492
493 493 def _validate_branch_name(self, branch_name):
494 494 if branch_name and branch_name not in self.branches_all:
495 495 msg = ("Branch %s not found in %s" % (branch_name, self))
496 496 raise BranchDoesNotExistError(msg)
497 497
498 498 #
499 499 # Supporting deprecated API parts
500 500 # TODO: johbo: consider to move this into a mixin
501 501 #
502 502
503 503 @property
504 504 def EMPTY_CHANGESET(self):
505 505 warnings.warn(
506 506 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
507 507 return self.EMPTY_COMMIT_ID
508 508
509 509 @property
510 510 def revisions(self):
511 511 warnings.warn("Use commits attribute instead", DeprecationWarning)
512 512 return self.commit_ids
513 513
514 514 @revisions.setter
515 515 def revisions(self, value):
516 516 warnings.warn("Use commits attribute instead", DeprecationWarning)
517 517 self.commit_ids = value
518 518
519 519 def get_changeset(self, revision=None, pre_load=None):
520 520 warnings.warn("Use get_commit instead", DeprecationWarning)
521 521 commit_id = None
522 522 commit_idx = None
523 523 if isinstance(revision, basestring):
524 524 commit_id = revision
525 525 else:
526 526 commit_idx = revision
527 527 return self.get_commit(
528 528 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
529 529
530 530 def get_changesets(
531 531 self, start=None, end=None, start_date=None, end_date=None,
532 532 branch_name=None, pre_load=None):
533 533 warnings.warn("Use get_commits instead", DeprecationWarning)
534 534 start_id = self._revision_to_commit(start)
535 535 end_id = self._revision_to_commit(end)
536 536 return self.get_commits(
537 537 start_id=start_id, end_id=end_id, start_date=start_date,
538 538 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
539 539
540 540 def _revision_to_commit(self, revision):
541 541 """
542 542 Translates a revision to a commit_id
543 543
544 544 Helps to support the old changeset based API which allows to use
545 545 commit ids and commit indices interchangeable.
546 546 """
547 547 if revision is None:
548 548 return revision
549 549
550 550 if isinstance(revision, basestring):
551 551 commit_id = revision
552 552 else:
553 553 commit_id = self.commit_ids[revision]
554 554 return commit_id
555 555
556 556 @property
557 557 def in_memory_changeset(self):
558 558 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
559 559 return self.in_memory_commit
560 560
561 561
562 562 class BaseCommit(object):
563 563 """
564 564 Each backend should implement it's commit representation.
565 565
566 566 **Attributes**
567 567
568 568 ``repository``
569 569 repository object within which commit exists
570 570
571 571 ``id``
572 572 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
573 573 just ``tip``.
574 574
575 575 ``raw_id``
576 576 raw commit representation (i.e. full 40 length sha for git
577 577 backend)
578 578
579 579 ``short_id``
580 580 shortened (if apply) version of ``raw_id``; it would be simple
581 581 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
582 582 as ``raw_id`` for subversion
583 583
584 584 ``idx``
585 585 commit index
586 586
587 587 ``files``
588 588 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
589 589
590 590 ``dirs``
591 591 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
592 592
593 593 ``nodes``
594 594 combined list of ``Node`` objects
595 595
596 596 ``author``
597 597 author of the commit, as unicode
598 598
599 599 ``message``
600 600 message of the commit, as unicode
601 601
602 602 ``parents``
603 603 list of parent commits
604 604
605 605 """
606 606
607 607 branch = None
608 608 """
609 609 Depending on the backend this should be set to the branch name of the
610 610 commit. Backends not supporting branches on commits should leave this
611 611 value as ``None``.
612 612 """
613 613
614 614 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
615 615 """
616 616 This template is used to generate a default prefix for repository archives
617 617 if no prefix has been specified.
618 618 """
619 619
620 620 def __str__(self):
621 621 return '<%s at %s:%s>' % (
622 622 self.__class__.__name__, self.idx, self.short_id)
623 623
624 624 def __repr__(self):
625 625 return self.__str__()
626 626
627 627 def __unicode__(self):
628 628 return u'%s:%s' % (self.idx, self.short_id)
629 629
630 630 def __eq__(self, other):
631 return self.raw_id == other.raw_id
631 same_instance = isinstance(other, self.__class__)
632 return same_instance and self.raw_id == other.raw_id
632 633
633 634 def __json__(self):
634 635 parents = []
635 636 try:
636 637 for parent in self.parents:
637 638 parents.append({'raw_id': parent.raw_id})
638 639 except NotImplementedError:
639 640 # empty commit doesn't have parents implemented
640 641 pass
641 642
642 643 return {
643 644 'short_id': self.short_id,
644 645 'raw_id': self.raw_id,
645 646 'revision': self.idx,
646 647 'message': self.message,
647 648 'date': self.date,
648 649 'author': self.author,
649 650 'parents': parents,
650 651 'branch': self.branch
651 652 }
652 653
653 654 @LazyProperty
654 655 def last(self):
655 656 """
656 657 ``True`` if this is last commit in repository, ``False``
657 658 otherwise; trying to access this attribute while there is no
658 659 commits would raise `EmptyRepositoryError`
659 660 """
660 661 if self.repository is None:
661 662 raise CommitError("Cannot check if it's most recent commit")
662 663 return self.raw_id == self.repository.commit_ids[-1]
663 664
664 665 @LazyProperty
665 666 def parents(self):
666 667 """
667 668 Returns list of parent commits.
668 669 """
669 670 raise NotImplementedError
670 671
671 672 @property
672 673 def merge(self):
673 674 """
674 675 Returns boolean if commit is a merge.
675 676 """
676 677 return len(self.parents) > 1
677 678
678 679 @LazyProperty
679 680 def children(self):
680 681 """
681 682 Returns list of child commits.
682 683 """
683 684 raise NotImplementedError
684 685
685 686 @LazyProperty
686 687 def id(self):
687 688 """
688 689 Returns string identifying this commit.
689 690 """
690 691 raise NotImplementedError
691 692
692 693 @LazyProperty
693 694 def raw_id(self):
694 695 """
695 696 Returns raw string identifying this commit.
696 697 """
697 698 raise NotImplementedError
698 699
699 700 @LazyProperty
700 701 def short_id(self):
701 702 """
702 703 Returns shortened version of ``raw_id`` attribute, as string,
703 704 identifying this commit, useful for presentation to users.
704 705 """
705 706 raise NotImplementedError
706 707
707 708 @LazyProperty
708 709 def idx(self):
709 710 """
710 711 Returns integer identifying this commit.
711 712 """
712 713 raise NotImplementedError
713 714
714 715 @LazyProperty
715 716 def committer(self):
716 717 """
717 718 Returns committer for this commit
718 719 """
719 720 raise NotImplementedError
720 721
721 722 @LazyProperty
722 723 def committer_name(self):
723 724 """
724 725 Returns committer name for this commit
725 726 """
726 727
727 728 return author_name(self.committer)
728 729
729 730 @LazyProperty
730 731 def committer_email(self):
731 732 """
732 733 Returns committer email address for this commit
733 734 """
734 735
735 736 return author_email(self.committer)
736 737
737 738 @LazyProperty
738 739 def author(self):
739 740 """
740 741 Returns author for this commit
741 742 """
742 743
743 744 raise NotImplementedError
744 745
745 746 @LazyProperty
746 747 def author_name(self):
747 748 """
748 749 Returns author name for this commit
749 750 """
750 751
751 752 return author_name(self.author)
752 753
753 754 @LazyProperty
754 755 def author_email(self):
755 756 """
756 757 Returns author email address for this commit
757 758 """
758 759
759 760 return author_email(self.author)
760 761
761 762 def get_file_mode(self, path):
762 763 """
763 764 Returns stat mode of the file at `path`.
764 765 """
765 766 raise NotImplementedError
766 767
767 768 def is_link(self, path):
768 769 """
769 770 Returns ``True`` if given `path` is a symlink
770 771 """
771 772 raise NotImplementedError
772 773
773 774 def get_file_content(self, path):
774 775 """
775 776 Returns content of the file at the given `path`.
776 777 """
777 778 raise NotImplementedError
778 779
779 780 def get_file_size(self, path):
780 781 """
781 782 Returns size of the file at the given `path`.
782 783 """
783 784 raise NotImplementedError
784 785
785 786 def get_file_commit(self, path, pre_load=None):
786 787 """
787 788 Returns last commit of the file at the given `path`.
788 789
789 790 :param pre_load: Optional. List of commit attributes to load.
790 791 """
791 792 return self.get_file_history(path, limit=1, pre_load=pre_load)[0]
792 793
793 794 def get_file_history(self, path, limit=None, pre_load=None):
794 795 """
795 796 Returns history of file as reversed list of :class:`BaseCommit`
796 797 objects for which file at given `path` has been modified.
797 798
798 799 :param limit: Optional. Allows to limit the size of the returned
799 800 history. This is intended as a hint to the underlying backend, so
800 801 that it can apply optimizations depending on the limit.
801 802 :param pre_load: Optional. List of commit attributes to load.
802 803 """
803 804 raise NotImplementedError
804 805
805 806 def get_file_annotate(self, path, pre_load=None):
806 807 """
807 808 Returns a generator of four element tuples with
808 809 lineno, sha, commit lazy loader and line
809 810
810 811 :param pre_load: Optional. List of commit attributes to load.
811 812 """
812 813 raise NotImplementedError
813 814
814 815 def get_nodes(self, path):
815 816 """
816 817 Returns combined ``DirNode`` and ``FileNode`` objects list representing
817 818 state of commit at the given ``path``.
818 819
819 820 :raises ``CommitError``: if node at the given ``path`` is not
820 821 instance of ``DirNode``
821 822 """
822 823 raise NotImplementedError
823 824
824 825 def get_node(self, path):
825 826 """
826 827 Returns ``Node`` object from the given ``path``.
827 828
828 829 :raises ``NodeDoesNotExistError``: if there is no node at the given
829 830 ``path``
830 831 """
831 832 raise NotImplementedError
832 833
833 834 def get_largefile_node(self, path):
834 835 """
835 836 Returns the path to largefile from Mercurial storage.
836 837 """
837 838 raise NotImplementedError
838 839
839 840 def archive_repo(self, file_path, kind='tgz', subrepos=None,
840 841 prefix=None, write_metadata=False, mtime=None):
841 842 """
842 843 Creates an archive containing the contents of the repository.
843 844
844 845 :param file_path: path to the file which to create the archive.
845 846 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
846 847 :param prefix: name of root directory in archive.
847 848 Default is repository name and commit's short_id joined with dash:
848 849 ``"{repo_name}-{short_id}"``.
849 850 :param write_metadata: write a metadata file into archive.
850 851 :param mtime: custom modification time for archive creation, defaults
851 852 to time.time() if not given.
852 853
853 854 :raise VCSError: If prefix has a problem.
854 855 """
855 856 allowed_kinds = settings.ARCHIVE_SPECS.keys()
856 857 if kind not in allowed_kinds:
857 858 raise ImproperArchiveTypeError(
858 859 'Archive kind (%s) not supported use one of %s' %
859 860 (kind, allowed_kinds))
860 861
861 862 prefix = self._validate_archive_prefix(prefix)
862 863
863 864 mtime = mtime or time.mktime(self.date.timetuple())
864 865
865 866 file_info = []
866 867 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
867 868 for _r, _d, files in cur_rev.walk('/'):
868 869 for f in files:
869 870 f_path = os.path.join(prefix, f.path)
870 871 file_info.append(
871 872 (f_path, f.mode, f.is_link(), f.raw_bytes))
872 873
873 874 if write_metadata:
874 875 metadata = [
875 876 ('repo_name', self.repository.name),
876 877 ('rev', self.raw_id),
877 878 ('create_time', mtime),
878 879 ('branch', self.branch),
879 880 ('tags', ','.join(self.tags)),
880 881 ]
881 882 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
882 883 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
883 884
884 885 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
885 886
886 887 def _validate_archive_prefix(self, prefix):
887 888 if prefix is None:
888 889 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
889 890 repo_name=safe_str(self.repository.name),
890 891 short_id=self.short_id)
891 892 elif not isinstance(prefix, str):
892 893 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
893 894 elif prefix.startswith('/'):
894 895 raise VCSError("Prefix cannot start with leading slash")
895 896 elif prefix.strip() == '':
896 897 raise VCSError("Prefix cannot be empty")
897 898 return prefix
898 899
899 900 @LazyProperty
900 901 def root(self):
901 902 """
902 903 Returns ``RootNode`` object for this commit.
903 904 """
904 905 return self.get_node('')
905 906
906 907 def next(self, branch=None):
907 908 """
908 909 Returns next commit from current, if branch is gives it will return
909 910 next commit belonging to this branch
910 911
911 912 :param branch: show commits within the given named branch
912 913 """
913 914 indexes = xrange(self.idx + 1, self.repository.count())
914 915 return self._find_next(indexes, branch)
915 916
916 917 def prev(self, branch=None):
917 918 """
918 919 Returns previous commit from current, if branch is gives it will
919 920 return previous commit belonging to this branch
920 921
921 922 :param branch: show commit within the given named branch
922 923 """
923 924 indexes = xrange(self.idx - 1, -1, -1)
924 925 return self._find_next(indexes, branch)
925 926
926 927 def _find_next(self, indexes, branch=None):
927 928 if branch and self.branch != branch:
928 929 raise VCSError('Branch option used on commit not belonging '
929 930 'to that branch')
930 931
931 932 for next_idx in indexes:
932 933 commit = self.repository.get_commit(commit_idx=next_idx)
933 934 if branch and branch != commit.branch:
934 935 continue
935 936 return commit
936 937 raise CommitDoesNotExistError
937 938
938 939 def diff(self, ignore_whitespace=True, context=3):
939 940 """
940 941 Returns a `Diff` object representing the change made by this commit.
941 942 """
942 943 parent = (
943 944 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
944 945 diff = self.repository.get_diff(
945 946 parent, self,
946 947 ignore_whitespace=ignore_whitespace,
947 948 context=context)
948 949 return diff
949 950
950 951 @LazyProperty
951 952 def added(self):
952 953 """
953 954 Returns list of added ``FileNode`` objects.
954 955 """
955 956 raise NotImplementedError
956 957
957 958 @LazyProperty
958 959 def changed(self):
959 960 """
960 961 Returns list of modified ``FileNode`` objects.
961 962 """
962 963 raise NotImplementedError
963 964
964 965 @LazyProperty
965 966 def removed(self):
966 967 """
967 968 Returns list of removed ``FileNode`` objects.
968 969 """
969 970 raise NotImplementedError
970 971
971 972 @LazyProperty
972 973 def size(self):
973 974 """
974 975 Returns total number of bytes from contents of all filenodes.
975 976 """
976 977 return sum((node.size for node in self.get_filenodes_generator()))
977 978
978 979 def walk(self, topurl=''):
979 980 """
980 981 Similar to os.walk method. Insted of filesystem it walks through
981 982 commit starting at given ``topurl``. Returns generator of tuples
982 983 (topnode, dirnodes, filenodes).
983 984 """
984 985 topnode = self.get_node(topurl)
985 986 if not topnode.is_dir():
986 987 return
987 988 yield (topnode, topnode.dirs, topnode.files)
988 989 for dirnode in topnode.dirs:
989 990 for tup in self.walk(dirnode.path):
990 991 yield tup
991 992
992 993 def get_filenodes_generator(self):
993 994 """
994 995 Returns generator that yields *all* file nodes.
995 996 """
996 997 for topnode, dirs, files in self.walk():
997 998 for node in files:
998 999 yield node
999 1000
1000 1001 #
1001 1002 # Utilities for sub classes to support consistent behavior
1002 1003 #
1003 1004
1004 1005 def no_node_at_path(self, path):
1005 1006 return NodeDoesNotExistError(
1006 1007 "There is no file nor directory at the given path: "
1007 1008 "'%s' at commit %s" % (path, self.short_id))
1008 1009
1009 1010 def _fix_path(self, path):
1010 1011 """
1011 1012 Paths are stored without trailing slash so we need to get rid off it if
1012 1013 needed.
1013 1014 """
1014 1015 return path.rstrip('/')
1015 1016
1016 1017 #
1017 1018 # Deprecated API based on changesets
1018 1019 #
1019 1020
1020 1021 @property
1021 1022 def revision(self):
1022 1023 warnings.warn("Use idx instead", DeprecationWarning)
1023 1024 return self.idx
1024 1025
1025 1026 @revision.setter
1026 1027 def revision(self, value):
1027 1028 warnings.warn("Use idx instead", DeprecationWarning)
1028 1029 self.idx = value
1029 1030
1030 1031 def get_file_changeset(self, path):
1031 1032 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1032 1033 return self.get_file_commit(path)
1033 1034
1034 1035
1035 1036 class BaseChangesetClass(type):
1036 1037
1037 1038 def __instancecheck__(self, instance):
1038 1039 return isinstance(instance, BaseCommit)
1039 1040
1040 1041
1041 1042 class BaseChangeset(BaseCommit):
1042 1043
1043 1044 __metaclass__ = BaseChangesetClass
1044 1045
1045 1046 def __new__(cls, *args, **kwargs):
1046 1047 warnings.warn(
1047 1048 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1048 1049 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1049 1050
1050 1051
1051 1052 class BaseInMemoryCommit(object):
1052 1053 """
1053 1054 Represents differences between repository's state (most recent head) and
1054 1055 changes made *in place*.
1055 1056
1056 1057 **Attributes**
1057 1058
1058 1059 ``repository``
1059 1060 repository object for this in-memory-commit
1060 1061
1061 1062 ``added``
1062 1063 list of ``FileNode`` objects marked as *added*
1063 1064
1064 1065 ``changed``
1065 1066 list of ``FileNode`` objects marked as *changed*
1066 1067
1067 1068 ``removed``
1068 1069 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1069 1070 *removed*
1070 1071
1071 1072 ``parents``
1072 1073 list of :class:`BaseCommit` instances representing parents of
1073 1074 in-memory commit. Should always be 2-element sequence.
1074 1075
1075 1076 """
1076 1077
1077 1078 def __init__(self, repository):
1078 1079 self.repository = repository
1079 1080 self.added = []
1080 1081 self.changed = []
1081 1082 self.removed = []
1082 1083 self.parents = []
1083 1084
1084 1085 def add(self, *filenodes):
1085 1086 """
1086 1087 Marks given ``FileNode`` objects as *to be committed*.
1087 1088
1088 1089 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1089 1090 latest commit
1090 1091 :raises ``NodeAlreadyAddedError``: if node with same path is already
1091 1092 marked as *added*
1092 1093 """
1093 1094 # Check if not already marked as *added* first
1094 1095 for node in filenodes:
1095 1096 if node.path in (n.path for n in self.added):
1096 1097 raise NodeAlreadyAddedError(
1097 1098 "Such FileNode %s is already marked for addition"
1098 1099 % node.path)
1099 1100 for node in filenodes:
1100 1101 self.added.append(node)
1101 1102
1102 1103 def change(self, *filenodes):
1103 1104 """
1104 1105 Marks given ``FileNode`` objects to be *changed* in next commit.
1105 1106
1106 1107 :raises ``EmptyRepositoryError``: if there are no commits yet
1107 1108 :raises ``NodeAlreadyExistsError``: if node with same path is already
1108 1109 marked to be *changed*
1109 1110 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1110 1111 marked to be *removed*
1111 1112 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1112 1113 commit
1113 1114 :raises ``NodeNotChangedError``: if node hasn't really be changed
1114 1115 """
1115 1116 for node in filenodes:
1116 1117 if node.path in (n.path for n in self.removed):
1117 1118 raise NodeAlreadyRemovedError(
1118 1119 "Node at %s is already marked as removed" % node.path)
1119 1120 try:
1120 1121 self.repository.get_commit()
1121 1122 except EmptyRepositoryError:
1122 1123 raise EmptyRepositoryError(
1123 1124 "Nothing to change - try to *add* new nodes rather than "
1124 1125 "changing them")
1125 1126 for node in filenodes:
1126 1127 if node.path in (n.path for n in self.changed):
1127 1128 raise NodeAlreadyChangedError(
1128 1129 "Node at '%s' is already marked as changed" % node.path)
1129 1130 self.changed.append(node)
1130 1131
1131 1132 def remove(self, *filenodes):
1132 1133 """
1133 1134 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1134 1135 *removed* in next commit.
1135 1136
1136 1137 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1137 1138 be *removed*
1138 1139 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1139 1140 be *changed*
1140 1141 """
1141 1142 for node in filenodes:
1142 1143 if node.path in (n.path for n in self.removed):
1143 1144 raise NodeAlreadyRemovedError(
1144 1145 "Node is already marked to for removal at %s" % node.path)
1145 1146 if node.path in (n.path for n in self.changed):
1146 1147 raise NodeAlreadyChangedError(
1147 1148 "Node is already marked to be changed at %s" % node.path)
1148 1149 # We only mark node as *removed* - real removal is done by
1149 1150 # commit method
1150 1151 self.removed.append(node)
1151 1152
1152 1153 def reset(self):
1153 1154 """
1154 1155 Resets this instance to initial state (cleans ``added``, ``changed``
1155 1156 and ``removed`` lists).
1156 1157 """
1157 1158 self.added = []
1158 1159 self.changed = []
1159 1160 self.removed = []
1160 1161 self.parents = []
1161 1162
1162 1163 def get_ipaths(self):
1163 1164 """
1164 1165 Returns generator of paths from nodes marked as added, changed or
1165 1166 removed.
1166 1167 """
1167 1168 for node in itertools.chain(self.added, self.changed, self.removed):
1168 1169 yield node.path
1169 1170
1170 1171 def get_paths(self):
1171 1172 """
1172 1173 Returns list of paths from nodes marked as added, changed or removed.
1173 1174 """
1174 1175 return list(self.get_ipaths())
1175 1176
1176 1177 def check_integrity(self, parents=None):
1177 1178 """
1178 1179 Checks in-memory commit's integrity. Also, sets parents if not
1179 1180 already set.
1180 1181
1181 1182 :raises CommitError: if any error occurs (i.e.
1182 1183 ``NodeDoesNotExistError``).
1183 1184 """
1184 1185 if not self.parents:
1185 1186 parents = parents or []
1186 1187 if len(parents) == 0:
1187 1188 try:
1188 1189 parents = [self.repository.get_commit(), None]
1189 1190 except EmptyRepositoryError:
1190 1191 parents = [None, None]
1191 1192 elif len(parents) == 1:
1192 1193 parents += [None]
1193 1194 self.parents = parents
1194 1195
1195 1196 # Local parents, only if not None
1196 1197 parents = [p for p in self.parents if p]
1197 1198
1198 1199 # Check nodes marked as added
1199 1200 for p in parents:
1200 1201 for node in self.added:
1201 1202 try:
1202 1203 p.get_node(node.path)
1203 1204 except NodeDoesNotExistError:
1204 1205 pass
1205 1206 else:
1206 1207 raise NodeAlreadyExistsError(
1207 1208 "Node `%s` already exists at %s" % (node.path, p))
1208 1209
1209 1210 # Check nodes marked as changed
1210 1211 missing = set(self.changed)
1211 1212 not_changed = set(self.changed)
1212 1213 if self.changed and not parents:
1213 1214 raise NodeDoesNotExistError(str(self.changed[0].path))
1214 1215 for p in parents:
1215 1216 for node in self.changed:
1216 1217 try:
1217 1218 old = p.get_node(node.path)
1218 1219 missing.remove(node)
1219 1220 # if content actually changed, remove node from not_changed
1220 1221 if old.content != node.content:
1221 1222 not_changed.remove(node)
1222 1223 except NodeDoesNotExistError:
1223 1224 pass
1224 1225 if self.changed and missing:
1225 1226 raise NodeDoesNotExistError(
1226 1227 "Node `%s` marked as modified but missing in parents: %s"
1227 1228 % (node.path, parents))
1228 1229
1229 1230 if self.changed and not_changed:
1230 1231 raise NodeNotChangedError(
1231 1232 "Node `%s` wasn't actually changed (parents: %s)"
1232 1233 % (not_changed.pop().path, parents))
1233 1234
1234 1235 # Check nodes marked as removed
1235 1236 if self.removed and not parents:
1236 1237 raise NodeDoesNotExistError(
1237 1238 "Cannot remove node at %s as there "
1238 1239 "were no parents specified" % self.removed[0].path)
1239 1240 really_removed = set()
1240 1241 for p in parents:
1241 1242 for node in self.removed:
1242 1243 try:
1243 1244 p.get_node(node.path)
1244 1245 really_removed.add(node)
1245 1246 except CommitError:
1246 1247 pass
1247 1248 not_removed = set(self.removed) - really_removed
1248 1249 if not_removed:
1249 1250 # TODO: johbo: This code branch does not seem to be covered
1250 1251 raise NodeDoesNotExistError(
1251 1252 "Cannot remove node at %s from "
1252 1253 "following parents: %s" % (not_removed, parents))
1253 1254
1254 1255 def commit(
1255 1256 self, message, author, parents=None, branch=None, date=None,
1256 1257 **kwargs):
1257 1258 """
1258 1259 Performs in-memory commit (doesn't check workdir in any way) and
1259 1260 returns newly created :class:`BaseCommit`. Updates repository's
1260 1261 attribute `commits`.
1261 1262
1262 1263 .. note::
1263 1264
1264 1265 While overriding this method each backend's should call
1265 1266 ``self.check_integrity(parents)`` in the first place.
1266 1267
1267 1268 :param message: message of the commit
1268 1269 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1269 1270 :param parents: single parent or sequence of parents from which commit
1270 1271 would be derived
1271 1272 :param date: ``datetime.datetime`` instance. Defaults to
1272 1273 ``datetime.datetime.now()``.
1273 1274 :param branch: branch name, as string. If none given, default backend's
1274 1275 branch would be used.
1275 1276
1276 1277 :raises ``CommitError``: if any error occurs while committing
1277 1278 """
1278 1279 raise NotImplementedError
1279 1280
1280 1281
1281 1282 class BaseInMemoryChangesetClass(type):
1282 1283
1283 1284 def __instancecheck__(self, instance):
1284 1285 return isinstance(instance, BaseInMemoryCommit)
1285 1286
1286 1287
1287 1288 class BaseInMemoryChangeset(BaseInMemoryCommit):
1288 1289
1289 1290 __metaclass__ = BaseInMemoryChangesetClass
1290 1291
1291 1292 def __new__(cls, *args, **kwargs):
1292 1293 warnings.warn(
1293 1294 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1294 1295 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1295 1296
1296 1297
1297 1298 class EmptyCommit(BaseCommit):
1298 1299 """
1299 1300 An dummy empty commit. It's possible to pass hash when creating
1300 1301 an EmptyCommit
1301 1302 """
1302 1303
1303 1304 def __init__(
1304 1305 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1305 1306 message='', author='', date=None):
1306 1307 self._empty_commit_id = commit_id
1307 1308 # TODO: johbo: Solve idx parameter, default value does not make
1308 1309 # too much sense
1309 1310 self.idx = idx
1310 1311 self.message = message
1311 1312 self.author = author
1312 1313 self.date = date or datetime.datetime.fromtimestamp(0)
1313 1314 self.repository = repo
1314 1315 self.alias = alias
1315 1316
1316 1317 @LazyProperty
1317 1318 def raw_id(self):
1318 1319 """
1319 1320 Returns raw string identifying this commit, useful for web
1320 1321 representation.
1321 1322 """
1322 1323
1323 1324 return self._empty_commit_id
1324 1325
1325 1326 @LazyProperty
1326 1327 def branch(self):
1327 1328 if self.alias:
1328 1329 from rhodecode.lib.vcs.backends import get_backend
1329 1330 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1330 1331
1331 1332 @LazyProperty
1332 1333 def short_id(self):
1333 1334 return self.raw_id[:12]
1334 1335
1335 1336 @LazyProperty
1336 1337 def id(self):
1337 1338 return self.raw_id
1338 1339
1339 1340 def get_file_commit(self, path):
1340 1341 return self
1341 1342
1342 1343 def get_file_content(self, path):
1343 1344 return u''
1344 1345
1345 1346 def get_file_size(self, path):
1346 1347 return 0
1347 1348
1348 1349
1349 1350 class EmptyChangesetClass(type):
1350 1351
1351 1352 def __instancecheck__(self, instance):
1352 1353 return isinstance(instance, EmptyCommit)
1353 1354
1354 1355
1355 1356 class EmptyChangeset(EmptyCommit):
1356 1357
1357 1358 __metaclass__ = EmptyChangesetClass
1358 1359
1359 1360 def __new__(cls, *args, **kwargs):
1360 1361 warnings.warn(
1361 1362 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1362 1363 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1363 1364
1364 1365 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1365 1366 alias=None, revision=-1, message='', author='', date=None):
1366 1367 if requested_revision is not None:
1367 1368 warnings.warn(
1368 1369 "Parameter requested_revision not supported anymore",
1369 1370 DeprecationWarning)
1370 1371 super(EmptyChangeset, self).__init__(
1371 1372 commit_id=cs, repo=repo, alias=alias, idx=revision,
1372 1373 message=message, author=author, date=date)
1373 1374
1374 1375 @property
1375 1376 def revision(self):
1376 1377 warnings.warn("Use idx instead", DeprecationWarning)
1377 1378 return self.idx
1378 1379
1379 1380 @revision.setter
1380 1381 def revision(self, value):
1381 1382 warnings.warn("Use idx instead", DeprecationWarning)
1382 1383 self.idx = value
1383 1384
1384 1385
1385 1386 class CollectionGenerator(object):
1386 1387
1387 1388 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1388 1389 self.repo = repo
1389 1390 self.commit_ids = commit_ids
1390 1391 # TODO: (oliver) this isn't currently hooked up
1391 1392 self.collection_size = None
1392 1393 self.pre_load = pre_load
1393 1394
1394 1395 def __len__(self):
1395 1396 if self.collection_size is not None:
1396 1397 return self.collection_size
1397 1398 return self.commit_ids.__len__()
1398 1399
1399 1400 def __iter__(self):
1400 1401 for commit_id in self.commit_ids:
1401 1402 # TODO: johbo: Mercurial passes in commit indices or commit ids
1402 1403 yield self._commit_factory(commit_id)
1403 1404
1404 1405 def _commit_factory(self, commit_id):
1405 1406 """
1406 1407 Allows backends to override the way commits are generated.
1407 1408 """
1408 1409 return self.repo.get_commit(commit_id=commit_id,
1409 1410 pre_load=self.pre_load)
1410 1411
1411 1412 def __getslice__(self, i, j):
1412 1413 """
1413 1414 Returns an iterator of sliced repository
1414 1415 """
1415 1416 commit_ids = self.commit_ids[i:j]
1416 1417 return self.__class__(
1417 1418 self.repo, commit_ids, pre_load=self.pre_load)
1418 1419
1419 1420 def __repr__(self):
1420 1421 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1421 1422
1422 1423
1423 1424 class Config(object):
1424 1425 """
1425 1426 Represents the configuration for a repository.
1426 1427
1427 1428 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1428 1429 standard library. It implements only the needed subset.
1429 1430 """
1430 1431
1431 1432 def __init__(self):
1432 1433 self._values = {}
1433 1434
1434 1435 def copy(self):
1435 1436 clone = Config()
1436 1437 for section, values in self._values.items():
1437 1438 clone._values[section] = values.copy()
1438 1439 return clone
1439 1440
1440 1441 def __repr__(self):
1441 1442 return '<Config(%s sections) at %s>' % (
1442 1443 len(self._values), hex(id(self)))
1443 1444
1444 1445 def items(self, section):
1445 1446 return self._values.get(section, {}).iteritems()
1446 1447
1447 1448 def get(self, section, option):
1448 1449 return self._values.get(section, {}).get(option)
1449 1450
1450 1451 def set(self, section, option, value):
1451 1452 section_values = self._values.setdefault(section, {})
1452 1453 section_values[option] = value
1453 1454
1454 1455 def clear_section(self, section):
1455 1456 self._values[section] = {}
1456 1457
1457 1458 def serialize(self):
1458 1459 """
1459 1460 Creates a list of three tuples (section, key, value) representing
1460 1461 this config object.
1461 1462 """
1462 1463 items = []
1463 1464 for section in self._values:
1464 1465 for option, value in self._values[section].items():
1465 1466 items.append(
1466 1467 (safe_str(section), safe_str(option), safe_str(value)))
1467 1468 return items
1468 1469
1469 1470
1470 1471 class Diff(object):
1471 1472 """
1472 1473 Represents a diff result from a repository backend.
1473 1474
1474 1475 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1475 1476 """
1476 1477
1477 1478 _header_re = None
1478 1479
1479 1480 def __init__(self, raw_diff):
1480 1481 self.raw = raw_diff
1481 1482
1482 1483 def chunks(self):
1483 1484 """
1484 1485 split the diff in chunks of separate --git a/file b/file chunks
1485 1486 to make diffs consistent we must prepend with \n, and make sure
1486 1487 we can detect last chunk as this was also has special rule
1487 1488 """
1488 1489 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1489 1490 total_chunks = len(chunks)
1490 1491 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1491 1492 for cur_chunk, chunk in enumerate(chunks, start=1))
1492 1493
1493 1494
1494 1495 class DiffChunk(object):
1495 1496
1496 1497 def __init__(self, chunk, diff, last_chunk):
1497 1498 self._diff = diff
1498 1499
1499 1500 # since we split by \ndiff --git that part is lost from original diff
1500 1501 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1501 1502 if not last_chunk:
1502 1503 chunk += '\n'
1503 1504
1504 1505 match = self._diff._header_re.match(chunk)
1505 1506 self.header = match.groupdict()
1506 1507 self.diff = chunk[match.end():]
1507 1508 self.raw = chunk
@@ -1,564 +1,577 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import datetime
22 22 import time
23 23
24 24 import pytest
25 25
26 26 from rhodecode.lib.vcs.backends.base import (
27 27 CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit)
28 28 from rhodecode.lib.vcs.exceptions import (
29 29 BranchDoesNotExistError, CommitDoesNotExistError,
30 30 RepositoryError, EmptyRepositoryError)
31 31 from rhodecode.lib.vcs.nodes import (
32 32 FileNode, AddedFileNodesGenerator,
33 33 ChangedFileNodesGenerator, RemovedFileNodesGenerator)
34 34 from rhodecode.tests import get_new_dir
35 35 from rhodecode.tests.vcs.base import BackendTestMixin
36 36
37 37
38 38 class TestBaseChangeset:
39 39
40 40 def test_is_deprecated(self):
41 41 from rhodecode.lib.vcs.backends.base import BaseChangeset
42 42 pytest.deprecated_call(BaseChangeset)
43 43
44 44
45 45 class TestEmptyCommit:
46 46
47 47 def test_branch_without_alias_returns_none(self):
48 48 commit = EmptyCommit()
49 49 assert commit.branch is None
50 50
51 51
52 52 class TestCommitsInNonEmptyRepo(BackendTestMixin):
53 53 recreate_repo_per_test = True
54 54
55 55 @classmethod
56 56 def _get_commits(cls):
57 57 start_date = datetime.datetime(2010, 1, 1, 20)
58 58 for x in xrange(5):
59 59 yield {
60 60 'message': 'Commit %d' % x,
61 61 'author': 'Joe Doe <joe.doe@example.com>',
62 62 'date': start_date + datetime.timedelta(hours=12 * x),
63 63 'added': [
64 64 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
65 65 ],
66 66 }
67 67
68 68 def test_walk_returns_empty_list_in_case_of_file(self):
69 69 result = list(self.tip.walk('file_0.txt'))
70 70 assert result == []
71 71
72 72 @pytest.mark.backends("git", "hg")
73 73 def test_new_branch(self):
74 74 self.imc.add(FileNode('docs/index.txt',
75 75 content='Documentation\n'))
76 76 foobar_tip = self.imc.commit(
77 77 message=u'New branch: foobar',
78 78 author=u'joe',
79 79 branch='foobar',
80 80 )
81 81 assert 'foobar' in self.repo.branches
82 82 assert foobar_tip.branch == 'foobar'
83 83 # 'foobar' should be the only branch that contains the new commit
84 84 branch = self.repo.branches.values()
85 85 assert branch[0] != branch[1]
86 86
87 87 @pytest.mark.backends("git", "hg")
88 88 def test_new_head_in_default_branch(self):
89 89 tip = self.repo.get_commit()
90 90 self.imc.add(FileNode('docs/index.txt',
91 91 content='Documentation\n'))
92 92 foobar_tip = self.imc.commit(
93 93 message=u'New branch: foobar',
94 94 author=u'joe',
95 95 branch='foobar',
96 96 parents=[tip],
97 97 )
98 98 self.imc.change(FileNode('docs/index.txt',
99 99 content='Documentation\nand more...\n'))
100 100 newtip = self.imc.commit(
101 101 message=u'At default branch',
102 102 author=u'joe',
103 103 branch=foobar_tip.branch,
104 104 parents=[foobar_tip],
105 105 )
106 106
107 107 newest_tip = self.imc.commit(
108 108 message=u'Merged with %s' % foobar_tip.raw_id,
109 109 author=u'joe',
110 110 branch=self.backend_class.DEFAULT_BRANCH_NAME,
111 111 parents=[newtip, foobar_tip],
112 112 )
113 113
114 114 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
115 115
116 116 @pytest.mark.backends("git", "hg")
117 117 def test_get_commits_respects_branch_name(self):
118 118 """
119 119 * e1930d0 (HEAD, master) Back in default branch
120 120 | * e1930d0 (docs) New Branch: docs2
121 121 | * dcc14fa New branch: docs
122 122 |/
123 123 * e63c41a Initial commit
124 124 ...
125 125 * 624d3db Commit 0
126 126
127 127 :return:
128 128 """
129 129 DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME
130 130 TEST_BRANCH = 'docs'
131 131 org_tip = self.repo.get_commit()
132 132
133 133 self.imc.add(FileNode('readme.txt', content='Document\n'))
134 134 initial = self.imc.commit(
135 135 message=u'Initial commit',
136 136 author=u'joe',
137 137 parents=[org_tip],
138 138 branch=DEFAULT_BRANCH,)
139 139
140 140 self.imc.add(FileNode('newdoc.txt', content='foobar\n'))
141 141 docs_branch_commit1 = self.imc.commit(
142 142 message=u'New branch: docs',
143 143 author=u'joe',
144 144 parents=[initial],
145 145 branch=TEST_BRANCH,)
146 146
147 147 self.imc.add(FileNode('newdoc2.txt', content='foobar2\n'))
148 148 docs_branch_commit2 = self.imc.commit(
149 149 message=u'New branch: docs2',
150 150 author=u'joe',
151 151 parents=[docs_branch_commit1],
152 152 branch=TEST_BRANCH,)
153 153
154 154 self.imc.add(FileNode('newfile', content='hello world\n'))
155 155 self.imc.commit(
156 156 message=u'Back in default branch',
157 157 author=u'joe',
158 158 parents=[initial],
159 159 branch=DEFAULT_BRANCH,)
160 160
161 161 default_branch_commits = self.repo.get_commits(
162 162 branch_name=DEFAULT_BRANCH)
163 163 assert docs_branch_commit1 not in list(default_branch_commits)
164 164 assert docs_branch_commit2 not in list(default_branch_commits)
165 165
166 166 docs_branch_commits = self.repo.get_commits(
167 167 start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1],
168 168 branch_name=TEST_BRANCH)
169 169 assert docs_branch_commit1 in list(docs_branch_commits)
170 170 assert docs_branch_commit2 in list(docs_branch_commits)
171 171
172 172 @pytest.mark.backends("svn")
173 173 def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn):
174 174 repo = vcsbackend_svn['svn-simple-layout']
175 175 commits = repo.get_commits(branch_name='trunk')
176 176 commit_indexes = [c.idx for c in commits]
177 177 assert commit_indexes == [1, 2, 3, 7, 12, 15]
178 178
179 179 def test_get_commit_by_branch(self):
180 180 for branch, commit_id in self.repo.branches.iteritems():
181 181 assert commit_id == self.repo.get_commit(branch).raw_id
182 182
183 183 def test_get_commit_by_tag(self):
184 184 for tag, commit_id in self.repo.tags.iteritems():
185 185 assert commit_id == self.repo.get_commit(tag).raw_id
186 186
187 187 def test_get_commit_parents(self):
188 188 repo = self.repo
189 189 for test_idx in [1, 2, 3]:
190 190 commit = repo.get_commit(commit_idx=test_idx - 1)
191 191 assert [commit] == repo.get_commit(commit_idx=test_idx).parents
192 192
193 193 def test_get_commit_children(self):
194 194 repo = self.repo
195 195 for test_idx in [1, 2, 3]:
196 196 commit = repo.get_commit(commit_idx=test_idx + 1)
197 197 assert [commit] == repo.get_commit(commit_idx=test_idx).children
198 198
199 199
200 200 class TestCommits(BackendTestMixin):
201 201 recreate_repo_per_test = False
202 202
203 203 @classmethod
204 204 def _get_commits(cls):
205 205 start_date = datetime.datetime(2010, 1, 1, 20)
206 206 for x in xrange(5):
207 207 yield {
208 208 'message': u'Commit %d' % x,
209 209 'author': u'Joe Doe <joe.doe@example.com>',
210 210 'date': start_date + datetime.timedelta(hours=12 * x),
211 211 'added': [
212 212 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
213 213 ],
214 214 }
215 215
216 216 def test_simple(self):
217 217 tip = self.repo.get_commit()
218 218 assert tip.date, datetime.datetime(2010, 1, 3 == 20)
219 219
220 220 def test_simple_serialized_commit(self):
221 221 tip = self.repo.get_commit()
222 222 # json.dumps(tip) uses .__json__() method
223 223 data = tip.__json__()
224 224 assert 'branch' in data
225 225 assert data['revision']
226 226
227 227 def test_retrieve_tip(self):
228 228 tip = self.repo.get_commit('tip')
229 229 assert tip == self.repo.get_commit()
230 230
231 231 def test_invalid(self):
232 232 with pytest.raises(CommitDoesNotExistError):
233 233 self.repo.get_commit(commit_idx=123456789)
234 234
235 235 def test_idx(self):
236 236 commit = self.repo[0]
237 237 assert commit.idx == 0
238 238
239 239 def test_negative_idx(self):
240 240 commit = self.repo.get_commit(commit_idx=-1)
241 241 assert commit.idx >= 0
242 242
243 243 def test_revision_is_deprecated(self):
244 244 def get_revision(commit):
245 245 return commit.revision
246 246
247 247 commit = self.repo[0]
248 248 pytest.deprecated_call(get_revision, commit)
249 249
250 250 def test_size(self):
251 251 tip = self.repo.get_commit()
252 252 size = 5 * len('Foobar N') # Size of 5 files
253 253 assert tip.size == size
254 254
255 255 def test_size_at_commit(self):
256 256 tip = self.repo.get_commit()
257 257 size = 5 * len('Foobar N') # Size of 5 files
258 258 assert self.repo.size_at_commit(tip.raw_id) == size
259 259
260 260 def test_size_at_first_commit(self):
261 261 commit = self.repo[0]
262 262 size = len('Foobar N') # Size of 1 file
263 263 assert self.repo.size_at_commit(commit.raw_id) == size
264 264
265 265 def test_author(self):
266 266 tip = self.repo.get_commit()
267 267 assert_text_equal(tip.author, u'Joe Doe <joe.doe@example.com>')
268 268
269 269 def test_author_name(self):
270 270 tip = self.repo.get_commit()
271 271 assert_text_equal(tip.author_name, u'Joe Doe')
272 272
273 273 def test_author_email(self):
274 274 tip = self.repo.get_commit()
275 275 assert_text_equal(tip.author_email, u'joe.doe@example.com')
276 276
277 277 def test_message(self):
278 278 tip = self.repo.get_commit()
279 279 assert_text_equal(tip.message, u'Commit 4')
280 280
281 281 def test_diff(self):
282 282 tip = self.repo.get_commit()
283 283 diff = tip.diff()
284 284 assert "+Foobar 4" in diff.raw
285 285
286 286 def test_prev(self):
287 287 tip = self.repo.get_commit()
288 288 prev_commit = tip.prev()
289 289 assert prev_commit.message == 'Commit 3'
290 290
291 291 def test_prev_raises_on_first_commit(self):
292 292 commit = self.repo.get_commit(commit_idx=0)
293 293 with pytest.raises(CommitDoesNotExistError):
294 294 commit.prev()
295 295
296 296 def test_prev_works_on_second_commit_issue_183(self):
297 297 commit = self.repo.get_commit(commit_idx=1)
298 298 prev_commit = commit.prev()
299 299 assert prev_commit.idx == 0
300 300
301 301 def test_next(self):
302 302 commit = self.repo.get_commit(commit_idx=2)
303 303 next_commit = commit.next()
304 304 assert next_commit.message == 'Commit 3'
305 305
306 306 def test_next_raises_on_tip(self):
307 307 commit = self.repo.get_commit()
308 308 with pytest.raises(CommitDoesNotExistError):
309 309 commit.next()
310 310
311 311 def test_get_file_commit(self):
312 312 commit = self.repo.get_commit()
313 313 commit.get_file_commit('file_4.txt')
314 314 assert commit.message == 'Commit 4'
315 315
316 316 def test_get_filenodes_generator(self):
317 317 tip = self.repo.get_commit()
318 318 filepaths = [node.path for node in tip.get_filenodes_generator()]
319 319 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
320 320
321 321 def test_get_file_annotate(self):
322 322 file_added_commit = self.repo.get_commit(commit_idx=3)
323 323 annotations = list(file_added_commit.get_file_annotate('file_3.txt'))
324 324 line_no, commit_id, commit_loader, line = annotations[0]
325 325 assert line_no == 1
326 326 assert commit_id == file_added_commit.raw_id
327 327 assert commit_loader() == file_added_commit
328 328
329 329 # git annotation is generated differently thus different results
330 330 if self.repo.alias == 'git':
331 331 assert line == '(Joe Doe 2010-01-03 08:00:00 +0000 1) Foobar 3'
332 332 else:
333 333 assert line == 'Foobar 3'
334 334
335 335 def test_get_file_annotate_does_not_exist(self):
336 336 file_added_commit = self.repo.get_commit(commit_idx=2)
337 337 # TODO: Should use a specific exception class here?
338 338 with pytest.raises(Exception):
339 339 list(file_added_commit.get_file_annotate('file_3.txt'))
340 340
341 341 def test_get_file_annotate_tip(self):
342 342 tip = self.repo.get_commit()
343 343 commit = self.repo.get_commit(commit_idx=3)
344 344 expected_values = list(commit.get_file_annotate('file_3.txt'))
345 345 annotations = list(tip.get_file_annotate('file_3.txt'))
346 346
347 347 # Note: Skip index 2 because the loader function is not the same
348 348 for idx in (0, 1, 3):
349 349 assert annotations[0][idx] == expected_values[0][idx]
350 350
351 351 def test_get_commits_is_ordered_by_date(self):
352 352 commits = self.repo.get_commits()
353 353 assert isinstance(commits, CollectionGenerator)
354 354 assert len(commits) == 0 or len(commits) != 0
355 355 commits = list(commits)
356 356 ordered_by_date = sorted(commits, key=lambda commit: commit.date)
357 357 assert commits == ordered_by_date
358 358
359 359 def test_get_commits_respects_start(self):
360 360 second_id = self.repo.commit_ids[1]
361 361 commits = self.repo.get_commits(start_id=second_id)
362 362 assert isinstance(commits, CollectionGenerator)
363 363 commits = list(commits)
364 364 assert len(commits) == 4
365 365
366 366 def test_get_commits_includes_start_commit(self):
367 367 second_id = self.repo.commit_ids[1]
368 368 commits = self.repo.get_commits(start_id=second_id)
369 369 assert isinstance(commits, CollectionGenerator)
370 370 commits = list(commits)
371 371 assert commits[0].raw_id == second_id
372 372
373 373 def test_get_commits_respects_end(self):
374 374 second_id = self.repo.commit_ids[1]
375 375 commits = self.repo.get_commits(end_id=second_id)
376 376 assert isinstance(commits, CollectionGenerator)
377 377 commits = list(commits)
378 378 assert commits[-1].raw_id == second_id
379 379 assert len(commits) == 2
380 380
381 381 def test_get_commits_respects_both_start_and_end(self):
382 382 second_id = self.repo.commit_ids[1]
383 383 third_id = self.repo.commit_ids[2]
384 384 commits = self.repo.get_commits(start_id=second_id, end_id=third_id)
385 385 assert isinstance(commits, CollectionGenerator)
386 386 commits = list(commits)
387 387 assert len(commits) == 2
388 388
389 389 def test_get_commits_on_empty_repo_raises_EmptyRepository_error(self):
390 390 repo_path = get_new_dir(str(time.time()))
391 391 repo = self.Backend(repo_path, create=True)
392 392
393 393 with pytest.raises(EmptyRepositoryError):
394 394 list(repo.get_commits(start_id='foobar'))
395 395
396 396 def test_get_commits_includes_end_commit(self):
397 397 second_id = self.repo.commit_ids[1]
398 398 commits = self.repo.get_commits(end_id=second_id)
399 399 assert isinstance(commits, CollectionGenerator)
400 400 assert len(commits) == 2
401 401 commits = list(commits)
402 402 assert commits[-1].raw_id == second_id
403 403
404 404 def test_get_commits_respects_start_date(self):
405 405 start_date = datetime.datetime(2010, 1, 2)
406 406 commits = self.repo.get_commits(start_date=start_date)
407 407 assert isinstance(commits, CollectionGenerator)
408 408 # Should be 4 commits after 2010-01-02 00:00:00
409 409 assert len(commits) == 4
410 410 for c in commits:
411 411 assert c.date >= start_date
412 412
413 413 def test_get_commits_respects_start_date_and_end_date(self):
414 414 start_date = datetime.datetime(2010, 1, 2)
415 415 end_date = datetime.datetime(2010, 1, 3)
416 416 commits = self.repo.get_commits(start_date=start_date,
417 417 end_date=end_date)
418 418 assert isinstance(commits, CollectionGenerator)
419 419 assert len(commits) == 2
420 420 for c in commits:
421 421 assert c.date >= start_date
422 422 assert c.date <= end_date
423 423
424 424 def test_get_commits_respects_end_date(self):
425 425 end_date = datetime.datetime(2010, 1, 2)
426 426 commits = self.repo.get_commits(end_date=end_date)
427 427 assert isinstance(commits, CollectionGenerator)
428 428 assert len(commits) == 1
429 429 for c in commits:
430 430 assert c.date <= end_date
431 431
432 432 def test_get_commits_respects_reverse(self):
433 433 commits = self.repo.get_commits() # no longer reverse support
434 434 assert isinstance(commits, CollectionGenerator)
435 435 assert len(commits) == 5
436 436 commit_ids = reversed([c.raw_id for c in commits])
437 437 assert list(commit_ids) == list(reversed(self.repo.commit_ids))
438 438
439 439 def test_get_commits_slice_generator(self):
440 440 commits = self.repo.get_commits(
441 441 branch_name=self.repo.DEFAULT_BRANCH_NAME)
442 442 assert isinstance(commits, CollectionGenerator)
443 443 commit_slice = list(commits[1:3])
444 444 assert len(commit_slice) == 2
445 445
446 446 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self):
447 447 with pytest.raises(CommitDoesNotExistError):
448 448 list(self.repo.get_commits(start_id='foobar'))
449 449
450 450 def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self):
451 451 with pytest.raises(CommitDoesNotExistError):
452 452 list(self.repo.get_commits(end_id='foobar'))
453 453
454 454 def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self):
455 455 with pytest.raises(BranchDoesNotExistError):
456 456 list(self.repo.get_commits(branch_name='foobar'))
457 457
458 458 def test_get_commits_raise_repositoryerror_for_wrong_start_end(self):
459 459 start_id = self.repo.commit_ids[-1]
460 460 end_id = self.repo.commit_ids[0]
461 461 with pytest.raises(RepositoryError):
462 462 list(self.repo.get_commits(start_id=start_id, end_id=end_id))
463 463
464 464 def test_get_commits_raises_for_numerical_ids(self):
465 465 with pytest.raises(TypeError):
466 466 self.repo.get_commits(start_id=1, end_id=2)
467 467
468 def test_commit_equality(self):
469 commit1 = self.repo.get_commit(self.repo.commit_ids[0])
470 commit2 = self.repo.get_commit(self.repo.commit_ids[1])
471
472 assert commit1 == commit1
473 assert commit2 == commit2
474 assert commit1 != commit2
475 assert commit2 != commit1
476 assert commit1 != None
477 assert None != commit1
478 assert 1 != commit1
479 assert 'string' != commit1
480
468 481
469 482 @pytest.mark.parametrize("filename, expected", [
470 483 ("README.rst", False),
471 484 ("README", True),
472 485 ])
473 486 def test_commit_is_link(vcsbackend, filename, expected):
474 487 commit = vcsbackend.repo.get_commit()
475 488 link_status = commit.is_link(filename)
476 489 assert link_status is expected
477 490
478 491
479 492 class TestCommitsChanges(BackendTestMixin):
480 493 recreate_repo_per_test = False
481 494
482 495 @classmethod
483 496 def _get_commits(cls):
484 497 return [
485 498 {
486 499 'message': u'Initial',
487 500 'author': u'Joe Doe <joe.doe@example.com>',
488 501 'date': datetime.datetime(2010, 1, 1, 20),
489 502 'added': [
490 503 FileNode('foo/bar', content='foo'),
491 504 FileNode('foo/bał', content='foo'),
492 505 FileNode('foobar', content='foo'),
493 506 FileNode('qwe', content='foo'),
494 507 ],
495 508 },
496 509 {
497 510 'message': u'Massive changes',
498 511 'author': u'Joe Doe <joe.doe@example.com>',
499 512 'date': datetime.datetime(2010, 1, 1, 22),
500 513 'added': [FileNode('fallout', content='War never changes')],
501 514 'changed': [
502 515 FileNode('foo/bar', content='baz'),
503 516 FileNode('foobar', content='baz'),
504 517 ],
505 518 'removed': [FileNode('qwe')],
506 519 },
507 520 ]
508 521
509 522 def test_initial_commit(self):
510 523 commit = self.repo.get_commit(commit_idx=0)
511 524 assert set(commit.added) == set([
512 525 commit.get_node('foo/bar'),
513 526 commit.get_node('foo/bał'),
514 527 commit.get_node('foobar'),
515 528 commit.get_node('qwe'),
516 529 ])
517 530 assert set(commit.changed) == set()
518 531 assert set(commit.removed) == set()
519 532 assert set(commit.affected_files) == set(
520 533 ['foo/bar', 'foo/bał', 'foobar', 'qwe'])
521 534 assert commit.date == datetime.datetime(2010, 1, 1, 20, 0)
522 535
523 536 def test_head_added(self):
524 537 commit = self.repo.get_commit()
525 538 assert isinstance(commit.added, AddedFileNodesGenerator)
526 539 assert set(commit.added) == set([commit.get_node('fallout')])
527 540 assert isinstance(commit.changed, ChangedFileNodesGenerator)
528 541 assert set(commit.changed) == set([
529 542 commit.get_node('foo/bar'),
530 543 commit.get_node('foobar'),
531 544 ])
532 545 assert isinstance(commit.removed, RemovedFileNodesGenerator)
533 546 assert len(commit.removed) == 1
534 547 assert list(commit.removed)[0].path == 'qwe'
535 548
536 549 def test_get_filemode(self):
537 550 commit = self.repo.get_commit()
538 551 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar')
539 552
540 553 def test_get_filemode_non_ascii(self):
541 554 commit = self.repo.get_commit()
542 555 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał')
543 556 assert FILEMODE_DEFAULT == commit.get_file_mode(u'foo/bał')
544 557
545 558 def test_get_file_history(self):
546 559 commit = self.repo.get_commit()
547 560 history = commit.get_file_history('foo/bar')
548 561 assert len(history) == 2
549 562
550 563 def test_get_file_history_with_limit(self):
551 564 commit = self.repo.get_commit()
552 565 history = commit.get_file_history('foo/bar', limit=1)
553 566 assert len(history) == 1
554 567
555 568 def test_get_file_history_first_commit(self):
556 569 commit = self.repo[0]
557 570 history = commit.get_file_history('foo/bar')
558 571 assert len(history) == 1
559 572
560 573
561 574 def assert_text_equal(expected, given):
562 575 assert expected == given
563 576 assert isinstance(expected, unicode)
564 577 assert isinstance(given, unicode)
General Comments 0
You need to be logged in to leave comments. Login now