##// END OF EJS Templates
path-permissions: Throw RepositoryRequirementError is hgacl cannot be read
idlsoft -
r2622:e5345aa5 default
parent child Browse files
Show More
@@ -1,926 +1,929 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 HG repository module
23 23 """
24 24 import ConfigParser
25 25 import logging
26 26 import binascii
27 27 import os
28 28 import shutil
29 29 import urllib
30 30
31 31 from zope.cachedescriptors.property import Lazy as LazyProperty
32 32
33 33 from rhodecode.lib.compat import OrderedDict
34 34 from rhodecode.lib.datelib import (
35 35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate,
36 36 date_astimestamp)
37 37 from rhodecode.lib.utils import safe_unicode, safe_str
38 from rhodecode.lib.vcs import connection
38 from rhodecode.lib.vcs import connection, exceptions
39 39 from rhodecode.lib.vcs.backends.base import (
40 40 BaseRepository, CollectionGenerator, Config, MergeResponse,
41 41 MergeFailureReason, Reference, BasePathPermissionChecker)
42 42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
43 43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
44 44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
47 47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
48 48
49 49 hexlify = binascii.hexlify
50 50 nullid = "\0" * 20
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MercurialRepository(BaseRepository):
56 56 """
57 57 Mercurial repository backend
58 58 """
59 59 DEFAULT_BRANCH_NAME = 'default'
60 60
61 61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 62 update_after_clone=False, with_wire=None):
63 63 """
64 64 Raises RepositoryError if repository could not be find at the given
65 65 ``repo_path``.
66 66
67 67 :param repo_path: local path of the repository
68 68 :param config: config object containing the repo configuration
69 69 :param create=False: if set to True, would try to create repository if
70 70 it does not exist rather than raising exception
71 71 :param src_url=None: would try to clone repository from given location
72 72 :param update_after_clone=False: sets update of working copy after
73 73 making a clone
74 74 """
75 75
76 76 self.path = safe_str(os.path.abspath(repo_path))
77 77 # mercurial since 4.4.X requires certain configuration to be present
78 78 # because sometimes we init the repos with config we need to meet
79 79 # special requirements
80 80 self.config = config if config else self.get_default_config(
81 81 default=[('extensions', 'largefiles', '1')])
82 82
83 83 self._remote = connection.Hg(
84 84 self.path, self.config, with_wire=with_wire)
85 85
86 86 self._init_repo(create, src_url, update_after_clone)
87 87
88 88 # caches
89 89 self._commit_ids = {}
90 90
91 91 @LazyProperty
92 92 def commit_ids(self):
93 93 """
94 94 Returns list of commit ids, in ascending order. Being lazy
95 95 attribute allows external tools to inject shas from cache.
96 96 """
97 97 commit_ids = self._get_all_commit_ids()
98 98 self._rebuild_cache(commit_ids)
99 99 return commit_ids
100 100
101 101 def _rebuild_cache(self, commit_ids):
102 102 self._commit_ids = dict((commit_id, index)
103 103 for index, commit_id in enumerate(commit_ids))
104 104
105 105 @LazyProperty
106 106 def branches(self):
107 107 return self._get_branches()
108 108
109 109 @LazyProperty
110 110 def branches_closed(self):
111 111 return self._get_branches(active=False, closed=True)
112 112
113 113 @LazyProperty
114 114 def branches_all(self):
115 115 all_branches = {}
116 116 all_branches.update(self.branches)
117 117 all_branches.update(self.branches_closed)
118 118 return all_branches
119 119
120 120 def _get_branches(self, active=True, closed=False):
121 121 """
122 122 Gets branches for this repository
123 123 Returns only not closed active branches by default
124 124
125 125 :param active: return also active branches
126 126 :param closed: return also closed branches
127 127
128 128 """
129 129 if self.is_empty():
130 130 return {}
131 131
132 132 def get_name(ctx):
133 133 return ctx[0]
134 134
135 135 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
136 136 self._remote.branches(active, closed).items()]
137 137
138 138 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
139 139
140 140 @LazyProperty
141 141 def tags(self):
142 142 """
143 143 Gets tags for this repository
144 144 """
145 145 return self._get_tags()
146 146
147 147 def _get_tags(self):
148 148 if self.is_empty():
149 149 return {}
150 150
151 151 def get_name(ctx):
152 152 return ctx[0]
153 153
154 154 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
155 155 self._remote.tags().items()]
156 156
157 157 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
158 158
159 159 def tag(self, name, user, commit_id=None, message=None, date=None,
160 160 **kwargs):
161 161 """
162 162 Creates and returns a tag for the given ``commit_id``.
163 163
164 164 :param name: name for new tag
165 165 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
166 166 :param commit_id: commit id for which new tag would be created
167 167 :param message: message of the tag's commit
168 168 :param date: date of tag's commit
169 169
170 170 :raises TagAlreadyExistError: if tag with same name already exists
171 171 """
172 172 if name in self.tags:
173 173 raise TagAlreadyExistError("Tag %s already exists" % name)
174 174 commit = self.get_commit(commit_id=commit_id)
175 175 local = kwargs.setdefault('local', False)
176 176
177 177 if message is None:
178 178 message = "Added tag %s for commit %s" % (name, commit.short_id)
179 179
180 180 date, tz = date_to_timestamp_plus_offset(date)
181 181
182 182 self._remote.tag(
183 183 name, commit.raw_id, message, local, user, date, tz)
184 184 self._remote.invalidate_vcs_cache()
185 185
186 186 # Reinitialize tags
187 187 self.tags = self._get_tags()
188 188 tag_id = self.tags[name]
189 189
190 190 return self.get_commit(commit_id=tag_id)
191 191
192 192 def remove_tag(self, name, user, message=None, date=None):
193 193 """
194 194 Removes tag with the given `name`.
195 195
196 196 :param name: name of the tag to be removed
197 197 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
198 198 :param message: message of the tag's removal commit
199 199 :param date: date of tag's removal commit
200 200
201 201 :raises TagDoesNotExistError: if tag with given name does not exists
202 202 """
203 203 if name not in self.tags:
204 204 raise TagDoesNotExistError("Tag %s does not exist" % name)
205 205 if message is None:
206 206 message = "Removed tag %s" % name
207 207 local = False
208 208
209 209 date, tz = date_to_timestamp_plus_offset(date)
210 210
211 211 self._remote.tag(name, nullid, message, local, user, date, tz)
212 212 self._remote.invalidate_vcs_cache()
213 213 self.tags = self._get_tags()
214 214
215 215 @LazyProperty
216 216 def bookmarks(self):
217 217 """
218 218 Gets bookmarks for this repository
219 219 """
220 220 return self._get_bookmarks()
221 221
222 222 def _get_bookmarks(self):
223 223 if self.is_empty():
224 224 return {}
225 225
226 226 def get_name(ctx):
227 227 return ctx[0]
228 228
229 229 _bookmarks = [
230 230 (safe_unicode(n), hexlify(h)) for n, h in
231 231 self._remote.bookmarks().items()]
232 232
233 233 return OrderedDict(sorted(_bookmarks, key=get_name))
234 234
235 235 def _get_all_commit_ids(self):
236 236 return self._remote.get_all_commit_ids('visible')
237 237
238 238 def get_diff(
239 239 self, commit1, commit2, path='', ignore_whitespace=False,
240 240 context=3, path1=None):
241 241 """
242 242 Returns (git like) *diff*, as plain text. Shows changes introduced by
243 243 `commit2` since `commit1`.
244 244
245 245 :param commit1: Entry point from which diff is shown. Can be
246 246 ``self.EMPTY_COMMIT`` - in this case, patch showing all
247 247 the changes since empty state of the repository until `commit2`
248 248 :param commit2: Until which commit changes should be shown.
249 249 :param ignore_whitespace: If set to ``True``, would not show whitespace
250 250 changes. Defaults to ``False``.
251 251 :param context: How many lines before/after changed lines should be
252 252 shown. Defaults to ``3``.
253 253 """
254 254 self._validate_diff_commits(commit1, commit2)
255 255 if path1 is not None and path1 != path:
256 256 raise ValueError("Diff of two different paths not supported.")
257 257
258 258 if path:
259 259 file_filter = [self.path, path]
260 260 else:
261 261 file_filter = None
262 262
263 263 diff = self._remote.diff(
264 264 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
265 265 opt_git=True, opt_ignorews=ignore_whitespace,
266 266 context=context)
267 267 return MercurialDiff(diff)
268 268
269 269 def strip(self, commit_id, branch=None):
270 270 self._remote.strip(commit_id, update=False, backup="none")
271 271
272 272 self._remote.invalidate_vcs_cache()
273 273 self.commit_ids = self._get_all_commit_ids()
274 274 self._rebuild_cache(self.commit_ids)
275 275
276 276 def verify(self):
277 277 verify = self._remote.verify()
278 278
279 279 self._remote.invalidate_vcs_cache()
280 280 return verify
281 281
282 282 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
283 283 if commit_id1 == commit_id2:
284 284 return commit_id1
285 285
286 286 ancestors = self._remote.revs_from_revspec(
287 287 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
288 288 other_path=repo2.path)
289 289 return repo2[ancestors[0]].raw_id if ancestors else None
290 290
291 291 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
292 292 if commit_id1 == commit_id2:
293 293 commits = []
294 294 else:
295 295 if merge:
296 296 indexes = self._remote.revs_from_revspec(
297 297 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
298 298 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
299 299 else:
300 300 indexes = self._remote.revs_from_revspec(
301 301 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
302 302 commit_id1, other_path=repo2.path)
303 303
304 304 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
305 305 for idx in indexes]
306 306
307 307 return commits
308 308
309 309 @staticmethod
310 310 def check_url(url, config):
311 311 """
312 312 Function will check given url and try to verify if it's a valid
313 313 link. Sometimes it may happened that mercurial will issue basic
314 314 auth request that can cause whole API to hang when used from python
315 315 or other external calls.
316 316
317 317 On failures it'll raise urllib2.HTTPError, exception is also thrown
318 318 when the return code is non 200
319 319 """
320 320 # check first if it's not an local url
321 321 if os.path.isdir(url) or url.startswith('file:'):
322 322 return True
323 323
324 324 # Request the _remote to verify the url
325 325 return connection.Hg.check_url(url, config.serialize())
326 326
327 327 @staticmethod
328 328 def is_valid_repository(path):
329 329 return os.path.isdir(os.path.join(path, '.hg'))
330 330
331 331 def _init_repo(self, create, src_url=None, update_after_clone=False):
332 332 """
333 333 Function will check for mercurial repository in given path. If there
334 334 is no repository in that path it will raise an exception unless
335 335 `create` parameter is set to True - in that case repository would
336 336 be created.
337 337
338 338 If `src_url` is given, would try to clone repository from the
339 339 location at given clone_point. Additionally it'll make update to
340 340 working copy accordingly to `update_after_clone` flag.
341 341 """
342 342 if create and os.path.exists(self.path):
343 343 raise RepositoryError(
344 344 "Cannot create repository at %s, location already exist"
345 345 % self.path)
346 346
347 347 if src_url:
348 348 url = str(self._get_url(src_url))
349 349 MercurialRepository.check_url(url, self.config)
350 350
351 351 self._remote.clone(url, self.path, update_after_clone)
352 352
353 353 # Don't try to create if we've already cloned repo
354 354 create = False
355 355
356 356 if create:
357 357 os.makedirs(self.path, mode=0755)
358 358
359 359 self._remote.localrepository(create)
360 360
361 361 @LazyProperty
362 362 def in_memory_commit(self):
363 363 return MercurialInMemoryCommit(self)
364 364
365 365 @LazyProperty
366 366 def description(self):
367 367 description = self._remote.get_config_value(
368 368 'web', 'description', untrusted=True)
369 369 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
370 370
371 371 @LazyProperty
372 372 def contact(self):
373 373 contact = (
374 374 self._remote.get_config_value("web", "contact") or
375 375 self._remote.get_config_value("ui", "username"))
376 376 return safe_unicode(contact or self.DEFAULT_CONTACT)
377 377
378 378 @LazyProperty
379 379 def last_change(self):
380 380 """
381 381 Returns last change made on this repository as
382 382 `datetime.datetime` object.
383 383 """
384 384 try:
385 385 return self.get_commit().date
386 386 except RepositoryError:
387 387 tzoffset = makedate()[1]
388 388 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
389 389
390 390 def _get_fs_mtime(self):
391 391 # fallback to filesystem
392 392 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
393 393 st_path = os.path.join(self.path, '.hg', "store")
394 394 if os.path.exists(cl_path):
395 395 return os.stat(cl_path).st_mtime
396 396 else:
397 397 return os.stat(st_path).st_mtime
398 398
399 399 def _sanitize_commit_idx(self, idx):
400 400 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
401 401 # number. A `long` is treated in the correct way though. So we convert
402 402 # `int` to `long` here to make sure it is handled correctly.
403 403 if isinstance(idx, int):
404 404 return long(idx)
405 405 return idx
406 406
407 407 def _get_url(self, url):
408 408 """
409 409 Returns normalized url. If schema is not given, would fall
410 410 to filesystem
411 411 (``file:///``) schema.
412 412 """
413 413 url = url.encode('utf8')
414 414 if url != 'default' and '://' not in url:
415 415 url = "file:" + urllib.pathname2url(url)
416 416 return url
417 417
418 418 def get_hook_location(self):
419 419 """
420 420 returns absolute path to location where hooks are stored
421 421 """
422 422 return os.path.join(self.path, '.hg', '.hgrc')
423 423
424 424 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
425 425 """
426 426 Returns ``MercurialCommit`` object representing repository's
427 427 commit at the given `commit_id` or `commit_idx`.
428 428 """
429 429 if self.is_empty():
430 430 raise EmptyRepositoryError("There are no commits yet")
431 431
432 432 if commit_id is not None:
433 433 self._validate_commit_id(commit_id)
434 434 try:
435 435 idx = self._commit_ids[commit_id]
436 436 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
437 437 except KeyError:
438 438 pass
439 439 elif commit_idx is not None:
440 440 self._validate_commit_idx(commit_idx)
441 441 commit_idx = self._sanitize_commit_idx(commit_idx)
442 442 try:
443 443 id_ = self.commit_ids[commit_idx]
444 444 if commit_idx < 0:
445 445 commit_idx += len(self.commit_ids)
446 446 return MercurialCommit(
447 447 self, id_, commit_idx, pre_load=pre_load)
448 448 except IndexError:
449 449 commit_id = commit_idx
450 450 else:
451 451 commit_id = "tip"
452 452
453 453 # TODO Paris: Ugly hack to "serialize" long for msgpack
454 454 if isinstance(commit_id, long):
455 455 commit_id = float(commit_id)
456 456
457 457 if isinstance(commit_id, unicode):
458 458 commit_id = safe_str(commit_id)
459 459
460 460 try:
461 461 raw_id, idx = self._remote.lookup(commit_id, both=True)
462 462 except CommitDoesNotExistError:
463 463 msg = "Commit %s does not exist for %s" % (
464 464 commit_id, self)
465 465 raise CommitDoesNotExistError(msg)
466 466
467 467 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
468 468
469 469 def get_commits(
470 470 self, start_id=None, end_id=None, start_date=None, end_date=None,
471 471 branch_name=None, show_hidden=False, pre_load=None):
472 472 """
473 473 Returns generator of ``MercurialCommit`` objects from start to end
474 474 (both are inclusive)
475 475
476 476 :param start_id: None, str(commit_id)
477 477 :param end_id: None, str(commit_id)
478 478 :param start_date: if specified, commits with commit date less than
479 479 ``start_date`` would be filtered out from returned set
480 480 :param end_date: if specified, commits with commit date greater than
481 481 ``end_date`` would be filtered out from returned set
482 482 :param branch_name: if specified, commits not reachable from given
483 483 branch would be filtered out from returned set
484 484 :param show_hidden: Show hidden commits such as obsolete or hidden from
485 485 Mercurial evolve
486 486 :raise BranchDoesNotExistError: If given ``branch_name`` does not
487 487 exist.
488 488 :raise CommitDoesNotExistError: If commit for given ``start`` or
489 489 ``end`` could not be found.
490 490 """
491 491 # actually we should check now if it's not an empty repo
492 492 branch_ancestors = False
493 493 if self.is_empty():
494 494 raise EmptyRepositoryError("There are no commits yet")
495 495 self._validate_branch_name(branch_name)
496 496
497 497 if start_id is not None:
498 498 self._validate_commit_id(start_id)
499 499 c_start = self.get_commit(commit_id=start_id)
500 500 start_pos = self._commit_ids[c_start.raw_id]
501 501 else:
502 502 start_pos = None
503 503
504 504 if end_id is not None:
505 505 self._validate_commit_id(end_id)
506 506 c_end = self.get_commit(commit_id=end_id)
507 507 end_pos = max(0, self._commit_ids[c_end.raw_id])
508 508 else:
509 509 end_pos = None
510 510
511 511 if None not in [start_id, end_id] and start_pos > end_pos:
512 512 raise RepositoryError(
513 513 "Start commit '%s' cannot be after end commit '%s'" %
514 514 (start_id, end_id))
515 515
516 516 if end_pos is not None:
517 517 end_pos += 1
518 518
519 519 commit_filter = []
520 520
521 521 if branch_name and not branch_ancestors:
522 522 commit_filter.append('branch("%s")' % (branch_name,))
523 523 elif branch_name and branch_ancestors:
524 524 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
525 525
526 526 if start_date and not end_date:
527 527 commit_filter.append('date(">%s")' % (start_date,))
528 528 if end_date and not start_date:
529 529 commit_filter.append('date("<%s")' % (end_date,))
530 530 if start_date and end_date:
531 531 commit_filter.append(
532 532 'date(">%s") and date("<%s")' % (start_date, end_date))
533 533
534 534 if not show_hidden:
535 535 commit_filter.append('not obsolete()')
536 536 commit_filter.append('not hidden()')
537 537
538 538 # TODO: johbo: Figure out a simpler way for this solution
539 539 collection_generator = CollectionGenerator
540 540 if commit_filter:
541 541 commit_filter = ' and '.join(map(safe_str, commit_filter))
542 542 revisions = self._remote.rev_range([commit_filter])
543 543 collection_generator = MercurialIndexBasedCollectionGenerator
544 544 else:
545 545 revisions = self.commit_ids
546 546
547 547 if start_pos or end_pos:
548 548 revisions = revisions[start_pos:end_pos]
549 549
550 550 return collection_generator(self, revisions, pre_load=pre_load)
551 551
552 552 def pull(self, url, commit_ids=None):
553 553 """
554 554 Tries to pull changes from external location.
555 555
556 556 :param commit_ids: Optional. Can be set to a list of commit ids
557 557 which shall be pulled from the other repository.
558 558 """
559 559 url = self._get_url(url)
560 560 self._remote.pull(url, commit_ids=commit_ids)
561 561 self._remote.invalidate_vcs_cache()
562 562
563 563 def push(self, url):
564 564 url = self._get_url(url)
565 565 self._remote.sync_push(url)
566 566
567 567 def _local_clone(self, clone_path):
568 568 """
569 569 Create a local clone of the current repo.
570 570 """
571 571 self._remote.clone(self.path, clone_path, update_after_clone=True,
572 572 hooks=False)
573 573
574 574 def _update(self, revision, clean=False):
575 575 """
576 576 Update the working copy to the specified revision.
577 577 """
578 578 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
579 579 self._remote.update(revision, clean=clean)
580 580
581 581 def _identify(self):
582 582 """
583 583 Return the current state of the working directory.
584 584 """
585 585 return self._remote.identify().strip().rstrip('+')
586 586
587 587 def _heads(self, branch=None):
588 588 """
589 589 Return the commit ids of the repository heads.
590 590 """
591 591 return self._remote.heads(branch=branch).strip().split(' ')
592 592
593 593 def _ancestor(self, revision1, revision2):
594 594 """
595 595 Return the common ancestor of the two revisions.
596 596 """
597 597 return self._remote.ancestor(revision1, revision2)
598 598
599 599 def _local_push(
600 600 self, revision, repository_path, push_branches=False,
601 601 enable_hooks=False):
602 602 """
603 603 Push the given revision to the specified repository.
604 604
605 605 :param push_branches: allow to create branches in the target repo.
606 606 """
607 607 self._remote.push(
608 608 [revision], repository_path, hooks=enable_hooks,
609 609 push_branches=push_branches)
610 610
611 611 def _local_merge(self, target_ref, merge_message, user_name, user_email,
612 612 source_ref, use_rebase=False, dry_run=False):
613 613 """
614 614 Merge the given source_revision into the checked out revision.
615 615
616 616 Returns the commit id of the merge and a boolean indicating if the
617 617 commit needs to be pushed.
618 618 """
619 619 self._update(target_ref.commit_id)
620 620
621 621 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
622 622 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
623 623
624 624 if ancestor == source_ref.commit_id:
625 625 # Nothing to do, the changes were already integrated
626 626 return target_ref.commit_id, False
627 627
628 628 elif ancestor == target_ref.commit_id and is_the_same_branch:
629 629 # In this case we should force a commit message
630 630 return source_ref.commit_id, True
631 631
632 632 if use_rebase:
633 633 try:
634 634 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
635 635 target_ref.commit_id)
636 636 self.bookmark(bookmark_name, revision=source_ref.commit_id)
637 637 self._remote.rebase(
638 638 source=source_ref.commit_id, dest=target_ref.commit_id)
639 639 self._remote.invalidate_vcs_cache()
640 640 self._update(bookmark_name)
641 641 return self._identify(), True
642 642 except RepositoryError:
643 643 # The rebase-abort may raise another exception which 'hides'
644 644 # the original one, therefore we log it here.
645 645 log.exception('Error while rebasing shadow repo during merge.')
646 646
647 647 # Cleanup any rebase leftovers
648 648 self._remote.invalidate_vcs_cache()
649 649 self._remote.rebase(abort=True)
650 650 self._remote.invalidate_vcs_cache()
651 651 self._remote.update(clean=True)
652 652 raise
653 653 else:
654 654 try:
655 655 self._remote.merge(source_ref.commit_id)
656 656 self._remote.invalidate_vcs_cache()
657 657 self._remote.commit(
658 658 message=safe_str(merge_message),
659 659 username=safe_str('%s <%s>' % (user_name, user_email)))
660 660 self._remote.invalidate_vcs_cache()
661 661 return self._identify(), True
662 662 except RepositoryError:
663 663 # Cleanup any merge leftovers
664 664 self._remote.update(clean=True)
665 665 raise
666 666
667 667 def _local_close(self, target_ref, user_name, user_email,
668 668 source_ref, close_message=''):
669 669 """
670 670 Close the branch of the given source_revision
671 671
672 672 Returns the commit id of the close and a boolean indicating if the
673 673 commit needs to be pushed.
674 674 """
675 675 self._update(source_ref.commit_id)
676 676 message = close_message or "Closing branch: `{}`".format(source_ref.name)
677 677 try:
678 678 self._remote.commit(
679 679 message=safe_str(message),
680 680 username=safe_str('%s <%s>' % (user_name, user_email)),
681 681 close_branch=True)
682 682 self._remote.invalidate_vcs_cache()
683 683 return self._identify(), True
684 684 except RepositoryError:
685 685 # Cleanup any commit leftovers
686 686 self._remote.update(clean=True)
687 687 raise
688 688
689 689 def _is_the_same_branch(self, target_ref, source_ref):
690 690 return (
691 691 self._get_branch_name(target_ref) ==
692 692 self._get_branch_name(source_ref))
693 693
694 694 def _get_branch_name(self, ref):
695 695 if ref.type == 'branch':
696 696 return ref.name
697 697 return self._remote.ctx_branch(ref.commit_id)
698 698
699 699 def _get_shadow_repository_path(self, workspace_id):
700 700 # The name of the shadow repository must start with '.', so it is
701 701 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
702 702 return os.path.join(
703 703 os.path.dirname(self.path),
704 704 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
705 705
706 706 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref, unused_source_ref):
707 707 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
708 708 if not os.path.exists(shadow_repository_path):
709 709 self._local_clone(shadow_repository_path)
710 710 log.debug(
711 711 'Prepared shadow repository in %s', shadow_repository_path)
712 712
713 713 return shadow_repository_path
714 714
715 715 def cleanup_merge_workspace(self, workspace_id):
716 716 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
717 717 shutil.rmtree(shadow_repository_path, ignore_errors=True)
718 718
719 719 def _merge_repo(self, shadow_repository_path, target_ref,
720 720 source_repo, source_ref, merge_message,
721 721 merger_name, merger_email, dry_run=False,
722 722 use_rebase=False, close_branch=False):
723 723
724 724 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
725 725 'rebase' if use_rebase else 'merge', dry_run)
726 726 if target_ref.commit_id not in self._heads():
727 727 return MergeResponse(
728 728 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
729 729
730 730 try:
731 731 if (target_ref.type == 'branch' and
732 732 len(self._heads(target_ref.name)) != 1):
733 733 return MergeResponse(
734 734 False, False, None,
735 735 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
736 736 except CommitDoesNotExistError:
737 737 log.exception('Failure when looking up branch heads on hg target')
738 738 return MergeResponse(
739 739 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
740 740
741 741 shadow_repo = self._get_shadow_instance(shadow_repository_path)
742 742
743 743 log.debug('Pulling in target reference %s', target_ref)
744 744 self._validate_pull_reference(target_ref)
745 745 shadow_repo._local_pull(self.path, target_ref)
746 746 try:
747 747 log.debug('Pulling in source reference %s', source_ref)
748 748 source_repo._validate_pull_reference(source_ref)
749 749 shadow_repo._local_pull(source_repo.path, source_ref)
750 750 except CommitDoesNotExistError:
751 751 log.exception('Failure when doing local pull on hg shadow repo')
752 752 return MergeResponse(
753 753 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
754 754
755 755 merge_ref = None
756 756 merge_commit_id = None
757 757 close_commit_id = None
758 758 merge_failure_reason = MergeFailureReason.NONE
759 759
760 760 # enforce that close branch should be used only in case we source from
761 761 # an actual Branch
762 762 close_branch = close_branch and source_ref.type == 'branch'
763 763
764 764 # don't allow to close branch if source and target are the same
765 765 close_branch = close_branch and source_ref.name != target_ref.name
766 766
767 767 needs_push_on_close = False
768 768 if close_branch and not use_rebase and not dry_run:
769 769 try:
770 770 close_commit_id, needs_push_on_close = shadow_repo._local_close(
771 771 target_ref, merger_name, merger_email, source_ref)
772 772 merge_possible = True
773 773 except RepositoryError:
774 774 log.exception(
775 775 'Failure when doing close branch on hg shadow repo')
776 776 merge_possible = False
777 777 merge_failure_reason = MergeFailureReason.MERGE_FAILED
778 778 else:
779 779 merge_possible = True
780 780
781 781 if merge_possible:
782 782 try:
783 783 merge_commit_id, needs_push = shadow_repo._local_merge(
784 784 target_ref, merge_message, merger_name, merger_email,
785 785 source_ref, use_rebase=use_rebase, dry_run=dry_run)
786 786 merge_possible = True
787 787
788 788 # read the state of the close action, if it
789 789 # maybe required a push
790 790 needs_push = needs_push or needs_push_on_close
791 791
792 792 # Set a bookmark pointing to the merge commit. This bookmark
793 793 # may be used to easily identify the last successful merge
794 794 # commit in the shadow repository.
795 795 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
796 796 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
797 797 except SubrepoMergeError:
798 798 log.exception(
799 799 'Subrepo merge error during local merge on hg shadow repo.')
800 800 merge_possible = False
801 801 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
802 802 needs_push = False
803 803 except RepositoryError:
804 804 log.exception('Failure when doing local merge on hg shadow repo')
805 805 merge_possible = False
806 806 merge_failure_reason = MergeFailureReason.MERGE_FAILED
807 807 needs_push = False
808 808
809 809 if merge_possible and not dry_run:
810 810 if needs_push:
811 811 # In case the target is a bookmark, update it, so after pushing
812 812 # the bookmarks is also updated in the target.
813 813 if target_ref.type == 'book':
814 814 shadow_repo.bookmark(
815 815 target_ref.name, revision=merge_commit_id)
816 816 try:
817 817 shadow_repo_with_hooks = self._get_shadow_instance(
818 818 shadow_repository_path,
819 819 enable_hooks=True)
820 820 # This is the actual merge action, we push from shadow
821 821 # into origin.
822 822 # Note: the push_branches option will push any new branch
823 823 # defined in the source repository to the target. This may
824 824 # be dangerous as branches are permanent in Mercurial.
825 825 # This feature was requested in issue #441.
826 826 shadow_repo_with_hooks._local_push(
827 827 merge_commit_id, self.path, push_branches=True,
828 828 enable_hooks=True)
829 829
830 830 # maybe we also need to push the close_commit_id
831 831 if close_commit_id:
832 832 shadow_repo_with_hooks._local_push(
833 833 close_commit_id, self.path, push_branches=True,
834 834 enable_hooks=True)
835 835 merge_succeeded = True
836 836 except RepositoryError:
837 837 log.exception(
838 838 'Failure when doing local push from the shadow '
839 839 'repository to the target repository.')
840 840 merge_succeeded = False
841 841 merge_failure_reason = MergeFailureReason.PUSH_FAILED
842 842 else:
843 843 merge_succeeded = True
844 844 else:
845 845 merge_succeeded = False
846 846
847 847 return MergeResponse(
848 848 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
849 849
850 850 def _get_shadow_instance(
851 851 self, shadow_repository_path, enable_hooks=False):
852 852 config = self.config.copy()
853 853 if not enable_hooks:
854 854 config.clear_section('hooks')
855 855 return MercurialRepository(shadow_repository_path, config)
856 856
857 857 def _validate_pull_reference(self, reference):
858 858 if not (reference.name in self.bookmarks or
859 859 reference.name in self.branches or
860 860 self.get_commit(reference.commit_id)):
861 861 raise CommitDoesNotExistError(
862 862 'Unknown branch, bookmark or commit id')
863 863
864 864 def _local_pull(self, repository_path, reference):
865 865 """
866 866 Fetch a branch, bookmark or commit from a local repository.
867 867 """
868 868 repository_path = os.path.abspath(repository_path)
869 869 if repository_path == self.path:
870 870 raise ValueError('Cannot pull from the same repository')
871 871
872 872 reference_type_to_option_name = {
873 873 'book': 'bookmark',
874 874 'branch': 'branch',
875 875 }
876 876 option_name = reference_type_to_option_name.get(
877 877 reference.type, 'revision')
878 878
879 879 if option_name == 'revision':
880 880 ref = reference.commit_id
881 881 else:
882 882 ref = reference.name
883 883
884 884 options = {option_name: [ref]}
885 885 self._remote.pull_cmd(repository_path, hooks=False, **options)
886 886 self._remote.invalidate_vcs_cache()
887 887
888 888 def bookmark(self, bookmark, revision=None):
889 889 if isinstance(bookmark, unicode):
890 890 bookmark = safe_str(bookmark)
891 891 self._remote.bookmark(bookmark, revision=revision)
892 892 self._remote.invalidate_vcs_cache()
893 893
894 894 def get_path_permissions(self, username):
895 895 hgacl_file = self.path + '/.hg/hgacl'
896 896 if os.path.exists(hgacl_file):
897 hgacl = ConfigParser.RawConfigParser()
898 hgacl.read(hgacl_file)
899 def read_patterns(suffix):
900 svalue = None
901 try:
902 svalue = hgacl.get('narrowhgacl', username + suffix)
903 except ConfigParser.NoOptionError:
897 try:
898 hgacl = ConfigParser.RawConfigParser()
899 hgacl.read(hgacl_file)
900 def read_patterns(suffix):
901 svalue = None
904 902 try:
905 svalue = hgacl.get('narrowhgacl', 'default' + suffix)
903 svalue = hgacl.get('narrowhgacl', username + suffix)
906 904 except ConfigParser.NoOptionError:
907 pass
908 if not svalue:
909 return None
910 result = ['/']
911 for pattern in svalue.split():
912 result.append(pattern)
913 if '*' not in pattern and '?' not in pattern:
914 result.append(pattern + '/*')
915 return result
916 includes = read_patterns('.includes')
917 excludes = read_patterns('.excludes')
918 return BasePathPermissionChecker.create_from_patterns(includes, excludes)
905 try:
906 svalue = hgacl.get('narrowhgacl', 'default' + suffix)
907 except ConfigParser.NoOptionError:
908 pass
909 if not svalue:
910 return None
911 result = ['/']
912 for pattern in svalue.split():
913 result.append(pattern)
914 if '*' not in pattern and '?' not in pattern:
915 result.append(pattern + '/*')
916 return result
917 includes = read_patterns('.includes')
918 excludes = read_patterns('.excludes')
919 return BasePathPermissionChecker.create_from_patterns(includes, excludes)
920 except BaseException as e:
921 raise exceptions.RepositoryRequirementError('Cannot read ACL settings for {}: {}'.format(self.name, e))
919 922 else:
920 923 return None
921 924
922 925 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
923 926
924 927 def _commit_factory(self, commit_id):
925 928 return self.repo.get_commit(
926 929 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now