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