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