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