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