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