##// END OF EJS Templates
merge: introduce 'commitinfo' in mergeresult...
Pulkit Goyal -
r45832:8e8d5139 default
parent child Browse files
Show More
@@ -1,729 +1,730 b''
1 1 # hg.py - hg backend for convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # Notes for hg->hg conversion:
9 9 #
10 10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 11 # of commit messages, but new versions do. Changesets created by
12 12 # those older versions, then converted, may thus have different
13 13 # hashes for changesets that are otherwise identical.
14 14 #
15 15 # * Using "--config convert.hg.saverev=true" will make the source
16 16 # identifier to be stored in the converted revision. This will cause
17 17 # the converted revision to have a different identity than the
18 18 # source.
19 19 from __future__ import absolute_import
20 20
21 21 import os
22 22 import re
23 23 import time
24 24
25 25 from mercurial.i18n import _
26 26 from mercurial.pycompat import open
27 27 from mercurial import (
28 28 bookmarks,
29 29 context,
30 30 error,
31 31 exchange,
32 32 hg,
33 33 lock as lockmod,
34 34 merge as mergemod,
35 35 node as nodemod,
36 36 phases,
37 37 pycompat,
38 38 scmutil,
39 39 util,
40 40 )
41 41 from mercurial.utils import dateutil
42 42
43 43 stringio = util.stringio
44 44
45 45 from . import common
46 46
47 47 mapfile = common.mapfile
48 48 NoRepo = common.NoRepo
49 49
50 50 sha1re = re.compile(br'\b[0-9a-f]{12,40}\b')
51 51
52 52
53 53 class mercurial_sink(common.converter_sink):
54 54 def __init__(self, ui, repotype, path):
55 55 common.converter_sink.__init__(self, ui, repotype, path)
56 56 self.branchnames = ui.configbool(b'convert', b'hg.usebranchnames')
57 57 self.clonebranches = ui.configbool(b'convert', b'hg.clonebranches')
58 58 self.tagsbranch = ui.config(b'convert', b'hg.tagsbranch')
59 59 self.lastbranch = None
60 60 if os.path.isdir(path) and len(os.listdir(path)) > 0:
61 61 try:
62 62 self.repo = hg.repository(self.ui, path)
63 63 if not self.repo.local():
64 64 raise NoRepo(
65 65 _(b'%s is not a local Mercurial repository') % path
66 66 )
67 67 except error.RepoError as err:
68 68 ui.traceback()
69 69 raise NoRepo(err.args[0])
70 70 else:
71 71 try:
72 72 ui.status(_(b'initializing destination %s repository\n') % path)
73 73 self.repo = hg.repository(self.ui, path, create=True)
74 74 if not self.repo.local():
75 75 raise NoRepo(
76 76 _(b'%s is not a local Mercurial repository') % path
77 77 )
78 78 self.created.append(path)
79 79 except error.RepoError:
80 80 ui.traceback()
81 81 raise NoRepo(
82 82 _(b"could not create hg repository %s as sink") % path
83 83 )
84 84 self.lock = None
85 85 self.wlock = None
86 86 self.filemapmode = False
87 87 self.subrevmaps = {}
88 88
89 89 def before(self):
90 90 self.ui.debug(b'run hg sink pre-conversion action\n')
91 91 self.wlock = self.repo.wlock()
92 92 self.lock = self.repo.lock()
93 93
94 94 def after(self):
95 95 self.ui.debug(b'run hg sink post-conversion action\n')
96 96 if self.lock:
97 97 self.lock.release()
98 98 if self.wlock:
99 99 self.wlock.release()
100 100
101 101 def revmapfile(self):
102 102 return self.repo.vfs.join(b"shamap")
103 103
104 104 def authorfile(self):
105 105 return self.repo.vfs.join(b"authormap")
106 106
107 107 def setbranch(self, branch, pbranches):
108 108 if not self.clonebranches:
109 109 return
110 110
111 111 setbranch = branch != self.lastbranch
112 112 self.lastbranch = branch
113 113 if not branch:
114 114 branch = b'default'
115 115 pbranches = [(b[0], b[1] and b[1] or b'default') for b in pbranches]
116 116
117 117 branchpath = os.path.join(self.path, branch)
118 118 if setbranch:
119 119 self.after()
120 120 try:
121 121 self.repo = hg.repository(self.ui, branchpath)
122 122 except Exception:
123 123 self.repo = hg.repository(self.ui, branchpath, create=True)
124 124 self.before()
125 125
126 126 # pbranches may bring revisions from other branches (merge parents)
127 127 # Make sure we have them, or pull them.
128 128 missings = {}
129 129 for b in pbranches:
130 130 try:
131 131 self.repo.lookup(b[0])
132 132 except Exception:
133 133 missings.setdefault(b[1], []).append(b[0])
134 134
135 135 if missings:
136 136 self.after()
137 137 for pbranch, heads in sorted(pycompat.iteritems(missings)):
138 138 pbranchpath = os.path.join(self.path, pbranch)
139 139 prepo = hg.peer(self.ui, {}, pbranchpath)
140 140 self.ui.note(
141 141 _(b'pulling from %s into %s\n') % (pbranch, branch)
142 142 )
143 143 exchange.pull(
144 144 self.repo, prepo, [prepo.lookup(h) for h in heads]
145 145 )
146 146 self.before()
147 147
148 148 def _rewritetags(self, source, revmap, data):
149 149 fp = stringio()
150 150 for line in data.splitlines():
151 151 s = line.split(b' ', 1)
152 152 if len(s) != 2:
153 153 self.ui.warn(_(b'invalid tag entry: "%s"\n') % line)
154 154 fp.write(b'%s\n' % line) # Bogus, but keep for hash stability
155 155 continue
156 156 revid = revmap.get(source.lookuprev(s[0]))
157 157 if not revid:
158 158 if s[0] == nodemod.nullhex:
159 159 revid = s[0]
160 160 else:
161 161 # missing, but keep for hash stability
162 162 self.ui.warn(_(b'missing tag entry: "%s"\n') % line)
163 163 fp.write(b'%s\n' % line)
164 164 continue
165 165 fp.write(b'%s %s\n' % (revid, s[1]))
166 166 return fp.getvalue()
167 167
168 168 def _rewritesubstate(self, source, data):
169 169 fp = stringio()
170 170 for line in data.splitlines():
171 171 s = line.split(b' ', 1)
172 172 if len(s) != 2:
173 173 continue
174 174
175 175 revid = s[0]
176 176 subpath = s[1]
177 177 if revid != nodemod.nullhex:
178 178 revmap = self.subrevmaps.get(subpath)
179 179 if revmap is None:
180 180 revmap = mapfile(
181 181 self.ui, self.repo.wjoin(subpath, b'.hg/shamap')
182 182 )
183 183 self.subrevmaps[subpath] = revmap
184 184
185 185 # It is reasonable that one or more of the subrepos don't
186 186 # need to be converted, in which case they can be cloned
187 187 # into place instead of converted. Therefore, only warn
188 188 # once.
189 189 msg = _(b'no ".hgsubstate" updates will be made for "%s"\n')
190 190 if len(revmap) == 0:
191 191 sub = self.repo.wvfs.reljoin(subpath, b'.hg')
192 192
193 193 if self.repo.wvfs.exists(sub):
194 194 self.ui.warn(msg % subpath)
195 195
196 196 newid = revmap.get(revid)
197 197 if not newid:
198 198 if len(revmap) > 0:
199 199 self.ui.warn(
200 200 _(b"%s is missing from %s/.hg/shamap\n")
201 201 % (revid, subpath)
202 202 )
203 203 else:
204 204 revid = newid
205 205
206 206 fp.write(b'%s %s\n' % (revid, subpath))
207 207
208 208 return fp.getvalue()
209 209
210 210 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
211 211 """Calculates the files from p2 that we need to pull in when merging p1
212 212 and p2, given that the merge is coming from the given source.
213 213
214 214 This prevents us from losing files that only exist in the target p2 and
215 215 that don't come from the source repo (like if you're merging multiple
216 216 repositories together).
217 217 """
218 218 anc = [p1ctx.ancestor(p2ctx)]
219 219 # Calculate what files are coming from p2
220 # TODO: mresult.commitinfo might be able to get that info
220 221 mresult = mergemod.calculateupdates(
221 222 self.repo,
222 223 p1ctx,
223 224 p2ctx,
224 225 anc,
225 226 branchmerge=True,
226 227 force=True,
227 228 acceptremote=False,
228 229 followcopies=False,
229 230 )
230 231
231 232 for file, (action, info, msg) in pycompat.iteritems(mresult.actions):
232 233 if source.targetfilebelongstosource(file):
233 234 # If the file belongs to the source repo, ignore the p2
234 235 # since it will be covered by the existing fileset.
235 236 continue
236 237
237 238 # If the file requires actual merging, abort. We don't have enough
238 239 # context to resolve merges correctly.
239 240 if action in [b'm', b'dm', b'cd', b'dc']:
240 241 raise error.Abort(
241 242 _(
242 243 b"unable to convert merge commit "
243 244 b"since target parents do not merge cleanly (file "
244 245 b"%s, parents %s and %s)"
245 246 )
246 247 % (file, p1ctx, p2ctx)
247 248 )
248 249 elif action == b'k':
249 250 # 'keep' means nothing changed from p1
250 251 continue
251 252 else:
252 253 # Any other change means we want to take the p2 version
253 254 yield file
254 255
255 256 def putcommit(
256 257 self, files, copies, parents, commit, source, revmap, full, cleanp2
257 258 ):
258 259 files = dict(files)
259 260
260 261 def getfilectx(repo, memctx, f):
261 262 if p2ctx and f in p2files and f not in copies:
262 263 self.ui.debug(b'reusing %s from p2\n' % f)
263 264 try:
264 265 return p2ctx[f]
265 266 except error.ManifestLookupError:
266 267 # If the file doesn't exist in p2, then we're syncing a
267 268 # delete, so just return None.
268 269 return None
269 270 try:
270 271 v = files[f]
271 272 except KeyError:
272 273 return None
273 274 data, mode = source.getfile(f, v)
274 275 if data is None:
275 276 return None
276 277 if f == b'.hgtags':
277 278 data = self._rewritetags(source, revmap, data)
278 279 if f == b'.hgsubstate':
279 280 data = self._rewritesubstate(source, data)
280 281 return context.memfilectx(
281 282 self.repo,
282 283 memctx,
283 284 f,
284 285 data,
285 286 b'l' in mode,
286 287 b'x' in mode,
287 288 copies.get(f),
288 289 )
289 290
290 291 pl = []
291 292 for p in parents:
292 293 if p not in pl:
293 294 pl.append(p)
294 295 parents = pl
295 296 nparents = len(parents)
296 297 if self.filemapmode and nparents == 1:
297 298 m1node = self.repo.changelog.read(nodemod.bin(parents[0]))[0]
298 299 parent = parents[0]
299 300
300 301 if len(parents) < 2:
301 302 parents.append(nodemod.nullid)
302 303 if len(parents) < 2:
303 304 parents.append(nodemod.nullid)
304 305 p2 = parents.pop(0)
305 306
306 307 text = commit.desc
307 308
308 309 sha1s = re.findall(sha1re, text)
309 310 for sha1 in sha1s:
310 311 oldrev = source.lookuprev(sha1)
311 312 newrev = revmap.get(oldrev)
312 313 if newrev is not None:
313 314 text = text.replace(sha1, newrev[: len(sha1)])
314 315
315 316 extra = commit.extra.copy()
316 317
317 318 sourcename = self.repo.ui.config(b'convert', b'hg.sourcename')
318 319 if sourcename:
319 320 extra[b'convert_source'] = sourcename
320 321
321 322 for label in (
322 323 b'source',
323 324 b'transplant_source',
324 325 b'rebase_source',
325 326 b'intermediate-source',
326 327 ):
327 328 node = extra.get(label)
328 329
329 330 if node is None:
330 331 continue
331 332
332 333 # Only transplant stores its reference in binary
333 334 if label == b'transplant_source':
334 335 node = nodemod.hex(node)
335 336
336 337 newrev = revmap.get(node)
337 338 if newrev is not None:
338 339 if label == b'transplant_source':
339 340 newrev = nodemod.bin(newrev)
340 341
341 342 extra[label] = newrev
342 343
343 344 if self.branchnames and commit.branch:
344 345 extra[b'branch'] = commit.branch
345 346 if commit.rev and commit.saverev:
346 347 extra[b'convert_revision'] = commit.rev
347 348
348 349 while parents:
349 350 p1 = p2
350 351 p2 = parents.pop(0)
351 352 p1ctx = self.repo[p1]
352 353 p2ctx = None
353 354 if p2 != nodemod.nullid:
354 355 p2ctx = self.repo[p2]
355 356 fileset = set(files)
356 357 if full:
357 358 fileset.update(self.repo[p1])
358 359 fileset.update(self.repo[p2])
359 360
360 361 if p2ctx:
361 362 p2files = set(cleanp2)
362 363 for file in self._calculatemergedfiles(source, p1ctx, p2ctx):
363 364 p2files.add(file)
364 365 fileset.add(file)
365 366
366 367 ctx = context.memctx(
367 368 self.repo,
368 369 (p1, p2),
369 370 text,
370 371 fileset,
371 372 getfilectx,
372 373 commit.author,
373 374 commit.date,
374 375 extra,
375 376 )
376 377
377 378 # We won't know if the conversion changes the node until after the
378 379 # commit, so copy the source's phase for now.
379 380 self.repo.ui.setconfig(
380 381 b'phases',
381 382 b'new-commit',
382 383 phases.phasenames[commit.phase],
383 384 b'convert',
384 385 )
385 386
386 387 with self.repo.transaction(b"convert") as tr:
387 388 if self.repo.ui.config(b'convert', b'hg.preserve-hash'):
388 389 origctx = commit.ctx
389 390 else:
390 391 origctx = None
391 392 node = nodemod.hex(self.repo.commitctx(ctx, origctx=origctx))
392 393
393 394 # If the node value has changed, but the phase is lower than
394 395 # draft, set it back to draft since it hasn't been exposed
395 396 # anywhere.
396 397 if commit.rev != node:
397 398 ctx = self.repo[node]
398 399 if ctx.phase() < phases.draft:
399 400 phases.registernew(
400 401 self.repo, tr, phases.draft, [ctx.node()]
401 402 )
402 403
403 404 text = b"(octopus merge fixup)\n"
404 405 p2 = node
405 406
406 407 if self.filemapmode and nparents == 1:
407 408 man = self.repo.manifestlog.getstorage(b'')
408 409 mnode = self.repo.changelog.read(nodemod.bin(p2))[0]
409 410 closed = b'close' in commit.extra
410 411 if not closed and not man.cmp(m1node, man.revision(mnode)):
411 412 self.ui.status(_(b"filtering out empty revision\n"))
412 413 self.repo.rollback(force=True)
413 414 return parent
414 415 return p2
415 416
416 417 def puttags(self, tags):
417 418 tagparent = self.repo.branchtip(self.tagsbranch, ignoremissing=True)
418 419 tagparent = tagparent or nodemod.nullid
419 420
420 421 oldlines = set()
421 422 for branch, heads in pycompat.iteritems(self.repo.branchmap()):
422 423 for h in heads:
423 424 if b'.hgtags' in self.repo[h]:
424 425 oldlines.update(
425 426 set(self.repo[h][b'.hgtags'].data().splitlines(True))
426 427 )
427 428 oldlines = sorted(list(oldlines))
428 429
429 430 newlines = sorted([(b"%s %s\n" % (tags[tag], tag)) for tag in tags])
430 431 if newlines == oldlines:
431 432 return None, None
432 433
433 434 # if the old and new tags match, then there is nothing to update
434 435 oldtags = set()
435 436 newtags = set()
436 437 for line in oldlines:
437 438 s = line.strip().split(b' ', 1)
438 439 if len(s) != 2:
439 440 continue
440 441 oldtags.add(s[1])
441 442 for line in newlines:
442 443 s = line.strip().split(b' ', 1)
443 444 if len(s) != 2:
444 445 continue
445 446 if s[1] not in oldtags:
446 447 newtags.add(s[1].strip())
447 448
448 449 if not newtags:
449 450 return None, None
450 451
451 452 data = b"".join(newlines)
452 453
453 454 def getfilectx(repo, memctx, f):
454 455 return context.memfilectx(repo, memctx, f, data, False, False, None)
455 456
456 457 self.ui.status(_(b"updating tags\n"))
457 458 date = b"%d 0" % int(time.mktime(time.gmtime()))
458 459 extra = {b'branch': self.tagsbranch}
459 460 ctx = context.memctx(
460 461 self.repo,
461 462 (tagparent, None),
462 463 b"update tags",
463 464 [b".hgtags"],
464 465 getfilectx,
465 466 b"convert-repo",
466 467 date,
467 468 extra,
468 469 )
469 470 node = self.repo.commitctx(ctx)
470 471 return nodemod.hex(node), nodemod.hex(tagparent)
471 472
472 473 def setfilemapmode(self, active):
473 474 self.filemapmode = active
474 475
475 476 def putbookmarks(self, updatedbookmark):
476 477 if not len(updatedbookmark):
477 478 return
478 479 wlock = lock = tr = None
479 480 try:
480 481 wlock = self.repo.wlock()
481 482 lock = self.repo.lock()
482 483 tr = self.repo.transaction(b'bookmark')
483 484 self.ui.status(_(b"updating bookmarks\n"))
484 485 destmarks = self.repo._bookmarks
485 486 changes = [
486 487 (bookmark, nodemod.bin(updatedbookmark[bookmark]))
487 488 for bookmark in updatedbookmark
488 489 ]
489 490 destmarks.applychanges(self.repo, tr, changes)
490 491 tr.close()
491 492 finally:
492 493 lockmod.release(lock, wlock, tr)
493 494
494 495 def hascommitfrommap(self, rev):
495 496 # the exact semantics of clonebranches is unclear so we can't say no
496 497 return rev in self.repo or self.clonebranches
497 498
498 499 def hascommitforsplicemap(self, rev):
499 500 if rev not in self.repo and self.clonebranches:
500 501 raise error.Abort(
501 502 _(
502 503 b'revision %s not found in destination '
503 504 b'repository (lookups with clonebranches=true '
504 505 b'are not implemented)'
505 506 )
506 507 % rev
507 508 )
508 509 return rev in self.repo
509 510
510 511
511 512 class mercurial_source(common.converter_source):
512 513 def __init__(self, ui, repotype, path, revs=None):
513 514 common.converter_source.__init__(self, ui, repotype, path, revs)
514 515 self.ignoreerrors = ui.configbool(b'convert', b'hg.ignoreerrors')
515 516 self.ignored = set()
516 517 self.saverev = ui.configbool(b'convert', b'hg.saverev')
517 518 try:
518 519 self.repo = hg.repository(self.ui, path)
519 520 # try to provoke an exception if this isn't really a hg
520 521 # repo, but some other bogus compatible-looking url
521 522 if not self.repo.local():
522 523 raise error.RepoError
523 524 except error.RepoError:
524 525 ui.traceback()
525 526 raise NoRepo(_(b"%s is not a local Mercurial repository") % path)
526 527 self.lastrev = None
527 528 self.lastctx = None
528 529 self._changescache = None, None
529 530 self.convertfp = None
530 531 # Restrict converted revisions to startrev descendants
531 532 startnode = ui.config(b'convert', b'hg.startrev')
532 533 hgrevs = ui.config(b'convert', b'hg.revs')
533 534 if hgrevs is None:
534 535 if startnode is not None:
535 536 try:
536 537 startnode = self.repo.lookup(startnode)
537 538 except error.RepoError:
538 539 raise error.Abort(
539 540 _(b'%s is not a valid start revision') % startnode
540 541 )
541 542 startrev = self.repo.changelog.rev(startnode)
542 543 children = {startnode: 1}
543 544 for r in self.repo.changelog.descendants([startrev]):
544 545 children[self.repo.changelog.node(r)] = 1
545 546 self.keep = children.__contains__
546 547 else:
547 548 self.keep = util.always
548 549 if revs:
549 550 self._heads = [self.repo.lookup(r) for r in revs]
550 551 else:
551 552 self._heads = self.repo.heads()
552 553 else:
553 554 if revs or startnode is not None:
554 555 raise error.Abort(
555 556 _(
556 557 b'hg.revs cannot be combined with '
557 558 b'hg.startrev or --rev'
558 559 )
559 560 )
560 561 nodes = set()
561 562 parents = set()
562 563 for r in scmutil.revrange(self.repo, [hgrevs]):
563 564 ctx = self.repo[r]
564 565 nodes.add(ctx.node())
565 566 parents.update(p.node() for p in ctx.parents())
566 567 self.keep = nodes.__contains__
567 568 self._heads = nodes - parents
568 569
569 570 def _changectx(self, rev):
570 571 if self.lastrev != rev:
571 572 self.lastctx = self.repo[rev]
572 573 self.lastrev = rev
573 574 return self.lastctx
574 575
575 576 def _parents(self, ctx):
576 577 return [p for p in ctx.parents() if p and self.keep(p.node())]
577 578
578 579 def getheads(self):
579 580 return [nodemod.hex(h) for h in self._heads if self.keep(h)]
580 581
581 582 def getfile(self, name, rev):
582 583 try:
583 584 fctx = self._changectx(rev)[name]
584 585 return fctx.data(), fctx.flags()
585 586 except error.LookupError:
586 587 return None, None
587 588
588 589 def _changedfiles(self, ctx1, ctx2):
589 590 ma, r = [], []
590 591 maappend = ma.append
591 592 rappend = r.append
592 593 d = ctx1.manifest().diff(ctx2.manifest())
593 594 for f, ((node1, flag1), (node2, flag2)) in pycompat.iteritems(d):
594 595 if node2 is None:
595 596 rappend(f)
596 597 else:
597 598 maappend(f)
598 599 return ma, r
599 600
600 601 def getchanges(self, rev, full):
601 602 ctx = self._changectx(rev)
602 603 parents = self._parents(ctx)
603 604 if full or not parents:
604 605 files = copyfiles = ctx.manifest()
605 606 if parents:
606 607 if self._changescache[0] == rev:
607 608 ma, r = self._changescache[1]
608 609 else:
609 610 ma, r = self._changedfiles(parents[0], ctx)
610 611 if not full:
611 612 files = ma + r
612 613 copyfiles = ma
613 614 # _getcopies() is also run for roots and before filtering so missing
614 615 # revlogs are detected early
615 616 copies = self._getcopies(ctx, parents, copyfiles)
616 617 cleanp2 = set()
617 618 if len(parents) == 2:
618 619 d = parents[1].manifest().diff(ctx.manifest(), clean=True)
619 620 for f, value in pycompat.iteritems(d):
620 621 if value is None:
621 622 cleanp2.add(f)
622 623 changes = [(f, rev) for f in files if f not in self.ignored]
623 624 changes.sort()
624 625 return changes, copies, cleanp2
625 626
626 627 def _getcopies(self, ctx, parents, files):
627 628 copies = {}
628 629 for name in files:
629 630 if name in self.ignored:
630 631 continue
631 632 try:
632 633 copysource = ctx.filectx(name).copysource()
633 634 if copysource in self.ignored:
634 635 continue
635 636 # Ignore copy sources not in parent revisions
636 637 if not any(copysource in p for p in parents):
637 638 continue
638 639 copies[name] = copysource
639 640 except TypeError:
640 641 pass
641 642 except error.LookupError as e:
642 643 if not self.ignoreerrors:
643 644 raise
644 645 self.ignored.add(name)
645 646 self.ui.warn(_(b'ignoring: %s\n') % e)
646 647 return copies
647 648
648 649 def getcommit(self, rev):
649 650 ctx = self._changectx(rev)
650 651 _parents = self._parents(ctx)
651 652 parents = [p.hex() for p in _parents]
652 653 optparents = [p.hex() for p in ctx.parents() if p and p not in _parents]
653 654 crev = rev
654 655
655 656 return common.commit(
656 657 author=ctx.user(),
657 658 date=dateutil.datestr(ctx.date(), b'%Y-%m-%d %H:%M:%S %1%2'),
658 659 desc=ctx.description(),
659 660 rev=crev,
660 661 parents=parents,
661 662 optparents=optparents,
662 663 branch=ctx.branch(),
663 664 extra=ctx.extra(),
664 665 sortkey=ctx.rev(),
665 666 saverev=self.saverev,
666 667 phase=ctx.phase(),
667 668 ctx=ctx,
668 669 )
669 670
670 671 def numcommits(self):
671 672 return len(self.repo)
672 673
673 674 def gettags(self):
674 675 # This will get written to .hgtags, filter non global tags out.
675 676 tags = [
676 677 t
677 678 for t in self.repo.tagslist()
678 679 if self.repo.tagtype(t[0]) == b'global'
679 680 ]
680 681 return {
681 682 name: nodemod.hex(node) for name, node in tags if self.keep(node)
682 683 }
683 684
684 685 def getchangedfiles(self, rev, i):
685 686 ctx = self._changectx(rev)
686 687 parents = self._parents(ctx)
687 688 if not parents and i is None:
688 689 i = 0
689 690 ma, r = ctx.manifest().keys(), []
690 691 else:
691 692 i = i or 0
692 693 ma, r = self._changedfiles(parents[i], ctx)
693 694 ma, r = [[f for f in l if f not in self.ignored] for l in (ma, r)]
694 695
695 696 if i == 0:
696 697 self._changescache = (rev, (ma, r))
697 698
698 699 return ma + r
699 700
700 701 def converted(self, rev, destrev):
701 702 if self.convertfp is None:
702 703 self.convertfp = open(self.repo.vfs.join(b'shamap'), b'ab')
703 704 self.convertfp.write(util.tonativeeol(b'%s %s\n' % (destrev, rev)))
704 705 self.convertfp.flush()
705 706
706 707 def before(self):
707 708 self.ui.debug(b'run hg source pre-conversion action\n')
708 709
709 710 def after(self):
710 711 self.ui.debug(b'run hg source post-conversion action\n')
711 712
712 713 def hasnativeorder(self):
713 714 return True
714 715
715 716 def hasnativeclose(self):
716 717 return True
717 718
718 719 def lookuprev(self, rev):
719 720 try:
720 721 return nodemod.hex(self.repo.lookup(rev))
721 722 except (error.RepoError, error.LookupError):
722 723 return None
723 724
724 725 def getbookmarks(self):
725 726 return bookmarks.listbookmarks(self.repo)
726 727
727 728 def checkrevformat(self, revstr, mapname=b'splicemap'):
728 729 """ Mercurial, revision string is a 40 byte hex """
729 730 self.checkhexformat(revstr, mapname)
@@ -1,2143 +1,2159 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import stat
12 12 import struct
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 addednodeid,
17 17 modifiednodeid,
18 18 nullid,
19 19 nullrev,
20 20 )
21 21 from .thirdparty import attr
22 22 from . import (
23 23 copies,
24 24 encoding,
25 25 error,
26 26 filemerge,
27 27 match as matchmod,
28 28 mergestate as mergestatemod,
29 29 obsutil,
30 30 pathutil,
31 31 pycompat,
32 32 scmutil,
33 33 subrepoutil,
34 34 util,
35 35 worker,
36 36 )
37 37
38 38 _pack = struct.pack
39 39 _unpack = struct.unpack
40 40
41 41
42 42 def _getcheckunknownconfig(repo, section, name):
43 43 config = repo.ui.config(section, name)
44 44 valid = [b'abort', b'ignore', b'warn']
45 45 if config not in valid:
46 46 validstr = b', '.join([b"'" + v + b"'" for v in valid])
47 47 raise error.ConfigError(
48 48 _(b"%s.%s not valid ('%s' is none of %s)")
49 49 % (section, name, config, validstr)
50 50 )
51 51 return config
52 52
53 53
54 54 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
55 55 if wctx.isinmemory():
56 56 # Nothing to do in IMM because nothing in the "working copy" can be an
57 57 # unknown file.
58 58 #
59 59 # Note that we should bail out here, not in ``_checkunknownfiles()``,
60 60 # because that function does other useful work.
61 61 return False
62 62
63 63 if f2 is None:
64 64 f2 = f
65 65 return (
66 66 repo.wvfs.audit.check(f)
67 67 and repo.wvfs.isfileorlink(f)
68 68 and repo.dirstate.normalize(f) not in repo.dirstate
69 69 and mctx[f2].cmp(wctx[f])
70 70 )
71 71
72 72
73 73 class _unknowndirschecker(object):
74 74 """
75 75 Look for any unknown files or directories that may have a path conflict
76 76 with a file. If any path prefix of the file exists as a file or link,
77 77 then it conflicts. If the file itself is a directory that contains any
78 78 file that is not tracked, then it conflicts.
79 79
80 80 Returns the shortest path at which a conflict occurs, or None if there is
81 81 no conflict.
82 82 """
83 83
84 84 def __init__(self):
85 85 # A set of paths known to be good. This prevents repeated checking of
86 86 # dirs. It will be updated with any new dirs that are checked and found
87 87 # to be safe.
88 88 self._unknowndircache = set()
89 89
90 90 # A set of paths that are known to be absent. This prevents repeated
91 91 # checking of subdirectories that are known not to exist. It will be
92 92 # updated with any new dirs that are checked and found to be absent.
93 93 self._missingdircache = set()
94 94
95 95 def __call__(self, repo, wctx, f):
96 96 if wctx.isinmemory():
97 97 # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
98 98 return False
99 99
100 100 # Check for path prefixes that exist as unknown files.
101 101 for p in reversed(list(pathutil.finddirs(f))):
102 102 if p in self._missingdircache:
103 103 return
104 104 if p in self._unknowndircache:
105 105 continue
106 106 if repo.wvfs.audit.check(p):
107 107 if (
108 108 repo.wvfs.isfileorlink(p)
109 109 and repo.dirstate.normalize(p) not in repo.dirstate
110 110 ):
111 111 return p
112 112 if not repo.wvfs.lexists(p):
113 113 self._missingdircache.add(p)
114 114 return
115 115 self._unknowndircache.add(p)
116 116
117 117 # Check if the file conflicts with a directory containing unknown files.
118 118 if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
119 119 # Does the directory contain any files that are not in the dirstate?
120 120 for p, dirs, files in repo.wvfs.walk(f):
121 121 for fn in files:
122 122 relf = util.pconvert(repo.wvfs.reljoin(p, fn))
123 123 relf = repo.dirstate.normalize(relf, isknown=True)
124 124 if relf not in repo.dirstate:
125 125 return f
126 126 return None
127 127
128 128
129 129 def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
130 130 """
131 131 Considers any actions that care about the presence of conflicting unknown
132 132 files. For some actions, the result is to abort; for others, it is to
133 133 choose a different action.
134 134 """
135 135 fileconflicts = set()
136 136 pathconflicts = set()
137 137 warnconflicts = set()
138 138 abortconflicts = set()
139 139 unknownconfig = _getcheckunknownconfig(repo, b'merge', b'checkunknown')
140 140 ignoredconfig = _getcheckunknownconfig(repo, b'merge', b'checkignored')
141 141 pathconfig = repo.ui.configbool(
142 142 b'experimental', b'merge.checkpathconflicts'
143 143 )
144 144 if not force:
145 145
146 146 def collectconflicts(conflicts, config):
147 147 if config == b'abort':
148 148 abortconflicts.update(conflicts)
149 149 elif config == b'warn':
150 150 warnconflicts.update(conflicts)
151 151
152 152 checkunknowndirs = _unknowndirschecker()
153 153 for f, (m, args, msg) in pycompat.iteritems(actions):
154 154 if m in (
155 155 mergestatemod.ACTION_CREATED,
156 156 mergestatemod.ACTION_DELETED_CHANGED,
157 157 ):
158 158 if _checkunknownfile(repo, wctx, mctx, f):
159 159 fileconflicts.add(f)
160 160 elif pathconfig and f not in wctx:
161 161 path = checkunknowndirs(repo, wctx, f)
162 162 if path is not None:
163 163 pathconflicts.add(path)
164 164 elif m == mergestatemod.ACTION_LOCAL_DIR_RENAME_GET:
165 165 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
166 166 fileconflicts.add(f)
167 167
168 168 allconflicts = fileconflicts | pathconflicts
169 169 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)}
170 170 unknownconflicts = allconflicts - ignoredconflicts
171 171 collectconflicts(ignoredconflicts, ignoredconfig)
172 172 collectconflicts(unknownconflicts, unknownconfig)
173 173 else:
174 174 for f, (m, args, msg) in pycompat.iteritems(actions):
175 175 if m == mergestatemod.ACTION_CREATED_MERGE:
176 176 fl2, anc = args
177 177 different = _checkunknownfile(repo, wctx, mctx, f)
178 178 if repo.dirstate._ignore(f):
179 179 config = ignoredconfig
180 180 else:
181 181 config = unknownconfig
182 182
183 183 # The behavior when force is True is described by this table:
184 184 # config different mergeforce | action backup
185 185 # * n * | get n
186 186 # * y y | merge -
187 187 # abort y n | merge - (1)
188 188 # warn y n | warn + get y
189 189 # ignore y n | get y
190 190 #
191 191 # (1) this is probably the wrong behavior here -- we should
192 192 # probably abort, but some actions like rebases currently
193 193 # don't like an abort happening in the middle of
194 194 # merge.update.
195 195 if not different:
196 196 actions[f] = (
197 197 mergestatemod.ACTION_GET,
198 198 (fl2, False),
199 199 b'remote created',
200 200 )
201 201 elif mergeforce or config == b'abort':
202 202 actions[f] = (
203 203 mergestatemod.ACTION_MERGE,
204 204 (f, f, None, False, anc),
205 205 b'remote differs from untracked local',
206 206 )
207 207 elif config == b'abort':
208 208 abortconflicts.add(f)
209 209 else:
210 210 if config == b'warn':
211 211 warnconflicts.add(f)
212 212 actions[f] = (
213 213 mergestatemod.ACTION_GET,
214 214 (fl2, True),
215 215 b'remote created',
216 216 )
217 217
218 218 for f in sorted(abortconflicts):
219 219 warn = repo.ui.warn
220 220 if f in pathconflicts:
221 221 if repo.wvfs.isfileorlink(f):
222 222 warn(_(b"%s: untracked file conflicts with directory\n") % f)
223 223 else:
224 224 warn(_(b"%s: untracked directory conflicts with file\n") % f)
225 225 else:
226 226 warn(_(b"%s: untracked file differs\n") % f)
227 227 if abortconflicts:
228 228 raise error.Abort(
229 229 _(
230 230 b"untracked files in working directory "
231 231 b"differ from files in requested revision"
232 232 )
233 233 )
234 234
235 235 for f in sorted(warnconflicts):
236 236 if repo.wvfs.isfileorlink(f):
237 237 repo.ui.warn(_(b"%s: replacing untracked file\n") % f)
238 238 else:
239 239 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
240 240
241 241 for f, (m, args, msg) in pycompat.iteritems(actions):
242 242 if m == mergestatemod.ACTION_CREATED:
243 243 backup = (
244 244 f in fileconflicts
245 245 or f in pathconflicts
246 246 or any(p in pathconflicts for p in pathutil.finddirs(f))
247 247 )
248 248 (flags,) = args
249 249 actions[f] = (mergestatemod.ACTION_GET, (flags, backup), msg)
250 250
251 251
252 252 def _forgetremoved(wctx, mctx, branchmerge):
253 253 """
254 254 Forget removed files
255 255
256 256 If we're jumping between revisions (as opposed to merging), and if
257 257 neither the working directory nor the target rev has the file,
258 258 then we need to remove it from the dirstate, to prevent the
259 259 dirstate from listing the file when it is no longer in the
260 260 manifest.
261 261
262 262 If we're merging, and the other revision has removed a file
263 263 that is not present in the working directory, we need to mark it
264 264 as removed.
265 265 """
266 266
267 267 actions = {}
268 268 m = mergestatemod.ACTION_FORGET
269 269 if branchmerge:
270 270 m = mergestatemod.ACTION_REMOVE
271 271 for f in wctx.deleted():
272 272 if f not in mctx:
273 273 actions[f] = m, None, b"forget deleted"
274 274
275 275 if not branchmerge:
276 276 for f in wctx.removed():
277 277 if f not in mctx:
278 278 actions[f] = (
279 279 mergestatemod.ACTION_FORGET,
280 280 None,
281 281 b"forget removed",
282 282 )
283 283
284 284 return actions
285 285
286 286
287 287 def _checkcollision(repo, wmf, actions):
288 288 """
289 289 Check for case-folding collisions.
290 290 """
291 291 # If the repo is narrowed, filter out files outside the narrowspec.
292 292 narrowmatch = repo.narrowmatch()
293 293 if not narrowmatch.always():
294 294 pmmf = set(wmf.walk(narrowmatch))
295 295 if actions:
296 296 narrowactions = {}
297 297 for m, actionsfortype in pycompat.iteritems(actions):
298 298 narrowactions[m] = []
299 299 for (f, args, msg) in actionsfortype:
300 300 if narrowmatch(f):
301 301 narrowactions[m].append((f, args, msg))
302 302 actions = narrowactions
303 303 else:
304 304 # build provisional merged manifest up
305 305 pmmf = set(wmf)
306 306
307 307 if actions:
308 308 # KEEP and EXEC are no-op
309 309 for m in (
310 310 mergestatemod.ACTION_ADD,
311 311 mergestatemod.ACTION_ADD_MODIFIED,
312 312 mergestatemod.ACTION_FORGET,
313 313 mergestatemod.ACTION_GET,
314 314 mergestatemod.ACTION_CHANGED_DELETED,
315 315 mergestatemod.ACTION_DELETED_CHANGED,
316 316 ):
317 317 for f, args, msg in actions[m]:
318 318 pmmf.add(f)
319 319 for f, args, msg in actions[mergestatemod.ACTION_REMOVE]:
320 320 pmmf.discard(f)
321 321 for f, args, msg in actions[mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL]:
322 322 f2, flags = args
323 323 pmmf.discard(f2)
324 324 pmmf.add(f)
325 325 for f, args, msg in actions[mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]:
326 326 pmmf.add(f)
327 327 for f, args, msg in actions[mergestatemod.ACTION_MERGE]:
328 328 f1, f2, fa, move, anc = args
329 329 if move:
330 330 pmmf.discard(f1)
331 331 pmmf.add(f)
332 332
333 333 # check case-folding collision in provisional merged manifest
334 334 foldmap = {}
335 335 for f in pmmf:
336 336 fold = util.normcase(f)
337 337 if fold in foldmap:
338 338 raise error.Abort(
339 339 _(b"case-folding collision between %s and %s")
340 340 % (f, foldmap[fold])
341 341 )
342 342 foldmap[fold] = f
343 343
344 344 # check case-folding of directories
345 345 foldprefix = unfoldprefix = lastfull = b''
346 346 for fold, f in sorted(foldmap.items()):
347 347 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
348 348 # the folded prefix matches but actual casing is different
349 349 raise error.Abort(
350 350 _(b"case-folding collision between %s and directory of %s")
351 351 % (lastfull, f)
352 352 )
353 353 foldprefix = fold + b'/'
354 354 unfoldprefix = f + b'/'
355 355 lastfull = f
356 356
357 357
358 358 def driverpreprocess(repo, ms, wctx, labels=None):
359 359 """run the preprocess step of the merge driver, if any
360 360
361 361 This is currently not implemented -- it's an extension point."""
362 362 return True
363 363
364 364
365 365 def driverconclude(repo, ms, wctx, labels=None):
366 366 """run the conclude step of the merge driver, if any
367 367
368 368 This is currently not implemented -- it's an extension point."""
369 369 return True
370 370
371 371
372 372 def _filesindirs(repo, manifest, dirs):
373 373 """
374 374 Generator that yields pairs of all the files in the manifest that are found
375 375 inside the directories listed in dirs, and which directory they are found
376 376 in.
377 377 """
378 378 for f in manifest:
379 379 for p in pathutil.finddirs(f):
380 380 if p in dirs:
381 381 yield f, p
382 382 break
383 383
384 384
385 385 def checkpathconflicts(repo, wctx, mctx, actions):
386 386 """
387 387 Check if any actions introduce path conflicts in the repository, updating
388 388 actions to record or handle the path conflict accordingly.
389 389 """
390 390 mf = wctx.manifest()
391 391
392 392 # The set of local files that conflict with a remote directory.
393 393 localconflicts = set()
394 394
395 395 # The set of directories that conflict with a remote file, and so may cause
396 396 # conflicts if they still contain any files after the merge.
397 397 remoteconflicts = set()
398 398
399 399 # The set of directories that appear as both a file and a directory in the
400 400 # remote manifest. These indicate an invalid remote manifest, which
401 401 # can't be updated to cleanly.
402 402 invalidconflicts = set()
403 403
404 404 # The set of directories that contain files that are being created.
405 405 createdfiledirs = set()
406 406
407 407 # The set of files deleted by all the actions.
408 408 deletedfiles = set()
409 409
410 410 for f, (m, args, msg) in actions.items():
411 411 if m in (
412 412 mergestatemod.ACTION_CREATED,
413 413 mergestatemod.ACTION_DELETED_CHANGED,
414 414 mergestatemod.ACTION_MERGE,
415 415 mergestatemod.ACTION_CREATED_MERGE,
416 416 ):
417 417 # This action may create a new local file.
418 418 createdfiledirs.update(pathutil.finddirs(f))
419 419 if mf.hasdir(f):
420 420 # The file aliases a local directory. This might be ok if all
421 421 # the files in the local directory are being deleted. This
422 422 # will be checked once we know what all the deleted files are.
423 423 remoteconflicts.add(f)
424 424 # Track the names of all deleted files.
425 425 if m == mergestatemod.ACTION_REMOVE:
426 426 deletedfiles.add(f)
427 427 if m == mergestatemod.ACTION_MERGE:
428 428 f1, f2, fa, move, anc = args
429 429 if move:
430 430 deletedfiles.add(f1)
431 431 if m == mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL:
432 432 f2, flags = args
433 433 deletedfiles.add(f2)
434 434
435 435 # Check all directories that contain created files for path conflicts.
436 436 for p in createdfiledirs:
437 437 if p in mf:
438 438 if p in mctx:
439 439 # A file is in a directory which aliases both a local
440 440 # and a remote file. This is an internal inconsistency
441 441 # within the remote manifest.
442 442 invalidconflicts.add(p)
443 443 else:
444 444 # A file is in a directory which aliases a local file.
445 445 # We will need to rename the local file.
446 446 localconflicts.add(p)
447 447 if p in actions and actions[p][0] in (
448 448 mergestatemod.ACTION_CREATED,
449 449 mergestatemod.ACTION_DELETED_CHANGED,
450 450 mergestatemod.ACTION_MERGE,
451 451 mergestatemod.ACTION_CREATED_MERGE,
452 452 ):
453 453 # The file is in a directory which aliases a remote file.
454 454 # This is an internal inconsistency within the remote
455 455 # manifest.
456 456 invalidconflicts.add(p)
457 457
458 458 # Rename all local conflicting files that have not been deleted.
459 459 for p in localconflicts:
460 460 if p not in deletedfiles:
461 461 ctxname = bytes(wctx).rstrip(b'+')
462 462 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
463 463 porig = wctx[p].copysource() or p
464 464 actions[pnew] = (
465 465 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
466 466 (p, porig),
467 467 b'local path conflict',
468 468 )
469 469 actions[p] = (
470 470 mergestatemod.ACTION_PATH_CONFLICT,
471 471 (pnew, b'l'),
472 472 b'path conflict',
473 473 )
474 474
475 475 if remoteconflicts:
476 476 # Check if all files in the conflicting directories have been removed.
477 477 ctxname = bytes(mctx).rstrip(b'+')
478 478 for f, p in _filesindirs(repo, mf, remoteconflicts):
479 479 if f not in deletedfiles:
480 480 m, args, msg = actions[p]
481 481 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
482 482 if m in (
483 483 mergestatemod.ACTION_DELETED_CHANGED,
484 484 mergestatemod.ACTION_MERGE,
485 485 ):
486 486 # Action was merge, just update target.
487 487 actions[pnew] = (m, args, msg)
488 488 else:
489 489 # Action was create, change to renamed get action.
490 490 fl = args[0]
491 491 actions[pnew] = (
492 492 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
493 493 (p, fl),
494 494 b'remote path conflict',
495 495 )
496 496 actions[p] = (
497 497 mergestatemod.ACTION_PATH_CONFLICT,
498 498 (pnew, mergestatemod.ACTION_REMOVE),
499 499 b'path conflict',
500 500 )
501 501 remoteconflicts.remove(p)
502 502 break
503 503
504 504 if invalidconflicts:
505 505 for p in invalidconflicts:
506 506 repo.ui.warn(_(b"%s: is both a file and a directory\n") % p)
507 507 raise error.Abort(_(b"destination manifest contains path conflicts"))
508 508
509 509
510 510 def _filternarrowactions(narrowmatch, branchmerge, actions):
511 511 """
512 512 Filters out actions that can ignored because the repo is narrowed.
513 513
514 514 Raise an exception if the merge cannot be completed because the repo is
515 515 narrowed.
516 516 """
517 517 nooptypes = {b'k'} # TODO: handle with nonconflicttypes
518 518 nonconflicttypes = set(b'a am c cm f g gs r e'.split())
519 519 # We mutate the items in the dict during iteration, so iterate
520 520 # over a copy.
521 521 for f, action in list(actions.items()):
522 522 if narrowmatch(f):
523 523 pass
524 524 elif not branchmerge:
525 525 del actions[f] # just updating, ignore changes outside clone
526 526 elif action[0] in nooptypes:
527 527 del actions[f] # merge does not affect file
528 528 elif action[0] in nonconflicttypes:
529 529 raise error.Abort(
530 530 _(
531 531 b'merge affects file \'%s\' outside narrow, '
532 532 b'which is not yet supported'
533 533 )
534 534 % f,
535 535 hint=_(b'merging in the other direction may work'),
536 536 )
537 537 else:
538 538 raise error.Abort(
539 539 _(b'conflict in file \'%s\' is outside narrow clone') % f
540 540 )
541 541
542 542
543 543 class mergeresult(object):
544 544 ''''An object representing result of merging manifests.
545 545
546 546 It has information about what actions need to be performed on dirstate
547 547 mapping of divergent renames and other such cases. '''
548 548
549 def __init__(self, actions, diverge, renamedelete):
549 def __init__(self, actions, diverge, renamedelete, commitinfo):
550 550 """
551 551 actions: dict of filename as keys and action related info as values
552 552 diverge: mapping of source name -> list of dest name for
553 553 divergent renames
554 554 renamedelete: mapping of source name -> list of destinations for files
555 555 deleted on one side and renamed on other.
556 commitinfo: dict containing data which should be used on commit
557 contains a filename -> info mapping
556 558 """
557 559
558 560 self._actions = actions
559 561 self._diverge = diverge
560 562 self._renamedelete = renamedelete
563 self._commitinfo = commitinfo
561 564
562 565 @property
563 566 def actions(self):
564 567 return self._actions
565 568
566 569 @property
567 570 def diverge(self):
568 571 return self._diverge
569 572
570 573 @property
571 574 def renamedelete(self):
572 575 return self._renamedelete
573 576
577 @property
578 def commitinfo(self):
579 return self._commitinfo
580
574 581 def setactions(self, actions):
575 582 self._actions = actions
576 583
577 584
578 585 def manifestmerge(
579 586 repo,
580 587 wctx,
581 588 p2,
582 589 pa,
583 590 branchmerge,
584 591 force,
585 592 matcher,
586 593 acceptremote,
587 594 followcopies,
588 595 forcefulldiff=False,
589 596 ):
590 597 """
591 598 Merge wctx and p2 with ancestor pa and generate merge action list
592 599
593 600 branchmerge and force are as passed in to update
594 601 matcher = matcher to filter file lists
595 602 acceptremote = accept the incoming changes without prompting
596 603
597 604 Returns an object of mergeresult class
598 605 """
599 606 if matcher is not None and matcher.always():
600 607 matcher = None
601 608
602 609 # manifests fetched in order are going to be faster, so prime the caches
603 610 [
604 611 x.manifest()
605 612 for x in sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)
606 613 ]
607 614
608 615 branch_copies1 = copies.branch_copies()
609 616 branch_copies2 = copies.branch_copies()
610 617 diverge = {}
618 # information from merge which is needed at commit time
619 # for example choosing filelog of which parent to commit
620 # TODO: use specific constants in future for this mapping
621 commitinfo = {}
611 622 if followcopies:
612 623 branch_copies1, branch_copies2, diverge = copies.mergecopies(
613 624 repo, wctx, p2, pa
614 625 )
615 626
616 627 boolbm = pycompat.bytestr(bool(branchmerge))
617 628 boolf = pycompat.bytestr(bool(force))
618 629 boolm = pycompat.bytestr(bool(matcher))
619 630 repo.ui.note(_(b"resolving manifests\n"))
620 631 repo.ui.debug(
621 632 b" branchmerge: %s, force: %s, partial: %s\n" % (boolbm, boolf, boolm)
622 633 )
623 634 repo.ui.debug(b" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
624 635
625 636 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
626 637 copied1 = set(branch_copies1.copy.values())
627 638 copied1.update(branch_copies1.movewithdir.values())
628 639 copied2 = set(branch_copies2.copy.values())
629 640 copied2.update(branch_copies2.movewithdir.values())
630 641
631 642 if b'.hgsubstate' in m1 and wctx.rev() is None:
632 643 # Check whether sub state is modified, and overwrite the manifest
633 644 # to flag the change. If wctx is a committed revision, we shouldn't
634 645 # care for the dirty state of the working directory.
635 646 if any(wctx.sub(s).dirty() for s in wctx.substate):
636 647 m1[b'.hgsubstate'] = modifiednodeid
637 648
638 649 # Don't use m2-vs-ma optimization if:
639 650 # - ma is the same as m1 or m2, which we're just going to diff again later
640 651 # - The caller specifically asks for a full diff, which is useful during bid
641 652 # merge.
642 653 if pa not in ([wctx, p2] + wctx.parents()) and not forcefulldiff:
643 654 # Identify which files are relevant to the merge, so we can limit the
644 655 # total m1-vs-m2 diff to just those files. This has significant
645 656 # performance benefits in large repositories.
646 657 relevantfiles = set(ma.diff(m2).keys())
647 658
648 659 # For copied and moved files, we need to add the source file too.
649 660 for copykey, copyvalue in pycompat.iteritems(branch_copies1.copy):
650 661 if copyvalue in relevantfiles:
651 662 relevantfiles.add(copykey)
652 663 for movedirkey in branch_copies1.movewithdir:
653 664 relevantfiles.add(movedirkey)
654 665 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
655 666 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
656 667
657 668 diff = m1.diff(m2, match=matcher)
658 669
659 670 actions = {}
660 671 for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
661 672 if n1 and n2: # file exists on both local and remote side
662 673 if f not in ma:
663 674 # TODO: what if they're renamed from different sources?
664 675 fa = branch_copies1.copy.get(
665 676 f, None
666 677 ) or branch_copies2.copy.get(f, None)
667 678 if fa is not None:
668 679 actions[f] = (
669 680 mergestatemod.ACTION_MERGE,
670 681 (f, f, fa, False, pa.node()),
671 682 b'both renamed from %s' % fa,
672 683 )
673 684 else:
674 685 actions[f] = (
675 686 mergestatemod.ACTION_MERGE,
676 687 (f, f, None, False, pa.node()),
677 688 b'both created',
678 689 )
679 690 else:
680 691 a = ma[f]
681 692 fla = ma.flags(f)
682 693 nol = b'l' not in fl1 + fl2 + fla
683 694 if n2 == a and fl2 == fla:
684 695 actions[f] = (
685 696 mergestatemod.ACTION_KEEP,
686 697 (),
687 698 b'remote unchanged',
688 699 )
689 700 elif n1 == a and fl1 == fla: # local unchanged - use remote
690 701 if n1 == n2: # optimization: keep local content
691 702 actions[f] = (
692 703 mergestatemod.ACTION_EXEC,
693 704 (fl2,),
694 705 b'update permissions',
695 706 )
696 707 else:
697 708 actions[f] = (
698 709 mergestatemod.ACTION_GET_OTHER_AND_STORE
699 710 if branchmerge
700 711 else mergestatemod.ACTION_GET,
701 712 (fl2, False),
702 713 b'remote is newer',
703 714 )
715 if branchmerge:
716 commitinfo[f] = b'other'
704 717 elif nol and n2 == a: # remote only changed 'x'
705 718 actions[f] = (
706 719 mergestatemod.ACTION_EXEC,
707 720 (fl2,),
708 721 b'update permissions',
709 722 )
710 723 elif nol and n1 == a: # local only changed 'x'
711 724 actions[f] = (
712 725 mergestatemod.ACTION_GET_OTHER_AND_STORE
713 726 if branchmerge
714 727 else mergestatemod.ACTION_GET,
715 728 (fl1, False),
716 729 b'remote is newer',
717 730 )
731 if branchmerge:
732 commitinfo[f] = b'other'
718 733 else: # both changed something
719 734 actions[f] = (
720 735 mergestatemod.ACTION_MERGE,
721 736 (f, f, f, False, pa.node()),
722 737 b'versions differ',
723 738 )
724 739 elif n1: # file exists only on local side
725 740 if f in copied2:
726 741 pass # we'll deal with it on m2 side
727 742 elif (
728 743 f in branch_copies1.movewithdir
729 744 ): # directory rename, move local
730 745 f2 = branch_copies1.movewithdir[f]
731 746 if f2 in m2:
732 747 actions[f2] = (
733 748 mergestatemod.ACTION_MERGE,
734 749 (f, f2, None, True, pa.node()),
735 750 b'remote directory rename, both created',
736 751 )
737 752 else:
738 753 actions[f2] = (
739 754 mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,
740 755 (f, fl1),
741 756 b'remote directory rename - move from %s' % f,
742 757 )
743 758 elif f in branch_copies1.copy:
744 759 f2 = branch_copies1.copy[f]
745 760 actions[f] = (
746 761 mergestatemod.ACTION_MERGE,
747 762 (f, f2, f2, False, pa.node()),
748 763 b'local copied/moved from %s' % f2,
749 764 )
750 765 elif f in ma: # clean, a different, no remote
751 766 if n1 != ma[f]:
752 767 if acceptremote:
753 768 actions[f] = (
754 769 mergestatemod.ACTION_REMOVE,
755 770 None,
756 771 b'remote delete',
757 772 )
758 773 else:
759 774 actions[f] = (
760 775 mergestatemod.ACTION_CHANGED_DELETED,
761 776 (f, None, f, False, pa.node()),
762 777 b'prompt changed/deleted',
763 778 )
764 779 elif n1 == addednodeid:
765 780 # This file was locally added. We should forget it instead of
766 781 # deleting it.
767 782 actions[f] = (
768 783 mergestatemod.ACTION_FORGET,
769 784 None,
770 785 b'remote deleted',
771 786 )
772 787 else:
773 788 actions[f] = (
774 789 mergestatemod.ACTION_REMOVE,
775 790 None,
776 791 b'other deleted',
777 792 )
778 793 elif n2: # file exists only on remote side
779 794 if f in copied1:
780 795 pass # we'll deal with it on m1 side
781 796 elif f in branch_copies2.movewithdir:
782 797 f2 = branch_copies2.movewithdir[f]
783 798 if f2 in m1:
784 799 actions[f2] = (
785 800 mergestatemod.ACTION_MERGE,
786 801 (f2, f, None, False, pa.node()),
787 802 b'local directory rename, both created',
788 803 )
789 804 else:
790 805 actions[f2] = (
791 806 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
792 807 (f, fl2),
793 808 b'local directory rename - get from %s' % f,
794 809 )
795 810 elif f in branch_copies2.copy:
796 811 f2 = branch_copies2.copy[f]
797 812 if f2 in m2:
798 813 actions[f] = (
799 814 mergestatemod.ACTION_MERGE,
800 815 (f2, f, f2, False, pa.node()),
801 816 b'remote copied from %s' % f2,
802 817 )
803 818 else:
804 819 actions[f] = (
805 820 mergestatemod.ACTION_MERGE,
806 821 (f2, f, f2, True, pa.node()),
807 822 b'remote moved from %s' % f2,
808 823 )
809 824 elif f not in ma:
810 825 # local unknown, remote created: the logic is described by the
811 826 # following table:
812 827 #
813 828 # force branchmerge different | action
814 829 # n * * | create
815 830 # y n * | create
816 831 # y y n | create
817 832 # y y y | merge
818 833 #
819 834 # Checking whether the files are different is expensive, so we
820 835 # don't do that when we can avoid it.
821 836 if not force:
822 837 actions[f] = (
823 838 mergestatemod.ACTION_CREATED,
824 839 (fl2,),
825 840 b'remote created',
826 841 )
827 842 elif not branchmerge:
828 843 actions[f] = (
829 844 mergestatemod.ACTION_CREATED,
830 845 (fl2,),
831 846 b'remote created',
832 847 )
833 848 else:
834 849 actions[f] = (
835 850 mergestatemod.ACTION_CREATED_MERGE,
836 851 (fl2, pa.node()),
837 852 b'remote created, get or merge',
838 853 )
839 854 elif n2 != ma[f]:
840 855 df = None
841 856 for d in branch_copies1.dirmove:
842 857 if f.startswith(d):
843 858 # new file added in a directory that was moved
844 859 df = branch_copies1.dirmove[d] + f[len(d) :]
845 860 break
846 861 if df is not None and df in m1:
847 862 actions[df] = (
848 863 mergestatemod.ACTION_MERGE,
849 864 (df, f, f, False, pa.node()),
850 865 b'local directory rename - respect move '
851 866 b'from %s' % f,
852 867 )
853 868 elif acceptremote:
854 869 actions[f] = (
855 870 mergestatemod.ACTION_CREATED,
856 871 (fl2,),
857 872 b'remote recreating',
858 873 )
859 874 else:
860 875 actions[f] = (
861 876 mergestatemod.ACTION_DELETED_CHANGED,
862 877 (None, f, f, False, pa.node()),
863 878 b'prompt deleted/changed',
864 879 )
865 880
866 881 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'):
867 882 # If we are merging, look for path conflicts.
868 883 checkpathconflicts(repo, wctx, p2, actions)
869 884
870 885 narrowmatch = repo.narrowmatch()
871 886 if not narrowmatch.always():
872 887 # Updates "actions" in place
873 888 _filternarrowactions(narrowmatch, branchmerge, actions)
874 889
875 890 renamedelete = branch_copies1.renamedelete
876 891 renamedelete.update(branch_copies2.renamedelete)
877 892
878 return mergeresult(actions, diverge, renamedelete)
893 return mergeresult(actions, diverge, renamedelete, commitinfo)
879 894
880 895
881 896 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
882 897 """Resolves false conflicts where the nodeid changed but the content
883 898 remained the same."""
884 899 # We force a copy of actions.items() because we're going to mutate
885 900 # actions as we resolve trivial conflicts.
886 901 for f, (m, args, msg) in list(actions.items()):
887 902 if (
888 903 m == mergestatemod.ACTION_CHANGED_DELETED
889 904 and f in ancestor
890 905 and not wctx[f].cmp(ancestor[f])
891 906 ):
892 907 # local did change but ended up with same content
893 908 actions[f] = mergestatemod.ACTION_REMOVE, None, b'prompt same'
894 909 elif (
895 910 m == mergestatemod.ACTION_DELETED_CHANGED
896 911 and f in ancestor
897 912 and not mctx[f].cmp(ancestor[f])
898 913 ):
899 914 # remote did change but ended up with same content
900 915 del actions[f] # don't get = keep local deleted
901 916
902 917
903 918 def calculateupdates(
904 919 repo,
905 920 wctx,
906 921 mctx,
907 922 ancestors,
908 923 branchmerge,
909 924 force,
910 925 acceptremote,
911 926 followcopies,
912 927 matcher=None,
913 928 mergeforce=False,
914 929 ):
915 930 """
916 931 Calculate the actions needed to merge mctx into wctx using ancestors
917 932
918 933 Uses manifestmerge() to merge manifest and get list of actions required to
919 934 perform for merging two manifests. If there are multiple ancestors, uses bid
920 935 merge if enabled.
921 936
922 937 Also filters out actions which are unrequired if repository is sparse.
923 938
924 939 Returns mergeresult object same as manifestmerge().
925 940 """
926 941 # Avoid cycle.
927 942 from . import sparse
928 943
929 944 if len(ancestors) == 1: # default
930 945 mresult = manifestmerge(
931 946 repo,
932 947 wctx,
933 948 mctx,
934 949 ancestors[0],
935 950 branchmerge,
936 951 force,
937 952 matcher,
938 953 acceptremote,
939 954 followcopies,
940 955 )
941 956 _checkunknownfiles(repo, wctx, mctx, force, mresult.actions, mergeforce)
942 957
943 958 else: # only when merge.preferancestor=* - the default
944 959 repo.ui.note(
945 960 _(b"note: merging %s and %s using bids from ancestors %s\n")
946 961 % (
947 962 wctx,
948 963 mctx,
949 964 _(b' and ').join(pycompat.bytestr(anc) for anc in ancestors),
950 965 )
951 966 )
952 967
953 968 # Call for bids
954 969 fbids = (
955 970 {}
956 971 ) # mapping filename to bids (action method to list af actions)
957 972 diverge, renamedelete = None, None
958 973 for ancestor in ancestors:
959 974 repo.ui.note(_(b'\ncalculating bids for ancestor %s\n') % ancestor)
960 975 mresult1 = manifestmerge(
961 976 repo,
962 977 wctx,
963 978 mctx,
964 979 ancestor,
965 980 branchmerge,
966 981 force,
967 982 matcher,
968 983 acceptremote,
969 984 followcopies,
970 985 forcefulldiff=True,
971 986 )
972 987 _checkunknownfiles(
973 988 repo, wctx, mctx, force, mresult1.actions, mergeforce
974 989 )
975 990
976 991 # Track the shortest set of warning on the theory that bid
977 992 # merge will correctly incorporate more information
978 993 if diverge is None or len(mresult1.diverge) < len(diverge):
979 994 diverge = mresult1.diverge
980 995 if renamedelete is None or len(renamedelete) < len(
981 996 mresult1.renamedelete
982 997 ):
983 998 renamedelete = mresult1.renamedelete
984 999
985 1000 for f, a in sorted(pycompat.iteritems(mresult1.actions)):
986 1001 m, args, msg = a
987 1002 if m == mergestatemod.ACTION_GET_OTHER_AND_STORE:
988 1003 m = mergestatemod.ACTION_GET
989 1004 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
990 1005 if f in fbids:
991 1006 d = fbids[f]
992 1007 if m in d:
993 1008 d[m].append(a)
994 1009 else:
995 1010 d[m] = [a]
996 1011 else:
997 1012 fbids[f] = {m: [a]}
998 1013
999 1014 # Pick the best bid for each file
1000 1015 repo.ui.note(_(b'\nauction for merging merge bids\n'))
1001 1016 actions = {}
1002 1017 for f, bids in sorted(fbids.items()):
1003 1018 # bids is a mapping from action method to list af actions
1004 1019 # Consensus?
1005 1020 if len(bids) == 1: # all bids are the same kind of method
1006 1021 m, l = list(bids.items())[0]
1007 1022 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1008 1023 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m))
1009 1024 actions[f] = l[0]
1010 1025 continue
1011 1026 # If keep is an option, just do it.
1012 1027 if mergestatemod.ACTION_KEEP in bids:
1013 1028 repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
1014 1029 actions[f] = bids[mergestatemod.ACTION_KEEP][0]
1015 1030 continue
1016 1031 # If there are gets and they all agree [how could they not?], do it.
1017 1032 if mergestatemod.ACTION_GET in bids:
1018 1033 ga0 = bids[mergestatemod.ACTION_GET][0]
1019 1034 if all(a == ga0 for a in bids[mergestatemod.ACTION_GET][1:]):
1020 1035 repo.ui.note(_(b" %s: picking 'get' action\n") % f)
1021 1036 actions[f] = ga0
1022 1037 continue
1023 1038 # TODO: Consider other simple actions such as mode changes
1024 1039 # Handle inefficient democrazy.
1025 1040 repo.ui.note(_(b' %s: multiple bids for merge action:\n') % f)
1026 1041 for m, l in sorted(bids.items()):
1027 1042 for _f, args, msg in l:
1028 1043 repo.ui.note(b' %s -> %s\n' % (msg, m))
1029 1044 # Pick random action. TODO: Instead, prompt user when resolving
1030 1045 m, l = list(bids.items())[0]
1031 1046 repo.ui.warn(
1032 1047 _(b' %s: ambiguous merge - picked %s action\n') % (f, m)
1033 1048 )
1034 1049 actions[f] = l[0]
1035 1050 continue
1036 1051 repo.ui.note(_(b'end of auction\n\n'))
1037 mresult = mergeresult(actions, diverge, renamedelete)
1052 # TODO: think about commitinfo when bid merge is used
1053 mresult = mergeresult(actions, diverge, renamedelete, {})
1038 1054
1039 1055 if wctx.rev() is None:
1040 1056 fractions = _forgetremoved(wctx, mctx, branchmerge)
1041 1057 mresult.actions.update(fractions)
1042 1058
1043 1059 prunedactions = sparse.filterupdatesactions(
1044 1060 repo, wctx, mctx, branchmerge, mresult.actions
1045 1061 )
1046 1062 _resolvetrivial(repo, wctx, mctx, ancestors[0], mresult.actions)
1047 1063
1048 1064 mresult.setactions(prunedactions)
1049 1065 return mresult
1050 1066
1051 1067
1052 1068 def _getcwd():
1053 1069 try:
1054 1070 return encoding.getcwd()
1055 1071 except OSError as err:
1056 1072 if err.errno == errno.ENOENT:
1057 1073 return None
1058 1074 raise
1059 1075
1060 1076
1061 1077 def batchremove(repo, wctx, actions):
1062 1078 """apply removes to the working directory
1063 1079
1064 1080 yields tuples for progress updates
1065 1081 """
1066 1082 verbose = repo.ui.verbose
1067 1083 cwd = _getcwd()
1068 1084 i = 0
1069 1085 for f, args, msg in actions:
1070 1086 repo.ui.debug(b" %s: %s -> r\n" % (f, msg))
1071 1087 if verbose:
1072 1088 repo.ui.note(_(b"removing %s\n") % f)
1073 1089 wctx[f].audit()
1074 1090 try:
1075 1091 wctx[f].remove(ignoremissing=True)
1076 1092 except OSError as inst:
1077 1093 repo.ui.warn(
1078 1094 _(b"update failed to remove %s: %s!\n") % (f, inst.strerror)
1079 1095 )
1080 1096 if i == 100:
1081 1097 yield i, f
1082 1098 i = 0
1083 1099 i += 1
1084 1100 if i > 0:
1085 1101 yield i, f
1086 1102
1087 1103 if cwd and not _getcwd():
1088 1104 # cwd was removed in the course of removing files; print a helpful
1089 1105 # warning.
1090 1106 repo.ui.warn(
1091 1107 _(
1092 1108 b"current directory was removed\n"
1093 1109 b"(consider changing to repo root: %s)\n"
1094 1110 )
1095 1111 % repo.root
1096 1112 )
1097 1113
1098 1114
1099 1115 def batchget(repo, mctx, wctx, wantfiledata, actions):
1100 1116 """apply gets to the working directory
1101 1117
1102 1118 mctx is the context to get from
1103 1119
1104 1120 Yields arbitrarily many (False, tuple) for progress updates, followed by
1105 1121 exactly one (True, filedata). When wantfiledata is false, filedata is an
1106 1122 empty dict. When wantfiledata is true, filedata[f] is a triple (mode, size,
1107 1123 mtime) of the file f written for each action.
1108 1124 """
1109 1125 filedata = {}
1110 1126 verbose = repo.ui.verbose
1111 1127 fctx = mctx.filectx
1112 1128 ui = repo.ui
1113 1129 i = 0
1114 1130 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1115 1131 for f, (flags, backup), msg in actions:
1116 1132 repo.ui.debug(b" %s: %s -> g\n" % (f, msg))
1117 1133 if verbose:
1118 1134 repo.ui.note(_(b"getting %s\n") % f)
1119 1135
1120 1136 if backup:
1121 1137 # If a file or directory exists with the same name, back that
1122 1138 # up. Otherwise, look to see if there is a file that conflicts
1123 1139 # with a directory this file is in, and if so, back that up.
1124 1140 conflicting = f
1125 1141 if not repo.wvfs.lexists(f):
1126 1142 for p in pathutil.finddirs(f):
1127 1143 if repo.wvfs.isfileorlink(p):
1128 1144 conflicting = p
1129 1145 break
1130 1146 if repo.wvfs.lexists(conflicting):
1131 1147 orig = scmutil.backuppath(ui, repo, conflicting)
1132 1148 util.rename(repo.wjoin(conflicting), orig)
1133 1149 wfctx = wctx[f]
1134 1150 wfctx.clearunknown()
1135 1151 atomictemp = ui.configbool(b"experimental", b"update.atomic-file")
1136 1152 size = wfctx.write(
1137 1153 fctx(f).data(),
1138 1154 flags,
1139 1155 backgroundclose=True,
1140 1156 atomictemp=atomictemp,
1141 1157 )
1142 1158 if wantfiledata:
1143 1159 s = wfctx.lstat()
1144 1160 mode = s.st_mode
1145 1161 mtime = s[stat.ST_MTIME]
1146 1162 filedata[f] = (mode, size, mtime) # for dirstate.normal
1147 1163 if i == 100:
1148 1164 yield False, (i, f)
1149 1165 i = 0
1150 1166 i += 1
1151 1167 if i > 0:
1152 1168 yield False, (i, f)
1153 1169 yield True, filedata
1154 1170
1155 1171
1156 1172 def _prefetchfiles(repo, ctx, actions):
1157 1173 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1158 1174 of merge actions. ``ctx`` is the context being merged in."""
1159 1175
1160 1176 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1161 1177 # don't touch the context to be merged in. 'cd' is skipped, because
1162 1178 # changed/deleted never resolves to something from the remote side.
1163 1179 oplist = [
1164 1180 actions[a]
1165 1181 for a in (
1166 1182 mergestatemod.ACTION_GET,
1167 1183 mergestatemod.ACTION_DELETED_CHANGED,
1168 1184 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
1169 1185 mergestatemod.ACTION_MERGE,
1170 1186 )
1171 1187 ]
1172 1188 prefetch = scmutil.prefetchfiles
1173 1189 matchfiles = scmutil.matchfiles
1174 1190 prefetch(
1175 1191 repo,
1176 1192 [
1177 1193 (
1178 1194 ctx.rev(),
1179 1195 matchfiles(
1180 1196 repo, [f for sublist in oplist for f, args, msg in sublist]
1181 1197 ),
1182 1198 )
1183 1199 ],
1184 1200 )
1185 1201
1186 1202
1187 1203 @attr.s(frozen=True)
1188 1204 class updateresult(object):
1189 1205 updatedcount = attr.ib()
1190 1206 mergedcount = attr.ib()
1191 1207 removedcount = attr.ib()
1192 1208 unresolvedcount = attr.ib()
1193 1209
1194 1210 def isempty(self):
1195 1211 return not (
1196 1212 self.updatedcount
1197 1213 or self.mergedcount
1198 1214 or self.removedcount
1199 1215 or self.unresolvedcount
1200 1216 )
1201 1217
1202 1218
1203 1219 def emptyactions():
1204 1220 """create an actions dict, to be populated and passed to applyupdates()"""
1205 1221 return {
1206 1222 m: []
1207 1223 for m in (
1208 1224 mergestatemod.ACTION_ADD,
1209 1225 mergestatemod.ACTION_ADD_MODIFIED,
1210 1226 mergestatemod.ACTION_FORGET,
1211 1227 mergestatemod.ACTION_GET,
1212 1228 mergestatemod.ACTION_CHANGED_DELETED,
1213 1229 mergestatemod.ACTION_DELETED_CHANGED,
1214 1230 mergestatemod.ACTION_REMOVE,
1215 1231 mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,
1216 1232 mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
1217 1233 mergestatemod.ACTION_MERGE,
1218 1234 mergestatemod.ACTION_EXEC,
1219 1235 mergestatemod.ACTION_KEEP,
1220 1236 mergestatemod.ACTION_PATH_CONFLICT,
1221 1237 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
1222 1238 mergestatemod.ACTION_GET_OTHER_AND_STORE,
1223 1239 )
1224 1240 }
1225 1241
1226 1242
1227 1243 def applyupdates(
1228 1244 repo, actions, wctx, mctx, overwrite, wantfiledata, labels=None
1229 1245 ):
1230 1246 """apply the merge action list to the working directory
1231 1247
1232 1248 wctx is the working copy context
1233 1249 mctx is the context to be merged into the working copy
1234 1250
1235 1251 Return a tuple of (counts, filedata), where counts is a tuple
1236 1252 (updated, merged, removed, unresolved) that describes how many
1237 1253 files were affected by the update, and filedata is as described in
1238 1254 batchget.
1239 1255 """
1240 1256
1241 1257 _prefetchfiles(repo, mctx, actions)
1242 1258
1243 1259 updated, merged, removed = 0, 0, 0
1244 1260 ms = mergestatemod.mergestate.clean(
1245 1261 repo, wctx.p1().node(), mctx.node(), labels
1246 1262 )
1247 1263
1248 1264 # add ACTION_GET_OTHER_AND_STORE to mergestate
1249 1265 for e in actions[mergestatemod.ACTION_GET_OTHER_AND_STORE]:
1250 1266 ms.addmergedother(e[0])
1251 1267
1252 1268 moves = []
1253 1269 for m, l in actions.items():
1254 1270 l.sort()
1255 1271
1256 1272 # 'cd' and 'dc' actions are treated like other merge conflicts
1257 1273 mergeactions = sorted(actions[mergestatemod.ACTION_CHANGED_DELETED])
1258 1274 mergeactions.extend(sorted(actions[mergestatemod.ACTION_DELETED_CHANGED]))
1259 1275 mergeactions.extend(actions[mergestatemod.ACTION_MERGE])
1260 1276 for f, args, msg in mergeactions:
1261 1277 f1, f2, fa, move, anc = args
1262 1278 if f == b'.hgsubstate': # merged internally
1263 1279 continue
1264 1280 if f1 is None:
1265 1281 fcl = filemerge.absentfilectx(wctx, fa)
1266 1282 else:
1267 1283 repo.ui.debug(b" preserving %s for resolve of %s\n" % (f1, f))
1268 1284 fcl = wctx[f1]
1269 1285 if f2 is None:
1270 1286 fco = filemerge.absentfilectx(mctx, fa)
1271 1287 else:
1272 1288 fco = mctx[f2]
1273 1289 actx = repo[anc]
1274 1290 if fa in actx:
1275 1291 fca = actx[fa]
1276 1292 else:
1277 1293 # TODO: move to absentfilectx
1278 1294 fca = repo.filectx(f1, fileid=nullrev)
1279 1295 ms.add(fcl, fco, fca, f)
1280 1296 if f1 != f and move:
1281 1297 moves.append(f1)
1282 1298
1283 1299 # remove renamed files after safely stored
1284 1300 for f in moves:
1285 1301 if wctx[f].lexists():
1286 1302 repo.ui.debug(b"removing %s\n" % f)
1287 1303 wctx[f].audit()
1288 1304 wctx[f].remove()
1289 1305
1290 1306 numupdates = sum(
1291 1307 len(l) for m, l in actions.items() if m != mergestatemod.ACTION_KEEP
1292 1308 )
1293 1309 progress = repo.ui.makeprogress(
1294 1310 _(b'updating'), unit=_(b'files'), total=numupdates
1295 1311 )
1296 1312
1297 1313 if [
1298 1314 a
1299 1315 for a in actions[mergestatemod.ACTION_REMOVE]
1300 1316 if a[0] == b'.hgsubstate'
1301 1317 ]:
1302 1318 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1303 1319
1304 1320 # record path conflicts
1305 1321 for f, args, msg in actions[mergestatemod.ACTION_PATH_CONFLICT]:
1306 1322 f1, fo = args
1307 1323 s = repo.ui.status
1308 1324 s(
1309 1325 _(
1310 1326 b"%s: path conflict - a file or link has the same name as a "
1311 1327 b"directory\n"
1312 1328 )
1313 1329 % f
1314 1330 )
1315 1331 if fo == b'l':
1316 1332 s(_(b"the local file has been renamed to %s\n") % f1)
1317 1333 else:
1318 1334 s(_(b"the remote file has been renamed to %s\n") % f1)
1319 1335 s(_(b"resolve manually then use 'hg resolve --mark %s'\n") % f)
1320 1336 ms.addpathconflict(f, f1, fo)
1321 1337 progress.increment(item=f)
1322 1338
1323 1339 # When merging in-memory, we can't support worker processes, so set the
1324 1340 # per-item cost at 0 in that case.
1325 1341 cost = 0 if wctx.isinmemory() else 0.001
1326 1342
1327 1343 # remove in parallel (must come before resolving path conflicts and getting)
1328 1344 prog = worker.worker(
1329 1345 repo.ui,
1330 1346 cost,
1331 1347 batchremove,
1332 1348 (repo, wctx),
1333 1349 actions[mergestatemod.ACTION_REMOVE],
1334 1350 )
1335 1351 for i, item in prog:
1336 1352 progress.increment(step=i, item=item)
1337 1353 removed = len(actions[mergestatemod.ACTION_REMOVE])
1338 1354
1339 1355 # resolve path conflicts (must come before getting)
1340 1356 for f, args, msg in actions[mergestatemod.ACTION_PATH_CONFLICT_RESOLVE]:
1341 1357 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
1342 1358 (f0, origf0) = args
1343 1359 if wctx[f0].lexists():
1344 1360 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1345 1361 wctx[f].audit()
1346 1362 wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
1347 1363 wctx[f0].remove()
1348 1364 progress.increment(item=f)
1349 1365
1350 1366 # get in parallel.
1351 1367 threadsafe = repo.ui.configbool(
1352 1368 b'experimental', b'worker.wdir-get-thread-safe'
1353 1369 )
1354 1370 prog = worker.worker(
1355 1371 repo.ui,
1356 1372 cost,
1357 1373 batchget,
1358 1374 (repo, mctx, wctx, wantfiledata),
1359 1375 actions[mergestatemod.ACTION_GET],
1360 1376 threadsafe=threadsafe,
1361 1377 hasretval=True,
1362 1378 )
1363 1379 getfiledata = {}
1364 1380 for final, res in prog:
1365 1381 if final:
1366 1382 getfiledata = res
1367 1383 else:
1368 1384 i, item = res
1369 1385 progress.increment(step=i, item=item)
1370 1386 updated = len(actions[mergestatemod.ACTION_GET])
1371 1387
1372 1388 if [a for a in actions[mergestatemod.ACTION_GET] if a[0] == b'.hgsubstate']:
1373 1389 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1374 1390
1375 1391 # forget (manifest only, just log it) (must come first)
1376 1392 for f, args, msg in actions[mergestatemod.ACTION_FORGET]:
1377 1393 repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
1378 1394 progress.increment(item=f)
1379 1395
1380 1396 # re-add (manifest only, just log it)
1381 1397 for f, args, msg in actions[mergestatemod.ACTION_ADD]:
1382 1398 repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
1383 1399 progress.increment(item=f)
1384 1400
1385 1401 # re-add/mark as modified (manifest only, just log it)
1386 1402 for f, args, msg in actions[mergestatemod.ACTION_ADD_MODIFIED]:
1387 1403 repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
1388 1404 progress.increment(item=f)
1389 1405
1390 1406 # keep (noop, just log it)
1391 1407 for f, args, msg in actions[mergestatemod.ACTION_KEEP]:
1392 1408 repo.ui.debug(b" %s: %s -> k\n" % (f, msg))
1393 1409 # no progress
1394 1410
1395 1411 # directory rename, move local
1396 1412 for f, args, msg in actions[mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL]:
1397 1413 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
1398 1414 progress.increment(item=f)
1399 1415 f0, flags = args
1400 1416 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1401 1417 wctx[f].audit()
1402 1418 wctx[f].write(wctx.filectx(f0).data(), flags)
1403 1419 wctx[f0].remove()
1404 1420 updated += 1
1405 1421
1406 1422 # local directory rename, get
1407 1423 for f, args, msg in actions[mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]:
1408 1424 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
1409 1425 progress.increment(item=f)
1410 1426 f0, flags = args
1411 1427 repo.ui.note(_(b"getting %s to %s\n") % (f0, f))
1412 1428 wctx[f].write(mctx.filectx(f0).data(), flags)
1413 1429 updated += 1
1414 1430
1415 1431 # exec
1416 1432 for f, args, msg in actions[mergestatemod.ACTION_EXEC]:
1417 1433 repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
1418 1434 progress.increment(item=f)
1419 1435 (flags,) = args
1420 1436 wctx[f].audit()
1421 1437 wctx[f].setflags(b'l' in flags, b'x' in flags)
1422 1438 updated += 1
1423 1439
1424 1440 # the ordering is important here -- ms.mergedriver will raise if the merge
1425 1441 # driver has changed, and we want to be able to bypass it when overwrite is
1426 1442 # True
1427 1443 usemergedriver = not overwrite and mergeactions and ms.mergedriver
1428 1444
1429 1445 if usemergedriver:
1430 1446 if wctx.isinmemory():
1431 1447 raise error.InMemoryMergeConflictsError(
1432 1448 b"in-memory merge does not support mergedriver"
1433 1449 )
1434 1450 ms.commit()
1435 1451 proceed = driverpreprocess(repo, ms, wctx, labels=labels)
1436 1452 # the driver might leave some files unresolved
1437 1453 unresolvedf = set(ms.unresolved())
1438 1454 if not proceed:
1439 1455 # XXX setting unresolved to at least 1 is a hack to make sure we
1440 1456 # error out
1441 1457 return updateresult(
1442 1458 updated, merged, removed, max(len(unresolvedf), 1)
1443 1459 )
1444 1460 newactions = []
1445 1461 for f, args, msg in mergeactions:
1446 1462 if f in unresolvedf:
1447 1463 newactions.append((f, args, msg))
1448 1464 mergeactions = newactions
1449 1465
1450 1466 try:
1451 1467 # premerge
1452 1468 tocomplete = []
1453 1469 for f, args, msg in mergeactions:
1454 1470 repo.ui.debug(b" %s: %s -> m (premerge)\n" % (f, msg))
1455 1471 progress.increment(item=f)
1456 1472 if f == b'.hgsubstate': # subrepo states need updating
1457 1473 subrepoutil.submerge(
1458 1474 repo, wctx, mctx, wctx.ancestor(mctx), overwrite, labels
1459 1475 )
1460 1476 continue
1461 1477 wctx[f].audit()
1462 1478 complete, r = ms.preresolve(f, wctx)
1463 1479 if not complete:
1464 1480 numupdates += 1
1465 1481 tocomplete.append((f, args, msg))
1466 1482
1467 1483 # merge
1468 1484 for f, args, msg in tocomplete:
1469 1485 repo.ui.debug(b" %s: %s -> m (merge)\n" % (f, msg))
1470 1486 progress.increment(item=f, total=numupdates)
1471 1487 ms.resolve(f, wctx)
1472 1488
1473 1489 finally:
1474 1490 ms.commit()
1475 1491
1476 1492 unresolved = ms.unresolvedcount()
1477 1493
1478 1494 if (
1479 1495 usemergedriver
1480 1496 and not unresolved
1481 1497 and ms.mdstate() != mergestatemod.MERGE_DRIVER_STATE_SUCCESS
1482 1498 ):
1483 1499 if not driverconclude(repo, ms, wctx, labels=labels):
1484 1500 # XXX setting unresolved to at least 1 is a hack to make sure we
1485 1501 # error out
1486 1502 unresolved = max(unresolved, 1)
1487 1503
1488 1504 ms.commit()
1489 1505
1490 1506 msupdated, msmerged, msremoved = ms.counts()
1491 1507 updated += msupdated
1492 1508 merged += msmerged
1493 1509 removed += msremoved
1494 1510
1495 1511 extraactions = ms.actions()
1496 1512 if extraactions:
1497 1513 mfiles = {a[0] for a in actions[mergestatemod.ACTION_MERGE]}
1498 1514 for k, acts in pycompat.iteritems(extraactions):
1499 1515 actions[k].extend(acts)
1500 1516 if k == mergestatemod.ACTION_GET and wantfiledata:
1501 1517 # no filedata until mergestate is updated to provide it
1502 1518 for a in acts:
1503 1519 getfiledata[a[0]] = None
1504 1520 # Remove these files from actions[ACTION_MERGE] as well. This is
1505 1521 # important because in recordupdates, files in actions[ACTION_MERGE]
1506 1522 # are processed after files in other actions, and the merge driver
1507 1523 # might add files to those actions via extraactions above. This can
1508 1524 # lead to a file being recorded twice, with poor results. This is
1509 1525 # especially problematic for actions[ACTION_REMOVE] (currently only
1510 1526 # possible with the merge driver in the initial merge process;
1511 1527 # interrupted merges don't go through this flow).
1512 1528 #
1513 1529 # The real fix here is to have indexes by both file and action so
1514 1530 # that when the action for a file is changed it is automatically
1515 1531 # reflected in the other action lists. But that involves a more
1516 1532 # complex data structure, so this will do for now.
1517 1533 #
1518 1534 # We don't need to do the same operation for 'dc' and 'cd' because
1519 1535 # those lists aren't consulted again.
1520 1536 mfiles.difference_update(a[0] for a in acts)
1521 1537
1522 1538 actions[mergestatemod.ACTION_MERGE] = [
1523 1539 a for a in actions[mergestatemod.ACTION_MERGE] if a[0] in mfiles
1524 1540 ]
1525 1541
1526 1542 progress.complete()
1527 1543 assert len(getfiledata) == (
1528 1544 len(actions[mergestatemod.ACTION_GET]) if wantfiledata else 0
1529 1545 )
1530 1546 return updateresult(updated, merged, removed, unresolved), getfiledata
1531 1547
1532 1548
1533 1549 def _advertisefsmonitor(repo, num_gets, p1node):
1534 1550 # Advertise fsmonitor when its presence could be useful.
1535 1551 #
1536 1552 # We only advertise when performing an update from an empty working
1537 1553 # directory. This typically only occurs during initial clone.
1538 1554 #
1539 1555 # We give users a mechanism to disable the warning in case it is
1540 1556 # annoying.
1541 1557 #
1542 1558 # We only allow on Linux and MacOS because that's where fsmonitor is
1543 1559 # considered stable.
1544 1560 fsmonitorwarning = repo.ui.configbool(b'fsmonitor', b'warn_when_unused')
1545 1561 fsmonitorthreshold = repo.ui.configint(
1546 1562 b'fsmonitor', b'warn_update_file_count'
1547 1563 )
1548 1564 try:
1549 1565 # avoid cycle: extensions -> cmdutil -> merge
1550 1566 from . import extensions
1551 1567
1552 1568 extensions.find(b'fsmonitor')
1553 1569 fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off'
1554 1570 # We intentionally don't look at whether fsmonitor has disabled
1555 1571 # itself because a) fsmonitor may have already printed a warning
1556 1572 # b) we only care about the config state here.
1557 1573 except KeyError:
1558 1574 fsmonitorenabled = False
1559 1575
1560 1576 if (
1561 1577 fsmonitorwarning
1562 1578 and not fsmonitorenabled
1563 1579 and p1node == nullid
1564 1580 and num_gets >= fsmonitorthreshold
1565 1581 and pycompat.sysplatform.startswith((b'linux', b'darwin'))
1566 1582 ):
1567 1583 repo.ui.warn(
1568 1584 _(
1569 1585 b'(warning: large working directory being used without '
1570 1586 b'fsmonitor enabled; enable fsmonitor to improve performance; '
1571 1587 b'see "hg help -e fsmonitor")\n'
1572 1588 )
1573 1589 )
1574 1590
1575 1591
1576 1592 UPDATECHECK_ABORT = b'abort' # handled at higher layers
1577 1593 UPDATECHECK_NONE = b'none'
1578 1594 UPDATECHECK_LINEAR = b'linear'
1579 1595 UPDATECHECK_NO_CONFLICT = b'noconflict'
1580 1596
1581 1597
1582 1598 def update(
1583 1599 repo,
1584 1600 node,
1585 1601 branchmerge,
1586 1602 force,
1587 1603 ancestor=None,
1588 1604 mergeancestor=False,
1589 1605 labels=None,
1590 1606 matcher=None,
1591 1607 mergeforce=False,
1592 1608 updatedirstate=True,
1593 1609 updatecheck=None,
1594 1610 wc=None,
1595 1611 ):
1596 1612 """
1597 1613 Perform a merge between the working directory and the given node
1598 1614
1599 1615 node = the node to update to
1600 1616 branchmerge = whether to merge between branches
1601 1617 force = whether to force branch merging or file overwriting
1602 1618 matcher = a matcher to filter file lists (dirstate not updated)
1603 1619 mergeancestor = whether it is merging with an ancestor. If true,
1604 1620 we should accept the incoming changes for any prompts that occur.
1605 1621 If false, merging with an ancestor (fast-forward) is only allowed
1606 1622 between different named branches. This flag is used by rebase extension
1607 1623 as a temporary fix and should be avoided in general.
1608 1624 labels = labels to use for base, local and other
1609 1625 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
1610 1626 this is True, then 'force' should be True as well.
1611 1627
1612 1628 The table below shows all the behaviors of the update command given the
1613 1629 -c/--check and -C/--clean or no options, whether the working directory is
1614 1630 dirty, whether a revision is specified, and the relationship of the parent
1615 1631 rev to the target rev (linear or not). Match from top first. The -n
1616 1632 option doesn't exist on the command line, but represents the
1617 1633 experimental.updatecheck=noconflict option.
1618 1634
1619 1635 This logic is tested by test-update-branches.t.
1620 1636
1621 1637 -c -C -n -m dirty rev linear | result
1622 1638 y y * * * * * | (1)
1623 1639 y * y * * * * | (1)
1624 1640 y * * y * * * | (1)
1625 1641 * y y * * * * | (1)
1626 1642 * y * y * * * | (1)
1627 1643 * * y y * * * | (1)
1628 1644 * * * * * n n | x
1629 1645 * * * * n * * | ok
1630 1646 n n n n y * y | merge
1631 1647 n n n n y y n | (2)
1632 1648 n n n y y * * | merge
1633 1649 n n y n y * * | merge if no conflict
1634 1650 n y n n y * * | discard
1635 1651 y n n n y * * | (3)
1636 1652
1637 1653 x = can't happen
1638 1654 * = don't-care
1639 1655 1 = incompatible options (checked in commands.py)
1640 1656 2 = abort: uncommitted changes (commit or update --clean to discard changes)
1641 1657 3 = abort: uncommitted changes (checked in commands.py)
1642 1658
1643 1659 The merge is performed inside ``wc``, a workingctx-like objects. It defaults
1644 1660 to repo[None] if None is passed.
1645 1661
1646 1662 Return the same tuple as applyupdates().
1647 1663 """
1648 1664 # Avoid cycle.
1649 1665 from . import sparse
1650 1666
1651 1667 # This function used to find the default destination if node was None, but
1652 1668 # that's now in destutil.py.
1653 1669 assert node is not None
1654 1670 if not branchmerge and not force:
1655 1671 # TODO: remove the default once all callers that pass branchmerge=False
1656 1672 # and force=False pass a value for updatecheck. We may want to allow
1657 1673 # updatecheck='abort' to better suppport some of these callers.
1658 1674 if updatecheck is None:
1659 1675 updatecheck = UPDATECHECK_LINEAR
1660 1676 if updatecheck not in (
1661 1677 UPDATECHECK_NONE,
1662 1678 UPDATECHECK_LINEAR,
1663 1679 UPDATECHECK_NO_CONFLICT,
1664 1680 ):
1665 1681 raise ValueError(
1666 1682 r'Invalid updatecheck %r (can accept %r)'
1667 1683 % (
1668 1684 updatecheck,
1669 1685 (
1670 1686 UPDATECHECK_NONE,
1671 1687 UPDATECHECK_LINEAR,
1672 1688 UPDATECHECK_NO_CONFLICT,
1673 1689 ),
1674 1690 )
1675 1691 )
1676 1692 if wc is not None and wc.isinmemory():
1677 1693 maybe_wlock = util.nullcontextmanager()
1678 1694 else:
1679 1695 maybe_wlock = repo.wlock()
1680 1696 with maybe_wlock:
1681 1697 if wc is None:
1682 1698 wc = repo[None]
1683 1699 pl = wc.parents()
1684 1700 p1 = pl[0]
1685 1701 p2 = repo[node]
1686 1702 if ancestor is not None:
1687 1703 pas = [repo[ancestor]]
1688 1704 else:
1689 1705 if repo.ui.configlist(b'merge', b'preferancestor') == [b'*']:
1690 1706 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
1691 1707 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
1692 1708 else:
1693 1709 pas = [p1.ancestor(p2, warn=branchmerge)]
1694 1710
1695 1711 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
1696 1712
1697 1713 overwrite = force and not branchmerge
1698 1714 ### check phase
1699 1715 if not overwrite:
1700 1716 if len(pl) > 1:
1701 1717 raise error.Abort(_(b"outstanding uncommitted merge"))
1702 1718 ms = mergestatemod.mergestate.read(repo)
1703 1719 if list(ms.unresolved()):
1704 1720 raise error.Abort(
1705 1721 _(b"outstanding merge conflicts"),
1706 1722 hint=_(b"use 'hg resolve' to resolve"),
1707 1723 )
1708 1724 if branchmerge:
1709 1725 if pas == [p2]:
1710 1726 raise error.Abort(
1711 1727 _(
1712 1728 b"merging with a working directory ancestor"
1713 1729 b" has no effect"
1714 1730 )
1715 1731 )
1716 1732 elif pas == [p1]:
1717 1733 if not mergeancestor and wc.branch() == p2.branch():
1718 1734 raise error.Abort(
1719 1735 _(b"nothing to merge"),
1720 1736 hint=_(b"use 'hg update' or check 'hg heads'"),
1721 1737 )
1722 1738 if not force and (wc.files() or wc.deleted()):
1723 1739 raise error.Abort(
1724 1740 _(b"uncommitted changes"),
1725 1741 hint=_(b"use 'hg status' to list changes"),
1726 1742 )
1727 1743 if not wc.isinmemory():
1728 1744 for s in sorted(wc.substate):
1729 1745 wc.sub(s).bailifchanged()
1730 1746
1731 1747 elif not overwrite:
1732 1748 if p1 == p2: # no-op update
1733 1749 # call the hooks and exit early
1734 1750 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
1735 1751 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
1736 1752 return updateresult(0, 0, 0, 0)
1737 1753
1738 1754 if updatecheck == UPDATECHECK_LINEAR and pas not in (
1739 1755 [p1],
1740 1756 [p2],
1741 1757 ): # nonlinear
1742 1758 dirty = wc.dirty(missing=True)
1743 1759 if dirty:
1744 1760 # Branching is a bit strange to ensure we do the minimal
1745 1761 # amount of call to obsutil.foreground.
1746 1762 foreground = obsutil.foreground(repo, [p1.node()])
1747 1763 # note: the <node> variable contains a random identifier
1748 1764 if repo[node].node() in foreground:
1749 1765 pass # allow updating to successors
1750 1766 else:
1751 1767 msg = _(b"uncommitted changes")
1752 1768 hint = _(b"commit or update --clean to discard changes")
1753 1769 raise error.UpdateAbort(msg, hint=hint)
1754 1770 else:
1755 1771 # Allow jumping branches if clean and specific rev given
1756 1772 pass
1757 1773
1758 1774 if overwrite:
1759 1775 pas = [wc]
1760 1776 elif not branchmerge:
1761 1777 pas = [p1]
1762 1778
1763 1779 # deprecated config: merge.followcopies
1764 1780 followcopies = repo.ui.configbool(b'merge', b'followcopies')
1765 1781 if overwrite:
1766 1782 followcopies = False
1767 1783 elif not pas[0]:
1768 1784 followcopies = False
1769 1785 if not branchmerge and not wc.dirty(missing=True):
1770 1786 followcopies = False
1771 1787
1772 1788 ### calculate phase
1773 1789 mresult = calculateupdates(
1774 1790 repo,
1775 1791 wc,
1776 1792 p2,
1777 1793 pas,
1778 1794 branchmerge,
1779 1795 force,
1780 1796 mergeancestor,
1781 1797 followcopies,
1782 1798 matcher=matcher,
1783 1799 mergeforce=mergeforce,
1784 1800 )
1785 1801
1786 1802 actionbyfile = mresult.actions
1787 1803
1788 1804 if updatecheck == UPDATECHECK_NO_CONFLICT:
1789 1805 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
1790 1806 if m not in (
1791 1807 mergestatemod.ACTION_GET,
1792 1808 mergestatemod.ACTION_KEEP,
1793 1809 mergestatemod.ACTION_EXEC,
1794 1810 mergestatemod.ACTION_REMOVE,
1795 1811 mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
1796 1812 mergestatemod.ACTION_GET_OTHER_AND_STORE,
1797 1813 ):
1798 1814 msg = _(b"conflicting changes")
1799 1815 hint = _(b"commit or update --clean to discard changes")
1800 1816 raise error.Abort(msg, hint=hint)
1801 1817
1802 1818 # Prompt and create actions. Most of this is in the resolve phase
1803 1819 # already, but we can't handle .hgsubstate in filemerge or
1804 1820 # subrepoutil.submerge yet so we have to keep prompting for it.
1805 1821 if b'.hgsubstate' in actionbyfile:
1806 1822 f = b'.hgsubstate'
1807 1823 m, args, msg = actionbyfile[f]
1808 1824 prompts = filemerge.partextras(labels)
1809 1825 prompts[b'f'] = f
1810 1826 if m == mergestatemod.ACTION_CHANGED_DELETED:
1811 1827 if repo.ui.promptchoice(
1812 1828 _(
1813 1829 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
1814 1830 b"use (c)hanged version or (d)elete?"
1815 1831 b"$$ &Changed $$ &Delete"
1816 1832 )
1817 1833 % prompts,
1818 1834 0,
1819 1835 ):
1820 1836 actionbyfile[f] = (
1821 1837 mergestatemod.ACTION_REMOVE,
1822 1838 None,
1823 1839 b'prompt delete',
1824 1840 )
1825 1841 elif f in p1:
1826 1842 actionbyfile[f] = (
1827 1843 mergestatemod.ACTION_ADD_MODIFIED,
1828 1844 None,
1829 1845 b'prompt keep',
1830 1846 )
1831 1847 else:
1832 1848 actionbyfile[f] = (
1833 1849 mergestatemod.ACTION_ADD,
1834 1850 None,
1835 1851 b'prompt keep',
1836 1852 )
1837 1853 elif m == mergestatemod.ACTION_DELETED_CHANGED:
1838 1854 f1, f2, fa, move, anc = args
1839 1855 flags = p2[f2].flags()
1840 1856 if (
1841 1857 repo.ui.promptchoice(
1842 1858 _(
1843 1859 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
1844 1860 b"use (c)hanged version or leave (d)eleted?"
1845 1861 b"$$ &Changed $$ &Deleted"
1846 1862 )
1847 1863 % prompts,
1848 1864 0,
1849 1865 )
1850 1866 == 0
1851 1867 ):
1852 1868 actionbyfile[f] = (
1853 1869 mergestatemod.ACTION_GET,
1854 1870 (flags, False),
1855 1871 b'prompt recreating',
1856 1872 )
1857 1873 else:
1858 1874 del actionbyfile[f]
1859 1875
1860 1876 # Convert to dictionary-of-lists format
1861 1877 actions = emptyactions()
1862 1878 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
1863 1879 if m not in actions:
1864 1880 actions[m] = []
1865 1881 actions[m].append((f, args, msg))
1866 1882
1867 1883 # ACTION_GET_OTHER_AND_STORE is a mergestatemod.ACTION_GET + store in mergestate
1868 1884 for e in actions[mergestatemod.ACTION_GET_OTHER_AND_STORE]:
1869 1885 actions[mergestatemod.ACTION_GET].append(e)
1870 1886
1871 1887 if not util.fscasesensitive(repo.path):
1872 1888 # check collision between files only in p2 for clean update
1873 1889 if not branchmerge and (
1874 1890 force or not wc.dirty(missing=True, branch=False)
1875 1891 ):
1876 1892 _checkcollision(repo, p2.manifest(), None)
1877 1893 else:
1878 1894 _checkcollision(repo, wc.manifest(), actions)
1879 1895
1880 1896 # divergent renames
1881 1897 for f, fl in sorted(pycompat.iteritems(mresult.diverge)):
1882 1898 repo.ui.warn(
1883 1899 _(
1884 1900 b"note: possible conflict - %s was renamed "
1885 1901 b"multiple times to:\n"
1886 1902 )
1887 1903 % f
1888 1904 )
1889 1905 for nf in sorted(fl):
1890 1906 repo.ui.warn(b" %s\n" % nf)
1891 1907
1892 1908 # rename and delete
1893 1909 for f, fl in sorted(pycompat.iteritems(mresult.renamedelete)):
1894 1910 repo.ui.warn(
1895 1911 _(
1896 1912 b"note: possible conflict - %s was deleted "
1897 1913 b"and renamed to:\n"
1898 1914 )
1899 1915 % f
1900 1916 )
1901 1917 for nf in sorted(fl):
1902 1918 repo.ui.warn(b" %s\n" % nf)
1903 1919
1904 1920 ### apply phase
1905 1921 if not branchmerge: # just jump to the new rev
1906 1922 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, b''
1907 1923 # If we're doing a partial update, we need to skip updating
1908 1924 # the dirstate.
1909 1925 always = matcher is None or matcher.always()
1910 1926 updatedirstate = updatedirstate and always and not wc.isinmemory()
1911 1927 if updatedirstate:
1912 1928 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
1913 1929 # note that we're in the middle of an update
1914 1930 repo.vfs.write(b'updatestate', p2.hex())
1915 1931
1916 1932 _advertisefsmonitor(
1917 1933 repo, len(actions[mergestatemod.ACTION_GET]), p1.node()
1918 1934 )
1919 1935
1920 1936 wantfiledata = updatedirstate and not branchmerge
1921 1937 stats, getfiledata = applyupdates(
1922 1938 repo, actions, wc, p2, overwrite, wantfiledata, labels=labels
1923 1939 )
1924 1940
1925 1941 if updatedirstate:
1926 1942 with repo.dirstate.parentchange():
1927 1943 repo.setparents(fp1, fp2)
1928 1944 mergestatemod.recordupdates(
1929 1945 repo, actions, branchmerge, getfiledata
1930 1946 )
1931 1947 # update completed, clear state
1932 1948 util.unlink(repo.vfs.join(b'updatestate'))
1933 1949
1934 1950 if not branchmerge:
1935 1951 repo.dirstate.setbranch(p2.branch())
1936 1952
1937 1953 # If we're updating to a location, clean up any stale temporary includes
1938 1954 # (ex: this happens during hg rebase --abort).
1939 1955 if not branchmerge:
1940 1956 sparse.prunetemporaryincludes(repo)
1941 1957
1942 1958 if updatedirstate:
1943 1959 repo.hook(
1944 1960 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
1945 1961 )
1946 1962 return stats
1947 1963
1948 1964
1949 1965 def merge(ctx, labels=None, force=False, wc=None):
1950 1966 """Merge another topological branch into the working copy.
1951 1967
1952 1968 force = whether the merge was run with 'merge --force' (deprecated)
1953 1969 """
1954 1970
1955 1971 return update(
1956 1972 ctx.repo(),
1957 1973 ctx.rev(),
1958 1974 labels=labels,
1959 1975 branchmerge=True,
1960 1976 force=force,
1961 1977 mergeforce=force,
1962 1978 wc=wc,
1963 1979 )
1964 1980
1965 1981
1966 1982 def clean_update(ctx, wc=None):
1967 1983 """Do a clean update to the given commit.
1968 1984
1969 1985 This involves updating to the commit and discarding any changes in the
1970 1986 working copy.
1971 1987 """
1972 1988 return update(ctx.repo(), ctx.rev(), branchmerge=False, force=True, wc=wc)
1973 1989
1974 1990
1975 1991 def revert_to(ctx, matcher=None, wc=None):
1976 1992 """Revert the working copy to the given commit.
1977 1993
1978 1994 The working copy will keep its current parent(s) but its content will
1979 1995 be the same as in the given commit.
1980 1996 """
1981 1997
1982 1998 return update(
1983 1999 ctx.repo(),
1984 2000 ctx.rev(),
1985 2001 branchmerge=False,
1986 2002 force=True,
1987 2003 updatedirstate=False,
1988 2004 matcher=matcher,
1989 2005 wc=wc,
1990 2006 )
1991 2007
1992 2008
1993 2009 def graft(
1994 2010 repo,
1995 2011 ctx,
1996 2012 base=None,
1997 2013 labels=None,
1998 2014 keepparent=False,
1999 2015 keepconflictparent=False,
2000 2016 wctx=None,
2001 2017 ):
2002 2018 """Do a graft-like merge.
2003 2019
2004 2020 This is a merge where the merge ancestor is chosen such that one
2005 2021 or more changesets are grafted onto the current changeset. In
2006 2022 addition to the merge, this fixes up the dirstate to include only
2007 2023 a single parent (if keepparent is False) and tries to duplicate any
2008 2024 renames/copies appropriately.
2009 2025
2010 2026 ctx - changeset to rebase
2011 2027 base - merge base, or ctx.p1() if not specified
2012 2028 labels - merge labels eg ['local', 'graft']
2013 2029 keepparent - keep second parent if any
2014 2030 keepconflictparent - if unresolved, keep parent used for the merge
2015 2031
2016 2032 """
2017 2033 # If we're grafting a descendant onto an ancestor, be sure to pass
2018 2034 # mergeancestor=True to update. This does two things: 1) allows the merge if
2019 2035 # the destination is the same as the parent of the ctx (so we can use graft
2020 2036 # to copy commits), and 2) informs update that the incoming changes are
2021 2037 # newer than the destination so it doesn't prompt about "remote changed foo
2022 2038 # which local deleted".
2023 2039 # We also pass mergeancestor=True when base is the same revision as p1. 2)
2024 2040 # doesn't matter as there can't possibly be conflicts, but 1) is necessary.
2025 2041 wctx = wctx or repo[None]
2026 2042 pctx = wctx.p1()
2027 2043 base = base or ctx.p1()
2028 2044 mergeancestor = (
2029 2045 repo.changelog.isancestor(pctx.node(), ctx.node())
2030 2046 or pctx.rev() == base.rev()
2031 2047 )
2032 2048
2033 2049 stats = update(
2034 2050 repo,
2035 2051 ctx.node(),
2036 2052 True,
2037 2053 True,
2038 2054 base.node(),
2039 2055 mergeancestor=mergeancestor,
2040 2056 labels=labels,
2041 2057 wc=wctx,
2042 2058 )
2043 2059
2044 2060 if keepconflictparent and stats.unresolvedcount:
2045 2061 pother = ctx.node()
2046 2062 else:
2047 2063 pother = nullid
2048 2064 parents = ctx.parents()
2049 2065 if keepparent and len(parents) == 2 and base in parents:
2050 2066 parents.remove(base)
2051 2067 pother = parents[0].node()
2052 2068 # Never set both parents equal to each other
2053 2069 if pother == pctx.node():
2054 2070 pother = nullid
2055 2071
2056 2072 if wctx.isinmemory():
2057 2073 wctx.setparents(pctx.node(), pother)
2058 2074 # fix up dirstate for copies and renames
2059 2075 copies.graftcopies(wctx, ctx, base)
2060 2076 else:
2061 2077 with repo.dirstate.parentchange():
2062 2078 repo.setparents(pctx.node(), pother)
2063 2079 repo.dirstate.write(repo.currenttransaction())
2064 2080 # fix up dirstate for copies and renames
2065 2081 copies.graftcopies(wctx, ctx, base)
2066 2082 return stats
2067 2083
2068 2084
2069 2085 def purge(
2070 2086 repo,
2071 2087 matcher,
2072 2088 unknown=True,
2073 2089 ignored=False,
2074 2090 removeemptydirs=True,
2075 2091 removefiles=True,
2076 2092 abortonerror=False,
2077 2093 noop=False,
2078 2094 ):
2079 2095 """Purge the working directory of untracked files.
2080 2096
2081 2097 ``matcher`` is a matcher configured to scan the working directory -
2082 2098 potentially a subset.
2083 2099
2084 2100 ``unknown`` controls whether unknown files should be purged.
2085 2101
2086 2102 ``ignored`` controls whether ignored files should be purged.
2087 2103
2088 2104 ``removeemptydirs`` controls whether empty directories should be removed.
2089 2105
2090 2106 ``removefiles`` controls whether files are removed.
2091 2107
2092 2108 ``abortonerror`` causes an exception to be raised if an error occurs
2093 2109 deleting a file or directory.
2094 2110
2095 2111 ``noop`` controls whether to actually remove files. If not defined, actions
2096 2112 will be taken.
2097 2113
2098 2114 Returns an iterable of relative paths in the working directory that were
2099 2115 or would be removed.
2100 2116 """
2101 2117
2102 2118 def remove(removefn, path):
2103 2119 try:
2104 2120 removefn(path)
2105 2121 except OSError:
2106 2122 m = _(b'%s cannot be removed') % path
2107 2123 if abortonerror:
2108 2124 raise error.Abort(m)
2109 2125 else:
2110 2126 repo.ui.warn(_(b'warning: %s\n') % m)
2111 2127
2112 2128 # There's no API to copy a matcher. So mutate the passed matcher and
2113 2129 # restore it when we're done.
2114 2130 oldtraversedir = matcher.traversedir
2115 2131
2116 2132 res = []
2117 2133
2118 2134 try:
2119 2135 if removeemptydirs:
2120 2136 directories = []
2121 2137 matcher.traversedir = directories.append
2122 2138
2123 2139 status = repo.status(match=matcher, ignored=ignored, unknown=unknown)
2124 2140
2125 2141 if removefiles:
2126 2142 for f in sorted(status.unknown + status.ignored):
2127 2143 if not noop:
2128 2144 repo.ui.note(_(b'removing file %s\n') % f)
2129 2145 remove(repo.wvfs.unlink, f)
2130 2146 res.append(f)
2131 2147
2132 2148 if removeemptydirs:
2133 2149 for f in sorted(directories, reverse=True):
2134 2150 if matcher(f) and not repo.wvfs.listdir(f):
2135 2151 if not noop:
2136 2152 repo.ui.note(_(b'removing directory %s\n') % f)
2137 2153 remove(repo.wvfs.rmdir, f)
2138 2154 res.append(f)
2139 2155
2140 2156 return res
2141 2157
2142 2158 finally:
2143 2159 matcher.traversedir = oldtraversedir
General Comments 0
You need to be logged in to leave comments. Login now