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