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