##// END OF EJS Templates
merge: remove useless dirstate.normallookup() invocation in applyupdates()...
FUJIWARA Katsunori -
r25754:19cc443a default
parent child Browse files
Show More
@@ -1,1207 +1,1197 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 import struct
9 9
10 10 from node import nullid, nullrev, hex, bin
11 11 from i18n import _
12 12 from mercurial import obsolete
13 13 import error as errormod, util, filemerge, copies, subrepo, worker
14 14 import errno, os, shutil
15 15
16 16 _pack = struct.pack
17 17 _unpack = struct.unpack
18 18
19 19 def _droponode(data):
20 20 # used for compatibility for v1
21 21 bits = data.split('\0')
22 22 bits = bits[:-2] + bits[-1:]
23 23 return '\0'.join(bits)
24 24
25 25 class mergestate(object):
26 26 '''track 3-way merge state of individual files
27 27
28 28 it is stored on disk when needed. Two file are used, one with an old
29 29 format, one with a new format. Both contains similar data, but the new
30 30 format can store new kind of field.
31 31
32 32 Current new format is a list of arbitrary record of the form:
33 33
34 34 [type][length][content]
35 35
36 36 Type is a single character, length is a 4 bytes integer, content is an
37 37 arbitrary suites of bytes of length `length`.
38 38
39 39 Type should be a letter. Capital letter are mandatory record, Mercurial
40 40 should abort if they are unknown. lower case record can be safely ignored.
41 41
42 42 Currently known record:
43 43
44 44 L: the node of the "local" part of the merge (hexified version)
45 45 O: the node of the "other" part of the merge (hexified version)
46 46 F: a file to be merged entry
47 47 '''
48 48 statepathv1 = 'merge/state'
49 49 statepathv2 = 'merge/state2'
50 50
51 51 def __init__(self, repo):
52 52 self._repo = repo
53 53 self._dirty = False
54 54 self._read()
55 55
56 56 def reset(self, node=None, other=None):
57 57 self._state = {}
58 58 self._local = None
59 59 self._other = None
60 60 if node:
61 61 self._local = node
62 62 self._other = other
63 63 shutil.rmtree(self._repo.join('merge'), True)
64 64 self._dirty = False
65 65
66 66 def _read(self):
67 67 """Analyse each record content to restore a serialized state from disk
68 68
69 69 This function process "record" entry produced by the de-serialization
70 70 of on disk file.
71 71 """
72 72 self._state = {}
73 73 self._local = None
74 74 self._other = None
75 75 records = self._readrecords()
76 76 for rtype, record in records:
77 77 if rtype == 'L':
78 78 self._local = bin(record)
79 79 elif rtype == 'O':
80 80 self._other = bin(record)
81 81 elif rtype == 'F':
82 82 bits = record.split('\0')
83 83 self._state[bits[0]] = bits[1:]
84 84 elif not rtype.islower():
85 85 raise util.Abort(_('unsupported merge state record: %s')
86 86 % rtype)
87 87 self._dirty = False
88 88
89 89 def _readrecords(self):
90 90 """Read merge state from disk and return a list of record (TYPE, data)
91 91
92 92 We read data from both v1 and v2 files and decide which one to use.
93 93
94 94 V1 has been used by version prior to 2.9.1 and contains less data than
95 95 v2. We read both versions and check if no data in v2 contradicts
96 96 v1. If there is not contradiction we can safely assume that both v1
97 97 and v2 were written at the same time and use the extract data in v2. If
98 98 there is contradiction we ignore v2 content as we assume an old version
99 99 of Mercurial has overwritten the mergestate file and left an old v2
100 100 file around.
101 101
102 102 returns list of record [(TYPE, data), ...]"""
103 103 v1records = self._readrecordsv1()
104 104 v2records = self._readrecordsv2()
105 105 oldv2 = set() # old format version of v2 record
106 106 for rec in v2records:
107 107 if rec[0] == 'L':
108 108 oldv2.add(rec)
109 109 elif rec[0] == 'F':
110 110 # drop the onode data (not contained in v1)
111 111 oldv2.add(('F', _droponode(rec[1])))
112 112 for rec in v1records:
113 113 if rec not in oldv2:
114 114 # v1 file is newer than v2 file, use it
115 115 # we have to infer the "other" changeset of the merge
116 116 # we cannot do better than that with v1 of the format
117 117 mctx = self._repo[None].parents()[-1]
118 118 v1records.append(('O', mctx.hex()))
119 119 # add place holder "other" file node information
120 120 # nobody is using it yet so we do no need to fetch the data
121 121 # if mctx was wrong `mctx[bits[-2]]` may fails.
122 122 for idx, r in enumerate(v1records):
123 123 if r[0] == 'F':
124 124 bits = r[1].split('\0')
125 125 bits.insert(-2, '')
126 126 v1records[idx] = (r[0], '\0'.join(bits))
127 127 return v1records
128 128 else:
129 129 return v2records
130 130
131 131 def _readrecordsv1(self):
132 132 """read on disk merge state for version 1 file
133 133
134 134 returns list of record [(TYPE, data), ...]
135 135
136 136 Note: the "F" data from this file are one entry short
137 137 (no "other file node" entry)
138 138 """
139 139 records = []
140 140 try:
141 141 f = self._repo.vfs(self.statepathv1)
142 142 for i, l in enumerate(f):
143 143 if i == 0:
144 144 records.append(('L', l[:-1]))
145 145 else:
146 146 records.append(('F', l[:-1]))
147 147 f.close()
148 148 except IOError as err:
149 149 if err.errno != errno.ENOENT:
150 150 raise
151 151 return records
152 152
153 153 def _readrecordsv2(self):
154 154 """read on disk merge state for version 2 file
155 155
156 156 returns list of record [(TYPE, data), ...]
157 157 """
158 158 records = []
159 159 try:
160 160 f = self._repo.vfs(self.statepathv2)
161 161 data = f.read()
162 162 off = 0
163 163 end = len(data)
164 164 while off < end:
165 165 rtype = data[off]
166 166 off += 1
167 167 length = _unpack('>I', data[off:(off + 4)])[0]
168 168 off += 4
169 169 record = data[off:(off + length)]
170 170 off += length
171 171 records.append((rtype, record))
172 172 f.close()
173 173 except IOError as err:
174 174 if err.errno != errno.ENOENT:
175 175 raise
176 176 return records
177 177
178 178 def active(self):
179 179 """Whether mergestate is active.
180 180
181 181 Returns True if there appears to be mergestate. This is a rough proxy
182 182 for "is a merge in progress."
183 183 """
184 184 # Check local variables before looking at filesystem for performance
185 185 # reasons.
186 186 return bool(self._local) or bool(self._state) or \
187 187 self._repo.vfs.exists(self.statepathv1) or \
188 188 self._repo.vfs.exists(self.statepathv2)
189 189
190 190 def commit(self):
191 191 """Write current state on disk (if necessary)"""
192 192 if self._dirty:
193 193 records = []
194 194 records.append(('L', hex(self._local)))
195 195 records.append(('O', hex(self._other)))
196 196 for d, v in self._state.iteritems():
197 197 records.append(('F', '\0'.join([d] + v)))
198 198 self._writerecords(records)
199 199 self._dirty = False
200 200
201 201 def _writerecords(self, records):
202 202 """Write current state on disk (both v1 and v2)"""
203 203 self._writerecordsv1(records)
204 204 self._writerecordsv2(records)
205 205
206 206 def _writerecordsv1(self, records):
207 207 """Write current state on disk in a version 1 file"""
208 208 f = self._repo.vfs(self.statepathv1, 'w')
209 209 irecords = iter(records)
210 210 lrecords = irecords.next()
211 211 assert lrecords[0] == 'L'
212 212 f.write(hex(self._local) + '\n')
213 213 for rtype, data in irecords:
214 214 if rtype == 'F':
215 215 f.write('%s\n' % _droponode(data))
216 216 f.close()
217 217
218 218 def _writerecordsv2(self, records):
219 219 """Write current state on disk in a version 2 file"""
220 220 f = self._repo.vfs(self.statepathv2, 'w')
221 221 for key, data in records:
222 222 assert len(key) == 1
223 223 format = '>sI%is' % len(data)
224 224 f.write(_pack(format, key, len(data), data))
225 225 f.close()
226 226
227 227 def add(self, fcl, fco, fca, fd):
228 228 """add a new (potentially?) conflicting file the merge state
229 229 fcl: file context for local,
230 230 fco: file context for remote,
231 231 fca: file context for ancestors,
232 232 fd: file path of the resulting merge.
233 233
234 234 note: also write the local version to the `.hg/merge` directory.
235 235 """
236 236 hash = util.sha1(fcl.path()).hexdigest()
237 237 self._repo.vfs.write('merge/' + hash, fcl.data())
238 238 self._state[fd] = ['u', hash, fcl.path(),
239 239 fca.path(), hex(fca.filenode()),
240 240 fco.path(), hex(fco.filenode()),
241 241 fcl.flags()]
242 242 self._dirty = True
243 243
244 244 def __contains__(self, dfile):
245 245 return dfile in self._state
246 246
247 247 def __getitem__(self, dfile):
248 248 return self._state[dfile][0]
249 249
250 250 def __iter__(self):
251 251 return iter(sorted(self._state))
252 252
253 253 def files(self):
254 254 return self._state.keys()
255 255
256 256 def mark(self, dfile, state):
257 257 self._state[dfile][0] = state
258 258 self._dirty = True
259 259
260 260 def unresolved(self):
261 261 """Obtain the paths of unresolved files."""
262 262
263 263 for f, entry in self._state.items():
264 264 if entry[0] == 'u':
265 265 yield f
266 266
267 267 def resolve(self, dfile, wctx, labels=None):
268 268 """rerun merge process for file path `dfile`"""
269 269 if self[dfile] == 'r':
270 270 return 0
271 271 stateentry = self._state[dfile]
272 272 state, hash, lfile, afile, anode, ofile, onode, flags = stateentry
273 273 octx = self._repo[self._other]
274 274 fcd = wctx[dfile]
275 275 fco = octx[ofile]
276 276 fca = self._repo.filectx(afile, fileid=anode)
277 277 # "premerge" x flags
278 278 flo = fco.flags()
279 279 fla = fca.flags()
280 280 if 'x' in flags + flo + fla and 'l' not in flags + flo + fla:
281 281 if fca.node() == nullid:
282 282 self._repo.ui.warn(_('warning: cannot merge flags for %s\n') %
283 283 afile)
284 284 elif flags == fla:
285 285 flags = flo
286 286 # restore local
287 287 f = self._repo.vfs('merge/' + hash)
288 288 self._repo.wwrite(dfile, f.read(), flags)
289 289 f.close()
290 290 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca,
291 291 labels=labels)
292 292 if r is None:
293 293 # no real conflict
294 294 del self._state[dfile]
295 295 self._dirty = True
296 296 elif not r:
297 297 self.mark(dfile, 'r')
298 298 return r
299 299
300 300 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
301 301 if f2 is None:
302 302 f2 = f
303 303 return (os.path.isfile(repo.wjoin(f))
304 304 and repo.wvfs.audit.check(f)
305 305 and repo.dirstate.normalize(f) not in repo.dirstate
306 306 and mctx[f2].cmp(wctx[f]))
307 307
308 308 def _checkunknownfiles(repo, wctx, mctx, force, actions):
309 309 """
310 310 Considers any actions that care about the presence of conflicting unknown
311 311 files. For some actions, the result is to abort; for others, it is to
312 312 choose a different action.
313 313 """
314 314 aborts = []
315 315 if not force:
316 316 for f, (m, args, msg) in actions.iteritems():
317 317 if m in ('c', 'dc'):
318 318 if _checkunknownfile(repo, wctx, mctx, f):
319 319 aborts.append(f)
320 320 elif m == 'dg':
321 321 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
322 322 aborts.append(f)
323 323
324 324 for f in sorted(aborts):
325 325 repo.ui.warn(_("%s: untracked file differs\n") % f)
326 326 if aborts:
327 327 raise util.Abort(_("untracked files in working directory differ "
328 328 "from files in requested revision"))
329 329
330 330 for f, (m, args, msg) in actions.iteritems():
331 331 if m == 'c':
332 332 actions[f] = ('g', args, msg)
333 333 elif m == 'cm':
334 334 fl2, anc = args
335 335 different = _checkunknownfile(repo, wctx, mctx, f)
336 336 if different:
337 337 actions[f] = ('m', (f, f, None, False, anc),
338 338 "remote differs from untracked local")
339 339 else:
340 340 actions[f] = ('g', (fl2,), "remote created")
341 341
342 342 def _forgetremoved(wctx, mctx, branchmerge):
343 343 """
344 344 Forget removed files
345 345
346 346 If we're jumping between revisions (as opposed to merging), and if
347 347 neither the working directory nor the target rev has the file,
348 348 then we need to remove it from the dirstate, to prevent the
349 349 dirstate from listing the file when it is no longer in the
350 350 manifest.
351 351
352 352 If we're merging, and the other revision has removed a file
353 353 that is not present in the working directory, we need to mark it
354 354 as removed.
355 355 """
356 356
357 357 actions = {}
358 358 m = 'f'
359 359 if branchmerge:
360 360 m = 'r'
361 361 for f in wctx.deleted():
362 362 if f not in mctx:
363 363 actions[f] = m, None, "forget deleted"
364 364
365 365 if not branchmerge:
366 366 for f in wctx.removed():
367 367 if f not in mctx:
368 368 actions[f] = 'f', None, "forget removed"
369 369
370 370 return actions
371 371
372 372 def _checkcollision(repo, wmf, actions):
373 373 # build provisional merged manifest up
374 374 pmmf = set(wmf)
375 375
376 376 if actions:
377 377 # k, dr, e and rd are no-op
378 378 for m in 'a', 'f', 'g', 'cd', 'dc':
379 379 for f, args, msg in actions[m]:
380 380 pmmf.add(f)
381 381 for f, args, msg in actions['r']:
382 382 pmmf.discard(f)
383 383 for f, args, msg in actions['dm']:
384 384 f2, flags = args
385 385 pmmf.discard(f2)
386 386 pmmf.add(f)
387 387 for f, args, msg in actions['dg']:
388 388 pmmf.add(f)
389 389 for f, args, msg in actions['m']:
390 390 f1, f2, fa, move, anc = args
391 391 if move:
392 392 pmmf.discard(f1)
393 393 pmmf.add(f)
394 394
395 395 # check case-folding collision in provisional merged manifest
396 396 foldmap = {}
397 397 for f in sorted(pmmf):
398 398 fold = util.normcase(f)
399 399 if fold in foldmap:
400 400 raise util.Abort(_("case-folding collision between %s and %s")
401 401 % (f, foldmap[fold]))
402 402 foldmap[fold] = f
403 403
404 404 def manifestmerge(repo, wctx, p2, pa, branchmerge, force, partial,
405 405 acceptremote, followcopies):
406 406 """
407 407 Merge p1 and p2 with ancestor pa and generate merge action list
408 408
409 409 branchmerge and force are as passed in to update
410 410 partial = function to filter file lists
411 411 acceptremote = accept the incoming changes without prompting
412 412 """
413 413
414 414 copy, movewithdir, diverge, renamedelete = {}, {}, {}, {}
415 415
416 416 # manifests fetched in order are going to be faster, so prime the caches
417 417 [x.manifest() for x in
418 418 sorted(wctx.parents() + [p2, pa], key=lambda x: x.rev())]
419 419
420 420 if followcopies:
421 421 ret = copies.mergecopies(repo, wctx, p2, pa)
422 422 copy, movewithdir, diverge, renamedelete = ret
423 423
424 424 repo.ui.note(_("resolving manifests\n"))
425 425 repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n"
426 426 % (bool(branchmerge), bool(force), bool(partial)))
427 427 repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
428 428
429 429 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
430 430 copied = set(copy.values())
431 431 copied.update(movewithdir.values())
432 432
433 433 if '.hgsubstate' in m1:
434 434 # check whether sub state is modified
435 435 for s in sorted(wctx.substate):
436 436 if wctx.sub(s).dirty():
437 437 m1['.hgsubstate'] += '+'
438 438 break
439 439
440 440 # Compare manifests
441 441 diff = m1.diff(m2)
442 442
443 443 actions = {}
444 444 for f, ((n1, fl1), (n2, fl2)) in diff.iteritems():
445 445 if partial and not partial(f):
446 446 continue
447 447 if n1 and n2: # file exists on both local and remote side
448 448 if f not in ma:
449 449 fa = copy.get(f, None)
450 450 if fa is not None:
451 451 actions[f] = ('m', (f, f, fa, False, pa.node()),
452 452 "both renamed from " + fa)
453 453 else:
454 454 actions[f] = ('m', (f, f, None, False, pa.node()),
455 455 "both created")
456 456 else:
457 457 a = ma[f]
458 458 fla = ma.flags(f)
459 459 nol = 'l' not in fl1 + fl2 + fla
460 460 if n2 == a and fl2 == fla:
461 461 actions[f] = ('k' , (), "remote unchanged")
462 462 elif n1 == a and fl1 == fla: # local unchanged - use remote
463 463 if n1 == n2: # optimization: keep local content
464 464 actions[f] = ('e', (fl2,), "update permissions")
465 465 else:
466 466 actions[f] = ('g', (fl2,), "remote is newer")
467 467 elif nol and n2 == a: # remote only changed 'x'
468 468 actions[f] = ('e', (fl2,), "update permissions")
469 469 elif nol and n1 == a: # local only changed 'x'
470 470 actions[f] = ('g', (fl1,), "remote is newer")
471 471 else: # both changed something
472 472 actions[f] = ('m', (f, f, f, False, pa.node()),
473 473 "versions differ")
474 474 elif n1: # file exists only on local side
475 475 if f in copied:
476 476 pass # we'll deal with it on m2 side
477 477 elif f in movewithdir: # directory rename, move local
478 478 f2 = movewithdir[f]
479 479 if f2 in m2:
480 480 actions[f2] = ('m', (f, f2, None, True, pa.node()),
481 481 "remote directory rename, both created")
482 482 else:
483 483 actions[f2] = ('dm', (f, fl1),
484 484 "remote directory rename - move from " + f)
485 485 elif f in copy:
486 486 f2 = copy[f]
487 487 actions[f] = ('m', (f, f2, f2, False, pa.node()),
488 488 "local copied/moved from " + f2)
489 489 elif f in ma: # clean, a different, no remote
490 490 if n1 != ma[f]:
491 491 if acceptremote:
492 492 actions[f] = ('r', None, "remote delete")
493 493 else:
494 494 actions[f] = ('cd', None, "prompt changed/deleted")
495 495 elif n1[20:] == 'a':
496 496 # This extra 'a' is added by working copy manifest to mark
497 497 # the file as locally added. We should forget it instead of
498 498 # deleting it.
499 499 actions[f] = ('f', None, "remote deleted")
500 500 else:
501 501 actions[f] = ('r', None, "other deleted")
502 502 elif n2: # file exists only on remote side
503 503 if f in copied:
504 504 pass # we'll deal with it on m1 side
505 505 elif f in movewithdir:
506 506 f2 = movewithdir[f]
507 507 if f2 in m1:
508 508 actions[f2] = ('m', (f2, f, None, False, pa.node()),
509 509 "local directory rename, both created")
510 510 else:
511 511 actions[f2] = ('dg', (f, fl2),
512 512 "local directory rename - get from " + f)
513 513 elif f in copy:
514 514 f2 = copy[f]
515 515 if f2 in m2:
516 516 actions[f] = ('m', (f2, f, f2, False, pa.node()),
517 517 "remote copied from " + f2)
518 518 else:
519 519 actions[f] = ('m', (f2, f, f2, True, pa.node()),
520 520 "remote moved from " + f2)
521 521 elif f not in ma:
522 522 # local unknown, remote created: the logic is described by the
523 523 # following table:
524 524 #
525 525 # force branchmerge different | action
526 526 # n * * | create
527 527 # y n * | create
528 528 # y y n | create
529 529 # y y y | merge
530 530 #
531 531 # Checking whether the files are different is expensive, so we
532 532 # don't do that when we can avoid it.
533 533 if not force:
534 534 actions[f] = ('c', (fl2,), "remote created")
535 535 elif not branchmerge:
536 536 actions[f] = ('c', (fl2,), "remote created")
537 537 else:
538 538 actions[f] = ('cm', (fl2, pa.node()),
539 539 "remote created, get or merge")
540 540 elif n2 != ma[f]:
541 541 if acceptremote:
542 542 actions[f] = ('c', (fl2,), "remote recreating")
543 543 else:
544 544 actions[f] = ('dc', (fl2,), "prompt deleted/changed")
545 545
546 546 return actions, diverge, renamedelete
547 547
548 548 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
549 549 """Resolves false conflicts where the nodeid changed but the content
550 550 remained the same."""
551 551
552 552 for f, (m, args, msg) in actions.items():
553 553 if m == 'cd' and f in ancestor and not wctx[f].cmp(ancestor[f]):
554 554 # local did change but ended up with same content
555 555 actions[f] = 'r', None, "prompt same"
556 556 elif m == 'dc' and f in ancestor and not mctx[f].cmp(ancestor[f]):
557 557 # remote did change but ended up with same content
558 558 del actions[f] # don't get = keep local deleted
559 559
560 560 def calculateupdates(repo, wctx, mctx, ancestors, branchmerge, force, partial,
561 561 acceptremote, followcopies):
562 562 "Calculate the actions needed to merge mctx into wctx using ancestors"
563 563
564 564 if len(ancestors) == 1: # default
565 565 actions, diverge, renamedelete = manifestmerge(
566 566 repo, wctx, mctx, ancestors[0], branchmerge, force, partial,
567 567 acceptremote, followcopies)
568 568 _checkunknownfiles(repo, wctx, mctx, force, actions)
569 569
570 570 else: # only when merge.preferancestor=* - the default
571 571 repo.ui.note(
572 572 _("note: merging %s and %s using bids from ancestors %s\n") %
573 573 (wctx, mctx, _(' and ').join(str(anc) for anc in ancestors)))
574 574
575 575 # Call for bids
576 576 fbids = {} # mapping filename to bids (action method to list af actions)
577 577 diverge, renamedelete = None, None
578 578 for ancestor in ancestors:
579 579 repo.ui.note(_('\ncalculating bids for ancestor %s\n') % ancestor)
580 580 actions, diverge1, renamedelete1 = manifestmerge(
581 581 repo, wctx, mctx, ancestor, branchmerge, force, partial,
582 582 acceptremote, followcopies)
583 583 _checkunknownfiles(repo, wctx, mctx, force, actions)
584 584 if diverge is None: # and renamedelete is None.
585 585 # Arbitrarily pick warnings from first iteration
586 586 diverge = diverge1
587 587 renamedelete = renamedelete1
588 588 for f, a in sorted(actions.iteritems()):
589 589 m, args, msg = a
590 590 repo.ui.debug(' %s: %s -> %s\n' % (f, msg, m))
591 591 if f in fbids:
592 592 d = fbids[f]
593 593 if m in d:
594 594 d[m].append(a)
595 595 else:
596 596 d[m] = [a]
597 597 else:
598 598 fbids[f] = {m: [a]}
599 599
600 600 # Pick the best bid for each file
601 601 repo.ui.note(_('\nauction for merging merge bids\n'))
602 602 actions = {}
603 603 for f, bids in sorted(fbids.items()):
604 604 # bids is a mapping from action method to list af actions
605 605 # Consensus?
606 606 if len(bids) == 1: # all bids are the same kind of method
607 607 m, l = bids.items()[0]
608 608 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
609 609 repo.ui.note(" %s: consensus for %s\n" % (f, m))
610 610 actions[f] = l[0]
611 611 continue
612 612 # If keep is an option, just do it.
613 613 if 'k' in bids:
614 614 repo.ui.note(" %s: picking 'keep' action\n" % f)
615 615 actions[f] = bids['k'][0]
616 616 continue
617 617 # If there are gets and they all agree [how could they not?], do it.
618 618 if 'g' in bids:
619 619 ga0 = bids['g'][0]
620 620 if all(a == ga0 for a in bids['g'][1:]):
621 621 repo.ui.note(" %s: picking 'get' action\n" % f)
622 622 actions[f] = ga0
623 623 continue
624 624 # TODO: Consider other simple actions such as mode changes
625 625 # Handle inefficient democrazy.
626 626 repo.ui.note(_(' %s: multiple bids for merge action:\n') % f)
627 627 for m, l in sorted(bids.items()):
628 628 for _f, args, msg in l:
629 629 repo.ui.note(' %s -> %s\n' % (msg, m))
630 630 # Pick random action. TODO: Instead, prompt user when resolving
631 631 m, l = bids.items()[0]
632 632 repo.ui.warn(_(' %s: ambiguous merge - picked %s action\n') %
633 633 (f, m))
634 634 actions[f] = l[0]
635 635 continue
636 636 repo.ui.note(_('end of auction\n\n'))
637 637
638 638 _resolvetrivial(repo, wctx, mctx, ancestors[0], actions)
639 639
640 640 if wctx.rev() is None:
641 641 fractions = _forgetremoved(wctx, mctx, branchmerge)
642 642 actions.update(fractions)
643 643
644 644 return actions, diverge, renamedelete
645 645
646 646 def batchremove(repo, actions):
647 647 """apply removes to the working directory
648 648
649 649 yields tuples for progress updates
650 650 """
651 651 verbose = repo.ui.verbose
652 652 unlink = util.unlinkpath
653 653 wjoin = repo.wjoin
654 654 audit = repo.wvfs.audit
655 655 i = 0
656 656 for f, args, msg in actions:
657 657 repo.ui.debug(" %s: %s -> r\n" % (f, msg))
658 658 if verbose:
659 659 repo.ui.note(_("removing %s\n") % f)
660 660 audit(f)
661 661 try:
662 662 unlink(wjoin(f), ignoremissing=True)
663 663 except OSError as inst:
664 664 repo.ui.warn(_("update failed to remove %s: %s!\n") %
665 665 (f, inst.strerror))
666 666 if i == 100:
667 667 yield i, f
668 668 i = 0
669 669 i += 1
670 670 if i > 0:
671 671 yield i, f
672 672
673 673 def batchget(repo, mctx, actions):
674 674 """apply gets to the working directory
675 675
676 676 mctx is the context to get from
677 677
678 678 yields tuples for progress updates
679 679 """
680 680 verbose = repo.ui.verbose
681 681 fctx = mctx.filectx
682 682 wwrite = repo.wwrite
683 683 i = 0
684 684 for f, args, msg in actions:
685 685 repo.ui.debug(" %s: %s -> g\n" % (f, msg))
686 686 if verbose:
687 687 repo.ui.note(_("getting %s\n") % f)
688 688 wwrite(f, fctx(f).data(), args[0])
689 689 if i == 100:
690 690 yield i, f
691 691 i = 0
692 692 i += 1
693 693 if i > 0:
694 694 yield i, f
695 695
696 696 def applyupdates(repo, actions, wctx, mctx, overwrite, labels=None):
697 697 """apply the merge action list to the working directory
698 698
699 699 wctx is the working copy context
700 700 mctx is the context to be merged into the working copy
701 701
702 702 Return a tuple of counts (updated, merged, removed, unresolved) that
703 703 describes how many files were affected by the update.
704 704 """
705 705
706 706 updated, merged, removed, unresolved = 0, 0, 0, 0
707 707 ms = mergestate(repo)
708 708 ms.reset(wctx.p1().node(), mctx.node())
709 709 moves = []
710 710 for m, l in actions.items():
711 711 l.sort()
712 712
713 713 # prescan for merges
714 714 for f, args, msg in actions['m']:
715 715 f1, f2, fa, move, anc = args
716 716 if f == '.hgsubstate': # merged internally
717 717 continue
718 718 repo.ui.debug(" preserving %s for resolve of %s\n" % (f1, f))
719 719 fcl = wctx[f1]
720 720 fco = mctx[f2]
721 721 actx = repo[anc]
722 722 if fa in actx:
723 723 fca = actx[fa]
724 724 else:
725 725 fca = repo.filectx(f1, fileid=nullrev)
726 726 ms.add(fcl, fco, fca, f)
727 727 if f1 != f and move:
728 728 moves.append(f1)
729 729
730 730 audit = repo.wvfs.audit
731 731 _updating = _('updating')
732 732 _files = _('files')
733 733 progress = repo.ui.progress
734 734
735 735 # remove renamed files after safely stored
736 736 for f in moves:
737 737 if os.path.lexists(repo.wjoin(f)):
738 738 repo.ui.debug("removing %s\n" % f)
739 739 audit(f)
740 740 util.unlinkpath(repo.wjoin(f))
741 741
742 742 numupdates = sum(len(l) for m, l in actions.items() if m != 'k')
743 743
744 def dirtysubstate():
745 # mark '.hgsubstate' as possibly dirty forcibly, because
746 # modified '.hgsubstate' is misunderstood as clean,
747 # when both st_size/st_mtime of '.hgsubstate' aren't changed,
748 # even if "submerge" fails and '.hgsubstate' is inconsistent
749 repo.dirstate.normallookup('.hgsubstate')
750
751 744 if [a for a in actions['r'] if a[0] == '.hgsubstate']:
752 dirtysubstate()
753 745 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
754 746
755 747 # remove in parallel (must come first)
756 748 z = 0
757 749 prog = worker.worker(repo.ui, 0.001, batchremove, (repo,), actions['r'])
758 750 for i, item in prog:
759 751 z += i
760 752 progress(_updating, z, item=item, total=numupdates, unit=_files)
761 753 removed = len(actions['r'])
762 754
763 755 # get in parallel
764 756 prog = worker.worker(repo.ui, 0.001, batchget, (repo, mctx), actions['g'])
765 757 for i, item in prog:
766 758 z += i
767 759 progress(_updating, z, item=item, total=numupdates, unit=_files)
768 760 updated = len(actions['g'])
769 761
770 762 if [a for a in actions['g'] if a[0] == '.hgsubstate']:
771 dirtysubstate()
772 763 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
773 764
774 765 # forget (manifest only, just log it) (must come first)
775 766 for f, args, msg in actions['f']:
776 767 repo.ui.debug(" %s: %s -> f\n" % (f, msg))
777 768 z += 1
778 769 progress(_updating, z, item=f, total=numupdates, unit=_files)
779 770
780 771 # re-add (manifest only, just log it)
781 772 for f, args, msg in actions['a']:
782 773 repo.ui.debug(" %s: %s -> a\n" % (f, msg))
783 774 z += 1
784 775 progress(_updating, z, item=f, total=numupdates, unit=_files)
785 776
786 777 # keep (noop, just log it)
787 778 for f, args, msg in actions['k']:
788 779 repo.ui.debug(" %s: %s -> k\n" % (f, msg))
789 780 # no progress
790 781
791 782 # merge
792 783 for f, args, msg in actions['m']:
793 784 repo.ui.debug(" %s: %s -> m\n" % (f, msg))
794 785 z += 1
795 786 progress(_updating, z, item=f, total=numupdates, unit=_files)
796 787 if f == '.hgsubstate': # subrepo states need updating
797 dirtysubstate()
798 788 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
799 789 overwrite)
800 790 continue
801 791 audit(f)
802 792 r = ms.resolve(f, wctx, labels=labels)
803 793 if r is not None and r > 0:
804 794 unresolved += 1
805 795 else:
806 796 if r is None:
807 797 updated += 1
808 798 else:
809 799 merged += 1
810 800
811 801 # directory rename, move local
812 802 for f, args, msg in actions['dm']:
813 803 repo.ui.debug(" %s: %s -> dm\n" % (f, msg))
814 804 z += 1
815 805 progress(_updating, z, item=f, total=numupdates, unit=_files)
816 806 f0, flags = args
817 807 repo.ui.note(_("moving %s to %s\n") % (f0, f))
818 808 audit(f)
819 809 repo.wwrite(f, wctx.filectx(f0).data(), flags)
820 810 util.unlinkpath(repo.wjoin(f0))
821 811 updated += 1
822 812
823 813 # local directory rename, get
824 814 for f, args, msg in actions['dg']:
825 815 repo.ui.debug(" %s: %s -> dg\n" % (f, msg))
826 816 z += 1
827 817 progress(_updating, z, item=f, total=numupdates, unit=_files)
828 818 f0, flags = args
829 819 repo.ui.note(_("getting %s to %s\n") % (f0, f))
830 820 repo.wwrite(f, mctx.filectx(f0).data(), flags)
831 821 updated += 1
832 822
833 823 # exec
834 824 for f, args, msg in actions['e']:
835 825 repo.ui.debug(" %s: %s -> e\n" % (f, msg))
836 826 z += 1
837 827 progress(_updating, z, item=f, total=numupdates, unit=_files)
838 828 flags, = args
839 829 audit(f)
840 830 util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
841 831 updated += 1
842 832
843 833 ms.commit()
844 834 progress(_updating, None, total=numupdates, unit=_files)
845 835
846 836 return updated, merged, removed, unresolved
847 837
848 838 def recordupdates(repo, actions, branchmerge):
849 839 "record merge actions to the dirstate"
850 840 # remove (must come first)
851 841 for f, args, msg in actions['r']:
852 842 if branchmerge:
853 843 repo.dirstate.remove(f)
854 844 else:
855 845 repo.dirstate.drop(f)
856 846
857 847 # forget (must come first)
858 848 for f, args, msg in actions['f']:
859 849 repo.dirstate.drop(f)
860 850
861 851 # re-add
862 852 for f, args, msg in actions['a']:
863 853 if not branchmerge:
864 854 repo.dirstate.add(f)
865 855
866 856 # exec change
867 857 for f, args, msg in actions['e']:
868 858 repo.dirstate.normallookup(f)
869 859
870 860 # keep
871 861 for f, args, msg in actions['k']:
872 862 pass
873 863
874 864 # get
875 865 for f, args, msg in actions['g']:
876 866 if branchmerge:
877 867 repo.dirstate.otherparent(f)
878 868 else:
879 869 repo.dirstate.normal(f)
880 870
881 871 # merge
882 872 for f, args, msg in actions['m']:
883 873 f1, f2, fa, move, anc = args
884 874 if branchmerge:
885 875 # We've done a branch merge, mark this file as merged
886 876 # so that we properly record the merger later
887 877 repo.dirstate.merge(f)
888 878 if f1 != f2: # copy/rename
889 879 if move:
890 880 repo.dirstate.remove(f1)
891 881 if f1 != f:
892 882 repo.dirstate.copy(f1, f)
893 883 else:
894 884 repo.dirstate.copy(f2, f)
895 885 else:
896 886 # We've update-merged a locally modified file, so
897 887 # we set the dirstate to emulate a normal checkout
898 888 # of that file some time in the past. Thus our
899 889 # merge will appear as a normal local file
900 890 # modification.
901 891 if f2 == f: # file not locally copied/moved
902 892 repo.dirstate.normallookup(f)
903 893 if move:
904 894 repo.dirstate.drop(f1)
905 895
906 896 # directory rename, move local
907 897 for f, args, msg in actions['dm']:
908 898 f0, flag = args
909 899 if branchmerge:
910 900 repo.dirstate.add(f)
911 901 repo.dirstate.remove(f0)
912 902 repo.dirstate.copy(f0, f)
913 903 else:
914 904 repo.dirstate.normal(f)
915 905 repo.dirstate.drop(f0)
916 906
917 907 # directory rename, get
918 908 for f, args, msg in actions['dg']:
919 909 f0, flag = args
920 910 if branchmerge:
921 911 repo.dirstate.add(f)
922 912 repo.dirstate.copy(f0, f)
923 913 else:
924 914 repo.dirstate.normal(f)
925 915
926 916 def update(repo, node, branchmerge, force, partial, ancestor=None,
927 917 mergeancestor=False, labels=None):
928 918 """
929 919 Perform a merge between the working directory and the given node
930 920
931 921 node = the node to update to, or None if unspecified
932 922 branchmerge = whether to merge between branches
933 923 force = whether to force branch merging or file overwriting
934 924 partial = a function to filter file lists (dirstate not updated)
935 925 mergeancestor = whether it is merging with an ancestor. If true,
936 926 we should accept the incoming changes for any prompts that occur.
937 927 If false, merging with an ancestor (fast-forward) is only allowed
938 928 between different named branches. This flag is used by rebase extension
939 929 as a temporary fix and should be avoided in general.
940 930
941 931 The table below shows all the behaviors of the update command
942 932 given the -c and -C or no options, whether the working directory
943 933 is dirty, whether a revision is specified, and the relationship of
944 934 the parent rev to the target rev (linear, on the same named
945 935 branch, or on another named branch).
946 936
947 937 This logic is tested by test-update-branches.t.
948 938
949 939 -c -C dirty rev | linear same cross
950 940 n n n n | ok (1) x
951 941 n n n y | ok ok ok
952 942 n n y n | merge (2) (2)
953 943 n n y y | merge (3) (3)
954 944 n y * * | --- discard ---
955 945 y n y * | --- (4) ---
956 946 y n n * | --- ok ---
957 947 y y * * | --- (5) ---
958 948
959 949 x = can't happen
960 950 * = don't-care
961 951 1 = abort: not a linear update (merge or update --check to force update)
962 952 2 = abort: uncommitted changes (commit and merge, or update --clean to
963 953 discard changes)
964 954 3 = abort: uncommitted changes (commit or update --clean to discard changes)
965 955 4 = abort: uncommitted changes (checked in commands.py)
966 956 5 = incompatible options (checked in commands.py)
967 957
968 958 Return the same tuple as applyupdates().
969 959 """
970 960
971 961 onode = node
972 962 wlock = repo.wlock()
973 963 try:
974 964 wc = repo[None]
975 965 pl = wc.parents()
976 966 p1 = pl[0]
977 967 pas = [None]
978 968 if ancestor is not None:
979 969 pas = [repo[ancestor]]
980 970
981 971 if node is None:
982 972 # Here is where we should consider bookmarks, divergent bookmarks,
983 973 # foreground changesets (successors), and tip of current branch;
984 974 # but currently we are only checking the branch tips.
985 975 try:
986 976 node = repo.branchtip(wc.branch())
987 977 except errormod.RepoLookupError:
988 978 if wc.branch() == 'default': # no default branch!
989 979 node = repo.lookup('tip') # update to tip
990 980 else:
991 981 raise util.Abort(_("branch %s not found") % wc.branch())
992 982
993 983 if p1.obsolete() and not p1.children():
994 984 # allow updating to successors
995 985 successors = obsolete.successorssets(repo, p1.node())
996 986
997 987 # behavior of certain cases is as follows,
998 988 #
999 989 # divergent changesets: update to highest rev, similar to what
1000 990 # is currently done when there are more than one head
1001 991 # (i.e. 'tip')
1002 992 #
1003 993 # replaced changesets: same as divergent except we know there
1004 994 # is no conflict
1005 995 #
1006 996 # pruned changeset: no update is done; though, we could
1007 997 # consider updating to the first non-obsolete parent,
1008 998 # similar to what is current done for 'hg prune'
1009 999
1010 1000 if successors:
1011 1001 # flatten the list here handles both divergent (len > 1)
1012 1002 # and the usual case (len = 1)
1013 1003 successors = [n for sub in successors for n in sub]
1014 1004
1015 1005 # get the max revision for the given successors set,
1016 1006 # i.e. the 'tip' of a set
1017 1007 node = repo.revs('max(%ln)', successors).first()
1018 1008 pas = [p1]
1019 1009
1020 1010 overwrite = force and not branchmerge
1021 1011
1022 1012 p2 = repo[node]
1023 1013 if pas[0] is None:
1024 1014 if repo.ui.config('merge', 'preferancestor', '*') == '*':
1025 1015 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
1026 1016 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
1027 1017 else:
1028 1018 pas = [p1.ancestor(p2, warn=branchmerge)]
1029 1019
1030 1020 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
1031 1021
1032 1022 ### check phase
1033 1023 if not overwrite and len(pl) > 1:
1034 1024 raise util.Abort(_("outstanding uncommitted merge"))
1035 1025 if branchmerge:
1036 1026 if pas == [p2]:
1037 1027 raise util.Abort(_("merging with a working directory ancestor"
1038 1028 " has no effect"))
1039 1029 elif pas == [p1]:
1040 1030 if not mergeancestor and p1.branch() == p2.branch():
1041 1031 raise util.Abort(_("nothing to merge"),
1042 1032 hint=_("use 'hg update' "
1043 1033 "or check 'hg heads'"))
1044 1034 if not force and (wc.files() or wc.deleted()):
1045 1035 raise util.Abort(_("uncommitted changes"),
1046 1036 hint=_("use 'hg status' to list changes"))
1047 1037 for s in sorted(wc.substate):
1048 1038 wc.sub(s).bailifchanged()
1049 1039
1050 1040 elif not overwrite:
1051 1041 if p1 == p2: # no-op update
1052 1042 # call the hooks and exit early
1053 1043 repo.hook('preupdate', throw=True, parent1=xp2, parent2='')
1054 1044 repo.hook('update', parent1=xp2, parent2='', error=0)
1055 1045 return 0, 0, 0, 0
1056 1046
1057 1047 if pas not in ([p1], [p2]): # nonlinear
1058 1048 dirty = wc.dirty(missing=True)
1059 1049 if dirty or onode is None:
1060 1050 # Branching is a bit strange to ensure we do the minimal
1061 1051 # amount of call to obsolete.background.
1062 1052 foreground = obsolete.foreground(repo, [p1.node()])
1063 1053 # note: the <node> variable contains a random identifier
1064 1054 if repo[node].node() in foreground:
1065 1055 pas = [p1] # allow updating to successors
1066 1056 elif dirty:
1067 1057 msg = _("uncommitted changes")
1068 1058 if onode is None:
1069 1059 hint = _("commit and merge, or update --clean to"
1070 1060 " discard changes")
1071 1061 else:
1072 1062 hint = _("commit or update --clean to discard"
1073 1063 " changes")
1074 1064 raise util.Abort(msg, hint=hint)
1075 1065 else: # node is none
1076 1066 msg = _("not a linear update")
1077 1067 hint = _("merge or update --check to force update")
1078 1068 raise util.Abort(msg, hint=hint)
1079 1069 else:
1080 1070 # Allow jumping branches if clean and specific rev given
1081 1071 pas = [p1]
1082 1072
1083 1073 followcopies = False
1084 1074 if overwrite:
1085 1075 pas = [wc]
1086 1076 elif pas == [p2]: # backwards
1087 1077 pas = [wc.p1()]
1088 1078 elif not branchmerge and not wc.dirty(missing=True):
1089 1079 pass
1090 1080 elif pas[0] and repo.ui.configbool('merge', 'followcopies', True):
1091 1081 followcopies = True
1092 1082
1093 1083 ### calculate phase
1094 1084 actionbyfile, diverge, renamedelete = calculateupdates(
1095 1085 repo, wc, p2, pas, branchmerge, force, partial, mergeancestor,
1096 1086 followcopies)
1097 1087 # Convert to dictionary-of-lists format
1098 1088 actions = dict((m, []) for m in 'a f g cd dc r dm dg m e k'.split())
1099 1089 for f, (m, args, msg) in actionbyfile.iteritems():
1100 1090 if m not in actions:
1101 1091 actions[m] = []
1102 1092 actions[m].append((f, args, msg))
1103 1093
1104 1094 if not util.checkcase(repo.path):
1105 1095 # check collision between files only in p2 for clean update
1106 1096 if (not branchmerge and
1107 1097 (force or not wc.dirty(missing=True, branch=False))):
1108 1098 _checkcollision(repo, p2.manifest(), None)
1109 1099 else:
1110 1100 _checkcollision(repo, wc.manifest(), actions)
1111 1101
1112 1102 # Prompt and create actions. TODO: Move this towards resolve phase.
1113 1103 for f, args, msg in sorted(actions['cd']):
1114 1104 if repo.ui.promptchoice(
1115 1105 _("local changed %s which remote deleted\n"
1116 1106 "use (c)hanged version or (d)elete?"
1117 1107 "$$ &Changed $$ &Delete") % f, 0):
1118 1108 actions['r'].append((f, None, "prompt delete"))
1119 1109 else:
1120 1110 actions['a'].append((f, None, "prompt keep"))
1121 1111 del actions['cd'][:]
1122 1112
1123 1113 for f, args, msg in sorted(actions['dc']):
1124 1114 flags, = args
1125 1115 if repo.ui.promptchoice(
1126 1116 _("remote changed %s which local deleted\n"
1127 1117 "use (c)hanged version or leave (d)eleted?"
1128 1118 "$$ &Changed $$ &Deleted") % f, 0) == 0:
1129 1119 actions['g'].append((f, (flags,), "prompt recreating"))
1130 1120 del actions['dc'][:]
1131 1121
1132 1122 ### apply phase
1133 1123 if not branchmerge: # just jump to the new rev
1134 1124 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
1135 1125 if not partial:
1136 1126 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
1137 1127 # note that we're in the middle of an update
1138 1128 repo.vfs.write('updatestate', p2.hex())
1139 1129
1140 1130 stats = applyupdates(repo, actions, wc, p2, overwrite, labels=labels)
1141 1131
1142 1132 # divergent renames
1143 1133 for f, fl in sorted(diverge.iteritems()):
1144 1134 repo.ui.warn(_("note: possible conflict - %s was renamed "
1145 1135 "multiple times to:\n") % f)
1146 1136 for nf in fl:
1147 1137 repo.ui.warn(" %s\n" % nf)
1148 1138
1149 1139 # rename and delete
1150 1140 for f, fl in sorted(renamedelete.iteritems()):
1151 1141 repo.ui.warn(_("note: possible conflict - %s was deleted "
1152 1142 "and renamed to:\n") % f)
1153 1143 for nf in fl:
1154 1144 repo.ui.warn(" %s\n" % nf)
1155 1145
1156 1146 if not partial:
1157 1147 repo.dirstate.beginparentchange()
1158 1148 repo.setparents(fp1, fp2)
1159 1149 recordupdates(repo, actions, branchmerge)
1160 1150 # update completed, clear state
1161 1151 util.unlink(repo.join('updatestate'))
1162 1152
1163 1153 if not branchmerge:
1164 1154 repo.dirstate.setbranch(p2.branch())
1165 1155 repo.dirstate.endparentchange()
1166 1156 finally:
1167 1157 wlock.release()
1168 1158
1169 1159 if not partial:
1170 1160 def updatehook(parent1=xp1, parent2=xp2, error=stats[3]):
1171 1161 repo.hook('update', parent1=parent1, parent2=parent2, error=error)
1172 1162 repo._afterlock(updatehook)
1173 1163 return stats
1174 1164
1175 1165 def graft(repo, ctx, pctx, labels):
1176 1166 """Do a graft-like merge.
1177 1167
1178 1168 This is a merge where the merge ancestor is chosen such that one
1179 1169 or more changesets are grafted onto the current changeset. In
1180 1170 addition to the merge, this fixes up the dirstate to include only
1181 1171 a single parent and tries to duplicate any renames/copies
1182 1172 appropriately.
1183 1173
1184 1174 ctx - changeset to rebase
1185 1175 pctx - merge base, usually ctx.p1()
1186 1176 labels - merge labels eg ['local', 'graft']
1187 1177
1188 1178 """
1189 1179 # If we're grafting a descendant onto an ancestor, be sure to pass
1190 1180 # mergeancestor=True to update. This does two things: 1) allows the merge if
1191 1181 # the destination is the same as the parent of the ctx (so we can use graft
1192 1182 # to copy commits), and 2) informs update that the incoming changes are
1193 1183 # newer than the destination so it doesn't prompt about "remote changed foo
1194 1184 # which local deleted".
1195 1185 mergeancestor = repo.changelog.isancestor(repo['.'].node(), ctx.node())
1196 1186
1197 1187 stats = update(repo, ctx.node(), True, True, False, pctx.node(),
1198 1188 mergeancestor=mergeancestor, labels=labels)
1199 1189
1200 1190 # drop the second merge parent
1201 1191 repo.dirstate.beginparentchange()
1202 1192 repo.setparents(repo['.'].node(), nullid)
1203 1193 repo.dirstate.write()
1204 1194 # fix up dirstate for copies and renames
1205 1195 copies.duplicatecopies(repo, ctx.rev(), pctx.rev())
1206 1196 repo.dirstate.endparentchange()
1207 1197 return stats
General Comments 0
You need to be logged in to leave comments. Login now