##// END OF EJS Templates
copies: simplify the conditional for _filter's case 3...
marmoute -
r47128:1d6d1a15 default
parent child Browse files
Show More
@@ -1,1231 +1,1230 b''
1 1 # coding: utf8
2 2 # copies.py - copy detection for Mercurial
3 3 #
4 4 # Copyright 2008 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import collections
12 12 import os
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 nullid,
17 17 nullrev,
18 18 )
19 19
20 20 from . import (
21 21 match as matchmod,
22 22 pathutil,
23 23 policy,
24 24 pycompat,
25 25 util,
26 26 )
27 27
28 28
29 29 from .utils import stringutil
30 30
31 31 from .revlogutils import (
32 32 flagutil,
33 33 sidedata as sidedatamod,
34 34 )
35 35
36 36 rustmod = policy.importrust("copy_tracing")
37 37
38 38
39 39 def _filter(src, dst, t):
40 40 """filters out invalid copies after chaining"""
41 41
42 42 # When _chain()'ing copies in 'a' (from 'src' via some other commit 'mid')
43 43 # with copies in 'b' (from 'mid' to 'dst'), we can get the different cases
44 44 # in the following table (not including trivial cases). For example, case 6
45 45 # is where a file existed in 'src' and remained under that name in 'mid' and
46 46 # then was renamed between 'mid' and 'dst'.
47 47 #
48 48 # case src mid dst result
49 49 # 1 x y - -
50 50 # 2 x y y x->y
51 51 # 3 x y x -
52 52 # 4 x y z x->z
53 53 # 5 - x y -
54 54 # 6 x x y x->y
55 55 #
56 56 # _chain() takes care of chaining the copies in 'a' and 'b', but it
57 57 # cannot tell the difference between cases 1 and 2, between 3 and 4, or
58 58 # between 5 and 6, so it includes all cases in its result.
59 59 # Cases 1, 3, and 5 are then removed by _filter().
60 60
61 61 for k, v in list(t.items()):
62 # remove copies from files that didn't exist
63 if v not in src: # case 5
62 if k == v: # case 3
64 63 del t[k]
65 # remove criss-crossed copies
66 elif k in src and v in dst:
64 elif v not in src: # case 5
65 # remove copies from files that didn't exist
67 66 del t[k]
68 # remove copies to files that were then removed
69 67 elif k not in dst: # case 1
68 # remove copies to files that were then removed
70 69 del t[k]
71 70
72 71
73 72 def _chain(prefix, suffix):
74 73 """chain two sets of copies 'prefix' and 'suffix'"""
75 74 result = prefix.copy()
76 75 for key, value in pycompat.iteritems(suffix):
77 76 result[key] = prefix.get(value, value)
78 77 return result
79 78
80 79
81 80 def _tracefile(fctx, am, basemf):
82 81 """return file context that is the ancestor of fctx present in ancestor
83 82 manifest am
84 83
85 84 Note: we used to try and stop after a given limit, however checking if that
86 85 limit is reached turned out to be very expensive. we are better off
87 86 disabling that feature."""
88 87
89 88 for f in fctx.ancestors():
90 89 path = f.path()
91 90 if am.get(path, None) == f.filenode():
92 91 return path
93 92 if basemf and basemf.get(path, None) == f.filenode():
94 93 return path
95 94
96 95
97 96 def _dirstatecopies(repo, match=None):
98 97 ds = repo.dirstate
99 98 c = ds.copies().copy()
100 99 for k in list(c):
101 100 if ds[k] not in b'anm' or (match and not match(k)):
102 101 del c[k]
103 102 return c
104 103
105 104
106 105 def _computeforwardmissing(a, b, match=None):
107 106 """Computes which files are in b but not a.
108 107 This is its own function so extensions can easily wrap this call to see what
109 108 files _forwardcopies is about to process.
110 109 """
111 110 ma = a.manifest()
112 111 mb = b.manifest()
113 112 return mb.filesnotin(ma, match=match)
114 113
115 114
116 115 def usechangesetcentricalgo(repo):
117 116 """Checks if we should use changeset-centric copy algorithms"""
118 117 if repo.filecopiesmode == b'changeset-sidedata':
119 118 return True
120 119 readfrom = repo.ui.config(b'experimental', b'copies.read-from')
121 120 changesetsource = (b'changeset-only', b'compatibility')
122 121 return readfrom in changesetsource
123 122
124 123
125 124 def _committedforwardcopies(a, b, base, match):
126 125 """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
127 126 # files might have to be traced back to the fctx parent of the last
128 127 # one-side-only changeset, but not further back than that
129 128 repo = a._repo
130 129
131 130 if usechangesetcentricalgo(repo):
132 131 return _changesetforwardcopies(a, b, match)
133 132
134 133 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
135 134 dbg = repo.ui.debug
136 135 if debug:
137 136 dbg(b'debug.copies: looking into rename from %s to %s\n' % (a, b))
138 137 am = a.manifest()
139 138 basemf = None if base is None else base.manifest()
140 139
141 140 # find where new files came from
142 141 # we currently don't try to find where old files went, too expensive
143 142 # this means we can miss a case like 'hg rm b; hg cp a b'
144 143 cm = {}
145 144
146 145 # Computing the forward missing is quite expensive on large manifests, since
147 146 # it compares the entire manifests. We can optimize it in the common use
148 147 # case of computing what copies are in a commit versus its parent (like
149 148 # during a rebase or histedit). Note, we exclude merge commits from this
150 149 # optimization, since the ctx.files() for a merge commit is not correct for
151 150 # this comparison.
152 151 forwardmissingmatch = match
153 152 if b.p1() == a and b.p2().node() == nullid:
154 153 filesmatcher = matchmod.exact(b.files())
155 154 forwardmissingmatch = matchmod.intersectmatchers(match, filesmatcher)
156 155 missing = _computeforwardmissing(a, b, match=forwardmissingmatch)
157 156
158 157 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
159 158
160 159 if debug:
161 160 dbg(b'debug.copies: missing files to search: %d\n' % len(missing))
162 161
163 162 for f in sorted(missing):
164 163 if debug:
165 164 dbg(b'debug.copies: tracing file: %s\n' % f)
166 165 fctx = b[f]
167 166 fctx._ancestrycontext = ancestrycontext
168 167
169 168 if debug:
170 169 start = util.timer()
171 170 opath = _tracefile(fctx, am, basemf)
172 171 if opath:
173 172 if debug:
174 173 dbg(b'debug.copies: rename of: %s\n' % opath)
175 174 cm[f] = opath
176 175 if debug:
177 176 dbg(
178 177 b'debug.copies: time: %f seconds\n'
179 178 % (util.timer() - start)
180 179 )
181 180 return cm
182 181
183 182
184 183 def _revinfo_getter(repo, match):
185 184 """returns a function that returns the following data given a <rev>"
186 185
187 186 * p1: revision number of first parent
188 187 * p2: revision number of first parent
189 188 * changes: a ChangingFiles object
190 189 """
191 190 cl = repo.changelog
192 191 parents = cl.parentrevs
193 192 flags = cl.flags
194 193
195 194 HASCOPIESINFO = flagutil.REVIDX_HASCOPIESINFO
196 195
197 196 changelogrevision = cl.changelogrevision
198 197
199 198 if rustmod is not None:
200 199
201 200 def revinfo(rev):
202 201 p1, p2 = parents(rev)
203 202 if flags(rev) & HASCOPIESINFO:
204 203 raw = changelogrevision(rev)._sidedata.get(sidedatamod.SD_FILES)
205 204 else:
206 205 raw = None
207 206 return (p1, p2, raw)
208 207
209 208 else:
210 209
211 210 def revinfo(rev):
212 211 p1, p2 = parents(rev)
213 212 if flags(rev) & HASCOPIESINFO:
214 213 changes = changelogrevision(rev).changes
215 214 else:
216 215 changes = None
217 216 return (p1, p2, changes)
218 217
219 218 return revinfo
220 219
221 220
222 221 def cached_is_ancestor(is_ancestor):
223 222 """return a cached version of is_ancestor"""
224 223 cache = {}
225 224
226 225 def _is_ancestor(anc, desc):
227 226 if anc > desc:
228 227 return False
229 228 elif anc == desc:
230 229 return True
231 230 key = (anc, desc)
232 231 ret = cache.get(key)
233 232 if ret is None:
234 233 ret = cache[key] = is_ancestor(anc, desc)
235 234 return ret
236 235
237 236 return _is_ancestor
238 237
239 238
240 239 def _changesetforwardcopies(a, b, match):
241 240 if a.rev() in (nullrev, b.rev()):
242 241 return {}
243 242
244 243 repo = a.repo().unfiltered()
245 244 children = {}
246 245
247 246 cl = repo.changelog
248 247 isancestor = cl.isancestorrev
249 248
250 249 # To track rename from "A" to B, we need to gather all parent β†’ children
251 250 # edges that are contains in `::B` but not in `::A`.
252 251 #
253 252 #
254 253 # To do so, we need to gather all revisions exclusiveΒΉ to "B" (ieΒΉ: `::b -
255 254 # ::a`) and also all the "roots point", ie the parents of the exclusive set
256 255 # that belong to ::a. These are exactly all the revisions needed to express
257 256 # the parent β†’ children we need to combine.
258 257 #
259 258 # [1] actually, we need to gather all the edges within `(::a)::b`, ie:
260 259 # excluding paths that leads to roots that are not ancestors of `a`. We
261 260 # keep this out of the explanation because it is hard enough without this special case..
262 261
263 262 parents = cl._uncheckedparentrevs
264 263 graph_roots = (nullrev, nullrev)
265 264
266 265 ancestors = cl.ancestors([a.rev()], inclusive=True)
267 266 revs = cl.findmissingrevs(common=[a.rev()], heads=[b.rev()])
268 267 roots = set()
269 268 has_graph_roots = False
270 269
271 270 # iterate over `only(B, A)`
272 271 for r in revs:
273 272 ps = parents(r)
274 273 if ps == graph_roots:
275 274 has_graph_roots = True
276 275 else:
277 276 p1, p2 = ps
278 277
279 278 # find all the "root points" (see larger comment above)
280 279 if p1 != nullrev and p1 in ancestors:
281 280 roots.add(p1)
282 281 if p2 != nullrev and p2 in ancestors:
283 282 roots.add(p2)
284 283 if not roots:
285 284 # no common revision to track copies from
286 285 return {}
287 286 if has_graph_roots:
288 287 # this deal with the special case mentionned in the [1] footnotes. We
289 288 # must filter out revisions that leads to non-common graphroots.
290 289 roots = list(roots)
291 290 m = min(roots)
292 291 h = [b.rev()]
293 292 roots_to_head = cl.reachableroots(m, h, roots, includepath=True)
294 293 roots_to_head = set(roots_to_head)
295 294 revs = [r for r in revs if r in roots_to_head]
296 295
297 296 if repo.filecopiesmode == b'changeset-sidedata':
298 297 # When using side-data, we will process the edges "from" the children.
299 298 # We iterate over the childre, gathering previous collected data for
300 299 # the parents. Do know when the parents data is no longer necessary, we
301 300 # keep a counter of how many children each revision has.
302 301 #
303 302 # An interresting property of `children_count` is that it only contains
304 303 # revision that will be relevant for a edge of the graph. So if a
305 304 # children has parent not in `children_count`, that edges should not be
306 305 # processed.
307 306 children_count = dict((r, 0) for r in roots)
308 307 for r in revs:
309 308 for p in cl.parentrevs(r):
310 309 if p == nullrev:
311 310 continue
312 311 children_count[r] = 0
313 312 if p in children_count:
314 313 children_count[p] += 1
315 314 revinfo = _revinfo_getter(repo, match)
316 315 return _combine_changeset_copies(
317 316 revs, children_count, b.rev(), revinfo, match, isancestor
318 317 )
319 318 else:
320 319 # When not using side-data, we will process the edges "from" the parent.
321 320 # so we need a full mapping of the parent -> children relation.
322 321 children = dict((r, []) for r in roots)
323 322 for r in revs:
324 323 for p in cl.parentrevs(r):
325 324 if p == nullrev:
326 325 continue
327 326 children[r] = []
328 327 if p in children:
329 328 children[p].append(r)
330 329 x = revs.pop()
331 330 assert x == b.rev()
332 331 revs.extend(roots)
333 332 revs.sort()
334 333
335 334 revinfo = _revinfo_getter_extra(repo)
336 335 return _combine_changeset_copies_extra(
337 336 revs, children, b.rev(), revinfo, match, isancestor
338 337 )
339 338
340 339
341 340 def _combine_changeset_copies(
342 341 revs, children_count, targetrev, revinfo, match, isancestor
343 342 ):
344 343 """combine the copies information for each item of iterrevs
345 344
346 345 revs: sorted iterable of revision to visit
347 346 children_count: a {parent: <number-of-relevant-children>} mapping.
348 347 targetrev: the final copies destination revision (not in iterrevs)
349 348 revinfo(rev): a function that return (p1, p2, p1copies, p2copies, removed)
350 349 match: a matcher
351 350
352 351 It returns the aggregated copies information for `targetrev`.
353 352 """
354 353
355 354 alwaysmatch = match.always()
356 355
357 356 if rustmod is not None:
358 357 final_copies = rustmod.combine_changeset_copies(
359 358 list(revs), children_count, targetrev, revinfo, isancestor
360 359 )
361 360 else:
362 361 isancestor = cached_is_ancestor(isancestor)
363 362
364 363 all_copies = {}
365 364 # iterate over all the "children" side of copy tracing "edge"
366 365 for current_rev in revs:
367 366 p1, p2, changes = revinfo(current_rev)
368 367 current_copies = None
369 368 # iterate over all parents to chain the existing data with the
370 369 # data from the parent β†’ child edge.
371 370 for parent, parent_rev in ((1, p1), (2, p2)):
372 371 if parent_rev == nullrev:
373 372 continue
374 373 remaining_children = children_count.get(parent_rev)
375 374 if remaining_children is None:
376 375 continue
377 376 remaining_children -= 1
378 377 children_count[parent_rev] = remaining_children
379 378 if remaining_children:
380 379 copies = all_copies.get(parent_rev, None)
381 380 else:
382 381 copies = all_copies.pop(parent_rev, None)
383 382
384 383 if copies is None:
385 384 # this is a root
386 385 newcopies = copies = {}
387 386 elif remaining_children:
388 387 newcopies = copies.copy()
389 388 else:
390 389 newcopies = copies
391 390 # chain the data in the edge with the existing data
392 391 if changes is not None:
393 392 childcopies = {}
394 393 if parent == 1:
395 394 childcopies = changes.copied_from_p1
396 395 elif parent == 2:
397 396 childcopies = changes.copied_from_p2
398 397
399 398 if childcopies:
400 399 newcopies = copies.copy()
401 400 for dest, source in pycompat.iteritems(childcopies):
402 401 prev = copies.get(source)
403 402 if prev is not None and prev[1] is not None:
404 403 source = prev[1]
405 404 newcopies[dest] = (current_rev, source)
406 405 assert newcopies is not copies
407 406 if changes.removed:
408 407 for f in changes.removed:
409 408 if f in newcopies:
410 409 if newcopies is copies:
411 410 # copy on write to avoid affecting potential other
412 411 # branches. when there are no other branches, this
413 412 # could be avoided.
414 413 newcopies = copies.copy()
415 414 newcopies[f] = (current_rev, None)
416 415 # check potential need to combine the data from another parent (for
417 416 # that child). See comment below for details.
418 417 if current_copies is None:
419 418 current_copies = newcopies
420 419 else:
421 420 # we are the second parent to work on c, we need to merge our
422 421 # work with the other.
423 422 #
424 423 # In case of conflict, parent 1 take precedence over parent 2.
425 424 # This is an arbitrary choice made anew when implementing
426 425 # changeset based copies. It was made without regards with
427 426 # potential filelog related behavior.
428 427 assert parent == 2
429 428 current_copies = _merge_copies_dict(
430 429 newcopies, current_copies, isancestor, changes
431 430 )
432 431 all_copies[current_rev] = current_copies
433 432
434 433 # filter out internal details and return a {dest: source mapping}
435 434 final_copies = {}
436 435 for dest, (tt, source) in all_copies[targetrev].items():
437 436 if source is not None:
438 437 final_copies[dest] = source
439 438 if not alwaysmatch:
440 439 for filename in list(final_copies.keys()):
441 440 if not match(filename):
442 441 del final_copies[filename]
443 442 return final_copies
444 443
445 444
446 445 # constant to decide which side to pick with _merge_copies_dict
447 446 PICK_MINOR = 0
448 447 PICK_MAJOR = 1
449 448 PICK_EITHER = 2
450 449
451 450
452 451 def _merge_copies_dict(minor, major, isancestor, changes):
453 452 """merge two copies-mapping together, minor and major
454 453
455 454 In case of conflict, value from "major" will be picked.
456 455
457 456 - `isancestors(low_rev, high_rev)`: callable return True if `low_rev` is an
458 457 ancestors of `high_rev`,
459 458
460 459 - `ismerged(path)`: callable return True if `path` have been merged in the
461 460 current revision,
462 461
463 462 return the resulting dict (in practice, the "minor" object, updated)
464 463 """
465 464 for dest, value in major.items():
466 465 other = minor.get(dest)
467 466 if other is None:
468 467 minor[dest] = value
469 468 else:
470 469 pick = _compare_values(changes, isancestor, dest, other, value)
471 470 if pick == PICK_MAJOR:
472 471 minor[dest] = value
473 472 return minor
474 473
475 474
476 475 def _compare_values(changes, isancestor, dest, minor, major):
477 476 """compare two value within a _merge_copies_dict loop iteration"""
478 477 major_tt, major_value = major
479 478 minor_tt, minor_value = minor
480 479
481 480 # evacuate some simple case first:
482 481 if major_tt == minor_tt:
483 482 # if it comes from the same revision it must be the same value
484 483 assert major_value == minor_value
485 484 return PICK_EITHER
486 485 elif major[1] == minor[1]:
487 486 return PICK_EITHER
488 487
489 488 # actual merging needed: content from "major" wins, unless it is older than
490 489 # the branch point or there is a merge
491 490 elif changes is not None and major[1] is None and dest in changes.salvaged:
492 491 return PICK_MINOR
493 492 elif changes is not None and minor[1] is None and dest in changes.salvaged:
494 493 return PICK_MAJOR
495 494 elif changes is not None and dest in changes.merged:
496 495 return PICK_MAJOR
497 496 elif not isancestor(major_tt, minor_tt):
498 497 if major[1] is not None:
499 498 return PICK_MAJOR
500 499 elif isancestor(minor_tt, major_tt):
501 500 return PICK_MAJOR
502 501 return PICK_MINOR
503 502
504 503
505 504 def _revinfo_getter_extra(repo):
506 505 """return a function that return multiple data given a <rev>"i
507 506
508 507 * p1: revision number of first parent
509 508 * p2: revision number of first parent
510 509 * p1copies: mapping of copies from p1
511 510 * p2copies: mapping of copies from p2
512 511 * removed: a list of removed files
513 512 * ismerged: a callback to know if file was merged in that revision
514 513 """
515 514 cl = repo.changelog
516 515 parents = cl.parentrevs
517 516
518 517 def get_ismerged(rev):
519 518 ctx = repo[rev]
520 519
521 520 def ismerged(path):
522 521 if path not in ctx.files():
523 522 return False
524 523 fctx = ctx[path]
525 524 parents = fctx._filelog.parents(fctx._filenode)
526 525 nb_parents = 0
527 526 for n in parents:
528 527 if n != nullid:
529 528 nb_parents += 1
530 529 return nb_parents >= 2
531 530
532 531 return ismerged
533 532
534 533 def revinfo(rev):
535 534 p1, p2 = parents(rev)
536 535 ctx = repo[rev]
537 536 p1copies, p2copies = ctx._copies
538 537 removed = ctx.filesremoved()
539 538 return p1, p2, p1copies, p2copies, removed, get_ismerged(rev)
540 539
541 540 return revinfo
542 541
543 542
544 543 def _combine_changeset_copies_extra(
545 544 revs, children, targetrev, revinfo, match, isancestor
546 545 ):
547 546 """version of `_combine_changeset_copies` that works with the Google
548 547 specific "extra" based storage for copy information"""
549 548 all_copies = {}
550 549 alwaysmatch = match.always()
551 550 for r in revs:
552 551 copies = all_copies.pop(r, None)
553 552 if copies is None:
554 553 # this is a root
555 554 copies = {}
556 555 for i, c in enumerate(children[r]):
557 556 p1, p2, p1copies, p2copies, removed, ismerged = revinfo(c)
558 557 if r == p1:
559 558 parent = 1
560 559 childcopies = p1copies
561 560 else:
562 561 assert r == p2
563 562 parent = 2
564 563 childcopies = p2copies
565 564 if not alwaysmatch:
566 565 childcopies = {
567 566 dst: src for dst, src in childcopies.items() if match(dst)
568 567 }
569 568 newcopies = copies
570 569 if childcopies:
571 570 newcopies = copies.copy()
572 571 for dest, source in pycompat.iteritems(childcopies):
573 572 prev = copies.get(source)
574 573 if prev is not None and prev[1] is not None:
575 574 source = prev[1]
576 575 newcopies[dest] = (c, source)
577 576 assert newcopies is not copies
578 577 for f in removed:
579 578 if f in newcopies:
580 579 if newcopies is copies:
581 580 # copy on write to avoid affecting potential other
582 581 # branches. when there are no other branches, this
583 582 # could be avoided.
584 583 newcopies = copies.copy()
585 584 newcopies[f] = (c, None)
586 585 othercopies = all_copies.get(c)
587 586 if othercopies is None:
588 587 all_copies[c] = newcopies
589 588 else:
590 589 # we are the second parent to work on c, we need to merge our
591 590 # work with the other.
592 591 #
593 592 # In case of conflict, parent 1 take precedence over parent 2.
594 593 # This is an arbitrary choice made anew when implementing
595 594 # changeset based copies. It was made without regards with
596 595 # potential filelog related behavior.
597 596 if parent == 1:
598 597 _merge_copies_dict_extra(
599 598 othercopies, newcopies, isancestor, ismerged
600 599 )
601 600 else:
602 601 _merge_copies_dict_extra(
603 602 newcopies, othercopies, isancestor, ismerged
604 603 )
605 604 all_copies[c] = newcopies
606 605
607 606 final_copies = {}
608 607 for dest, (tt, source) in all_copies[targetrev].items():
609 608 if source is not None:
610 609 final_copies[dest] = source
611 610 return final_copies
612 611
613 612
614 613 def _merge_copies_dict_extra(minor, major, isancestor, ismerged):
615 614 """version of `_merge_copies_dict` that works with the Google
616 615 specific "extra" based storage for copy information"""
617 616 for dest, value in major.items():
618 617 other = minor.get(dest)
619 618 if other is None:
620 619 minor[dest] = value
621 620 else:
622 621 new_tt = value[0]
623 622 other_tt = other[0]
624 623 if value[1] == other[1]:
625 624 continue
626 625 # content from "major" wins, unless it is older
627 626 # than the branch point or there is a merge
628 627 if (
629 628 new_tt == other_tt
630 629 or not isancestor(new_tt, other_tt)
631 630 or ismerged(dest)
632 631 ):
633 632 minor[dest] = value
634 633
635 634
636 635 def _forwardcopies(a, b, base=None, match=None):
637 636 """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
638 637
639 638 if base is None:
640 639 base = a
641 640 match = a.repo().narrowmatch(match)
642 641 # check for working copy
643 642 if b.rev() is None:
644 643 cm = _committedforwardcopies(a, b.p1(), base, match)
645 644 # combine copies from dirstate if necessary
646 645 copies = _chain(cm, _dirstatecopies(b._repo, match))
647 646 else:
648 647 copies = _committedforwardcopies(a, b, base, match)
649 648 return copies
650 649
651 650
652 651 def _backwardrenames(a, b, match):
653 652 if a._repo.ui.config(b'experimental', b'copytrace') == b'off':
654 653 return {}
655 654
656 655 # Even though we're not taking copies into account, 1:n rename situations
657 656 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
658 657 # arbitrarily pick one of the renames.
659 658 # We don't want to pass in "match" here, since that would filter
660 659 # the destination by it. Since we're reversing the copies, we want
661 660 # to filter the source instead.
662 661 f = _forwardcopies(b, a)
663 662 r = {}
664 663 for k, v in sorted(pycompat.iteritems(f)):
665 664 if match and not match(v):
666 665 continue
667 666 # remove copies
668 667 if v in a:
669 668 continue
670 669 r[v] = k
671 670 return r
672 671
673 672
674 673 def pathcopies(x, y, match=None):
675 674 """find {dst@y: src@x} copy mapping for directed compare"""
676 675 repo = x._repo
677 676 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
678 677 if debug:
679 678 repo.ui.debug(
680 679 b'debug.copies: searching copies from %s to %s\n' % (x, y)
681 680 )
682 681 if x == y or not x or not y:
683 682 return {}
684 683 if y.rev() is None and x == y.p1():
685 684 if debug:
686 685 repo.ui.debug(b'debug.copies: search mode: dirstate\n')
687 686 # short-circuit to avoid issues with merge states
688 687 return _dirstatecopies(repo, match)
689 688 a = y.ancestor(x)
690 689 if a == x:
691 690 if debug:
692 691 repo.ui.debug(b'debug.copies: search mode: forward\n')
693 692 copies = _forwardcopies(x, y, match=match)
694 693 elif a == y:
695 694 if debug:
696 695 repo.ui.debug(b'debug.copies: search mode: backward\n')
697 696 copies = _backwardrenames(x, y, match=match)
698 697 else:
699 698 if debug:
700 699 repo.ui.debug(b'debug.copies: search mode: combined\n')
701 700 base = None
702 701 if a.rev() != nullrev:
703 702 base = x
704 703 copies = _chain(
705 704 _backwardrenames(x, a, match=match),
706 705 _forwardcopies(a, y, base, match=match),
707 706 )
708 707 _filter(x, y, copies)
709 708 return copies
710 709
711 710
712 711 def mergecopies(repo, c1, c2, base):
713 712 """
714 713 Finds moves and copies between context c1 and c2 that are relevant for
715 714 merging. 'base' will be used as the merge base.
716 715
717 716 Copytracing is used in commands like rebase, merge, unshelve, etc to merge
718 717 files that were moved/ copied in one merge parent and modified in another.
719 718 For example:
720 719
721 720 o ---> 4 another commit
722 721 |
723 722 | o ---> 3 commit that modifies a.txt
724 723 | /
725 724 o / ---> 2 commit that moves a.txt to b.txt
726 725 |/
727 726 o ---> 1 merge base
728 727
729 728 If we try to rebase revision 3 on revision 4, since there is no a.txt in
730 729 revision 4, and if user have copytrace disabled, we prints the following
731 730 message:
732 731
733 732 ```other changed <file> which local deleted```
734 733
735 734 Returns a tuple where:
736 735
737 736 "branch_copies" an instance of branch_copies.
738 737
739 738 "diverge" is a mapping of source name -> list of destination names
740 739 for divergent renames.
741 740
742 741 This function calls different copytracing algorithms based on config.
743 742 """
744 743 # avoid silly behavior for update from empty dir
745 744 if not c1 or not c2 or c1 == c2:
746 745 return branch_copies(), branch_copies(), {}
747 746
748 747 narrowmatch = c1.repo().narrowmatch()
749 748
750 749 # avoid silly behavior for parent -> working dir
751 750 if c2.node() is None and c1.node() == repo.dirstate.p1():
752 751 return (
753 752 branch_copies(_dirstatecopies(repo, narrowmatch)),
754 753 branch_copies(),
755 754 {},
756 755 )
757 756
758 757 copytracing = repo.ui.config(b'experimental', b'copytrace')
759 758 if stringutil.parsebool(copytracing) is False:
760 759 # stringutil.parsebool() returns None when it is unable to parse the
761 760 # value, so we should rely on making sure copytracing is on such cases
762 761 return branch_copies(), branch_copies(), {}
763 762
764 763 if usechangesetcentricalgo(repo):
765 764 # The heuristics don't make sense when we need changeset-centric algos
766 765 return _fullcopytracing(repo, c1, c2, base)
767 766
768 767 # Copy trace disabling is explicitly below the node == p1 logic above
769 768 # because the logic above is required for a simple copy to be kept across a
770 769 # rebase.
771 770 if copytracing == b'heuristics':
772 771 # Do full copytracing if only non-public revisions are involved as
773 772 # that will be fast enough and will also cover the copies which could
774 773 # be missed by heuristics
775 774 if _isfullcopytraceable(repo, c1, base):
776 775 return _fullcopytracing(repo, c1, c2, base)
777 776 return _heuristicscopytracing(repo, c1, c2, base)
778 777 else:
779 778 return _fullcopytracing(repo, c1, c2, base)
780 779
781 780
782 781 def _isfullcopytraceable(repo, c1, base):
783 782 """Checks that if base, source and destination are all no-public branches,
784 783 if yes let's use the full copytrace algorithm for increased capabilities
785 784 since it will be fast enough.
786 785
787 786 `experimental.copytrace.sourcecommitlimit` can be used to set a limit for
788 787 number of changesets from c1 to base such that if number of changesets are
789 788 more than the limit, full copytracing algorithm won't be used.
790 789 """
791 790 if c1.rev() is None:
792 791 c1 = c1.p1()
793 792 if c1.mutable() and base.mutable():
794 793 sourcecommitlimit = repo.ui.configint(
795 794 b'experimental', b'copytrace.sourcecommitlimit'
796 795 )
797 796 commits = len(repo.revs(b'%d::%d', base.rev(), c1.rev()))
798 797 return commits < sourcecommitlimit
799 798 return False
800 799
801 800
802 801 def _checksinglesidecopies(
803 802 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete
804 803 ):
805 804 if src not in m2:
806 805 # deleted on side 2
807 806 if src not in m1:
808 807 # renamed on side 1, deleted on side 2
809 808 renamedelete[src] = dsts1
810 809 elif src not in mb:
811 810 # Work around the "short-circuit to avoid issues with merge states"
812 811 # thing in pathcopies(): pathcopies(x, y) can return a copy where the
813 812 # destination doesn't exist in y.
814 813 pass
815 814 elif mb[src] != m2[src] and not _related(c2[src], base[src]):
816 815 return
817 816 elif mb[src] != m2[src] or mb.flags(src) != m2.flags(src):
818 817 # modified on side 2
819 818 for dst in dsts1:
820 819 copy[dst] = src
821 820
822 821
823 822 class branch_copies(object):
824 823 """Information about copies made on one side of a merge/graft.
825 824
826 825 "copy" is a mapping from destination name -> source name,
827 826 where source is in c1 and destination is in c2 or vice-versa.
828 827
829 828 "movewithdir" is a mapping from source name -> destination name,
830 829 where the file at source present in one context but not the other
831 830 needs to be moved to destination by the merge process, because the
832 831 other context moved the directory it is in.
833 832
834 833 "renamedelete" is a mapping of source name -> list of destination
835 834 names for files deleted in c1 that were renamed in c2 or vice-versa.
836 835
837 836 "dirmove" is a mapping of detected source dir -> destination dir renames.
838 837 This is needed for handling changes to new files previously grafted into
839 838 renamed directories.
840 839 """
841 840
842 841 def __init__(
843 842 self, copy=None, renamedelete=None, dirmove=None, movewithdir=None
844 843 ):
845 844 self.copy = {} if copy is None else copy
846 845 self.renamedelete = {} if renamedelete is None else renamedelete
847 846 self.dirmove = {} if dirmove is None else dirmove
848 847 self.movewithdir = {} if movewithdir is None else movewithdir
849 848
850 849 def __repr__(self):
851 850 return '<branch_copies\n copy=%r\n renamedelete=%r\n dirmove=%r\n movewithdir=%r\n>' % (
852 851 self.copy,
853 852 self.renamedelete,
854 853 self.dirmove,
855 854 self.movewithdir,
856 855 )
857 856
858 857
859 858 def _fullcopytracing(repo, c1, c2, base):
860 859 """The full copytracing algorithm which finds all the new files that were
861 860 added from merge base up to the top commit and for each file it checks if
862 861 this file was copied from another file.
863 862
864 863 This is pretty slow when a lot of changesets are involved but will track all
865 864 the copies.
866 865 """
867 866 m1 = c1.manifest()
868 867 m2 = c2.manifest()
869 868 mb = base.manifest()
870 869
871 870 copies1 = pathcopies(base, c1)
872 871 copies2 = pathcopies(base, c2)
873 872
874 873 if not (copies1 or copies2):
875 874 return branch_copies(), branch_copies(), {}
876 875
877 876 inversecopies1 = {}
878 877 inversecopies2 = {}
879 878 for dst, src in copies1.items():
880 879 inversecopies1.setdefault(src, []).append(dst)
881 880 for dst, src in copies2.items():
882 881 inversecopies2.setdefault(src, []).append(dst)
883 882
884 883 copy1 = {}
885 884 copy2 = {}
886 885 diverge = {}
887 886 renamedelete1 = {}
888 887 renamedelete2 = {}
889 888 allsources = set(inversecopies1) | set(inversecopies2)
890 889 for src in allsources:
891 890 dsts1 = inversecopies1.get(src)
892 891 dsts2 = inversecopies2.get(src)
893 892 if dsts1 and dsts2:
894 893 # copied/renamed on both sides
895 894 if src not in m1 and src not in m2:
896 895 # renamed on both sides
897 896 dsts1 = set(dsts1)
898 897 dsts2 = set(dsts2)
899 898 # If there's some overlap in the rename destinations, we
900 899 # consider it not divergent. For example, if side 1 copies 'a'
901 900 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c'
902 901 # and 'd' and deletes 'a'.
903 902 if dsts1 & dsts2:
904 903 for dst in dsts1 & dsts2:
905 904 copy1[dst] = src
906 905 copy2[dst] = src
907 906 else:
908 907 diverge[src] = sorted(dsts1 | dsts2)
909 908 elif src in m1 and src in m2:
910 909 # copied on both sides
911 910 dsts1 = set(dsts1)
912 911 dsts2 = set(dsts2)
913 912 for dst in dsts1 & dsts2:
914 913 copy1[dst] = src
915 914 copy2[dst] = src
916 915 # TODO: Handle cases where it was renamed on one side and copied
917 916 # on the other side
918 917 elif dsts1:
919 918 # copied/renamed only on side 1
920 919 _checksinglesidecopies(
921 920 src, dsts1, m1, m2, mb, c2, base, copy1, renamedelete1
922 921 )
923 922 elif dsts2:
924 923 # copied/renamed only on side 2
925 924 _checksinglesidecopies(
926 925 src, dsts2, m2, m1, mb, c1, base, copy2, renamedelete2
927 926 )
928 927
929 928 # find interesting file sets from manifests
930 929 cache = []
931 930
932 931 def _get_addedfiles(idx):
933 932 if not cache:
934 933 addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
935 934 addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
936 935 u1 = sorted(addedinm1 - addedinm2)
937 936 u2 = sorted(addedinm2 - addedinm1)
938 937 cache.extend((u1, u2))
939 938 return cache[idx]
940 939
941 940 u1fn = lambda: _get_addedfiles(0)
942 941 u2fn = lambda: _get_addedfiles(1)
943 942 if repo.ui.debugflag:
944 943 u1 = u1fn()
945 944 u2 = u2fn()
946 945
947 946 header = b" unmatched files in %s"
948 947 if u1:
949 948 repo.ui.debug(
950 949 b"%s:\n %s\n" % (header % b'local', b"\n ".join(u1))
951 950 )
952 951 if u2:
953 952 repo.ui.debug(
954 953 b"%s:\n %s\n" % (header % b'other', b"\n ".join(u2))
955 954 )
956 955
957 956 renamedeleteset = set()
958 957 divergeset = set()
959 958 for dsts in diverge.values():
960 959 divergeset.update(dsts)
961 960 for dsts in renamedelete1.values():
962 961 renamedeleteset.update(dsts)
963 962 for dsts in renamedelete2.values():
964 963 renamedeleteset.update(dsts)
965 964
966 965 repo.ui.debug(
967 966 b" all copies found (* = to merge, ! = divergent, "
968 967 b"% = renamed and deleted):\n"
969 968 )
970 969 for side, copies in ((b"local", copies1), (b"remote", copies2)):
971 970 if not copies:
972 971 continue
973 972 repo.ui.debug(b" on %s side:\n" % side)
974 973 for f in sorted(copies):
975 974 note = b""
976 975 if f in copy1 or f in copy2:
977 976 note += b"*"
978 977 if f in divergeset:
979 978 note += b"!"
980 979 if f in renamedeleteset:
981 980 note += b"%"
982 981 repo.ui.debug(
983 982 b" src: '%s' -> dst: '%s' %s\n" % (copies[f], f, note)
984 983 )
985 984 del renamedeleteset
986 985 del divergeset
987 986
988 987 repo.ui.debug(b" checking for directory renames\n")
989 988
990 989 dirmove1, movewithdir2 = _dir_renames(repo, c1, copy1, copies1, u2fn)
991 990 dirmove2, movewithdir1 = _dir_renames(repo, c2, copy2, copies2, u1fn)
992 991
993 992 branch_copies1 = branch_copies(copy1, renamedelete1, dirmove1, movewithdir1)
994 993 branch_copies2 = branch_copies(copy2, renamedelete2, dirmove2, movewithdir2)
995 994
996 995 return branch_copies1, branch_copies2, diverge
997 996
998 997
999 998 def _dir_renames(repo, ctx, copy, fullcopy, addedfilesfn):
1000 999 """Finds moved directories and files that should move with them.
1001 1000
1002 1001 ctx: the context for one of the sides
1003 1002 copy: files copied on the same side (as ctx)
1004 1003 fullcopy: files copied on the same side (as ctx), including those that
1005 1004 merge.manifestmerge() won't care about
1006 1005 addedfilesfn: function returning added files on the other side (compared to
1007 1006 ctx)
1008 1007 """
1009 1008 # generate a directory move map
1010 1009 invalid = set()
1011 1010 dirmove = {}
1012 1011
1013 1012 # examine each file copy for a potential directory move, which is
1014 1013 # when all the files in a directory are moved to a new directory
1015 1014 for dst, src in pycompat.iteritems(fullcopy):
1016 1015 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
1017 1016 if dsrc in invalid:
1018 1017 # already seen to be uninteresting
1019 1018 continue
1020 1019 elif ctx.hasdir(dsrc) and ctx.hasdir(ddst):
1021 1020 # directory wasn't entirely moved locally
1022 1021 invalid.add(dsrc)
1023 1022 elif dsrc in dirmove and dirmove[dsrc] != ddst:
1024 1023 # files from the same directory moved to two different places
1025 1024 invalid.add(dsrc)
1026 1025 else:
1027 1026 # looks good so far
1028 1027 dirmove[dsrc] = ddst
1029 1028
1030 1029 for i in invalid:
1031 1030 if i in dirmove:
1032 1031 del dirmove[i]
1033 1032 del invalid
1034 1033
1035 1034 if not dirmove:
1036 1035 return {}, {}
1037 1036
1038 1037 dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)}
1039 1038
1040 1039 for d in dirmove:
1041 1040 repo.ui.debug(
1042 1041 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d])
1043 1042 )
1044 1043
1045 1044 movewithdir = {}
1046 1045 # check unaccounted nonoverlapping files against directory moves
1047 1046 for f in addedfilesfn():
1048 1047 if f not in fullcopy:
1049 1048 for d in dirmove:
1050 1049 if f.startswith(d):
1051 1050 # new file added in a directory that was moved, move it
1052 1051 df = dirmove[d] + f[len(d) :]
1053 1052 if df not in copy:
1054 1053 movewithdir[f] = df
1055 1054 repo.ui.debug(
1056 1055 b" pending file src: '%s' -> dst: '%s'\n"
1057 1056 % (f, df)
1058 1057 )
1059 1058 break
1060 1059
1061 1060 return dirmove, movewithdir
1062 1061
1063 1062
1064 1063 def _heuristicscopytracing(repo, c1, c2, base):
1065 1064 """Fast copytracing using filename heuristics
1066 1065
1067 1066 Assumes that moves or renames are of following two types:
1068 1067
1069 1068 1) Inside a directory only (same directory name but different filenames)
1070 1069 2) Move from one directory to another
1071 1070 (same filenames but different directory names)
1072 1071
1073 1072 Works only when there are no merge commits in the "source branch".
1074 1073 Source branch is commits from base up to c2 not including base.
1075 1074
1076 1075 If merge is involved it fallbacks to _fullcopytracing().
1077 1076
1078 1077 Can be used by setting the following config:
1079 1078
1080 1079 [experimental]
1081 1080 copytrace = heuristics
1082 1081
1083 1082 In some cases the copy/move candidates found by heuristics can be very large
1084 1083 in number and that will make the algorithm slow. The number of possible
1085 1084 candidates to check can be limited by using the config
1086 1085 `experimental.copytrace.movecandidateslimit` which defaults to 100.
1087 1086 """
1088 1087
1089 1088 if c1.rev() is None:
1090 1089 c1 = c1.p1()
1091 1090 if c2.rev() is None:
1092 1091 c2 = c2.p1()
1093 1092
1094 1093 changedfiles = set()
1095 1094 m1 = c1.manifest()
1096 1095 if not repo.revs(b'%d::%d', base.rev(), c2.rev()):
1097 1096 # If base is not in c2 branch, we switch to fullcopytracing
1098 1097 repo.ui.debug(
1099 1098 b"switching to full copytracing as base is not "
1100 1099 b"an ancestor of c2\n"
1101 1100 )
1102 1101 return _fullcopytracing(repo, c1, c2, base)
1103 1102
1104 1103 ctx = c2
1105 1104 while ctx != base:
1106 1105 if len(ctx.parents()) == 2:
1107 1106 # To keep things simple let's not handle merges
1108 1107 repo.ui.debug(b"switching to full copytracing because of merges\n")
1109 1108 return _fullcopytracing(repo, c1, c2, base)
1110 1109 changedfiles.update(ctx.files())
1111 1110 ctx = ctx.p1()
1112 1111
1113 1112 copies2 = {}
1114 1113 cp = _forwardcopies(base, c2)
1115 1114 for dst, src in pycompat.iteritems(cp):
1116 1115 if src in m1:
1117 1116 copies2[dst] = src
1118 1117
1119 1118 # file is missing if it isn't present in the destination, but is present in
1120 1119 # the base and present in the source.
1121 1120 # Presence in the base is important to exclude added files, presence in the
1122 1121 # source is important to exclude removed files.
1123 1122 filt = lambda f: f not in m1 and f in base and f in c2
1124 1123 missingfiles = [f for f in changedfiles if filt(f)]
1125 1124
1126 1125 copies1 = {}
1127 1126 if missingfiles:
1128 1127 basenametofilename = collections.defaultdict(list)
1129 1128 dirnametofilename = collections.defaultdict(list)
1130 1129
1131 1130 for f in m1.filesnotin(base.manifest()):
1132 1131 basename = os.path.basename(f)
1133 1132 dirname = os.path.dirname(f)
1134 1133 basenametofilename[basename].append(f)
1135 1134 dirnametofilename[dirname].append(f)
1136 1135
1137 1136 for f in missingfiles:
1138 1137 basename = os.path.basename(f)
1139 1138 dirname = os.path.dirname(f)
1140 1139 samebasename = basenametofilename[basename]
1141 1140 samedirname = dirnametofilename[dirname]
1142 1141 movecandidates = samebasename + samedirname
1143 1142 # f is guaranteed to be present in c2, that's why
1144 1143 # c2.filectx(f) won't fail
1145 1144 f2 = c2.filectx(f)
1146 1145 # we can have a lot of candidates which can slow down the heuristics
1147 1146 # config value to limit the number of candidates moves to check
1148 1147 maxcandidates = repo.ui.configint(
1149 1148 b'experimental', b'copytrace.movecandidateslimit'
1150 1149 )
1151 1150
1152 1151 if len(movecandidates) > maxcandidates:
1153 1152 repo.ui.status(
1154 1153 _(
1155 1154 b"skipping copytracing for '%s', more "
1156 1155 b"candidates than the limit: %d\n"
1157 1156 )
1158 1157 % (f, len(movecandidates))
1159 1158 )
1160 1159 continue
1161 1160
1162 1161 for candidate in movecandidates:
1163 1162 f1 = c1.filectx(candidate)
1164 1163 if _related(f1, f2):
1165 1164 # if there are a few related copies then we'll merge
1166 1165 # changes into all of them. This matches the behaviour
1167 1166 # of upstream copytracing
1168 1167 copies1[candidate] = f
1169 1168
1170 1169 return branch_copies(copies1), branch_copies(copies2), {}
1171 1170
1172 1171
1173 1172 def _related(f1, f2):
1174 1173 """return True if f1 and f2 filectx have a common ancestor
1175 1174
1176 1175 Walk back to common ancestor to see if the two files originate
1177 1176 from the same file. Since workingfilectx's rev() is None it messes
1178 1177 up the integer comparison logic, hence the pre-step check for
1179 1178 None (f1 and f2 can only be workingfilectx's initially).
1180 1179 """
1181 1180
1182 1181 if f1 == f2:
1183 1182 return True # a match
1184 1183
1185 1184 g1, g2 = f1.ancestors(), f2.ancestors()
1186 1185 try:
1187 1186 f1r, f2r = f1.linkrev(), f2.linkrev()
1188 1187
1189 1188 if f1r is None:
1190 1189 f1 = next(g1)
1191 1190 if f2r is None:
1192 1191 f2 = next(g2)
1193 1192
1194 1193 while True:
1195 1194 f1r, f2r = f1.linkrev(), f2.linkrev()
1196 1195 if f1r > f2r:
1197 1196 f1 = next(g1)
1198 1197 elif f2r > f1r:
1199 1198 f2 = next(g2)
1200 1199 else: # f1 and f2 point to files in the same linkrev
1201 1200 return f1 == f2 # true if they point to the same file
1202 1201 except StopIteration:
1203 1202 return False
1204 1203
1205 1204
1206 1205 def graftcopies(wctx, ctx, base):
1207 1206 """reproduce copies between base and ctx in the wctx
1208 1207
1209 1208 Unlike mergecopies(), this function will only consider copies between base
1210 1209 and ctx; it will ignore copies between base and wctx. Also unlike
1211 1210 mergecopies(), this function will apply copies to the working copy (instead
1212 1211 of just returning information about the copies). That makes it cheaper
1213 1212 (especially in the common case of base==ctx.p1()) and useful also when
1214 1213 experimental.copytrace=off.
1215 1214
1216 1215 merge.update() will have already marked most copies, but it will only
1217 1216 mark copies if it thinks the source files are related (see
1218 1217 merge._related()). It will also not mark copies if the file wasn't modified
1219 1218 on the local side. This function adds the copies that were "missed"
1220 1219 by merge.update().
1221 1220 """
1222 1221 new_copies = pathcopies(base, ctx)
1223 1222 parent = wctx.p1()
1224 1223 _filter(parent, wctx, new_copies)
1225 1224 # extra filtering to drop copy information for files that existed before
1226 1225 # the graft (otherwise we would create merge filelog for non-merge commit
1227 1226 for dest, __ in list(new_copies.items()):
1228 1227 if dest in parent:
1229 1228 del new_copies[dest]
1230 1229 for dst, src in pycompat.iteritems(new_copies):
1231 1230 wctx[dst].markcopied(src)
@@ -1,721 +1,721 b''
1 1 #testcases filelog compatibility changeset sidedata
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [extensions]
5 5 > rebase=
6 6 > [alias]
7 7 > l = log -G -T '{rev} {desc}\n{files}\n'
8 8 > EOF
9 9
10 10 #if compatibility
11 11 $ cat >> $HGRCPATH << EOF
12 12 > [experimental]
13 13 > copies.read-from = compatibility
14 14 > EOF
15 15 #endif
16 16
17 17 #if changeset
18 18 $ cat >> $HGRCPATH << EOF
19 19 > [experimental]
20 20 > copies.read-from = changeset-only
21 21 > copies.write-to = changeset-only
22 22 > EOF
23 23 #endif
24 24
25 25 #if sidedata
26 26 $ cat >> $HGRCPATH << EOF
27 27 > [format]
28 28 > exp-use-copies-side-data-changeset = yes
29 29 > EOF
30 30 #endif
31 31
32 32 $ REPONUM=0
33 33 $ newrepo() {
34 34 > cd $TESTTMP
35 35 > REPONUM=`expr $REPONUM + 1`
36 36 > hg init repo-$REPONUM
37 37 > cd repo-$REPONUM
38 38 > }
39 39
40 40 Simple rename case
41 41 $ newrepo
42 42 $ echo x > x
43 43 $ hg ci -Aqm 'add x'
44 44 $ hg mv x y
45 45 $ hg debugp1copies
46 46 x -> y
47 47 $ hg debugp2copies
48 48 $ hg ci -m 'rename x to y'
49 49 $ hg l
50 50 @ 1 rename x to y
51 51 | x y
52 52 o 0 add x
53 53 x
54 54 $ hg debugp1copies -r 1
55 55 x -> y
56 56 $ hg debugpathcopies 0 1
57 57 x -> y
58 58 $ hg debugpathcopies 1 0
59 59 y -> x
60 60 Test filtering copies by path. We do filtering by destination.
61 61 $ hg debugpathcopies 0 1 x
62 62 $ hg debugpathcopies 1 0 x
63 63 y -> x
64 64 $ hg debugpathcopies 0 1 y
65 65 x -> y
66 66 $ hg debugpathcopies 1 0 y
67 67
68 68 Copies not including commit changes
69 69 $ newrepo
70 70 $ echo x > x
71 71 $ hg ci -Aqm 'add x'
72 72 $ hg mv x y
73 73 $ hg debugpathcopies . .
74 74 $ hg debugpathcopies . 'wdir()'
75 75 x -> y
76 76 $ hg debugpathcopies 'wdir()' .
77 77 y -> x
78 78
79 79 Copy a file onto another file
80 80 $ newrepo
81 81 $ echo x > x
82 82 $ echo y > y
83 83 $ hg ci -Aqm 'add x and y'
84 84 $ hg cp -f x y
85 85 $ hg debugp1copies
86 86 x -> y
87 87 $ hg debugp2copies
88 88 $ hg ci -m 'copy x onto y'
89 89 $ hg l
90 90 @ 1 copy x onto y
91 91 | y
92 92 o 0 add x and y
93 93 x y
94 94 $ hg debugp1copies -r 1
95 95 x -> y
96 Incorrectly doesn't show the rename
97 96 $ hg debugpathcopies 0 1
97 x -> y (no-filelog !)
98 98
99 99 Copy a file onto another file with same content. If metadata is stored in changeset, this does not
100 100 produce a new filelog entry. The changeset's "files" entry should still list the file.
101 101 $ newrepo
102 102 $ echo x > x
103 103 $ echo x > x2
104 104 $ hg ci -Aqm 'add x and x2 with same content'
105 105 $ hg cp -f x x2
106 106 $ hg ci -m 'copy x onto x2'
107 107 $ hg l
108 108 @ 1 copy x onto x2
109 109 | x2
110 110 o 0 add x and x2 with same content
111 111 x x2
112 112 $ hg debugp1copies -r 1
113 113 x -> x2
114 Incorrectly doesn't show the rename
115 114 $ hg debugpathcopies 0 1
115 x -> x2 (no-filelog !)
116 116
117 117 Rename file in a loop: x->y->z->x
118 118 $ newrepo
119 119 $ echo x > x
120 120 $ hg ci -Aqm 'add x'
121 121 $ hg mv x y
122 122 $ hg debugp1copies
123 123 x -> y
124 124 $ hg debugp2copies
125 125 $ hg ci -m 'rename x to y'
126 126 $ hg mv y z
127 127 $ hg ci -m 'rename y to z'
128 128 $ hg mv z x
129 129 $ hg ci -m 'rename z to x'
130 130 $ hg l
131 131 @ 3 rename z to x
132 132 | x z
133 133 o 2 rename y to z
134 134 | y z
135 135 o 1 rename x to y
136 136 | x y
137 137 o 0 add x
138 138 x
139 139 $ hg debugpathcopies 0 3
140 140
141 141 Copy x to z, then remove z, then copy x2 (same content as x) to z. With copy metadata in the
142 142 changeset, the two copies here will have the same filelog entry, so ctx['z'].introrev() might point
143 143 to the first commit that added the file. We should still report the copy as being from x2.
144 144 $ newrepo
145 145 $ echo x > x
146 146 $ echo x > x2
147 147 $ hg ci -Aqm 'add x and x2 with same content'
148 148 $ hg cp x z
149 149 $ hg ci -qm 'copy x to z'
150 150 $ hg rm z
151 151 $ hg ci -m 'remove z'
152 152 $ hg cp x2 z
153 153 $ hg ci -m 'copy x2 to z'
154 154 $ hg l
155 155 @ 3 copy x2 to z
156 156 | z
157 157 o 2 remove z
158 158 | z
159 159 o 1 copy x to z
160 160 | z
161 161 o 0 add x and x2 with same content
162 162 x x2
163 163 $ hg debugp1copies -r 3
164 164 x2 -> z
165 165 $ hg debugpathcopies 0 3
166 166 x2 -> z
167 167
168 168 Create x and y, then rename them both to the same name, but on different sides of a fork
169 169 $ newrepo
170 170 $ echo x > x
171 171 $ echo y > y
172 172 $ hg ci -Aqm 'add x and y'
173 173 $ hg mv x z
174 174 $ hg ci -qm 'rename x to z'
175 175 $ hg co -q 0
176 176 $ hg mv y z
177 177 $ hg ci -qm 'rename y to z'
178 178 $ hg l
179 179 @ 2 rename y to z
180 180 | y z
181 181 | o 1 rename x to z
182 182 |/ x z
183 183 o 0 add x and y
184 184 x y
185 185 $ hg debugpathcopies 1 2
186 186 z -> x
187 187 y -> z
188 188
189 189 Fork renames x to y on one side and removes x on the other
190 190 $ newrepo
191 191 $ echo x > x
192 192 $ hg ci -Aqm 'add x'
193 193 $ hg mv x y
194 194 $ hg ci -m 'rename x to y'
195 195 $ hg co -q 0
196 196 $ hg rm x
197 197 $ hg ci -m 'remove x'
198 198 created new head
199 199 $ hg l
200 200 @ 2 remove x
201 201 | x
202 202 | o 1 rename x to y
203 203 |/ x y
204 204 o 0 add x
205 205 x
206 206 $ hg debugpathcopies 1 2
207 207
208 208 Merge rename from other branch
209 209 $ newrepo
210 210 $ echo x > x
211 211 $ hg ci -Aqm 'add x'
212 212 $ hg mv x y
213 213 $ hg ci -m 'rename x to y'
214 214 $ hg co -q 0
215 215 $ echo z > z
216 216 $ hg ci -Aqm 'add z'
217 217 $ hg merge -q 1
218 218 $ hg debugp1copies
219 219 $ hg debugp2copies
220 220 $ hg ci -m 'merge rename from p2'
221 221 $ hg l
222 222 @ 3 merge rename from p2
223 223 |\
224 224 | o 2 add z
225 225 | | z
226 226 o | 1 rename x to y
227 227 |/ x y
228 228 o 0 add x
229 229 x
230 230 Perhaps we should indicate the rename here, but `hg status` is documented to be weird during
231 231 merges, so...
232 232 $ hg debugp1copies -r 3
233 233 $ hg debugp2copies -r 3
234 234 $ hg debugpathcopies 0 3
235 235 x -> y
236 236 $ hg debugpathcopies 1 2
237 237 y -> x
238 238 $ hg debugpathcopies 1 3
239 239 $ hg debugpathcopies 2 3
240 240 x -> y
241 241
242 242 Copy file from either side in a merge
243 243 $ newrepo
244 244 $ echo x > x
245 245 $ hg ci -Aqm 'add x'
246 246 $ hg co -q null
247 247 $ echo y > y
248 248 $ hg ci -Aqm 'add y'
249 249 $ hg merge -q 0
250 250 $ hg cp y z
251 251 $ hg debugp1copies
252 252 y -> z
253 253 $ hg debugp2copies
254 254 $ hg ci -m 'copy file from p1 in merge'
255 255 $ hg co -q 1
256 256 $ hg merge -q 0
257 257 $ hg cp x z
258 258 $ hg debugp1copies
259 259 $ hg debugp2copies
260 260 x -> z
261 261 $ hg ci -qm 'copy file from p2 in merge'
262 262 $ hg l
263 263 @ 3 copy file from p2 in merge
264 264 |\ z
265 265 +---o 2 copy file from p1 in merge
266 266 | |/ z
267 267 | o 1 add y
268 268 | y
269 269 o 0 add x
270 270 x
271 271 $ hg debugp1copies -r 2
272 272 y -> z
273 273 $ hg debugp2copies -r 2
274 274 $ hg debugpathcopies 1 2
275 275 y -> z
276 276 $ hg debugpathcopies 0 2
277 277 $ hg debugp1copies -r 3
278 278 $ hg debugp2copies -r 3
279 279 x -> z
280 280 $ hg debugpathcopies 1 3
281 281 $ hg debugpathcopies 0 3
282 282 x -> z
283 283
284 284 Copy file that exists on both sides of the merge, same content on both sides
285 285 $ newrepo
286 286 $ echo x > x
287 287 $ hg ci -Aqm 'add x on branch 1'
288 288 $ hg co -q null
289 289 $ echo x > x
290 290 $ hg ci -Aqm 'add x on branch 2'
291 291 $ hg merge -q 0
292 292 $ hg cp x z
293 293 $ hg debugp1copies
294 294 x -> z
295 295 $ hg debugp2copies
296 296 $ hg ci -qm 'merge'
297 297 $ hg l
298 298 @ 2 merge
299 299 |\ z
300 300 | o 1 add x on branch 2
301 301 | x
302 302 o 0 add x on branch 1
303 303 x
304 304 $ hg debugp1copies -r 2
305 305 x -> z
306 306 $ hg debugp2copies -r 2
307 307 It's a little weird that it shows up on both sides
308 308 $ hg debugpathcopies 1 2
309 309 x -> z
310 310 $ hg debugpathcopies 0 2
311 311 x -> z (filelog !)
312 312
313 313 Copy file that exists on both sides of the merge, different content
314 314 $ newrepo
315 315 $ echo branch1 > x
316 316 $ hg ci -Aqm 'add x on branch 1'
317 317 $ hg co -q null
318 318 $ echo branch2 > x
319 319 $ hg ci -Aqm 'add x on branch 2'
320 320 $ hg merge -q 0
321 321 warning: conflicts while merging x! (edit, then use 'hg resolve --mark')
322 322 [1]
323 323 $ echo resolved > x
324 324 $ hg resolve -m x
325 325 (no more unresolved files)
326 326 $ hg cp x z
327 327 $ hg debugp1copies
328 328 x -> z
329 329 $ hg debugp2copies
330 330 $ hg ci -qm 'merge'
331 331 $ hg l
332 332 @ 2 merge
333 333 |\ x z
334 334 | o 1 add x on branch 2
335 335 | x
336 336 o 0 add x on branch 1
337 337 x
338 338 $ hg debugp1copies -r 2
339 339 x -> z (changeset !)
340 340 x -> z (sidedata !)
341 341 $ hg debugp2copies -r 2
342 342 x -> z (no-changeset no-sidedata !)
343 343 $ hg debugpathcopies 1 2
344 344 x -> z (changeset !)
345 345 x -> z (sidedata !)
346 346 $ hg debugpathcopies 0 2
347 347 x -> z (no-changeset no-sidedata !)
348 348
349 349 Copy x->y on one side of merge and copy x->z on the other side. Pathcopies from one parent
350 350 of the merge to the merge should include the copy from the other side.
351 351 $ newrepo
352 352 $ echo x > x
353 353 $ hg ci -Aqm 'add x'
354 354 $ hg cp x y
355 355 $ hg ci -qm 'copy x to y'
356 356 $ hg co -q 0
357 357 $ hg cp x z
358 358 $ hg ci -qm 'copy x to z'
359 359 $ hg merge -q 1
360 360 $ hg ci -m 'merge copy x->y and copy x->z'
361 361 $ hg l
362 362 @ 3 merge copy x->y and copy x->z
363 363 |\
364 364 | o 2 copy x to z
365 365 | | z
366 366 o | 1 copy x to y
367 367 |/ y
368 368 o 0 add x
369 369 x
370 370 $ hg debugp1copies -r 3
371 371 $ hg debugp2copies -r 3
372 372 $ hg debugpathcopies 2 3
373 373 x -> y
374 374 $ hg debugpathcopies 1 3
375 375 x -> z
376 376
377 377 Copy x to y on one side of merge, create y and rename to z on the other side.
378 378 $ newrepo
379 379 $ echo x > x
380 380 $ hg ci -Aqm 'add x'
381 381 $ hg cp x y
382 382 $ hg ci -qm 'copy x to y'
383 383 $ hg co -q 0
384 384 $ echo y > y
385 385 $ hg ci -Aqm 'add y'
386 386 $ hg mv y z
387 387 $ hg ci -m 'rename y to z'
388 388 $ hg merge -q 1
389 389 $ hg ci -m 'merge'
390 390 $ hg l
391 391 @ 4 merge
392 392 |\
393 393 | o 3 rename y to z
394 394 | | y z
395 395 | o 2 add y
396 396 | | y
397 397 o | 1 copy x to y
398 398 |/ y
399 399 o 0 add x
400 400 x
401 401 $ hg debugp1copies -r 3
402 402 y -> z
403 403 $ hg debugp2copies -r 3
404 404 $ hg debugpathcopies 2 3
405 405 y -> z
406 406 $ hg debugpathcopies 1 3
407 407 y -> z (no-filelog !)
408 408
409 409 Create x and y, then rename x to z on one side of merge, and rename y to z and
410 410 modify z on the other side. When storing copies in the changeset, we don't
411 411 filter out copies whose target was created on the other side of the merge.
412 412 $ newrepo
413 413 $ echo x > x
414 414 $ echo y > y
415 415 $ hg ci -Aqm 'add x and y'
416 416 $ hg mv x z
417 417 $ hg ci -qm 'rename x to z'
418 418 $ hg co -q 0
419 419 $ hg mv y z
420 420 $ hg ci -qm 'rename y to z'
421 421 $ echo z >> z
422 422 $ hg ci -m 'modify z'
423 423 $ hg merge -q 1
424 424 warning: conflicts while merging z! (edit, then use 'hg resolve --mark')
425 425 [1]
426 426 $ echo z > z
427 427 $ hg resolve -qm z
428 428 $ hg ci -m 'merge 1 into 3'
429 429 Try merging the other direction too
430 430 $ hg co -q 1
431 431 $ hg merge -q 3
432 432 warning: conflicts while merging z! (edit, then use 'hg resolve --mark')
433 433 [1]
434 434 $ echo z > z
435 435 $ hg resolve -qm z
436 436 $ hg ci -m 'merge 3 into 1'
437 437 created new head
438 438 $ hg l
439 439 @ 5 merge 3 into 1
440 440 |\ z
441 441 +---o 4 merge 1 into 3
442 442 | |/ z
443 443 | o 3 modify z
444 444 | | z
445 445 | o 2 rename y to z
446 446 | | y z
447 447 o | 1 rename x to z
448 448 |/ x z
449 449 o 0 add x and y
450 450 x y
451 451 $ hg debugpathcopies 1 4
452 452 y -> z (no-filelog !)
453 453 $ hg debugpathcopies 2 4
454 454 x -> z (no-filelog !)
455 455 $ hg debugpathcopies 0 4
456 456 x -> z (filelog !)
457 457 y -> z (no-filelog !)
458 458 $ hg debugpathcopies 1 5
459 459 y -> z (no-filelog !)
460 460 $ hg debugpathcopies 2 5
461 461 x -> z (no-filelog !)
462 462 $ hg debugpathcopies 0 5
463 463 x -> z
464 464
465 465 Create x and y, then remove y and rename x to y on one side of merge, and
466 466 modify x on the other side. The modification to x from the second side
467 467 should be propagated to y.
468 468 $ newrepo
469 469 $ echo original > x
470 470 $ hg add x
471 471 $ echo unrelated > y
472 472 $ hg add y
473 473 $ hg commit -m 'add x and y'
474 474 $ hg remove y
475 475 $ hg commit -m 'remove y'
476 476 $ hg rename x y
477 477 $ hg commit -m 'rename x to y'
478 478 $ hg checkout -q 0
479 479 $ echo modified > x
480 480 $ hg commit -m 'modify x'
481 481 created new head
482 482 $ hg l
483 483 @ 3 modify x
484 484 | x
485 485 | o 2 rename x to y
486 486 | | x y
487 487 | o 1 remove y
488 488 |/ y
489 489 o 0 add x and y
490 490 x y
491 491 #if filelog
492 492 $ hg merge 2
493 493 file 'x' was deleted in other [merge rev] but was modified in local [working copy].
494 494 You can use (c)hanged version, (d)elete, or leave (u)nresolved.
495 495 What do you want to do? u
496 496 1 files updated, 0 files merged, 0 files removed, 1 files unresolved
497 497 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
498 498 [1]
499 499 This should ideally be "modified", but we will probably not be able to fix
500 500 that in the filelog case.
501 501 $ cat y
502 502 original
503 503 #else
504 504 $ hg merge 2
505 505 merging x and y to y
506 506 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
507 507 (branch merge, don't forget to commit)
508 508 $ cat y
509 509 modified
510 510 #endif
511 511 Same as above, but in the opposite direction
512 512 #if filelog
513 513 $ hg co -qC 2
514 514 $ hg merge 3
515 515 file 'x' was deleted in local [working copy] but was modified in other [merge rev].
516 516 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
517 517 What do you want to do? u
518 518 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
519 519 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
520 520 [1]
521 521 BROKEN: should be "modified"
522 522 $ cat y
523 523 original
524 524 #else
525 525 $ hg co -qC 2
526 526 $ hg merge 3
527 527 merging y and x to y
528 528 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
529 529 (branch merge, don't forget to commit)
530 530 $ cat y
531 531 modified
532 532 #endif
533 533
534 534 Create x and y, then rename x to z on one side of merge, and rename y to z and
535 535 then delete z on the other side.
536 536 $ newrepo
537 537 $ echo x > x
538 538 $ echo y > y
539 539 $ hg ci -Aqm 'add x and y'
540 540 $ hg mv x z
541 541 $ hg ci -qm 'rename x to z'
542 542 $ hg co -q 0
543 543 $ hg mv y z
544 544 $ hg ci -qm 'rename y to z'
545 545 $ hg rm z
546 546 $ hg ci -m 'delete z'
547 547 $ hg merge -q 1
548 548 $ echo z > z
549 549 $ hg ci -m 'merge 1 into 3'
550 550 Try merging the other direction too
551 551 $ hg co -q 1
552 552 $ hg merge -q 3
553 553 $ echo z > z
554 554 $ hg ci -m 'merge 3 into 1'
555 555 created new head
556 556 $ hg l
557 557 @ 5 merge 3 into 1
558 558 |\ z
559 559 +---o 4 merge 1 into 3
560 560 | |/ z
561 561 | o 3 delete z
562 562 | | z
563 563 | o 2 rename y to z
564 564 | | y z
565 565 o | 1 rename x to z
566 566 |/ x z
567 567 o 0 add x and y
568 568 x y
569 569 $ hg debugpathcopies 1 4
570 570 $ hg debugpathcopies 2 4
571 571 x -> z (no-filelog !)
572 572 $ hg debugpathcopies 0 4
573 573 x -> z (no-changeset no-compatibility !)
574 574 $ hg debugpathcopies 1 5
575 575 $ hg debugpathcopies 2 5
576 576 x -> z (no-filelog !)
577 577 $ hg debugpathcopies 0 5
578 578 x -> z
579 579
580 580
581 581 Test for a case in fullcopytracing algorithm where neither of the merging csets
582 582 is a descendant of the merge base. This test reflects that the algorithm
583 583 correctly finds the copies:
584 584
585 585 $ cat >> $HGRCPATH << EOF
586 586 > [experimental]
587 587 > evolution.createmarkers=True
588 588 > evolution.allowunstable=True
589 589 > EOF
590 590
591 591 $ newrepo
592 592 $ echo a > a
593 593 $ hg add a
594 594 $ hg ci -m "added a"
595 595 $ echo b > b
596 596 $ hg add b
597 597 $ hg ci -m "added b"
598 598
599 599 $ hg mv b b1
600 600 $ hg ci -m "rename b to b1"
601 601
602 602 $ hg up ".^"
603 603 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
604 604 $ echo d > d
605 605 $ hg add d
606 606 $ hg ci -m "added d"
607 607 created new head
608 608
609 609 $ echo baba >> b
610 610 $ hg ci --amend -m "added d, modified b"
611 611
612 612 $ hg l --hidden
613 613 @ 4 added d, modified b
614 614 | b d
615 615 | x 3 added d
616 616 |/ d
617 617 | o 2 rename b to b1
618 618 |/ b b1
619 619 o 1 added b
620 620 | b
621 621 o 0 added a
622 622 a
623 623
624 624 Grafting revision 4 on top of revision 2, showing that it respect the rename:
625 625
626 626 $ hg up 2 -q
627 627 $ hg graft -r 4 --base 3 --hidden
628 628 grafting 4:af28412ec03c "added d, modified b" (tip) (no-changeset !)
629 629 grafting 4:6325ca0b7a1c "added d, modified b" (tip) (changeset !)
630 630 merging b1 and b to b1
631 631
632 632 $ hg l -l1 -p
633 633 @ 5 added d, modified b
634 634 | b1
635 635 ~ diff -r 5a4825cc2926 -r 94a2f1a0e8e2 b1 (no-changeset !)
636 636 ~ diff -r 0a0ed3b3251c -r d544fb655520 b1 (changeset !)
637 637 --- a/b1 Thu Jan 01 00:00:00 1970 +0000
638 638 +++ b/b1 Thu Jan 01 00:00:00 1970 +0000
639 639 @@ -1,1 +1,2 @@
640 640 b
641 641 +baba
642 642
643 643 Test to make sure that fullcopytracing algorithm doesn't fail when neither of the
644 644 merging csets is a descendant of the base.
645 645 -------------------------------------------------------------------------------------------------
646 646
647 647 $ newrepo
648 648 $ echo a > a
649 649 $ hg add a
650 650 $ hg ci -m "added a"
651 651 $ echo b > b
652 652 $ hg add b
653 653 $ hg ci -m "added b"
654 654
655 655 $ echo foobar > willconflict
656 656 $ hg add willconflict
657 657 $ hg ci -m "added willconflict"
658 658 $ echo c > c
659 659 $ hg add c
660 660 $ hg ci -m "added c"
661 661
662 662 $ hg l
663 663 @ 3 added c
664 664 | c
665 665 o 2 added willconflict
666 666 | willconflict
667 667 o 1 added b
668 668 | b
669 669 o 0 added a
670 670 a
671 671
672 672 $ hg up ".^^"
673 673 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
674 674 $ echo d > d
675 675 $ hg add d
676 676 $ hg ci -m "added d"
677 677 created new head
678 678
679 679 $ echo barfoo > willconflict
680 680 $ hg add willconflict
681 681 $ hg ci --amend -m "added willconflict and d"
682 682
683 683 $ hg l
684 684 @ 5 added willconflict and d
685 685 | d willconflict
686 686 | o 3 added c
687 687 | | c
688 688 | o 2 added willconflict
689 689 |/ willconflict
690 690 o 1 added b
691 691 | b
692 692 o 0 added a
693 693 a
694 694
695 695 $ hg rebase -r . -d 2 -t :other
696 696 rebasing 5:5018b1509e94 tip "added willconflict and d" (no-changeset !)
697 697 rebasing 5:af8d273bf580 tip "added willconflict and d" (changeset !)
698 698
699 699 $ hg up 3 -q
700 700 $ hg l --hidden
701 701 o 6 added willconflict and d
702 702 | d willconflict
703 703 | x 5 added willconflict and d
704 704 | | d willconflict
705 705 | | x 4 added d
706 706 | |/ d
707 707 +---@ 3 added c
708 708 | | c
709 709 o | 2 added willconflict
710 710 |/ willconflict
711 711 o 1 added b
712 712 | b
713 713 o 0 added a
714 714 a
715 715
716 716 Now if we trigger a merge between revision 3 and 6 using base revision 4,
717 717 neither of the merging csets will be a descendant of the base revision:
718 718
719 719 $ hg graft -r 6 --base 4 --hidden -t :other
720 720 grafting 6:99802e4f1e46 "added willconflict and d" (tip) (no-changeset !)
721 721 grafting 6:b19f0df72728 "added willconflict and d" (tip) (changeset !)
General Comments 0
You need to be logged in to leave comments. Login now