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