##// END OF EJS Templates
absorb: prompt user to accept absorb changes by default...
Mark Thomas -
r40226:31dfa7da default
parent child Browse files
Show More
@@ -1,1016 +1,1022
1 1 # absorb.py
2 2 #
3 3 # Copyright 2016 Facebook, Inc.
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 """apply working directory changes to changesets (EXPERIMENTAL)
9 9
10 10 The absorb extension provides a command to use annotate information to
11 11 amend modified chunks into the corresponding non-public changesets.
12 12
13 13 ::
14 14
15 15 [absorb]
16 16 # only check 50 recent non-public changesets at most
17 17 max-stack-size = 50
18 18 # whether to add noise to new commits to avoid obsolescence cycle
19 19 add-noise = 1
20 20 # make `amend --correlated` a shortcut to the main command
21 21 amend-flag = correlated
22 22
23 23 [color]
24 24 absorb.description = yellow
25 25 absorb.node = blue bold
26 26 absorb.path = bold
27 27 """
28 28
29 29 # TODO:
30 30 # * Rename config items to [commands] namespace
31 31 # * Converge getdraftstack() with other code in core
32 32 # * move many attributes on fixupstate to be private
33 33
34 34 from __future__ import absolute_import
35 35
36 36 import collections
37 37
38 38 from mercurial.i18n import _
39 39 from mercurial import (
40 40 cmdutil,
41 41 commands,
42 42 context,
43 43 crecord,
44 44 error,
45 45 linelog,
46 46 mdiff,
47 47 node,
48 48 obsolete,
49 49 patch,
50 50 phases,
51 51 pycompat,
52 52 registrar,
53 53 repair,
54 54 scmutil,
55 55 util,
56 56 )
57 57 from mercurial.utils import (
58 58 stringutil,
59 59 )
60 60
61 61 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
62 62 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
63 63 # be specifying the version(s) of Mercurial they are tested with, or
64 64 # leave the attribute unspecified.
65 65 testedwith = 'ships-with-hg-core'
66 66
67 67 cmdtable = {}
68 68 command = registrar.command(cmdtable)
69 69
70 70 configtable = {}
71 71 configitem = registrar.configitem(configtable)
72 72
73 73 configitem('absorb', 'add-noise', default=True)
74 74 configitem('absorb', 'amend-flag', default=None)
75 75 configitem('absorb', 'max-stack-size', default=50)
76 76
77 77 colortable = {
78 78 'absorb.description': 'yellow',
79 79 'absorb.node': 'blue bold',
80 80 'absorb.path': 'bold',
81 81 }
82 82
83 83 defaultdict = collections.defaultdict
84 84
85 85 class nullui(object):
86 86 """blank ui object doing nothing"""
87 87 debugflag = False
88 88 verbose = False
89 89 quiet = True
90 90
91 91 def __getitem__(name):
92 92 def nullfunc(*args, **kwds):
93 93 return
94 94 return nullfunc
95 95
96 96 class emptyfilecontext(object):
97 97 """minimal filecontext representing an empty file"""
98 98 def data(self):
99 99 return ''
100 100
101 101 def node(self):
102 102 return node.nullid
103 103
104 104 def uniq(lst):
105 105 """list -> list. remove duplicated items without changing the order"""
106 106 seen = set()
107 107 result = []
108 108 for x in lst:
109 109 if x not in seen:
110 110 seen.add(x)
111 111 result.append(x)
112 112 return result
113 113
114 114 def getdraftstack(headctx, limit=None):
115 115 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
116 116
117 117 changesets are sorted in topo order, oldest first.
118 118 return at most limit items, if limit is a positive number.
119 119
120 120 merges are considered as non-draft as well. i.e. every commit
121 121 returned has and only has 1 parent.
122 122 """
123 123 ctx = headctx
124 124 result = []
125 125 while ctx.phase() != phases.public:
126 126 if limit and len(result) >= limit:
127 127 break
128 128 parents = ctx.parents()
129 129 if len(parents) != 1:
130 130 break
131 131 result.append(ctx)
132 132 ctx = parents[0]
133 133 result.reverse()
134 134 return result
135 135
136 136 def getfilestack(stack, path, seenfctxs=None):
137 137 """([ctx], str, set) -> [fctx], {ctx: fctx}
138 138
139 139 stack is a list of contexts, from old to new. usually they are what
140 140 "getdraftstack" returns.
141 141
142 142 follows renames, but not copies.
143 143
144 144 seenfctxs is a set of filecontexts that will be considered "immutable".
145 145 they are usually what this function returned in earlier calls, useful
146 146 to avoid issues that a file was "moved" to multiple places and was then
147 147 modified differently, like: "a" was copied to "b", "a" was also copied to
148 148 "c" and then "a" was deleted, then both "b" and "c" were "moved" from "a"
149 149 and we enforce only one of them to be able to affect "a"'s content.
150 150
151 151 return an empty list and an empty dict, if the specified path does not
152 152 exist in stack[-1] (the top of the stack).
153 153
154 154 otherwise, return a list of de-duplicated filecontexts, and the map to
155 155 convert ctx in the stack to fctx, for possible mutable fctxs. the first item
156 156 of the list would be outside the stack and should be considered immutable.
157 157 the remaining items are within the stack.
158 158
159 159 for example, given the following changelog and corresponding filelog
160 160 revisions:
161 161
162 162 changelog: 3----4----5----6----7
163 163 filelog: x 0----1----1----2 (x: no such file yet)
164 164
165 165 - if stack = [5, 6, 7], returns ([0, 1, 2], {5: 1, 6: 1, 7: 2})
166 166 - if stack = [3, 4, 5], returns ([e, 0, 1], {4: 0, 5: 1}), where "e" is a
167 167 dummy empty filecontext.
168 168 - if stack = [2], returns ([], {})
169 169 - if stack = [7], returns ([1, 2], {7: 2})
170 170 - if stack = [6, 7], returns ([1, 2], {6: 1, 7: 2}), although {6: 1} can be
171 171 removed, since 1 is immutable.
172 172 """
173 173 if seenfctxs is None:
174 174 seenfctxs = set()
175 175 assert stack
176 176
177 177 if path not in stack[-1]:
178 178 return [], {}
179 179
180 180 fctxs = []
181 181 fctxmap = {}
182 182
183 183 pctx = stack[0].p1() # the public (immutable) ctx we stop at
184 184 for ctx in reversed(stack):
185 185 if path not in ctx: # the file is added in the next commit
186 186 pctx = ctx
187 187 break
188 188 fctx = ctx[path]
189 189 fctxs.append(fctx)
190 190 if fctx in seenfctxs: # treat fctx as the immutable one
191 191 pctx = None # do not add another immutable fctx
192 192 break
193 193 fctxmap[ctx] = fctx # only for mutable fctxs
194 194 renamed = fctx.renamed()
195 195 if renamed:
196 196 path = renamed[0] # follow rename
197 197 if path in ctx: # but do not follow copy
198 198 pctx = ctx.p1()
199 199 break
200 200
201 201 if pctx is not None: # need an extra immutable fctx
202 202 if path in pctx:
203 203 fctxs.append(pctx[path])
204 204 else:
205 205 fctxs.append(emptyfilecontext())
206 206
207 207 fctxs.reverse()
208 208 # note: we rely on a property of hg: filerev is not reused for linear
209 209 # history. i.e. it's impossible to have:
210 210 # changelog: 4----5----6 (linear, no merges)
211 211 # filelog: 1----2----1
212 212 # ^ reuse filerev (impossible)
213 213 # because parents are part of the hash. if that's not true, we need to
214 214 # remove uniq and find a different way to identify fctxs.
215 215 return uniq(fctxs), fctxmap
216 216
217 217 class overlaystore(patch.filestore):
218 218 """read-only, hybrid store based on a dict and ctx.
219 219 memworkingcopy: {path: content}, overrides file contents.
220 220 """
221 221 def __init__(self, basectx, memworkingcopy):
222 222 self.basectx = basectx
223 223 self.memworkingcopy = memworkingcopy
224 224
225 225 def getfile(self, path):
226 226 """comply with mercurial.patch.filestore.getfile"""
227 227 if path not in self.basectx:
228 228 return None, None, None
229 229 fctx = self.basectx[path]
230 230 if path in self.memworkingcopy:
231 231 content = self.memworkingcopy[path]
232 232 else:
233 233 content = fctx.data()
234 234 mode = (fctx.islink(), fctx.isexec())
235 235 renamed = fctx.renamed() # False or (path, node)
236 236 return content, mode, (renamed and renamed[0])
237 237
238 238 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
239 239 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
240 240 memworkingcopy overrides file contents.
241 241 """
242 242 # parents must contain 2 items: (node1, node2)
243 243 if parents is None:
244 244 parents = ctx.repo().changelog.parents(ctx.node())
245 245 if extra is None:
246 246 extra = ctx.extra()
247 247 date = ctx.date()
248 248 desc = ctx.description()
249 249 user = ctx.user()
250 250 files = set(ctx.files()).union(memworkingcopy)
251 251 store = overlaystore(ctx, memworkingcopy)
252 252 return context.memctx(
253 253 repo=ctx.repo(), parents=parents, text=desc,
254 254 files=files, filectxfn=store, user=user, date=date,
255 255 branch=None, extra=extra)
256 256
257 257 class filefixupstate(object):
258 258 """state needed to apply fixups to a single file
259 259
260 260 internally, it keeps file contents of several revisions and a linelog.
261 261
262 262 the linelog uses odd revision numbers for original contents (fctxs passed
263 263 to __init__), and even revision numbers for fixups, like:
264 264
265 265 linelog rev 1: self.fctxs[0] (from an immutable "public" changeset)
266 266 linelog rev 2: fixups made to self.fctxs[0]
267 267 linelog rev 3: self.fctxs[1] (a child of fctxs[0])
268 268 linelog rev 4: fixups made to self.fctxs[1]
269 269 ...
270 270
271 271 a typical use is like:
272 272
273 273 1. call diffwith, to calculate self.fixups
274 274 2. (optionally), present self.fixups to the user, or change it
275 275 3. call apply, to apply changes
276 276 4. read results from "finalcontents", or call getfinalcontent
277 277 """
278 278
279 279 def __init__(self, fctxs, path, ui=None, opts=None):
280 280 """([fctx], ui or None) -> None
281 281
282 282 fctxs should be linear, and sorted by topo order - oldest first.
283 283 fctxs[0] will be considered as "immutable" and will not be changed.
284 284 """
285 285 self.fctxs = fctxs
286 286 self.path = path
287 287 self.ui = ui or nullui()
288 288 self.opts = opts or {}
289 289
290 290 # following fields are built from fctxs. they exist for perf reason
291 291 self.contents = [f.data() for f in fctxs]
292 292 self.contentlines = pycompat.maplist(mdiff.splitnewlines, self.contents)
293 293 self.linelog = self._buildlinelog()
294 294 if self.ui.debugflag:
295 295 assert self._checkoutlinelog() == self.contents
296 296
297 297 # following fields will be filled later
298 298 self.chunkstats = [0, 0] # [adopted, total : int]
299 299 self.targetlines = [] # [str]
300 300 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
301 301 self.finalcontents = [] # [str]
302 302 self.ctxaffected = set()
303 303
304 304 def diffwith(self, targetfctx, fm=None):
305 305 """calculate fixups needed by examining the differences between
306 306 self.fctxs[-1] and targetfctx, chunk by chunk.
307 307
308 308 targetfctx is the target state we move towards. we may or may not be
309 309 able to get there because not all modified chunks can be amended into
310 310 a non-public fctx unambiguously.
311 311
312 312 call this only once, before apply().
313 313
314 314 update self.fixups, self.chunkstats, and self.targetlines.
315 315 """
316 316 a = self.contents[-1]
317 317 alines = self.contentlines[-1]
318 318 b = targetfctx.data()
319 319 blines = mdiff.splitnewlines(b)
320 320 self.targetlines = blines
321 321
322 322 self.linelog.annotate(self.linelog.maxrev)
323 323 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
324 324 assert len(annotated) == len(alines)
325 325 # add a dummy end line to make insertion at the end easier
326 326 if annotated:
327 327 dummyendline = (annotated[-1][0], annotated[-1][1] + 1)
328 328 annotated.append(dummyendline)
329 329
330 330 # analyse diff blocks
331 331 for chunk in self._alldiffchunks(a, b, alines, blines):
332 332 newfixups = self._analysediffchunk(chunk, annotated)
333 333 self.chunkstats[0] += bool(newfixups) # 1 or 0
334 334 self.chunkstats[1] += 1
335 335 self.fixups += newfixups
336 336 if fm is not None:
337 337 self._showchanges(fm, alines, blines, chunk, newfixups)
338 338
339 339 def apply(self):
340 340 """apply self.fixups. update self.linelog, self.finalcontents.
341 341
342 342 call this only once, before getfinalcontent(), after diffwith().
343 343 """
344 344 # the following is unnecessary, as it's done by "diffwith":
345 345 # self.linelog.annotate(self.linelog.maxrev)
346 346 for rev, a1, a2, b1, b2 in reversed(self.fixups):
347 347 blines = self.targetlines[b1:b2]
348 348 if self.ui.debugflag:
349 349 idx = (max(rev - 1, 0)) // 2
350 350 self.ui.write(_('%s: chunk %d:%d -> %d lines\n')
351 351 % (node.short(self.fctxs[idx].node()),
352 352 a1, a2, len(blines)))
353 353 self.linelog.replacelines(rev, a1, a2, b1, b2)
354 354 if self.opts.get('edit_lines', False):
355 355 self.finalcontents = self._checkoutlinelogwithedits()
356 356 else:
357 357 self.finalcontents = self._checkoutlinelog()
358 358
359 359 def getfinalcontent(self, fctx):
360 360 """(fctx) -> str. get modified file content for a given filecontext"""
361 361 idx = self.fctxs.index(fctx)
362 362 return self.finalcontents[idx]
363 363
364 364 def _analysediffchunk(self, chunk, annotated):
365 365 """analyse a different chunk and return new fixups found
366 366
367 367 return [] if no lines from the chunk can be safely applied.
368 368
369 369 the chunk (or lines) cannot be safely applied, if, for example:
370 370 - the modified (deleted) lines belong to a public changeset
371 371 (self.fctxs[0])
372 372 - the chunk is a pure insertion and the adjacent lines (at most 2
373 373 lines) belong to different non-public changesets, or do not belong
374 374 to any non-public changesets.
375 375 - the chunk is modifying lines from different changesets.
376 376 in this case, if the number of lines deleted equals to the number
377 377 of lines added, assume it's a simple 1:1 map (could be wrong).
378 378 otherwise, give up.
379 379 - the chunk is modifying lines from a single non-public changeset,
380 380 but other revisions touch the area as well. i.e. the lines are
381 381 not continuous as seen from the linelog.
382 382 """
383 383 a1, a2, b1, b2 = chunk
384 384 # find involved indexes from annotate result
385 385 involved = annotated[a1:a2]
386 386 if not involved and annotated: # a1 == a2 and a is not empty
387 387 # pure insertion, check nearby lines. ignore lines belong
388 388 # to the public (first) changeset (i.e. annotated[i][0] == 1)
389 389 nearbylinenums = {a2, max(0, a1 - 1)}
390 390 involved = [annotated[i]
391 391 for i in nearbylinenums if annotated[i][0] != 1]
392 392 involvedrevs = list(set(r for r, l in involved))
393 393 newfixups = []
394 394 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
395 395 # chunk belongs to a single revision
396 396 rev = involvedrevs[0]
397 397 if rev > 1:
398 398 fixuprev = rev + 1
399 399 newfixups.append((fixuprev, a1, a2, b1, b2))
400 400 elif a2 - a1 == b2 - b1 or b1 == b2:
401 401 # 1:1 line mapping, or chunk was deleted
402 402 for i in pycompat.xrange(a1, a2):
403 403 rev, linenum = annotated[i]
404 404 if rev > 1:
405 405 if b1 == b2: # deletion, simply remove that single line
406 406 nb1 = nb2 = 0
407 407 else: # 1:1 line mapping, change the corresponding rev
408 408 nb1 = b1 + i - a1
409 409 nb2 = nb1 + 1
410 410 fixuprev = rev + 1
411 411 newfixups.append((fixuprev, i, i + 1, nb1, nb2))
412 412 return self._optimizefixups(newfixups)
413 413
414 414 @staticmethod
415 415 def _alldiffchunks(a, b, alines, blines):
416 416 """like mdiff.allblocks, but only care about differences"""
417 417 blocks = mdiff.allblocks(a, b, lines1=alines, lines2=blines)
418 418 for chunk, btype in blocks:
419 419 if btype != '!':
420 420 continue
421 421 yield chunk
422 422
423 423 def _buildlinelog(self):
424 424 """calculate the initial linelog based on self.content{,line}s.
425 425 this is similar to running a partial "annotate".
426 426 """
427 427 llog = linelog.linelog()
428 428 a, alines = '', []
429 429 for i in pycompat.xrange(len(self.contents)):
430 430 b, blines = self.contents[i], self.contentlines[i]
431 431 llrev = i * 2 + 1
432 432 chunks = self._alldiffchunks(a, b, alines, blines)
433 433 for a1, a2, b1, b2 in reversed(list(chunks)):
434 434 llog.replacelines(llrev, a1, a2, b1, b2)
435 435 a, alines = b, blines
436 436 return llog
437 437
438 438 def _checkoutlinelog(self):
439 439 """() -> [str]. check out file contents from linelog"""
440 440 contents = []
441 441 for i in pycompat.xrange(len(self.contents)):
442 442 rev = (i + 1) * 2
443 443 self.linelog.annotate(rev)
444 444 content = ''.join(map(self._getline, self.linelog.annotateresult))
445 445 contents.append(content)
446 446 return contents
447 447
448 448 def _checkoutlinelogwithedits(self):
449 449 """() -> [str]. prompt all lines for edit"""
450 450 alllines = self.linelog.getalllines()
451 451 # header
452 452 editortext = (_('HG: editing %s\nHG: "y" means the line to the right '
453 453 'exists in the changeset to the top\nHG:\n')
454 454 % self.fctxs[-1].path())
455 455 # [(idx, fctx)]. hide the dummy emptyfilecontext
456 456 visiblefctxs = [(i, f)
457 457 for i, f in enumerate(self.fctxs)
458 458 if not isinstance(f, emptyfilecontext)]
459 459 for i, (j, f) in enumerate(visiblefctxs):
460 460 editortext += (_('HG: %s/%s %s %s\n') %
461 461 ('|' * i, '-' * (len(visiblefctxs) - i + 1),
462 462 node.short(f.node()),
463 463 f.description().split('\n',1)[0]))
464 464 editortext += _('HG: %s\n') % ('|' * len(visiblefctxs))
465 465 # figure out the lifetime of a line, this is relatively inefficient,
466 466 # but probably fine
467 467 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
468 468 for i, f in visiblefctxs:
469 469 self.linelog.annotate((i + 1) * 2)
470 470 for l in self.linelog.annotateresult:
471 471 lineset[l].add(i)
472 472 # append lines
473 473 for l in alllines:
474 474 editortext += (' %s : %s' %
475 475 (''.join([('y' if i in lineset[l] else ' ')
476 476 for i, _f in visiblefctxs]),
477 477 self._getline(l)))
478 478 # run editor
479 479 editedtext = self.ui.edit(editortext, '', action='absorb')
480 480 if not editedtext:
481 481 raise error.Abort(_('empty editor text'))
482 482 # parse edited result
483 483 contents = ['' for i in self.fctxs]
484 484 leftpadpos = 4
485 485 colonpos = leftpadpos + len(visiblefctxs) + 1
486 486 for l in mdiff.splitnewlines(editedtext):
487 487 if l.startswith('HG:'):
488 488 continue
489 489 if l[colonpos - 1:colonpos + 2] != ' : ':
490 490 raise error.Abort(_('malformed line: %s') % l)
491 491 linecontent = l[colonpos + 2:]
492 492 for i, ch in enumerate(l[leftpadpos:colonpos - 1]):
493 493 if ch == 'y':
494 494 contents[visiblefctxs[i][0]] += linecontent
495 495 # chunkstats is hard to calculate if anything changes, therefore
496 496 # set them to just a simple value (1, 1).
497 497 if editedtext != editortext:
498 498 self.chunkstats = [1, 1]
499 499 return contents
500 500
501 501 def _getline(self, lineinfo):
502 502 """((rev, linenum)) -> str. convert rev+line number to line content"""
503 503 rev, linenum = lineinfo
504 504 if rev & 1: # odd: original line taken from fctxs
505 505 return self.contentlines[rev // 2][linenum]
506 506 else: # even: fixup line from targetfctx
507 507 return self.targetlines[linenum]
508 508
509 509 def _iscontinuous(self, a1, a2, closedinterval=False):
510 510 """(a1, a2 : int) -> bool
511 511
512 512 check if these lines are continuous. i.e. no other insertions or
513 513 deletions (from other revisions) among these lines.
514 514
515 515 closedinterval decides whether a2 should be included or not. i.e. is
516 516 it [a1, a2), or [a1, a2] ?
517 517 """
518 518 if a1 >= a2:
519 519 return True
520 520 llog = self.linelog
521 521 offset1 = llog.getoffset(a1)
522 522 offset2 = llog.getoffset(a2) + int(closedinterval)
523 523 linesinbetween = llog.getalllines(offset1, offset2)
524 524 return len(linesinbetween) == a2 - a1 + int(closedinterval)
525 525
526 526 def _optimizefixups(self, fixups):
527 527 """[(rev, a1, a2, b1, b2)] -> [(rev, a1, a2, b1, b2)].
528 528 merge adjacent fixups to make them less fragmented.
529 529 """
530 530 result = []
531 531 pcurrentchunk = [[-1, -1, -1, -1, -1]]
532 532
533 533 def pushchunk():
534 534 if pcurrentchunk[0][0] != -1:
535 535 result.append(tuple(pcurrentchunk[0]))
536 536
537 537 for i, chunk in enumerate(fixups):
538 538 rev, a1, a2, b1, b2 = chunk
539 539 lastrev = pcurrentchunk[0][0]
540 540 lasta2 = pcurrentchunk[0][2]
541 541 lastb2 = pcurrentchunk[0][4]
542 542 if (a1 == lasta2 and b1 == lastb2 and rev == lastrev and
543 543 self._iscontinuous(max(a1 - 1, 0), a1)):
544 544 # merge into currentchunk
545 545 pcurrentchunk[0][2] = a2
546 546 pcurrentchunk[0][4] = b2
547 547 else:
548 548 pushchunk()
549 549 pcurrentchunk[0] = list(chunk)
550 550 pushchunk()
551 551 return result
552 552
553 553 def _showchanges(self, fm, alines, blines, chunk, fixups):
554 554
555 555 def trim(line):
556 556 if line.endswith('\n'):
557 557 line = line[:-1]
558 558 return line
559 559
560 560 # this is not optimized for perf but _showchanges only gets executed
561 561 # with an extra command-line flag.
562 562 a1, a2, b1, b2 = chunk
563 563 aidxs, bidxs = [0] * (a2 - a1), [0] * (b2 - b1)
564 564 for idx, fa1, fa2, fb1, fb2 in fixups:
565 565 for i in pycompat.xrange(fa1, fa2):
566 566 aidxs[i - a1] = (max(idx, 1) - 1) // 2
567 567 for i in pycompat.xrange(fb1, fb2):
568 568 bidxs[i - b1] = (max(idx, 1) - 1) // 2
569 569
570 570 fm.startitem()
571 571 fm.write('hunk', ' %s\n',
572 572 '@@ -%d,%d +%d,%d @@'
573 573 % (a1, a2 - a1, b1, b2 - b1), label='diff.hunk')
574 574 fm.data(path=self.path, linetype='hunk')
575 575
576 576 def writeline(idx, diffchar, line, linetype, linelabel):
577 577 fm.startitem()
578 578 node = ''
579 579 if idx:
580 580 ctx = self.fctxs[idx]
581 581 fm.context(fctx=ctx)
582 582 node = ctx.hex()
583 583 self.ctxaffected.add(ctx.changectx())
584 584 fm.write('node', '%-7.7s ', node, label='absorb.node')
585 585 fm.write('diffchar ' + linetype, '%s%s\n', diffchar, line,
586 586 label=linelabel)
587 587 fm.data(path=self.path, linetype=linetype)
588 588
589 589 for i in pycompat.xrange(a1, a2):
590 590 writeline(aidxs[i - a1], '-', trim(alines[i]), 'deleted',
591 591 'diff.deleted')
592 592 for i in pycompat.xrange(b1, b2):
593 593 writeline(bidxs[i - b1], '+', trim(blines[i]), 'inserted',
594 594 'diff.inserted')
595 595
596 596 class fixupstate(object):
597 597 """state needed to run absorb
598 598
599 599 internally, it keeps paths and filefixupstates.
600 600
601 601 a typical use is like filefixupstates:
602 602
603 603 1. call diffwith, to calculate fixups
604 604 2. (optionally), present fixups to the user, or edit fixups
605 605 3. call apply, to apply changes to memory
606 606 4. call commit, to commit changes to hg database
607 607 """
608 608
609 609 def __init__(self, stack, ui=None, opts=None):
610 610 """([ctx], ui or None) -> None
611 611
612 612 stack: should be linear, and sorted by topo order - oldest first.
613 613 all commits in stack are considered mutable.
614 614 """
615 615 assert stack
616 616 self.ui = ui or nullui()
617 617 self.opts = opts or {}
618 618 self.stack = stack
619 619 self.repo = stack[-1].repo().unfiltered()
620 620
621 621 # following fields will be filled later
622 622 self.paths = [] # [str]
623 623 self.status = None # ctx.status output
624 624 self.fctxmap = {} # {path: {ctx: fctx}}
625 625 self.fixupmap = {} # {path: filefixupstate}
626 626 self.replacemap = {} # {oldnode: newnode or None}
627 627 self.finalnode = None # head after all fixups
628 628 self.ctxaffected = set() # ctx that will be absorbed into
629 629
630 630 def diffwith(self, targetctx, match=None, fm=None):
631 631 """diff and prepare fixups. update self.fixupmap, self.paths"""
632 632 # only care about modified files
633 633 self.status = self.stack[-1].status(targetctx, match)
634 634 self.paths = []
635 635 # but if --edit-lines is used, the user may want to edit files
636 636 # even if they are not modified
637 637 editopt = self.opts.get('edit_lines')
638 638 if not self.status.modified and editopt and match:
639 639 interestingpaths = match.files()
640 640 else:
641 641 interestingpaths = self.status.modified
642 642 # prepare the filefixupstate
643 643 seenfctxs = set()
644 644 # sorting is necessary to eliminate ambiguity for the "double move"
645 645 # case: "hg cp A B; hg cp A C; hg rm A", then only "B" can affect "A".
646 646 for path in sorted(interestingpaths):
647 647 self.ui.debug('calculating fixups for %s\n' % path)
648 648 targetfctx = targetctx[path]
649 649 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
650 650 # ignore symbolic links or binary, or unchanged files
651 651 if any(f.islink() or stringutil.binary(f.data())
652 652 for f in [targetfctx] + fctxs
653 653 if not isinstance(f, emptyfilecontext)):
654 654 continue
655 655 if targetfctx.data() == fctxs[-1].data() and not editopt:
656 656 continue
657 657 seenfctxs.update(fctxs[1:])
658 658 self.fctxmap[path] = ctx2fctx
659 659 fstate = filefixupstate(fctxs, path, ui=self.ui, opts=self.opts)
660 660 if fm is not None:
661 661 fm.startitem()
662 662 fm.plain('showing changes for ')
663 663 fm.write('path', '%s\n', path, label='absorb.path')
664 664 fm.data(linetype='path')
665 665 fstate.diffwith(targetfctx, fm)
666 666 self.fixupmap[path] = fstate
667 667 self.paths.append(path)
668 668 self.ctxaffected.update(fstate.ctxaffected)
669 669
670 670 def apply(self):
671 671 """apply fixups to individual filefixupstates"""
672 672 for path, state in self.fixupmap.iteritems():
673 673 if self.ui.debugflag:
674 674 self.ui.write(_('applying fixups to %s\n') % path)
675 675 state.apply()
676 676
677 677 @property
678 678 def chunkstats(self):
679 679 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
680 680 return dict((path, state.chunkstats)
681 681 for path, state in self.fixupmap.iteritems())
682 682
683 683 def commit(self):
684 684 """commit changes. update self.finalnode, self.replacemap"""
685 685 with self.repo.wlock(), self.repo.lock():
686 686 with self.repo.transaction('absorb') as tr:
687 687 self._commitstack()
688 688 self._movebookmarks(tr)
689 689 if self.repo['.'].node() in self.replacemap:
690 690 self._moveworkingdirectoryparent()
691 691 if self._useobsolete:
692 692 self._obsoleteoldcommits()
693 693 if not self._useobsolete: # strip must be outside transactions
694 694 self._stripoldcommits()
695 695 return self.finalnode
696 696
697 697 def printchunkstats(self):
698 698 """print things like '1 of 2 chunk(s) applied'"""
699 699 ui = self.ui
700 700 chunkstats = self.chunkstats
701 701 if ui.verbose:
702 702 # chunkstats for each file
703 703 for path, stat in chunkstats.iteritems():
704 704 if stat[0]:
705 705 ui.write(_('%s: %d of %d chunk(s) applied\n')
706 706 % (path, stat[0], stat[1]))
707 707 elif not ui.quiet:
708 708 # a summary for all files
709 709 stats = chunkstats.values()
710 710 applied, total = (sum(s[i] for s in stats) for i in (0, 1))
711 711 ui.write(_('%d of %d chunk(s) applied\n') % (applied, total))
712 712
713 713 def _commitstack(self):
714 714 """make new commits. update self.finalnode, self.replacemap.
715 715 it is splitted from "commit" to avoid too much indentation.
716 716 """
717 717 # last node (20-char) committed by us
718 718 lastcommitted = None
719 719 # p1 which overrides the parent of the next commit, "None" means use
720 720 # the original parent unchanged
721 721 nextp1 = None
722 722 for ctx in self.stack:
723 723 memworkingcopy = self._getnewfilecontents(ctx)
724 724 if not memworkingcopy and not lastcommitted:
725 725 # nothing changed, nothing commited
726 726 nextp1 = ctx
727 727 continue
728 728 msg = ''
729 729 if self._willbecomenoop(memworkingcopy, ctx, nextp1):
730 730 # changeset is no longer necessary
731 731 self.replacemap[ctx.node()] = None
732 732 msg = _('became empty and was dropped')
733 733 else:
734 734 # changeset needs re-commit
735 735 nodestr = self._commitsingle(memworkingcopy, ctx, p1=nextp1)
736 736 lastcommitted = self.repo[nodestr]
737 737 nextp1 = lastcommitted
738 738 self.replacemap[ctx.node()] = lastcommitted.node()
739 739 if memworkingcopy:
740 740 msg = _('%d file(s) changed, became %s') % (
741 741 len(memworkingcopy), self._ctx2str(lastcommitted))
742 742 else:
743 743 msg = _('became %s') % self._ctx2str(lastcommitted)
744 744 if self.ui.verbose and msg:
745 745 self.ui.write(_('%s: %s\n') % (self._ctx2str(ctx), msg))
746 746 self.finalnode = lastcommitted and lastcommitted.node()
747 747
748 748 def _ctx2str(self, ctx):
749 749 if self.ui.debugflag:
750 750 return '%d:%s' % (ctx.rev(), ctx.hex())
751 751 else:
752 752 return '%d:%s' % (ctx.rev(), node.short(ctx.node()))
753 753
754 754 def _getnewfilecontents(self, ctx):
755 755 """(ctx) -> {path: str}
756 756
757 757 fetch file contents from filefixupstates.
758 758 return the working copy overrides - files different from ctx.
759 759 """
760 760 result = {}
761 761 for path in self.paths:
762 762 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
763 763 if ctx not in ctx2fctx:
764 764 continue
765 765 fctx = ctx2fctx[ctx]
766 766 content = fctx.data()
767 767 newcontent = self.fixupmap[path].getfinalcontent(fctx)
768 768 if content != newcontent:
769 769 result[fctx.path()] = newcontent
770 770 return result
771 771
772 772 def _movebookmarks(self, tr):
773 773 repo = self.repo
774 774 needupdate = [(name, self.replacemap[hsh])
775 775 for name, hsh in repo._bookmarks.iteritems()
776 776 if hsh in self.replacemap]
777 777 changes = []
778 778 for name, hsh in needupdate:
779 779 if hsh:
780 780 changes.append((name, hsh))
781 781 if self.ui.verbose:
782 782 self.ui.write(_('moving bookmark %s to %s\n')
783 783 % (name, node.hex(hsh)))
784 784 else:
785 785 changes.append((name, None))
786 786 if self.ui.verbose:
787 787 self.ui.write(_('deleting bookmark %s\n') % name)
788 788 repo._bookmarks.applychanges(repo, tr, changes)
789 789
790 790 def _moveworkingdirectoryparent(self):
791 791 if not self.finalnode:
792 792 # Find the latest not-{obsoleted,stripped} parent.
793 793 revs = self.repo.revs('max(::. - %ln)', self.replacemap.keys())
794 794 ctx = self.repo[revs.first()]
795 795 self.finalnode = ctx.node()
796 796 else:
797 797 ctx = self.repo[self.finalnode]
798 798
799 799 dirstate = self.repo.dirstate
800 800 # dirstate.rebuild invalidates fsmonitorstate, causing "hg status" to
801 801 # be slow. in absorb's case, no need to invalidate fsmonitorstate.
802 802 noop = lambda: 0
803 803 restore = noop
804 804 if util.safehasattr(dirstate, '_fsmonitorstate'):
805 805 bak = dirstate._fsmonitorstate.invalidate
806 806 def restore():
807 807 dirstate._fsmonitorstate.invalidate = bak
808 808 dirstate._fsmonitorstate.invalidate = noop
809 809 try:
810 810 with dirstate.parentchange():
811 811 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
812 812 finally:
813 813 restore()
814 814
815 815 @staticmethod
816 816 def _willbecomenoop(memworkingcopy, ctx, pctx=None):
817 817 """({path: content}, ctx, ctx) -> bool. test if a commit will be noop
818 818
819 819 if it will become an empty commit (does not change anything, after the
820 820 memworkingcopy overrides), return True. otherwise return False.
821 821 """
822 822 if not pctx:
823 823 parents = ctx.parents()
824 824 if len(parents) != 1:
825 825 return False
826 826 pctx = parents[0]
827 827 # ctx changes more files (not a subset of memworkingcopy)
828 828 if not set(ctx.files()).issubset(set(memworkingcopy)):
829 829 return False
830 830 for path, content in memworkingcopy.iteritems():
831 831 if path not in pctx or path not in ctx:
832 832 return False
833 833 fctx = ctx[path]
834 834 pfctx = pctx[path]
835 835 if pfctx.flags() != fctx.flags():
836 836 return False
837 837 if pfctx.data() != content:
838 838 return False
839 839 return True
840 840
841 841 def _commitsingle(self, memworkingcopy, ctx, p1=None):
842 842 """(ctx, {path: content}, node) -> node. make a single commit
843 843
844 844 the commit is a clone from ctx, with a (optionally) different p1, and
845 845 different file contents replaced by memworkingcopy.
846 846 """
847 847 parents = p1 and (p1, node.nullid)
848 848 extra = ctx.extra()
849 849 if self._useobsolete and self.ui.configbool('absorb', 'add-noise'):
850 850 extra['absorb_source'] = ctx.hex()
851 851 mctx = overlaycontext(memworkingcopy, ctx, parents, extra=extra)
852 852 # preserve phase
853 853 with mctx.repo().ui.configoverride({
854 854 ('phases', 'new-commit'): ctx.phase()}):
855 855 return mctx.commit()
856 856
857 857 @util.propertycache
858 858 def _useobsolete(self):
859 859 """() -> bool"""
860 860 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
861 861
862 862 def _obsoleteoldcommits(self):
863 863 relations = [(self.repo[k], v and (self.repo[v],) or ())
864 864 for k, v in self.replacemap.iteritems()]
865 865 if relations:
866 866 obsolete.createmarkers(self.repo, relations)
867 867
868 868 def _stripoldcommits(self):
869 869 nodelist = self.replacemap.keys()
870 870 # make sure we don't strip innocent children
871 871 revs = self.repo.revs('%ln - (::(heads(%ln::)-%ln))', nodelist,
872 872 nodelist, nodelist)
873 873 tonode = self.repo.changelog.node
874 874 nodelist = [tonode(r) for r in revs]
875 875 if nodelist:
876 876 repair.strip(self.repo.ui, self.repo, nodelist)
877 877
878 878 def _parsechunk(hunk):
879 879 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
880 880 if type(hunk) not in (crecord.uihunk, patch.recordhunk):
881 881 return None, None
882 882 path = hunk.header.filename()
883 883 a1 = hunk.fromline + len(hunk.before) - 1
884 884 # remove before and after context
885 885 hunk.before = hunk.after = []
886 886 buf = util.stringio()
887 887 hunk.write(buf)
888 888 patchlines = mdiff.splitnewlines(buf.getvalue())
889 889 # hunk.prettystr() will update hunk.removed
890 890 a2 = a1 + hunk.removed
891 891 blines = [l[1:] for l in patchlines[1:] if l[0] != '-']
892 892 return path, (a1, a2, blines)
893 893
894 894 def overlaydiffcontext(ctx, chunks):
895 895 """(ctx, [crecord.uihunk]) -> memctx
896 896
897 897 return a memctx with some [1] patches (chunks) applied to ctx.
898 898 [1]: modifications are handled. renames, mode changes, etc. are ignored.
899 899 """
900 900 # sadly the applying-patch logic is hardly reusable, and messy:
901 901 # 1. the core logic "_applydiff" is too heavy - it writes .rej files, it
902 902 # needs a file stream of a patch and will re-parse it, while we have
903 903 # structured hunk objects at hand.
904 904 # 2. a lot of different implementations about "chunk" (patch.hunk,
905 905 # patch.recordhunk, crecord.uihunk)
906 906 # as we only care about applying changes to modified files, no mode
907 907 # change, no binary diff, and no renames, it's probably okay to
908 908 # re-invent the logic using much simpler code here.
909 909 memworkingcopy = {} # {path: content}
910 910 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
911 911 for path, info in map(_parsechunk, chunks):
912 912 if not path or not info:
913 913 continue
914 914 patchmap[path].append(info)
915 915 for path, patches in patchmap.iteritems():
916 916 if path not in ctx or not patches:
917 917 continue
918 918 patches.sort(reverse=True)
919 919 lines = mdiff.splitnewlines(ctx[path].data())
920 920 for a1, a2, blines in patches:
921 921 lines[a1:a2] = blines
922 922 memworkingcopy[path] = ''.join(lines)
923 923 return overlaycontext(memworkingcopy, ctx)
924 924
925 925 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
926 926 """pick fixup chunks from targetctx, apply them to stack.
927 927
928 928 if targetctx is None, the working copy context will be used.
929 929 if stack is None, the current draft stack will be used.
930 930 return fixupstate.
931 931 """
932 932 if stack is None:
933 933 limit = ui.configint('absorb', 'max-stack-size')
934 934 stack = getdraftstack(repo['.'], limit)
935 935 if limit and len(stack) >= limit:
936 936 ui.warn(_('absorb: only the recent %d changesets will '
937 937 'be analysed\n')
938 938 % limit)
939 939 if not stack:
940 940 raise error.Abort(_('no mutable changeset to change'))
941 941 if targetctx is None: # default to working copy
942 942 targetctx = repo[None]
943 943 if pats is None:
944 944 pats = ()
945 945 if opts is None:
946 946 opts = {}
947 947 state = fixupstate(stack, ui=ui, opts=opts)
948 948 matcher = scmutil.match(targetctx, pats, opts)
949 949 if opts.get('interactive'):
950 950 diff = patch.diff(repo, stack[-1].node(), targetctx.node(), matcher)
951 951 origchunks = patch.parsepatch(diff)
952 952 chunks = cmdutil.recordfilter(ui, origchunks)[0]
953 953 targetctx = overlaydiffcontext(stack[-1], chunks)
954 954 fm = None
955 if opts.get('print_changes'):
955 if opts.get('print_changes') or not opts.get('apply_changes'):
956 956 fm = ui.formatter('absorb', opts)
957 957 state.diffwith(targetctx, matcher, fm)
958 958 if fm is not None:
959 959 fm.startitem()
960 960 fm.write("count", "\n%d changesets affected\n", len(state.ctxaffected))
961 961 fm.data(linetype='summary')
962 962 for ctx in reversed(stack):
963 963 if ctx not in state.ctxaffected:
964 964 continue
965 965 fm.startitem()
966 966 fm.context(ctx=ctx)
967 967 fm.data(linetype='changeset')
968 968 fm.write('node', '%-7.7s ', ctx.hex(), label='absorb.node')
969 969 descfirstline = ctx.description().splitlines()[0]
970 970 fm.write('descfirstline', '%s\n', descfirstline,
971 971 label='absorb.description')
972 972 fm.end()
973 973 if not opts.get('dry_run'):
974 if not opts.get('apply_changes'):
975 if ui.promptchoice("apply changes (yn)? $$ &Yes $$ &No", default=1):
976 raise error.Abort(_('absorb cancelled\n'))
977
974 978 state.apply()
975 979 if state.commit():
976 980 state.printchunkstats()
977 981 elif not ui.quiet:
978 982 ui.write(_('nothing applied\n'))
979 983 return state
980 984
981 985 @command('^absorb',
982 [('p', 'print-changes', None,
983 _('print which changesets are modified by which changes')),
986 [('a', 'apply-changes', None,
987 _('apply changes without prompting for confirmation')),
988 ('p', 'print-changes', None,
989 _('just print which changesets are modified by which changes')),
984 990 ('i', 'interactive', None,
985 991 _('interactively select which chunks to apply (EXPERIMENTAL)')),
986 992 ('e', 'edit-lines', None,
987 993 _('edit what lines belong to which changesets before commit '
988 994 '(EXPERIMENTAL)')),
989 995 ] + commands.dryrunopts + commands.templateopts + commands.walkopts,
990 996 _('hg absorb [OPTION] [FILE]...'))
991 997 def absorbcmd(ui, repo, *pats, **opts):
992 998 """incorporate corrections into the stack of draft changesets
993 999
994 1000 absorb analyzes each change in your working directory and attempts to
995 1001 amend the changed lines into the changesets in your stack that first
996 1002 introduced those lines.
997 1003
998 1004 If absorb cannot find an unambiguous changeset to amend for a change,
999 1005 that change will be left in the working directory, untouched. They can be
1000 1006 observed by :hg:`status` or :hg:`diff` afterwards. In other words,
1001 1007 absorb does not write to the working directory.
1002 1008
1003 1009 Changesets outside the revset `::. and not public() and not merge()` will
1004 1010 not be changed.
1005 1011
1006 1012 Changesets that become empty after applying the changes will be deleted.
1007 1013
1008 1014 If in doubt, run :hg:`absorb -pn` to preview what changesets will
1009 1015 be amended by what changed lines, without actually changing anything.
1010 1016
1011 1017 Returns 0 on success, 1 if all chunks were ignored and nothing amended.
1012 1018 """
1013 1019 opts = pycompat.byteskwargs(opts)
1014 1020 state = absorb(ui, repo, pats=pats, opts=opts)
1015 1021 if sum(s[0] for s in state.chunkstats.values()) == 0:
1016 1022 return 1
@@ -1,61 +1,61
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [extensions]
3 3 > absorb=
4 4 > EOF
5 5
6 6 $ hg init repo1
7 7 $ cd repo1
8 8
9 9 Make some commits:
10 10
11 11 $ for i in 1 2 3; do
12 12 > echo $i >> a
13 13 > hg commit -A a -m "commit $i" -q
14 14 > done
15 15
16 16 absorb --edit-lines will run the editor if filename is provided:
17 17
18 $ hg absorb --edit-lines
18 $ hg absorb --edit-lines --apply-changes
19 19 nothing applied
20 20 [1]
21 $ HGEDITOR=cat hg absorb --edit-lines a
21 $ HGEDITOR=cat hg absorb --edit-lines --apply-changes a
22 22 HG: editing a
23 23 HG: "y" means the line to the right exists in the changeset to the top
24 24 HG:
25 25 HG: /---- 4ec16f85269a commit 1
26 26 HG: |/--- 5c5f95224a50 commit 2
27 27 HG: ||/-- 43f0a75bede7 commit 3
28 28 HG: |||
29 29 yyy : 1
30 30 yy : 2
31 31 y : 3
32 32 nothing applied
33 33 [1]
34 34
35 35 Edit the file using --edit-lines:
36 36
37 37 $ cat > editortext << EOF
38 38 > y : a
39 39 > yy : b
40 40 > y : c
41 41 > yy : d
42 42 > y y : e
43 43 > y : f
44 44 > yyy : g
45 45 > EOF
46 $ HGEDITOR='cat editortext >' hg absorb -q --edit-lines a
46 $ HGEDITOR='cat editortext >' hg absorb -q --edit-lines --apply-changes a
47 47 $ hg cat -r 0 a
48 48 d
49 49 e
50 50 f
51 51 g
52 52 $ hg cat -r 1 a
53 53 b
54 54 c
55 55 d
56 56 g
57 57 $ hg cat -r 2 a
58 58 a
59 59 b
60 60 e
61 61 g
@@ -1,30 +1,30
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [extensions]
3 3 > absorb=
4 4 > drawdag=$TESTDIR/drawdag.py
5 5 > EOF
6 6
7 7 $ hg init
8 8 $ hg debugdrawdag <<'EOS'
9 9 > C
10 10 > |
11 11 > B
12 12 > |
13 13 > A
14 14 > EOS
15 15
16 16 $ hg phase -r A --public -q
17 17 $ hg phase -r C --secret --force -q
18 18
19 19 $ hg update C -q
20 20 $ printf B1 > B
21 21
22 $ hg absorb -q
22 $ hg absorb -aq
23 23
24 24 $ hg log -G -T '{desc} {phase}'
25 25 @ C secret
26 26 |
27 27 o B draft
28 28 |
29 29 o A public
30 30
@@ -1,387 +1,387
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [diff]
3 3 > git=1
4 4 > [extensions]
5 5 > absorb=
6 6 > EOF
7 7
8 8 $ sedi() { # workaround check-code
9 9 > pattern="$1"
10 10 > shift
11 11 > for i in "$@"; do
12 12 > sed "$pattern" "$i" > "$i".tmp
13 13 > mv "$i".tmp "$i"
14 14 > done
15 15 > }
16 16
17 17 rename a to b, then b to a
18 18
19 19 $ hg init repo1
20 20 $ cd repo1
21 21
22 22 $ echo 1 > a
23 23 $ hg ci -A a -m 1
24 24 $ hg mv a b
25 25 $ echo 2 >> b
26 26 $ hg ci -m 2
27 27 $ hg mv b a
28 28 $ echo 3 >> a
29 29 $ hg ci -m 3
30 30
31 31 $ hg annotate -ncf a
32 32 0 eff892de26ec a: 1
33 33 1 bf56e1f4f857 b: 2
34 34 2 0b888b00216c a: 3
35 35
36 36 $ sedi 's/$/a/' a
37 $ hg absorb -pq
37 $ hg absorb -apq
38 38 showing changes for a
39 39 @@ -0,3 +0,3 @@
40 40 eff892d -1
41 41 bf56e1f -2
42 42 0b888b0 -3
43 43 eff892d +1a
44 44 bf56e1f +2a
45 45 0b888b0 +3a
46 46
47 47 3 changesets affected
48 48 0b888b0 3
49 49 bf56e1f 2
50 50 eff892d 1
51 51
52 52 $ hg status
53 53
54 54 $ hg annotate -ncf a
55 55 0 5d1c5620e6f2 a: 1a
56 56 1 9a14ffe67ae9 b: 2a
57 57 2 9191d121a268 a: 3a
58 58
59 59 when the first changeset is public
60 60
61 61 $ hg phase --public -r 0
62 62
63 63 $ sedi 's/a/A/' a
64 64
65 $ hg absorb -pq
65 $ hg absorb -apq
66 66 showing changes for a
67 67 @@ -0,3 +0,3 @@
68 68 -1a
69 69 9a14ffe -2a
70 70 9191d12 -3a
71 71 +1A
72 72 9a14ffe +2A
73 73 9191d12 +3A
74 74
75 75 2 changesets affected
76 76 9191d12 3
77 77 9a14ffe 2
78 78
79 79 $ hg diff
80 80 diff --git a/a b/a
81 81 --- a/a
82 82 +++ b/a
83 83 @@ -1,3 +1,3 @@
84 84 -1a
85 85 +1A
86 86 2A
87 87 3A
88 88
89 89 copy a to b
90 90
91 91 $ cd ..
92 92 $ hg init repo2
93 93 $ cd repo2
94 94
95 95 $ echo 1 > a
96 96 $ hg ci -A a -m 1
97 97 $ hg cp a b
98 98 $ echo 2 >> b
99 99 $ hg ci -m 2
100 100
101 101 $ hg log -T '{rev}:{node|short} {desc}\n'
102 102 1:17b72129ab68 2
103 103 0:eff892de26ec 1
104 104
105 105 $ sedi 's/$/a/' a
106 106 $ sedi 's/$/b/' b
107 107
108 $ hg absorb -pq
108 $ hg absorb -apq
109 109 showing changes for a
110 110 @@ -0,1 +0,1 @@
111 111 eff892d -1
112 112 eff892d +1a
113 113 showing changes for b
114 114 @@ -0,2 +0,2 @@
115 115 -1
116 116 17b7212 -2
117 117 +1b
118 118 17b7212 +2b
119 119
120 120 2 changesets affected
121 121 17b7212 2
122 122 eff892d 1
123 123
124 124 $ hg diff
125 125 diff --git a/b b/b
126 126 --- a/b
127 127 +++ b/b
128 128 @@ -1,2 +1,2 @@
129 129 -1
130 130 +1b
131 131 2b
132 132
133 133 copy b to a
134 134
135 135 $ cd ..
136 136 $ hg init repo3
137 137 $ cd repo3
138 138
139 139 $ echo 1 > b
140 140 $ hg ci -A b -m 1
141 141 $ hg cp b a
142 142 $ echo 2 >> a
143 143 $ hg ci -m 2
144 144
145 145 $ hg log -T '{rev}:{node|short} {desc}\n'
146 146 1:e62c256d8b24 2
147 147 0:55105f940d5c 1
148 148
149 149 $ sedi 's/$/a/' a
150 150 $ sedi 's/$/a/' b
151 151
152 $ hg absorb -pq
152 $ hg absorb -apq
153 153 showing changes for a
154 154 @@ -0,2 +0,2 @@
155 155 -1
156 156 e62c256 -2
157 157 +1a
158 158 e62c256 +2a
159 159 showing changes for b
160 160 @@ -0,1 +0,1 @@
161 161 55105f9 -1
162 162 55105f9 +1a
163 163
164 164 2 changesets affected
165 165 e62c256 2
166 166 55105f9 1
167 167
168 168 $ hg diff
169 169 diff --git a/a b/a
170 170 --- a/a
171 171 +++ b/a
172 172 @@ -1,2 +1,2 @@
173 173 -1
174 174 +1a
175 175 2a
176 176
177 177 "move" b to both a and c, follow a - sorted alphabetically
178 178
179 179 $ cd ..
180 180 $ hg init repo4
181 181 $ cd repo4
182 182
183 183 $ echo 1 > b
184 184 $ hg ci -A b -m 1
185 185 $ hg cp b a
186 186 $ hg cp b c
187 187 $ hg rm b
188 188 $ echo 2 >> a
189 189 $ echo 3 >> c
190 190 $ hg commit -m cp
191 191
192 192 $ hg log -T '{rev}:{node|short} {desc}\n'
193 193 1:366daad8e679 cp
194 194 0:55105f940d5c 1
195 195
196 196 $ sedi 's/$/a/' a
197 197 $ sedi 's/$/c/' c
198 198
199 $ hg absorb -pq
199 $ hg absorb -apq
200 200 showing changes for a
201 201 @@ -0,2 +0,2 @@
202 202 55105f9 -1
203 203 366daad -2
204 204 55105f9 +1a
205 205 366daad +2a
206 206 showing changes for c
207 207 @@ -0,2 +0,2 @@
208 208 -1
209 209 366daad -3
210 210 +1c
211 211 366daad +3c
212 212
213 213 2 changesets affected
214 214 366daad cp
215 215 55105f9 1
216 216
217 217 $ hg log -G -p -T '{rev}:{node|short} {desc}\n'
218 218 @ 1:70606019f91b cp
219 219 | diff --git a/b b/a
220 220 | rename from b
221 221 | rename to a
222 222 | --- a/b
223 223 | +++ b/a
224 224 | @@ -1,1 +1,2 @@
225 225 | 1a
226 226 | +2a
227 227 | diff --git a/b b/c
228 228 | copy from b
229 229 | copy to c
230 230 | --- a/b
231 231 | +++ b/c
232 232 | @@ -1,1 +1,2 @@
233 233 | -1a
234 234 | +1
235 235 | +3c
236 236 |
237 237 o 0:bfb67c3539c1 1
238 238 diff --git a/b b/b
239 239 new file mode 100644
240 240 --- /dev/null
241 241 +++ b/b
242 242 @@ -0,0 +1,1 @@
243 243 +1a
244 244
245 245 run absorb again would apply the change to c
246 246
247 $ hg absorb -pq
247 $ hg absorb -apq
248 248 showing changes for c
249 249 @@ -0,1 +0,1 @@
250 250 7060601 -1
251 251 7060601 +1c
252 252
253 253 1 changesets affected
254 254 7060601 cp
255 255
256 256 $ hg log -G -p -T '{rev}:{node|short} {desc}\n'
257 257 @ 1:8bd536cce368 cp
258 258 | diff --git a/b b/a
259 259 | rename from b
260 260 | rename to a
261 261 | --- a/b
262 262 | +++ b/a
263 263 | @@ -1,1 +1,2 @@
264 264 | 1a
265 265 | +2a
266 266 | diff --git a/b b/c
267 267 | copy from b
268 268 | copy to c
269 269 | --- a/b
270 270 | +++ b/c
271 271 | @@ -1,1 +1,2 @@
272 272 | -1a
273 273 | +1c
274 274 | +3c
275 275 |
276 276 o 0:bfb67c3539c1 1
277 277 diff --git a/b b/b
278 278 new file mode 100644
279 279 --- /dev/null
280 280 +++ b/b
281 281 @@ -0,0 +1,1 @@
282 282 +1a
283 283
284 284 "move" b to a, c and d, follow d if a gets renamed to e, and c is deleted
285 285
286 286 $ cd ..
287 287 $ hg init repo5
288 288 $ cd repo5
289 289
290 290 $ echo 1 > b
291 291 $ hg ci -A b -m 1
292 292 $ hg cp b a
293 293 $ hg cp b c
294 294 $ hg cp b d
295 295 $ hg rm b
296 296 $ echo 2 >> a
297 297 $ echo 3 >> c
298 298 $ echo 4 >> d
299 299 $ hg commit -m cp
300 300 $ hg mv a e
301 301 $ hg rm c
302 302 $ hg commit -m mv
303 303
304 304 $ hg log -T '{rev}:{node|short} {desc}\n'
305 305 2:49911557c471 mv
306 306 1:7bc3d43ede83 cp
307 307 0:55105f940d5c 1
308 308
309 309 $ sedi 's/$/e/' e
310 310 $ sedi 's/$/d/' d
311 311
312 $ hg absorb -pq
312 $ hg absorb -apq
313 313 showing changes for d
314 314 @@ -0,2 +0,2 @@
315 315 55105f9 -1
316 316 7bc3d43 -4
317 317 55105f9 +1d
318 318 7bc3d43 +4d
319 319 showing changes for e
320 320 @@ -0,2 +0,2 @@
321 321 -1
322 322 7bc3d43 -2
323 323 +1e
324 324 7bc3d43 +2e
325 325
326 326 2 changesets affected
327 327 7bc3d43 cp
328 328 55105f9 1
329 329
330 330 $ hg diff
331 331 diff --git a/e b/e
332 332 --- a/e
333 333 +++ b/e
334 334 @@ -1,2 +1,2 @@
335 335 -1
336 336 +1e
337 337 2e
338 338
339 339 $ hg log -G -p -T '{rev}:{node|short} {desc}\n'
340 340 @ 2:34be9b0c786e mv
341 341 | diff --git a/c b/c
342 342 | deleted file mode 100644
343 343 | --- a/c
344 344 | +++ /dev/null
345 345 | @@ -1,2 +0,0 @@
346 346 | -1
347 347 | -3
348 348 | diff --git a/a b/e
349 349 | rename from a
350 350 | rename to e
351 351 |
352 352 o 1:13e56db5948d cp
353 353 | diff --git a/b b/a
354 354 | rename from b
355 355 | rename to a
356 356 | --- a/b
357 357 | +++ b/a
358 358 | @@ -1,1 +1,2 @@
359 359 | -1d
360 360 | +1
361 361 | +2e
362 362 | diff --git a/b b/c
363 363 | copy from b
364 364 | copy to c
365 365 | --- a/b
366 366 | +++ b/c
367 367 | @@ -1,1 +1,2 @@
368 368 | -1d
369 369 | +1
370 370 | +3
371 371 | diff --git a/b b/d
372 372 | copy from b
373 373 | copy to d
374 374 | --- a/b
375 375 | +++ b/d
376 376 | @@ -1,1 +1,2 @@
377 377 | 1d
378 378 | +4d
379 379 |
380 380 o 0:0037613a5dc6 1
381 381 diff --git a/b b/b
382 382 new file mode 100644
383 383 --- /dev/null
384 384 +++ b/b
385 385 @@ -0,0 +1,1 @@
386 386 +1d
387 387
@@ -1,45 +1,45
1 1 Do not strip innocent children. See https://bitbucket.org/facebook/hg-experimental/issues/6/hg-absorb-merges-diverged-commits
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [extensions]
5 5 > absorb=
6 6 > drawdag=$TESTDIR/drawdag.py
7 7 > EOF
8 8
9 9 $ hg init
10 10 $ hg debugdrawdag << EOF
11 11 > E
12 12 > |
13 13 > D F
14 14 > |/
15 15 > C
16 16 > |
17 17 > B
18 18 > |
19 19 > A
20 20 > EOF
21 21
22 22 $ hg up E -q
23 23 $ echo 1 >> B
24 24 $ echo 2 >> D
25 $ hg absorb
25 $ hg absorb -a
26 26 saved backup bundle to * (glob)
27 27 2 of 2 chunk(s) applied
28 28
29 29 $ hg log -G -T '{desc}'
30 30 @ E
31 31 |
32 32 o D
33 33 |
34 34 o C
35 35 |
36 36 o B
37 37 |
38 38 | o F
39 39 | |
40 40 | o C
41 41 | |
42 42 | o B
43 43 |/
44 44 o A
45 45
@@ -1,476 +1,489
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [extensions]
3 3 > absorb=
4 4 > EOF
5 5
6 6 $ sedi() { # workaround check-code
7 7 > pattern="$1"
8 8 > shift
9 9 > for i in "$@"; do
10 10 > sed "$pattern" "$i" > "$i".tmp
11 11 > mv "$i".tmp "$i"
12 12 > done
13 13 > }
14 14
15 15 $ hg init repo1
16 16 $ cd repo1
17 17
18 18 Do not crash with empty repo:
19 19
20 20 $ hg absorb
21 21 abort: no mutable changeset to change
22 22 [255]
23 23
24 24 Make some commits:
25 25
26 26 $ for i in 1 2 3 4 5; do
27 27 > echo $i >> a
28 28 > hg commit -A a -m "commit $i" -q
29 29 > done
30 30
31 31 $ hg annotate a
32 32 0: 1
33 33 1: 2
34 34 2: 3
35 35 3: 4
36 36 4: 5
37 37
38 38 Change a few lines:
39 39
40 40 $ cat > a <<EOF
41 41 > 1a
42 42 > 2b
43 43 > 3
44 44 > 4d
45 45 > 5e
46 46 > EOF
47 47
48 48 Preview absorb changes:
49 49
50 50 $ hg absorb --print-changes --dry-run
51 51 showing changes for a
52 52 @@ -0,2 +0,2 @@
53 53 4ec16f8 -1
54 54 5c5f952 -2
55 55 4ec16f8 +1a
56 56 5c5f952 +2b
57 57 @@ -3,2 +3,2 @@
58 58 ad8b8b7 -4
59 59 4f55fa6 -5
60 60 ad8b8b7 +4d
61 61 4f55fa6 +5e
62 62
63 63 4 changesets affected
64 64 4f55fa6 commit 5
65 65 ad8b8b7 commit 4
66 66 5c5f952 commit 2
67 67 4ec16f8 commit 1
68 68
69 69 Run absorb:
70 70
71 $ hg absorb
71 $ hg absorb --apply-changes
72 72 saved backup bundle to * (glob)
73 73 2 of 2 chunk(s) applied
74 74 $ hg annotate a
75 75 0: 1a
76 76 1: 2b
77 77 2: 3
78 78 3: 4d
79 79 4: 5e
80 80
81 81 Delete a few lines and related commits will be removed if they will be empty:
82 82
83 83 $ cat > a <<EOF
84 84 > 2b
85 85 > 4d
86 86 > EOF
87 $ hg absorb
87 $ echo y | hg absorb --config ui.interactive=1
88 showing changes for a
89 @@ -0,1 +0,0 @@
90 f548282 -1a
91 @@ -2,1 +1,0 @@
92 ff5d556 -3
93 @@ -4,1 +2,0 @@
94 84e5416 -5e
95
96 3 changesets affected
97 84e5416 commit 5
98 ff5d556 commit 3
99 f548282 commit 1
100 apply changes (yn)? y
88 101 saved backup bundle to * (glob)
89 102 3 of 3 chunk(s) applied
90 103 $ hg annotate a
91 104 1: 2b
92 105 2: 4d
93 106 $ hg log -T '{rev} {desc}\n' -Gp
94 107 @ 2 commit 4
95 108 | diff -r 1cae118c7ed8 -r 58a62bade1c6 a
96 109 | --- a/a Thu Jan 01 00:00:00 1970 +0000
97 110 | +++ b/a Thu Jan 01 00:00:00 1970 +0000
98 111 | @@ -1,1 +1,2 @@
99 112 | 2b
100 113 | +4d
101 114 |
102 115 o 1 commit 2
103 116 | diff -r 84add69aeac0 -r 1cae118c7ed8 a
104 117 | --- a/a Thu Jan 01 00:00:00 1970 +0000
105 118 | +++ b/a Thu Jan 01 00:00:00 1970 +0000
106 119 | @@ -0,0 +1,1 @@
107 120 | +2b
108 121 |
109 122 o 0 commit 1
110 123
111 124
112 125 Non 1:1 map changes will be ignored:
113 126
114 127 $ echo 1 > a
115 $ hg absorb
128 $ hg absorb --apply-changes
116 129 nothing applied
117 130 [1]
118 131
119 132 Insertaions:
120 133
121 134 $ cat > a << EOF
122 135 > insert before 2b
123 136 > 2b
124 137 > 4d
125 138 > insert aftert 4d
126 139 > EOF
127 $ hg absorb -q
140 $ hg absorb -q --apply-changes
128 141 $ hg status
129 142 $ hg annotate a
130 143 1: insert before 2b
131 144 1: 2b
132 145 2: 4d
133 146 2: insert aftert 4d
134 147
135 148 Bookmarks are moved:
136 149
137 150 $ hg bookmark -r 1 b1
138 151 $ hg bookmark -r 2 b2
139 152 $ hg bookmark ba
140 153 $ hg bookmarks
141 154 b1 1:b35060a57a50
142 155 b2 2:946e4bc87915
143 156 * ba 2:946e4bc87915
144 157 $ sedi 's/insert/INSERT/' a
145 $ hg absorb -q
158 $ hg absorb -q --apply-changes
146 159 $ hg status
147 160 $ hg bookmarks
148 161 b1 1:a4183e9b3d31
149 162 b2 2:c9b20c925790
150 163 * ba 2:c9b20c925790
151 164
152 165 Non-mofified files are ignored:
153 166
154 167 $ touch b
155 168 $ hg commit -A b -m b
156 169 $ touch c
157 170 $ hg add c
158 171 $ hg rm b
159 $ hg absorb
172 $ hg absorb --apply-changes
160 173 nothing applied
161 174 [1]
162 175 $ sedi 's/INSERT/Insert/' a
163 $ hg absorb
176 $ hg absorb --apply-changes
164 177 saved backup bundle to * (glob)
165 178 2 of 2 chunk(s) applied
166 179 $ hg status
167 180 A c
168 181 R b
169 182
170 183 Public commits will not be changed:
171 184
172 185 $ hg phase -p 1
173 186 $ sedi 's/Insert/insert/' a
174 187 $ hg absorb -pn
175 188 showing changes for a
176 189 @@ -0,1 +0,1 @@
177 190 -Insert before 2b
178 191 +insert before 2b
179 192 @@ -3,1 +3,1 @@
180 193 85b4e0e -Insert aftert 4d
181 194 85b4e0e +insert aftert 4d
182 195
183 196 1 changesets affected
184 197 85b4e0e commit 4
185 $ hg absorb
198 $ hg absorb --apply-changes
186 199 saved backup bundle to * (glob)
187 200 1 of 2 chunk(s) applied
188 201 $ hg diff -U 0
189 202 diff -r 1c8eadede62a a
190 203 --- a/a Thu Jan 01 00:00:00 1970 +0000
191 204 +++ b/a * (glob)
192 205 @@ -1,1 +1,1 @@
193 206 -Insert before 2b
194 207 +insert before 2b
195 208 $ hg annotate a
196 209 1: Insert before 2b
197 210 1: 2b
198 211 2: 4d
199 212 2: insert aftert 4d
200 213
201 214 Make working copy clean:
202 215
203 216 $ hg revert -q -C a b
204 217 $ hg forget c
205 218 $ rm c
206 219 $ hg status
207 220
208 221 Merge commit will not be changed:
209 222
210 223 $ echo 1 > m1
211 224 $ hg commit -A m1 -m m1
212 225 $ hg bookmark -q -i m1
213 226 $ hg update -q '.^'
214 227 $ echo 2 > m2
215 228 $ hg commit -q -A m2 -m m2
216 229 $ hg merge -q m1
217 230 $ hg commit -m merge
218 231 $ hg bookmark -d m1
219 232 $ hg log -G -T '{rev} {desc} {phase}\n'
220 233 @ 6 merge draft
221 234 |\
222 235 | o 5 m2 draft
223 236 | |
224 237 o | 4 m1 draft
225 238 |/
226 239 o 3 b draft
227 240 |
228 241 o 2 commit 4 draft
229 242 |
230 243 o 1 commit 2 public
231 244 |
232 245 o 0 commit 1 public
233 246
234 247 $ echo 2 >> m1
235 248 $ echo 2 >> m2
236 $ hg absorb
249 $ hg absorb --apply-changes
237 250 abort: no mutable changeset to change
238 251 [255]
239 252 $ hg revert -q -C m1 m2
240 253
241 254 Use a new repo:
242 255
243 256 $ cd ..
244 257 $ hg init repo2
245 258 $ cd repo2
246 259
247 260 Make some commits to multiple files:
248 261
249 262 $ for f in a b; do
250 263 > for i in 1 2; do
251 264 > echo $f line $i >> $f
252 265 > hg commit -A $f -m "commit $f $i" -q
253 266 > done
254 267 > done
255 268
256 269 Use pattern to select files to be fixed up:
257 270
258 271 $ sedi 's/line/Line/' a b
259 272 $ hg status
260 273 M a
261 274 M b
262 $ hg absorb a
275 $ hg absorb --apply-changes a
263 276 saved backup bundle to * (glob)
264 277 1 of 1 chunk(s) applied
265 278 $ hg status
266 279 M b
267 $ hg absorb --exclude b
280 $ hg absorb --apply-changes --exclude b
268 281 nothing applied
269 282 [1]
270 $ hg absorb b
283 $ hg absorb --apply-changes b
271 284 saved backup bundle to * (glob)
272 285 1 of 1 chunk(s) applied
273 286 $ hg status
274 287 $ cat a b
275 288 a Line 1
276 289 a Line 2
277 290 b Line 1
278 291 b Line 2
279 292
280 293 Test config option absorb.max-stack-size:
281 294
282 295 $ sedi 's/Line/line/' a b
283 296 $ hg log -T '{rev}:{node} {desc}\n'
284 297 3:712d16a8f445834e36145408eabc1d29df05ec09 commit b 2
285 298 2:74cfa6294160149d60adbf7582b99ce37a4597ec commit b 1
286 299 1:28f10dcf96158f84985358a2e5d5b3505ca69c22 commit a 2
287 300 0:f9a81da8dc53380ed91902e5b82c1b36255a4bd0 commit a 1
288 301 $ hg --config absorb.max-stack-size=1 absorb -pn
289 302 absorb: only the recent 1 changesets will be analysed
290 303 showing changes for a
291 304 @@ -0,2 +0,2 @@
292 305 -a Line 1
293 306 -a Line 2
294 307 +a line 1
295 308 +a line 2
296 309 showing changes for b
297 310 @@ -0,2 +0,2 @@
298 311 -b Line 1
299 312 712d16a -b Line 2
300 313 +b line 1
301 314 712d16a +b line 2
302 315
303 316 1 changesets affected
304 317 712d16a commit b 2
305 318
306 319 Test obsolete markers creation:
307 320
308 321 $ cat >> $HGRCPATH << EOF
309 322 > [experimental]
310 323 > evolution=createmarkers
311 324 > [absorb]
312 325 > add-noise=1
313 326 > EOF
314 327
315 $ hg --config absorb.max-stack-size=3 absorb
328 $ hg --config absorb.max-stack-size=3 absorb -a
316 329 absorb: only the recent 3 changesets will be analysed
317 330 2 of 2 chunk(s) applied
318 331 $ hg log -T '{rev}:{node|short} {desc} {get(extras, "absorb_source")}\n'
319 332 6:3dfde4199b46 commit b 2 712d16a8f445834e36145408eabc1d29df05ec09
320 333 5:99cfab7da5ff commit b 1 74cfa6294160149d60adbf7582b99ce37a4597ec
321 334 4:fec2b3bd9e08 commit a 2 28f10dcf96158f84985358a2e5d5b3505ca69c22
322 335 0:f9a81da8dc53 commit a 1
323 $ hg absorb
336 $ hg absorb --apply-changes
324 337 1 of 1 chunk(s) applied
325 338 $ hg log -T '{rev}:{node|short} {desc} {get(extras, "absorb_source")}\n'
326 339 10:e1c8c1e030a4 commit b 2 3dfde4199b4610ea6e3c6fa9f5bdad8939d69524
327 340 9:816c30955758 commit b 1 99cfab7da5ffdaf3b9fc6643b14333e194d87f46
328 341 8:5867d584106b commit a 2 fec2b3bd9e0834b7cb6a564348a0058171aed811
329 342 7:8c76602baf10 commit a 1 f9a81da8dc53380ed91902e5b82c1b36255a4bd0
330 343
331 344 Executable files:
332 345
333 346 $ cat >> $HGRCPATH << EOF
334 347 > [diff]
335 348 > git=True
336 349 > EOF
337 350 $ cd ..
338 351 $ hg init repo3
339 352 $ cd repo3
340 353
341 354 #if execbit
342 355 $ echo > foo.py
343 356 $ chmod +x foo.py
344 357 $ hg add foo.py
345 358 $ hg commit -mfoo
346 359 #else
347 360 $ hg import -q --bypass - <<EOF
348 361 > # HG changeset patch
349 362 > foo
350 363 >
351 364 > diff --git a/foo.py b/foo.py
352 365 > new file mode 100755
353 366 > --- /dev/null
354 367 > +++ b/foo.py
355 368 > @@ -0,0 +1,1 @@
356 369 > +
357 370 > EOF
358 371 $ hg up -q
359 372 #endif
360 373
361 374 $ echo bla > foo.py
362 375 $ hg absorb --dry-run --print-changes
363 376 showing changes for foo.py
364 377 @@ -0,1 +0,1 @@
365 378 99b4ae7 -
366 379 99b4ae7 +bla
367 380
368 381 1 changesets affected
369 382 99b4ae7 foo
370 $ hg absorb
383 $ hg absorb --apply-changes
371 384 1 of 1 chunk(s) applied
372 385 $ hg diff -c .
373 386 diff --git a/foo.py b/foo.py
374 387 new file mode 100755
375 388 --- /dev/null
376 389 +++ b/foo.py
377 390 @@ -0,0 +1,1 @@
378 391 +bla
379 392 $ hg diff
380 393
381 394 Remove lines may delete changesets:
382 395
383 396 $ cd ..
384 397 $ hg init repo4
385 398 $ cd repo4
386 399 $ cat > a <<EOF
387 400 > 1
388 401 > 2
389 402 > EOF
390 403 $ hg commit -m a12 -A a
391 404 $ cat > b <<EOF
392 405 > 1
393 406 > 2
394 407 > EOF
395 408 $ hg commit -m b12 -A b
396 409 $ echo 3 >> b
397 410 $ hg commit -m b3
398 411 $ echo 4 >> b
399 412 $ hg commit -m b4
400 413 $ echo 1 > b
401 414 $ echo 3 >> a
402 415 $ hg absorb -pn
403 416 showing changes for a
404 417 @@ -2,0 +2,1 @@
405 418 bfafb49 +3
406 419 showing changes for b
407 420 @@ -1,3 +1,0 @@
408 421 1154859 -2
409 422 30970db -3
410 423 a393a58 -4
411 424
412 425 4 changesets affected
413 426 a393a58 b4
414 427 30970db b3
415 428 1154859 b12
416 429 bfafb49 a12
417 $ hg absorb -v | grep became
430 $ hg absorb -av | grep became
418 431 0:bfafb49242db: 1 file(s) changed, became 4:1a2de97fc652
419 432 1:115485984805: 2 file(s) changed, became 5:0c930dfab74c
420 433 2:30970dbf7b40: became empty and was dropped
421 434 3:a393a58b9a85: became empty and was dropped
422 435 $ hg log -T '{rev} {desc}\n' -Gp
423 436 @ 5 b12
424 437 | diff --git a/b b/b
425 438 | new file mode 100644
426 439 | --- /dev/null
427 440 | +++ b/b
428 441 | @@ -0,0 +1,1 @@
429 442 | +1
430 443 |
431 444 o 4 a12
432 445 diff --git a/a b/a
433 446 new file mode 100644
434 447 --- /dev/null
435 448 +++ b/a
436 449 @@ -0,0 +1,3 @@
437 450 +1
438 451 +2
439 452 +3
440 453
441 454
442 455 Use revert to make the current change and its parent disappear.
443 456 This should move us to the non-obsolete ancestor.
444 457
445 458 $ cd ..
446 459 $ hg init repo5
447 460 $ cd repo5
448 461 $ cat > a <<EOF
449 462 > 1
450 463 > 2
451 464 > EOF
452 465 $ hg commit -m a12 -A a
453 466 $ hg id
454 467 bfafb49242db tip
455 468 $ echo 3 >> a
456 469 $ hg commit -m a123 a
457 470 $ echo 4 >> a
458 471 $ hg commit -m a1234 a
459 472 $ hg id
460 473 82dbe7fd19f0 tip
461 474 $ hg revert -r 0 a
462 475 $ hg absorb -pn
463 476 showing changes for a
464 477 @@ -2,2 +2,0 @@
465 478 f1c23dd -3
466 479 82dbe7f -4
467 480
468 481 2 changesets affected
469 482 82dbe7f a1234
470 483 f1c23dd a123
471 $ hg absorb --verbose
484 $ hg absorb --apply-changes --verbose
472 485 1:f1c23dd5d08d: became empty and was dropped
473 486 2:82dbe7fd19f0: became empty and was dropped
474 487 a: 1 of 1 chunk(s) applied
475 488 $ hg id
476 489 bfafb49242db tip
General Comments 0
You need to be logged in to leave comments. Login now