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