##// END OF EJS Templates
mergestate: implement unresolvedcount() in terms of unresolved()...
Martin von Zweigbergk -
r33311:f8f716da default
parent child Browse files
Show More
@@ -1,1748 +1,1747 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 hashlib
12 12 import shutil
13 13 import struct
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 addednodeid,
18 18 bin,
19 19 hex,
20 20 modifiednodeid,
21 21 nullhex,
22 22 nullid,
23 23 nullrev,
24 24 )
25 25 from . import (
26 26 copies,
27 27 error,
28 28 filemerge,
29 29 match as matchmod,
30 30 obsutil,
31 31 pycompat,
32 32 scmutil,
33 33 subrepo,
34 34 util,
35 35 worker,
36 36 )
37 37
38 38 _pack = struct.pack
39 39 _unpack = struct.unpack
40 40
41 41 def _droponode(data):
42 42 # used for compatibility for v1
43 43 bits = data.split('\0')
44 44 bits = bits[:-2] + bits[-1:]
45 45 return '\0'.join(bits)
46 46
47 47 class mergestate(object):
48 48 '''track 3-way merge state of individual files
49 49
50 50 The merge state is stored on disk when needed. Two files are used: one with
51 51 an old format (version 1), and one with a new format (version 2). Version 2
52 52 stores a superset of the data in version 1, including new kinds of records
53 53 in the future. For more about the new format, see the documentation for
54 54 `_readrecordsv2`.
55 55
56 56 Each record can contain arbitrary content, and has an associated type. This
57 57 `type` should be a letter. If `type` is uppercase, the record is mandatory:
58 58 versions of Mercurial that don't support it should abort. If `type` is
59 59 lowercase, the record can be safely ignored.
60 60
61 61 Currently known records:
62 62
63 63 L: the node of the "local" part of the merge (hexified version)
64 64 O: the node of the "other" part of the merge (hexified version)
65 65 F: a file to be merged entry
66 66 C: a change/delete or delete/change conflict
67 67 D: a file that the external merge driver will merge internally
68 68 (experimental)
69 69 m: the external merge driver defined for this merge plus its run state
70 70 (experimental)
71 71 f: a (filename, dictionary) tuple of optional values for a given file
72 72 X: unsupported mandatory record type (used in tests)
73 73 x: unsupported advisory record type (used in tests)
74 74 l: the labels for the parts of the merge.
75 75
76 76 Merge driver run states (experimental):
77 77 u: driver-resolved files unmarked -- needs to be run next time we're about
78 78 to resolve or commit
79 79 m: driver-resolved files marked -- only needs to be run before commit
80 80 s: success/skipped -- does not need to be run any more
81 81
82 82 '''
83 83 statepathv1 = 'merge/state'
84 84 statepathv2 = 'merge/state2'
85 85
86 86 @staticmethod
87 87 def clean(repo, node=None, other=None, labels=None):
88 88 """Initialize a brand new merge state, removing any existing state on
89 89 disk."""
90 90 ms = mergestate(repo)
91 91 ms.reset(node, other, labels)
92 92 return ms
93 93
94 94 @staticmethod
95 95 def read(repo):
96 96 """Initialize the merge state, reading it from disk."""
97 97 ms = mergestate(repo)
98 98 ms._read()
99 99 return ms
100 100
101 101 def __init__(self, repo):
102 102 """Initialize the merge state.
103 103
104 104 Do not use this directly! Instead call read() or clean()."""
105 105 self._repo = repo
106 106 self._dirty = False
107 107 self._labels = None
108 108
109 109 def reset(self, node=None, other=None, labels=None):
110 110 self._state = {}
111 111 self._stateextras = {}
112 112 self._local = None
113 113 self._other = None
114 114 self._labels = labels
115 115 for var in ('localctx', 'otherctx'):
116 116 if var in vars(self):
117 117 delattr(self, var)
118 118 if node:
119 119 self._local = node
120 120 self._other = other
121 121 self._readmergedriver = None
122 122 if self.mergedriver:
123 123 self._mdstate = 's'
124 124 else:
125 125 self._mdstate = 'u'
126 126 shutil.rmtree(self._repo.vfs.join('merge'), True)
127 127 self._results = {}
128 128 self._dirty = False
129 129
130 130 def _read(self):
131 131 """Analyse each record content to restore a serialized state from disk
132 132
133 133 This function process "record" entry produced by the de-serialization
134 134 of on disk file.
135 135 """
136 136 self._state = {}
137 137 self._stateextras = {}
138 138 self._local = None
139 139 self._other = None
140 140 for var in ('localctx', 'otherctx'):
141 141 if var in vars(self):
142 142 delattr(self, var)
143 143 self._readmergedriver = None
144 144 self._mdstate = 's'
145 145 unsupported = set()
146 146 records = self._readrecords()
147 147 for rtype, record in records:
148 148 if rtype == 'L':
149 149 self._local = bin(record)
150 150 elif rtype == 'O':
151 151 self._other = bin(record)
152 152 elif rtype == 'm':
153 153 bits = record.split('\0', 1)
154 154 mdstate = bits[1]
155 155 if len(mdstate) != 1 or mdstate not in 'ums':
156 156 # the merge driver should be idempotent, so just rerun it
157 157 mdstate = 'u'
158 158
159 159 self._readmergedriver = bits[0]
160 160 self._mdstate = mdstate
161 161 elif rtype in 'FDC':
162 162 bits = record.split('\0')
163 163 self._state[bits[0]] = bits[1:]
164 164 elif rtype == 'f':
165 165 filename, rawextras = record.split('\0', 1)
166 166 extraparts = rawextras.split('\0')
167 167 extras = {}
168 168 i = 0
169 169 while i < len(extraparts):
170 170 extras[extraparts[i]] = extraparts[i + 1]
171 171 i += 2
172 172
173 173 self._stateextras[filename] = extras
174 174 elif rtype == 'l':
175 175 labels = record.split('\0', 2)
176 176 self._labels = [l for l in labels if len(l) > 0]
177 177 elif not rtype.islower():
178 178 unsupported.add(rtype)
179 179 self._results = {}
180 180 self._dirty = False
181 181
182 182 if unsupported:
183 183 raise error.UnsupportedMergeRecords(unsupported)
184 184
185 185 def _readrecords(self):
186 186 """Read merge state from disk and return a list of record (TYPE, data)
187 187
188 188 We read data from both v1 and v2 files and decide which one to use.
189 189
190 190 V1 has been used by version prior to 2.9.1 and contains less data than
191 191 v2. We read both versions and check if no data in v2 contradicts
192 192 v1. If there is not contradiction we can safely assume that both v1
193 193 and v2 were written at the same time and use the extract data in v2. If
194 194 there is contradiction we ignore v2 content as we assume an old version
195 195 of Mercurial has overwritten the mergestate file and left an old v2
196 196 file around.
197 197
198 198 returns list of record [(TYPE, data), ...]"""
199 199 v1records = self._readrecordsv1()
200 200 v2records = self._readrecordsv2()
201 201 if self._v1v2match(v1records, v2records):
202 202 return v2records
203 203 else:
204 204 # v1 file is newer than v2 file, use it
205 205 # we have to infer the "other" changeset of the merge
206 206 # we cannot do better than that with v1 of the format
207 207 mctx = self._repo[None].parents()[-1]
208 208 v1records.append(('O', mctx.hex()))
209 209 # add place holder "other" file node information
210 210 # nobody is using it yet so we do no need to fetch the data
211 211 # if mctx was wrong `mctx[bits[-2]]` may fails.
212 212 for idx, r in enumerate(v1records):
213 213 if r[0] == 'F':
214 214 bits = r[1].split('\0')
215 215 bits.insert(-2, '')
216 216 v1records[idx] = (r[0], '\0'.join(bits))
217 217 return v1records
218 218
219 219 def _v1v2match(self, v1records, v2records):
220 220 oldv2 = set() # old format version of v2 record
221 221 for rec in v2records:
222 222 if rec[0] == 'L':
223 223 oldv2.add(rec)
224 224 elif rec[0] == 'F':
225 225 # drop the onode data (not contained in v1)
226 226 oldv2.add(('F', _droponode(rec[1])))
227 227 for rec in v1records:
228 228 if rec not in oldv2:
229 229 return False
230 230 else:
231 231 return True
232 232
233 233 def _readrecordsv1(self):
234 234 """read on disk merge state for version 1 file
235 235
236 236 returns list of record [(TYPE, data), ...]
237 237
238 238 Note: the "F" data from this file are one entry short
239 239 (no "other file node" entry)
240 240 """
241 241 records = []
242 242 try:
243 243 f = self._repo.vfs(self.statepathv1)
244 244 for i, l in enumerate(f):
245 245 if i == 0:
246 246 records.append(('L', l[:-1]))
247 247 else:
248 248 records.append(('F', l[:-1]))
249 249 f.close()
250 250 except IOError as err:
251 251 if err.errno != errno.ENOENT:
252 252 raise
253 253 return records
254 254
255 255 def _readrecordsv2(self):
256 256 """read on disk merge state for version 2 file
257 257
258 258 This format is a list of arbitrary records of the form:
259 259
260 260 [type][length][content]
261 261
262 262 `type` is a single character, `length` is a 4 byte integer, and
263 263 `content` is an arbitrary byte sequence of length `length`.
264 264
265 265 Mercurial versions prior to 3.7 have a bug where if there are
266 266 unsupported mandatory merge records, attempting to clear out the merge
267 267 state with hg update --clean or similar aborts. The 't' record type
268 268 works around that by writing out what those versions treat as an
269 269 advisory record, but later versions interpret as special: the first
270 270 character is the 'real' record type and everything onwards is the data.
271 271
272 272 Returns list of records [(TYPE, data), ...]."""
273 273 records = []
274 274 try:
275 275 f = self._repo.vfs(self.statepathv2)
276 276 data = f.read()
277 277 off = 0
278 278 end = len(data)
279 279 while off < end:
280 280 rtype = data[off]
281 281 off += 1
282 282 length = _unpack('>I', data[off:(off + 4)])[0]
283 283 off += 4
284 284 record = data[off:(off + length)]
285 285 off += length
286 286 if rtype == 't':
287 287 rtype, record = record[0], record[1:]
288 288 records.append((rtype, record))
289 289 f.close()
290 290 except IOError as err:
291 291 if err.errno != errno.ENOENT:
292 292 raise
293 293 return records
294 294
295 295 @util.propertycache
296 296 def mergedriver(self):
297 297 # protect against the following:
298 298 # - A configures a malicious merge driver in their hgrc, then
299 299 # pauses the merge
300 300 # - A edits their hgrc to remove references to the merge driver
301 301 # - A gives a copy of their entire repo, including .hg, to B
302 302 # - B inspects .hgrc and finds it to be clean
303 303 # - B then continues the merge and the malicious merge driver
304 304 # gets invoked
305 305 configmergedriver = self._repo.ui.config('experimental', 'mergedriver')
306 306 if (self._readmergedriver is not None
307 307 and self._readmergedriver != configmergedriver):
308 308 raise error.ConfigError(
309 309 _("merge driver changed since merge started"),
310 310 hint=_("revert merge driver change or abort merge"))
311 311
312 312 return configmergedriver
313 313
314 314 @util.propertycache
315 315 def localctx(self):
316 316 if self._local is None:
317 317 msg = "localctx accessed but self._local isn't set"
318 318 raise error.ProgrammingError(msg)
319 319 return self._repo[self._local]
320 320
321 321 @util.propertycache
322 322 def otherctx(self):
323 323 if self._other is None:
324 324 msg = "otherctx accessed but self._other isn't set"
325 325 raise error.ProgrammingError(msg)
326 326 return self._repo[self._other]
327 327
328 328 def active(self):
329 329 """Whether mergestate is active.
330 330
331 331 Returns True if there appears to be mergestate. This is a rough proxy
332 332 for "is a merge in progress."
333 333 """
334 334 # Check local variables before looking at filesystem for performance
335 335 # reasons.
336 336 return bool(self._local) or bool(self._state) or \
337 337 self._repo.vfs.exists(self.statepathv1) or \
338 338 self._repo.vfs.exists(self.statepathv2)
339 339
340 340 def commit(self):
341 341 """Write current state on disk (if necessary)"""
342 342 if self._dirty:
343 343 records = self._makerecords()
344 344 self._writerecords(records)
345 345 self._dirty = False
346 346
347 347 def _makerecords(self):
348 348 records = []
349 349 records.append(('L', hex(self._local)))
350 350 records.append(('O', hex(self._other)))
351 351 if self.mergedriver:
352 352 records.append(('m', '\0'.join([
353 353 self.mergedriver, self._mdstate])))
354 354 for d, v in self._state.iteritems():
355 355 if v[0] == 'd':
356 356 records.append(('D', '\0'.join([d] + v)))
357 357 # v[1] == local ('cd'), v[6] == other ('dc') -- not supported by
358 358 # older versions of Mercurial
359 359 elif v[1] == nullhex or v[6] == nullhex:
360 360 records.append(('C', '\0'.join([d] + v)))
361 361 else:
362 362 records.append(('F', '\0'.join([d] + v)))
363 363 for filename, extras in sorted(self._stateextras.iteritems()):
364 364 rawextras = '\0'.join('%s\0%s' % (k, v) for k, v in
365 365 extras.iteritems())
366 366 records.append(('f', '%s\0%s' % (filename, rawextras)))
367 367 if self._labels is not None:
368 368 labels = '\0'.join(self._labels)
369 369 records.append(('l', labels))
370 370 return records
371 371
372 372 def _writerecords(self, records):
373 373 """Write current state on disk (both v1 and v2)"""
374 374 self._writerecordsv1(records)
375 375 self._writerecordsv2(records)
376 376
377 377 def _writerecordsv1(self, records):
378 378 """Write current state on disk in a version 1 file"""
379 379 f = self._repo.vfs(self.statepathv1, 'w')
380 380 irecords = iter(records)
381 381 lrecords = next(irecords)
382 382 assert lrecords[0] == 'L'
383 383 f.write(hex(self._local) + '\n')
384 384 for rtype, data in irecords:
385 385 if rtype == 'F':
386 386 f.write('%s\n' % _droponode(data))
387 387 f.close()
388 388
389 389 def _writerecordsv2(self, records):
390 390 """Write current state on disk in a version 2 file
391 391
392 392 See the docstring for _readrecordsv2 for why we use 't'."""
393 393 # these are the records that all version 2 clients can read
394 394 whitelist = 'LOF'
395 395 f = self._repo.vfs(self.statepathv2, 'w')
396 396 for key, data in records:
397 397 assert len(key) == 1
398 398 if key not in whitelist:
399 399 key, data = 't', '%s%s' % (key, data)
400 400 format = '>sI%is' % len(data)
401 401 f.write(_pack(format, key, len(data), data))
402 402 f.close()
403 403
404 404 def add(self, fcl, fco, fca, fd):
405 405 """add a new (potentially?) conflicting file the merge state
406 406 fcl: file context for local,
407 407 fco: file context for remote,
408 408 fca: file context for ancestors,
409 409 fd: file path of the resulting merge.
410 410
411 411 note: also write the local version to the `.hg/merge` directory.
412 412 """
413 413 if fcl.isabsent():
414 414 hash = nullhex
415 415 else:
416 416 hash = hex(hashlib.sha1(fcl.path()).digest())
417 417 self._repo.vfs.write('merge/' + hash, fcl.data())
418 418 self._state[fd] = ['u', hash, fcl.path(),
419 419 fca.path(), hex(fca.filenode()),
420 420 fco.path(), hex(fco.filenode()),
421 421 fcl.flags()]
422 422 self._stateextras[fd] = { 'ancestorlinknode' : hex(fca.node()) }
423 423 self._dirty = True
424 424
425 425 def __contains__(self, dfile):
426 426 return dfile in self._state
427 427
428 428 def __getitem__(self, dfile):
429 429 return self._state[dfile][0]
430 430
431 431 def __iter__(self):
432 432 return iter(sorted(self._state))
433 433
434 434 def files(self):
435 435 return self._state.keys()
436 436
437 437 def mark(self, dfile, state):
438 438 self._state[dfile][0] = state
439 439 self._dirty = True
440 440
441 441 def mdstate(self):
442 442 return self._mdstate
443 443
444 444 def unresolved(self):
445 445 """Obtain the paths of unresolved files."""
446 446
447 447 for f, entry in self._state.iteritems():
448 448 if entry[0] == 'u':
449 449 yield f
450 450
451 451 def driverresolved(self):
452 452 """Obtain the paths of driver-resolved files."""
453 453
454 454 for f, entry in self._state.items():
455 455 if entry[0] == 'd':
456 456 yield f
457 457
458 458 def extras(self, filename):
459 459 return self._stateextras.setdefault(filename, {})
460 460
461 461 def _resolve(self, preresolve, dfile, wctx):
462 462 """rerun merge process for file path `dfile`"""
463 463 if self[dfile] in 'rd':
464 464 return True, 0
465 465 stateentry = self._state[dfile]
466 466 state, hash, lfile, afile, anode, ofile, onode, flags = stateentry
467 467 octx = self._repo[self._other]
468 468 extras = self.extras(dfile)
469 469 anccommitnode = extras.get('ancestorlinknode')
470 470 if anccommitnode:
471 471 actx = self._repo[anccommitnode]
472 472 else:
473 473 actx = None
474 474 fcd = self._filectxorabsent(hash, wctx, dfile)
475 475 fco = self._filectxorabsent(onode, octx, ofile)
476 476 # TODO: move this to filectxorabsent
477 477 fca = self._repo.filectx(afile, fileid=anode, changeid=actx)
478 478 # "premerge" x flags
479 479 flo = fco.flags()
480 480 fla = fca.flags()
481 481 if 'x' in flags + flo + fla and 'l' not in flags + flo + fla:
482 482 if fca.node() == nullid and flags != flo:
483 483 if preresolve:
484 484 self._repo.ui.warn(
485 485 _('warning: cannot merge flags for %s '
486 486 'without common ancestor - keeping local flags\n')
487 487 % afile)
488 488 elif flags == fla:
489 489 flags = flo
490 490 if preresolve:
491 491 # restore local
492 492 if hash != nullhex:
493 493 f = self._repo.vfs('merge/' + hash)
494 494 wctx[dfile].write(f.read(), flags)
495 495 f.close()
496 496 else:
497 497 wctx[dfile].remove(ignoremissing=True)
498 498 complete, r, deleted = filemerge.premerge(self._repo, self._local,
499 499 lfile, fcd, fco, fca,
500 500 labels=self._labels)
501 501 else:
502 502 complete, r, deleted = filemerge.filemerge(self._repo, self._local,
503 503 lfile, fcd, fco, fca,
504 504 labels=self._labels)
505 505 if r is None:
506 506 # no real conflict
507 507 del self._state[dfile]
508 508 self._stateextras.pop(dfile, None)
509 509 self._dirty = True
510 510 elif not r:
511 511 self.mark(dfile, 'r')
512 512
513 513 if complete:
514 514 action = None
515 515 if deleted:
516 516 if fcd.isabsent():
517 517 # dc: local picked. Need to drop if present, which may
518 518 # happen on re-resolves.
519 519 action = 'f'
520 520 else:
521 521 # cd: remote picked (or otherwise deleted)
522 522 action = 'r'
523 523 else:
524 524 if fcd.isabsent(): # dc: remote picked
525 525 action = 'g'
526 526 elif fco.isabsent(): # cd: local picked
527 527 if dfile in self.localctx:
528 528 action = 'am'
529 529 else:
530 530 action = 'a'
531 531 # else: regular merges (no action necessary)
532 532 self._results[dfile] = r, action
533 533
534 534 return complete, r
535 535
536 536 def _filectxorabsent(self, hexnode, ctx, f):
537 537 if hexnode == nullhex:
538 538 return filemerge.absentfilectx(ctx, f)
539 539 else:
540 540 return ctx[f]
541 541
542 542 def preresolve(self, dfile, wctx):
543 543 """run premerge process for dfile
544 544
545 545 Returns whether the merge is complete, and the exit code."""
546 546 return self._resolve(True, dfile, wctx)
547 547
548 548 def resolve(self, dfile, wctx):
549 549 """run merge process (assuming premerge was run) for dfile
550 550
551 551 Returns the exit code of the merge."""
552 552 return self._resolve(False, dfile, wctx)[1]
553 553
554 554 def counts(self):
555 555 """return counts for updated, merged and removed files in this
556 556 session"""
557 557 updated, merged, removed = 0, 0, 0
558 558 for r, action in self._results.itervalues():
559 559 if r is None:
560 560 updated += 1
561 561 elif r == 0:
562 562 if action == 'r':
563 563 removed += 1
564 564 else:
565 565 merged += 1
566 566 return updated, merged, removed
567 567
568 568 def unresolvedcount(self):
569 569 """get unresolved count for this merge (persistent)"""
570 return len([True for f, entry in self._state.iteritems()
571 if entry[0] == 'u'])
570 return len(list(self.unresolved()))
572 571
573 572 def actions(self):
574 573 """return lists of actions to perform on the dirstate"""
575 574 actions = {'r': [], 'f': [], 'a': [], 'am': [], 'g': []}
576 575 for f, (r, action) in self._results.iteritems():
577 576 if action is not None:
578 577 actions[action].append((f, None, "merge result"))
579 578 return actions
580 579
581 580 def recordactions(self):
582 581 """record remove/add/get actions in the dirstate"""
583 582 branchmerge = self._repo.dirstate.p2() != nullid
584 583 recordupdates(self._repo, self.actions(), branchmerge)
585 584
586 585 def queueremove(self, f):
587 586 """queues a file to be removed from the dirstate
588 587
589 588 Meant for use by custom merge drivers."""
590 589 self._results[f] = 0, 'r'
591 590
592 591 def queueadd(self, f):
593 592 """queues a file to be added to the dirstate
594 593
595 594 Meant for use by custom merge drivers."""
596 595 self._results[f] = 0, 'a'
597 596
598 597 def queueget(self, f):
599 598 """queues a file to be marked modified in the dirstate
600 599
601 600 Meant for use by custom merge drivers."""
602 601 self._results[f] = 0, 'g'
603 602
604 603 def _getcheckunknownconfig(repo, section, name):
605 604 config = repo.ui.config(section, name, default='abort')
606 605 valid = ['abort', 'ignore', 'warn']
607 606 if config not in valid:
608 607 validstr = ', '.join(["'" + v + "'" for v in valid])
609 608 raise error.ConfigError(_("%s.%s not valid "
610 609 "('%s' is none of %s)")
611 610 % (section, name, config, validstr))
612 611 return config
613 612
614 613 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
615 614 if f2 is None:
616 615 f2 = f
617 616 return (repo.wvfs.audit.check(f)
618 617 and repo.wvfs.isfileorlink(f)
619 618 and repo.dirstate.normalize(f) not in repo.dirstate
620 619 and mctx[f2].cmp(wctx[f]))
621 620
622 621 def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
623 622 """
624 623 Considers any actions that care about the presence of conflicting unknown
625 624 files. For some actions, the result is to abort; for others, it is to
626 625 choose a different action.
627 626 """
628 627 conflicts = set()
629 628 warnconflicts = set()
630 629 abortconflicts = set()
631 630 unknownconfig = _getcheckunknownconfig(repo, 'merge', 'checkunknown')
632 631 ignoredconfig = _getcheckunknownconfig(repo, 'merge', 'checkignored')
633 632 if not force:
634 633 def collectconflicts(conflicts, config):
635 634 if config == 'abort':
636 635 abortconflicts.update(conflicts)
637 636 elif config == 'warn':
638 637 warnconflicts.update(conflicts)
639 638
640 639 for f, (m, args, msg) in actions.iteritems():
641 640 if m in ('c', 'dc'):
642 641 if _checkunknownfile(repo, wctx, mctx, f):
643 642 conflicts.add(f)
644 643 elif m == 'dg':
645 644 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
646 645 conflicts.add(f)
647 646
648 647 ignoredconflicts = set([c for c in conflicts
649 648 if repo.dirstate._ignore(c)])
650 649 unknownconflicts = conflicts - ignoredconflicts
651 650 collectconflicts(ignoredconflicts, ignoredconfig)
652 651 collectconflicts(unknownconflicts, unknownconfig)
653 652 else:
654 653 for f, (m, args, msg) in actions.iteritems():
655 654 if m == 'cm':
656 655 fl2, anc = args
657 656 different = _checkunknownfile(repo, wctx, mctx, f)
658 657 if repo.dirstate._ignore(f):
659 658 config = ignoredconfig
660 659 else:
661 660 config = unknownconfig
662 661
663 662 # The behavior when force is True is described by this table:
664 663 # config different mergeforce | action backup
665 664 # * n * | get n
666 665 # * y y | merge -
667 666 # abort y n | merge - (1)
668 667 # warn y n | warn + get y
669 668 # ignore y n | get y
670 669 #
671 670 # (1) this is probably the wrong behavior here -- we should
672 671 # probably abort, but some actions like rebases currently
673 672 # don't like an abort happening in the middle of
674 673 # merge.update.
675 674 if not different:
676 675 actions[f] = ('g', (fl2, False), "remote created")
677 676 elif mergeforce or config == 'abort':
678 677 actions[f] = ('m', (f, f, None, False, anc),
679 678 "remote differs from untracked local")
680 679 elif config == 'abort':
681 680 abortconflicts.add(f)
682 681 else:
683 682 if config == 'warn':
684 683 warnconflicts.add(f)
685 684 actions[f] = ('g', (fl2, True), "remote created")
686 685
687 686 for f in sorted(abortconflicts):
688 687 repo.ui.warn(_("%s: untracked file differs\n") % f)
689 688 if abortconflicts:
690 689 raise error.Abort(_("untracked files in working directory "
691 690 "differ from files in requested revision"))
692 691
693 692 for f in sorted(warnconflicts):
694 693 repo.ui.warn(_("%s: replacing untracked file\n") % f)
695 694
696 695 for f, (m, args, msg) in actions.iteritems():
697 696 backup = f in conflicts
698 697 if m == 'c':
699 698 flags, = args
700 699 actions[f] = ('g', (flags, backup), msg)
701 700
702 701 def _forgetremoved(wctx, mctx, branchmerge):
703 702 """
704 703 Forget removed files
705 704
706 705 If we're jumping between revisions (as opposed to merging), and if
707 706 neither the working directory nor the target rev has the file,
708 707 then we need to remove it from the dirstate, to prevent the
709 708 dirstate from listing the file when it is no longer in the
710 709 manifest.
711 710
712 711 If we're merging, and the other revision has removed a file
713 712 that is not present in the working directory, we need to mark it
714 713 as removed.
715 714 """
716 715
717 716 actions = {}
718 717 m = 'f'
719 718 if branchmerge:
720 719 m = 'r'
721 720 for f in wctx.deleted():
722 721 if f not in mctx:
723 722 actions[f] = m, None, "forget deleted"
724 723
725 724 if not branchmerge:
726 725 for f in wctx.removed():
727 726 if f not in mctx:
728 727 actions[f] = 'f', None, "forget removed"
729 728
730 729 return actions
731 730
732 731 def _checkcollision(repo, wmf, actions):
733 732 # build provisional merged manifest up
734 733 pmmf = set(wmf)
735 734
736 735 if actions:
737 736 # k, dr, e and rd are no-op
738 737 for m in 'a', 'am', 'f', 'g', 'cd', 'dc':
739 738 for f, args, msg in actions[m]:
740 739 pmmf.add(f)
741 740 for f, args, msg in actions['r']:
742 741 pmmf.discard(f)
743 742 for f, args, msg in actions['dm']:
744 743 f2, flags = args
745 744 pmmf.discard(f2)
746 745 pmmf.add(f)
747 746 for f, args, msg in actions['dg']:
748 747 pmmf.add(f)
749 748 for f, args, msg in actions['m']:
750 749 f1, f2, fa, move, anc = args
751 750 if move:
752 751 pmmf.discard(f1)
753 752 pmmf.add(f)
754 753
755 754 # check case-folding collision in provisional merged manifest
756 755 foldmap = {}
757 756 for f in sorted(pmmf):
758 757 fold = util.normcase(f)
759 758 if fold in foldmap:
760 759 raise error.Abort(_("case-folding collision between %s and %s")
761 760 % (f, foldmap[fold]))
762 761 foldmap[fold] = f
763 762
764 763 # check case-folding of directories
765 764 foldprefix = unfoldprefix = lastfull = ''
766 765 for fold, f in sorted(foldmap.items()):
767 766 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
768 767 # the folded prefix matches but actual casing is different
769 768 raise error.Abort(_("case-folding collision between "
770 769 "%s and directory of %s") % (lastfull, f))
771 770 foldprefix = fold + '/'
772 771 unfoldprefix = f + '/'
773 772 lastfull = f
774 773
775 774 def driverpreprocess(repo, ms, wctx, labels=None):
776 775 """run the preprocess step of the merge driver, if any
777 776
778 777 This is currently not implemented -- it's an extension point."""
779 778 return True
780 779
781 780 def driverconclude(repo, ms, wctx, labels=None):
782 781 """run the conclude step of the merge driver, if any
783 782
784 783 This is currently not implemented -- it's an extension point."""
785 784 return True
786 785
787 786 def manifestmerge(repo, wctx, p2, pa, branchmerge, force, matcher,
788 787 acceptremote, followcopies, forcefulldiff=False):
789 788 """
790 789 Merge wctx and p2 with ancestor pa and generate merge action list
791 790
792 791 branchmerge and force are as passed in to update
793 792 matcher = matcher to filter file lists
794 793 acceptremote = accept the incoming changes without prompting
795 794 """
796 795 if matcher is not None and matcher.always():
797 796 matcher = None
798 797
799 798 copy, movewithdir, diverge, renamedelete, dirmove = {}, {}, {}, {}, {}
800 799
801 800 # manifests fetched in order are going to be faster, so prime the caches
802 801 [x.manifest() for x in
803 802 sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)]
804 803
805 804 if followcopies:
806 805 ret = copies.mergecopies(repo, wctx, p2, pa)
807 806 copy, movewithdir, diverge, renamedelete, dirmove = ret
808 807
809 808 boolbm = pycompat.bytestr(bool(branchmerge))
810 809 boolf = pycompat.bytestr(bool(force))
811 810 boolm = pycompat.bytestr(bool(matcher))
812 811 repo.ui.note(_("resolving manifests\n"))
813 812 repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n"
814 813 % (boolbm, boolf, boolm))
815 814 repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
816 815
817 816 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
818 817 copied = set(copy.values())
819 818 copied.update(movewithdir.values())
820 819
821 820 if '.hgsubstate' in m1:
822 821 # check whether sub state is modified
823 822 if any(wctx.sub(s).dirty() for s in wctx.substate):
824 823 m1['.hgsubstate'] = modifiednodeid
825 824
826 825 # Don't use m2-vs-ma optimization if:
827 826 # - ma is the same as m1 or m2, which we're just going to diff again later
828 827 # - The caller specifically asks for a full diff, which is useful during bid
829 828 # merge.
830 829 if (pa not in ([wctx, p2] + wctx.parents()) and not forcefulldiff):
831 830 # Identify which files are relevant to the merge, so we can limit the
832 831 # total m1-vs-m2 diff to just those files. This has significant
833 832 # performance benefits in large repositories.
834 833 relevantfiles = set(ma.diff(m2).keys())
835 834
836 835 # For copied and moved files, we need to add the source file too.
837 836 for copykey, copyvalue in copy.iteritems():
838 837 if copyvalue in relevantfiles:
839 838 relevantfiles.add(copykey)
840 839 for movedirkey in movewithdir:
841 840 relevantfiles.add(movedirkey)
842 841 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
843 842 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
844 843
845 844 diff = m1.diff(m2, match=matcher)
846 845
847 846 if matcher is None:
848 847 matcher = matchmod.always('', '')
849 848
850 849 actions = {}
851 850 for f, ((n1, fl1), (n2, fl2)) in diff.iteritems():
852 851 if n1 and n2: # file exists on both local and remote side
853 852 if f not in ma:
854 853 fa = copy.get(f, None)
855 854 if fa is not None:
856 855 actions[f] = ('m', (f, f, fa, False, pa.node()),
857 856 "both renamed from " + fa)
858 857 else:
859 858 actions[f] = ('m', (f, f, None, False, pa.node()),
860 859 "both created")
861 860 else:
862 861 a = ma[f]
863 862 fla = ma.flags(f)
864 863 nol = 'l' not in fl1 + fl2 + fla
865 864 if n2 == a and fl2 == fla:
866 865 actions[f] = ('k' , (), "remote unchanged")
867 866 elif n1 == a and fl1 == fla: # local unchanged - use remote
868 867 if n1 == n2: # optimization: keep local content
869 868 actions[f] = ('e', (fl2,), "update permissions")
870 869 else:
871 870 actions[f] = ('g', (fl2, False), "remote is newer")
872 871 elif nol and n2 == a: # remote only changed 'x'
873 872 actions[f] = ('e', (fl2,), "update permissions")
874 873 elif nol and n1 == a: # local only changed 'x'
875 874 actions[f] = ('g', (fl1, False), "remote is newer")
876 875 else: # both changed something
877 876 actions[f] = ('m', (f, f, f, False, pa.node()),
878 877 "versions differ")
879 878 elif n1: # file exists only on local side
880 879 if f in copied:
881 880 pass # we'll deal with it on m2 side
882 881 elif f in movewithdir: # directory rename, move local
883 882 f2 = movewithdir[f]
884 883 if f2 in m2:
885 884 actions[f2] = ('m', (f, f2, None, True, pa.node()),
886 885 "remote directory rename, both created")
887 886 else:
888 887 actions[f2] = ('dm', (f, fl1),
889 888 "remote directory rename - move from " + f)
890 889 elif f in copy:
891 890 f2 = copy[f]
892 891 actions[f] = ('m', (f, f2, f2, False, pa.node()),
893 892 "local copied/moved from " + f2)
894 893 elif f in ma: # clean, a different, no remote
895 894 if n1 != ma[f]:
896 895 if acceptremote:
897 896 actions[f] = ('r', None, "remote delete")
898 897 else:
899 898 actions[f] = ('cd', (f, None, f, False, pa.node()),
900 899 "prompt changed/deleted")
901 900 elif n1 == addednodeid:
902 901 # This extra 'a' is added by working copy manifest to mark
903 902 # the file as locally added. We should forget it instead of
904 903 # deleting it.
905 904 actions[f] = ('f', None, "remote deleted")
906 905 else:
907 906 actions[f] = ('r', None, "other deleted")
908 907 elif n2: # file exists only on remote side
909 908 if f in copied:
910 909 pass # we'll deal with it on m1 side
911 910 elif f in movewithdir:
912 911 f2 = movewithdir[f]
913 912 if f2 in m1:
914 913 actions[f2] = ('m', (f2, f, None, False, pa.node()),
915 914 "local directory rename, both created")
916 915 else:
917 916 actions[f2] = ('dg', (f, fl2),
918 917 "local directory rename - get from " + f)
919 918 elif f in copy:
920 919 f2 = copy[f]
921 920 if f2 in m2:
922 921 actions[f] = ('m', (f2, f, f2, False, pa.node()),
923 922 "remote copied from " + f2)
924 923 else:
925 924 actions[f] = ('m', (f2, f, f2, True, pa.node()),
926 925 "remote moved from " + f2)
927 926 elif f not in ma:
928 927 # local unknown, remote created: the logic is described by the
929 928 # following table:
930 929 #
931 930 # force branchmerge different | action
932 931 # n * * | create
933 932 # y n * | create
934 933 # y y n | create
935 934 # y y y | merge
936 935 #
937 936 # Checking whether the files are different is expensive, so we
938 937 # don't do that when we can avoid it.
939 938 if not force:
940 939 actions[f] = ('c', (fl2,), "remote created")
941 940 elif not branchmerge:
942 941 actions[f] = ('c', (fl2,), "remote created")
943 942 else:
944 943 actions[f] = ('cm', (fl2, pa.node()),
945 944 "remote created, get or merge")
946 945 elif n2 != ma[f]:
947 946 df = None
948 947 for d in dirmove:
949 948 if f.startswith(d):
950 949 # new file added in a directory that was moved
951 950 df = dirmove[d] + f[len(d):]
952 951 break
953 952 if df is not None and df in m1:
954 953 actions[df] = ('m', (df, f, f, False, pa.node()),
955 954 "local directory rename - respect move from " + f)
956 955 elif acceptremote:
957 956 actions[f] = ('c', (fl2,), "remote recreating")
958 957 else:
959 958 actions[f] = ('dc', (None, f, f, False, pa.node()),
960 959 "prompt deleted/changed")
961 960
962 961 return actions, diverge, renamedelete
963 962
964 963 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
965 964 """Resolves false conflicts where the nodeid changed but the content
966 965 remained the same."""
967 966
968 967 for f, (m, args, msg) in actions.items():
969 968 if m == 'cd' and f in ancestor and not wctx[f].cmp(ancestor[f]):
970 969 # local did change but ended up with same content
971 970 actions[f] = 'r', None, "prompt same"
972 971 elif m == 'dc' and f in ancestor and not mctx[f].cmp(ancestor[f]):
973 972 # remote did change but ended up with same content
974 973 del actions[f] # don't get = keep local deleted
975 974
976 975 def calculateupdates(repo, wctx, mctx, ancestors, branchmerge, force,
977 976 acceptremote, followcopies, matcher=None,
978 977 mergeforce=False):
979 978 "Calculate the actions needed to merge mctx into wctx using ancestors"
980 979 if len(ancestors) == 1: # default
981 980 actions, diverge, renamedelete = manifestmerge(
982 981 repo, wctx, mctx, ancestors[0], branchmerge, force, matcher,
983 982 acceptremote, followcopies)
984 983 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
985 984
986 985 else: # only when merge.preferancestor=* - the default
987 986 repo.ui.note(
988 987 _("note: merging %s and %s using bids from ancestors %s\n") %
989 988 (wctx, mctx, _(' and ').join(str(anc) for anc in ancestors)))
990 989
991 990 # Call for bids
992 991 fbids = {} # mapping filename to bids (action method to list af actions)
993 992 diverge, renamedelete = None, None
994 993 for ancestor in ancestors:
995 994 repo.ui.note(_('\ncalculating bids for ancestor %s\n') % ancestor)
996 995 actions, diverge1, renamedelete1 = manifestmerge(
997 996 repo, wctx, mctx, ancestor, branchmerge, force, matcher,
998 997 acceptremote, followcopies, forcefulldiff=True)
999 998 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1000 999
1001 1000 # Track the shortest set of warning on the theory that bid
1002 1001 # merge will correctly incorporate more information
1003 1002 if diverge is None or len(diverge1) < len(diverge):
1004 1003 diverge = diverge1
1005 1004 if renamedelete is None or len(renamedelete) < len(renamedelete1):
1006 1005 renamedelete = renamedelete1
1007 1006
1008 1007 for f, a in sorted(actions.iteritems()):
1009 1008 m, args, msg = a
1010 1009 repo.ui.debug(' %s: %s -> %s\n' % (f, msg, m))
1011 1010 if f in fbids:
1012 1011 d = fbids[f]
1013 1012 if m in d:
1014 1013 d[m].append(a)
1015 1014 else:
1016 1015 d[m] = [a]
1017 1016 else:
1018 1017 fbids[f] = {m: [a]}
1019 1018
1020 1019 # Pick the best bid for each file
1021 1020 repo.ui.note(_('\nauction for merging merge bids\n'))
1022 1021 actions = {}
1023 1022 dms = [] # filenames that have dm actions
1024 1023 for f, bids in sorted(fbids.items()):
1025 1024 # bids is a mapping from action method to list af actions
1026 1025 # Consensus?
1027 1026 if len(bids) == 1: # all bids are the same kind of method
1028 1027 m, l = bids.items()[0]
1029 1028 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1030 1029 repo.ui.note(_(" %s: consensus for %s\n") % (f, m))
1031 1030 actions[f] = l[0]
1032 1031 if m == 'dm':
1033 1032 dms.append(f)
1034 1033 continue
1035 1034 # If keep is an option, just do it.
1036 1035 if 'k' in bids:
1037 1036 repo.ui.note(_(" %s: picking 'keep' action\n") % f)
1038 1037 actions[f] = bids['k'][0]
1039 1038 continue
1040 1039 # If there are gets and they all agree [how could they not?], do it.
1041 1040 if 'g' in bids:
1042 1041 ga0 = bids['g'][0]
1043 1042 if all(a == ga0 for a in bids['g'][1:]):
1044 1043 repo.ui.note(_(" %s: picking 'get' action\n") % f)
1045 1044 actions[f] = ga0
1046 1045 continue
1047 1046 # TODO: Consider other simple actions such as mode changes
1048 1047 # Handle inefficient democrazy.
1049 1048 repo.ui.note(_(' %s: multiple bids for merge action:\n') % f)
1050 1049 for m, l in sorted(bids.items()):
1051 1050 for _f, args, msg in l:
1052 1051 repo.ui.note(' %s -> %s\n' % (msg, m))
1053 1052 # Pick random action. TODO: Instead, prompt user when resolving
1054 1053 m, l = bids.items()[0]
1055 1054 repo.ui.warn(_(' %s: ambiguous merge - picked %s action\n') %
1056 1055 (f, m))
1057 1056 actions[f] = l[0]
1058 1057 if m == 'dm':
1059 1058 dms.append(f)
1060 1059 continue
1061 1060 # Work around 'dm' that can cause multiple actions for the same file
1062 1061 for f in dms:
1063 1062 dm, (f0, flags), msg = actions[f]
1064 1063 assert dm == 'dm', dm
1065 1064 if f0 in actions and actions[f0][0] == 'r':
1066 1065 # We have one bid for removing a file and another for moving it.
1067 1066 # These two could be merged as first move and then delete ...
1068 1067 # but instead drop moving and just delete.
1069 1068 del actions[f]
1070 1069 repo.ui.note(_('end of auction\n\n'))
1071 1070
1072 1071 _resolvetrivial(repo, wctx, mctx, ancestors[0], actions)
1073 1072
1074 1073 if wctx.rev() is None:
1075 1074 fractions = _forgetremoved(wctx, mctx, branchmerge)
1076 1075 actions.update(fractions)
1077 1076
1078 1077 return actions, diverge, renamedelete
1079 1078
1080 1079 def batchremove(repo, wctx, actions):
1081 1080 """apply removes to the working directory
1082 1081
1083 1082 yields tuples for progress updates
1084 1083 """
1085 1084 verbose = repo.ui.verbose
1086 1085 try:
1087 1086 cwd = pycompat.getcwd()
1088 1087 except OSError as err:
1089 1088 if err.errno != errno.ENOENT:
1090 1089 raise
1091 1090 cwd = None
1092 1091 i = 0
1093 1092 for f, args, msg in actions:
1094 1093 repo.ui.debug(" %s: %s -> r\n" % (f, msg))
1095 1094 if verbose:
1096 1095 repo.ui.note(_("removing %s\n") % f)
1097 1096 wctx[f].audit()
1098 1097 try:
1099 1098 wctx[f].remove(ignoremissing=True)
1100 1099 except OSError as inst:
1101 1100 repo.ui.warn(_("update failed to remove %s: %s!\n") %
1102 1101 (f, inst.strerror))
1103 1102 if i == 100:
1104 1103 yield i, f
1105 1104 i = 0
1106 1105 i += 1
1107 1106 if i > 0:
1108 1107 yield i, f
1109 1108 if cwd:
1110 1109 # cwd was present before we started to remove files
1111 1110 # let's check if it is present after we removed them
1112 1111 try:
1113 1112 pycompat.getcwd()
1114 1113 except OSError as err:
1115 1114 if err.errno != errno.ENOENT:
1116 1115 raise
1117 1116 # Print a warning if cwd was deleted
1118 1117 repo.ui.warn(_("current directory was removed\n"
1119 1118 "(consider changing to repo root: %s)\n") %
1120 1119 repo.root)
1121 1120
1122 1121 def batchget(repo, mctx, wctx, actions):
1123 1122 """apply gets to the working directory
1124 1123
1125 1124 mctx is the context to get from
1126 1125
1127 1126 yields tuples for progress updates
1128 1127 """
1129 1128 verbose = repo.ui.verbose
1130 1129 fctx = mctx.filectx
1131 1130 ui = repo.ui
1132 1131 i = 0
1133 1132 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1134 1133 for f, (flags, backup), msg in actions:
1135 1134 repo.ui.debug(" %s: %s -> g\n" % (f, msg))
1136 1135 if verbose:
1137 1136 repo.ui.note(_("getting %s\n") % f)
1138 1137
1139 1138 if backup:
1140 1139 absf = repo.wjoin(f)
1141 1140 orig = scmutil.origpath(ui, repo, absf)
1142 1141 try:
1143 1142 if repo.wvfs.isfileorlink(f):
1144 1143 util.rename(absf, orig)
1145 1144 except OSError as e:
1146 1145 if e.errno != errno.ENOENT:
1147 1146 raise
1148 1147
1149 1148 if repo.wvfs.isdir(f) and not repo.wvfs.islink(f):
1150 1149 repo.wvfs.removedirs(f)
1151 1150 wctx[f].write(fctx(f).data(), flags, backgroundclose=True)
1152 1151 if i == 100:
1153 1152 yield i, f
1154 1153 i = 0
1155 1154 i += 1
1156 1155 if i > 0:
1157 1156 yield i, f
1158 1157
1159 1158 def applyupdates(repo, actions, wctx, mctx, overwrite, labels=None):
1160 1159 """apply the merge action list to the working directory
1161 1160
1162 1161 wctx is the working copy context
1163 1162 mctx is the context to be merged into the working copy
1164 1163
1165 1164 Return a tuple of counts (updated, merged, removed, unresolved) that
1166 1165 describes how many files were affected by the update.
1167 1166 """
1168 1167
1169 1168 updated, merged, removed = 0, 0, 0
1170 1169 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels)
1171 1170 moves = []
1172 1171 for m, l in actions.items():
1173 1172 l.sort()
1174 1173
1175 1174 # 'cd' and 'dc' actions are treated like other merge conflicts
1176 1175 mergeactions = sorted(actions['cd'])
1177 1176 mergeactions.extend(sorted(actions['dc']))
1178 1177 mergeactions.extend(actions['m'])
1179 1178 for f, args, msg in mergeactions:
1180 1179 f1, f2, fa, move, anc = args
1181 1180 if f == '.hgsubstate': # merged internally
1182 1181 continue
1183 1182 if f1 is None:
1184 1183 fcl = filemerge.absentfilectx(wctx, fa)
1185 1184 else:
1186 1185 repo.ui.debug(" preserving %s for resolve of %s\n" % (f1, f))
1187 1186 fcl = wctx[f1]
1188 1187 if f2 is None:
1189 1188 fco = filemerge.absentfilectx(mctx, fa)
1190 1189 else:
1191 1190 fco = mctx[f2]
1192 1191 actx = repo[anc]
1193 1192 if fa in actx:
1194 1193 fca = actx[fa]
1195 1194 else:
1196 1195 # TODO: move to absentfilectx
1197 1196 fca = repo.filectx(f1, fileid=nullrev)
1198 1197 ms.add(fcl, fco, fca, f)
1199 1198 if f1 != f and move:
1200 1199 moves.append(f1)
1201 1200
1202 1201 _updating = _('updating')
1203 1202 _files = _('files')
1204 1203 progress = repo.ui.progress
1205 1204
1206 1205 # remove renamed files after safely stored
1207 1206 for f in moves:
1208 1207 if wctx[f].lexists():
1209 1208 repo.ui.debug("removing %s\n" % f)
1210 1209 wctx[f].audit()
1211 1210 wctx[f].remove()
1212 1211
1213 1212 numupdates = sum(len(l) for m, l in actions.items() if m != 'k')
1214 1213
1215 1214 if [a for a in actions['r'] if a[0] == '.hgsubstate']:
1216 1215 subrepo.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1217 1216
1218 1217 # remove in parallel (must come first)
1219 1218 z = 0
1220 1219 prog = worker.worker(repo.ui, 0.001, batchremove, (repo, wctx),
1221 1220 actions['r'])
1222 1221 for i, item in prog:
1223 1222 z += i
1224 1223 progress(_updating, z, item=item, total=numupdates, unit=_files)
1225 1224 removed = len(actions['r'])
1226 1225
1227 1226 # get in parallel
1228 1227 prog = worker.worker(repo.ui, 0.001, batchget, (repo, mctx, wctx),
1229 1228 actions['g'])
1230 1229 for i, item in prog:
1231 1230 z += i
1232 1231 progress(_updating, z, item=item, total=numupdates, unit=_files)
1233 1232 updated = len(actions['g'])
1234 1233
1235 1234 if [a for a in actions['g'] if a[0] == '.hgsubstate']:
1236 1235 subrepo.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1237 1236
1238 1237 # forget (manifest only, just log it) (must come first)
1239 1238 for f, args, msg in actions['f']:
1240 1239 repo.ui.debug(" %s: %s -> f\n" % (f, msg))
1241 1240 z += 1
1242 1241 progress(_updating, z, item=f, total=numupdates, unit=_files)
1243 1242
1244 1243 # re-add (manifest only, just log it)
1245 1244 for f, args, msg in actions['a']:
1246 1245 repo.ui.debug(" %s: %s -> a\n" % (f, msg))
1247 1246 z += 1
1248 1247 progress(_updating, z, item=f, total=numupdates, unit=_files)
1249 1248
1250 1249 # re-add/mark as modified (manifest only, just log it)
1251 1250 for f, args, msg in actions['am']:
1252 1251 repo.ui.debug(" %s: %s -> am\n" % (f, msg))
1253 1252 z += 1
1254 1253 progress(_updating, z, item=f, total=numupdates, unit=_files)
1255 1254
1256 1255 # keep (noop, just log it)
1257 1256 for f, args, msg in actions['k']:
1258 1257 repo.ui.debug(" %s: %s -> k\n" % (f, msg))
1259 1258 # no progress
1260 1259
1261 1260 # directory rename, move local
1262 1261 for f, args, msg in actions['dm']:
1263 1262 repo.ui.debug(" %s: %s -> dm\n" % (f, msg))
1264 1263 z += 1
1265 1264 progress(_updating, z, item=f, total=numupdates, unit=_files)
1266 1265 f0, flags = args
1267 1266 repo.ui.note(_("moving %s to %s\n") % (f0, f))
1268 1267 wctx[f].audit()
1269 1268 wctx[f].write(wctx.filectx(f0).data(), flags)
1270 1269 wctx[f0].remove()
1271 1270 updated += 1
1272 1271
1273 1272 # local directory rename, get
1274 1273 for f, args, msg in actions['dg']:
1275 1274 repo.ui.debug(" %s: %s -> dg\n" % (f, msg))
1276 1275 z += 1
1277 1276 progress(_updating, z, item=f, total=numupdates, unit=_files)
1278 1277 f0, flags = args
1279 1278 repo.ui.note(_("getting %s to %s\n") % (f0, f))
1280 1279 wctx[f].write(mctx.filectx(f0).data(), flags)
1281 1280 updated += 1
1282 1281
1283 1282 # exec
1284 1283 for f, args, msg in actions['e']:
1285 1284 repo.ui.debug(" %s: %s -> e\n" % (f, msg))
1286 1285 z += 1
1287 1286 progress(_updating, z, item=f, total=numupdates, unit=_files)
1288 1287 flags, = args
1289 1288 wctx[f].audit()
1290 1289 wctx[f].setflags('l' in flags, 'x' in flags)
1291 1290 updated += 1
1292 1291
1293 1292 # the ordering is important here -- ms.mergedriver will raise if the merge
1294 1293 # driver has changed, and we want to be able to bypass it when overwrite is
1295 1294 # True
1296 1295 usemergedriver = not overwrite and mergeactions and ms.mergedriver
1297 1296
1298 1297 if usemergedriver:
1299 1298 ms.commit()
1300 1299 proceed = driverpreprocess(repo, ms, wctx, labels=labels)
1301 1300 # the driver might leave some files unresolved
1302 1301 unresolvedf = set(ms.unresolved())
1303 1302 if not proceed:
1304 1303 # XXX setting unresolved to at least 1 is a hack to make sure we
1305 1304 # error out
1306 1305 return updated, merged, removed, max(len(unresolvedf), 1)
1307 1306 newactions = []
1308 1307 for f, args, msg in mergeactions:
1309 1308 if f in unresolvedf:
1310 1309 newactions.append((f, args, msg))
1311 1310 mergeactions = newactions
1312 1311
1313 1312 # premerge
1314 1313 tocomplete = []
1315 1314 for f, args, msg in mergeactions:
1316 1315 repo.ui.debug(" %s: %s -> m (premerge)\n" % (f, msg))
1317 1316 z += 1
1318 1317 progress(_updating, z, item=f, total=numupdates, unit=_files)
1319 1318 if f == '.hgsubstate': # subrepo states need updating
1320 1319 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
1321 1320 overwrite, labels)
1322 1321 continue
1323 1322 wctx[f].audit()
1324 1323 complete, r = ms.preresolve(f, wctx)
1325 1324 if not complete:
1326 1325 numupdates += 1
1327 1326 tocomplete.append((f, args, msg))
1328 1327
1329 1328 # merge
1330 1329 for f, args, msg in tocomplete:
1331 1330 repo.ui.debug(" %s: %s -> m (merge)\n" % (f, msg))
1332 1331 z += 1
1333 1332 progress(_updating, z, item=f, total=numupdates, unit=_files)
1334 1333 ms.resolve(f, wctx)
1335 1334
1336 1335 ms.commit()
1337 1336
1338 1337 unresolved = ms.unresolvedcount()
1339 1338
1340 1339 if usemergedriver and not unresolved and ms.mdstate() != 's':
1341 1340 if not driverconclude(repo, ms, wctx, labels=labels):
1342 1341 # XXX setting unresolved to at least 1 is a hack to make sure we
1343 1342 # error out
1344 1343 unresolved = max(unresolved, 1)
1345 1344
1346 1345 ms.commit()
1347 1346
1348 1347 msupdated, msmerged, msremoved = ms.counts()
1349 1348 updated += msupdated
1350 1349 merged += msmerged
1351 1350 removed += msremoved
1352 1351
1353 1352 extraactions = ms.actions()
1354 1353 if extraactions:
1355 1354 mfiles = set(a[0] for a in actions['m'])
1356 1355 for k, acts in extraactions.iteritems():
1357 1356 actions[k].extend(acts)
1358 1357 # Remove these files from actions['m'] as well. This is important
1359 1358 # because in recordupdates, files in actions['m'] are processed
1360 1359 # after files in other actions, and the merge driver might add
1361 1360 # files to those actions via extraactions above. This can lead to a
1362 1361 # file being recorded twice, with poor results. This is especially
1363 1362 # problematic for actions['r'] (currently only possible with the
1364 1363 # merge driver in the initial merge process; interrupted merges
1365 1364 # don't go through this flow).
1366 1365 #
1367 1366 # The real fix here is to have indexes by both file and action so
1368 1367 # that when the action for a file is changed it is automatically
1369 1368 # reflected in the other action lists. But that involves a more
1370 1369 # complex data structure, so this will do for now.
1371 1370 #
1372 1371 # We don't need to do the same operation for 'dc' and 'cd' because
1373 1372 # those lists aren't consulted again.
1374 1373 mfiles.difference_update(a[0] for a in acts)
1375 1374
1376 1375 actions['m'] = [a for a in actions['m'] if a[0] in mfiles]
1377 1376
1378 1377 progress(_updating, None, total=numupdates, unit=_files)
1379 1378
1380 1379 return updated, merged, removed, unresolved
1381 1380
1382 1381 def recordupdates(repo, actions, branchmerge):
1383 1382 "record merge actions to the dirstate"
1384 1383 # remove (must come first)
1385 1384 for f, args, msg in actions.get('r', []):
1386 1385 if branchmerge:
1387 1386 repo.dirstate.remove(f)
1388 1387 else:
1389 1388 repo.dirstate.drop(f)
1390 1389
1391 1390 # forget (must come first)
1392 1391 for f, args, msg in actions.get('f', []):
1393 1392 repo.dirstate.drop(f)
1394 1393
1395 1394 # re-add
1396 1395 for f, args, msg in actions.get('a', []):
1397 1396 repo.dirstate.add(f)
1398 1397
1399 1398 # re-add/mark as modified
1400 1399 for f, args, msg in actions.get('am', []):
1401 1400 if branchmerge:
1402 1401 repo.dirstate.normallookup(f)
1403 1402 else:
1404 1403 repo.dirstate.add(f)
1405 1404
1406 1405 # exec change
1407 1406 for f, args, msg in actions.get('e', []):
1408 1407 repo.dirstate.normallookup(f)
1409 1408
1410 1409 # keep
1411 1410 for f, args, msg in actions.get('k', []):
1412 1411 pass
1413 1412
1414 1413 # get
1415 1414 for f, args, msg in actions.get('g', []):
1416 1415 if branchmerge:
1417 1416 repo.dirstate.otherparent(f)
1418 1417 else:
1419 1418 repo.dirstate.normal(f)
1420 1419
1421 1420 # merge
1422 1421 for f, args, msg in actions.get('m', []):
1423 1422 f1, f2, fa, move, anc = args
1424 1423 if branchmerge:
1425 1424 # We've done a branch merge, mark this file as merged
1426 1425 # so that we properly record the merger later
1427 1426 repo.dirstate.merge(f)
1428 1427 if f1 != f2: # copy/rename
1429 1428 if move:
1430 1429 repo.dirstate.remove(f1)
1431 1430 if f1 != f:
1432 1431 repo.dirstate.copy(f1, f)
1433 1432 else:
1434 1433 repo.dirstate.copy(f2, f)
1435 1434 else:
1436 1435 # We've update-merged a locally modified file, so
1437 1436 # we set the dirstate to emulate a normal checkout
1438 1437 # of that file some time in the past. Thus our
1439 1438 # merge will appear as a normal local file
1440 1439 # modification.
1441 1440 if f2 == f: # file not locally copied/moved
1442 1441 repo.dirstate.normallookup(f)
1443 1442 if move:
1444 1443 repo.dirstate.drop(f1)
1445 1444
1446 1445 # directory rename, move local
1447 1446 for f, args, msg in actions.get('dm', []):
1448 1447 f0, flag = args
1449 1448 if branchmerge:
1450 1449 repo.dirstate.add(f)
1451 1450 repo.dirstate.remove(f0)
1452 1451 repo.dirstate.copy(f0, f)
1453 1452 else:
1454 1453 repo.dirstate.normal(f)
1455 1454 repo.dirstate.drop(f0)
1456 1455
1457 1456 # directory rename, get
1458 1457 for f, args, msg in actions.get('dg', []):
1459 1458 f0, flag = args
1460 1459 if branchmerge:
1461 1460 repo.dirstate.add(f)
1462 1461 repo.dirstate.copy(f0, f)
1463 1462 else:
1464 1463 repo.dirstate.normal(f)
1465 1464
1466 1465 def update(repo, node, branchmerge, force, ancestor=None,
1467 1466 mergeancestor=False, labels=None, matcher=None, mergeforce=False,
1468 1467 updatecheck=None):
1469 1468 """
1470 1469 Perform a merge between the working directory and the given node
1471 1470
1472 1471 node = the node to update to
1473 1472 branchmerge = whether to merge between branches
1474 1473 force = whether to force branch merging or file overwriting
1475 1474 matcher = a matcher to filter file lists (dirstate not updated)
1476 1475 mergeancestor = whether it is merging with an ancestor. If true,
1477 1476 we should accept the incoming changes for any prompts that occur.
1478 1477 If false, merging with an ancestor (fast-forward) is only allowed
1479 1478 between different named branches. This flag is used by rebase extension
1480 1479 as a temporary fix and should be avoided in general.
1481 1480 labels = labels to use for base, local and other
1482 1481 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
1483 1482 this is True, then 'force' should be True as well.
1484 1483
1485 1484 The table below shows all the behaviors of the update command
1486 1485 given the -c and -C or no options, whether the working directory
1487 1486 is dirty, whether a revision is specified, and the relationship of
1488 1487 the parent rev to the target rev (linear or not). Match from top first. The
1489 1488 -n option doesn't exist on the command line, but represents the
1490 1489 experimental.updatecheck=noconflict option.
1491 1490
1492 1491 This logic is tested by test-update-branches.t.
1493 1492
1494 1493 -c -C -n -m dirty rev linear | result
1495 1494 y y * * * * * | (1)
1496 1495 y * y * * * * | (1)
1497 1496 y * * y * * * | (1)
1498 1497 * y y * * * * | (1)
1499 1498 * y * y * * * | (1)
1500 1499 * * y y * * * | (1)
1501 1500 * * * * * n n | x
1502 1501 * * * * n * * | ok
1503 1502 n n n n y * y | merge
1504 1503 n n n n y y n | (2)
1505 1504 n n n y y * * | merge
1506 1505 n n y n y * * | merge if no conflict
1507 1506 n y n n y * * | discard
1508 1507 y n n n y * * | (3)
1509 1508
1510 1509 x = can't happen
1511 1510 * = don't-care
1512 1511 1 = incompatible options (checked in commands.py)
1513 1512 2 = abort: uncommitted changes (commit or update --clean to discard changes)
1514 1513 3 = abort: uncommitted changes (checked in commands.py)
1515 1514
1516 1515 Return the same tuple as applyupdates().
1517 1516 """
1518 1517
1519 1518 # This function used to find the default destination if node was None, but
1520 1519 # that's now in destutil.py.
1521 1520 assert node is not None
1522 1521 if not branchmerge and not force:
1523 1522 # TODO: remove the default once all callers that pass branchmerge=False
1524 1523 # and force=False pass a value for updatecheck. We may want to allow
1525 1524 # updatecheck='abort' to better suppport some of these callers.
1526 1525 if updatecheck is None:
1527 1526 updatecheck = 'linear'
1528 1527 assert updatecheck in ('none', 'linear', 'noconflict')
1529 1528 # If we're doing a partial update, we need to skip updating
1530 1529 # the dirstate, so make a note of any partial-ness to the
1531 1530 # update here.
1532 1531 if matcher is None or matcher.always():
1533 1532 partial = False
1534 1533 else:
1535 1534 partial = True
1536 1535 with repo.wlock():
1537 1536 wc = repo[None]
1538 1537 pl = wc.parents()
1539 1538 p1 = pl[0]
1540 1539 pas = [None]
1541 1540 if ancestor is not None:
1542 1541 pas = [repo[ancestor]]
1543 1542
1544 1543 overwrite = force and not branchmerge
1545 1544
1546 1545 p2 = repo[node]
1547 1546 if pas[0] is None:
1548 1547 if repo.ui.configlist('merge', 'preferancestor', ['*']) == ['*']:
1549 1548 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
1550 1549 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
1551 1550 else:
1552 1551 pas = [p1.ancestor(p2, warn=branchmerge)]
1553 1552
1554 1553 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
1555 1554
1556 1555 ### check phase
1557 1556 if not overwrite:
1558 1557 if len(pl) > 1:
1559 1558 raise error.Abort(_("outstanding uncommitted merge"))
1560 1559 ms = mergestate.read(repo)
1561 1560 if list(ms.unresolved()):
1562 1561 raise error.Abort(_("outstanding merge conflicts"))
1563 1562 if branchmerge:
1564 1563 if pas == [p2]:
1565 1564 raise error.Abort(_("merging with a working directory ancestor"
1566 1565 " has no effect"))
1567 1566 elif pas == [p1]:
1568 1567 if not mergeancestor and wc.branch() == p2.branch():
1569 1568 raise error.Abort(_("nothing to merge"),
1570 1569 hint=_("use 'hg update' "
1571 1570 "or check 'hg heads'"))
1572 1571 if not force and (wc.files() or wc.deleted()):
1573 1572 raise error.Abort(_("uncommitted changes"),
1574 1573 hint=_("use 'hg status' to list changes"))
1575 1574 for s in sorted(wc.substate):
1576 1575 wc.sub(s).bailifchanged()
1577 1576
1578 1577 elif not overwrite:
1579 1578 if p1 == p2: # no-op update
1580 1579 # call the hooks and exit early
1581 1580 repo.hook('preupdate', throw=True, parent1=xp2, parent2='')
1582 1581 repo.hook('update', parent1=xp2, parent2='', error=0)
1583 1582 return 0, 0, 0, 0
1584 1583
1585 1584 if (updatecheck == 'linear' and
1586 1585 pas not in ([p1], [p2])): # nonlinear
1587 1586 dirty = wc.dirty(missing=True)
1588 1587 if dirty:
1589 1588 # Branching is a bit strange to ensure we do the minimal
1590 1589 # amount of call to obsutil.foreground.
1591 1590 foreground = obsutil.foreground(repo, [p1.node()])
1592 1591 # note: the <node> variable contains a random identifier
1593 1592 if repo[node].node() in foreground:
1594 1593 pass # allow updating to successors
1595 1594 else:
1596 1595 msg = _("uncommitted changes")
1597 1596 hint = _("commit or update --clean to discard changes")
1598 1597 raise error.UpdateAbort(msg, hint=hint)
1599 1598 else:
1600 1599 # Allow jumping branches if clean and specific rev given
1601 1600 pass
1602 1601
1603 1602 if overwrite:
1604 1603 pas = [wc]
1605 1604 elif not branchmerge:
1606 1605 pas = [p1]
1607 1606
1608 1607 # deprecated config: merge.followcopies
1609 1608 followcopies = repo.ui.configbool('merge', 'followcopies', True)
1610 1609 if overwrite:
1611 1610 followcopies = False
1612 1611 elif not pas[0]:
1613 1612 followcopies = False
1614 1613 if not branchmerge and not wc.dirty(missing=True):
1615 1614 followcopies = False
1616 1615
1617 1616 ### calculate phase
1618 1617 actionbyfile, diverge, renamedelete = calculateupdates(
1619 1618 repo, wc, p2, pas, branchmerge, force, mergeancestor,
1620 1619 followcopies, matcher=matcher, mergeforce=mergeforce)
1621 1620
1622 1621 if updatecheck == 'noconflict':
1623 1622 for f, (m, args, msg) in actionbyfile.iteritems():
1624 1623 if m not in ('g', 'k', 'e', 'r'):
1625 1624 msg = _("conflicting changes")
1626 1625 hint = _("commit or update --clean to discard changes")
1627 1626 raise error.Abort(msg, hint=hint)
1628 1627
1629 1628 # Prompt and create actions. Most of this is in the resolve phase
1630 1629 # already, but we can't handle .hgsubstate in filemerge or
1631 1630 # subrepo.submerge yet so we have to keep prompting for it.
1632 1631 if '.hgsubstate' in actionbyfile:
1633 1632 f = '.hgsubstate'
1634 1633 m, args, msg = actionbyfile[f]
1635 1634 prompts = filemerge.partextras(labels)
1636 1635 prompts['f'] = f
1637 1636 if m == 'cd':
1638 1637 if repo.ui.promptchoice(
1639 1638 _("local%(l)s changed %(f)s which other%(o)s deleted\n"
1640 1639 "use (c)hanged version or (d)elete?"
1641 1640 "$$ &Changed $$ &Delete") % prompts, 0):
1642 1641 actionbyfile[f] = ('r', None, "prompt delete")
1643 1642 elif f in p1:
1644 1643 actionbyfile[f] = ('am', None, "prompt keep")
1645 1644 else:
1646 1645 actionbyfile[f] = ('a', None, "prompt keep")
1647 1646 elif m == 'dc':
1648 1647 f1, f2, fa, move, anc = args
1649 1648 flags = p2[f2].flags()
1650 1649 if repo.ui.promptchoice(
1651 1650 _("other%(o)s changed %(f)s which local%(l)s deleted\n"
1652 1651 "use (c)hanged version or leave (d)eleted?"
1653 1652 "$$ &Changed $$ &Deleted") % prompts, 0) == 0:
1654 1653 actionbyfile[f] = ('g', (flags, False), "prompt recreating")
1655 1654 else:
1656 1655 del actionbyfile[f]
1657 1656
1658 1657 # Convert to dictionary-of-lists format
1659 1658 actions = dict((m, []) for m in 'a am f g cd dc r dm dg m e k'.split())
1660 1659 for f, (m, args, msg) in actionbyfile.iteritems():
1661 1660 if m not in actions:
1662 1661 actions[m] = []
1663 1662 actions[m].append((f, args, msg))
1664 1663
1665 1664 if not util.fscasesensitive(repo.path):
1666 1665 # check collision between files only in p2 for clean update
1667 1666 if (not branchmerge and
1668 1667 (force or not wc.dirty(missing=True, branch=False))):
1669 1668 _checkcollision(repo, p2.manifest(), None)
1670 1669 else:
1671 1670 _checkcollision(repo, wc.manifest(), actions)
1672 1671
1673 1672 # divergent renames
1674 1673 for f, fl in sorted(diverge.iteritems()):
1675 1674 repo.ui.warn(_("note: possible conflict - %s was renamed "
1676 1675 "multiple times to:\n") % f)
1677 1676 for nf in fl:
1678 1677 repo.ui.warn(" %s\n" % nf)
1679 1678
1680 1679 # rename and delete
1681 1680 for f, fl in sorted(renamedelete.iteritems()):
1682 1681 repo.ui.warn(_("note: possible conflict - %s was deleted "
1683 1682 "and renamed to:\n") % f)
1684 1683 for nf in fl:
1685 1684 repo.ui.warn(" %s\n" % nf)
1686 1685
1687 1686 ### apply phase
1688 1687 if not branchmerge: # just jump to the new rev
1689 1688 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
1690 1689 if not partial:
1691 1690 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
1692 1691 # note that we're in the middle of an update
1693 1692 repo.vfs.write('updatestate', p2.hex())
1694 1693
1695 1694 stats = applyupdates(repo, actions, wc, p2, overwrite, labels=labels)
1696 1695
1697 1696 if not partial:
1698 1697 with repo.dirstate.parentchange():
1699 1698 repo.setparents(fp1, fp2)
1700 1699 recordupdates(repo, actions, branchmerge)
1701 1700 # update completed, clear state
1702 1701 util.unlink(repo.vfs.join('updatestate'))
1703 1702
1704 1703 if not branchmerge:
1705 1704 repo.dirstate.setbranch(p2.branch())
1706 1705
1707 1706 if not partial:
1708 1707 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
1709 1708 return stats
1710 1709
1711 1710 def graft(repo, ctx, pctx, labels, keepparent=False):
1712 1711 """Do a graft-like merge.
1713 1712
1714 1713 This is a merge where the merge ancestor is chosen such that one
1715 1714 or more changesets are grafted onto the current changeset. In
1716 1715 addition to the merge, this fixes up the dirstate to include only
1717 1716 a single parent (if keepparent is False) and tries to duplicate any
1718 1717 renames/copies appropriately.
1719 1718
1720 1719 ctx - changeset to rebase
1721 1720 pctx - merge base, usually ctx.p1()
1722 1721 labels - merge labels eg ['local', 'graft']
1723 1722 keepparent - keep second parent if any
1724 1723
1725 1724 """
1726 1725 # If we're grafting a descendant onto an ancestor, be sure to pass
1727 1726 # mergeancestor=True to update. This does two things: 1) allows the merge if
1728 1727 # the destination is the same as the parent of the ctx (so we can use graft
1729 1728 # to copy commits), and 2) informs update that the incoming changes are
1730 1729 # newer than the destination so it doesn't prompt about "remote changed foo
1731 1730 # which local deleted".
1732 1731 mergeancestor = repo.changelog.isancestor(repo['.'].node(), ctx.node())
1733 1732
1734 1733 stats = update(repo, ctx.node(), True, True, pctx.node(),
1735 1734 mergeancestor=mergeancestor, labels=labels)
1736 1735
1737 1736 pother = nullid
1738 1737 parents = ctx.parents()
1739 1738 if keepparent and len(parents) == 2 and pctx in parents:
1740 1739 parents.remove(pctx)
1741 1740 pother = parents[0].node()
1742 1741
1743 1742 with repo.dirstate.parentchange():
1744 1743 repo.setparents(repo['.'].node(), pother)
1745 1744 repo.dirstate.write(repo.currenttransaction())
1746 1745 # fix up dirstate for copies and renames
1747 1746 copies.duplicatecopies(repo, ctx.rev(), pctx.rev())
1748 1747 return stats
General Comments 0
You need to be logged in to leave comments. Login now