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