1# Copyright 2017 Octobus <contact@octobus.net> 2# 3# This software may be used and distributed according to the terms of the 4# GNU General Public License version 2 or any later version. 5""" 6Compatibility module 7""" 8 9import contextlib 10 11from mercurial.i18n import _ 12from mercurial import ( 13 cmdutil, 14 context, 15 copies as copiesmod, 16 dirstate, 17 error, 18 hg, 19 logcmdutil, 20 merge as mergemod, 21 node, 22 obsolete, 23 pycompat, 24 registrar, 25 scmutil, 26 util, 27) 28 29# hg <= 5.2 (c21aca51b392) 30try: 31 from mercurial import pathutil 32 dirs = pathutil.dirs 33except (AttributeError, ImportError): 34 dirs = util.dirs # pytype: disable=module-attr 35 36# hg <= 5.4 (b7808443ed6a) 37try: 38 from mercurial import mergestate as mergestatemod 39 mergestate = mergestatemod.mergestate 40except (AttributeError, ImportError): 41 mergestate = mergemod.mergestate # pytype: disable=module-attr 42 43from . import ( 44 exthelper, 45) 46 47eh = exthelper.exthelper() 48 49# Evolution renaming compat 50 51TROUBLES = { 52 r'ORPHAN': b'orphan', 53 r'CONTENTDIVERGENT': b'content-divergent', 54 r'PHASEDIVERGENT': b'phase-divergent', 55} 56 57# XXX: Better detection of property cache 58if r'predecessors' not in dir(obsolete.obsstore): 59 @property 60 def predecessors(self): 61 return self.precursors 62 63 obsolete.obsstore.predecessors = predecessors 64 65def memfilectx(repo, ctx, fctx, flags, copied, path): 66 # XXX Would it be better at the module level? 67 varnames = context.memfilectx.__init__.__code__.co_varnames # pytype: disable=attribute-error 68 69 if r"copysource" in varnames: 70 mctx = context.memfilectx(repo, ctx, fctx.path(), fctx.data(), 71 islink=b'l' in flags, 72 isexec=b'x' in flags, 73 copysource=copied.get(path)) 74 # hg <= 4.9 (550a172a603b) 75 elif varnames[2] == r"changectx": 76 mctx = context.memfilectx(repo, ctx, fctx.path(), fctx.data(), 77 islink=b'l' in flags, 78 isexec=b'x' in flags, 79 copied=copied.get(path)) # pytype: disable=wrong-keyword-args 80 return mctx 81 82hg48 = util.safehasattr(copiesmod, 'stringutil') 83# code imported from Mercurial core at ae17555ef93f + patch 84def fixedcopytracing(repo, c1, c2, base): 85 """A complete copy-patse of copies._fullcopytrace with a one line fix to 86 handle when the base is not parent of both c1 and c2. This should be 87 converted in a compat function once https://phab.mercurial-scm.org/D3896 88 gets in and once we drop support for 4.9, this should be removed.""" 89 90 from mercurial import pathutil 91 copies = copiesmod 92 93 # In certain scenarios (e.g. graft, update or rebase), base can be 94 # overridden We still need to know a real common ancestor in this case We 95 # can't just compute _c1.ancestor(_c2) and compare it to ca, because there 96 # can be multiple common ancestors, e.g. in case of bidmerge. Because our 97 # caller may not know if the revision passed in lieu of the CA is a genuine 98 # common ancestor or not without explicitly checking it, it's better to 99 # determine that here. 100 # 101 # base.isancestorof(wc) is False, work around that 102 _c1 = c1.p1() if c1.rev() is None else c1 103 _c2 = c2.p1() if c2.rev() is None else c2 104 # an endpoint is "dirty" if it isn't a descendant of the merge base 105 # if we have a dirty endpoint, we need to trigger graft logic, and also 106 # keep track of which endpoint is dirty 107 dirtyc1 = not base.isancestorof(_c1) 108 dirtyc2 = not base.isancestorof(_c2) 109 graft = dirtyc1 or dirtyc2 110 tca = base 111 if graft: 112 tca = _c1.ancestor(_c2) 113 114 # hg <= 4.9 (dc50121126ae) 115 try: 116 limit = copies._findlimit(repo, c1, c2) # pytype: disable=module-attr 117 except (AttributeError, TypeError): 118 limit = copies._findlimit(repo, c1.rev(), c2.rev()) # pytype: disable=module-attr 119 if limit is None: 120 # no common ancestor, no copies 121 return {}, {}, {}, {}, {} 122 repo.ui.debug(b" searching for copies back to rev %d\n" % limit) 123 124 m1 = c1.manifest() 125 m2 = c2.manifest() 126 mb = base.manifest() 127 128 # gather data from _checkcopies: 129 # - diverge = record all diverges in this dict 130 # - copy = record all non-divergent copies in this dict 131 # - fullcopy = record all copies in this dict 132 # - incomplete = record non-divergent partial copies here 133 # - incompletediverge = record divergent partial copies here 134 diverge = {} # divergence data is shared 135 incompletediverge = {} 136 data1 = {b'copy': {}, 137 b'fullcopy': {}, 138 b'incomplete': {}, 139 b'diverge': diverge, 140 b'incompletediverge': incompletediverge, 141 } 142 data2 = {b'copy': {}, 143 b'fullcopy': {}, 144 b'incomplete': {}, 145 b'diverge': diverge, 146 b'incompletediverge': incompletediverge, 147 } 148 149 # find interesting file sets from manifests 150 if hg48: 151 addedinm1 = m1.filesnotin(mb, repo.narrowmatch()) 152 addedinm2 = m2.filesnotin(mb, repo.narrowmatch()) 153 else: 154 addedinm1 = m1.filesnotin(mb) 155 addedinm2 = m2.filesnotin(mb) 156 bothnew = sorted(addedinm1 & addedinm2) 157 if tca == base: 158 # unmatched file from base 159 u1r, u2r = copies._computenonoverlap(repo, c1, c2, addedinm1, addedinm2) # pytype: disable=module-attr 160 u1u, u2u = u1r, u2r 161 else: 162 # unmatched file from base (DAG rotation in the graft case) 163 u1r, u2r = copies._computenonoverlap(repo, c1, c2, addedinm1, addedinm2, # pytype: disable=module-attr 164 baselabel=b'base') 165 # unmatched file from topological common ancestors (no DAG rotation) 166 # need to recompute this for directory move handling when grafting 167 mta = tca.manifest() 168 if hg48: 169 m1f = m1.filesnotin(mta, repo.narrowmatch()) 170 m2f = m2.filesnotin(mta, repo.narrowmatch()) 171 baselabel = b'topological common ancestor' 172 u1u, u2u = copies._computenonoverlap(repo, c1, c2, m1f, m2f, # pytype: disable=module-attr 173 baselabel=baselabel) 174 else: 175 u1u, u2u = copies._computenonoverlap(repo, c1, c2, m1.filesnotin(mta), # pytype: disable=module-attr 176 m2.filesnotin(mta), 177 baselabel=b'topological common ancestor') 178 179 for f in u1u: 180 copies._checkcopies(c1, c2, f, base, tca, dirtyc1, limit, data1) # pytype: disable=module-attr 181 182 for f in u2u: 183 copies._checkcopies(c2, c1, f, base, tca, dirtyc2, limit, data2) # pytype: disable=module-attr 184 185 copy = dict(data1[b'copy']) 186 copy.update(data2[b'copy']) 187 fullcopy = dict(data1[b'fullcopy']) 188 fullcopy.update(data2[b'fullcopy']) 189 190 if dirtyc1: 191 copies._combinecopies(data2[b'incomplete'], data1[b'incomplete'], copy, diverge, # pytype: disable=module-attr 192 incompletediverge) 193 else: 194 copies._combinecopies(data1[b'incomplete'], data2[b'incomplete'], copy, diverge, # pytype: disable=module-attr 195 incompletediverge) 196 197 renamedelete = {} 198 renamedeleteset = set() 199 divergeset = set() 200 for of, fl in list(diverge.items()): 201 if len(fl) == 1 or of in c1 or of in c2: 202 del diverge[of] # not actually divergent, or not a rename 203 if of not in c1 and of not in c2: 204 # renamed on one side, deleted on the other side, but filter 205 # out files that have been renamed and then deleted 206 renamedelete[of] = [f for f in fl if f in c1 or f in c2] 207 renamedeleteset.update(fl) # reverse map for below 208 else: 209 divergeset.update(fl) # reverse map for below 210 211 if bothnew: 212 repo.ui.debug(b" unmatched files new in both:\n %s\n" 213 % b"\n ".join(bothnew)) 214 bothdiverge = {} 215 bothincompletediverge = {} 216 remainder = {} 217 both1 = {b'copy': {}, 218 b'fullcopy': {}, 219 b'incomplete': {}, 220 b'diverge': bothdiverge, 221 b'incompletediverge': bothincompletediverge 222 } 223 both2 = {b'copy': {}, 224 b'fullcopy': {}, 225 b'incomplete': {}, 226 b'diverge': bothdiverge, 227 b'incompletediverge': bothincompletediverge 228 } 229 for f in bothnew: 230 copies._checkcopies(c1, c2, f, base, tca, dirtyc1, limit, both1) # pytype: disable=module-attr 231 copies._checkcopies(c2, c1, f, base, tca, dirtyc2, limit, both2) # pytype: disable=module-attr 232 233 if dirtyc1 and dirtyc2: 234 pass 235 elif dirtyc1: 236 # incomplete copies may only be found on the "dirty" side for bothnew 237 assert not both2[b'incomplete'] 238 remainder = copies._combinecopies({}, both1[b'incomplete'], copy, bothdiverge, # pytype: disable=module-attr 239 bothincompletediverge) 240 elif dirtyc2: 241 assert not both1[b'incomplete'] 242 remainder = copies._combinecopies({}, both2[b'incomplete'], copy, bothdiverge, # pytype: disable=module-attr 243 bothincompletediverge) 244 else: 245 # incomplete copies and divergences can't happen outside grafts 246 assert not both1[b'incomplete'] 247 assert not both2[b'incomplete'] 248 assert not bothincompletediverge 249 for f in remainder: 250 assert f not in bothdiverge 251 ic = remainder[f] 252 if ic[0] in (m1 if dirtyc1 else m2): 253 # backed-out rename on one side, but watch out for deleted files 254 bothdiverge[f] = ic 255 for of, fl in bothdiverge.items(): 256 if len(fl) == 2 and fl[0] == fl[1]: 257 copy[fl[0]] = of # not actually divergent, just matching renames 258 259 if fullcopy and repo.ui.debugflag: 260 repo.ui.debug(b" all copies found (* = to merge, ! = divergent, " 261 b"% = renamed and deleted):\n") 262 for f in sorted(fullcopy): 263 note = b"" 264 if f in copy: 265 note += b"*" 266 if f in divergeset: 267 note += b"!" 268 if f in renamedeleteset: 269 note += b"%" 270 repo.ui.debug(b" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f, 271 note)) 272 del divergeset 273 274 if not fullcopy: 275 return copy, {}, diverge, renamedelete, {} 276 277 repo.ui.debug(b" checking for directory renames\n") 278 279 # generate a directory move map 280 d1, d2 = c1.dirs(), c2.dirs() 281 # Hack for adding '', which is not otherwise added, to d1 and d2 282 d1.addpath(b'/') 283 d2.addpath(b'/') 284 invalid = set() 285 dirmove = {} 286 287 # examine each file copy for a potential directory move, which is 288 # when all the files in a directory are moved to a new directory 289 for dst, src in fullcopy.items(): 290 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst) 291 if dsrc in invalid: 292 # already seen to be uninteresting 293 continue 294 elif dsrc in d1 and ddst in d1: 295 # directory wasn't entirely moved locally 296 invalid.add(dsrc + b"/") 297 elif dsrc in d2 and ddst in d2: 298 # directory wasn't entirely moved remotely 299 invalid.add(dsrc + b"/") 300 elif dsrc + b"/" in dirmove and dirmove[dsrc + b"/"] != ddst + b"/": 301 # files from the same directory moved to two different places 302 invalid.add(dsrc + b"/") 303 else: 304 # looks good so far 305 dirmove[dsrc + b"/"] = ddst + b"/" 306 307 for i in invalid: 308 if i in dirmove: 309 del dirmove[i] 310 del d1, d2, invalid 311 312 if not dirmove: 313 return copy, {}, diverge, renamedelete, {} 314 315 for d in dirmove: 316 repo.ui.debug(b" discovered dir src: '%s' -> dst: '%s'\n" % 317 (d, dirmove[d])) 318 319 movewithdir = {} 320 # check unaccounted nonoverlapping files against directory moves 321 for f in u1r + u2r: 322 if f not in fullcopy: 323 for d in dirmove: 324 if f.startswith(d): 325 # new file added in a directory that was moved, move it 326 df = dirmove[d] + f[len(d):] 327 if df not in copy: 328 movewithdir[f] = df 329 repo.ui.debug((b" pending file src: '%s' -> " 330 b"dst: '%s'\n") % (f, df)) 331 break 332 333 return copy, movewithdir, diverge, renamedelete, dirmove 334 335# hg <= 4.9 (7694b685bb10) 336fixupstreamed = util.safehasattr(scmutil, 'movedirstate') 337if not fixupstreamed: 338 copiesmod._fullcopytracing = fixedcopytracing 339 340# help category compatibility 341# hg <= 4.7 (c303d65d2e34) 342def helpcategorykwargs(categoryname): 343 """Backwards-compatible specification of the helpategory argument.""" 344 category = getattr(registrar.command, categoryname, None) 345 if not category: 346 return {} 347 return {'helpcategory': category} 348 349# nodemap.get and index.[has_node|rev|get_rev] 350# hg <= 5.2 (02802fa87b74) 351def getgetrev(cl): 352 """Returns index.get_rev or nodemap.get (for pre-5.3 Mercurial).""" 353 if util.safehasattr(cl.index, 'get_rev'): 354 return cl.index.get_rev 355 return cl.nodemap.get 356 357@contextlib.contextmanager 358def parentchange(repo): 359 try: 360 yield 361 finally: 362 # hg <= 5.2 (85c4cd73996b) 363 if util.safehasattr(repo, '_quick_access_changeid_invalidate'): 364 repo._quick_access_changeid_invalidate() 365 366if util.safehasattr(mergemod, '_update'): 367 def _update(*args, **kwargs): 368 return mergemod._update(*args, **kwargs) 369else: 370 # hg <= 5.5 (2c86b9587740) 371 def _update(*args, **kwargs): 372 return mergemod.update(*args, **kwargs) 373 374if (util.safehasattr(mergemod, '_update') 375 and util.safehasattr(mergemod, 'update')): 376 377 def update(ctx): 378 mergemod.update(ctx) 379 380 def clean_update(ctx): 381 mergemod.clean_update(ctx) 382else: 383 # hg <= 5.5 (c1b603cdc95a) 384 def update(ctx): 385 hg.updaterepo(ctx.repo(), ctx.node(), overwrite=False) 386 387 def clean_update(ctx): 388 hg.updaterepo(ctx.repo(), ctx.node(), overwrite=True) 389 390def cleanupnodes(repo, replacements, operation, moves=None, metadata=None): 391 # Use this condition as a proxy since the commit we care about 392 # (b99903534e06) didn't change any signatures. 393 if util.safehasattr(scmutil, 'nullrev'): 394 fixedreplacements = replacements 395 else: 396 # hg <= 4.7 (b99903534e06) 397 fixedreplacements = {} 398 for oldnodes, newnodes in replacements.items(): 399 for oldnode in oldnodes: 400 fixedreplacements[oldnode] = newnodes 401 402 scmutil.cleanupnodes(repo, replacements=fixedreplacements, operation=operation, 403 moves=moves, metadata=metadata) 404 405if util.safehasattr(cmdutil, 'format_changeset_summary'): 406 def format_changeset_summary_fn(ui, repo, command, default_spec): 407 def show(ctx): 408 text = cmdutil.format_changeset_summary(ui, ctx, command=command, 409 default_spec=default_spec) 410 ui.write(b'%s\n' % text) 411 return show 412else: 413 # hg <= 5.6 (96fcc37a9c80) 414 def format_changeset_summary_fn(ui, repo, command, default_spec): 415 return logcmdutil.changesetdisplayer(ui, repo, 416 {b'template': default_spec}).show 417 418if util.safehasattr(cmdutil, 'check_at_most_one_arg'): 419 def check_at_most_one_arg(opts, *args): 420 return cmdutil.check_at_most_one_arg(opts, *args) 421else: 422 # hg <= 5.2 (d587937600be) 423 def check_at_most_one_arg(opts, *args): 424 def to_display(name): 425 return pycompat.sysbytes(name).replace(b'_', b'-') 426 427 if util.safehasattr(error, 'InputError'): 428 err = error.InputError 429 else: 430 # hg <= 5.6 (8d72e29ad1e0) 431 err = error.Abort 432 previous = None 433 for x in args: 434 if opts.get(x): 435 if previous: 436 raise err(_(b'cannot specify both --%s and --%s') 437 % (to_display(previous), to_display(x))) 438 previous = x 439 return previous 440 441if util.safehasattr(cmdutil, 'check_incompatible_arguments'): 442 code = cmdutil.check_incompatible_arguments.__code__ 443 if r'others' in code.co_varnames[:code.co_argcount]: 444 def check_incompatible_arguments(opts, first, others): 445 return cmdutil.check_incompatible_arguments(opts, first, others) 446 else: 447 # hg <= 5.3 (d4c1501225c4) 448 def check_incompatible_arguments(opts, first, others): 449 return cmdutil.check_incompatible_arguments(opts, first, *others) 450else: 451 # hg <= 5.2 (023ad45e2fd2) 452 def check_incompatible_arguments(opts, first, others): 453 for other in others: 454 check_at_most_one_arg(opts, first, other) 455 456# allowdivergenceopt is a much newer addition to obsolete.py 457# hg <= 5.8 (ba6881c6a178) 458allowdivergenceopt = b'allowdivergence' 459def isenabled(repo, option): 460 if option == allowdivergenceopt: 461 if obsolete._getoptionvalue(repo, obsolete.createmarkersopt): 462 return obsolete._getoptionvalue(repo, allowdivergenceopt) 463 else: 464 # note that we're not raising error.Abort when divergence is 465 # allowed, but creating markers is not, even on older hg versions 466 return False 467 else: 468 return obsolete.isenabled(repo, option) 469 470if util.safehasattr(dirstate.dirstate, 'set_clean'): 471 movedirstate = scmutil.movedirstate 472else: # hg <= 5.8 (8a50fb0784a9) 473 # TODO: call core's version once we've dropped support for hg <= 4.9 474 def movedirstate(repo, newctx, match=None): 475 """Move the dirstate to newctx and adjust it as necessary. 476 477 A matcher can be provided as an optimization. It is probably a bug to pass 478 a matcher that doesn't match all the differences between the parent of the 479 working copy and newctx. 480 """ 481 oldctx = repo[b'.'] 482 ds = repo.dirstate 483 dscopies = dict(ds.copies()) 484 ds.setparents(newctx.node(), node.nullid) 485 s = newctx.status(oldctx, match=match) 486 for f in s.modified: 487 if ds[f] == b'r': 488 # modified + removed -> removed 489 continue 490 ds.normallookup(f) 491 492 for f in s.added: 493 if ds[f] == b'r': 494 # added + removed -> unknown 495 ds.drop(f) 496 elif ds[f] != b'a': 497 ds.add(f) 498 499 for f in s.removed: 500 if ds[f] == b'a': 501 # removed + added -> normal 502 ds.normallookup(f) 503 elif ds[f] != b'r': 504 ds.remove(f) 505 506 # Merge old parent and old working dir copies 507 oldcopies = copiesmod.pathcopies(newctx, oldctx, match) 508 oldcopies.update(dscopies) 509 newcopies = { 510 dst: oldcopies.get(src, src) 511 for dst, src in oldcopies.items() 512 } 513 # Adjust the dirstate copies 514 for dst, src in newcopies.items(): 515 if src not in newctx or dst in newctx or ds[dst] != b'a': 516 src = None 517 ds.copy(src, dst) 518 519# hg <= 4.9 (e1ceefab9bca) 520code = context.overlayworkingctx._markdirty.__code__ 521if 'copied' not in code.co_varnames[:code.co_argcount]: 522 def fixedmarkcopied(self, path, origin): 523 self._markdirty(path, exists=True, date=self.filedate(path), 524 flags=self.flags(path), copied=origin) 525 526 context.overlayworkingctx.markcopied = fixedmarkcopied 527 528# what we're actually targeting here is e079e001d536 529# hg <= 5.0 (dc3fdd1b5af4) 530try: 531 from mercurial import state as statemod 532 markdirtyfixed = util.safehasattr(statemod, '_statecheck') 533except (AttributeError, ImportError): 534 markdirtyfixed = False 535if not markdirtyfixed: 536 def fixedmarkdirty( 537 self, 538 path, 539 exists, 540 data=None, 541 date=None, 542 flags='', 543 copied=None, 544 ): 545 # data not provided, let's see if we already have some; if not, let's 546 # grab it from our underlying context, so that we always have data if 547 # the file is marked as existing. 548 if exists and data is None: 549 oldentry = self._cache.get(path) or {} 550 data = oldentry.get('data') 551 if data is None: 552 data = self._wrappedctx[path].data() 553 554 self._cache[path] = { 555 'exists': exists, 556 'data': data, 557 'date': date, 558 'flags': flags, 559 'copied': copied, 560 } 561 562 context.overlayworkingctx._markdirty = fixedmarkdirty 563 564if util.safehasattr(dirstate.dirstate, 'get_entry'): 565 def dirchanges(dirstate): 566 return [ 567 f for f in dirstate if not dirstate.get_entry(f).maybe_clean 568 ] 569else: 570 # hg <= 5.9 (dcd97b082b3b) 571 def dirchanges(dirstate): 572 return [f for f in dirstate if dirstate[f] != b'n'] 573