##// END OF EJS Templates
subrepo: Handle subrepo merge errors.
Martin Bornhold -
r1108:ebe0247c default
parent child Browse files
Show More
@@ -1,803 +1,808 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 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 TagDoesNotExistError, CommitDoesNotExistError)
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 270 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
271 271 if commit_id1 == commit_id2:
272 272 return commit_id1
273 273
274 274 ancestors = self._remote.revs_from_revspec(
275 275 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
276 276 other_path=repo2.path)
277 277 return repo2[ancestors[0]].raw_id if ancestors else None
278 278
279 279 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
280 280 if commit_id1 == commit_id2:
281 281 commits = []
282 282 else:
283 283 if merge:
284 284 indexes = self._remote.revs_from_revspec(
285 285 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
286 286 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
287 287 else:
288 288 indexes = self._remote.revs_from_revspec(
289 289 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
290 290 commit_id1, other_path=repo2.path)
291 291
292 292 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
293 293 for idx in indexes]
294 294
295 295 return commits
296 296
297 297 @staticmethod
298 298 def check_url(url, config):
299 299 """
300 300 Function will check given url and try to verify if it's a valid
301 301 link. Sometimes it may happened that mercurial will issue basic
302 302 auth request that can cause whole API to hang when used from python
303 303 or other external calls.
304 304
305 305 On failures it'll raise urllib2.HTTPError, exception is also thrown
306 306 when the return code is non 200
307 307 """
308 308 # check first if it's not an local url
309 309 if os.path.isdir(url) or url.startswith('file:'):
310 310 return True
311 311
312 312 # Request the _remote to verify the url
313 313 return connection.Hg.check_url(url, config.serialize())
314 314
315 315 @staticmethod
316 316 def is_valid_repository(path):
317 317 return os.path.isdir(os.path.join(path, '.hg'))
318 318
319 319 def _init_repo(self, create, src_url=None, update_after_clone=False):
320 320 """
321 321 Function will check for mercurial repository in given path. If there
322 322 is no repository in that path it will raise an exception unless
323 323 `create` parameter is set to True - in that case repository would
324 324 be created.
325 325
326 326 If `src_url` is given, would try to clone repository from the
327 327 location at given clone_point. Additionally it'll make update to
328 328 working copy accordingly to `update_after_clone` flag.
329 329 """
330 330 if create and os.path.exists(self.path):
331 331 raise RepositoryError(
332 332 "Cannot create repository at %s, location already exist"
333 333 % self.path)
334 334
335 335 if src_url:
336 336 url = str(self._get_url(src_url))
337 337 MercurialRepository.check_url(url, self.config)
338 338
339 339 self._remote.clone(url, self.path, update_after_clone)
340 340
341 341 # Don't try to create if we've already cloned repo
342 342 create = False
343 343
344 344 if create:
345 345 os.makedirs(self.path, mode=0755)
346 346
347 347 self._remote.localrepository(create)
348 348
349 349 @LazyProperty
350 350 def in_memory_commit(self):
351 351 return MercurialInMemoryCommit(self)
352 352
353 353 @LazyProperty
354 354 def description(self):
355 355 description = self._remote.get_config_value(
356 356 'web', 'description', untrusted=True)
357 357 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
358 358
359 359 @LazyProperty
360 360 def contact(self):
361 361 contact = (
362 362 self._remote.get_config_value("web", "contact") or
363 363 self._remote.get_config_value("ui", "username"))
364 364 return safe_unicode(contact or self.DEFAULT_CONTACT)
365 365
366 366 @LazyProperty
367 367 def last_change(self):
368 368 """
369 369 Returns last change made on this repository as
370 370 `datetime.datetime` object
371 371 """
372 372 return utcdate_fromtimestamp(self._get_mtime(), makedate()[1])
373 373
374 374 def _get_mtime(self):
375 375 try:
376 376 return date_astimestamp(self.get_commit().date)
377 377 except RepositoryError:
378 378 # fallback to filesystem
379 379 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
380 380 st_path = os.path.join(self.path, '.hg', "store")
381 381 if os.path.exists(cl_path):
382 382 return os.stat(cl_path).st_mtime
383 383 else:
384 384 return os.stat(st_path).st_mtime
385 385
386 386 def _sanitize_commit_idx(self, idx):
387 387 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
388 388 # number. A `long` is treated in the correct way though. So we convert
389 389 # `int` to `long` here to make sure it is handled correctly.
390 390 if isinstance(idx, int):
391 391 return long(idx)
392 392 return idx
393 393
394 394 def _get_url(self, url):
395 395 """
396 396 Returns normalized url. If schema is not given, would fall
397 397 to filesystem
398 398 (``file:///``) schema.
399 399 """
400 400 url = url.encode('utf8')
401 401 if url != 'default' and '://' not in url:
402 402 url = "file:" + urllib.pathname2url(url)
403 403 return url
404 404
405 405 def get_hook_location(self):
406 406 """
407 407 returns absolute path to location where hooks are stored
408 408 """
409 409 return os.path.join(self.path, '.hg', '.hgrc')
410 410
411 411 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
412 412 """
413 413 Returns ``MercurialCommit`` object representing repository's
414 414 commit at the given `commit_id` or `commit_idx`.
415 415 """
416 416 if self.is_empty():
417 417 raise EmptyRepositoryError("There are no commits yet")
418 418
419 419 if commit_id is not None:
420 420 self._validate_commit_id(commit_id)
421 421 try:
422 422 idx = self._commit_ids[commit_id]
423 423 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
424 424 except KeyError:
425 425 pass
426 426 elif commit_idx is not None:
427 427 self._validate_commit_idx(commit_idx)
428 428 commit_idx = self._sanitize_commit_idx(commit_idx)
429 429 try:
430 430 id_ = self.commit_ids[commit_idx]
431 431 if commit_idx < 0:
432 432 commit_idx += len(self.commit_ids)
433 433 return MercurialCommit(
434 434 self, id_, commit_idx, pre_load=pre_load)
435 435 except IndexError:
436 436 commit_id = commit_idx
437 437 else:
438 438 commit_id = "tip"
439 439
440 440 # TODO Paris: Ugly hack to "serialize" long for msgpack
441 441 if isinstance(commit_id, long):
442 442 commit_id = float(commit_id)
443 443
444 444 if isinstance(commit_id, unicode):
445 445 commit_id = safe_str(commit_id)
446 446
447 447 raw_id, idx = self._remote.lookup(commit_id, both=True)
448 448
449 449 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
450 450
451 451 def get_commits(
452 452 self, start_id=None, end_id=None, start_date=None, end_date=None,
453 453 branch_name=None, pre_load=None):
454 454 """
455 455 Returns generator of ``MercurialCommit`` objects from start to end
456 456 (both are inclusive)
457 457
458 458 :param start_id: None, str(commit_id)
459 459 :param end_id: None, str(commit_id)
460 460 :param start_date: if specified, commits with commit date less than
461 461 ``start_date`` would be filtered out from returned set
462 462 :param end_date: if specified, commits with commit date greater than
463 463 ``end_date`` would be filtered out from returned set
464 464 :param branch_name: if specified, commits not reachable from given
465 465 branch would be filtered out from returned set
466 466
467 467 :raise BranchDoesNotExistError: If given ``branch_name`` does not
468 468 exist.
469 469 :raise CommitDoesNotExistError: If commit for given ``start`` or
470 470 ``end`` could not be found.
471 471 """
472 472 # actually we should check now if it's not an empty repo
473 473 branch_ancestors = False
474 474 if self.is_empty():
475 475 raise EmptyRepositoryError("There are no commits yet")
476 476 self._validate_branch_name(branch_name)
477 477
478 478 if start_id is not None:
479 479 self._validate_commit_id(start_id)
480 480 c_start = self.get_commit(commit_id=start_id)
481 481 start_pos = self._commit_ids[c_start.raw_id]
482 482 else:
483 483 start_pos = None
484 484
485 485 if end_id is not None:
486 486 self._validate_commit_id(end_id)
487 487 c_end = self.get_commit(commit_id=end_id)
488 488 end_pos = max(0, self._commit_ids[c_end.raw_id])
489 489 else:
490 490 end_pos = None
491 491
492 492 if None not in [start_id, end_id] and start_pos > end_pos:
493 493 raise RepositoryError(
494 494 "Start commit '%s' cannot be after end commit '%s'" %
495 495 (start_id, end_id))
496 496
497 497 if end_pos is not None:
498 498 end_pos += 1
499 499
500 500 commit_filter = []
501 501 if branch_name and not branch_ancestors:
502 502 commit_filter.append('branch("%s")' % branch_name)
503 503 elif branch_name and branch_ancestors:
504 504 commit_filter.append('ancestors(branch("%s"))' % branch_name)
505 505 if start_date and not end_date:
506 506 commit_filter.append('date(">%s")' % start_date)
507 507 if end_date and not start_date:
508 508 commit_filter.append('date("<%s")' % end_date)
509 509 if start_date and end_date:
510 510 commit_filter.append(
511 511 'date(">%s") and date("<%s")' % (start_date, end_date))
512 512
513 513 # TODO: johbo: Figure out a simpler way for this solution
514 514 collection_generator = CollectionGenerator
515 515 if commit_filter:
516 516 commit_filter = map(safe_str, commit_filter)
517 517 revisions = self._remote.rev_range(commit_filter)
518 518 collection_generator = MercurialIndexBasedCollectionGenerator
519 519 else:
520 520 revisions = self.commit_ids
521 521
522 522 if start_pos or end_pos:
523 523 revisions = revisions[start_pos:end_pos]
524 524
525 525 return collection_generator(self, revisions, pre_load=pre_load)
526 526
527 527 def pull(self, url, commit_ids=None):
528 528 """
529 529 Tries to pull changes from external location.
530 530
531 531 :param commit_ids: Optional. Can be set to a list of commit ids
532 532 which shall be pulled from the other repository.
533 533 """
534 534 url = self._get_url(url)
535 535 self._remote.pull(url, commit_ids=commit_ids)
536 536 self._remote.invalidate_vcs_cache()
537 537
538 538 def _local_clone(self, clone_path):
539 539 """
540 540 Create a local clone of the current repo.
541 541 """
542 542 self._remote.clone(self.path, clone_path, update_after_clone=True,
543 543 hooks=False)
544 544
545 545 def _update(self, revision, clean=False):
546 546 """
547 547 Update the working copty to the specified revision.
548 548 """
549 549 self._remote.update(revision, clean=clean)
550 550
551 551 def _identify(self):
552 552 """
553 553 Return the current state of the working directory.
554 554 """
555 555 return self._remote.identify().strip().rstrip('+')
556 556
557 557 def _heads(self, branch=None):
558 558 """
559 559 Return the commit ids of the repository heads.
560 560 """
561 561 return self._remote.heads(branch=branch).strip().split(' ')
562 562
563 563 def _ancestor(self, revision1, revision2):
564 564 """
565 565 Return the common ancestor of the two revisions.
566 566 """
567 567 return self._remote.ancestor(
568 568 revision1, revision2).strip().split(':')[-1]
569 569
570 570 def _local_push(
571 571 self, revision, repository_path, push_branches=False,
572 572 enable_hooks=False):
573 573 """
574 574 Push the given revision to the specified repository.
575 575
576 576 :param push_branches: allow to create branches in the target repo.
577 577 """
578 578 self._remote.push(
579 579 [revision], repository_path, hooks=enable_hooks,
580 580 push_branches=push_branches)
581 581
582 582 def _local_merge(self, target_ref, merge_message, user_name, user_email,
583 583 source_ref, use_rebase=False):
584 584 """
585 585 Merge the given source_revision into the checked out revision.
586 586
587 587 Returns the commit id of the merge and a boolean indicating if the
588 588 commit needs to be pushed.
589 589 """
590 590 self._update(target_ref.commit_id)
591 591
592 592 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
593 593 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
594 594
595 595 if ancestor == source_ref.commit_id:
596 596 # Nothing to do, the changes were already integrated
597 597 return target_ref.commit_id, False
598 598
599 599 elif ancestor == target_ref.commit_id and is_the_same_branch:
600 600 # In this case we should force a commit message
601 601 return source_ref.commit_id, True
602 602
603 603 if use_rebase:
604 604 try:
605 605 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
606 606 target_ref.commit_id)
607 607 self.bookmark(bookmark_name, revision=source_ref.commit_id)
608 608 self._remote.rebase(
609 609 source=source_ref.commit_id, dest=target_ref.commit_id)
610 610 self._remote.invalidate_vcs_cache()
611 611 self._update(bookmark_name)
612 612 return self._identify(), True
613 613 except RepositoryError:
614 614 # The rebase-abort may raise another exception which 'hides'
615 615 # the original one, therefore we log it here.
616 616 log.exception('Error while rebasing shadow repo during merge.')
617 617
618 618 # Cleanup any rebase leftovers
619 619 self._remote.invalidate_vcs_cache()
620 620 self._remote.rebase(abort=True)
621 621 self._remote.invalidate_vcs_cache()
622 622 self._remote.update(clean=True)
623 623 raise
624 624 else:
625 625 try:
626 626 self._remote.merge(source_ref.commit_id)
627 627 self._remote.invalidate_vcs_cache()
628 628 self._remote.commit(
629 629 message=safe_str(merge_message),
630 630 username=safe_str('%s <%s>' % (user_name, user_email)))
631 631 self._remote.invalidate_vcs_cache()
632 632 return self._identify(), True
633 633 except RepositoryError:
634 634 # Cleanup any merge leftovers
635 635 self._remote.update(clean=True)
636 636 raise
637 637
638 638 def _is_the_same_branch(self, target_ref, source_ref):
639 639 return (
640 640 self._get_branch_name(target_ref) ==
641 641 self._get_branch_name(source_ref))
642 642
643 643 def _get_branch_name(self, ref):
644 644 if ref.type == 'branch':
645 645 return ref.name
646 646 return self._remote.ctx_branch(ref.commit_id)
647 647
648 648 def _get_shadow_repository_path(self, workspace_id):
649 649 # The name of the shadow repository must start with '.', so it is
650 650 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
651 651 return os.path.join(
652 652 os.path.dirname(self.path),
653 653 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
654 654
655 655 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
656 656 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
657 657 if not os.path.exists(shadow_repository_path):
658 658 self._local_clone(shadow_repository_path)
659 659 log.debug(
660 660 'Prepared shadow repository in %s', shadow_repository_path)
661 661
662 662 return shadow_repository_path
663 663
664 664 def cleanup_merge_workspace(self, workspace_id):
665 665 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
666 666 shutil.rmtree(shadow_repository_path, ignore_errors=True)
667 667
668 668 def _merge_repo(self, shadow_repository_path, target_ref,
669 669 source_repo, source_ref, merge_message,
670 670 merger_name, merger_email, dry_run=False,
671 671 use_rebase=False):
672 672 if target_ref.commit_id not in self._heads():
673 673 return MergeResponse(
674 674 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
675 675
676 676 try:
677 677 if (target_ref.type == 'branch' and
678 678 len(self._heads(target_ref.name)) != 1):
679 679 return MergeResponse(
680 680 False, False, None,
681 681 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
682 682 except CommitDoesNotExistError as e:
683 683 log.exception('Failure when looking up branch heads on hg target')
684 684 return MergeResponse(
685 685 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
686 686
687 687 shadow_repo = self._get_shadow_instance(shadow_repository_path)
688 688
689 689 log.debug('Pulling in target reference %s', target_ref)
690 690 self._validate_pull_reference(target_ref)
691 691 shadow_repo._local_pull(self.path, target_ref)
692 692 try:
693 693 log.debug('Pulling in source reference %s', source_ref)
694 694 source_repo._validate_pull_reference(source_ref)
695 695 shadow_repo._local_pull(source_repo.path, source_ref)
696 696 except CommitDoesNotExistError:
697 697 log.exception('Failure when doing local pull on hg shadow repo')
698 698 return MergeResponse(
699 699 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
700 700
701 701 merge_ref = None
702 702 merge_failure_reason = MergeFailureReason.NONE
703 703
704 704 try:
705 705 merge_commit_id, needs_push = shadow_repo._local_merge(
706 706 target_ref, merge_message, merger_name, merger_email,
707 707 source_ref, use_rebase=use_rebase)
708 708 merge_possible = True
709 709
710 710 # Set a bookmark pointing to the merge commit. This bookmark may be
711 711 # used to easily identify the last successful merge commit in the
712 712 # shadow repository.
713 713 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
714 714 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
715 except SubrepoMergeError:
716 log.exception(
717 'Subrepo merge error during local merge on hg shadow repo.')
718 merge_possible = False
719 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
715 720 except RepositoryError:
716 721 log.exception('Failure when doing local merge on hg shadow repo')
717 722 merge_possible = False
718 723 merge_failure_reason = MergeFailureReason.MERGE_FAILED
719 724
720 725 if merge_possible and not dry_run:
721 726 if needs_push:
722 727 # In case the target is a bookmark, update it, so after pushing
723 728 # the bookmarks is also updated in the target.
724 729 if target_ref.type == 'book':
725 730 shadow_repo.bookmark(
726 731 target_ref.name, revision=merge_commit_id)
727 732
728 733 try:
729 734 shadow_repo_with_hooks = self._get_shadow_instance(
730 735 shadow_repository_path,
731 736 enable_hooks=True)
732 737 # Note: the push_branches option will push any new branch
733 738 # defined in the source repository to the target. This may
734 739 # be dangerous as branches are permanent in Mercurial.
735 740 # This feature was requested in issue #441.
736 741 shadow_repo_with_hooks._local_push(
737 742 merge_commit_id, self.path, push_branches=True,
738 743 enable_hooks=True)
739 744 merge_succeeded = True
740 745 except RepositoryError:
741 746 log.exception(
742 747 'Failure when doing local push from the shadow '
743 748 'repository to the target repository.')
744 749 merge_succeeded = False
745 750 merge_failure_reason = MergeFailureReason.PUSH_FAILED
746 751 else:
747 752 merge_succeeded = True
748 753 else:
749 754 merge_succeeded = False
750 755
751 756 return MergeResponse(
752 757 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
753 758
754 759 def _get_shadow_instance(
755 760 self, shadow_repository_path, enable_hooks=False):
756 761 config = self.config.copy()
757 762 if not enable_hooks:
758 763 config.clear_section('hooks')
759 764 return MercurialRepository(shadow_repository_path, config)
760 765
761 766 def _validate_pull_reference(self, reference):
762 767 if not (reference.name in self.bookmarks or
763 768 reference.name in self.branches or
764 769 self.get_commit(reference.commit_id)):
765 770 raise CommitDoesNotExistError(
766 771 'Unknown branch, bookmark or commit id')
767 772
768 773 def _local_pull(self, repository_path, reference):
769 774 """
770 775 Fetch a branch, bookmark or commit from a local repository.
771 776 """
772 777 repository_path = os.path.abspath(repository_path)
773 778 if repository_path == self.path:
774 779 raise ValueError('Cannot pull from the same repository')
775 780
776 781 reference_type_to_option_name = {
777 782 'book': 'bookmark',
778 783 'branch': 'branch',
779 784 }
780 785 option_name = reference_type_to_option_name.get(
781 786 reference.type, 'revision')
782 787
783 788 if option_name == 'revision':
784 789 ref = reference.commit_id
785 790 else:
786 791 ref = reference.name
787 792
788 793 options = {option_name: [ref]}
789 794 self._remote.pull_cmd(repository_path, hooks=False, **options)
790 795 self._remote.invalidate_vcs_cache()
791 796
792 797 def bookmark(self, bookmark, revision=None):
793 798 if isinstance(bookmark, unicode):
794 799 bookmark = safe_str(bookmark)
795 800 self._remote.bookmark(bookmark, revision=revision)
796 801 self._remote.invalidate_vcs_cache()
797 802
798 803
799 804 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
800 805
801 806 def _commit_factory(self, commit_id):
802 807 return self.repo.get_commit(
803 808 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now