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