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