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