##// END OF EJS Templates
commits: updated logic of in-memory-commits, fixed tests and re-architectured a bit how commit_ids are calculated and updated....
marcink -
r3743:b018c011 new-ui
parent child Browse files
Show More
@@ -1,1850 +1,1860 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 import os
25 25 import re
26 26 import time
27 27 import shutil
28 28 import datetime
29 29 import fnmatch
30 30 import itertools
31 31 import logging
32 32 import collections
33 33 import warnings
34 34
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 from zope.cachedescriptors.property import CachedProperty
37
36 38 from pyramid import compat
37 39
38 40 from rhodecode.translation import lazy_ugettext
39 41 from rhodecode.lib.utils2 import safe_str, safe_unicode
40 42 from rhodecode.lib.vcs import connection
41 43 from rhodecode.lib.vcs.utils import author_name, author_email
42 44 from rhodecode.lib.vcs.conf import settings
43 45 from rhodecode.lib.vcs.exceptions import (
44 46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
45 47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
46 48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
47 49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
48 50 RepositoryError)
49 51
50 52
51 53 log = logging.getLogger(__name__)
52 54
53 55
54 56 FILEMODE_DEFAULT = 0o100644
55 57 FILEMODE_EXECUTABLE = 0o100755
56 58
57 59 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
58 60
59 61
60 62 class MergeFailureReason(object):
61 63 """
62 64 Enumeration with all the reasons why the server side merge could fail.
63 65
64 66 DO NOT change the number of the reasons, as they may be stored in the
65 67 database.
66 68
67 69 Changing the name of a reason is acceptable and encouraged to deprecate old
68 70 reasons.
69 71 """
70 72
71 73 # Everything went well.
72 74 NONE = 0
73 75
74 76 # An unexpected exception was raised. Check the logs for more details.
75 77 UNKNOWN = 1
76 78
77 79 # The merge was not successful, there are conflicts.
78 80 MERGE_FAILED = 2
79 81
80 82 # The merge succeeded but we could not push it to the target repository.
81 83 PUSH_FAILED = 3
82 84
83 85 # The specified target is not a head in the target repository.
84 86 TARGET_IS_NOT_HEAD = 4
85 87
86 88 # The source repository contains more branches than the target. Pushing
87 89 # the merge will create additional branches in the target.
88 90 HG_SOURCE_HAS_MORE_BRANCHES = 5
89 91
90 92 # The target reference has multiple heads. That does not allow to correctly
91 93 # identify the target location. This could only happen for mercurial
92 94 # branches.
93 95 HG_TARGET_HAS_MULTIPLE_HEADS = 6
94 96
95 97 # The target repository is locked
96 98 TARGET_IS_LOCKED = 7
97 99
98 100 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
99 101 # A involved commit could not be found.
100 102 _DEPRECATED_MISSING_COMMIT = 8
101 103
102 104 # The target repo reference is missing.
103 105 MISSING_TARGET_REF = 9
104 106
105 107 # The source repo reference is missing.
106 108 MISSING_SOURCE_REF = 10
107 109
108 110 # The merge was not successful, there are conflicts related to sub
109 111 # repositories.
110 112 SUBREPO_MERGE_FAILED = 11
111 113
112 114
113 115 class UpdateFailureReason(object):
114 116 """
115 117 Enumeration with all the reasons why the pull request update could fail.
116 118
117 119 DO NOT change the number of the reasons, as they may be stored in the
118 120 database.
119 121
120 122 Changing the name of a reason is acceptable and encouraged to deprecate old
121 123 reasons.
122 124 """
123 125
124 126 # Everything went well.
125 127 NONE = 0
126 128
127 129 # An unexpected exception was raised. Check the logs for more details.
128 130 UNKNOWN = 1
129 131
130 132 # The pull request is up to date.
131 133 NO_CHANGE = 2
132 134
133 135 # The pull request has a reference type that is not supported for update.
134 136 WRONG_REF_TYPE = 3
135 137
136 138 # Update failed because the target reference is missing.
137 139 MISSING_TARGET_REF = 4
138 140
139 141 # Update failed because the source reference is missing.
140 142 MISSING_SOURCE_REF = 5
141 143
142 144
143 145 class MergeResponse(object):
144 146
145 147 # uses .format(**metadata) for variables
146 148 MERGE_STATUS_MESSAGES = {
147 149 MergeFailureReason.NONE: lazy_ugettext(
148 150 u'This pull request can be automatically merged.'),
149 151 MergeFailureReason.UNKNOWN: lazy_ugettext(
150 152 u'This pull request cannot be merged because of an unhandled exception. '
151 153 u'{exception}'),
152 154 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
153 155 u'This pull request cannot be merged because of merge conflicts.'),
154 156 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
155 157 u'This pull request could not be merged because push to '
156 158 u'target:`{target}@{merge_commit}` failed.'),
157 159 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
158 160 u'This pull request cannot be merged because the target '
159 161 u'`{target_ref.name}` is not a head.'),
160 162 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
161 163 u'This pull request cannot be merged because the source contains '
162 164 u'more branches than the target.'),
163 165 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
164 166 u'This pull request cannot be merged because the target `{target_ref.name}` '
165 167 u'has multiple heads: `{heads}`.'),
166 168 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
167 169 u'This pull request cannot be merged because the target repository is '
168 170 u'locked by {locked_by}.'),
169 171
170 172 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
171 173 u'This pull request cannot be merged because the target '
172 174 u'reference `{target_ref.name}` is missing.'),
173 175 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
174 176 u'This pull request cannot be merged because the source '
175 177 u'reference `{source_ref.name}` is missing.'),
176 178 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
177 179 u'This pull request cannot be merged because of conflicts related '
178 180 u'to sub repositories.'),
179 181
180 182 # Deprecations
181 183 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
182 184 u'This pull request cannot be merged because the target or the '
183 185 u'source reference is missing.'),
184 186
185 187 }
186 188
187 189 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
188 190 self.possible = possible
189 191 self.executed = executed
190 192 self.merge_ref = merge_ref
191 193 self.failure_reason = failure_reason
192 194 self.metadata = metadata or {}
193 195
194 196 def __repr__(self):
195 197 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
196 198
197 199 def __eq__(self, other):
198 200 same_instance = isinstance(other, self.__class__)
199 201 return same_instance \
200 202 and self.possible == other.possible \
201 203 and self.executed == other.executed \
202 204 and self.failure_reason == other.failure_reason
203 205
204 206 @property
205 207 def label(self):
206 208 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
207 209 not k.startswith('_'))
208 210 return label_dict.get(self.failure_reason)
209 211
210 212 @property
211 213 def merge_status_message(self):
212 214 """
213 215 Return a human friendly error message for the given merge status code.
214 216 """
215 217 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
216 218 try:
217 219 return msg.format(**self.metadata)
218 220 except Exception:
219 221 log.exception('Failed to format %s message', self)
220 222 return msg
221 223
222 224 def asdict(self):
223 225 data = {}
224 226 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
225 227 'merge_status_message']:
226 228 data[k] = getattr(self, k)
227 229 return data
228 230
229 231
230 232 class BaseRepository(object):
231 233 """
232 234 Base Repository for final backends
233 235
234 236 .. attribute:: DEFAULT_BRANCH_NAME
235 237
236 238 name of default branch (i.e. "trunk" for svn, "master" for git etc.
237 239
238 240 .. attribute:: commit_ids
239 241
240 242 list of all available commit ids, in ascending order
241 243
242 244 .. attribute:: path
243 245
244 246 absolute path to the repository
245 247
246 248 .. attribute:: bookmarks
247 249
248 250 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
249 251 there are no bookmarks or the backend implementation does not support
250 252 bookmarks.
251 253
252 254 .. attribute:: tags
253 255
254 256 Mapping from name to :term:`Commit ID` of the tag.
255 257
256 258 """
257 259
258 260 DEFAULT_BRANCH_NAME = None
259 261 DEFAULT_CONTACT = u"Unknown"
260 262 DEFAULT_DESCRIPTION = u"unknown"
261 263 EMPTY_COMMIT_ID = '0' * 40
262 264
263 265 path = None
266 _commit_ids_ver = 0
264 267
265 268 def __init__(self, repo_path, config=None, create=False, **kwargs):
266 269 """
267 270 Initializes repository. Raises RepositoryError if repository could
268 271 not be find at the given ``repo_path`` or directory at ``repo_path``
269 272 exists and ``create`` is set to True.
270 273
271 274 :param repo_path: local path of the repository
272 275 :param config: repository configuration
273 276 :param create=False: if set to True, would try to create repository.
274 277 :param src_url=None: if set, should be proper url from which repository
275 278 would be cloned; requires ``create`` parameter to be set to True -
276 279 raises RepositoryError if src_url is set and create evaluates to
277 280 False
278 281 """
279 282 raise NotImplementedError
280 283
281 284 def __repr__(self):
282 285 return '<%s at %s>' % (self.__class__.__name__, self.path)
283 286
284 287 def __len__(self):
285 288 return self.count()
286 289
287 290 def __eq__(self, other):
288 291 same_instance = isinstance(other, self.__class__)
289 292 return same_instance and other.path == self.path
290 293
291 294 def __ne__(self, other):
292 295 return not self.__eq__(other)
293 296
294 297 def get_create_shadow_cache_pr_path(self, db_repo):
295 298 path = db_repo.cached_diffs_dir
296 299 if not os.path.exists(path):
297 300 os.makedirs(path, 0o755)
298 301 return path
299 302
300 303 @classmethod
301 304 def get_default_config(cls, default=None):
302 305 config = Config()
303 306 if default and isinstance(default, list):
304 307 for section, key, val in default:
305 308 config.set(section, key, val)
306 309 return config
307 310
308 311 @LazyProperty
309 312 def _remote(self):
310 313 raise NotImplementedError
311 314
312 315 def _heads(self, branch=None):
313 316 return []
314 317
315 318 @LazyProperty
316 319 def EMPTY_COMMIT(self):
317 320 return EmptyCommit(self.EMPTY_COMMIT_ID)
318 321
319 322 @LazyProperty
320 323 def alias(self):
321 324 for k, v in settings.BACKENDS.items():
322 325 if v.split('.')[-1] == str(self.__class__.__name__):
323 326 return k
324 327
325 328 @LazyProperty
326 329 def name(self):
327 330 return safe_unicode(os.path.basename(self.path))
328 331
329 332 @LazyProperty
330 333 def description(self):
331 334 raise NotImplementedError
332 335
333 336 def refs(self):
334 337 """
335 338 returns a `dict` with branches, bookmarks, tags, and closed_branches
336 339 for this repository
337 340 """
338 341 return dict(
339 342 branches=self.branches,
340 343 branches_closed=self.branches_closed,
341 344 tags=self.tags,
342 345 bookmarks=self.bookmarks
343 346 )
344 347
345 348 @LazyProperty
346 349 def branches(self):
347 350 """
348 351 A `dict` which maps branch names to commit ids.
349 352 """
350 353 raise NotImplementedError
351 354
352 355 @LazyProperty
353 356 def branches_closed(self):
354 357 """
355 358 A `dict` which maps tags names to commit ids.
356 359 """
357 360 raise NotImplementedError
358 361
359 362 @LazyProperty
360 363 def bookmarks(self):
361 364 """
362 365 A `dict` which maps tags names to commit ids.
363 366 """
364 367 raise NotImplementedError
365 368
366 369 @LazyProperty
367 370 def tags(self):
368 371 """
369 372 A `dict` which maps tags names to commit ids.
370 373 """
371 374 raise NotImplementedError
372 375
373 376 @LazyProperty
374 377 def size(self):
375 378 """
376 379 Returns combined size in bytes for all repository files
377 380 """
378 381 tip = self.get_commit()
379 382 return tip.size
380 383
381 384 def size_at_commit(self, commit_id):
382 385 commit = self.get_commit(commit_id)
383 386 return commit.size
384 387
385 388 def is_empty(self):
386 389 return self._remote.is_empty()
387 390
388 391 @staticmethod
389 392 def check_url(url, config):
390 393 """
391 394 Function will check given url and try to verify if it's a valid
392 395 link.
393 396 """
394 397 raise NotImplementedError
395 398
396 399 @staticmethod
397 400 def is_valid_repository(path):
398 401 """
399 402 Check if given `path` contains a valid repository of this backend
400 403 """
401 404 raise NotImplementedError
402 405
403 406 # ==========================================================================
404 407 # COMMITS
405 408 # ==========================================================================
406 409
410 @CachedProperty('_commit_ids_ver')
411 def commit_ids(self):
412 raise NotImplementedError
413
414 def append_commit_id(self, commit_id):
415 if commit_id not in self.commit_ids:
416 self._rebuild_cache(self.commit_ids + [commit_id])
417 self._commit_ids_ver = time.time()
418
407 419 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
408 420 """
409 421 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
410 422 are both None, most recent commit is returned.
411 423
412 424 :param pre_load: Optional. List of commit attributes to load.
413 425
414 426 :raises ``EmptyRepositoryError``: if there are no commits
415 427 """
416 428 raise NotImplementedError
417 429
418 430 def __iter__(self):
419 431 for commit_id in self.commit_ids:
420 432 yield self.get_commit(commit_id=commit_id)
421 433
422 434 def get_commits(
423 435 self, start_id=None, end_id=None, start_date=None, end_date=None,
424 436 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
425 437 """
426 438 Returns iterator of `BaseCommit` objects from start to end
427 439 not inclusive. This should behave just like a list, ie. end is not
428 440 inclusive.
429 441
430 442 :param start_id: None or str, must be a valid commit id
431 443 :param end_id: None or str, must be a valid commit id
432 444 :param start_date:
433 445 :param end_date:
434 446 :param branch_name:
435 447 :param show_hidden:
436 448 :param pre_load:
437 449 :param translate_tags:
438 450 """
439 451 raise NotImplementedError
440 452
441 453 def __getitem__(self, key):
442 454 """
443 455 Allows index based access to the commit objects of this repository.
444 456 """
445 457 pre_load = ["author", "branch", "date", "message", "parents"]
446 458 if isinstance(key, slice):
447 459 return self._get_range(key, pre_load)
448 460 return self.get_commit(commit_idx=key, pre_load=pre_load)
449 461
450 462 def _get_range(self, slice_obj, pre_load):
451 463 for commit_id in self.commit_ids.__getitem__(slice_obj):
452 464 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
453 465
454 466 def count(self):
455 467 return len(self.commit_ids)
456 468
457 469 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
458 470 """
459 471 Creates and returns a tag for the given ``commit_id``.
460 472
461 473 :param name: name for new tag
462 474 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
463 475 :param commit_id: commit id for which new tag would be created
464 476 :param message: message of the tag's commit
465 477 :param date: date of tag's commit
466 478
467 479 :raises TagAlreadyExistError: if tag with same name already exists
468 480 """
469 481 raise NotImplementedError
470 482
471 483 def remove_tag(self, name, user, message=None, date=None):
472 484 """
473 485 Removes tag with the given ``name``.
474 486
475 487 :param name: name of the tag to be removed
476 488 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
477 489 :param message: message of the tag's removal commit
478 490 :param date: date of tag's removal commit
479 491
480 492 :raises TagDoesNotExistError: if tag with given name does not exists
481 493 """
482 494 raise NotImplementedError
483 495
484 496 def get_diff(
485 497 self, commit1, commit2, path=None, ignore_whitespace=False,
486 498 context=3, path1=None):
487 499 """
488 500 Returns (git like) *diff*, as plain text. Shows changes introduced by
489 501 `commit2` since `commit1`.
490 502
491 503 :param commit1: Entry point from which diff is shown. Can be
492 504 ``self.EMPTY_COMMIT`` - in this case, patch showing all
493 505 the changes since empty state of the repository until `commit2`
494 506 :param commit2: Until which commit changes should be shown.
495 507 :param path: Can be set to a path of a file to create a diff of that
496 508 file. If `path1` is also set, this value is only associated to
497 509 `commit2`.
498 510 :param ignore_whitespace: If set to ``True``, would not show whitespace
499 511 changes. Defaults to ``False``.
500 512 :param context: How many lines before/after changed lines should be
501 513 shown. Defaults to ``3``.
502 514 :param path1: Can be set to a path to associate with `commit1`. This
503 515 parameter works only for backends which support diff generation for
504 516 different paths. Other backends will raise a `ValueError` if `path1`
505 517 is set and has a different value than `path`.
506 518 :param file_path: filter this diff by given path pattern
507 519 """
508 520 raise NotImplementedError
509 521
510 522 def strip(self, commit_id, branch=None):
511 523 """
512 524 Strip given commit_id from the repository
513 525 """
514 526 raise NotImplementedError
515 527
516 528 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
517 529 """
518 530 Return a latest common ancestor commit if one exists for this repo
519 531 `commit_id1` vs `commit_id2` from `repo2`.
520 532
521 533 :param commit_id1: Commit it from this repository to use as a
522 534 target for the comparison.
523 535 :param commit_id2: Source commit id to use for comparison.
524 536 :param repo2: Source repository to use for comparison.
525 537 """
526 538 raise NotImplementedError
527 539
528 540 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
529 541 """
530 542 Compare this repository's revision `commit_id1` with `commit_id2`.
531 543
532 544 Returns a tuple(commits, ancestor) that would be merged from
533 545 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
534 546 will be returned as ancestor.
535 547
536 548 :param commit_id1: Commit it from this repository to use as a
537 549 target for the comparison.
538 550 :param commit_id2: Source commit id to use for comparison.
539 551 :param repo2: Source repository to use for comparison.
540 552 :param merge: If set to ``True`` will do a merge compare which also
541 553 returns the common ancestor.
542 554 :param pre_load: Optional. List of commit attributes to load.
543 555 """
544 556 raise NotImplementedError
545 557
546 558 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
547 559 user_name='', user_email='', message='', dry_run=False,
548 560 use_rebase=False, close_branch=False):
549 561 """
550 562 Merge the revisions specified in `source_ref` from `source_repo`
551 563 onto the `target_ref` of this repository.
552 564
553 565 `source_ref` and `target_ref` are named tupls with the following
554 566 fields `type`, `name` and `commit_id`.
555 567
556 568 Returns a MergeResponse named tuple with the following fields
557 569 'possible', 'executed', 'source_commit', 'target_commit',
558 570 'merge_commit'.
559 571
560 572 :param repo_id: `repo_id` target repo id.
561 573 :param workspace_id: `workspace_id` unique identifier.
562 574 :param target_ref: `target_ref` points to the commit on top of which
563 575 the `source_ref` should be merged.
564 576 :param source_repo: The repository that contains the commits to be
565 577 merged.
566 578 :param source_ref: `source_ref` points to the topmost commit from
567 579 the `source_repo` which should be merged.
568 580 :param user_name: Merge commit `user_name`.
569 581 :param user_email: Merge commit `user_email`.
570 582 :param message: Merge commit `message`.
571 583 :param dry_run: If `True` the merge will not take place.
572 584 :param use_rebase: If `True` commits from the source will be rebased
573 585 on top of the target instead of being merged.
574 586 :param close_branch: If `True` branch will be close before merging it
575 587 """
576 588 if dry_run:
577 589 message = message or settings.MERGE_DRY_RUN_MESSAGE
578 590 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
579 591 user_name = user_name or settings.MERGE_DRY_RUN_USER
580 592 else:
581 593 if not user_name:
582 594 raise ValueError('user_name cannot be empty')
583 595 if not user_email:
584 596 raise ValueError('user_email cannot be empty')
585 597 if not message:
586 598 raise ValueError('message cannot be empty')
587 599
588 600 try:
589 601 return self._merge_repo(
590 602 repo_id, workspace_id, target_ref, source_repo,
591 603 source_ref, message, user_name, user_email, dry_run=dry_run,
592 604 use_rebase=use_rebase, close_branch=close_branch)
593 605 except RepositoryError as exc:
594 606 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
595 607 return MergeResponse(
596 608 False, False, None, MergeFailureReason.UNKNOWN,
597 609 metadata={'exception': str(exc)})
598 610
599 611 def _merge_repo(self, repo_id, workspace_id, target_ref,
600 612 source_repo, source_ref, merge_message,
601 613 merger_name, merger_email, dry_run=False,
602 614 use_rebase=False, close_branch=False):
603 615 """Internal implementation of merge."""
604 616 raise NotImplementedError
605 617
606 618 def _maybe_prepare_merge_workspace(
607 619 self, repo_id, workspace_id, target_ref, source_ref):
608 620 """
609 621 Create the merge workspace.
610 622
611 623 :param workspace_id: `workspace_id` unique identifier.
612 624 """
613 625 raise NotImplementedError
614 626
615 627 def _get_legacy_shadow_repository_path(self, workspace_id):
616 628 """
617 629 Legacy version that was used before. We still need it for
618 630 backward compat
619 631 """
620 632 return os.path.join(
621 633 os.path.dirname(self.path),
622 634 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
623 635
624 636 def _get_shadow_repository_path(self, repo_id, workspace_id):
625 637 # The name of the shadow repository must start with '.', so it is
626 638 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
627 639 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
628 640 if os.path.exists(legacy_repository_path):
629 641 return legacy_repository_path
630 642 else:
631 643 return os.path.join(
632 644 os.path.dirname(self.path),
633 645 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
634 646
635 647 def cleanup_merge_workspace(self, repo_id, workspace_id):
636 648 """
637 649 Remove merge workspace.
638 650
639 651 This function MUST not fail in case there is no workspace associated to
640 652 the given `workspace_id`.
641 653
642 654 :param workspace_id: `workspace_id` unique identifier.
643 655 """
644 656 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
645 657 shadow_repository_path_del = '{}.{}.delete'.format(
646 658 shadow_repository_path, time.time())
647 659
648 660 # move the shadow repo, so it never conflicts with the one used.
649 661 # we use this method because shutil.rmtree had some edge case problems
650 662 # removing symlinked repositories
651 663 if not os.path.isdir(shadow_repository_path):
652 664 return
653 665
654 666 shutil.move(shadow_repository_path, shadow_repository_path_del)
655 667 try:
656 668 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
657 669 except Exception:
658 670 log.exception('Failed to gracefully remove shadow repo under %s',
659 671 shadow_repository_path_del)
660 672 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
661 673
662 674 # ========== #
663 675 # COMMIT API #
664 676 # ========== #
665 677
666 678 @LazyProperty
667 679 def in_memory_commit(self):
668 680 """
669 681 Returns :class:`InMemoryCommit` object for this repository.
670 682 """
671 683 raise NotImplementedError
672 684
673 685 # ======================== #
674 686 # UTILITIES FOR SUBCLASSES #
675 687 # ======================== #
676 688
677 689 def _validate_diff_commits(self, commit1, commit2):
678 690 """
679 691 Validates that the given commits are related to this repository.
680 692
681 693 Intended as a utility for sub classes to have a consistent validation
682 694 of input parameters in methods like :meth:`get_diff`.
683 695 """
684 696 self._validate_commit(commit1)
685 697 self._validate_commit(commit2)
686 698 if (isinstance(commit1, EmptyCommit) and
687 699 isinstance(commit2, EmptyCommit)):
688 700 raise ValueError("Cannot compare two empty commits")
689 701
690 702 def _validate_commit(self, commit):
691 703 if not isinstance(commit, BaseCommit):
692 704 raise TypeError(
693 705 "%s is not of type BaseCommit" % repr(commit))
694 706 if commit.repository != self and not isinstance(commit, EmptyCommit):
695 707 raise ValueError(
696 708 "Commit %s must be a valid commit from this repository %s, "
697 709 "related to this repository instead %s." %
698 710 (commit, self, commit.repository))
699 711
700 712 def _validate_commit_id(self, commit_id):
701 713 if not isinstance(commit_id, compat.string_types):
702 714 raise TypeError("commit_id must be a string value")
703 715
704 716 def _validate_commit_idx(self, commit_idx):
705 717 if not isinstance(commit_idx, (int, long)):
706 718 raise TypeError("commit_idx must be a numeric value")
707 719
708 720 def _validate_branch_name(self, branch_name):
709 721 if branch_name and branch_name not in self.branches_all:
710 722 msg = ("Branch %s not found in %s" % (branch_name, self))
711 723 raise BranchDoesNotExistError(msg)
712 724
713 725 #
714 726 # Supporting deprecated API parts
715 727 # TODO: johbo: consider to move this into a mixin
716 728 #
717 729
718 730 @property
719 731 def EMPTY_CHANGESET(self):
720 732 warnings.warn(
721 733 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
722 734 return self.EMPTY_COMMIT_ID
723 735
724 736 @property
725 737 def revisions(self):
726 738 warnings.warn("Use commits attribute instead", DeprecationWarning)
727 739 return self.commit_ids
728 740
729 741 @revisions.setter
730 742 def revisions(self, value):
731 743 warnings.warn("Use commits attribute instead", DeprecationWarning)
732 744 self.commit_ids = value
733 745
734 746 def get_changeset(self, revision=None, pre_load=None):
735 747 warnings.warn("Use get_commit instead", DeprecationWarning)
736 748 commit_id = None
737 749 commit_idx = None
738 750 if isinstance(revision, compat.string_types):
739 751 commit_id = revision
740 752 else:
741 753 commit_idx = revision
742 754 return self.get_commit(
743 755 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
744 756
745 757 def get_changesets(
746 758 self, start=None, end=None, start_date=None, end_date=None,
747 759 branch_name=None, pre_load=None):
748 760 warnings.warn("Use get_commits instead", DeprecationWarning)
749 761 start_id = self._revision_to_commit(start)
750 762 end_id = self._revision_to_commit(end)
751 763 return self.get_commits(
752 764 start_id=start_id, end_id=end_id, start_date=start_date,
753 765 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
754 766
755 767 def _revision_to_commit(self, revision):
756 768 """
757 769 Translates a revision to a commit_id
758 770
759 771 Helps to support the old changeset based API which allows to use
760 772 commit ids and commit indices interchangeable.
761 773 """
762 774 if revision is None:
763 775 return revision
764 776
765 777 if isinstance(revision, compat.string_types):
766 778 commit_id = revision
767 779 else:
768 780 commit_id = self.commit_ids[revision]
769 781 return commit_id
770 782
771 783 @property
772 784 def in_memory_changeset(self):
773 785 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
774 786 return self.in_memory_commit
775 787
776 788 def get_path_permissions(self, username):
777 789 """
778 790 Returns a path permission checker or None if not supported
779 791
780 792 :param username: session user name
781 793 :return: an instance of BasePathPermissionChecker or None
782 794 """
783 795 return None
784 796
785 797 def install_hooks(self, force=False):
786 798 return self._remote.install_hooks(force)
787 799
788 800 def get_hooks_info(self):
789 801 return self._remote.get_hooks_info()
790 802
791 803
792 804 class BaseCommit(object):
793 805 """
794 806 Each backend should implement it's commit representation.
795 807
796 808 **Attributes**
797 809
798 810 ``repository``
799 811 repository object within which commit exists
800 812
801 813 ``id``
802 814 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
803 815 just ``tip``.
804 816
805 817 ``raw_id``
806 818 raw commit representation (i.e. full 40 length sha for git
807 819 backend)
808 820
809 821 ``short_id``
810 822 shortened (if apply) version of ``raw_id``; it would be simple
811 823 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
812 824 as ``raw_id`` for subversion
813 825
814 826 ``idx``
815 827 commit index
816 828
817 829 ``files``
818 830 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
819 831
820 832 ``dirs``
821 833 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
822 834
823 835 ``nodes``
824 836 combined list of ``Node`` objects
825 837
826 838 ``author``
827 839 author of the commit, as unicode
828 840
829 841 ``message``
830 842 message of the commit, as unicode
831 843
832 844 ``parents``
833 845 list of parent commits
834 846
835 847 """
836 848
837 849 branch = None
838 850 """
839 851 Depending on the backend this should be set to the branch name of the
840 852 commit. Backends not supporting branches on commits should leave this
841 853 value as ``None``.
842 854 """
843 855
844 856 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
845 857 """
846 858 This template is used to generate a default prefix for repository archives
847 859 if no prefix has been specified.
848 860 """
849 861
850 862 def __str__(self):
851 863 return '<%s at %s:%s>' % (
852 864 self.__class__.__name__, self.idx, self.short_id)
853 865
854 866 def __repr__(self):
855 867 return self.__str__()
856 868
857 869 def __unicode__(self):
858 870 return u'%s:%s' % (self.idx, self.short_id)
859 871
860 872 def __eq__(self, other):
861 873 same_instance = isinstance(other, self.__class__)
862 874 return same_instance and self.raw_id == other.raw_id
863 875
864 876 def __json__(self):
865 877 parents = []
866 878 try:
867 879 for parent in self.parents:
868 880 parents.append({'raw_id': parent.raw_id})
869 881 except NotImplementedError:
870 882 # empty commit doesn't have parents implemented
871 883 pass
872 884
873 885 return {
874 886 'short_id': self.short_id,
875 887 'raw_id': self.raw_id,
876 888 'revision': self.idx,
877 889 'message': self.message,
878 890 'date': self.date,
879 891 'author': self.author,
880 892 'parents': parents,
881 893 'branch': self.branch
882 894 }
883 895
884 896 def __getstate__(self):
885 897 d = self.__dict__.copy()
886 898 d.pop('_remote', None)
887 899 d.pop('repository', None)
888 900 return d
889 901
890 902 def _get_refs(self):
891 903 return {
892 904 'branches': [self.branch] if self.branch else [],
893 905 'bookmarks': getattr(self, 'bookmarks', []),
894 906 'tags': self.tags
895 907 }
896 908
897 909 @LazyProperty
898 910 def last(self):
899 911 """
900 912 ``True`` if this is last commit in repository, ``False``
901 913 otherwise; trying to access this attribute while there is no
902 914 commits would raise `EmptyRepositoryError`
903 915 """
904 916 if self.repository is None:
905 917 raise CommitError("Cannot check if it's most recent commit")
906 918 return self.raw_id == self.repository.commit_ids[-1]
907 919
908 920 @LazyProperty
909 921 def parents(self):
910 922 """
911 923 Returns list of parent commits.
912 924 """
913 925 raise NotImplementedError
914 926
915 927 @LazyProperty
916 928 def first_parent(self):
917 929 """
918 930 Returns list of parent commits.
919 931 """
920 932 return self.parents[0] if self.parents else EmptyCommit()
921 933
922 934 @property
923 935 def merge(self):
924 936 """
925 937 Returns boolean if commit is a merge.
926 938 """
927 939 return len(self.parents) > 1
928 940
929 941 @LazyProperty
930 942 def children(self):
931 943 """
932 944 Returns list of child commits.
933 945 """
934 946 raise NotImplementedError
935 947
936 948 @LazyProperty
937 949 def id(self):
938 950 """
939 951 Returns string identifying this commit.
940 952 """
941 953 raise NotImplementedError
942 954
943 955 @LazyProperty
944 956 def raw_id(self):
945 957 """
946 958 Returns raw string identifying this commit.
947 959 """
948 960 raise NotImplementedError
949 961
950 962 @LazyProperty
951 963 def short_id(self):
952 964 """
953 965 Returns shortened version of ``raw_id`` attribute, as string,
954 966 identifying this commit, useful for presentation to users.
955 967 """
956 968 raise NotImplementedError
957 969
958 970 @LazyProperty
959 971 def idx(self):
960 972 """
961 973 Returns integer identifying this commit.
962 974 """
963 975 raise NotImplementedError
964 976
965 977 @LazyProperty
966 978 def committer(self):
967 979 """
968 980 Returns committer for this commit
969 981 """
970 982 raise NotImplementedError
971 983
972 984 @LazyProperty
973 985 def committer_name(self):
974 986 """
975 987 Returns committer name for this commit
976 988 """
977 989
978 990 return author_name(self.committer)
979 991
980 992 @LazyProperty
981 993 def committer_email(self):
982 994 """
983 995 Returns committer email address for this commit
984 996 """
985 997
986 998 return author_email(self.committer)
987 999
988 1000 @LazyProperty
989 1001 def author(self):
990 1002 """
991 1003 Returns author for this commit
992 1004 """
993 1005
994 1006 raise NotImplementedError
995 1007
996 1008 @LazyProperty
997 1009 def author_name(self):
998 1010 """
999 1011 Returns author name for this commit
1000 1012 """
1001 1013
1002 1014 return author_name(self.author)
1003 1015
1004 1016 @LazyProperty
1005 1017 def author_email(self):
1006 1018 """
1007 1019 Returns author email address for this commit
1008 1020 """
1009 1021
1010 1022 return author_email(self.author)
1011 1023
1012 1024 def get_file_mode(self, path):
1013 1025 """
1014 1026 Returns stat mode of the file at `path`.
1015 1027 """
1016 1028 raise NotImplementedError
1017 1029
1018 1030 def is_link(self, path):
1019 1031 """
1020 1032 Returns ``True`` if given `path` is a symlink
1021 1033 """
1022 1034 raise NotImplementedError
1023 1035
1024 1036 def get_file_content(self, path):
1025 1037 """
1026 1038 Returns content of the file at the given `path`.
1027 1039 """
1028 1040 raise NotImplementedError
1029 1041
1030 1042 def get_file_size(self, path):
1031 1043 """
1032 1044 Returns size of the file at the given `path`.
1033 1045 """
1034 1046 raise NotImplementedError
1035 1047
1036 1048 def get_path_commit(self, path, pre_load=None):
1037 1049 """
1038 1050 Returns last commit of the file at the given `path`.
1039 1051
1040 1052 :param pre_load: Optional. List of commit attributes to load.
1041 1053 """
1042 1054 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1043 1055 if not commits:
1044 1056 raise RepositoryError(
1045 1057 'Failed to fetch history for path {}. '
1046 1058 'Please check if such path exists in your repository'.format(
1047 1059 path))
1048 1060 return commits[0]
1049 1061
1050 1062 def get_path_history(self, path, limit=None, pre_load=None):
1051 1063 """
1052 1064 Returns history of file as reversed list of :class:`BaseCommit`
1053 1065 objects for which file at given `path` has been modified.
1054 1066
1055 1067 :param limit: Optional. Allows to limit the size of the returned
1056 1068 history. This is intended as a hint to the underlying backend, so
1057 1069 that it can apply optimizations depending on the limit.
1058 1070 :param pre_load: Optional. List of commit attributes to load.
1059 1071 """
1060 1072 raise NotImplementedError
1061 1073
1062 1074 def get_file_annotate(self, path, pre_load=None):
1063 1075 """
1064 1076 Returns a generator of four element tuples with
1065 1077 lineno, sha, commit lazy loader and line
1066 1078
1067 1079 :param pre_load: Optional. List of commit attributes to load.
1068 1080 """
1069 1081 raise NotImplementedError
1070 1082
1071 1083 def get_nodes(self, path):
1072 1084 """
1073 1085 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1074 1086 state of commit at the given ``path``.
1075 1087
1076 1088 :raises ``CommitError``: if node at the given ``path`` is not
1077 1089 instance of ``DirNode``
1078 1090 """
1079 1091 raise NotImplementedError
1080 1092
1081 1093 def get_node(self, path):
1082 1094 """
1083 1095 Returns ``Node`` object from the given ``path``.
1084 1096
1085 1097 :raises ``NodeDoesNotExistError``: if there is no node at the given
1086 1098 ``path``
1087 1099 """
1088 1100 raise NotImplementedError
1089 1101
1090 1102 def get_largefile_node(self, path):
1091 1103 """
1092 1104 Returns the path to largefile from Mercurial/Git-lfs storage.
1093 1105 or None if it's not a largefile node
1094 1106 """
1095 1107 return None
1096 1108
1097 1109 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1098 1110 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1099 1111 """
1100 1112 Creates an archive containing the contents of the repository.
1101 1113
1102 1114 :param archive_dest_path: path to the file which to create the archive.
1103 1115 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1104 1116 :param prefix: name of root directory in archive.
1105 1117 Default is repository name and commit's short_id joined with dash:
1106 1118 ``"{repo_name}-{short_id}"``.
1107 1119 :param write_metadata: write a metadata file into archive.
1108 1120 :param mtime: custom modification time for archive creation, defaults
1109 1121 to time.time() if not given.
1110 1122 :param archive_at_path: pack files at this path (default '/')
1111 1123
1112 1124 :raise VCSError: If prefix has a problem.
1113 1125 """
1114 1126 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1115 1127 if kind not in allowed_kinds:
1116 1128 raise ImproperArchiveTypeError(
1117 1129 'Archive kind (%s) not supported use one of %s' %
1118 1130 (kind, allowed_kinds))
1119 1131
1120 1132 prefix = self._validate_archive_prefix(prefix)
1121 1133
1122 1134 mtime = mtime is not None or time.mktime(self.date.timetuple())
1123 1135
1124 1136 file_info = []
1125 1137 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1126 1138 for _r, _d, files in cur_rev.walk(archive_at_path):
1127 1139 for f in files:
1128 1140 f_path = os.path.join(prefix, f.path)
1129 1141 file_info.append(
1130 1142 (f_path, f.mode, f.is_link(), f.raw_bytes))
1131 1143
1132 1144 if write_metadata:
1133 1145 metadata = [
1134 1146 ('repo_name', self.repository.name),
1135 1147 ('commit_id', self.raw_id),
1136 1148 ('mtime', mtime),
1137 1149 ('branch', self.branch),
1138 1150 ('tags', ','.join(self.tags)),
1139 1151 ]
1140 1152 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1141 1153 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1142 1154
1143 1155 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1144 1156
1145 1157 def _validate_archive_prefix(self, prefix):
1146 1158 if prefix is None:
1147 1159 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1148 1160 repo_name=safe_str(self.repository.name),
1149 1161 short_id=self.short_id)
1150 1162 elif not isinstance(prefix, str):
1151 1163 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1152 1164 elif prefix.startswith('/'):
1153 1165 raise VCSError("Prefix cannot start with leading slash")
1154 1166 elif prefix.strip() == '':
1155 1167 raise VCSError("Prefix cannot be empty")
1156 1168 return prefix
1157 1169
1158 1170 @LazyProperty
1159 1171 def root(self):
1160 1172 """
1161 1173 Returns ``RootNode`` object for this commit.
1162 1174 """
1163 1175 return self.get_node('')
1164 1176
1165 1177 def next(self, branch=None):
1166 1178 """
1167 1179 Returns next commit from current, if branch is gives it will return
1168 1180 next commit belonging to this branch
1169 1181
1170 1182 :param branch: show commits within the given named branch
1171 1183 """
1172 1184 indexes = xrange(self.idx + 1, self.repository.count())
1173 1185 return self._find_next(indexes, branch)
1174 1186
1175 1187 def prev(self, branch=None):
1176 1188 """
1177 1189 Returns previous commit from current, if branch is gives it will
1178 1190 return previous commit belonging to this branch
1179 1191
1180 1192 :param branch: show commit within the given named branch
1181 1193 """
1182 1194 indexes = xrange(self.idx - 1, -1, -1)
1183 1195 return self._find_next(indexes, branch)
1184 1196
1185 1197 def _find_next(self, indexes, branch=None):
1186 1198 if branch and self.branch != branch:
1187 1199 raise VCSError('Branch option used on commit not belonging '
1188 1200 'to that branch')
1189 1201
1190 1202 for next_idx in indexes:
1191 1203 commit = self.repository.get_commit(commit_idx=next_idx)
1192 1204 if branch and branch != commit.branch:
1193 1205 continue
1194 1206 return commit
1195 1207 raise CommitDoesNotExistError
1196 1208
1197 1209 def diff(self, ignore_whitespace=True, context=3):
1198 1210 """
1199 1211 Returns a `Diff` object representing the change made by this commit.
1200 1212 """
1201 1213 parent = self.first_parent
1202 1214 diff = self.repository.get_diff(
1203 1215 parent, self,
1204 1216 ignore_whitespace=ignore_whitespace,
1205 1217 context=context)
1206 1218 return diff
1207 1219
1208 1220 @LazyProperty
1209 1221 def added(self):
1210 1222 """
1211 1223 Returns list of added ``FileNode`` objects.
1212 1224 """
1213 1225 raise NotImplementedError
1214 1226
1215 1227 @LazyProperty
1216 1228 def changed(self):
1217 1229 """
1218 1230 Returns list of modified ``FileNode`` objects.
1219 1231 """
1220 1232 raise NotImplementedError
1221 1233
1222 1234 @LazyProperty
1223 1235 def removed(self):
1224 1236 """
1225 1237 Returns list of removed ``FileNode`` objects.
1226 1238 """
1227 1239 raise NotImplementedError
1228 1240
1229 1241 @LazyProperty
1230 1242 def size(self):
1231 1243 """
1232 1244 Returns total number of bytes from contents of all filenodes.
1233 1245 """
1234 1246 return sum((node.size for node in self.get_filenodes_generator()))
1235 1247
1236 1248 def walk(self, topurl=''):
1237 1249 """
1238 1250 Similar to os.walk method. Insted of filesystem it walks through
1239 1251 commit starting at given ``topurl``. Returns generator of tuples
1240 1252 (topnode, dirnodes, filenodes).
1241 1253 """
1242 1254 topnode = self.get_node(topurl)
1243 1255 if not topnode.is_dir():
1244 1256 return
1245 1257 yield (topnode, topnode.dirs, topnode.files)
1246 1258 for dirnode in topnode.dirs:
1247 1259 for tup in self.walk(dirnode.path):
1248 1260 yield tup
1249 1261
1250 1262 def get_filenodes_generator(self):
1251 1263 """
1252 1264 Returns generator that yields *all* file nodes.
1253 1265 """
1254 1266 for topnode, dirs, files in self.walk():
1255 1267 for node in files:
1256 1268 yield node
1257 1269
1258 1270 #
1259 1271 # Utilities for sub classes to support consistent behavior
1260 1272 #
1261 1273
1262 1274 def no_node_at_path(self, path):
1263 1275 return NodeDoesNotExistError(
1264 1276 u"There is no file nor directory at the given path: "
1265 1277 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1266 1278
1267 1279 def _fix_path(self, path):
1268 1280 """
1269 1281 Paths are stored without trailing slash so we need to get rid off it if
1270 1282 needed.
1271 1283 """
1272 1284 return path.rstrip('/')
1273 1285
1274 1286 #
1275 1287 # Deprecated API based on changesets
1276 1288 #
1277 1289
1278 1290 @property
1279 1291 def revision(self):
1280 1292 warnings.warn("Use idx instead", DeprecationWarning)
1281 1293 return self.idx
1282 1294
1283 1295 @revision.setter
1284 1296 def revision(self, value):
1285 1297 warnings.warn("Use idx instead", DeprecationWarning)
1286 1298 self.idx = value
1287 1299
1288 1300 def get_file_changeset(self, path):
1289 1301 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1290 1302 return self.get_path_commit(path)
1291 1303
1292 1304
1293 1305 class BaseChangesetClass(type):
1294 1306
1295 1307 def __instancecheck__(self, instance):
1296 1308 return isinstance(instance, BaseCommit)
1297 1309
1298 1310
1299 1311 class BaseChangeset(BaseCommit):
1300 1312
1301 1313 __metaclass__ = BaseChangesetClass
1302 1314
1303 1315 def __new__(cls, *args, **kwargs):
1304 1316 warnings.warn(
1305 1317 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1306 1318 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1307 1319
1308 1320
1309 1321 class BaseInMemoryCommit(object):
1310 1322 """
1311 1323 Represents differences between repository's state (most recent head) and
1312 1324 changes made *in place*.
1313 1325
1314 1326 **Attributes**
1315 1327
1316 1328 ``repository``
1317 1329 repository object for this in-memory-commit
1318 1330
1319 1331 ``added``
1320 1332 list of ``FileNode`` objects marked as *added*
1321 1333
1322 1334 ``changed``
1323 1335 list of ``FileNode`` objects marked as *changed*
1324 1336
1325 1337 ``removed``
1326 1338 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1327 1339 *removed*
1328 1340
1329 1341 ``parents``
1330 1342 list of :class:`BaseCommit` instances representing parents of
1331 1343 in-memory commit. Should always be 2-element sequence.
1332 1344
1333 1345 """
1334 1346
1335 1347 def __init__(self, repository):
1336 1348 self.repository = repository
1337 1349 self.added = []
1338 1350 self.changed = []
1339 1351 self.removed = []
1340 1352 self.parents = []
1341 1353
1342 1354 def add(self, *filenodes):
1343 1355 """
1344 1356 Marks given ``FileNode`` objects as *to be committed*.
1345 1357
1346 1358 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1347 1359 latest commit
1348 1360 :raises ``NodeAlreadyAddedError``: if node with same path is already
1349 1361 marked as *added*
1350 1362 """
1351 1363 # Check if not already marked as *added* first
1352 1364 for node in filenodes:
1353 1365 if node.path in (n.path for n in self.added):
1354 1366 raise NodeAlreadyAddedError(
1355 1367 "Such FileNode %s is already marked for addition"
1356 1368 % node.path)
1357 1369 for node in filenodes:
1358 1370 self.added.append(node)
1359 1371
1360 1372 def change(self, *filenodes):
1361 1373 """
1362 1374 Marks given ``FileNode`` objects to be *changed* in next commit.
1363 1375
1364 1376 :raises ``EmptyRepositoryError``: if there are no commits yet
1365 1377 :raises ``NodeAlreadyExistsError``: if node with same path is already
1366 1378 marked to be *changed*
1367 1379 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1368 1380 marked to be *removed*
1369 1381 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1370 1382 commit
1371 1383 :raises ``NodeNotChangedError``: if node hasn't really be changed
1372 1384 """
1373 1385 for node in filenodes:
1374 1386 if node.path in (n.path for n in self.removed):
1375 1387 raise NodeAlreadyRemovedError(
1376 1388 "Node at %s is already marked as removed" % node.path)
1377 1389 try:
1378 1390 self.repository.get_commit()
1379 1391 except EmptyRepositoryError:
1380 1392 raise EmptyRepositoryError(
1381 1393 "Nothing to change - try to *add* new nodes rather than "
1382 1394 "changing them")
1383 1395 for node in filenodes:
1384 1396 if node.path in (n.path for n in self.changed):
1385 1397 raise NodeAlreadyChangedError(
1386 1398 "Node at '%s' is already marked as changed" % node.path)
1387 1399 self.changed.append(node)
1388 1400
1389 1401 def remove(self, *filenodes):
1390 1402 """
1391 1403 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1392 1404 *removed* in next commit.
1393 1405
1394 1406 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1395 1407 be *removed*
1396 1408 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1397 1409 be *changed*
1398 1410 """
1399 1411 for node in filenodes:
1400 1412 if node.path in (n.path for n in self.removed):
1401 1413 raise NodeAlreadyRemovedError(
1402 1414 "Node is already marked to for removal at %s" % node.path)
1403 1415 if node.path in (n.path for n in self.changed):
1404 1416 raise NodeAlreadyChangedError(
1405 1417 "Node is already marked to be changed at %s" % node.path)
1406 1418 # We only mark node as *removed* - real removal is done by
1407 1419 # commit method
1408 1420 self.removed.append(node)
1409 1421
1410 1422 def reset(self):
1411 1423 """
1412 1424 Resets this instance to initial state (cleans ``added``, ``changed``
1413 1425 and ``removed`` lists).
1414 1426 """
1415 1427 self.added = []
1416 1428 self.changed = []
1417 1429 self.removed = []
1418 1430 self.parents = []
1419 1431
1420 1432 def get_ipaths(self):
1421 1433 """
1422 1434 Returns generator of paths from nodes marked as added, changed or
1423 1435 removed.
1424 1436 """
1425 1437 for node in itertools.chain(self.added, self.changed, self.removed):
1426 1438 yield node.path
1427 1439
1428 1440 def get_paths(self):
1429 1441 """
1430 1442 Returns list of paths from nodes marked as added, changed or removed.
1431 1443 """
1432 1444 return list(self.get_ipaths())
1433 1445
1434 1446 def check_integrity(self, parents=None):
1435 1447 """
1436 1448 Checks in-memory commit's integrity. Also, sets parents if not
1437 1449 already set.
1438 1450
1439 1451 :raises CommitError: if any error occurs (i.e.
1440 1452 ``NodeDoesNotExistError``).
1441 1453 """
1442 1454 if not self.parents:
1443 1455 parents = parents or []
1444 1456 if len(parents) == 0:
1445 1457 try:
1446 1458 parents = [self.repository.get_commit(), None]
1447 1459 except EmptyRepositoryError:
1448 1460 parents = [None, None]
1449 1461 elif len(parents) == 1:
1450 1462 parents += [None]
1451 1463 self.parents = parents
1452 1464
1453 1465 # Local parents, only if not None
1454 1466 parents = [p for p in self.parents if p]
1455 1467
1456 1468 # Check nodes marked as added
1457 1469 for p in parents:
1458 1470 for node in self.added:
1459 1471 try:
1460 1472 p.get_node(node.path)
1461 1473 except NodeDoesNotExistError:
1462 1474 pass
1463 1475 else:
1464 1476 raise NodeAlreadyExistsError(
1465 1477 "Node `%s` already exists at %s" % (node.path, p))
1466 1478
1467 1479 # Check nodes marked as changed
1468 1480 missing = set(self.changed)
1469 1481 not_changed = set(self.changed)
1470 1482 if self.changed and not parents:
1471 1483 raise NodeDoesNotExistError(str(self.changed[0].path))
1472 1484 for p in parents:
1473 1485 for node in self.changed:
1474 1486 try:
1475 1487 old = p.get_node(node.path)
1476 1488 missing.remove(node)
1477 1489 # if content actually changed, remove node from not_changed
1478 1490 if old.content != node.content:
1479 1491 not_changed.remove(node)
1480 1492 except NodeDoesNotExistError:
1481 1493 pass
1482 1494 if self.changed and missing:
1483 1495 raise NodeDoesNotExistError(
1484 1496 "Node `%s` marked as modified but missing in parents: %s"
1485 1497 % (node.path, parents))
1486 1498
1487 1499 if self.changed and not_changed:
1488 1500 raise NodeNotChangedError(
1489 1501 "Node `%s` wasn't actually changed (parents: %s)"
1490 1502 % (not_changed.pop().path, parents))
1491 1503
1492 1504 # Check nodes marked as removed
1493 1505 if self.removed and not parents:
1494 1506 raise NodeDoesNotExistError(
1495 1507 "Cannot remove node at %s as there "
1496 1508 "were no parents specified" % self.removed[0].path)
1497 1509 really_removed = set()
1498 1510 for p in parents:
1499 1511 for node in self.removed:
1500 1512 try:
1501 1513 p.get_node(node.path)
1502 1514 really_removed.add(node)
1503 1515 except CommitError:
1504 1516 pass
1505 1517 not_removed = set(self.removed) - really_removed
1506 1518 if not_removed:
1507 1519 # TODO: johbo: This code branch does not seem to be covered
1508 1520 raise NodeDoesNotExistError(
1509 1521 "Cannot remove node at %s from "
1510 1522 "following parents: %s" % (not_removed, parents))
1511 1523
1512 def commit(
1513 self, message, author, parents=None, branch=None, date=None,
1514 **kwargs):
1524 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1515 1525 """
1516 1526 Performs in-memory commit (doesn't check workdir in any way) and
1517 1527 returns newly created :class:`BaseCommit`. Updates repository's
1518 1528 attribute `commits`.
1519 1529
1520 1530 .. note::
1521 1531
1522 1532 While overriding this method each backend's should call
1523 1533 ``self.check_integrity(parents)`` in the first place.
1524 1534
1525 1535 :param message: message of the commit
1526 1536 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1527 1537 :param parents: single parent or sequence of parents from which commit
1528 1538 would be derived
1529 1539 :param date: ``datetime.datetime`` instance. Defaults to
1530 1540 ``datetime.datetime.now()``.
1531 1541 :param branch: branch name, as string. If none given, default backend's
1532 1542 branch would be used.
1533 1543
1534 1544 :raises ``CommitError``: if any error occurs while committing
1535 1545 """
1536 1546 raise NotImplementedError
1537 1547
1538 1548
1539 1549 class BaseInMemoryChangesetClass(type):
1540 1550
1541 1551 def __instancecheck__(self, instance):
1542 1552 return isinstance(instance, BaseInMemoryCommit)
1543 1553
1544 1554
1545 1555 class BaseInMemoryChangeset(BaseInMemoryCommit):
1546 1556
1547 1557 __metaclass__ = BaseInMemoryChangesetClass
1548 1558
1549 1559 def __new__(cls, *args, **kwargs):
1550 1560 warnings.warn(
1551 1561 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1552 1562 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1553 1563
1554 1564
1555 1565 class EmptyCommit(BaseCommit):
1556 1566 """
1557 1567 An dummy empty commit. It's possible to pass hash when creating
1558 1568 an EmptyCommit
1559 1569 """
1560 1570
1561 1571 def __init__(
1562 1572 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1563 1573 message='', author='', date=None):
1564 1574 self._empty_commit_id = commit_id
1565 1575 # TODO: johbo: Solve idx parameter, default value does not make
1566 1576 # too much sense
1567 1577 self.idx = idx
1568 1578 self.message = message
1569 1579 self.author = author
1570 1580 self.date = date or datetime.datetime.fromtimestamp(0)
1571 1581 self.repository = repo
1572 1582 self.alias = alias
1573 1583
1574 1584 @LazyProperty
1575 1585 def raw_id(self):
1576 1586 """
1577 1587 Returns raw string identifying this commit, useful for web
1578 1588 representation.
1579 1589 """
1580 1590
1581 1591 return self._empty_commit_id
1582 1592
1583 1593 @LazyProperty
1584 1594 def branch(self):
1585 1595 if self.alias:
1586 1596 from rhodecode.lib.vcs.backends import get_backend
1587 1597 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1588 1598
1589 1599 @LazyProperty
1590 1600 def short_id(self):
1591 1601 return self.raw_id[:12]
1592 1602
1593 1603 @LazyProperty
1594 1604 def id(self):
1595 1605 return self.raw_id
1596 1606
1597 1607 def get_path_commit(self, path):
1598 1608 return self
1599 1609
1600 1610 def get_file_content(self, path):
1601 1611 return u''
1602 1612
1603 1613 def get_file_size(self, path):
1604 1614 return 0
1605 1615
1606 1616
1607 1617 class EmptyChangesetClass(type):
1608 1618
1609 1619 def __instancecheck__(self, instance):
1610 1620 return isinstance(instance, EmptyCommit)
1611 1621
1612 1622
1613 1623 class EmptyChangeset(EmptyCommit):
1614 1624
1615 1625 __metaclass__ = EmptyChangesetClass
1616 1626
1617 1627 def __new__(cls, *args, **kwargs):
1618 1628 warnings.warn(
1619 1629 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1620 1630 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1621 1631
1622 1632 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1623 1633 alias=None, revision=-1, message='', author='', date=None):
1624 1634 if requested_revision is not None:
1625 1635 warnings.warn(
1626 1636 "Parameter requested_revision not supported anymore",
1627 1637 DeprecationWarning)
1628 1638 super(EmptyChangeset, self).__init__(
1629 1639 commit_id=cs, repo=repo, alias=alias, idx=revision,
1630 1640 message=message, author=author, date=date)
1631 1641
1632 1642 @property
1633 1643 def revision(self):
1634 1644 warnings.warn("Use idx instead", DeprecationWarning)
1635 1645 return self.idx
1636 1646
1637 1647 @revision.setter
1638 1648 def revision(self, value):
1639 1649 warnings.warn("Use idx instead", DeprecationWarning)
1640 1650 self.idx = value
1641 1651
1642 1652
1643 1653 class EmptyRepository(BaseRepository):
1644 1654 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1645 1655 pass
1646 1656
1647 1657 def get_diff(self, *args, **kwargs):
1648 1658 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1649 1659 return GitDiff('')
1650 1660
1651 1661
1652 1662 class CollectionGenerator(object):
1653 1663
1654 1664 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1655 1665 self.repo = repo
1656 1666 self.commit_ids = commit_ids
1657 1667 # TODO: (oliver) this isn't currently hooked up
1658 1668 self.collection_size = None
1659 1669 self.pre_load = pre_load
1660 1670 self.translate_tag = translate_tag
1661 1671
1662 1672 def __len__(self):
1663 1673 if self.collection_size is not None:
1664 1674 return self.collection_size
1665 1675 return self.commit_ids.__len__()
1666 1676
1667 1677 def __iter__(self):
1668 1678 for commit_id in self.commit_ids:
1669 1679 # TODO: johbo: Mercurial passes in commit indices or commit ids
1670 1680 yield self._commit_factory(commit_id)
1671 1681
1672 1682 def _commit_factory(self, commit_id):
1673 1683 """
1674 1684 Allows backends to override the way commits are generated.
1675 1685 """
1676 1686 return self.repo.get_commit(
1677 1687 commit_id=commit_id, pre_load=self.pre_load,
1678 1688 translate_tag=self.translate_tag)
1679 1689
1680 1690 def __getslice__(self, i, j):
1681 1691 """
1682 1692 Returns an iterator of sliced repository
1683 1693 """
1684 1694 commit_ids = self.commit_ids[i:j]
1685 1695 return self.__class__(
1686 1696 self.repo, commit_ids, pre_load=self.pre_load,
1687 1697 translate_tag=self.translate_tag)
1688 1698
1689 1699 def __repr__(self):
1690 1700 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1691 1701
1692 1702
1693 1703 class Config(object):
1694 1704 """
1695 1705 Represents the configuration for a repository.
1696 1706
1697 1707 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1698 1708 standard library. It implements only the needed subset.
1699 1709 """
1700 1710
1701 1711 def __init__(self):
1702 1712 self._values = {}
1703 1713
1704 1714 def copy(self):
1705 1715 clone = Config()
1706 1716 for section, values in self._values.items():
1707 1717 clone._values[section] = values.copy()
1708 1718 return clone
1709 1719
1710 1720 def __repr__(self):
1711 1721 return '<Config(%s sections) at %s>' % (
1712 1722 len(self._values), hex(id(self)))
1713 1723
1714 1724 def items(self, section):
1715 1725 return self._values.get(section, {}).iteritems()
1716 1726
1717 1727 def get(self, section, option):
1718 1728 return self._values.get(section, {}).get(option)
1719 1729
1720 1730 def set(self, section, option, value):
1721 1731 section_values = self._values.setdefault(section, {})
1722 1732 section_values[option] = value
1723 1733
1724 1734 def clear_section(self, section):
1725 1735 self._values[section] = {}
1726 1736
1727 1737 def serialize(self):
1728 1738 """
1729 1739 Creates a list of three tuples (section, key, value) representing
1730 1740 this config object.
1731 1741 """
1732 1742 items = []
1733 1743 for section in self._values:
1734 1744 for option, value in self._values[section].items():
1735 1745 items.append(
1736 1746 (safe_str(section), safe_str(option), safe_str(value)))
1737 1747 return items
1738 1748
1739 1749
1740 1750 class Diff(object):
1741 1751 """
1742 1752 Represents a diff result from a repository backend.
1743 1753
1744 1754 Subclasses have to provide a backend specific value for
1745 1755 :attr:`_header_re` and :attr:`_meta_re`.
1746 1756 """
1747 1757 _meta_re = None
1748 1758 _header_re = None
1749 1759
1750 1760 def __init__(self, raw_diff):
1751 1761 self.raw = raw_diff
1752 1762
1753 1763 def chunks(self):
1754 1764 """
1755 1765 split the diff in chunks of separate --git a/file b/file chunks
1756 1766 to make diffs consistent we must prepend with \n, and make sure
1757 1767 we can detect last chunk as this was also has special rule
1758 1768 """
1759 1769
1760 1770 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1761 1771 header = diff_parts[0]
1762 1772
1763 1773 if self._meta_re:
1764 1774 match = self._meta_re.match(header)
1765 1775
1766 1776 chunks = diff_parts[1:]
1767 1777 total_chunks = len(chunks)
1768 1778
1769 1779 return (
1770 1780 DiffChunk(chunk, self, cur_chunk == total_chunks)
1771 1781 for cur_chunk, chunk in enumerate(chunks, start=1))
1772 1782
1773 1783
1774 1784 class DiffChunk(object):
1775 1785
1776 1786 def __init__(self, chunk, diff, last_chunk):
1777 1787 self._diff = diff
1778 1788
1779 1789 # since we split by \ndiff --git that part is lost from original diff
1780 1790 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1781 1791 if not last_chunk:
1782 1792 chunk += '\n'
1783 1793
1784 1794 match = self._diff._header_re.match(chunk)
1785 1795 self.header = match.groupdict()
1786 1796 self.diff = chunk[match.end():]
1787 1797 self.raw = chunk
1788 1798
1789 1799
1790 1800 class BasePathPermissionChecker(object):
1791 1801
1792 1802 @staticmethod
1793 1803 def create_from_patterns(includes, excludes):
1794 1804 if includes and '*' in includes and not excludes:
1795 1805 return AllPathPermissionChecker()
1796 1806 elif excludes and '*' in excludes:
1797 1807 return NonePathPermissionChecker()
1798 1808 else:
1799 1809 return PatternPathPermissionChecker(includes, excludes)
1800 1810
1801 1811 @property
1802 1812 def has_full_access(self):
1803 1813 raise NotImplemented()
1804 1814
1805 1815 def has_access(self, path):
1806 1816 raise NotImplemented()
1807 1817
1808 1818
1809 1819 class AllPathPermissionChecker(BasePathPermissionChecker):
1810 1820
1811 1821 @property
1812 1822 def has_full_access(self):
1813 1823 return True
1814 1824
1815 1825 def has_access(self, path):
1816 1826 return True
1817 1827
1818 1828
1819 1829 class NonePathPermissionChecker(BasePathPermissionChecker):
1820 1830
1821 1831 @property
1822 1832 def has_full_access(self):
1823 1833 return False
1824 1834
1825 1835 def has_access(self, path):
1826 1836 return False
1827 1837
1828 1838
1829 1839 class PatternPathPermissionChecker(BasePathPermissionChecker):
1830 1840
1831 1841 def __init__(self, includes, excludes):
1832 1842 self.includes = includes
1833 1843 self.excludes = excludes
1834 1844 self.includes_re = [] if not includes else [
1835 1845 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1836 1846 self.excludes_re = [] if not excludes else [
1837 1847 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1838 1848
1839 1849 @property
1840 1850 def has_full_access(self):
1841 1851 return '*' in self.includes and not self.excludes
1842 1852
1843 1853 def has_access(self, path):
1844 1854 for regex in self.excludes_re:
1845 1855 if regex.match(path):
1846 1856 return False
1847 1857 for regex in self.includes_re:
1848 1858 if regex.match(path):
1849 1859 return True
1850 1860 return False
@@ -1,106 +1,104 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 GIT inmemory module
23 23 """
24 24
25 25 from rhodecode.lib.datelib import date_to_timestamp_plus_offset
26 26 from rhodecode.lib.utils import safe_str
27 27 from rhodecode.lib.vcs.backends import base
28 28
29 29
30 30 class GitInMemoryCommit(base.BaseInMemoryCommit):
31 31
32 def commit(self, message, author, parents=None, branch=None, date=None,
33 **kwargs):
32 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
34 33 """
35 34 Performs in-memory commit (doesn't check workdir in any way) and
36 35 returns newly created `GitCommit`. Updates repository's
37 36 `commit_ids`.
38 37
39 38 :param message: message of the commit
40 39 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
41 40 :param parents: single parent or sequence of parents from which commit
42 41 would be derived
43 42 :param date: `datetime.datetime` instance. Defaults to
44 43 ``datetime.datetime.now()``.
45 44 :param branch: branch name, as string. If none given, default backend's
46 45 branch would be used.
47 46
48 47 :raises `CommitError`: if any error occurs while committing
49 48 """
50 49 self.check_integrity(parents)
51 50 if branch is None:
52 51 branch = self.repository.DEFAULT_BRANCH_NAME
53 52
54 53 ENCODING = "UTF-8"
55 54
56 55 commit_tree = None
57 56 if self.parents[0]:
58 57 commit_tree = self.parents[0]._commit['tree']
59 58
60 59 updated = []
61 60 for node in self.added + self.changed:
62 61 if not node.is_binary:
63 62 content = node.content.encode(ENCODING)
64 63 else:
65 64 content = node.content
66 65 updated.append({
67 66 'path': node.path,
68 67 'node_path': node.name.encode(ENCODING),
69 68 'content': content,
70 69 'mode': node.mode,
71 70 })
72 71
73 72 removed = [node.path for node in self.removed]
74 73
75 74 date, tz = date_to_timestamp_plus_offset(date)
76 75
77 76 # TODO: johbo: Make kwargs explicit and check if this is needed.
78 77 author_time = kwargs.pop('author_time', date)
79 78 author_tz = kwargs.pop('author_timezone', tz)
80 79
81 80 commit_data = {
82 81 'parents': [p._commit['id'] for p in self.parents if p],
83 82 'author': safe_str(author),
84 83 'committer': safe_str(author),
85 84 'encoding': ENCODING,
86 85 'message': safe_str(message),
87 86 'commit_time': int(date),
88 87 'author_time': int(author_time),
89 88 'commit_timezone': tz,
90 89 'author_timezone': author_tz,
91 90 }
92 91
93 92 commit_id = self.repository._remote.commit(
94 93 commit_data, branch, commit_tree, updated, removed)
95 94
96 95 # Update vcs repository object
97 if commit_id not in self.repository.commit_ids:
98 self.repository.commit_ids.append(commit_id)
99 self.repository._rebuild_cache(self.repository.commit_ids)
96 self.repository.append_commit_id(commit_id)
100 97
101 98 # invalidate parsed refs after commit
102 99 self.repository._refs = self.repository._get_refs()
103 100 self.repository.branches = self.repository._get_branches()
104 tip = self.repository.get_commit()
101 tip = self.repository.get_commit(commit_id)
102
105 103 self.reset()
106 104 return tip
@@ -1,1031 +1,1037 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 GIT repository module
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 import time
28 29
29 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import CachedProperty
30 32
31 33 from rhodecode.lib.compat import OrderedDict
32 34 from rhodecode.lib.datelib import (
33 35 utcdate_fromtimestamp, makedate, date_astimestamp)
34 36 from rhodecode.lib.utils import safe_unicode, safe_str
35 37 from rhodecode.lib.vcs import connection, path as vcspath
36 38 from rhodecode.lib.vcs.backends.base import (
37 39 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 40 MergeFailureReason, Reference)
39 41 from rhodecode.lib.vcs.backends.git.commit import GitCommit
40 42 from rhodecode.lib.vcs.backends.git.diff import GitDiff
41 43 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
42 44 from rhodecode.lib.vcs.exceptions import (
43 45 CommitDoesNotExistError, EmptyRepositoryError,
44 46 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
45 47
46 48
47 49 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
48 50
49 51 log = logging.getLogger(__name__)
50 52
51 53
52 54 class GitRepository(BaseRepository):
53 55 """
54 56 Git repository backend.
55 57 """
56 58 DEFAULT_BRANCH_NAME = 'master'
57 59
58 60 contact = BaseRepository.DEFAULT_CONTACT
59 61
60 62 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 63 do_workspace_checkout=False, with_wire=None, bare=False):
62 64
63 65 self.path = safe_str(os.path.abspath(repo_path))
64 66 self.config = config if config else self.get_default_config()
65 67 self.with_wire = with_wire
66 68
67 69 self._init_repo(create, src_url, do_workspace_checkout, bare)
68 70
69 71 # caches
70 72 self._commit_ids = {}
71 73
74 # dependent that trigger re-computation of commit_ids
75 self._commit_ids_ver = 0
76
72 77 @LazyProperty
73 78 def _remote(self):
74 79 return connection.Git(self.path, self.config, with_wire=self.with_wire)
75 80
76 81 @LazyProperty
77 82 def bare(self):
78 83 return self._remote.bare()
79 84
80 85 @LazyProperty
81 86 def head(self):
82 87 return self._remote.head()
83 88
84 @LazyProperty
89 @CachedProperty('_commit_ids_ver')
85 90 def commit_ids(self):
86 91 """
87 92 Returns list of commit ids, in ascending order. Being lazy
88 93 attribute allows external tools to inject commit ids from cache.
89 94 """
90 95 commit_ids = self._get_all_commit_ids()
91 96 self._rebuild_cache(commit_ids)
92 97 return commit_ids
93 98
94 99 def _rebuild_cache(self, commit_ids):
95 100 self._commit_ids = dict((commit_id, index)
96 101 for index, commit_id in enumerate(commit_ids))
97 102
98 103 def run_git_command(self, cmd, **opts):
99 104 """
100 105 Runs given ``cmd`` as git command and returns tuple
101 106 (stdout, stderr).
102 107
103 108 :param cmd: git command to be executed
104 109 :param opts: env options to pass into Subprocess command
105 110 """
106 111 if not isinstance(cmd, list):
107 112 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
108 113
109 114 skip_stderr_log = opts.pop('skip_stderr_log', False)
110 115 out, err = self._remote.run_git_command(cmd, **opts)
111 116 if err and not skip_stderr_log:
112 117 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
113 118 return out, err
114 119
115 120 @staticmethod
116 121 def check_url(url, config):
117 122 """
118 123 Function will check given url and try to verify if it's a valid
119 124 link. Sometimes it may happened that git will issue basic
120 125 auth request that can cause whole API to hang when used from python
121 126 or other external calls.
122 127
123 128 On failures it'll raise urllib2.HTTPError, exception is also thrown
124 129 when the return code is non 200
125 130 """
126 131 # check first if it's not an url
127 132 if os.path.isdir(url) or url.startswith('file:'):
128 133 return True
129 134
130 135 if '+' in url.split('://', 1)[0]:
131 136 url = url.split('+', 1)[1]
132 137
133 138 # Request the _remote to verify the url
134 139 return connection.Git.check_url(url, config.serialize())
135 140
136 141 @staticmethod
137 142 def is_valid_repository(path):
138 143 if os.path.isdir(os.path.join(path, '.git')):
139 144 return True
140 145 # check case of bare repository
141 146 try:
142 147 GitRepository(path)
143 148 return True
144 149 except VCSError:
145 150 pass
146 151 return False
147 152
148 153 def _init_repo(self, create, src_url=None, do_workspace_checkout=False,
149 154 bare=False):
150 155 if create and os.path.exists(self.path):
151 156 raise RepositoryError(
152 157 "Cannot create repository at %s, location already exist"
153 158 % self.path)
154 159
155 160 if bare and do_workspace_checkout:
156 161 raise RepositoryError("Cannot update a bare repository")
157 162 try:
158 163
159 164 if src_url:
160 165 # check URL before any actions
161 166 GitRepository.check_url(src_url, self.config)
162 167
163 168 if create:
164 169 os.makedirs(self.path, mode=0o755)
165 170
166 171 if bare:
167 172 self._remote.init_bare()
168 173 else:
169 174 self._remote.init()
170 175
171 176 if src_url and bare:
172 177 # bare repository only allows a fetch and checkout is not allowed
173 178 self.fetch(src_url, commit_ids=None)
174 179 elif src_url:
175 180 self.pull(src_url, commit_ids=None,
176 181 update_after=do_workspace_checkout)
177 182
178 183 else:
179 184 if not self._remote.assert_correct_path():
180 185 raise RepositoryError(
181 186 'Path "%s" does not contain a Git repository' %
182 187 (self.path,))
183 188
184 189 # TODO: johbo: check if we have to translate the OSError here
185 190 except OSError as err:
186 191 raise RepositoryError(err)
187 192
188 193 def _get_all_commit_ids(self, filters=None):
189 194 # we must check if this repo is not empty, since later command
190 195 # fails if it is. And it's cheaper to ask than throw the subprocess
191 196 # errors
192 197
193 198 head = self._remote.head(show_exc=False)
194 199 if not head:
195 200 return []
196 201
197 202 rev_filter = ['--branches', '--tags']
198 203 extra_filter = []
199 204
200 205 if filters:
201 206 if filters.get('since'):
202 207 extra_filter.append('--since=%s' % (filters['since']))
203 208 if filters.get('until'):
204 209 extra_filter.append('--until=%s' % (filters['until']))
205 210 if filters.get('branch_name'):
206 211 rev_filter = ['--tags']
207 212 extra_filter.append(filters['branch_name'])
208 213 rev_filter.extend(extra_filter)
209 214
210 215 # if filters.get('start') or filters.get('end'):
211 216 # # skip is offset, max-count is limit
212 217 # if filters.get('start'):
213 218 # extra_filter += ' --skip=%s' % filters['start']
214 219 # if filters.get('end'):
215 220 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
216 221
217 222 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
218 223 try:
219 224 output, __ = self.run_git_command(cmd)
220 225 except RepositoryError:
221 226 # Can be raised for empty repositories
222 227 return []
223 228 return output.splitlines()
224 229
225 230 def _lookup_commit(self, commit_id_or_idx, translate_tag=True):
226 231 def is_null(value):
227 232 return len(value) == commit_id_or_idx.count('0')
228 233
229 234 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
230 235 return self.commit_ids[-1]
231 236
232 237 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
233 238 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
234 239 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
235 240 try:
236 241 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
237 242 except Exception:
238 243 msg = "Commit %s does not exist for %s" % (commit_id_or_idx, self.name)
239 244 raise CommitDoesNotExistError(msg)
240 245
241 246 elif is_bstr:
242 247 # check full path ref, eg. refs/heads/master
243 248 ref_id = self._refs.get(commit_id_or_idx)
244 249 if ref_id:
245 250 return ref_id
246 251
247 252 # check branch name
248 253 branch_ids = self.branches.values()
249 254 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
250 255 if ref_id:
251 256 return ref_id
252 257
253 258 # check tag name
254 259 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
255 260 if ref_id:
256 261 return ref_id
257 262
258 263 if (not SHA_PATTERN.match(commit_id_or_idx) or
259 264 commit_id_or_idx not in self.commit_ids):
260 265 msg = "Commit %s does not exist for %s" % (commit_id_or_idx, self.name)
261 266 raise CommitDoesNotExistError(msg)
262 267
263 268 # Ensure we return full id
264 269 if not SHA_PATTERN.match(str(commit_id_or_idx)):
265 270 raise CommitDoesNotExistError(
266 271 "Given commit id %s not recognized" % commit_id_or_idx)
267 272 return commit_id_or_idx
268 273
269 274 def get_hook_location(self):
270 275 """
271 276 returns absolute path to location where hooks are stored
272 277 """
273 278 loc = os.path.join(self.path, 'hooks')
274 279 if not self.bare:
275 280 loc = os.path.join(self.path, '.git', 'hooks')
276 281 return loc
277 282
278 283 @LazyProperty
279 284 def last_change(self):
280 285 """
281 286 Returns last change made on this repository as
282 287 `datetime.datetime` object.
283 288 """
284 289 try:
285 290 return self.get_commit().date
286 291 except RepositoryError:
287 292 tzoffset = makedate()[1]
288 293 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
289 294
290 295 def _get_fs_mtime(self):
291 296 idx_loc = '' if self.bare else '.git'
292 297 # fallback to filesystem
293 298 in_path = os.path.join(self.path, idx_loc, "index")
294 299 he_path = os.path.join(self.path, idx_loc, "HEAD")
295 300 if os.path.exists(in_path):
296 301 return os.stat(in_path).st_mtime
297 302 else:
298 303 return os.stat(he_path).st_mtime
299 304
300 305 @LazyProperty
301 306 def description(self):
302 307 description = self._remote.get_description()
303 308 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
304 309
305 310 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
306 311 if self.is_empty():
307 312 return OrderedDict()
308 313
309 314 result = []
310 315 for ref, sha in self._refs.iteritems():
311 316 if ref.startswith(prefix):
312 317 ref_name = ref
313 318 if strip_prefix:
314 319 ref_name = ref[len(prefix):]
315 320 result.append((safe_unicode(ref_name), sha))
316 321
317 322 def get_name(entry):
318 323 return entry[0]
319 324
320 325 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
321 326
322 327 def _get_branches(self):
323 328 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
324 329
325 330 @LazyProperty
326 331 def branches(self):
327 332 return self._get_branches()
328 333
329 334 @LazyProperty
330 335 def branches_closed(self):
331 336 return {}
332 337
333 338 @LazyProperty
334 339 def bookmarks(self):
335 340 return {}
336 341
337 342 @LazyProperty
338 343 def branches_all(self):
339 344 all_branches = {}
340 345 all_branches.update(self.branches)
341 346 all_branches.update(self.branches_closed)
342 347 return all_branches
343 348
344 349 @LazyProperty
345 350 def tags(self):
346 351 return self._get_tags()
347 352
348 353 def _get_tags(self):
349 354 return self._get_refs_entries(
350 355 prefix='refs/tags/', strip_prefix=True, reverse=True)
351 356
352 357 def tag(self, name, user, commit_id=None, message=None, date=None,
353 358 **kwargs):
354 359 # TODO: fix this method to apply annotated tags correct with message
355 360 """
356 361 Creates and returns a tag for the given ``commit_id``.
357 362
358 363 :param name: name for new tag
359 364 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
360 365 :param commit_id: commit id for which new tag would be created
361 366 :param message: message of the tag's commit
362 367 :param date: date of tag's commit
363 368
364 369 :raises TagAlreadyExistError: if tag with same name already exists
365 370 """
366 371 if name in self.tags:
367 372 raise TagAlreadyExistError("Tag %s already exists" % name)
368 373 commit = self.get_commit(commit_id=commit_id)
369 374 message = message or "Added tag %s for commit %s" % (
370 375 name, commit.raw_id)
371 376 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
372 377
373 378 self._refs = self._get_refs()
374 379 self.tags = self._get_tags()
375 380 return commit
376 381
377 382 def remove_tag(self, name, user, message=None, date=None):
378 383 """
379 384 Removes tag with the given ``name``.
380 385
381 386 :param name: name of the tag to be removed
382 387 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
383 388 :param message: message of the tag's removal commit
384 389 :param date: date of tag's removal commit
385 390
386 391 :raises TagDoesNotExistError: if tag with given name does not exists
387 392 """
388 393 if name not in self.tags:
389 394 raise TagDoesNotExistError("Tag %s does not exist" % name)
390 395 tagpath = vcspath.join(
391 396 self._remote.get_refs_path(), 'refs', 'tags', name)
392 397 try:
393 398 os.remove(tagpath)
394 399 self._refs = self._get_refs()
395 400 self.tags = self._get_tags()
396 401 except OSError as e:
397 402 raise RepositoryError(e.strerror)
398 403
399 404 def _get_refs(self):
400 405 return self._remote.get_refs()
401 406
402 407 @LazyProperty
403 408 def _refs(self):
404 409 return self._get_refs()
405 410
406 411 @property
407 412 def _ref_tree(self):
408 413 node = tree = {}
409 414 for ref, sha in self._refs.iteritems():
410 415 path = ref.split('/')
411 416 for bit in path[:-1]:
412 417 node = node.setdefault(bit, {})
413 418 node[path[-1]] = sha
414 419 node = tree
415 420 return tree
416 421
417 422 def get_remote_ref(self, ref_name):
418 423 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
419 424 try:
420 425 return self._refs[ref_key]
421 426 except Exception:
422 427 return
423 428
424 429 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=True):
425 430 """
426 431 Returns `GitCommit` object representing commit from git repository
427 432 at the given `commit_id` or head (most recent commit) if None given.
428 433 """
429 434 if self.is_empty():
430 435 raise EmptyRepositoryError("There are no commits yet")
431 436
432 437 if commit_id is not None:
433 438 self._validate_commit_id(commit_id)
434 439 try:
435 440 # we have cached idx, use it without contacting the remote
436 441 idx = self._commit_ids[commit_id]
437 442 return GitCommit(self, commit_id, idx, pre_load=pre_load)
438 443 except KeyError:
439 444 pass
440 445
441 446 elif commit_idx is not None:
442 447 self._validate_commit_idx(commit_idx)
443 448 try:
444 449 _commit_id = self.commit_ids[commit_idx]
445 450 if commit_idx < 0:
446 451 commit_idx = self.commit_ids.index(_commit_id)
447 452 return GitCommit(self, _commit_id, commit_idx, pre_load=pre_load)
448 453 except IndexError:
449 454 commit_id = commit_idx
450 455 else:
451 456 commit_id = "tip"
452 457
453 458 commit_id = self._lookup_commit(commit_id)
454 459 remote_idx = None
455 460 if translate_tag:
456 461 # Need to call remote to translate id for tagging scenario
457 462 remote_data = self._remote.get_object(commit_id)
458 463 commit_id = remote_data["commit_id"]
459 464 remote_idx = remote_data["idx"]
460 465
461 466 try:
462 467 idx = self._commit_ids[commit_id]
463 468 except KeyError:
464 469 idx = remote_idx or 0
465 470
466 471 return GitCommit(self, commit_id, idx, pre_load=pre_load)
467 472
468 473 def get_commits(
469 474 self, start_id=None, end_id=None, start_date=None, end_date=None,
470 475 branch_name=None, show_hidden=False, pre_load=None, translate_tags=True):
471 476 """
472 477 Returns generator of `GitCommit` objects from start to end (both
473 478 are inclusive), in ascending date order.
474 479
475 480 :param start_id: None, str(commit_id)
476 481 :param end_id: None, str(commit_id)
477 482 :param start_date: if specified, commits with commit date less than
478 483 ``start_date`` would be filtered out from returned set
479 484 :param end_date: if specified, commits with commit date greater than
480 485 ``end_date`` would be filtered out from returned set
481 486 :param branch_name: if specified, commits not reachable from given
482 487 branch would be filtered out from returned set
483 488 :param show_hidden: Show hidden commits such as obsolete or hidden from
484 489 Mercurial evolve
485 490 :raise BranchDoesNotExistError: If given `branch_name` does not
486 491 exist.
487 492 :raise CommitDoesNotExistError: If commits for given `start` or
488 493 `end` could not be found.
489 494
490 495 """
491 496 if self.is_empty():
492 497 raise EmptyRepositoryError("There are no commits yet")
493 498
494 499 self._validate_branch_name(branch_name)
495 500
496 501 if start_id is not None:
497 502 self._validate_commit_id(start_id)
498 503 if end_id is not None:
499 504 self._validate_commit_id(end_id)
500 505
501 506 start_raw_id = self._lookup_commit(start_id)
502 507 start_pos = self._commit_ids[start_raw_id] if start_id else None
503 508 end_raw_id = self._lookup_commit(end_id)
504 509 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
505 510
506 511 if None not in [start_id, end_id] and start_pos > end_pos:
507 512 raise RepositoryError(
508 513 "Start commit '%s' cannot be after end commit '%s'" %
509 514 (start_id, end_id))
510 515
511 516 if end_pos is not None:
512 517 end_pos += 1
513 518
514 519 filter_ = []
515 520 if branch_name:
516 521 filter_.append({'branch_name': branch_name})
517 522 if start_date and not end_date:
518 523 filter_.append({'since': start_date})
519 524 if end_date and not start_date:
520 525 filter_.append({'until': end_date})
521 526 if start_date and end_date:
522 527 filter_.append({'since': start_date})
523 528 filter_.append({'until': end_date})
524 529
525 530 # if start_pos or end_pos:
526 531 # filter_.append({'start': start_pos})
527 532 # filter_.append({'end': end_pos})
528 533
529 534 if filter_:
530 535 revfilters = {
531 536 'branch_name': branch_name,
532 537 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
533 538 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
534 539 'start': start_pos,
535 540 'end': end_pos,
536 541 }
537 542 commit_ids = self._get_all_commit_ids(filters=revfilters)
538 543
539 544 # pure python stuff, it's slow due to walker walking whole repo
540 545 # def get_revs(walker):
541 546 # for walker_entry in walker:
542 547 # yield walker_entry.commit.id
543 548 # revfilters = {}
544 549 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
545 550 else:
546 551 commit_ids = self.commit_ids
547 552
548 553 if start_pos or end_pos:
549 554 commit_ids = commit_ids[start_pos: end_pos]
550 555
551 556 return CollectionGenerator(self, commit_ids, pre_load=pre_load,
552 557 translate_tag=translate_tags)
553 558
554 559 def get_diff(
555 560 self, commit1, commit2, path='', ignore_whitespace=False,
556 561 context=3, path1=None):
557 562 """
558 563 Returns (git like) *diff*, as plain text. Shows changes introduced by
559 564 ``commit2`` since ``commit1``.
560 565
561 566 :param commit1: Entry point from which diff is shown. Can be
562 567 ``self.EMPTY_COMMIT`` - in this case, patch showing all
563 568 the changes since empty state of the repository until ``commit2``
564 569 :param commit2: Until which commits changes should be shown.
565 570 :param ignore_whitespace: If set to ``True``, would not show whitespace
566 571 changes. Defaults to ``False``.
567 572 :param context: How many lines before/after changed lines should be
568 573 shown. Defaults to ``3``.
569 574 """
570 575 self._validate_diff_commits(commit1, commit2)
571 576 if path1 is not None and path1 != path:
572 577 raise ValueError("Diff of two different paths not supported.")
573 578
574 579 flags = [
575 580 '-U%s' % context, '--full-index', '--binary', '-p',
576 581 '-M', '--abbrev=40']
577 582 if ignore_whitespace:
578 583 flags.append('-w')
579 584
580 585 if commit1 == self.EMPTY_COMMIT:
581 586 cmd = ['show'] + flags + [commit2.raw_id]
582 587 else:
583 588 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
584 589
585 590 if path:
586 591 cmd.extend(['--', path])
587 592
588 593 stdout, __ = self.run_git_command(cmd)
589 594 # If we used 'show' command, strip first few lines (until actual diff
590 595 # starts)
591 596 if commit1 == self.EMPTY_COMMIT:
592 597 lines = stdout.splitlines()
593 598 x = 0
594 599 for line in lines:
595 600 if line.startswith('diff'):
596 601 break
597 602 x += 1
598 603 # Append new line just like 'diff' command do
599 604 stdout = '\n'.join(lines[x:]) + '\n'
600 605 return GitDiff(stdout)
601 606
602 607 def strip(self, commit_id, branch_name):
603 608 commit = self.get_commit(commit_id=commit_id)
604 609 if commit.merge:
605 610 raise Exception('Cannot reset to merge commit')
606 611
607 612 # parent is going to be the new head now
608 613 commit = commit.parents[0]
609 614 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
610 615
611 self.commit_ids = self._get_all_commit_ids()
612 self._rebuild_cache(self.commit_ids)
616 self._commit_ids_ver = time.time()
617 # we updated _commit_ids_ver so accessing self.commit_ids will re-compute it
618 return len(self.commit_ids)
613 619
614 620 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
615 621 if commit_id1 == commit_id2:
616 622 return commit_id1
617 623
618 624 if self != repo2:
619 625 commits = self._remote.get_missing_revs(
620 626 commit_id1, commit_id2, repo2.path)
621 627 if commits:
622 628 commit = repo2.get_commit(commits[-1])
623 629 if commit.parents:
624 630 ancestor_id = commit.parents[0].raw_id
625 631 else:
626 632 ancestor_id = None
627 633 else:
628 634 # no commits from other repo, ancestor_id is the commit_id2
629 635 ancestor_id = commit_id2
630 636 else:
631 637 output, __ = self.run_git_command(
632 638 ['merge-base', commit_id1, commit_id2])
633 639 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
634 640
635 641 return ancestor_id
636 642
637 643 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
638 644 repo1 = self
639 645 ancestor_id = None
640 646
641 647 if commit_id1 == commit_id2:
642 648 commits = []
643 649 elif repo1 != repo2:
644 650 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
645 651 repo2.path)
646 652 commits = [
647 653 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
648 654 for commit_id in reversed(missing_ids)]
649 655 else:
650 656 output, __ = repo1.run_git_command(
651 657 ['log', '--reverse', '--pretty=format: %H', '-s',
652 658 '%s..%s' % (commit_id1, commit_id2)])
653 659 commits = [
654 660 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
655 661 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
656 662
657 663 return commits
658 664
659 665 @LazyProperty
660 666 def in_memory_commit(self):
661 667 """
662 668 Returns ``GitInMemoryCommit`` object for this repository.
663 669 """
664 670 return GitInMemoryCommit(self)
665 671
666 672 def pull(self, url, commit_ids=None, update_after=False):
667 673 """
668 674 Pull changes from external location. Pull is different in GIT
669 675 that fetch since it's doing a checkout
670 676
671 677 :param commit_ids: Optional. Can be set to a list of commit ids
672 678 which shall be pulled from the other repository.
673 679 """
674 680 refs = None
675 681 if commit_ids is not None:
676 682 remote_refs = self._remote.get_remote_refs(url)
677 683 refs = [ref for ref in remote_refs if remote_refs[ref] in commit_ids]
678 684 self._remote.pull(url, refs=refs, update_after=update_after)
679 685 self._remote.invalidate_vcs_cache()
680 686
681 687 def fetch(self, url, commit_ids=None):
682 688 """
683 689 Fetch all git objects from external location.
684 690 """
685 691 self._remote.sync_fetch(url, refs=commit_ids)
686 692 self._remote.invalidate_vcs_cache()
687 693
688 694 def push(self, url):
689 695 refs = None
690 696 self._remote.sync_push(url, refs=refs)
691 697
692 698 def set_refs(self, ref_name, commit_id):
693 699 self._remote.set_refs(ref_name, commit_id)
694 700
695 701 def remove_ref(self, ref_name):
696 702 self._remote.remove_ref(ref_name)
697 703
698 704 def _update_server_info(self):
699 705 """
700 706 runs gits update-server-info command in this repo instance
701 707 """
702 708 self._remote.update_server_info()
703 709
704 710 def _current_branch(self):
705 711 """
706 712 Return the name of the current branch.
707 713
708 714 It only works for non bare repositories (i.e. repositories with a
709 715 working copy)
710 716 """
711 717 if self.bare:
712 718 raise RepositoryError('Bare git repos do not have active branches')
713 719
714 720 if self.is_empty():
715 721 return None
716 722
717 723 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
718 724 return stdout.strip()
719 725
720 726 def _checkout(self, branch_name, create=False, force=False):
721 727 """
722 728 Checkout a branch in the working directory.
723 729
724 730 It tries to create the branch if create is True, failing if the branch
725 731 already exists.
726 732
727 733 It only works for non bare repositories (i.e. repositories with a
728 734 working copy)
729 735 """
730 736 if self.bare:
731 737 raise RepositoryError('Cannot checkout branches in a bare git repo')
732 738
733 739 cmd = ['checkout']
734 740 if force:
735 741 cmd.append('-f')
736 742 if create:
737 743 cmd.append('-b')
738 744 cmd.append(branch_name)
739 745 self.run_git_command(cmd, fail_on_stderr=False)
740 746
741 747 def _identify(self):
742 748 """
743 749 Return the current state of the working directory.
744 750 """
745 751 if self.bare:
746 752 raise RepositoryError('Bare git repos do not have active branches')
747 753
748 754 if self.is_empty():
749 755 return None
750 756
751 757 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
752 758 return stdout.strip()
753 759
754 760 def _local_clone(self, clone_path, branch_name, source_branch=None):
755 761 """
756 762 Create a local clone of the current repo.
757 763 """
758 764 # N.B.(skreft): the --branch option is required as otherwise the shallow
759 765 # clone will only fetch the active branch.
760 766 cmd = ['clone', '--branch', branch_name,
761 767 self.path, os.path.abspath(clone_path)]
762 768
763 769 self.run_git_command(cmd, fail_on_stderr=False)
764 770
765 771 # if we get the different source branch, make sure we also fetch it for
766 772 # merge conditions
767 773 if source_branch and source_branch != branch_name:
768 774 # check if the ref exists.
769 775 shadow_repo = GitRepository(os.path.abspath(clone_path))
770 776 if shadow_repo.get_remote_ref(source_branch):
771 777 cmd = ['fetch', self.path, source_branch]
772 778 self.run_git_command(cmd, fail_on_stderr=False)
773 779
774 780 def _local_fetch(self, repository_path, branch_name, use_origin=False):
775 781 """
776 782 Fetch a branch from a local repository.
777 783 """
778 784 repository_path = os.path.abspath(repository_path)
779 785 if repository_path == self.path:
780 786 raise ValueError('Cannot fetch from the same repository')
781 787
782 788 if use_origin:
783 789 branch_name = '+{branch}:refs/heads/{branch}'.format(
784 790 branch=branch_name)
785 791
786 792 cmd = ['fetch', '--no-tags', '--update-head-ok',
787 793 repository_path, branch_name]
788 794 self.run_git_command(cmd, fail_on_stderr=False)
789 795
790 796 def _local_reset(self, branch_name):
791 797 branch_name = '{}'.format(branch_name)
792 798 cmd = ['reset', '--hard', branch_name, '--']
793 799 self.run_git_command(cmd, fail_on_stderr=False)
794 800
795 801 def _last_fetch_heads(self):
796 802 """
797 803 Return the last fetched heads that need merging.
798 804
799 805 The algorithm is defined at
800 806 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
801 807 """
802 808 if not self.bare:
803 809 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
804 810 else:
805 811 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
806 812
807 813 heads = []
808 814 with open(fetch_heads_path) as f:
809 815 for line in f:
810 816 if ' not-for-merge ' in line:
811 817 continue
812 818 line = re.sub('\t.*', '', line, flags=re.DOTALL)
813 819 heads.append(line)
814 820
815 821 return heads
816 822
817 823 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
818 824 return GitRepository(shadow_repository_path)
819 825
820 826 def _local_pull(self, repository_path, branch_name, ff_only=True):
821 827 """
822 828 Pull a branch from a local repository.
823 829 """
824 830 if self.bare:
825 831 raise RepositoryError('Cannot pull into a bare git repository')
826 832 # N.B.(skreft): The --ff-only option is to make sure this is a
827 833 # fast-forward (i.e., we are only pulling new changes and there are no
828 834 # conflicts with our current branch)
829 835 # Additionally, that option needs to go before --no-tags, otherwise git
830 836 # pull complains about it being an unknown flag.
831 837 cmd = ['pull']
832 838 if ff_only:
833 839 cmd.append('--ff-only')
834 840 cmd.extend(['--no-tags', repository_path, branch_name])
835 841 self.run_git_command(cmd, fail_on_stderr=False)
836 842
837 843 def _local_merge(self, merge_message, user_name, user_email, heads):
838 844 """
839 845 Merge the given head into the checked out branch.
840 846
841 847 It will force a merge commit.
842 848
843 849 Currently it raises an error if the repo is empty, as it is not possible
844 850 to create a merge commit in an empty repo.
845 851
846 852 :param merge_message: The message to use for the merge commit.
847 853 :param heads: the heads to merge.
848 854 """
849 855 if self.bare:
850 856 raise RepositoryError('Cannot merge into a bare git repository')
851 857
852 858 if not heads:
853 859 return
854 860
855 861 if self.is_empty():
856 862 # TODO(skreft): do somehting more robust in this case.
857 863 raise RepositoryError(
858 864 'Do not know how to merge into empty repositories yet')
859 865
860 866 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
861 867 # commit message. We also specify the user who is doing the merge.
862 868 cmd = ['-c', 'user.name="%s"' % safe_str(user_name),
863 869 '-c', 'user.email=%s' % safe_str(user_email),
864 870 'merge', '--no-ff', '-m', safe_str(merge_message)]
865 871 cmd.extend(heads)
866 872 try:
867 873 output = self.run_git_command(cmd, fail_on_stderr=False)
868 874 except RepositoryError:
869 875 # Cleanup any merge leftovers
870 876 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
871 877 raise
872 878
873 879 def _local_push(
874 880 self, source_branch, repository_path, target_branch,
875 881 enable_hooks=False, rc_scm_data=None):
876 882 """
877 883 Push the source_branch to the given repository and target_branch.
878 884
879 885 Currently it if the target_branch is not master and the target repo is
880 886 empty, the push will work, but then GitRepository won't be able to find
881 887 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
882 888 pointing to master, which does not exist).
883 889
884 890 It does not run the hooks in the target repo.
885 891 """
886 892 # TODO(skreft): deal with the case in which the target repo is empty,
887 893 # and the target_branch is not master.
888 894 target_repo = GitRepository(repository_path)
889 895 if (not target_repo.bare and
890 896 target_repo._current_branch() == target_branch):
891 897 # Git prevents pushing to the checked out branch, so simulate it by
892 898 # pulling into the target repository.
893 899 target_repo._local_pull(self.path, source_branch)
894 900 else:
895 901 cmd = ['push', os.path.abspath(repository_path),
896 902 '%s:%s' % (source_branch, target_branch)]
897 903 gitenv = {}
898 904 if rc_scm_data:
899 905 gitenv.update({'RC_SCM_DATA': rc_scm_data})
900 906
901 907 if not enable_hooks:
902 908 gitenv['RC_SKIP_HOOKS'] = '1'
903 909 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
904 910
905 911 def _get_new_pr_branch(self, source_branch, target_branch):
906 912 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
907 913 pr_branches = []
908 914 for branch in self.branches:
909 915 if branch.startswith(prefix):
910 916 pr_branches.append(int(branch[len(prefix):]))
911 917
912 918 if not pr_branches:
913 919 branch_id = 0
914 920 else:
915 921 branch_id = max(pr_branches) + 1
916 922
917 923 return '%s%d' % (prefix, branch_id)
918 924
919 925 def _maybe_prepare_merge_workspace(
920 926 self, repo_id, workspace_id, target_ref, source_ref):
921 927 shadow_repository_path = self._get_shadow_repository_path(
922 928 repo_id, workspace_id)
923 929 if not os.path.exists(shadow_repository_path):
924 930 self._local_clone(
925 931 shadow_repository_path, target_ref.name, source_ref.name)
926 932 log.debug(
927 933 'Prepared shadow repository in %s', shadow_repository_path)
928 934
929 935 return shadow_repository_path
930 936
931 937 def _merge_repo(self, repo_id, workspace_id, target_ref,
932 938 source_repo, source_ref, merge_message,
933 939 merger_name, merger_email, dry_run=False,
934 940 use_rebase=False, close_branch=False):
935 941
936 942 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
937 943 'rebase' if use_rebase else 'merge', dry_run)
938 944 if target_ref.commit_id != self.branches[target_ref.name]:
939 945 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
940 946 target_ref.commit_id, self.branches[target_ref.name])
941 947 return MergeResponse(
942 948 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
943 949 metadata={'target_ref': target_ref})
944 950
945 951 shadow_repository_path = self._maybe_prepare_merge_workspace(
946 952 repo_id, workspace_id, target_ref, source_ref)
947 953 shadow_repo = self._get_shadow_instance(shadow_repository_path)
948 954
949 955 # checkout source, if it's different. Otherwise we could not
950 956 # fetch proper commits for merge testing
951 957 if source_ref.name != target_ref.name:
952 958 if shadow_repo.get_remote_ref(source_ref.name):
953 959 shadow_repo._checkout(source_ref.name, force=True)
954 960
955 961 # checkout target, and fetch changes
956 962 shadow_repo._checkout(target_ref.name, force=True)
957 963
958 964 # fetch/reset pull the target, in case it is changed
959 965 # this handles even force changes
960 966 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
961 967 shadow_repo._local_reset(target_ref.name)
962 968
963 969 # Need to reload repo to invalidate the cache, or otherwise we cannot
964 970 # retrieve the last target commit.
965 971 shadow_repo = self._get_shadow_instance(shadow_repository_path)
966 972 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
967 973 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
968 974 target_ref, target_ref.commit_id,
969 975 shadow_repo.branches[target_ref.name])
970 976 return MergeResponse(
971 977 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
972 978 metadata={'target_ref': target_ref})
973 979
974 980 # calculate new branch
975 981 pr_branch = shadow_repo._get_new_pr_branch(
976 982 source_ref.name, target_ref.name)
977 983 log.debug('using pull-request merge branch: `%s`', pr_branch)
978 984 # checkout to temp branch, and fetch changes
979 985 shadow_repo._checkout(pr_branch, create=True)
980 986 try:
981 987 shadow_repo._local_fetch(source_repo.path, source_ref.name)
982 988 except RepositoryError:
983 989 log.exception('Failure when doing local fetch on '
984 990 'shadow repo: %s', shadow_repo)
985 991 return MergeResponse(
986 992 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
987 993 metadata={'source_ref': source_ref})
988 994
989 995 merge_ref = None
990 996 merge_failure_reason = MergeFailureReason.NONE
991 997 metadata = {}
992 998 try:
993 999 shadow_repo._local_merge(merge_message, merger_name, merger_email,
994 1000 [source_ref.commit_id])
995 1001 merge_possible = True
996 1002
997 1003 # Need to reload repo to invalidate the cache, or otherwise we
998 1004 # cannot retrieve the merge commit.
999 1005 shadow_repo = GitRepository(shadow_repository_path)
1000 1006 merge_commit_id = shadow_repo.branches[pr_branch]
1001 1007
1002 1008 # Set a reference pointing to the merge commit. This reference may
1003 1009 # be used to easily identify the last successful merge commit in
1004 1010 # the shadow repository.
1005 1011 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
1006 1012 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
1007 1013 except RepositoryError:
1008 1014 log.exception('Failure when doing local merge on git shadow repo')
1009 1015 merge_possible = False
1010 1016 merge_failure_reason = MergeFailureReason.MERGE_FAILED
1011 1017
1012 1018 if merge_possible and not dry_run:
1013 1019 try:
1014 1020 shadow_repo._local_push(
1015 1021 pr_branch, self.path, target_ref.name, enable_hooks=True,
1016 1022 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
1017 1023 merge_succeeded = True
1018 1024 except RepositoryError:
1019 1025 log.exception(
1020 1026 'Failure when doing local push from the shadow '
1021 1027 'repository to the target repository at %s.', self.path)
1022 1028 merge_succeeded = False
1023 1029 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1024 1030 metadata['target'] = 'git shadow repo'
1025 1031 metadata['merge_commit'] = pr_branch
1026 1032 else:
1027 1033 merge_succeeded = False
1028 1034
1029 1035 return MergeResponse(
1030 1036 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
1031 1037 metadata=metadata)
@@ -1,98 +1,95 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 HG inmemory module
23 23 """
24 24
25 25 from rhodecode.lib.datelib import date_to_timestamp_plus_offset
26 26 from rhodecode.lib.utils import safe_str
27 27 from rhodecode.lib.vcs.backends.base import BaseInMemoryCommit
28 28 from rhodecode.lib.vcs.exceptions import RepositoryError
29 29
30 30
31 31 class MercurialInMemoryCommit(BaseInMemoryCommit):
32 32
33 def commit(self, message, author, parents=None, branch=None, date=None,
34 **kwargs):
33 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
35 34 """
36 35 Performs in-memory commit (doesn't check workdir in any way) and
37 36 returns newly created `MercurialCommit`. Updates repository's
38 37 `commit_ids`.
39 38
40 39 :param message: message of the commit
41 40 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
42 41 :param parents: single parent or sequence of parents from which commit
43 42 would be derived
44 43 :param date: `datetime.datetime` instance. Defaults to
45 44 ``datetime.datetime.now()``.
46 45 :param branch: Optional. Branch name as unicode. Will use the backend's
47 46 default if not given.
48 47
49 48 :raises `RepositoryError`: if any error occurs while committing
50 49 """
51 50 self.check_integrity(parents)
52 51
53 52 if not isinstance(message, unicode) or not isinstance(author, unicode):
54 53 # TODO: johbo: Should be a TypeError
55 54 raise RepositoryError('Given message and author needs to be '
56 55 'an <unicode> instance got %r & %r instead'
57 56 % (type(message), type(author)))
58 57
59 58 if branch is None:
60 59 branch = self.repository.DEFAULT_BRANCH_NAME
61 60 kwargs['branch'] = safe_str(branch)
62 61
63 62 message = safe_str(message)
64 63 author = safe_str(author)
65 64
66 65 parent_ids = [p.raw_id if p else None for p in self.parents]
67 66
68 67 ENCODING = "UTF-8"
69 68
70 69 updated = []
71 70 for node in self.added + self.changed:
72 71 if node.is_binary:
73 72 content = node.content
74 73 else:
75 74 content = node.content.encode(ENCODING)
76 75 updated.append({
77 76 'path': node.path,
78 77 'content': content,
79 78 'mode': node.mode,
80 79 })
81 80
82 81 removed = [node.path for node in self.removed]
83 82
84 83 date, tz = date_to_timestamp_plus_offset(date)
85 84
86 85 commit_id = self.repository._remote.commitctx(
87 86 message=message, parents=parent_ids,
88 87 commit_time=date, commit_timezone=tz, user=author,
89 88 files=self.get_paths(), extra=kwargs, removed=removed,
90 89 updated=updated)
91 if commit_id not in self.repository.commit_ids:
92 self.repository.commit_ids.append(commit_id)
93 self.repository._rebuild_cache(self.repository.commit_ids)
90 self.repository.append_commit_id(commit_id)
94 91
95 92 self.repository.branches = self.repository._get_branches()
96 tip = self.repository.get_commit()
93 tip = self.repository.get_commit(commit_id)
97 94 self.reset()
98 95 return tip
@@ -1,942 +1,949 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 HG repository module
23 23 """
24 24 import os
25 25 import logging
26 26 import binascii
27 import time
27 28 import urllib
28 29
29 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import CachedProperty
30 32
31 33 from rhodecode.lib.compat import OrderedDict
32 34 from rhodecode.lib.datelib import (
33 35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate)
34 36 from rhodecode.lib.utils import safe_unicode, safe_str
35 37 from rhodecode.lib.vcs import connection, exceptions
36 38 from rhodecode.lib.vcs.backends.base import (
37 39 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 40 MergeFailureReason, Reference, BasePathPermissionChecker)
39 41 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
40 42 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
41 43 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
42 44 from rhodecode.lib.vcs.exceptions import (
43 45 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
44 46 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
45 47 from rhodecode.lib.vcs.compat import configparser
46 48
47 49 hexlify = binascii.hexlify
48 50 nullid = "\0" * 20
49 51
50 52 log = logging.getLogger(__name__)
51 53
52 54
53 55 class MercurialRepository(BaseRepository):
54 56 """
55 57 Mercurial repository backend
56 58 """
57 59 DEFAULT_BRANCH_NAME = 'default'
58 60
59 61 def __init__(self, repo_path, config=None, create=False, src_url=None,
60 62 do_workspace_checkout=False, with_wire=None, bare=False):
61 63 """
62 64 Raises RepositoryError if repository could not be find at the given
63 65 ``repo_path``.
64 66
65 67 :param repo_path: local path of the repository
66 68 :param config: config object containing the repo configuration
67 69 :param create=False: if set to True, would try to create repository if
68 70 it does not exist rather than raising exception
69 71 :param src_url=None: would try to clone repository from given location
70 72 :param do_workspace_checkout=False: sets update of working copy after
71 73 making a clone
72 74 :param bare: not used, compatible with other VCS
73 75 """
74 76
75 77 self.path = safe_str(os.path.abspath(repo_path))
76 78 # mercurial since 4.4.X requires certain configuration to be present
77 79 # because sometimes we init the repos with config we need to meet
78 80 # special requirements
79 81 self.config = config if config else self.get_default_config(
80 82 default=[('extensions', 'largefiles', '1')])
81 83 self.with_wire = with_wire
82 84
83 85 self._init_repo(create, src_url, do_workspace_checkout)
84 86
85 87 # caches
86 88 self._commit_ids = {}
87 89
90 # dependent that trigger re-computation of commit_ids
91 self._commit_ids_ver = 0
92
88 93 @LazyProperty
89 94 def _remote(self):
90 95 return connection.Hg(self.path, self.config, with_wire=self.with_wire)
91 96
92 @LazyProperty
97 @CachedProperty('_commit_ids_ver')
93 98 def commit_ids(self):
94 99 """
95 100 Returns list of commit ids, in ascending order. Being lazy
96 101 attribute allows external tools to inject shas from cache.
97 102 """
98 103 commit_ids = self._get_all_commit_ids()
99 104 self._rebuild_cache(commit_ids)
100 105 return commit_ids
101 106
102 107 def _rebuild_cache(self, commit_ids):
103 108 self._commit_ids = dict((commit_id, index)
104 109 for index, commit_id in enumerate(commit_ids))
105 110
106 111 @LazyProperty
107 112 def branches(self):
108 113 return self._get_branches()
109 114
110 115 @LazyProperty
111 116 def branches_closed(self):
112 117 return self._get_branches(active=False, closed=True)
113 118
114 119 @LazyProperty
115 120 def branches_all(self):
116 121 all_branches = {}
117 122 all_branches.update(self.branches)
118 123 all_branches.update(self.branches_closed)
119 124 return all_branches
120 125
121 126 def _get_branches(self, active=True, closed=False):
122 127 """
123 128 Gets branches for this repository
124 129 Returns only not closed active branches by default
125 130
126 131 :param active: return also active branches
127 132 :param closed: return also closed branches
128 133
129 134 """
130 135 if self.is_empty():
131 136 return {}
132 137
133 138 def get_name(ctx):
134 139 return ctx[0]
135 140
136 141 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
137 142 self._remote.branches(active, closed).items()]
138 143
139 144 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
140 145
141 146 @LazyProperty
142 147 def tags(self):
143 148 """
144 149 Gets tags for this repository
145 150 """
146 151 return self._get_tags()
147 152
148 153 def _get_tags(self):
149 154 if self.is_empty():
150 155 return {}
151 156
152 157 def get_name(ctx):
153 158 return ctx[0]
154 159
155 160 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
156 161 self._remote.tags().items()]
157 162
158 163 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
159 164
160 def tag(self, name, user, commit_id=None, message=None, date=None,
161 **kwargs):
165 def tag(self, name, user, commit_id=None, message=None, date=None, **kwargs):
162 166 """
163 167 Creates and returns a tag for the given ``commit_id``.
164 168
165 169 :param name: name for new tag
166 170 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
167 171 :param commit_id: commit id for which new tag would be created
168 172 :param message: message of the tag's commit
169 173 :param date: date of tag's commit
170 174
171 175 :raises TagAlreadyExistError: if tag with same name already exists
172 176 """
173 177 if name in self.tags:
174 178 raise TagAlreadyExistError("Tag %s already exists" % name)
179
175 180 commit = self.get_commit(commit_id=commit_id)
176 181 local = kwargs.setdefault('local', False)
177 182
178 183 if message is None:
179 184 message = "Added tag %s for commit %s" % (name, commit.short_id)
180 185
181 186 date, tz = date_to_timestamp_plus_offset(date)
182 187
183 self._remote.tag(
184 name, commit.raw_id, message, local, user, date, tz)
188 self._remote.tag(name, commit.raw_id, message, local, user, date, tz)
185 189 self._remote.invalidate_vcs_cache()
186 190
187 191 # Reinitialize tags
188 192 self.tags = self._get_tags()
189 193 tag_id = self.tags[name]
190 194
191 195 return self.get_commit(commit_id=tag_id)
192 196
193 197 def remove_tag(self, name, user, message=None, date=None):
194 198 """
195 199 Removes tag with the given `name`.
196 200
197 201 :param name: name of the tag to be removed
198 202 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
199 203 :param message: message of the tag's removal commit
200 204 :param date: date of tag's removal commit
201 205
202 206 :raises TagDoesNotExistError: if tag with given name does not exists
203 207 """
204 208 if name not in self.tags:
205 209 raise TagDoesNotExistError("Tag %s does not exist" % name)
210
206 211 if message is None:
207 212 message = "Removed tag %s" % name
208 213 local = False
209 214
210 215 date, tz = date_to_timestamp_plus_offset(date)
211 216
212 217 self._remote.tag(name, nullid, message, local, user, date, tz)
213 218 self._remote.invalidate_vcs_cache()
214 219 self.tags = self._get_tags()
215 220
216 221 @LazyProperty
217 222 def bookmarks(self):
218 223 """
219 224 Gets bookmarks for this repository
220 225 """
221 226 return self._get_bookmarks()
222 227
223 228 def _get_bookmarks(self):
224 229 if self.is_empty():
225 230 return {}
226 231
227 232 def get_name(ctx):
228 233 return ctx[0]
229 234
230 235 _bookmarks = [
231 236 (safe_unicode(n), hexlify(h)) for n, h in
232 237 self._remote.bookmarks().items()]
233 238
234 239 return OrderedDict(sorted(_bookmarks, key=get_name))
235 240
236 241 def _get_all_commit_ids(self):
237 242 return self._remote.get_all_commit_ids('visible')
238 243
239 244 def get_diff(
240 245 self, commit1, commit2, path='', ignore_whitespace=False,
241 246 context=3, path1=None):
242 247 """
243 248 Returns (git like) *diff*, as plain text. Shows changes introduced by
244 249 `commit2` since `commit1`.
245 250
246 251 :param commit1: Entry point from which diff is shown. Can be
247 252 ``self.EMPTY_COMMIT`` - in this case, patch showing all
248 253 the changes since empty state of the repository until `commit2`
249 254 :param commit2: Until which commit changes should be shown.
250 255 :param ignore_whitespace: If set to ``True``, would not show whitespace
251 256 changes. Defaults to ``False``.
252 257 :param context: How many lines before/after changed lines should be
253 258 shown. Defaults to ``3``.
254 259 """
255 260 self._validate_diff_commits(commit1, commit2)
256 261 if path1 is not None and path1 != path:
257 262 raise ValueError("Diff of two different paths not supported.")
258 263
259 264 if path:
260 265 file_filter = [self.path, path]
261 266 else:
262 267 file_filter = None
263 268
264 269 diff = self._remote.diff(
265 270 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
266 271 opt_git=True, opt_ignorews=ignore_whitespace,
267 272 context=context)
268 273 return MercurialDiff(diff)
269 274
270 275 def strip(self, commit_id, branch=None):
271 276 self._remote.strip(commit_id, update=False, backup="none")
272 277
273 278 self._remote.invalidate_vcs_cache()
274 self.commit_ids = self._get_all_commit_ids()
275 self._rebuild_cache(self.commit_ids)
279 self._commit_ids_ver = time.time()
280 # we updated _commit_ids_ver so accessing self.commit_ids will re-compute it
281 return len(self.commit_ids)
276 282
277 283 def verify(self):
278 284 verify = self._remote.verify()
279 285
280 286 self._remote.invalidate_vcs_cache()
281 287 return verify
282 288
283 289 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
284 290 if commit_id1 == commit_id2:
285 291 return commit_id1
286 292
287 293 ancestors = self._remote.revs_from_revspec(
288 294 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
289 295 other_path=repo2.path)
290 296 return repo2[ancestors[0]].raw_id if ancestors else None
291 297
292 298 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
293 299 if commit_id1 == commit_id2:
294 300 commits = []
295 301 else:
296 302 if merge:
297 303 indexes = self._remote.revs_from_revspec(
298 304 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
299 305 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
300 306 else:
301 307 indexes = self._remote.revs_from_revspec(
302 308 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
303 309 commit_id1, other_path=repo2.path)
304 310
305 311 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
306 312 for idx in indexes]
307 313
308 314 return commits
309 315
310 316 @staticmethod
311 317 def check_url(url, config):
312 318 """
313 319 Function will check given url and try to verify if it's a valid
314 320 link. Sometimes it may happened that mercurial will issue basic
315 321 auth request that can cause whole API to hang when used from python
316 322 or other external calls.
317 323
318 324 On failures it'll raise urllib2.HTTPError, exception is also thrown
319 325 when the return code is non 200
320 326 """
321 327 # check first if it's not an local url
322 328 if os.path.isdir(url) or url.startswith('file:'):
323 329 return True
324 330
325 331 # Request the _remote to verify the url
326 332 return connection.Hg.check_url(url, config.serialize())
327 333
328 334 @staticmethod
329 335 def is_valid_repository(path):
330 336 return os.path.isdir(os.path.join(path, '.hg'))
331 337
332 338 def _init_repo(self, create, src_url=None, do_workspace_checkout=False):
333 339 """
334 340 Function will check for mercurial repository in given path. If there
335 341 is no repository in that path it will raise an exception unless
336 342 `create` parameter is set to True - in that case repository would
337 343 be created.
338 344
339 345 If `src_url` is given, would try to clone repository from the
340 346 location at given clone_point. Additionally it'll make update to
341 347 working copy accordingly to `do_workspace_checkout` flag.
342 348 """
343 349 if create and os.path.exists(self.path):
344 350 raise RepositoryError(
345 351 "Cannot create repository at %s, location already exist"
346 352 % self.path)
347 353
348 354 if src_url:
349 355 url = str(self._get_url(src_url))
350 356 MercurialRepository.check_url(url, self.config)
351 357
352 358 self._remote.clone(url, self.path, do_workspace_checkout)
353 359
354 360 # Don't try to create if we've already cloned repo
355 361 create = False
356 362
357 363 if create:
358 364 os.makedirs(self.path, mode=0o755)
359 365
360 366 self._remote.localrepository(create)
361 367
362 368 @LazyProperty
363 369 def in_memory_commit(self):
364 370 return MercurialInMemoryCommit(self)
365 371
366 372 @LazyProperty
367 373 def description(self):
368 374 description = self._remote.get_config_value(
369 375 'web', 'description', untrusted=True)
370 376 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
371 377
372 378 @LazyProperty
373 379 def contact(self):
374 380 contact = (
375 381 self._remote.get_config_value("web", "contact") or
376 382 self._remote.get_config_value("ui", "username"))
377 383 return safe_unicode(contact or self.DEFAULT_CONTACT)
378 384
379 385 @LazyProperty
380 386 def last_change(self):
381 387 """
382 388 Returns last change made on this repository as
383 389 `datetime.datetime` object.
384 390 """
385 391 try:
386 392 return self.get_commit().date
387 393 except RepositoryError:
388 394 tzoffset = makedate()[1]
389 395 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
390 396
391 397 def _get_fs_mtime(self):
392 398 # fallback to filesystem
393 399 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
394 400 st_path = os.path.join(self.path, '.hg', "store")
395 401 if os.path.exists(cl_path):
396 402 return os.stat(cl_path).st_mtime
397 403 else:
398 404 return os.stat(st_path).st_mtime
399 405
400 406 def _get_url(self, url):
401 407 """
402 408 Returns normalized url. If schema is not given, would fall
403 409 to filesystem
404 410 (``file:///``) schema.
405 411 """
406 412 url = url.encode('utf8')
407 413 if url != 'default' and '://' not in url:
408 414 url = "file:" + urllib.pathname2url(url)
409 415 return url
410 416
411 417 def get_hook_location(self):
412 418 """
413 419 returns absolute path to location where hooks are stored
414 420 """
415 421 return os.path.join(self.path, '.hg', '.hgrc')
416 422
417 423 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
418 424 """
419 425 Returns ``MercurialCommit`` object representing repository's
420 426 commit at the given `commit_id` or `commit_idx`.
421 427 """
422 428 if self.is_empty():
423 429 raise EmptyRepositoryError("There are no commits yet")
424 430
425 431 if commit_id is not None:
426 432 self._validate_commit_id(commit_id)
427 433 try:
428 434 # we have cached idx, use it without contacting the remote
429 435 idx = self._commit_ids[commit_id]
430 436 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
431 437 except KeyError:
432 438 pass
433 439
434 440 elif commit_idx is not None:
435 441 self._validate_commit_idx(commit_idx)
436 442 try:
437 443 _commit_id = self.commit_ids[commit_idx]
438 444 if commit_idx < 0:
439 445 commit_idx = self.commit_ids.index(_commit_id)
440 446
441 447 return MercurialCommit(self, _commit_id, commit_idx, pre_load=pre_load)
442 448 except IndexError:
443 449 commit_id = commit_idx
444 450 else:
445 451 commit_id = "tip"
446 452
447 453 if isinstance(commit_id, unicode):
448 454 commit_id = safe_str(commit_id)
449 455
450 456 try:
451 457 raw_id, idx = self._remote.lookup(commit_id, both=True)
452 458 except CommitDoesNotExistError:
453 msg = "Commit %s does not exist for %s" % (commit_id, self.name)
459 msg = "Commit {} does not exist for {}".format(
460 *map(safe_str, [commit_id, self.name]))
454 461 raise CommitDoesNotExistError(msg)
455 462
456 463 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
457 464
458 465 def get_commits(
459 466 self, start_id=None, end_id=None, start_date=None, end_date=None,
460 467 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
461 468 """
462 469 Returns generator of ``MercurialCommit`` objects from start to end
463 470 (both are inclusive)
464 471
465 472 :param start_id: None, str(commit_id)
466 473 :param end_id: None, str(commit_id)
467 474 :param start_date: if specified, commits with commit date less than
468 475 ``start_date`` would be filtered out from returned set
469 476 :param end_date: if specified, commits with commit date greater than
470 477 ``end_date`` would be filtered out from returned set
471 478 :param branch_name: if specified, commits not reachable from given
472 479 branch would be filtered out from returned set
473 480 :param show_hidden: Show hidden commits such as obsolete or hidden from
474 481 Mercurial evolve
475 482 :raise BranchDoesNotExistError: If given ``branch_name`` does not
476 483 exist.
477 484 :raise CommitDoesNotExistError: If commit for given ``start`` or
478 485 ``end`` could not be found.
479 486 """
480 487 # actually we should check now if it's not an empty repo
481 488 if self.is_empty():
482 489 raise EmptyRepositoryError("There are no commits yet")
483 490 self._validate_branch_name(branch_name)
484 491
485 492 branch_ancestors = False
486 493 if start_id is not None:
487 494 self._validate_commit_id(start_id)
488 495 c_start = self.get_commit(commit_id=start_id)
489 496 start_pos = self._commit_ids[c_start.raw_id]
490 497 else:
491 498 start_pos = None
492 499
493 500 if end_id is not None:
494 501 self._validate_commit_id(end_id)
495 502 c_end = self.get_commit(commit_id=end_id)
496 503 end_pos = max(0, self._commit_ids[c_end.raw_id])
497 504 else:
498 505 end_pos = None
499 506
500 507 if None not in [start_id, end_id] and start_pos > end_pos:
501 508 raise RepositoryError(
502 509 "Start commit '%s' cannot be after end commit '%s'" %
503 510 (start_id, end_id))
504 511
505 512 if end_pos is not None:
506 513 end_pos += 1
507 514
508 515 commit_filter = []
509 516
510 517 if branch_name and not branch_ancestors:
511 518 commit_filter.append('branch("%s")' % (branch_name,))
512 519 elif branch_name and branch_ancestors:
513 520 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
514 521
515 522 if start_date and not end_date:
516 523 commit_filter.append('date(">%s")' % (start_date,))
517 524 if end_date and not start_date:
518 525 commit_filter.append('date("<%s")' % (end_date,))
519 526 if start_date and end_date:
520 527 commit_filter.append(
521 528 'date(">%s") and date("<%s")' % (start_date, end_date))
522 529
523 530 if not show_hidden:
524 531 commit_filter.append('not obsolete()')
525 532 commit_filter.append('not hidden()')
526 533
527 534 # TODO: johbo: Figure out a simpler way for this solution
528 535 collection_generator = CollectionGenerator
529 536 if commit_filter:
530 537 commit_filter = ' and '.join(map(safe_str, commit_filter))
531 538 revisions = self._remote.rev_range([commit_filter])
532 539 collection_generator = MercurialIndexBasedCollectionGenerator
533 540 else:
534 541 revisions = self.commit_ids
535 542
536 543 if start_pos or end_pos:
537 544 revisions = revisions[start_pos:end_pos]
538 545
539 546 return collection_generator(self, revisions, pre_load=pre_load)
540 547
541 548 def pull(self, url, commit_ids=None):
542 549 """
543 550 Pull changes from external location.
544 551
545 552 :param commit_ids: Optional. Can be set to a list of commit ids
546 553 which shall be pulled from the other repository.
547 554 """
548 555 url = self._get_url(url)
549 556 self._remote.pull(url, commit_ids=commit_ids)
550 557 self._remote.invalidate_vcs_cache()
551 558
552 559 def fetch(self, url, commit_ids=None):
553 560 """
554 561 Backward compatibility with GIT fetch==pull
555 562 """
556 563 return self.pull(url, commit_ids=commit_ids)
557 564
558 565 def push(self, url):
559 566 url = self._get_url(url)
560 567 self._remote.sync_push(url)
561 568
562 569 def _local_clone(self, clone_path):
563 570 """
564 571 Create a local clone of the current repo.
565 572 """
566 573 self._remote.clone(self.path, clone_path, update_after_clone=True,
567 574 hooks=False)
568 575
569 576 def _update(self, revision, clean=False):
570 577 """
571 578 Update the working copy to the specified revision.
572 579 """
573 580 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
574 581 self._remote.update(revision, clean=clean)
575 582
576 583 def _identify(self):
577 584 """
578 585 Return the current state of the working directory.
579 586 """
580 587 return self._remote.identify().strip().rstrip('+')
581 588
582 589 def _heads(self, branch=None):
583 590 """
584 591 Return the commit ids of the repository heads.
585 592 """
586 593 return self._remote.heads(branch=branch).strip().split(' ')
587 594
588 595 def _ancestor(self, revision1, revision2):
589 596 """
590 597 Return the common ancestor of the two revisions.
591 598 """
592 599 return self._remote.ancestor(revision1, revision2)
593 600
594 601 def _local_push(
595 602 self, revision, repository_path, push_branches=False,
596 603 enable_hooks=False):
597 604 """
598 605 Push the given revision to the specified repository.
599 606
600 607 :param push_branches: allow to create branches in the target repo.
601 608 """
602 609 self._remote.push(
603 610 [revision], repository_path, hooks=enable_hooks,
604 611 push_branches=push_branches)
605 612
606 613 def _local_merge(self, target_ref, merge_message, user_name, user_email,
607 614 source_ref, use_rebase=False, dry_run=False):
608 615 """
609 616 Merge the given source_revision into the checked out revision.
610 617
611 618 Returns the commit id of the merge and a boolean indicating if the
612 619 commit needs to be pushed.
613 620 """
614 621 self._update(target_ref.commit_id, clean=True)
615 622
616 623 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
617 624 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
618 625
619 626 if ancestor == source_ref.commit_id:
620 627 # Nothing to do, the changes were already integrated
621 628 return target_ref.commit_id, False
622 629
623 630 elif ancestor == target_ref.commit_id and is_the_same_branch:
624 631 # In this case we should force a commit message
625 632 return source_ref.commit_id, True
626 633
627 634 if use_rebase:
628 635 try:
629 636 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
630 637 target_ref.commit_id)
631 638 self.bookmark(bookmark_name, revision=source_ref.commit_id)
632 639 self._remote.rebase(
633 640 source=source_ref.commit_id, dest=target_ref.commit_id)
634 641 self._remote.invalidate_vcs_cache()
635 642 self._update(bookmark_name, clean=True)
636 643 return self._identify(), True
637 644 except RepositoryError:
638 645 # The rebase-abort may raise another exception which 'hides'
639 646 # the original one, therefore we log it here.
640 647 log.exception('Error while rebasing shadow repo during merge.')
641 648
642 649 # Cleanup any rebase leftovers
643 650 self._remote.invalidate_vcs_cache()
644 651 self._remote.rebase(abort=True)
645 652 self._remote.invalidate_vcs_cache()
646 653 self._remote.update(clean=True)
647 654 raise
648 655 else:
649 656 try:
650 657 self._remote.merge(source_ref.commit_id)
651 658 self._remote.invalidate_vcs_cache()
652 659 self._remote.commit(
653 660 message=safe_str(merge_message),
654 661 username=safe_str('%s <%s>' % (user_name, user_email)))
655 662 self._remote.invalidate_vcs_cache()
656 663 return self._identify(), True
657 664 except RepositoryError:
658 665 # Cleanup any merge leftovers
659 666 self._remote.update(clean=True)
660 667 raise
661 668
662 669 def _local_close(self, target_ref, user_name, user_email,
663 670 source_ref, close_message=''):
664 671 """
665 672 Close the branch of the given source_revision
666 673
667 674 Returns the commit id of the close and a boolean indicating if the
668 675 commit needs to be pushed.
669 676 """
670 677 self._update(source_ref.commit_id)
671 678 message = close_message or "Closing branch: `{}`".format(source_ref.name)
672 679 try:
673 680 self._remote.commit(
674 681 message=safe_str(message),
675 682 username=safe_str('%s <%s>' % (user_name, user_email)),
676 683 close_branch=True)
677 684 self._remote.invalidate_vcs_cache()
678 685 return self._identify(), True
679 686 except RepositoryError:
680 687 # Cleanup any commit leftovers
681 688 self._remote.update(clean=True)
682 689 raise
683 690
684 691 def _is_the_same_branch(self, target_ref, source_ref):
685 692 return (
686 693 self._get_branch_name(target_ref) ==
687 694 self._get_branch_name(source_ref))
688 695
689 696 def _get_branch_name(self, ref):
690 697 if ref.type == 'branch':
691 698 return ref.name
692 699 return self._remote.ctx_branch(ref.commit_id)
693 700
694 701 def _maybe_prepare_merge_workspace(
695 702 self, repo_id, workspace_id, unused_target_ref, unused_source_ref):
696 703 shadow_repository_path = self._get_shadow_repository_path(
697 704 repo_id, workspace_id)
698 705 if not os.path.exists(shadow_repository_path):
699 706 self._local_clone(shadow_repository_path)
700 707 log.debug(
701 708 'Prepared shadow repository in %s', shadow_repository_path)
702 709
703 710 return shadow_repository_path
704 711
705 712 def _merge_repo(self, repo_id, workspace_id, target_ref,
706 713 source_repo, source_ref, merge_message,
707 714 merger_name, merger_email, dry_run=False,
708 715 use_rebase=False, close_branch=False):
709 716
710 717 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
711 718 'rebase' if use_rebase else 'merge', dry_run)
712 719 if target_ref.commit_id not in self._heads():
713 720 return MergeResponse(
714 721 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
715 722 metadata={'target_ref': target_ref})
716 723
717 724 try:
718 725 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
719 726 heads = '\n,'.join(self._heads(target_ref.name))
720 727 metadata = {
721 728 'target_ref': target_ref,
722 729 'source_ref': source_ref,
723 730 'heads': heads
724 731 }
725 732 return MergeResponse(
726 733 False, False, None,
727 734 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
728 735 metadata=metadata)
729 736 except CommitDoesNotExistError:
730 737 log.exception('Failure when looking up branch heads on hg target')
731 738 return MergeResponse(
732 739 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
733 740 metadata={'target_ref': target_ref})
734 741
735 742 shadow_repository_path = self._maybe_prepare_merge_workspace(
736 743 repo_id, workspace_id, target_ref, source_ref)
737 744 shadow_repo = self._get_shadow_instance(shadow_repository_path)
738 745
739 746 log.debug('Pulling in target reference %s', target_ref)
740 747 self._validate_pull_reference(target_ref)
741 748 shadow_repo._local_pull(self.path, target_ref)
742 749
743 750 try:
744 751 log.debug('Pulling in source reference %s', source_ref)
745 752 source_repo._validate_pull_reference(source_ref)
746 753 shadow_repo._local_pull(source_repo.path, source_ref)
747 754 except CommitDoesNotExistError:
748 755 log.exception('Failure when doing local pull on hg shadow repo')
749 756 return MergeResponse(
750 757 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
751 758 metadata={'source_ref': source_ref})
752 759
753 760 merge_ref = None
754 761 merge_commit_id = None
755 762 close_commit_id = None
756 763 merge_failure_reason = MergeFailureReason.NONE
757 764 metadata = {}
758 765
759 766 # enforce that close branch should be used only in case we source from
760 767 # an actual Branch
761 768 close_branch = close_branch and source_ref.type == 'branch'
762 769
763 770 # don't allow to close branch if source and target are the same
764 771 close_branch = close_branch and source_ref.name != target_ref.name
765 772
766 773 needs_push_on_close = False
767 774 if close_branch and not use_rebase and not dry_run:
768 775 try:
769 776 close_commit_id, needs_push_on_close = shadow_repo._local_close(
770 777 target_ref, merger_name, merger_email, source_ref)
771 778 merge_possible = True
772 779 except RepositoryError:
773 780 log.exception('Failure when doing close branch on '
774 781 'shadow repo: %s', shadow_repo)
775 782 merge_possible = False
776 783 merge_failure_reason = MergeFailureReason.MERGE_FAILED
777 784 else:
778 785 merge_possible = True
779 786
780 787 needs_push = False
781 788 if merge_possible:
782 789 try:
783 790 merge_commit_id, needs_push = shadow_repo._local_merge(
784 791 target_ref, merge_message, merger_name, merger_email,
785 792 source_ref, use_rebase=use_rebase, dry_run=dry_run)
786 793 merge_possible = True
787 794
788 795 # read the state of the close action, if it
789 796 # maybe required a push
790 797 needs_push = needs_push or needs_push_on_close
791 798
792 799 # Set a bookmark pointing to the merge commit. This bookmark
793 800 # may be used to easily identify the last successful merge
794 801 # commit in the shadow repository.
795 802 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
796 803 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
797 804 except SubrepoMergeError:
798 805 log.exception(
799 806 'Subrepo merge error during local merge on hg shadow repo.')
800 807 merge_possible = False
801 808 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
802 809 needs_push = False
803 810 except RepositoryError:
804 811 log.exception('Failure when doing local merge on hg shadow repo')
805 812 merge_possible = False
806 813 merge_failure_reason = MergeFailureReason.MERGE_FAILED
807 814 needs_push = False
808 815
809 816 if merge_possible and not dry_run:
810 817 if needs_push:
811 818 # In case the target is a bookmark, update it, so after pushing
812 819 # the bookmarks is also updated in the target.
813 820 if target_ref.type == 'book':
814 821 shadow_repo.bookmark(
815 822 target_ref.name, revision=merge_commit_id)
816 823 try:
817 824 shadow_repo_with_hooks = self._get_shadow_instance(
818 825 shadow_repository_path,
819 826 enable_hooks=True)
820 827 # This is the actual merge action, we push from shadow
821 828 # into origin.
822 829 # Note: the push_branches option will push any new branch
823 830 # defined in the source repository to the target. This may
824 831 # be dangerous as branches are permanent in Mercurial.
825 832 # This feature was requested in issue #441.
826 833 shadow_repo_with_hooks._local_push(
827 834 merge_commit_id, self.path, push_branches=True,
828 835 enable_hooks=True)
829 836
830 837 # maybe we also need to push the close_commit_id
831 838 if close_commit_id:
832 839 shadow_repo_with_hooks._local_push(
833 840 close_commit_id, self.path, push_branches=True,
834 841 enable_hooks=True)
835 842 merge_succeeded = True
836 843 except RepositoryError:
837 844 log.exception(
838 845 'Failure when doing local push from the shadow '
839 846 'repository to the target repository at %s.', self.path)
840 847 merge_succeeded = False
841 848 merge_failure_reason = MergeFailureReason.PUSH_FAILED
842 849 metadata['target'] = 'hg shadow repo'
843 850 metadata['merge_commit'] = merge_commit_id
844 851 else:
845 852 merge_succeeded = True
846 853 else:
847 854 merge_succeeded = False
848 855
849 856 return MergeResponse(
850 857 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
851 858 metadata=metadata)
852 859
853 860 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
854 861 config = self.config.copy()
855 862 if not enable_hooks:
856 863 config.clear_section('hooks')
857 864 return MercurialRepository(shadow_repository_path, config)
858 865
859 866 def _validate_pull_reference(self, reference):
860 867 if not (reference.name in self.bookmarks or
861 868 reference.name in self.branches or
862 869 self.get_commit(reference.commit_id)):
863 870 raise CommitDoesNotExistError(
864 871 'Unknown branch, bookmark or commit id')
865 872
866 873 def _local_pull(self, repository_path, reference):
867 874 """
868 875 Fetch a branch, bookmark or commit from a local repository.
869 876 """
870 877 repository_path = os.path.abspath(repository_path)
871 878 if repository_path == self.path:
872 879 raise ValueError('Cannot pull from the same repository')
873 880
874 881 reference_type_to_option_name = {
875 882 'book': 'bookmark',
876 883 'branch': 'branch',
877 884 }
878 885 option_name = reference_type_to_option_name.get(
879 886 reference.type, 'revision')
880 887
881 888 if option_name == 'revision':
882 889 ref = reference.commit_id
883 890 else:
884 891 ref = reference.name
885 892
886 893 options = {option_name: [ref]}
887 894 self._remote.pull_cmd(repository_path, hooks=False, **options)
888 895 self._remote.invalidate_vcs_cache()
889 896
890 897 def bookmark(self, bookmark, revision=None):
891 898 if isinstance(bookmark, unicode):
892 899 bookmark = safe_str(bookmark)
893 900 self._remote.bookmark(bookmark, revision=revision)
894 901 self._remote.invalidate_vcs_cache()
895 902
896 903 def get_path_permissions(self, username):
897 904 hgacl_file = os.path.join(self.path, '.hg/hgacl')
898 905
899 906 def read_patterns(suffix):
900 907 svalue = None
901 908 for section, option in [
902 909 ('narrowacl', username + suffix),
903 910 ('narrowacl', 'default' + suffix),
904 911 ('narrowhgacl', username + suffix),
905 912 ('narrowhgacl', 'default' + suffix)
906 913 ]:
907 914 try:
908 915 svalue = hgacl.get(section, option)
909 916 break # stop at the first value we find
910 917 except configparser.NoOptionError:
911 918 pass
912 919 if not svalue:
913 920 return None
914 921 result = ['/']
915 922 for pattern in svalue.split():
916 923 result.append(pattern)
917 924 if '*' not in pattern and '?' not in pattern:
918 925 result.append(pattern + '/*')
919 926 return result
920 927
921 928 if os.path.exists(hgacl_file):
922 929 try:
923 930 hgacl = configparser.RawConfigParser()
924 931 hgacl.read(hgacl_file)
925 932
926 933 includes = read_patterns('.includes')
927 934 excludes = read_patterns('.excludes')
928 935 return BasePathPermissionChecker.create_from_patterns(
929 936 includes, excludes)
930 937 except BaseException as e:
931 938 msg = 'Cannot read ACL settings from {} on {}: {}'.format(
932 939 hgacl_file, self.name, e)
933 940 raise exceptions.RepositoryRequirementError(msg)
934 941 else:
935 942 return None
936 943
937 944
938 945 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
939 946
940 947 def _commit_factory(self, commit_id):
941 948 return self.repo.get_commit(
942 949 commit_idx=commit_id, pre_load=self.pre_load)
@@ -1,81 +1,79 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 """
23 23 SVN inmemory module
24 24 """
25 25
26 26 from rhodecode.lib.datelib import date_astimestamp
27 27 from rhodecode.lib.utils import safe_str
28 28 from rhodecode.lib.vcs.backends import base
29 29
30 30
31 31 class SubversionInMemoryCommit(base.BaseInMemoryCommit):
32 32
33 def commit(self, message, author, parents=None, branch=None, date=None,
34 **kwargs):
33 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
35 34 if branch not in (None, self.repository.DEFAULT_BRANCH_NAME):
36 35 raise NotImplementedError("Branches are not yet supported")
37 36
38 37 self.check_integrity(parents)
39 38
40 39 message = safe_str(message)
41 40 author = safe_str(author)
42 41
43 42 updated = []
44 43 for node in self.added:
45 44 node_data = {
46 45 'path': node.path,
47 46 'content': safe_str(node.content),
48 47 'mode': node.mode,
49 48 }
50 49 if node.is_binary:
51 50 node_data['properties'] = {
52 51 'svn:mime-type': 'application/octet-stream'
53 52 }
54 53 updated.append(node_data)
55 54 for node in self.changed:
56 55 updated.append({
57 56 'path': node.path,
58 57 'content': safe_str(node.content),
59 58 'mode': node.mode,
60 59 })
61 60
62 61 removed = []
63 62 for node in self.removed:
64 63 removed.append({
65 64 'path': node.path,
66 65 })
67 66
68 67 timestamp = date_astimestamp(date) if date else None
69 68 svn_rev = self.repository._remote.commit(
70 69 message=message, author=author, timestamp=timestamp,
71 70 updated=updated, removed=removed)
72 71
73 72 # TODO: Find a nicer way. If commit_ids is not yet evaluated, then
74 73 # we should not add the commit_id, if it is already evaluated, it
75 74 # will not be evaluated again.
76 75 commit_id = str(svn_rev)
77 if commit_id not in self.repository.commit_ids:
78 self.repository.commit_ids.append(commit_id)
76 self.repository.append_commit_id(commit_id)
79 77 tip = self.repository.get_commit()
80 78 self.reset()
81 79 return tip
@@ -1,360 +1,367 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 repository module
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import urllib
28 28
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 from zope.cachedescriptors.property import CachedProperty
30 31
31 32 from rhodecode.lib.compat import OrderedDict
32 33 from rhodecode.lib.datelib import date_astimestamp
33 34 from rhodecode.lib.utils import safe_str, safe_unicode
34 35 from rhodecode.lib.vcs import connection, path as vcspath
35 36 from rhodecode.lib.vcs.backends import base
36 37 from rhodecode.lib.vcs.backends.svn.commit import (
37 38 SubversionCommit, _date_from_svn_properties)
38 39 from rhodecode.lib.vcs.backends.svn.diff import SubversionDiff
39 40 from rhodecode.lib.vcs.backends.svn.inmemory import SubversionInMemoryCommit
40 41 from rhodecode.lib.vcs.conf import settings
41 42 from rhodecode.lib.vcs.exceptions import (
42 43 CommitDoesNotExistError, EmptyRepositoryError, RepositoryError,
43 44 VCSError, NodeDoesNotExistError)
44 45
45 46
46 47 log = logging.getLogger(__name__)
47 48
48 49
49 50 class SubversionRepository(base.BaseRepository):
50 51 """
51 52 Subversion backend implementation
52 53
53 54 .. important::
54 55
55 56 It is very important to distinguish the commit index and the commit id
56 57 which is assigned by Subversion. The first one is always handled as an
57 58 `int` by this implementation. The commit id assigned by Subversion on
58 59 the other side will always be a `str`.
59 60
60 61 There is a specific trap since the first commit will have the index
61 62 ``0`` but the svn id will be ``"1"``.
62 63
63 64 """
64 65
65 66 # Note: Subversion does not really have a default branch name.
66 67 DEFAULT_BRANCH_NAME = None
67 68
68 69 contact = base.BaseRepository.DEFAULT_CONTACT
69 70 description = base.BaseRepository.DEFAULT_DESCRIPTION
70 71
71 72 def __init__(self, repo_path, config=None, create=False, src_url=None, bare=False,
72 73 **kwargs):
73 74 self.path = safe_str(os.path.abspath(repo_path))
74 75 self.config = config if config else self.get_default_config()
75 76
76 77 self._init_repo(create, src_url)
77 78
79 # dependent that trigger re-computation of commit_ids
80 self._commit_ids_ver = 0
81
78 82 @LazyProperty
79 83 def _remote(self):
80 84 return connection.Svn(self.path, self.config)
81 85
82 86 def _init_repo(self, create, src_url):
83 87 if create and os.path.exists(self.path):
84 88 raise RepositoryError(
85 89 "Cannot create repository at %s, location already exist"
86 90 % self.path)
87 91
88 92 if create:
89 93 self._remote.create_repository(settings.SVN_COMPATIBLE_VERSION)
90 94 if src_url:
91 95 src_url = _sanitize_url(src_url)
92 96 self._remote.import_remote_repository(src_url)
93 97 else:
94 98 self._check_path()
95 99
96 @LazyProperty
100 @CachedProperty('_commit_ids_ver')
97 101 def commit_ids(self):
98 102 head = self._remote.lookup(None)
99 103 return [str(r) for r in xrange(1, head + 1)]
100 104
105 def _rebuild_cache(self, commit_ids):
106 pass
107
101 108 def run_svn_command(self, cmd, **opts):
102 109 """
103 110 Runs given ``cmd`` as svn command and returns tuple
104 111 (stdout, stderr).
105 112
106 113 :param cmd: full svn command to be executed
107 114 :param opts: env options to pass into Subprocess command
108 115 """
109 116 if not isinstance(cmd, list):
110 117 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
111 118
112 119 skip_stderr_log = opts.pop('skip_stderr_log', False)
113 120 out, err = self._remote.run_svn_command(cmd, **opts)
114 121 if err and not skip_stderr_log:
115 122 log.debug('Stderr output of svn command "%s":\n%s', cmd, err)
116 123 return out, err
117 124
118 125 @LazyProperty
119 126 def branches(self):
120 127 return self._tags_or_branches('vcs_svn_branch')
121 128
122 129 @LazyProperty
123 130 def branches_closed(self):
124 131 return {}
125 132
126 133 @LazyProperty
127 134 def bookmarks(self):
128 135 return {}
129 136
130 137 @LazyProperty
131 138 def branches_all(self):
132 139 # TODO: johbo: Implement proper branch support
133 140 all_branches = {}
134 141 all_branches.update(self.branches)
135 142 all_branches.update(self.branches_closed)
136 143 return all_branches
137 144
138 145 @LazyProperty
139 146 def tags(self):
140 147 return self._tags_or_branches('vcs_svn_tag')
141 148
142 149 def _tags_or_branches(self, config_section):
143 150 found_items = {}
144 151
145 152 if self.is_empty():
146 153 return {}
147 154
148 155 for pattern in self._patterns_from_section(config_section):
149 156 pattern = vcspath.sanitize(pattern)
150 157 tip = self.get_commit()
151 158 try:
152 159 if pattern.endswith('*'):
153 160 basedir = tip.get_node(vcspath.dirname(pattern))
154 161 directories = basedir.dirs
155 162 else:
156 163 directories = (tip.get_node(pattern), )
157 164 except NodeDoesNotExistError:
158 165 continue
159 166 found_items.update(
160 167 (safe_unicode(n.path),
161 168 self.commit_ids[-1])
162 169 for n in directories)
163 170
164 171 def get_name(item):
165 172 return item[0]
166 173
167 174 return OrderedDict(sorted(found_items.items(), key=get_name))
168 175
169 176 def _patterns_from_section(self, section):
170 177 return (pattern for key, pattern in self.config.items(section))
171 178
172 179 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
173 180 if self != repo2:
174 181 raise ValueError(
175 182 "Subversion does not support getting common ancestor of"
176 183 " different repositories.")
177 184
178 185 if int(commit_id1) < int(commit_id2):
179 186 return commit_id1
180 187 return commit_id2
181 188
182 189 def verify(self):
183 190 verify = self._remote.verify()
184 191
185 192 self._remote.invalidate_vcs_cache()
186 193 return verify
187 194
188 195 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
189 196 # TODO: johbo: Implement better comparison, this is a very naive
190 197 # version which does not allow to compare branches, tags or folders
191 198 # at all.
192 199 if repo2 != self:
193 200 raise ValueError(
194 201 "Subversion does not support comparison of of different "
195 202 "repositories.")
196 203
197 204 if commit_id1 == commit_id2:
198 205 return []
199 206
200 207 commit_idx1 = self._get_commit_idx(commit_id1)
201 208 commit_idx2 = self._get_commit_idx(commit_id2)
202 209
203 210 commits = [
204 211 self.get_commit(commit_idx=idx)
205 212 for idx in range(commit_idx1 + 1, commit_idx2 + 1)]
206 213
207 214 return commits
208 215
209 216 def _get_commit_idx(self, commit_id):
210 217 try:
211 218 svn_rev = int(commit_id)
212 219 except:
213 220 # TODO: johbo: this might be only one case, HEAD, check this
214 221 svn_rev = self._remote.lookup(commit_id)
215 222 commit_idx = svn_rev - 1
216 223 if commit_idx >= len(self.commit_ids):
217 224 raise CommitDoesNotExistError(
218 225 "Commit at index %s does not exist." % (commit_idx, ))
219 226 return commit_idx
220 227
221 228 @staticmethod
222 229 def check_url(url, config):
223 230 """
224 231 Check if `url` is a valid source to import a Subversion repository.
225 232 """
226 233 # convert to URL if it's a local directory
227 234 if os.path.isdir(url):
228 235 url = 'file://' + urllib.pathname2url(url)
229 236 return connection.Svn.check_url(url, config.serialize())
230 237
231 238 @staticmethod
232 239 def is_valid_repository(path):
233 240 try:
234 241 SubversionRepository(path)
235 242 return True
236 243 except VCSError:
237 244 pass
238 245 return False
239 246
240 247 def _check_path(self):
241 248 if not os.path.exists(self.path):
242 249 raise VCSError('Path "%s" does not exist!' % (self.path, ))
243 250 if not self._remote.is_path_valid_repository(self.path):
244 251 raise VCSError(
245 252 'Path "%s" does not contain a Subversion repository' %
246 253 (self.path, ))
247 254
248 255 @LazyProperty
249 256 def last_change(self):
250 257 """
251 258 Returns last change made on this repository as
252 259 `datetime.datetime` object.
253 260 """
254 261 # Subversion always has a first commit which has id "0" and contains
255 262 # what we are looking for.
256 263 last_id = len(self.commit_ids)
257 264 properties = self._remote.revision_properties(last_id)
258 265 return _date_from_svn_properties(properties)
259 266
260 267 @LazyProperty
261 268 def in_memory_commit(self):
262 269 return SubversionInMemoryCommit(self)
263 270
264 271 def get_hook_location(self):
265 272 """
266 273 returns absolute path to location where hooks are stored
267 274 """
268 275 return os.path.join(self.path, 'hooks')
269 276
270 277 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
271 278 if self.is_empty():
272 279 raise EmptyRepositoryError("There are no commits yet")
273 280 if commit_id is not None:
274 281 self._validate_commit_id(commit_id)
275 282 elif commit_idx is not None:
276 283 self._validate_commit_idx(commit_idx)
277 284 try:
278 285 commit_id = self.commit_ids[commit_idx]
279 286 except IndexError:
280 287 raise CommitDoesNotExistError('No commit with idx: {}'.format(commit_idx))
281 288
282 289 commit_id = self._sanitize_commit_id(commit_id)
283 290 commit = SubversionCommit(repository=self, commit_id=commit_id)
284 291 return commit
285 292
286 293 def get_commits(
287 294 self, start_id=None, end_id=None, start_date=None, end_date=None,
288 295 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
289 296 if self.is_empty():
290 297 raise EmptyRepositoryError("There are no commit_ids yet")
291 298 self._validate_branch_name(branch_name)
292 299
293 300 if start_id is not None:
294 301 self._validate_commit_id(start_id)
295 302 if end_id is not None:
296 303 self._validate_commit_id(end_id)
297 304
298 305 start_raw_id = self._sanitize_commit_id(start_id)
299 306 start_pos = self.commit_ids.index(start_raw_id) if start_id else None
300 307 end_raw_id = self._sanitize_commit_id(end_id)
301 308 end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None
302 309
303 310 if None not in [start_id, end_id] and start_pos > end_pos:
304 311 raise RepositoryError(
305 312 "Start commit '%s' cannot be after end commit '%s'" %
306 313 (start_id, end_id))
307 314 if end_pos is not None:
308 315 end_pos += 1
309 316
310 317 # Date based filtering
311 318 if start_date or end_date:
312 319 start_raw_id, end_raw_id = self._remote.lookup_interval(
313 320 date_astimestamp(start_date) if start_date else None,
314 321 date_astimestamp(end_date) if end_date else None)
315 322 start_pos = start_raw_id - 1
316 323 end_pos = end_raw_id
317 324
318 325 commit_ids = self.commit_ids
319 326
320 327 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
321 328 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
322 329 svn_rev = long(self.commit_ids[-1])
323 330 commit_ids = self._remote.node_history(
324 331 path=branch_name, revision=svn_rev, limit=None)
325 332 commit_ids = [str(i) for i in reversed(commit_ids)]
326 333
327 334 if start_pos or end_pos:
328 335 commit_ids = commit_ids[start_pos:end_pos]
329 336 return base.CollectionGenerator(self, commit_ids, pre_load=pre_load)
330 337
331 338 def _sanitize_commit_id(self, commit_id):
332 339 if commit_id and commit_id.isdigit():
333 340 if int(commit_id) <= len(self.commit_ids):
334 341 return commit_id
335 342 else:
336 343 raise CommitDoesNotExistError(
337 344 "Commit %s does not exist." % (commit_id, ))
338 345 if commit_id not in [
339 346 None, 'HEAD', 'tip', self.DEFAULT_BRANCH_NAME]:
340 347 raise CommitDoesNotExistError(
341 348 "Commit id %s not understood." % (commit_id, ))
342 349 svn_rev = self._remote.lookup('HEAD')
343 350 return str(svn_rev)
344 351
345 352 def get_diff(
346 353 self, commit1, commit2, path=None, ignore_whitespace=False,
347 354 context=3, path1=None):
348 355 self._validate_diff_commits(commit1, commit2)
349 356 svn_rev1 = long(commit1.raw_id)
350 357 svn_rev2 = long(commit2.raw_id)
351 358 diff = self._remote.diff(
352 359 svn_rev1, svn_rev2, path1=path1, path2=path,
353 360 ignore_whitespace=ignore_whitespace, context=context)
354 361 return SubversionDiff(diff)
355 362
356 363
357 364 def _sanitize_url(url):
358 365 if '://' not in url:
359 366 url = 'file://' + urllib.pathname2url(url)
360 367 return url
@@ -1,151 +1,151 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 import datetime
22 22 import os
23 23 import shutil
24 24 import tarfile
25 25 import tempfile
26 26 import zipfile
27 27 import StringIO
28 28
29 29 import mock
30 30 import pytest
31 31
32 32 from rhodecode.lib.vcs.backends import base
33 33 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError, VCSError
34 34 from rhodecode.lib.vcs.nodes import FileNode
35 35 from rhodecode.tests.vcs.conftest import BackendTestMixin
36 36
37 37
38 38 @pytest.mark.usefixtures("vcs_repository_support")
39 39 class TestArchives(BackendTestMixin):
40 40
41 41 @pytest.fixture(autouse=True)
42 42 def tempfile(self, request):
43 43 self.temp_file = tempfile.mkstemp()[1]
44 44
45 45 @request.addfinalizer
46 46 def cleanup():
47 47 os.remove(self.temp_file)
48 48
49 49 @classmethod
50 50 def _get_commits(cls):
51 51 start_date = datetime.datetime(2010, 1, 1, 20)
52 52 for x in range(5):
53 53 yield {
54 54 'message': 'Commit %d' % x,
55 55 'author': 'Joe Doe <joe.doe@example.com>',
56 56 'date': start_date + datetime.timedelta(hours=12 * x),
57 57 'added': [
58 58 FileNode(
59 59 '%d/file_%d.txt' % (x, x), content='Foobar %d' % x),
60 60 ],
61 61 }
62 62
63 63 @pytest.mark.parametrize('compressor', ['gz', 'bz2'])
64 64 def test_archive_tar(self, compressor):
65 65 self.tip.archive_repo(
66 66 self.temp_file, kind='t' + compressor, prefix='repo')
67 67 out_dir = tempfile.mkdtemp()
68 68 out_file = tarfile.open(self.temp_file, 'r|' + compressor)
69 69 out_file.extractall(out_dir)
70 70 out_file.close()
71 71
72 72 for x in range(5):
73 73 node_path = '%d/file_%d.txt' % (x, x)
74 74 with open(os.path.join(out_dir, 'repo/' + node_path)) as f:
75 75 file_content = f.read()
76 76 assert file_content == self.tip.get_node(node_path).content
77 77
78 78 shutil.rmtree(out_dir)
79 79
80 80 def test_archive_zip(self):
81 81 self.tip.archive_repo(self.temp_file, kind='zip', prefix='repo')
82 82 out = zipfile.ZipFile(self.temp_file)
83 83
84 84 for x in range(5):
85 85 node_path = '%d/file_%d.txt' % (x, x)
86 86 decompressed = StringIO.StringIO()
87 87 decompressed.write(out.read('repo/' + node_path))
88 88 assert decompressed.getvalue() == \
89 89 self.tip.get_node(node_path).content
90 90 decompressed.close()
91 91
92 92 def test_archive_zip_with_metadata(self):
93 93 self.tip.archive_repo(self.temp_file, kind='zip',
94 94 prefix='repo', write_metadata=True)
95 95
96 96 out = zipfile.ZipFile(self.temp_file)
97 97 metafile = out.read('.archival.txt')
98 98
99 99 raw_id = self.tip.raw_id
100 assert 'rev:%s' % raw_id in metafile
100 assert 'commit_id:%s' % raw_id in metafile
101 101
102 102 for x in range(5):
103 103 node_path = '%d/file_%d.txt' % (x, x)
104 104 decompressed = StringIO.StringIO()
105 105 decompressed.write(out.read('repo/' + node_path))
106 106 assert decompressed.getvalue() == \
107 107 self.tip.get_node(node_path).content
108 108 decompressed.close()
109 109
110 110 def test_archive_wrong_kind(self):
111 111 with pytest.raises(ImproperArchiveTypeError):
112 112 self.tip.archive_repo(self.temp_file, kind='wrong kind')
113 113
114 114
115 115 @pytest.fixture
116 116 def base_commit():
117 117 """
118 118 Prepare a `base.BaseCommit` just enough for `_validate_archive_prefix`.
119 119 """
120 120 commit = base.BaseCommit()
121 121 commit.repository = mock.Mock()
122 122 commit.repository.name = u'fake_repo'
123 123 commit.short_id = 'fake_id'
124 124 return commit
125 125
126 126
127 127 @pytest.mark.parametrize("prefix", [u"unicode-prefix", u"Ünïcâdë"])
128 128 def test_validate_archive_prefix_enforces_bytes_as_prefix(prefix, base_commit):
129 129 with pytest.raises(ValueError):
130 130 base_commit._validate_archive_prefix(prefix)
131 131
132 132
133 133 def test_validate_archive_prefix_empty_prefix(base_commit):
134 134 # TODO: johbo: Should raise a ValueError here.
135 135 with pytest.raises(VCSError):
136 136 base_commit._validate_archive_prefix('')
137 137
138 138
139 139 def test_validate_archive_prefix_with_leading_slash(base_commit):
140 140 # TODO: johbo: Should raise a ValueError here.
141 141 with pytest.raises(VCSError):
142 142 base_commit._validate_archive_prefix('/any')
143 143
144 144
145 145 def test_validate_archive_prefix_falls_back_to_repository_name(base_commit):
146 146 prefix = base_commit._validate_archive_prefix(None)
147 147 expected_prefix = base_commit._ARCHIVE_PREFIX_TEMPLATE.format(
148 148 repo_name='fake_repo',
149 149 short_id='fake_id')
150 150 assert isinstance(prefix, str)
151 151 assert prefix == expected_prefix
@@ -1,183 +1,186 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 import pytest
22 22
23 23 from mock import call, patch
24 24
25 25 from rhodecode.lib.vcs.backends.base import Reference
26 26
27 27
28 28 class TestMercurialRemoteRepoInvalidation(object):
29 29 """
30 30 If the VCSServer is running with multiple processes or/and instances.
31 31 Operations on repositories are potentially handled by different processes
32 32 in a random fashion. The mercurial repository objects used in the VCSServer
33 33 are caching the commits of the repo. Therefore we have to invalidate the
34 34 VCSServer caching of these objects after a writing operation.
35 35 """
36 36
37 37 # Default reference used as a dummy during tests.
38 38 default_ref = Reference('branch', 'default', None)
39 39
40 40 # Methods of vcsserver.hg.HgRemote that are "writing" operations.
41 41 writing_methods = [
42 42 'bookmark',
43 43 'commit',
44 44 'merge',
45 45 'pull',
46 46 'pull_cmd',
47 47 'rebase',
48 48 'strip',
49 49 'tag',
50 50 ]
51 51
52 52 @pytest.mark.parametrize('method_name, method_args', [
53 53 ('_local_merge', [default_ref, None, None, None, default_ref]),
54 54 ('_local_pull', ['', default_ref]),
55 55 ('bookmark', [None]),
56 56 ('pull', ['', default_ref]),
57 57 ('remove_tag', ['mytag', None]),
58 58 ('strip', [None]),
59 59 ('tag', ['newtag', None]),
60 60 ])
61 61 def test_method_invokes_invalidate_on_remote_repo(
62 62 self, method_name, method_args, backend_hg):
63 63 """
64 64 Check that the listed methods are invalidating the VCSServer cache
65 65 after invoking a writing method of their remote repository object.
66 66 """
67 67 tags = {'mytag': 'mytag-id'}
68 68
69 69 def add_tag(name, raw_id, *args, **kwds):
70 70 tags[name] = raw_id
71 71
72 72 repo = backend_hg.repo.scm_instance()
73
73 74 with patch.object(repo, '_remote') as remote:
75 repo.tags = tags
74 76 remote.lookup.return_value = ('commit-id', 'commit-idx')
75 77 remote.tags.return_value = tags
76 78 remote._get_tags.return_value = tags
79 remote.is_empty.return_value = False
77 80 remote.tag.side_effect = add_tag
78 81
79 82 # Invoke method.
80 83 method = getattr(repo, method_name)
81 84 method(*method_args)
82 85
83 86 # Assert that every "writing" method is followed by an invocation
84 87 # of the cache invalidation method.
85 88 for counter, method_call in enumerate(remote.method_calls):
86 89 call_name = method_call[0]
87 90 if call_name in self.writing_methods:
88 91 next_call = remote.method_calls[counter + 1]
89 92 assert next_call == call.invalidate_vcs_cache()
90 93
91 94 def _prepare_shadow_repo(self, pull_request):
92 95 """
93 96 Helper that creates a shadow repo that can be used to reproduce the
94 97 CommitDoesNotExistError when pulling in from target and source
95 98 references.
96 99 """
97 100 from rhodecode.model.pull_request import PullRequestModel
98 101 repo_id = pull_request.target_repo.repo_id
99 102 target_vcs = pull_request.target_repo.scm_instance()
100 103 target_ref = pull_request.target_ref_parts
101 104 source_ref = pull_request.source_ref_parts
102 105
103 106 # Create shadow repository.
104 107 pr = PullRequestModel()
105 108 workspace_id = pr._workspace_id(pull_request)
106 109 shadow_repository_path = target_vcs._maybe_prepare_merge_workspace(
107 110 repo_id, workspace_id, target_ref, source_ref)
108 111 shadow_repo = target_vcs._get_shadow_instance(shadow_repository_path)
109 112
110 113 # This will populate the cache of the mercurial repository object
111 114 # inside of the VCSServer.
112 115 shadow_repo.get_commit()
113 116
114 117 return shadow_repo, source_ref, target_ref
115 118
116 119 @pytest.mark.backends('hg')
117 120 def test_commit_does_not_exist_error_happens(self, pr_util, app):
118 121 """
119 122 This test is somewhat special. It does not really test the system
120 123 instead it is more or less a precondition for the
121 124 "test_commit_does_not_exist_error_does_not_happen". It deactivates the
122 125 cache invalidation and asserts that the error occurs.
123 126 """
124 127 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
125 128
126 129 pull_request = pr_util.create_pull_request()
127 130 target_vcs = pull_request.target_repo.scm_instance()
128 131 source_vcs = pull_request.source_repo.scm_instance()
129 132 shadow_repo, source_ref, target_ref = self._prepare_shadow_repo(
130 133 pull_request)
131 134
132 135 # Pull from target and source references but without invalidation of
133 136 # RemoteRepo objects and without VCSServer caching of mercurial
134 137 # repository objects.
135 138 with patch.object(shadow_repo._remote, 'invalidate_vcs_cache'):
136 139 # NOTE: Do not use patch.dict() to disable the cache because it
137 140 # restores the WHOLE dict and not only the patched keys.
138 141 shadow_repo._remote._wire['cache'] = False
139 142 shadow_repo._local_pull(target_vcs.path, target_ref)
140 143 shadow_repo._local_pull(source_vcs.path, source_ref)
141 144 shadow_repo._remote._wire.pop('cache')
142 145
143 146 # Try to lookup the target_ref in shadow repo. This should work because
144 147 # the shadow repo is a clone of the target and always contains all off
145 148 # it's commits in the initial cache.
146 149 shadow_repo.get_commit(target_ref.commit_id)
147 150
148 151 # If we try to lookup the source_ref it should fail because the shadow
149 152 # repo commit cache doesn't get invalidated. (Due to patched
150 153 # invalidation and caching above).
151 154 with pytest.raises(CommitDoesNotExistError):
152 155 shadow_repo.get_commit(source_ref.commit_id)
153 156
154 157 @pytest.mark.backends('hg')
155 158 def test_commit_does_not_exist_error_does_not_happen(self, pr_util, app):
156 159 """
157 160 This test simulates a pull request merge in which the pull operations
158 161 are handled by a different VCSServer process than all other operations.
159 162 Without correct cache invalidation this leads to an error when
160 163 retrieving the pulled commits afterwards.
161 164 """
162 165
163 166 pull_request = pr_util.create_pull_request()
164 167 target_vcs = pull_request.target_repo.scm_instance()
165 168 source_vcs = pull_request.source_repo.scm_instance()
166 169 shadow_repo, source_ref, target_ref = self._prepare_shadow_repo(
167 170 pull_request)
168 171
169 172 # Pull from target and source references without without VCSServer
170 173 # caching of mercurial repository objects but with active invalidation
171 174 # of RemoteRepo objects.
172 175 # NOTE: Do not use patch.dict() to disable the cache because it
173 176 # restores the WHOLE dict and not only the patched keys.
174 177 shadow_repo._remote._wire['cache'] = False
175 178 shadow_repo._local_pull(target_vcs.path, target_ref)
176 179 shadow_repo._local_pull(source_vcs.path, source_ref)
177 180 shadow_repo._remote._wire.pop('cache')
178 181
179 182 # Try to lookup the target and source references in shadow repo. This
180 183 # should work because the RemoteRepo object gets invalidated during the
181 184 # above pull operations.
182 185 shadow_repo.get_commit(target_ref.commit_id)
183 186 shadow_repo.get_commit(source_ref.commit_id)
@@ -1,345 +1,353 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 Tests so called "in memory commits" commit API of vcs.
23 23 """
24 24 import datetime
25 25
26 26 import pytest
27 27
28 28 from rhodecode.lib.utils2 import safe_unicode
29 29 from rhodecode.lib.vcs.exceptions import (
30 30 EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyExistsError,
31 31 NodeAlreadyRemovedError, NodeAlreadyChangedError, NodeDoesNotExistError,
32 32 NodeNotChangedError)
33 33 from rhodecode.lib.vcs.nodes import DirNode, FileNode
34 34 from rhodecode.tests.vcs.conftest import BackendTestMixin
35 35
36 36
37 37 @pytest.fixture
38 38 def nodes():
39 39 nodes = [
40 40 FileNode('foobar', content='Foo & bar'),
41 41 FileNode('foobar2', content='Foo & bar, doubled!'),
42 42 FileNode('foo bar with spaces', content=''),
43 43 FileNode('foo/bar/baz', content='Inside'),
44 44 FileNode(
45 45 'foo/bar/file.bin',
46 46 content=(
47 47 '\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1\x00\x00\x00\x00\x00\x00'
48 48 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00;\x00\x03\x00\xfe'
49 49 '\xff\t\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
50 50 '\x01\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00'
51 51 '\x00\x18\x00\x00\x00\x01\x00\x00\x00\xfe\xff\xff\xff\x00\x00'
52 52 '\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff'
53 53 '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
54 54 )
55 55 ),
56 56 ]
57 57 return nodes
58 58
59 59
60 60 @pytest.mark.usefixtures("vcs_repository_support")
61 61 class TestInMemoryCommit(BackendTestMixin):
62 62 """
63 63 This is a backend independent test case class which should be created
64 64 with ``type`` method.
65 65
66 66 It is required to set following attributes at subclass:
67 67
68 68 - ``backend_alias``: alias of used backend (see ``vcs.BACKENDS``)
69 69 """
70 70
71 71 @classmethod
72 72 def _get_commits(cls):
73 73 return []
74 74
75 75 def test_add(self, nodes):
76 76 for node in nodes:
77 77 self.imc.add(node)
78 78
79 79 self.commit()
80 80 self.assert_succesful_commit(nodes)
81 81
82 @pytest.mark.skip_backends(
83 'svn', reason="Svn does not support commits on branches.")
84 def test_add_on_branch(self, nodes):
82 @pytest.mark.backends("hg")
83 def test_add_on_branch_hg(self, nodes):
84 for node in nodes:
85 self.imc.add(node)
86 self.commit(branch=u'stable')
87 self.assert_succesful_commit(nodes)
88
89 @pytest.mark.backends("git")
90 def test_add_on_branch_git(self, nodes):
91 self.repo._checkout('stable', create=True)
92
85 93 for node in nodes:
86 94 self.imc.add(node)
87 95 self.commit(branch=u'stable')
88 96 self.assert_succesful_commit(nodes)
89 97
90 98 def test_add_in_bulk(self, nodes):
91 99 self.imc.add(*nodes)
92 100
93 101 self.commit()
94 102 self.assert_succesful_commit(nodes)
95 103
96 104 def test_add_non_ascii_files(self):
97 105 nodes = [
98 106 FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko_utf8_str', content='Δ‡Δ‡Δ‡Δ‡'),
99 107 FileNode(u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_unicode', content=u'Δ‡Δ‡Δ‡Δ‡'),
100 108 ]
101 109
102 110 for node in nodes:
103 111 self.imc.add(node)
104 112
105 113 self.commit()
106 114 self.assert_succesful_commit(nodes)
107 115
108 116 def commit(self, branch=None):
109 117 self.old_commit_count = len(self.repo.commit_ids)
110 118 self.commit_message = u'Test commit with unicode: ΕΌΓ³Ε‚wik'
111 119 self.commit_author = unicode(self.__class__)
112 120 self.commit = self.imc.commit(
113 121 message=self.commit_message, author=self.commit_author,
114 122 branch=branch)
115 123
116 124 def test_add_actually_adds_all_nodes_at_second_commit_too(self):
117 125 to_add = [
118 126 FileNode('foo/bar/image.png', content='\0'),
119 127 FileNode('foo/README.txt', content='readme!'),
120 128 ]
121 129 self.imc.add(*to_add)
122 130 commit = self.imc.commit(u'Initial', u'joe.doe@example.com')
123 131 assert isinstance(commit.get_node('foo'), DirNode)
124 132 assert isinstance(commit.get_node('foo/bar'), DirNode)
125 133 self.assert_nodes_in_commit(commit, to_add)
126 134
127 135 # commit some more files again
128 136 to_add = [
129 137 FileNode('foo/bar/foobaz/bar', content='foo'),
130 138 FileNode('foo/bar/another/bar', content='foo'),
131 139 FileNode('foo/baz.txt', content='foo'),
132 140 FileNode('foobar/foobaz/file', content='foo'),
133 141 FileNode('foobar/barbaz', content='foo'),
134 142 ]
135 143 self.imc.add(*to_add)
136 144 commit = self.imc.commit(u'Another', u'joe.doe@example.com')
137 145 self.assert_nodes_in_commit(commit, to_add)
138 146
139 147 def test_add_raise_already_added(self):
140 148 node = FileNode('foobar', content='baz')
141 149 self.imc.add(node)
142 150 with pytest.raises(NodeAlreadyAddedError):
143 151 self.imc.add(node)
144 152
145 153 def test_check_integrity_raise_already_exist(self):
146 154 node = FileNode('foobar', content='baz')
147 155 self.imc.add(node)
148 156 self.imc.commit(message=u'Added foobar', author=unicode(self))
149 157 self.imc.add(node)
150 158 with pytest.raises(NodeAlreadyExistsError):
151 159 self.imc.commit(message='new message', author=str(self))
152 160
153 161 def test_change(self):
154 162 self.imc.add(FileNode('foo/bar/baz', content='foo'))
155 163 self.imc.add(FileNode('foo/fbar', content='foobar'))
156 164 tip = self.imc.commit(u'Initial', u'joe.doe@example.com')
157 165
158 166 # Change node's content
159 167 node = FileNode('foo/bar/baz', content='My **changed** content')
160 168 self.imc.change(node)
161 169 self.imc.commit(u'Changed %s' % node.path, u'joe.doe@example.com')
162 170
163 171 newtip = self.repo.get_commit()
164 172 assert tip != newtip
165 173 assert tip.id != newtip.id
166 174 self.assert_nodes_in_commit(newtip, (node,))
167 175
168 176 def test_change_non_ascii(self):
169 177 to_add = [
170 178 FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko', content='Δ‡Δ‡Δ‡Δ‡'),
171 179 FileNode(u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni', content=u'Δ‡Δ‡Δ‡Δ‡'),
172 180 ]
173 181 for node in to_add:
174 182 self.imc.add(node)
175 183
176 184 tip = self.imc.commit(u'Initial', u'joe.doe@example.com')
177 185
178 186 # Change node's content
179 187 node = FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko', content='My **changed** content')
180 188 self.imc.change(node)
181 189 self.imc.commit(u'Changed %s' % safe_unicode(node.path),
182 190 u'joe.doe@example.com')
183 191
184 192 node_uni = FileNode(
185 193 u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni', content=u'My **changed** content')
186 194 self.imc.change(node_uni)
187 195 self.imc.commit(u'Changed %s' % safe_unicode(node_uni.path),
188 196 u'joe.doe@example.com')
189 197
190 198 newtip = self.repo.get_commit()
191 199 assert tip != newtip
192 200 assert tip.id != newtip.id
193 201
194 202 self.assert_nodes_in_commit(newtip, (node, node_uni))
195 203
196 204 def test_change_raise_empty_repository(self):
197 205 node = FileNode('foobar')
198 206 with pytest.raises(EmptyRepositoryError):
199 207 self.imc.change(node)
200 208
201 209 def test_check_integrity_change_raise_node_does_not_exist(self):
202 210 node = FileNode('foobar', content='baz')
203 211 self.imc.add(node)
204 212 self.imc.commit(message=u'Added foobar', author=unicode(self))
205 213 node = FileNode('not-foobar', content='')
206 214 self.imc.change(node)
207 215 with pytest.raises(NodeDoesNotExistError):
208 216 self.imc.commit(
209 217 message='Changed not existing node',
210 218 author=str(self))
211 219
212 220 def test_change_raise_node_already_changed(self):
213 221 node = FileNode('foobar', content='baz')
214 222 self.imc.add(node)
215 223 self.imc.commit(message=u'Added foobar', author=unicode(self))
216 224 node = FileNode('foobar', content='more baz')
217 225 self.imc.change(node)
218 226 with pytest.raises(NodeAlreadyChangedError):
219 227 self.imc.change(node)
220 228
221 229 def test_check_integrity_change_raise_node_not_changed(self, nodes):
222 230 self.test_add(nodes) # Performs first commit
223 231
224 232 node = FileNode(nodes[0].path, content=nodes[0].content)
225 233 self.imc.change(node)
226 234 with pytest.raises(NodeNotChangedError):
227 235 self.imc.commit(
228 236 message=u'Trying to mark node as changed without touching it',
229 237 author=unicode(self))
230 238
231 239 def test_change_raise_node_already_removed(self):
232 240 node = FileNode('foobar', content='baz')
233 241 self.imc.add(node)
234 242 self.imc.commit(message=u'Added foobar', author=unicode(self))
235 243 self.imc.remove(FileNode('foobar'))
236 244 with pytest.raises(NodeAlreadyRemovedError):
237 245 self.imc.change(node)
238 246
239 247 def test_remove(self, nodes):
240 248 self.test_add(nodes) # Performs first commit
241 249
242 250 tip = self.repo.get_commit()
243 251 node = nodes[0]
244 252 assert node.content == tip.get_node(node.path).content
245 253 self.imc.remove(node)
246 254 self.imc.commit(
247 255 message=u'Removed %s' % node.path, author=unicode(self))
248 256
249 257 newtip = self.repo.get_commit()
250 258 assert tip != newtip
251 259 assert tip.id != newtip.id
252 260 with pytest.raises(NodeDoesNotExistError):
253 261 newtip.get_node(node.path)
254 262
255 263 def test_remove_last_file_from_directory(self):
256 264 node = FileNode('omg/qwe/foo/bar', content='foobar')
257 265 self.imc.add(node)
258 266 self.imc.commit(u'added', u'joe doe')
259 267
260 268 self.imc.remove(node)
261 269 tip = self.imc.commit(u'removed', u'joe doe')
262 270 with pytest.raises(NodeDoesNotExistError):
263 271 tip.get_node('omg/qwe/foo/bar')
264 272
265 273 def test_remove_raise_node_does_not_exist(self, nodes):
266 274 self.imc.remove(nodes[0])
267 275 with pytest.raises(NodeDoesNotExistError):
268 276 self.imc.commit(
269 277 message='Trying to remove node at empty repository',
270 278 author=str(self))
271 279
272 280 def test_check_integrity_remove_raise_node_does_not_exist(self, nodes):
273 281 self.test_add(nodes) # Performs first commit
274 282
275 283 node = FileNode('no-such-file')
276 284 self.imc.remove(node)
277 285 with pytest.raises(NodeDoesNotExistError):
278 286 self.imc.commit(
279 287 message=u'Trying to remove not existing node',
280 288 author=unicode(self))
281 289
282 290 def test_remove_raise_node_already_removed(self, nodes):
283 291 self.test_add(nodes) # Performs first commit
284 292
285 293 node = FileNode(nodes[0].path)
286 294 self.imc.remove(node)
287 295 with pytest.raises(NodeAlreadyRemovedError):
288 296 self.imc.remove(node)
289 297
290 298 def test_remove_raise_node_already_changed(self, nodes):
291 299 self.test_add(nodes) # Performs first commit
292 300
293 301 node = FileNode(nodes[0].path, content='Bending time')
294 302 self.imc.change(node)
295 303 with pytest.raises(NodeAlreadyChangedError):
296 304 self.imc.remove(node)
297 305
298 306 def test_reset(self):
299 307 self.imc.add(FileNode('foo', content='bar'))
300 308 # self.imc.change(FileNode('baz', content='new'))
301 309 # self.imc.remove(FileNode('qwe'))
302 310 self.imc.reset()
303 311 assert not any((self.imc.added, self.imc.changed, self.imc.removed))
304 312
305 313 def test_multiple_commits(self):
306 314 N = 3 # number of commits to perform
307 315 last = None
308 316 for x in xrange(N):
309 317 fname = 'file%s' % str(x).rjust(5, '0')
310 318 content = 'foobar\n' * x
311 319 node = FileNode(fname, content=content)
312 320 self.imc.add(node)
313 321 commit = self.imc.commit(u"Commit no. %s" % (x + 1), author=u'vcs')
314 322 assert last != commit
315 323 last = commit
316 324
317 325 # Check commit number for same repo
318 326 assert len(self.repo.commit_ids) == N
319 327
320 328 # Check commit number for recreated repo
321 329 repo = self.Backend(self.repo_path)
322 330 assert len(repo.commit_ids) == N
323 331
324 332 def test_date_attr(self, local_dt_to_utc):
325 333 node = FileNode('foobar.txt', content='Foobared!')
326 334 self.imc.add(node)
327 335 date = datetime.datetime(1985, 1, 30, 1, 45)
328 336 commit = self.imc.commit(
329 337 u"Committed at time when I was born ;-)",
330 338 author=u'lb', date=date)
331 339
332 340 assert commit.date == local_dt_to_utc(date)
333 341
334 342 def assert_succesful_commit(self, added_nodes):
335 343 newtip = self.repo.get_commit()
336 344 assert self.commit == newtip
337 345 assert self.old_commit_count + 1 == len(self.repo.commit_ids)
338 346 assert newtip.message == self.commit_message
339 347 assert newtip.author == self.commit_author
340 348 assert not any((self.imc.added, self.imc.changed, self.imc.removed))
341 349 self.assert_nodes_in_commit(newtip, added_nodes)
342 350
343 351 def assert_nodes_in_commit(self, commit, nodes):
344 352 for node in nodes:
345 353 assert commit.get_node(node.path).content == node.content
General Comments 0
You need to be logged in to leave comments. Login now