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