##// END OF EJS Templates
merges: fixed excessive data saved in merge metadata that could not fit inside the DB table....
milka -
r4628:a7083868 stable
parent child Browse files
Show More
@@ -1,996 +1,1012 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2020 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 import os
25 25 import logging
26 26 import binascii
27 27 import urllib
28 28
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 30
31 31 from rhodecode.lib.compat import OrderedDict
32 32 from rhodecode.lib.datelib import (
33 33 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate)
34 34 from rhodecode.lib.utils import safe_unicode, safe_str
35 35 from rhodecode.lib.utils2 import CachedProperty
36 36 from rhodecode.lib.vcs import connection, exceptions
37 37 from rhodecode.lib.vcs.backends.base import (
38 38 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 39 MergeFailureReason, Reference, BasePathPermissionChecker)
40 40 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
41 41 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
42 42 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
43 43 from rhodecode.lib.vcs.exceptions import (
44 44 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
45 45 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError, UnresolvedFilesInRepo)
46 46 from rhodecode.lib.vcs.compat import configparser
47 47
48 48 hexlify = binascii.hexlify
49 49 nullid = "\0" * 20
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class MercurialRepository(BaseRepository):
55 55 """
56 56 Mercurial repository backend
57 57 """
58 58 DEFAULT_BRANCH_NAME = 'default'
59 59
60 60 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 61 do_workspace_checkout=False, with_wire=None, bare=False):
62 62 """
63 63 Raises RepositoryError if repository could not be find at the given
64 64 ``repo_path``.
65 65
66 66 :param repo_path: local path of the repository
67 67 :param config: config object containing the repo configuration
68 68 :param create=False: if set to True, would try to create repository if
69 69 it does not exist rather than raising exception
70 70 :param src_url=None: would try to clone repository from given location
71 71 :param do_workspace_checkout=False: sets update of working copy after
72 72 making a clone
73 73 :param bare: not used, compatible with other VCS
74 74 """
75 75
76 76 self.path = safe_str(os.path.abspath(repo_path))
77 77 # mercurial since 4.4.X requires certain configuration to be present
78 78 # because sometimes we init the repos with config we need to meet
79 79 # special requirements
80 80 self.config = config if config else self.get_default_config(
81 81 default=[('extensions', 'largefiles', '1')])
82 82 self.with_wire = with_wire or {"cache": False} # default should not use cache
83 83
84 84 self._init_repo(create, src_url, do_workspace_checkout)
85 85
86 86 # caches
87 87 self._commit_ids = {}
88 88
89 89 @LazyProperty
90 90 def _remote(self):
91 91 repo_id = self.path
92 92 return connection.Hg(self.path, repo_id, self.config, with_wire=self.with_wire)
93 93
94 94 @CachedProperty
95 95 def commit_ids(self):
96 96 """
97 97 Returns list of commit ids, in ascending order. Being lazy
98 98 attribute allows external tools to inject shas from cache.
99 99 """
100 100 commit_ids = self._get_all_commit_ids()
101 101 self._rebuild_cache(commit_ids)
102 102 return commit_ids
103 103
104 104 def _rebuild_cache(self, commit_ids):
105 105 self._commit_ids = dict((commit_id, index)
106 106 for index, commit_id in enumerate(commit_ids))
107 107
108 108 @CachedProperty
109 109 def branches(self):
110 110 return self._get_branches()
111 111
112 112 @CachedProperty
113 113 def branches_closed(self):
114 114 return self._get_branches(active=False, closed=True)
115 115
116 116 @CachedProperty
117 117 def branches_all(self):
118 118 all_branches = {}
119 119 all_branches.update(self.branches)
120 120 all_branches.update(self.branches_closed)
121 121 return all_branches
122 122
123 123 def _get_branches(self, active=True, closed=False):
124 124 """
125 125 Gets branches for this repository
126 126 Returns only not closed active branches by default
127 127
128 128 :param active: return also active branches
129 129 :param closed: return also closed branches
130 130
131 131 """
132 132 if self.is_empty():
133 133 return {}
134 134
135 135 def get_name(ctx):
136 136 return ctx[0]
137 137
138 138 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
139 139 self._remote.branches(active, closed).items()]
140 140
141 141 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
142 142
143 143 @CachedProperty
144 144 def tags(self):
145 145 """
146 146 Gets tags for this repository
147 147 """
148 148 return self._get_tags()
149 149
150 150 def _get_tags(self):
151 151 if self.is_empty():
152 152 return {}
153 153
154 154 def get_name(ctx):
155 155 return ctx[0]
156 156
157 157 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
158 158 self._remote.tags().items()]
159 159
160 160 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
161 161
162 162 def tag(self, name, user, commit_id=None, message=None, date=None, **kwargs):
163 163 """
164 164 Creates and returns a tag for the given ``commit_id``.
165 165
166 166 :param name: name for new tag
167 167 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
168 168 :param commit_id: commit id for which new tag would be created
169 169 :param message: message of the tag's commit
170 170 :param date: date of tag's commit
171 171
172 172 :raises TagAlreadyExistError: if tag with same name already exists
173 173 """
174 174 if name in self.tags:
175 175 raise TagAlreadyExistError("Tag %s already exists" % name)
176 176
177 177 commit = self.get_commit(commit_id=commit_id)
178 178 local = kwargs.setdefault('local', False)
179 179
180 180 if message is None:
181 181 message = "Added tag %s for commit %s" % (name, commit.short_id)
182 182
183 183 date, tz = date_to_timestamp_plus_offset(date)
184 184
185 185 self._remote.tag(name, commit.raw_id, message, local, user, date, tz)
186 186 self._remote.invalidate_vcs_cache()
187 187
188 188 # Reinitialize tags
189 189 self._invalidate_prop_cache('tags')
190 190 tag_id = self.tags[name]
191 191
192 192 return self.get_commit(commit_id=tag_id)
193 193
194 194 def remove_tag(self, name, user, message=None, date=None):
195 195 """
196 196 Removes tag with the given `name`.
197 197
198 198 :param name: name of the tag to be removed
199 199 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
200 200 :param message: message of the tag's removal commit
201 201 :param date: date of tag's removal commit
202 202
203 203 :raises TagDoesNotExistError: if tag with given name does not exists
204 204 """
205 205 if name not in self.tags:
206 206 raise TagDoesNotExistError("Tag %s does not exist" % name)
207 207
208 208 if message is None:
209 209 message = "Removed tag %s" % name
210 210 local = False
211 211
212 212 date, tz = date_to_timestamp_plus_offset(date)
213 213
214 214 self._remote.tag(name, nullid, message, local, user, date, tz)
215 215 self._remote.invalidate_vcs_cache()
216 216 self._invalidate_prop_cache('tags')
217 217
218 218 @LazyProperty
219 219 def bookmarks(self):
220 220 """
221 221 Gets bookmarks for this repository
222 222 """
223 223 return self._get_bookmarks()
224 224
225 225 def _get_bookmarks(self):
226 226 if self.is_empty():
227 227 return {}
228 228
229 229 def get_name(ctx):
230 230 return ctx[0]
231 231
232 232 _bookmarks = [
233 233 (safe_unicode(n), hexlify(h)) for n, h in
234 234 self._remote.bookmarks().items()]
235 235
236 236 return OrderedDict(sorted(_bookmarks, key=get_name))
237 237
238 238 def _get_all_commit_ids(self):
239 239 return self._remote.get_all_commit_ids('visible')
240 240
241 241 def get_diff(
242 242 self, commit1, commit2, path='', ignore_whitespace=False,
243 243 context=3, path1=None):
244 244 """
245 245 Returns (git like) *diff*, as plain text. Shows changes introduced by
246 246 `commit2` since `commit1`.
247 247
248 248 :param commit1: Entry point from which diff is shown. Can be
249 249 ``self.EMPTY_COMMIT`` - in this case, patch showing all
250 250 the changes since empty state of the repository until `commit2`
251 251 :param commit2: Until which commit changes should be shown.
252 252 :param ignore_whitespace: If set to ``True``, would not show whitespace
253 253 changes. Defaults to ``False``.
254 254 :param context: How many lines before/after changed lines should be
255 255 shown. Defaults to ``3``.
256 256 """
257 257 self._validate_diff_commits(commit1, commit2)
258 258 if path1 is not None and path1 != path:
259 259 raise ValueError("Diff of two different paths not supported.")
260 260
261 261 if path:
262 262 file_filter = [self.path, path]
263 263 else:
264 264 file_filter = None
265 265
266 266 diff = self._remote.diff(
267 267 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
268 268 opt_git=True, opt_ignorews=ignore_whitespace,
269 269 context=context)
270 270 return MercurialDiff(diff)
271 271
272 272 def strip(self, commit_id, branch=None):
273 273 self._remote.strip(commit_id, update=False, backup="none")
274 274
275 275 self._remote.invalidate_vcs_cache()
276 276 # clear cache
277 277 self._invalidate_prop_cache('commit_ids')
278 278
279 279 return len(self.commit_ids)
280 280
281 281 def verify(self):
282 282 verify = self._remote.verify()
283 283
284 284 self._remote.invalidate_vcs_cache()
285 285 return verify
286 286
287 287 def hg_update_cache(self):
288 288 update_cache = self._remote.hg_update_cache()
289 289
290 290 self._remote.invalidate_vcs_cache()
291 291 return update_cache
292 292
293 293 def hg_rebuild_fn_cache(self):
294 294 update_cache = self._remote.hg_rebuild_fn_cache()
295 295
296 296 self._remote.invalidate_vcs_cache()
297 297 return update_cache
298 298
299 299 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
300 300 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
301 301 self, commit_id1, repo2, commit_id2)
302 302
303 303 if commit_id1 == commit_id2:
304 304 return commit_id1
305 305
306 306 ancestors = self._remote.revs_from_revspec(
307 307 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
308 308 other_path=repo2.path)
309 309
310 310 ancestor_id = repo2[ancestors[0]].raw_id if ancestors else None
311 311
312 312 log.debug('Found common ancestor with sha: %s', ancestor_id)
313 313 return ancestor_id
314 314
315 315 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
316 316 if commit_id1 == commit_id2:
317 317 commits = []
318 318 else:
319 319 if merge:
320 320 indexes = self._remote.revs_from_revspec(
321 321 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
322 322 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
323 323 else:
324 324 indexes = self._remote.revs_from_revspec(
325 325 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
326 326 commit_id1, other_path=repo2.path)
327 327
328 328 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
329 329 for idx in indexes]
330 330
331 331 return commits
332 332
333 333 @staticmethod
334 334 def check_url(url, config):
335 335 """
336 336 Function will check given url and try to verify if it's a valid
337 337 link. Sometimes it may happened that mercurial will issue basic
338 338 auth request that can cause whole API to hang when used from python
339 339 or other external calls.
340 340
341 341 On failures it'll raise urllib2.HTTPError, exception is also thrown
342 342 when the return code is non 200
343 343 """
344 344 # check first if it's not an local url
345 345 if os.path.isdir(url) or url.startswith('file:'):
346 346 return True
347 347
348 348 # Request the _remote to verify the url
349 349 return connection.Hg.check_url(url, config.serialize())
350 350
351 351 @staticmethod
352 352 def is_valid_repository(path):
353 353 return os.path.isdir(os.path.join(path, '.hg'))
354 354
355 355 def _init_repo(self, create, src_url=None, do_workspace_checkout=False):
356 356 """
357 357 Function will check for mercurial repository in given path. If there
358 358 is no repository in that path it will raise an exception unless
359 359 `create` parameter is set to True - in that case repository would
360 360 be created.
361 361
362 362 If `src_url` is given, would try to clone repository from the
363 363 location at given clone_point. Additionally it'll make update to
364 364 working copy accordingly to `do_workspace_checkout` flag.
365 365 """
366 366 if create and os.path.exists(self.path):
367 367 raise RepositoryError(
368 368 "Cannot create repository at %s, location already exist"
369 369 % self.path)
370 370
371 371 if src_url:
372 372 url = str(self._get_url(src_url))
373 373 MercurialRepository.check_url(url, self.config)
374 374
375 375 self._remote.clone(url, self.path, do_workspace_checkout)
376 376
377 377 # Don't try to create if we've already cloned repo
378 378 create = False
379 379
380 380 if create:
381 381 os.makedirs(self.path, mode=0o755)
382 382 self._remote.localrepository(create)
383 383
384 384 @LazyProperty
385 385 def in_memory_commit(self):
386 386 return MercurialInMemoryCommit(self)
387 387
388 388 @LazyProperty
389 389 def description(self):
390 390 description = self._remote.get_config_value(
391 391 'web', 'description', untrusted=True)
392 392 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
393 393
394 394 @LazyProperty
395 395 def contact(self):
396 396 contact = (
397 397 self._remote.get_config_value("web", "contact") or
398 398 self._remote.get_config_value("ui", "username"))
399 399 return safe_unicode(contact or self.DEFAULT_CONTACT)
400 400
401 401 @LazyProperty
402 402 def last_change(self):
403 403 """
404 404 Returns last change made on this repository as
405 405 `datetime.datetime` object.
406 406 """
407 407 try:
408 408 return self.get_commit().date
409 409 except RepositoryError:
410 410 tzoffset = makedate()[1]
411 411 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
412 412
413 413 def _get_fs_mtime(self):
414 414 # fallback to filesystem
415 415 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
416 416 st_path = os.path.join(self.path, '.hg', "store")
417 417 if os.path.exists(cl_path):
418 418 return os.stat(cl_path).st_mtime
419 419 else:
420 420 return os.stat(st_path).st_mtime
421 421
422 422 def _get_url(self, url):
423 423 """
424 424 Returns normalized url. If schema is not given, would fall
425 425 to filesystem
426 426 (``file:///``) schema.
427 427 """
428 428 url = url.encode('utf8')
429 429 if url != 'default' and '://' not in url:
430 430 url = "file:" + urllib.pathname2url(url)
431 431 return url
432 432
433 433 def get_hook_location(self):
434 434 """
435 435 returns absolute path to location where hooks are stored
436 436 """
437 437 return os.path.join(self.path, '.hg', '.hgrc')
438 438
439 439 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
440 440 translate_tag=None, maybe_unreachable=False):
441 441 """
442 442 Returns ``MercurialCommit`` object representing repository's
443 443 commit at the given `commit_id` or `commit_idx`.
444 444 """
445 445 if self.is_empty():
446 446 raise EmptyRepositoryError("There are no commits yet")
447 447
448 448 if commit_id is not None:
449 449 self._validate_commit_id(commit_id)
450 450 try:
451 451 # we have cached idx, use it without contacting the remote
452 452 idx = self._commit_ids[commit_id]
453 453 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
454 454 except KeyError:
455 455 pass
456 456
457 457 elif commit_idx is not None:
458 458 self._validate_commit_idx(commit_idx)
459 459 try:
460 460 _commit_id = self.commit_ids[commit_idx]
461 461 if commit_idx < 0:
462 462 commit_idx = self.commit_ids.index(_commit_id)
463 463
464 464 return MercurialCommit(self, _commit_id, commit_idx, pre_load=pre_load)
465 465 except IndexError:
466 466 commit_id = commit_idx
467 467 else:
468 468 commit_id = "tip"
469 469
470 470 if isinstance(commit_id, unicode):
471 471 commit_id = safe_str(commit_id)
472 472
473 473 try:
474 474 raw_id, idx = self._remote.lookup(commit_id, both=True)
475 475 except CommitDoesNotExistError:
476 476 msg = "Commit {} does not exist for `{}`".format(
477 477 *map(safe_str, [commit_id, self.name]))
478 478 raise CommitDoesNotExistError(msg)
479 479
480 480 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
481 481
482 482 def get_commits(
483 483 self, start_id=None, end_id=None, start_date=None, end_date=None,
484 484 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
485 485 """
486 486 Returns generator of ``MercurialCommit`` objects from start to end
487 487 (both are inclusive)
488 488
489 489 :param start_id: None, str(commit_id)
490 490 :param end_id: None, str(commit_id)
491 491 :param start_date: if specified, commits with commit date less than
492 492 ``start_date`` would be filtered out from returned set
493 493 :param end_date: if specified, commits with commit date greater than
494 494 ``end_date`` would be filtered out from returned set
495 495 :param branch_name: if specified, commits not reachable from given
496 496 branch would be filtered out from returned set
497 497 :param show_hidden: Show hidden commits such as obsolete or hidden from
498 498 Mercurial evolve
499 499 :raise BranchDoesNotExistError: If given ``branch_name`` does not
500 500 exist.
501 501 :raise CommitDoesNotExistError: If commit for given ``start`` or
502 502 ``end`` could not be found.
503 503 """
504 504 # actually we should check now if it's not an empty repo
505 505 if self.is_empty():
506 506 raise EmptyRepositoryError("There are no commits yet")
507 507 self._validate_branch_name(branch_name)
508 508
509 509 branch_ancestors = False
510 510 if start_id is not None:
511 511 self._validate_commit_id(start_id)
512 512 c_start = self.get_commit(commit_id=start_id)
513 513 start_pos = self._commit_ids[c_start.raw_id]
514 514 else:
515 515 start_pos = None
516 516
517 517 if end_id is not None:
518 518 self._validate_commit_id(end_id)
519 519 c_end = self.get_commit(commit_id=end_id)
520 520 end_pos = max(0, self._commit_ids[c_end.raw_id])
521 521 else:
522 522 end_pos = None
523 523
524 524 if None not in [start_id, end_id] and start_pos > end_pos:
525 525 raise RepositoryError(
526 526 "Start commit '%s' cannot be after end commit '%s'" %
527 527 (start_id, end_id))
528 528
529 529 if end_pos is not None:
530 530 end_pos += 1
531 531
532 532 commit_filter = []
533 533
534 534 if branch_name and not branch_ancestors:
535 535 commit_filter.append('branch("%s")' % (branch_name,))
536 536 elif branch_name and branch_ancestors:
537 537 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
538 538
539 539 if start_date and not end_date:
540 540 commit_filter.append('date(">%s")' % (start_date,))
541 541 if end_date and not start_date:
542 542 commit_filter.append('date("<%s")' % (end_date,))
543 543 if start_date and end_date:
544 544 commit_filter.append(
545 545 'date(">%s") and date("<%s")' % (start_date, end_date))
546 546
547 547 if not show_hidden:
548 548 commit_filter.append('not obsolete()')
549 549 commit_filter.append('not hidden()')
550 550
551 551 # TODO: johbo: Figure out a simpler way for this solution
552 552 collection_generator = CollectionGenerator
553 553 if commit_filter:
554 554 commit_filter = ' and '.join(map(safe_str, commit_filter))
555 555 revisions = self._remote.rev_range([commit_filter])
556 556 collection_generator = MercurialIndexBasedCollectionGenerator
557 557 else:
558 558 revisions = self.commit_ids
559 559
560 560 if start_pos or end_pos:
561 561 revisions = revisions[start_pos:end_pos]
562 562
563 563 return collection_generator(self, revisions, pre_load=pre_load)
564 564
565 565 def pull(self, url, commit_ids=None):
566 566 """
567 567 Pull changes from external location.
568 568
569 569 :param commit_ids: Optional. Can be set to a list of commit ids
570 570 which shall be pulled from the other repository.
571 571 """
572 572 url = self._get_url(url)
573 573 self._remote.pull(url, commit_ids=commit_ids)
574 574 self._remote.invalidate_vcs_cache()
575 575
576 576 def fetch(self, url, commit_ids=None):
577 577 """
578 578 Backward compatibility with GIT fetch==pull
579 579 """
580 580 return self.pull(url, commit_ids=commit_ids)
581 581
582 582 def push(self, url):
583 583 url = self._get_url(url)
584 584 self._remote.sync_push(url)
585 585
586 586 def _local_clone(self, clone_path):
587 587 """
588 588 Create a local clone of the current repo.
589 589 """
590 590 self._remote.clone(self.path, clone_path, update_after_clone=True,
591 591 hooks=False)
592 592
593 593 def _update(self, revision, clean=False):
594 594 """
595 595 Update the working copy to the specified revision.
596 596 """
597 597 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
598 598 self._remote.update(revision, clean=clean)
599 599
600 600 def _identify(self):
601 601 """
602 602 Return the current state of the working directory.
603 603 """
604 604 return self._remote.identify().strip().rstrip('+')
605 605
606 606 def _heads(self, branch=None):
607 607 """
608 608 Return the commit ids of the repository heads.
609 609 """
610 610 return self._remote.heads(branch=branch).strip().split(' ')
611 611
612 612 def _ancestor(self, revision1, revision2):
613 613 """
614 614 Return the common ancestor of the two revisions.
615 615 """
616 616 return self._remote.ancestor(revision1, revision2)
617 617
618 618 def _local_push(
619 619 self, revision, repository_path, push_branches=False,
620 620 enable_hooks=False):
621 621 """
622 622 Push the given revision to the specified repository.
623 623
624 624 :param push_branches: allow to create branches in the target repo.
625 625 """
626 626 self._remote.push(
627 627 [revision], repository_path, hooks=enable_hooks,
628 628 push_branches=push_branches)
629 629
630 630 def _local_merge(self, target_ref, merge_message, user_name, user_email,
631 631 source_ref, use_rebase=False, close_commit_id=None, dry_run=False):
632 632 """
633 633 Merge the given source_revision into the checked out revision.
634 634
635 635 Returns the commit id of the merge and a boolean indicating if the
636 636 commit needs to be pushed.
637 637 """
638 638 source_ref_commit_id = source_ref.commit_id
639 639 target_ref_commit_id = target_ref.commit_id
640 640
641 641 # update our workdir to target ref, for proper merge
642 642 self._update(target_ref_commit_id, clean=True)
643 643
644 644 ancestor = self._ancestor(target_ref_commit_id, source_ref_commit_id)
645 645 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
646 646
647 647 if close_commit_id:
648 648 # NOTE(marcink): if we get the close commit, this is our new source
649 649 # which will include the close commit itself.
650 650 source_ref_commit_id = close_commit_id
651 651
652 652 if ancestor == source_ref_commit_id:
653 653 # Nothing to do, the changes were already integrated
654 654 return target_ref_commit_id, False
655 655
656 656 elif ancestor == target_ref_commit_id and is_the_same_branch:
657 657 # In this case we should force a commit message
658 658 return source_ref_commit_id, True
659 659
660 660 unresolved = None
661 661 if use_rebase:
662 662 try:
663 663 bookmark_name = 'rcbook%s%s' % (source_ref_commit_id, target_ref_commit_id)
664 664 self.bookmark(bookmark_name, revision=source_ref.commit_id)
665 665 self._remote.rebase(
666 666 source=source_ref_commit_id, dest=target_ref_commit_id)
667 667 self._remote.invalidate_vcs_cache()
668 668 self._update(bookmark_name, clean=True)
669 669 return self._identify(), True
670 670 except RepositoryError as e:
671 671 # The rebase-abort may raise another exception which 'hides'
672 672 # the original one, therefore we log it here.
673 673 log.exception('Error while rebasing shadow repo during merge.')
674 674 if 'unresolved conflicts' in safe_str(e):
675 675 unresolved = self._remote.get_unresolved_files()
676 676 log.debug('unresolved files: %s', unresolved)
677 677
678 678 # Cleanup any rebase leftovers
679 679 self._remote.invalidate_vcs_cache()
680 680 self._remote.rebase(abort=True)
681 681 self._remote.invalidate_vcs_cache()
682 682 self._remote.update(clean=True)
683 683 if unresolved:
684 684 raise UnresolvedFilesInRepo(unresolved)
685 685 else:
686 686 raise
687 687 else:
688 688 try:
689 689 self._remote.merge(source_ref_commit_id)
690 690 self._remote.invalidate_vcs_cache()
691 691 self._remote.commit(
692 692 message=safe_str(merge_message),
693 693 username=safe_str('%s <%s>' % (user_name, user_email)))
694 694 self._remote.invalidate_vcs_cache()
695 695 return self._identify(), True
696 696 except RepositoryError as e:
697 697 # The merge-abort may raise another exception which 'hides'
698 698 # the original one, therefore we log it here.
699 699 log.exception('Error while merging shadow repo during merge.')
700 700 if 'unresolved merge conflicts' in safe_str(e):
701 701 unresolved = self._remote.get_unresolved_files()
702 702 log.debug('unresolved files: %s', unresolved)
703 703
704 704 # Cleanup any merge leftovers
705 705 self._remote.update(clean=True)
706 706 if unresolved:
707 707 raise UnresolvedFilesInRepo(unresolved)
708 708 else:
709 709 raise
710 710
711 711 def _local_close(self, target_ref, user_name, user_email,
712 712 source_ref, close_message=''):
713 713 """
714 714 Close the branch of the given source_revision
715 715
716 716 Returns the commit id of the close and a boolean indicating if the
717 717 commit needs to be pushed.
718 718 """
719 719 self._update(source_ref.commit_id)
720 720 message = close_message or "Closing branch: `{}`".format(source_ref.name)
721 721 try:
722 722 self._remote.commit(
723 723 message=safe_str(message),
724 724 username=safe_str('%s <%s>' % (user_name, user_email)),
725 725 close_branch=True)
726 726 self._remote.invalidate_vcs_cache()
727 727 return self._identify(), True
728 728 except RepositoryError:
729 729 # Cleanup any commit leftovers
730 730 self._remote.update(clean=True)
731 731 raise
732 732
733 733 def _is_the_same_branch(self, target_ref, source_ref):
734 734 return (
735 735 self._get_branch_name(target_ref) ==
736 736 self._get_branch_name(source_ref))
737 737
738 738 def _get_branch_name(self, ref):
739 739 if ref.type == 'branch':
740 740 return ref.name
741 741 return self._remote.ctx_branch(ref.commit_id)
742 742
743 743 def _maybe_prepare_merge_workspace(
744 744 self, repo_id, workspace_id, unused_target_ref, unused_source_ref):
745 745 shadow_repository_path = self._get_shadow_repository_path(
746 746 self.path, repo_id, workspace_id)
747 747 if not os.path.exists(shadow_repository_path):
748 748 self._local_clone(shadow_repository_path)
749 749 log.debug(
750 750 'Prepared shadow repository in %s', shadow_repository_path)
751 751
752 752 return shadow_repository_path
753 753
754 754 def _merge_repo(self, repo_id, workspace_id, target_ref,
755 755 source_repo, source_ref, merge_message,
756 756 merger_name, merger_email, dry_run=False,
757 757 use_rebase=False, close_branch=False):
758 758
759 759 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
760 760 'rebase' if use_rebase else 'merge', dry_run)
761 761 if target_ref.commit_id not in self._heads():
762 762 return MergeResponse(
763 763 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
764 764 metadata={'target_ref': target_ref})
765 765
766 766 try:
767 767 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
768 heads = '\n,'.join(self._heads(target_ref.name))
768 heads_all = self._heads(target_ref.name)
769 max_heads = 10
770 if len(heads_all) > max_heads:
771 heads = '\n,'.join(
772 heads_all[:max_heads] +
773 ['and {} more.'.format(len(heads_all)-max_heads)])
774 else:
775 heads = '\n,'.join(heads_all)
769 776 metadata = {
770 777 'target_ref': target_ref,
771 778 'source_ref': source_ref,
772 779 'heads': heads
773 780 }
774 781 return MergeResponse(
775 782 False, False, None,
776 783 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
777 784 metadata=metadata)
778 785 except CommitDoesNotExistError:
779 786 log.exception('Failure when looking up branch heads on hg target')
780 787 return MergeResponse(
781 788 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
782 789 metadata={'target_ref': target_ref})
783 790
784 791 shadow_repository_path = self._maybe_prepare_merge_workspace(
785 792 repo_id, workspace_id, target_ref, source_ref)
786 793 shadow_repo = self.get_shadow_instance(shadow_repository_path)
787 794
788 795 log.debug('Pulling in target reference %s', target_ref)
789 796 self._validate_pull_reference(target_ref)
790 797 shadow_repo._local_pull(self.path, target_ref)
791 798
792 799 try:
793 800 log.debug('Pulling in source reference %s', source_ref)
794 801 source_repo._validate_pull_reference(source_ref)
795 802 shadow_repo._local_pull(source_repo.path, source_ref)
796 803 except CommitDoesNotExistError:
797 804 log.exception('Failure when doing local pull on hg shadow repo')
798 805 return MergeResponse(
799 806 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
800 807 metadata={'source_ref': source_ref})
801 808
802 809 merge_ref = None
803 810 merge_commit_id = None
804 811 close_commit_id = None
805 812 merge_failure_reason = MergeFailureReason.NONE
806 813 metadata = {}
807 814
808 815 # enforce that close branch should be used only in case we source from
809 816 # an actual Branch
810 817 close_branch = close_branch and source_ref.type == 'branch'
811 818
812 819 # don't allow to close branch if source and target are the same
813 820 close_branch = close_branch and source_ref.name != target_ref.name
814 821
815 822 needs_push_on_close = False
816 823 if close_branch and not use_rebase and not dry_run:
817 824 try:
818 825 close_commit_id, needs_push_on_close = shadow_repo._local_close(
819 826 target_ref, merger_name, merger_email, source_ref)
820 827 merge_possible = True
821 828 except RepositoryError:
822 829 log.exception('Failure when doing close branch on '
823 830 'shadow repo: %s', shadow_repo)
824 831 merge_possible = False
825 832 merge_failure_reason = MergeFailureReason.MERGE_FAILED
826 833 else:
827 834 merge_possible = True
828 835
829 836 needs_push = False
830 837 if merge_possible:
831 838
832 839 try:
833 840 merge_commit_id, needs_push = shadow_repo._local_merge(
834 841 target_ref, merge_message, merger_name, merger_email,
835 842 source_ref, use_rebase=use_rebase,
836 843 close_commit_id=close_commit_id, dry_run=dry_run)
837 844 merge_possible = True
838 845
839 846 # read the state of the close action, if it
840 847 # maybe required a push
841 848 needs_push = needs_push or needs_push_on_close
842 849
843 850 # Set a bookmark pointing to the merge commit. This bookmark
844 851 # may be used to easily identify the last successful merge
845 852 # commit in the shadow repository.
846 853 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
847 854 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
848 855 except SubrepoMergeError:
849 856 log.exception(
850 857 'Subrepo merge error during local merge on hg shadow repo.')
851 858 merge_possible = False
852 859 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
853 860 needs_push = False
854 861 except RepositoryError as e:
855 862 log.exception('Failure when doing local merge on hg shadow repo')
856 863 if isinstance(e, UnresolvedFilesInRepo):
857 metadata['unresolved_files'] = '\n* conflict: ' + ('\n * conflict: '.join(e.args[0]))
864 all_conflicts = list(e.args[0])
865 max_conflicts = 20
866 if len(all_conflicts) > max_conflicts:
867 conflicts = all_conflicts[:max_conflicts] \
868 + ['and {} more.'.format(len(all_conflicts)-max_conflicts)]
869 else:
870 conflicts = all_conflicts
871 metadata['unresolved_files'] = \
872 '\n* conflict: ' + \
873 ('\n * conflict: '.join(conflicts))
858 874
859 875 merge_possible = False
860 876 merge_failure_reason = MergeFailureReason.MERGE_FAILED
861 877 needs_push = False
862 878
863 879 if merge_possible and not dry_run:
864 880 if needs_push:
865 881 # In case the target is a bookmark, update it, so after pushing
866 882 # the bookmarks is also updated in the target.
867 883 if target_ref.type == 'book':
868 884 shadow_repo.bookmark(
869 885 target_ref.name, revision=merge_commit_id)
870 886 try:
871 887 shadow_repo_with_hooks = self.get_shadow_instance(
872 888 shadow_repository_path,
873 889 enable_hooks=True)
874 890 # This is the actual merge action, we push from shadow
875 891 # into origin.
876 892 # Note: the push_branches option will push any new branch
877 893 # defined in the source repository to the target. This may
878 894 # be dangerous as branches are permanent in Mercurial.
879 895 # This feature was requested in issue #441.
880 896 shadow_repo_with_hooks._local_push(
881 897 merge_commit_id, self.path, push_branches=True,
882 898 enable_hooks=True)
883 899
884 900 # maybe we also need to push the close_commit_id
885 901 if close_commit_id:
886 902 shadow_repo_with_hooks._local_push(
887 903 close_commit_id, self.path, push_branches=True,
888 904 enable_hooks=True)
889 905 merge_succeeded = True
890 906 except RepositoryError:
891 907 log.exception(
892 908 'Failure when doing local push from the shadow '
893 909 'repository to the target repository at %s.', self.path)
894 910 merge_succeeded = False
895 911 merge_failure_reason = MergeFailureReason.PUSH_FAILED
896 912 metadata['target'] = 'hg shadow repo'
897 913 metadata['merge_commit'] = merge_commit_id
898 914 else:
899 915 merge_succeeded = True
900 916 else:
901 917 merge_succeeded = False
902 918
903 919 return MergeResponse(
904 920 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
905 921 metadata=metadata)
906 922
907 923 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
908 924 config = self.config.copy()
909 925 if not enable_hooks:
910 926 config.clear_section('hooks')
911 927 return MercurialRepository(shadow_repository_path, config, with_wire={"cache": cache})
912 928
913 929 def _validate_pull_reference(self, reference):
914 930 if not (reference.name in self.bookmarks or
915 931 reference.name in self.branches or
916 932 self.get_commit(reference.commit_id)):
917 933 raise CommitDoesNotExistError(
918 934 'Unknown branch, bookmark or commit id')
919 935
920 936 def _local_pull(self, repository_path, reference):
921 937 """
922 938 Fetch a branch, bookmark or commit from a local repository.
923 939 """
924 940 repository_path = os.path.abspath(repository_path)
925 941 if repository_path == self.path:
926 942 raise ValueError('Cannot pull from the same repository')
927 943
928 944 reference_type_to_option_name = {
929 945 'book': 'bookmark',
930 946 'branch': 'branch',
931 947 }
932 948 option_name = reference_type_to_option_name.get(
933 949 reference.type, 'revision')
934 950
935 951 if option_name == 'revision':
936 952 ref = reference.commit_id
937 953 else:
938 954 ref = reference.name
939 955
940 956 options = {option_name: [ref]}
941 957 self._remote.pull_cmd(repository_path, hooks=False, **options)
942 958 self._remote.invalidate_vcs_cache()
943 959
944 960 def bookmark(self, bookmark, revision=None):
945 961 if isinstance(bookmark, unicode):
946 962 bookmark = safe_str(bookmark)
947 963 self._remote.bookmark(bookmark, revision=revision)
948 964 self._remote.invalidate_vcs_cache()
949 965
950 966 def get_path_permissions(self, username):
951 967 hgacl_file = os.path.join(self.path, '.hg/hgacl')
952 968
953 969 def read_patterns(suffix):
954 970 svalue = None
955 971 for section, option in [
956 972 ('narrowacl', username + suffix),
957 973 ('narrowacl', 'default' + suffix),
958 974 ('narrowhgacl', username + suffix),
959 975 ('narrowhgacl', 'default' + suffix)
960 976 ]:
961 977 try:
962 978 svalue = hgacl.get(section, option)
963 979 break # stop at the first value we find
964 980 except configparser.NoOptionError:
965 981 pass
966 982 if not svalue:
967 983 return None
968 984 result = ['/']
969 985 for pattern in svalue.split():
970 986 result.append(pattern)
971 987 if '*' not in pattern and '?' not in pattern:
972 988 result.append(pattern + '/*')
973 989 return result
974 990
975 991 if os.path.exists(hgacl_file):
976 992 try:
977 993 hgacl = configparser.RawConfigParser()
978 994 hgacl.read(hgacl_file)
979 995
980 996 includes = read_patterns('.includes')
981 997 excludes = read_patterns('.excludes')
982 998 return BasePathPermissionChecker.create_from_patterns(
983 999 includes, excludes)
984 1000 except BaseException as e:
985 1001 msg = 'Cannot read ACL settings from {} on {}: {}'.format(
986 1002 hgacl_file, self.name, e)
987 1003 raise exceptions.RepositoryRequirementError(msg)
988 1004 else:
989 1005 return None
990 1006
991 1007
992 1008 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
993 1009
994 1010 def _commit_factory(self, commit_id):
995 1011 return self.repo.get_commit(
996 1012 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now