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