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