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