##// END OF EJS Templates
hg: Include state of dry_run in merge failure logging.
johbo -
r147:bffdfabf default
parent child Browse files
Show More
@@ -1,1496 +1,1498 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 """
370 370 Merge the revisions specified in `source_ref` from `source_repo`
371 371 onto the `target_ref` of this repository.
372 372
373 373 `source_ref` and `target_ref` are named tupls with the following
374 374 fields `type`, `name` and `commit_id`.
375 375
376 376 Returns a MergeResponse named tuple with the following fields
377 377 'possible', 'executed', 'source_commit', 'target_commit',
378 378 'merge_commit'.
379 379
380 380 :param target_ref: `target_ref` points to the commit on top of which
381 381 the `source_ref` should be merged.
382 382 :param source_repo: The repository that contains the commits to be
383 383 merged.
384 384 :param source_ref: `source_ref` points to the topmost commit from
385 385 the `source_repo` which should be merged.
386 386 :param workspace_id: `workspace_id` unique identifier.
387 387 :param user_name: Merge commit `user_name`.
388 388 :param user_email: Merge commit `user_email`.
389 389 :param message: Merge commit `message`.
390 390 :param dry_run: If `True` the merge will not take place.
391 391 """
392 392 if dry_run:
393 393 message = message or 'sample_message'
394 394 user_email = user_email or 'user@email.com'
395 395 user_name = user_name or 'user name'
396 396 else:
397 397 if not user_name:
398 398 raise ValueError('user_name cannot be empty')
399 399 if not user_email:
400 400 raise ValueError('user_email cannot be empty')
401 401 if not message:
402 402 raise ValueError('message cannot be empty')
403 403
404 404 shadow_repository_path = self._maybe_prepare_merge_workspace(
405 405 workspace_id, target_ref)
406 406
407 407 try:
408 408 return self._merge_repo(
409 409 shadow_repository_path, target_ref, source_repo,
410 410 source_ref, message, user_name, user_email, dry_run=dry_run)
411 411 except RepositoryError:
412 log.exception('Unexpected failure when running merge')
412 log.exception(
413 'Unexpected failure when running merge, dry-run=%s',
414 dry_run)
413 415 return MergeResponse(
414 416 False, False, None, MergeFailureReason.UNKNOWN)
415 417
416 418 def _merge_repo(self, shadow_repository_path, target_ref,
417 419 source_repo, source_ref, merge_message,
418 420 merger_name, merger_email, dry_run=False):
419 421 """Internal implementation of merge."""
420 422 raise NotImplementedError
421 423
422 424 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
423 425 """
424 426 Create the merge workspace.
425 427
426 428 :param workspace_id: `workspace_id` unique identifier.
427 429 """
428 430 raise NotImplementedError
429 431
430 432 def cleanup_merge_workspace(self, workspace_id):
431 433 """
432 434 Remove merge workspace.
433 435
434 436 This function MUST not fail in case there is no workspace associated to
435 437 the given `workspace_id`.
436 438
437 439 :param workspace_id: `workspace_id` unique identifier.
438 440 """
439 441 raise NotImplementedError
440 442
441 443 # ========== #
442 444 # COMMIT API #
443 445 # ========== #
444 446
445 447 @LazyProperty
446 448 def in_memory_commit(self):
447 449 """
448 450 Returns :class:`InMemoryCommit` object for this repository.
449 451 """
450 452 raise NotImplementedError
451 453
452 454 # ======================== #
453 455 # UTILITIES FOR SUBCLASSES #
454 456 # ======================== #
455 457
456 458 def _validate_diff_commits(self, commit1, commit2):
457 459 """
458 460 Validates that the given commits are related to this repository.
459 461
460 462 Intended as a utility for sub classes to have a consistent validation
461 463 of input parameters in methods like :meth:`get_diff`.
462 464 """
463 465 self._validate_commit(commit1)
464 466 self._validate_commit(commit2)
465 467 if (isinstance(commit1, EmptyCommit) and
466 468 isinstance(commit2, EmptyCommit)):
467 469 raise ValueError("Cannot compare two empty commits")
468 470
469 471 def _validate_commit(self, commit):
470 472 if not isinstance(commit, BaseCommit):
471 473 raise TypeError(
472 474 "%s is not of type BaseCommit" % repr(commit))
473 475 if commit.repository != self and not isinstance(commit, EmptyCommit):
474 476 raise ValueError(
475 477 "Commit %s must be a valid commit from this repository %s, "
476 478 "related to this repository instead %s." %
477 479 (commit, self, commit.repository))
478 480
479 481 def _validate_commit_id(self, commit_id):
480 482 if not isinstance(commit_id, basestring):
481 483 raise TypeError("commit_id must be a string value")
482 484
483 485 def _validate_commit_idx(self, commit_idx):
484 486 if not isinstance(commit_idx, (int, long)):
485 487 raise TypeError("commit_idx must be a numeric value")
486 488
487 489 def _validate_branch_name(self, branch_name):
488 490 if branch_name and branch_name not in self.branches_all:
489 491 msg = ("Branch %s not found in %s" % (branch_name, self))
490 492 raise BranchDoesNotExistError(msg)
491 493
492 494 #
493 495 # Supporting deprecated API parts
494 496 # TODO: johbo: consider to move this into a mixin
495 497 #
496 498
497 499 @property
498 500 def EMPTY_CHANGESET(self):
499 501 warnings.warn(
500 502 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
501 503 return self.EMPTY_COMMIT_ID
502 504
503 505 @property
504 506 def revisions(self):
505 507 warnings.warn("Use commits attribute instead", DeprecationWarning)
506 508 return self.commit_ids
507 509
508 510 @revisions.setter
509 511 def revisions(self, value):
510 512 warnings.warn("Use commits attribute instead", DeprecationWarning)
511 513 self.commit_ids = value
512 514
513 515 def get_changeset(self, revision=None, pre_load=None):
514 516 warnings.warn("Use get_commit instead", DeprecationWarning)
515 517 commit_id = None
516 518 commit_idx = None
517 519 if isinstance(revision, basestring):
518 520 commit_id = revision
519 521 else:
520 522 commit_idx = revision
521 523 return self.get_commit(
522 524 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
523 525
524 526 def get_changesets(
525 527 self, start=None, end=None, start_date=None, end_date=None,
526 528 branch_name=None, pre_load=None):
527 529 warnings.warn("Use get_commits instead", DeprecationWarning)
528 530 start_id = self._revision_to_commit(start)
529 531 end_id = self._revision_to_commit(end)
530 532 return self.get_commits(
531 533 start_id=start_id, end_id=end_id, start_date=start_date,
532 534 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
533 535
534 536 def _revision_to_commit(self, revision):
535 537 """
536 538 Translates a revision to a commit_id
537 539
538 540 Helps to support the old changeset based API which allows to use
539 541 commit ids and commit indices interchangeable.
540 542 """
541 543 if revision is None:
542 544 return revision
543 545
544 546 if isinstance(revision, basestring):
545 547 commit_id = revision
546 548 else:
547 549 commit_id = self.commit_ids[revision]
548 550 return commit_id
549 551
550 552 @property
551 553 def in_memory_changeset(self):
552 554 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
553 555 return self.in_memory_commit
554 556
555 557
556 558 class BaseCommit(object):
557 559 """
558 560 Each backend should implement it's commit representation.
559 561
560 562 **Attributes**
561 563
562 564 ``repository``
563 565 repository object within which commit exists
564 566
565 567 ``id``
566 568 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
567 569 just ``tip``.
568 570
569 571 ``raw_id``
570 572 raw commit representation (i.e. full 40 length sha for git
571 573 backend)
572 574
573 575 ``short_id``
574 576 shortened (if apply) version of ``raw_id``; it would be simple
575 577 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
576 578 as ``raw_id`` for subversion
577 579
578 580 ``idx``
579 581 commit index
580 582
581 583 ``files``
582 584 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
583 585
584 586 ``dirs``
585 587 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
586 588
587 589 ``nodes``
588 590 combined list of ``Node`` objects
589 591
590 592 ``author``
591 593 author of the commit, as unicode
592 594
593 595 ``message``
594 596 message of the commit, as unicode
595 597
596 598 ``parents``
597 599 list of parent commits
598 600
599 601 """
600 602
601 603 branch = None
602 604 """
603 605 Depending on the backend this should be set to the branch name of the
604 606 commit. Backends not supporting branches on commits should leave this
605 607 value as ``None``.
606 608 """
607 609
608 610 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
609 611 """
610 612 This template is used to generate a default prefix for repository archives
611 613 if no prefix has been specified.
612 614 """
613 615
614 616 def __str__(self):
615 617 return '<%s at %s:%s>' % (
616 618 self.__class__.__name__, self.idx, self.short_id)
617 619
618 620 def __repr__(self):
619 621 return self.__str__()
620 622
621 623 def __unicode__(self):
622 624 return u'%s:%s' % (self.idx, self.short_id)
623 625
624 626 def __eq__(self, other):
625 627 return self.raw_id == other.raw_id
626 628
627 629 def __json__(self):
628 630 parents = []
629 631 try:
630 632 for parent in self.parents:
631 633 parents.append({'raw_id': parent.raw_id})
632 634 except NotImplementedError:
633 635 # empty commit doesn't have parents implemented
634 636 pass
635 637
636 638 return {
637 639 'short_id': self.short_id,
638 640 'raw_id': self.raw_id,
639 641 'revision': self.idx,
640 642 'message': self.message,
641 643 'date': self.date,
642 644 'author': self.author,
643 645 'parents': parents,
644 646 'branch': self.branch
645 647 }
646 648
647 649 @LazyProperty
648 650 def last(self):
649 651 """
650 652 ``True`` if this is last commit in repository, ``False``
651 653 otherwise; trying to access this attribute while there is no
652 654 commits would raise `EmptyRepositoryError`
653 655 """
654 656 if self.repository is None:
655 657 raise CommitError("Cannot check if it's most recent commit")
656 658 return self.raw_id == self.repository.commit_ids[-1]
657 659
658 660 @LazyProperty
659 661 def parents(self):
660 662 """
661 663 Returns list of parent commits.
662 664 """
663 665 raise NotImplementedError
664 666
665 667 @property
666 668 def merge(self):
667 669 """
668 670 Returns boolean if commit is a merge.
669 671 """
670 672 return len(self.parents) > 1
671 673
672 674 @LazyProperty
673 675 def children(self):
674 676 """
675 677 Returns list of child commits.
676 678 """
677 679 raise NotImplementedError
678 680
679 681 @LazyProperty
680 682 def id(self):
681 683 """
682 684 Returns string identifying this commit.
683 685 """
684 686 raise NotImplementedError
685 687
686 688 @LazyProperty
687 689 def raw_id(self):
688 690 """
689 691 Returns raw string identifying this commit.
690 692 """
691 693 raise NotImplementedError
692 694
693 695 @LazyProperty
694 696 def short_id(self):
695 697 """
696 698 Returns shortened version of ``raw_id`` attribute, as string,
697 699 identifying this commit, useful for presentation to users.
698 700 """
699 701 raise NotImplementedError
700 702
701 703 @LazyProperty
702 704 def idx(self):
703 705 """
704 706 Returns integer identifying this commit.
705 707 """
706 708 raise NotImplementedError
707 709
708 710 @LazyProperty
709 711 def committer(self):
710 712 """
711 713 Returns committer for this commit
712 714 """
713 715 raise NotImplementedError
714 716
715 717 @LazyProperty
716 718 def committer_name(self):
717 719 """
718 720 Returns committer name for this commit
719 721 """
720 722
721 723 return author_name(self.committer)
722 724
723 725 @LazyProperty
724 726 def committer_email(self):
725 727 """
726 728 Returns committer email address for this commit
727 729 """
728 730
729 731 return author_email(self.committer)
730 732
731 733 @LazyProperty
732 734 def author(self):
733 735 """
734 736 Returns author for this commit
735 737 """
736 738
737 739 raise NotImplementedError
738 740
739 741 @LazyProperty
740 742 def author_name(self):
741 743 """
742 744 Returns author name for this commit
743 745 """
744 746
745 747 return author_name(self.author)
746 748
747 749 @LazyProperty
748 750 def author_email(self):
749 751 """
750 752 Returns author email address for this commit
751 753 """
752 754
753 755 return author_email(self.author)
754 756
755 757 def get_file_mode(self, path):
756 758 """
757 759 Returns stat mode of the file at `path`.
758 760 """
759 761 raise NotImplementedError
760 762
761 763 def is_link(self, path):
762 764 """
763 765 Returns ``True`` if given `path` is a symlink
764 766 """
765 767 raise NotImplementedError
766 768
767 769 def get_file_content(self, path):
768 770 """
769 771 Returns content of the file at the given `path`.
770 772 """
771 773 raise NotImplementedError
772 774
773 775 def get_file_size(self, path):
774 776 """
775 777 Returns size of the file at the given `path`.
776 778 """
777 779 raise NotImplementedError
778 780
779 781 def get_file_commit(self, path, pre_load=None):
780 782 """
781 783 Returns last commit of the file at the given `path`.
782 784
783 785 :param pre_load: Optional. List of commit attributes to load.
784 786 """
785 787 return self.get_file_history(path, limit=1, pre_load=pre_load)[0]
786 788
787 789 def get_file_history(self, path, limit=None, pre_load=None):
788 790 """
789 791 Returns history of file as reversed list of :class:`BaseCommit`
790 792 objects for which file at given `path` has been modified.
791 793
792 794 :param limit: Optional. Allows to limit the size of the returned
793 795 history. This is intended as a hint to the underlying backend, so
794 796 that it can apply optimizations depending on the limit.
795 797 :param pre_load: Optional. List of commit attributes to load.
796 798 """
797 799 raise NotImplementedError
798 800
799 801 def get_file_annotate(self, path, pre_load=None):
800 802 """
801 803 Returns a generator of four element tuples with
802 804 lineno, sha, commit lazy loader and line
803 805
804 806 :param pre_load: Optional. List of commit attributes to load.
805 807 """
806 808 raise NotImplementedError
807 809
808 810 def get_nodes(self, path):
809 811 """
810 812 Returns combined ``DirNode`` and ``FileNode`` objects list representing
811 813 state of commit at the given ``path``.
812 814
813 815 :raises ``CommitError``: if node at the given ``path`` is not
814 816 instance of ``DirNode``
815 817 """
816 818 raise NotImplementedError
817 819
818 820 def get_node(self, path):
819 821 """
820 822 Returns ``Node`` object from the given ``path``.
821 823
822 824 :raises ``NodeDoesNotExistError``: if there is no node at the given
823 825 ``path``
824 826 """
825 827 raise NotImplementedError
826 828
827 829 def get_largefile_node(self, path):
828 830 """
829 831 Returns the path to largefile from Mercurial storage.
830 832 """
831 833 raise NotImplementedError
832 834
833 835 def archive_repo(self, file_path, kind='tgz', subrepos=None,
834 836 prefix=None, write_metadata=False, mtime=None):
835 837 """
836 838 Creates an archive containing the contents of the repository.
837 839
838 840 :param file_path: path to the file which to create the archive.
839 841 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
840 842 :param prefix: name of root directory in archive.
841 843 Default is repository name and commit's short_id joined with dash:
842 844 ``"{repo_name}-{short_id}"``.
843 845 :param write_metadata: write a metadata file into archive.
844 846 :param mtime: custom modification time for archive creation, defaults
845 847 to time.time() if not given.
846 848
847 849 :raise VCSError: If prefix has a problem.
848 850 """
849 851 allowed_kinds = settings.ARCHIVE_SPECS.keys()
850 852 if kind not in allowed_kinds:
851 853 raise ImproperArchiveTypeError(
852 854 'Archive kind (%s) not supported use one of %s' %
853 855 (kind, allowed_kinds))
854 856
855 857 prefix = self._validate_archive_prefix(prefix)
856 858
857 859 mtime = mtime or time.time()
858 860
859 861 file_info = []
860 862 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
861 863 for _r, _d, files in cur_rev.walk('/'):
862 864 for f in files:
863 865 f_path = os.path.join(prefix, f.path)
864 866 file_info.append(
865 867 (f_path, f.mode, f.is_link(), f._get_content()))
866 868
867 869 if write_metadata:
868 870 metadata = [
869 871 ('repo_name', self.repository.name),
870 872 ('rev', self.raw_id),
871 873 ('create_time', mtime),
872 874 ('branch', self.branch),
873 875 ('tags', ','.join(self.tags)),
874 876 ]
875 877 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
876 878 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
877 879
878 880 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
879 881
880 882 def _validate_archive_prefix(self, prefix):
881 883 if prefix is None:
882 884 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
883 885 repo_name=safe_str(self.repository.name),
884 886 short_id=self.short_id)
885 887 elif not isinstance(prefix, str):
886 888 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
887 889 elif prefix.startswith('/'):
888 890 raise VCSError("Prefix cannot start with leading slash")
889 891 elif prefix.strip() == '':
890 892 raise VCSError("Prefix cannot be empty")
891 893 return prefix
892 894
893 895 @LazyProperty
894 896 def root(self):
895 897 """
896 898 Returns ``RootNode`` object for this commit.
897 899 """
898 900 return self.get_node('')
899 901
900 902 def next(self, branch=None):
901 903 """
902 904 Returns next commit from current, if branch is gives it will return
903 905 next commit belonging to this branch
904 906
905 907 :param branch: show commits within the given named branch
906 908 """
907 909 indexes = xrange(self.idx + 1, self.repository.count())
908 910 return self._find_next(indexes, branch)
909 911
910 912 def prev(self, branch=None):
911 913 """
912 914 Returns previous commit from current, if branch is gives it will
913 915 return previous commit belonging to this branch
914 916
915 917 :param branch: show commit within the given named branch
916 918 """
917 919 indexes = xrange(self.idx - 1, -1, -1)
918 920 return self._find_next(indexes, branch)
919 921
920 922 def _find_next(self, indexes, branch=None):
921 923 if branch and self.branch != branch:
922 924 raise VCSError('Branch option used on commit not belonging '
923 925 'to that branch')
924 926
925 927 for next_idx in indexes:
926 928 commit = self.repository.get_commit(commit_idx=next_idx)
927 929 if branch and branch != commit.branch:
928 930 continue
929 931 return commit
930 932 raise CommitDoesNotExistError
931 933
932 934 def diff(self, ignore_whitespace=True, context=3):
933 935 """
934 936 Returns a `Diff` object representing the change made by this commit.
935 937 """
936 938 parent = (
937 939 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
938 940 diff = self.repository.get_diff(
939 941 parent, self,
940 942 ignore_whitespace=ignore_whitespace,
941 943 context=context)
942 944 return diff
943 945
944 946 @LazyProperty
945 947 def added(self):
946 948 """
947 949 Returns list of added ``FileNode`` objects.
948 950 """
949 951 raise NotImplementedError
950 952
951 953 @LazyProperty
952 954 def changed(self):
953 955 """
954 956 Returns list of modified ``FileNode`` objects.
955 957 """
956 958 raise NotImplementedError
957 959
958 960 @LazyProperty
959 961 def removed(self):
960 962 """
961 963 Returns list of removed ``FileNode`` objects.
962 964 """
963 965 raise NotImplementedError
964 966
965 967 @LazyProperty
966 968 def size(self):
967 969 """
968 970 Returns total number of bytes from contents of all filenodes.
969 971 """
970 972 return sum((node.size for node in self.get_filenodes_generator()))
971 973
972 974 def walk(self, topurl=''):
973 975 """
974 976 Similar to os.walk method. Insted of filesystem it walks through
975 977 commit starting at given ``topurl``. Returns generator of tuples
976 978 (topnode, dirnodes, filenodes).
977 979 """
978 980 topnode = self.get_node(topurl)
979 981 if not topnode.is_dir():
980 982 return
981 983 yield (topnode, topnode.dirs, topnode.files)
982 984 for dirnode in topnode.dirs:
983 985 for tup in self.walk(dirnode.path):
984 986 yield tup
985 987
986 988 def get_filenodes_generator(self):
987 989 """
988 990 Returns generator that yields *all* file nodes.
989 991 """
990 992 for topnode, dirs, files in self.walk():
991 993 for node in files:
992 994 yield node
993 995
994 996 #
995 997 # Utilities for sub classes to support consistent behavior
996 998 #
997 999
998 1000 def no_node_at_path(self, path):
999 1001 return NodeDoesNotExistError(
1000 1002 "There is no file nor directory at the given path: "
1001 1003 "'%s' at commit %s" % (path, self.short_id))
1002 1004
1003 1005 def _fix_path(self, path):
1004 1006 """
1005 1007 Paths are stored without trailing slash so we need to get rid off it if
1006 1008 needed.
1007 1009 """
1008 1010 return path.rstrip('/')
1009 1011
1010 1012 #
1011 1013 # Deprecated API based on changesets
1012 1014 #
1013 1015
1014 1016 @property
1015 1017 def revision(self):
1016 1018 warnings.warn("Use idx instead", DeprecationWarning)
1017 1019 return self.idx
1018 1020
1019 1021 @revision.setter
1020 1022 def revision(self, value):
1021 1023 warnings.warn("Use idx instead", DeprecationWarning)
1022 1024 self.idx = value
1023 1025
1024 1026 def get_file_changeset(self, path):
1025 1027 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1026 1028 return self.get_file_commit(path)
1027 1029
1028 1030
1029 1031 class BaseChangesetClass(type):
1030 1032
1031 1033 def __instancecheck__(self, instance):
1032 1034 return isinstance(instance, BaseCommit)
1033 1035
1034 1036
1035 1037 class BaseChangeset(BaseCommit):
1036 1038
1037 1039 __metaclass__ = BaseChangesetClass
1038 1040
1039 1041 def __new__(cls, *args, **kwargs):
1040 1042 warnings.warn(
1041 1043 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1042 1044 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1043 1045
1044 1046
1045 1047 class BaseInMemoryCommit(object):
1046 1048 """
1047 1049 Represents differences between repository's state (most recent head) and
1048 1050 changes made *in place*.
1049 1051
1050 1052 **Attributes**
1051 1053
1052 1054 ``repository``
1053 1055 repository object for this in-memory-commit
1054 1056
1055 1057 ``added``
1056 1058 list of ``FileNode`` objects marked as *added*
1057 1059
1058 1060 ``changed``
1059 1061 list of ``FileNode`` objects marked as *changed*
1060 1062
1061 1063 ``removed``
1062 1064 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1063 1065 *removed*
1064 1066
1065 1067 ``parents``
1066 1068 list of :class:`BaseCommit` instances representing parents of
1067 1069 in-memory commit. Should always be 2-element sequence.
1068 1070
1069 1071 """
1070 1072
1071 1073 def __init__(self, repository):
1072 1074 self.repository = repository
1073 1075 self.added = []
1074 1076 self.changed = []
1075 1077 self.removed = []
1076 1078 self.parents = []
1077 1079
1078 1080 def add(self, *filenodes):
1079 1081 """
1080 1082 Marks given ``FileNode`` objects as *to be committed*.
1081 1083
1082 1084 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1083 1085 latest commit
1084 1086 :raises ``NodeAlreadyAddedError``: if node with same path is already
1085 1087 marked as *added*
1086 1088 """
1087 1089 # Check if not already marked as *added* first
1088 1090 for node in filenodes:
1089 1091 if node.path in (n.path for n in self.added):
1090 1092 raise NodeAlreadyAddedError(
1091 1093 "Such FileNode %s is already marked for addition"
1092 1094 % node.path)
1093 1095 for node in filenodes:
1094 1096 self.added.append(node)
1095 1097
1096 1098 def change(self, *filenodes):
1097 1099 """
1098 1100 Marks given ``FileNode`` objects to be *changed* in next commit.
1099 1101
1100 1102 :raises ``EmptyRepositoryError``: if there are no commits yet
1101 1103 :raises ``NodeAlreadyExistsError``: if node with same path is already
1102 1104 marked to be *changed*
1103 1105 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1104 1106 marked to be *removed*
1105 1107 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1106 1108 commit
1107 1109 :raises ``NodeNotChangedError``: if node hasn't really be changed
1108 1110 """
1109 1111 for node in filenodes:
1110 1112 if node.path in (n.path for n in self.removed):
1111 1113 raise NodeAlreadyRemovedError(
1112 1114 "Node at %s is already marked as removed" % node.path)
1113 1115 try:
1114 1116 self.repository.get_commit()
1115 1117 except EmptyRepositoryError:
1116 1118 raise EmptyRepositoryError(
1117 1119 "Nothing to change - try to *add* new nodes rather than "
1118 1120 "changing them")
1119 1121 for node in filenodes:
1120 1122 if node.path in (n.path for n in self.changed):
1121 1123 raise NodeAlreadyChangedError(
1122 1124 "Node at '%s' is already marked as changed" % node.path)
1123 1125 self.changed.append(node)
1124 1126
1125 1127 def remove(self, *filenodes):
1126 1128 """
1127 1129 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1128 1130 *removed* in next commit.
1129 1131
1130 1132 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1131 1133 be *removed*
1132 1134 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1133 1135 be *changed*
1134 1136 """
1135 1137 for node in filenodes:
1136 1138 if node.path in (n.path for n in self.removed):
1137 1139 raise NodeAlreadyRemovedError(
1138 1140 "Node is already marked to for removal at %s" % node.path)
1139 1141 if node.path in (n.path for n in self.changed):
1140 1142 raise NodeAlreadyChangedError(
1141 1143 "Node is already marked to be changed at %s" % node.path)
1142 1144 # We only mark node as *removed* - real removal is done by
1143 1145 # commit method
1144 1146 self.removed.append(node)
1145 1147
1146 1148 def reset(self):
1147 1149 """
1148 1150 Resets this instance to initial state (cleans ``added``, ``changed``
1149 1151 and ``removed`` lists).
1150 1152 """
1151 1153 self.added = []
1152 1154 self.changed = []
1153 1155 self.removed = []
1154 1156 self.parents = []
1155 1157
1156 1158 def get_ipaths(self):
1157 1159 """
1158 1160 Returns generator of paths from nodes marked as added, changed or
1159 1161 removed.
1160 1162 """
1161 1163 for node in itertools.chain(self.added, self.changed, self.removed):
1162 1164 yield node.path
1163 1165
1164 1166 def get_paths(self):
1165 1167 """
1166 1168 Returns list of paths from nodes marked as added, changed or removed.
1167 1169 """
1168 1170 return list(self.get_ipaths())
1169 1171
1170 1172 def check_integrity(self, parents=None):
1171 1173 """
1172 1174 Checks in-memory commit's integrity. Also, sets parents if not
1173 1175 already set.
1174 1176
1175 1177 :raises CommitError: if any error occurs (i.e.
1176 1178 ``NodeDoesNotExistError``).
1177 1179 """
1178 1180 if not self.parents:
1179 1181 parents = parents or []
1180 1182 if len(parents) == 0:
1181 1183 try:
1182 1184 parents = [self.repository.get_commit(), None]
1183 1185 except EmptyRepositoryError:
1184 1186 parents = [None, None]
1185 1187 elif len(parents) == 1:
1186 1188 parents += [None]
1187 1189 self.parents = parents
1188 1190
1189 1191 # Local parents, only if not None
1190 1192 parents = [p for p in self.parents if p]
1191 1193
1192 1194 # Check nodes marked as added
1193 1195 for p in parents:
1194 1196 for node in self.added:
1195 1197 try:
1196 1198 p.get_node(node.path)
1197 1199 except NodeDoesNotExistError:
1198 1200 pass
1199 1201 else:
1200 1202 raise NodeAlreadyExistsError(
1201 1203 "Node `%s` already exists at %s" % (node.path, p))
1202 1204
1203 1205 # Check nodes marked as changed
1204 1206 missing = set(self.changed)
1205 1207 not_changed = set(self.changed)
1206 1208 if self.changed and not parents:
1207 1209 raise NodeDoesNotExistError(str(self.changed[0].path))
1208 1210 for p in parents:
1209 1211 for node in self.changed:
1210 1212 try:
1211 1213 old = p.get_node(node.path)
1212 1214 missing.remove(node)
1213 1215 # if content actually changed, remove node from not_changed
1214 1216 if old.content != node.content:
1215 1217 not_changed.remove(node)
1216 1218 except NodeDoesNotExistError:
1217 1219 pass
1218 1220 if self.changed and missing:
1219 1221 raise NodeDoesNotExistError(
1220 1222 "Node `%s` marked as modified but missing in parents: %s"
1221 1223 % (node.path, parents))
1222 1224
1223 1225 if self.changed and not_changed:
1224 1226 raise NodeNotChangedError(
1225 1227 "Node `%s` wasn't actually changed (parents: %s)"
1226 1228 % (not_changed.pop().path, parents))
1227 1229
1228 1230 # Check nodes marked as removed
1229 1231 if self.removed and not parents:
1230 1232 raise NodeDoesNotExistError(
1231 1233 "Cannot remove node at %s as there "
1232 1234 "were no parents specified" % self.removed[0].path)
1233 1235 really_removed = set()
1234 1236 for p in parents:
1235 1237 for node in self.removed:
1236 1238 try:
1237 1239 p.get_node(node.path)
1238 1240 really_removed.add(node)
1239 1241 except CommitError:
1240 1242 pass
1241 1243 not_removed = set(self.removed) - really_removed
1242 1244 if not_removed:
1243 1245 # TODO: johbo: This code branch does not seem to be covered
1244 1246 raise NodeDoesNotExistError(
1245 1247 "Cannot remove node at %s from "
1246 1248 "following parents: %s" % (not_removed, parents))
1247 1249
1248 1250 def commit(
1249 1251 self, message, author, parents=None, branch=None, date=None,
1250 1252 **kwargs):
1251 1253 """
1252 1254 Performs in-memory commit (doesn't check workdir in any way) and
1253 1255 returns newly created :class:`BaseCommit`. Updates repository's
1254 1256 attribute `commits`.
1255 1257
1256 1258 .. note::
1257 1259
1258 1260 While overriding this method each backend's should call
1259 1261 ``self.check_integrity(parents)`` in the first place.
1260 1262
1261 1263 :param message: message of the commit
1262 1264 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1263 1265 :param parents: single parent or sequence of parents from which commit
1264 1266 would be derived
1265 1267 :param date: ``datetime.datetime`` instance. Defaults to
1266 1268 ``datetime.datetime.now()``.
1267 1269 :param branch: branch name, as string. If none given, default backend's
1268 1270 branch would be used.
1269 1271
1270 1272 :raises ``CommitError``: if any error occurs while committing
1271 1273 """
1272 1274 raise NotImplementedError
1273 1275
1274 1276
1275 1277 class BaseInMemoryChangesetClass(type):
1276 1278
1277 1279 def __instancecheck__(self, instance):
1278 1280 return isinstance(instance, BaseInMemoryCommit)
1279 1281
1280 1282
1281 1283 class BaseInMemoryChangeset(BaseInMemoryCommit):
1282 1284
1283 1285 __metaclass__ = BaseInMemoryChangesetClass
1284 1286
1285 1287 def __new__(cls, *args, **kwargs):
1286 1288 warnings.warn(
1287 1289 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1288 1290 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1289 1291
1290 1292
1291 1293 class EmptyCommit(BaseCommit):
1292 1294 """
1293 1295 An dummy empty commit. It's possible to pass hash when creating
1294 1296 an EmptyCommit
1295 1297 """
1296 1298
1297 1299 def __init__(
1298 1300 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1299 1301 message='', author='', date=None):
1300 1302 self._empty_commit_id = commit_id
1301 1303 # TODO: johbo: Solve idx parameter, default value does not make
1302 1304 # too much sense
1303 1305 self.idx = idx
1304 1306 self.message = message
1305 1307 self.author = author
1306 1308 self.date = date or datetime.datetime.fromtimestamp(0)
1307 1309 self.repository = repo
1308 1310 self.alias = alias
1309 1311
1310 1312 @LazyProperty
1311 1313 def raw_id(self):
1312 1314 """
1313 1315 Returns raw string identifying this commit, useful for web
1314 1316 representation.
1315 1317 """
1316 1318
1317 1319 return self._empty_commit_id
1318 1320
1319 1321 @LazyProperty
1320 1322 def branch(self):
1321 1323 if self.alias:
1322 1324 from rhodecode.lib.vcs.backends import get_backend
1323 1325 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1324 1326
1325 1327 @LazyProperty
1326 1328 def short_id(self):
1327 1329 return self.raw_id[:12]
1328 1330
1329 1331 def get_file_commit(self, path):
1330 1332 return self
1331 1333
1332 1334 def get_file_content(self, path):
1333 1335 return u''
1334 1336
1335 1337 def get_file_size(self, path):
1336 1338 return 0
1337 1339
1338 1340
1339 1341 class EmptyChangesetClass(type):
1340 1342
1341 1343 def __instancecheck__(self, instance):
1342 1344 return isinstance(instance, EmptyCommit)
1343 1345
1344 1346
1345 1347 class EmptyChangeset(EmptyCommit):
1346 1348
1347 1349 __metaclass__ = EmptyChangesetClass
1348 1350
1349 1351 def __new__(cls, *args, **kwargs):
1350 1352 warnings.warn(
1351 1353 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1352 1354 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1353 1355
1354 1356 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1355 1357 alias=None, revision=-1, message='', author='', date=None):
1356 1358 if requested_revision is not None:
1357 1359 warnings.warn(
1358 1360 "Parameter requested_revision not supported anymore",
1359 1361 DeprecationWarning)
1360 1362 super(EmptyChangeset, self).__init__(
1361 1363 commit_id=cs, repo=repo, alias=alias, idx=revision,
1362 1364 message=message, author=author, date=date)
1363 1365
1364 1366 @property
1365 1367 def revision(self):
1366 1368 warnings.warn("Use idx instead", DeprecationWarning)
1367 1369 return self.idx
1368 1370
1369 1371 @revision.setter
1370 1372 def revision(self, value):
1371 1373 warnings.warn("Use idx instead", DeprecationWarning)
1372 1374 self.idx = value
1373 1375
1374 1376
1375 1377 class CollectionGenerator(object):
1376 1378
1377 1379 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1378 1380 self.repo = repo
1379 1381 self.commit_ids = commit_ids
1380 1382 # TODO: (oliver) this isn't currently hooked up
1381 1383 self.collection_size = None
1382 1384 self.pre_load = pre_load
1383 1385
1384 1386 def __len__(self):
1385 1387 if self.collection_size is not None:
1386 1388 return self.collection_size
1387 1389 return self.commit_ids.__len__()
1388 1390
1389 1391 def __iter__(self):
1390 1392 for commit_id in self.commit_ids:
1391 1393 # TODO: johbo: Mercurial passes in commit indices or commit ids
1392 1394 yield self._commit_factory(commit_id)
1393 1395
1394 1396 def _commit_factory(self, commit_id):
1395 1397 """
1396 1398 Allows backends to override the way commits are generated.
1397 1399 """
1398 1400 return self.repo.get_commit(commit_id=commit_id,
1399 1401 pre_load=self.pre_load)
1400 1402
1401 1403 def __getslice__(self, i, j):
1402 1404 """
1403 1405 Returns an iterator of sliced repository
1404 1406 """
1405 1407 commit_ids = self.commit_ids[i:j]
1406 1408 return self.__class__(
1407 1409 self.repo, commit_ids, pre_load=self.pre_load)
1408 1410
1409 1411 def __repr__(self):
1410 1412 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1411 1413
1412 1414
1413 1415 class Config(object):
1414 1416 """
1415 1417 Represents the configuration for a repository.
1416 1418
1417 1419 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1418 1420 standard library. It implements only the needed subset.
1419 1421 """
1420 1422
1421 1423 def __init__(self):
1422 1424 self._values = {}
1423 1425
1424 1426 def copy(self):
1425 1427 clone = Config()
1426 1428 for section, values in self._values.items():
1427 1429 clone._values[section] = values.copy()
1428 1430 return clone
1429 1431
1430 1432 def __repr__(self):
1431 1433 return '<Config(%s values) at %s>' % (len(self._values), hex(id(self)))
1432 1434
1433 1435 def items(self, section):
1434 1436 return self._values.get(section, {}).iteritems()
1435 1437
1436 1438 def get(self, section, option):
1437 1439 return self._values.get(section, {}).get(option)
1438 1440
1439 1441 def set(self, section, option, value):
1440 1442 section_values = self._values.setdefault(section, {})
1441 1443 section_values[option] = value
1442 1444
1443 1445 def clear_section(self, section):
1444 1446 self._values[section] = {}
1445 1447
1446 1448 def serialize(self):
1447 1449 """
1448 1450 Creates a list of three tuples (section, key, value) representing
1449 1451 this config object.
1450 1452 """
1451 1453 items = []
1452 1454 for section in self._values:
1453 1455 for option, value in self._values[section].items():
1454 1456 items.append(
1455 1457 (safe_str(section), safe_str(option), safe_str(value)))
1456 1458 return items
1457 1459
1458 1460
1459 1461 class Diff(object):
1460 1462 """
1461 1463 Represents a diff result from a repository backend.
1462 1464
1463 1465 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1464 1466 """
1465 1467
1466 1468 _header_re = None
1467 1469
1468 1470 def __init__(self, raw_diff):
1469 1471 self.raw = raw_diff
1470 1472
1471 1473 def chunks(self):
1472 1474 """
1473 1475 split the diff in chunks of separate --git a/file b/file chunks
1474 1476 to make diffs consistent we must prepend with \n, and make sure
1475 1477 we can detect last chunk as this was also has special rule
1476 1478 """
1477 1479 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1478 1480 total_chunks = len(chunks)
1479 1481 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1480 1482 for cur_chunk, chunk in enumerate(chunks, start=1))
1481 1483
1482 1484
1483 1485 class DiffChunk(object):
1484 1486
1485 1487 def __init__(self, chunk, diff, last_chunk):
1486 1488 self._diff = diff
1487 1489
1488 1490 # since we split by \ndiff --git that part is lost from original diff
1489 1491 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1490 1492 if not last_chunk:
1491 1493 chunk += '\n'
1492 1494
1493 1495 match = self._diff._header_re.match(chunk)
1494 1496 self.header = match.groupdict()
1495 1497 self.diff = chunk[match.end():]
1496 1498 self.raw = chunk
General Comments 0
You need to be logged in to leave comments. Login now