##// END OF EJS Templates
absorb: update commit hash references in the new commits...
Matt Harbison -
r46304:0a330055 default
parent child Browse files
Show More
@@ -1,1147 +1,1159 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 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
247 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None, desc=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 if desc is None:
257 desc = ctx.description()
256 258 date = ctx.date()
257 desc = ctx.description()
258 259 user = ctx.user()
259 260 files = set(ctx.files()).union(memworkingcopy)
260 261 store = overlaystore(ctx, memworkingcopy)
261 262 return context.memctx(
262 263 repo=ctx.repo(),
263 264 parents=parents,
264 265 text=desc,
265 266 files=files,
266 267 filectxfn=store,
267 268 user=user,
268 269 date=date,
269 270 branch=None,
270 271 extra=extra,
271 272 )
272 273
273 274
274 275 class filefixupstate(object):
275 276 """state needed to apply fixups to a single file
276 277
277 278 internally, it keeps file contents of several revisions and a linelog.
278 279
279 280 the linelog uses odd revision numbers for original contents (fctxs passed
280 281 to __init__), and even revision numbers for fixups, like:
281 282
282 283 linelog rev 1: self.fctxs[0] (from an immutable "public" changeset)
283 284 linelog rev 2: fixups made to self.fctxs[0]
284 285 linelog rev 3: self.fctxs[1] (a child of fctxs[0])
285 286 linelog rev 4: fixups made to self.fctxs[1]
286 287 ...
287 288
288 289 a typical use is like:
289 290
290 291 1. call diffwith, to calculate self.fixups
291 292 2. (optionally), present self.fixups to the user, or change it
292 293 3. call apply, to apply changes
293 294 4. read results from "finalcontents", or call getfinalcontent
294 295 """
295 296
296 297 def __init__(self, fctxs, path, ui=None, opts=None):
297 298 """([fctx], ui or None) -> None
298 299
299 300 fctxs should be linear, and sorted by topo order - oldest first.
300 301 fctxs[0] will be considered as "immutable" and will not be changed.
301 302 """
302 303 self.fctxs = fctxs
303 304 self.path = path
304 305 self.ui = ui or nullui()
305 306 self.opts = opts or {}
306 307
307 308 # following fields are built from fctxs. they exist for perf reason
308 309 self.contents = [f.data() for f in fctxs]
309 310 self.contentlines = pycompat.maplist(mdiff.splitnewlines, self.contents)
310 311 self.linelog = self._buildlinelog()
311 312 if self.ui.debugflag:
312 313 assert self._checkoutlinelog() == self.contents
313 314
314 315 # following fields will be filled later
315 316 self.chunkstats = [0, 0] # [adopted, total : int]
316 317 self.targetlines = [] # [str]
317 318 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
318 319 self.finalcontents = [] # [str]
319 320 self.ctxaffected = set()
320 321
321 322 def diffwith(self, targetfctx, fm=None):
322 323 """calculate fixups needed by examining the differences between
323 324 self.fctxs[-1] and targetfctx, chunk by chunk.
324 325
325 326 targetfctx is the target state we move towards. we may or may not be
326 327 able to get there because not all modified chunks can be amended into
327 328 a non-public fctx unambiguously.
328 329
329 330 call this only once, before apply().
330 331
331 332 update self.fixups, self.chunkstats, and self.targetlines.
332 333 """
333 334 a = self.contents[-1]
334 335 alines = self.contentlines[-1]
335 336 b = targetfctx.data()
336 337 blines = mdiff.splitnewlines(b)
337 338 self.targetlines = blines
338 339
339 340 self.linelog.annotate(self.linelog.maxrev)
340 341 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
341 342 assert len(annotated) == len(alines)
342 343 # add a dummy end line to make insertion at the end easier
343 344 if annotated:
344 345 dummyendline = (annotated[-1][0], annotated[-1][1] + 1)
345 346 annotated.append(dummyendline)
346 347
347 348 # analyse diff blocks
348 349 for chunk in self._alldiffchunks(a, b, alines, blines):
349 350 newfixups = self._analysediffchunk(chunk, annotated)
350 351 self.chunkstats[0] += bool(newfixups) # 1 or 0
351 352 self.chunkstats[1] += 1
352 353 self.fixups += newfixups
353 354 if fm is not None:
354 355 self._showchanges(fm, alines, blines, chunk, newfixups)
355 356
356 357 def apply(self):
357 358 """apply self.fixups. update self.linelog, self.finalcontents.
358 359
359 360 call this only once, before getfinalcontent(), after diffwith().
360 361 """
361 362 # the following is unnecessary, as it's done by "diffwith":
362 363 # self.linelog.annotate(self.linelog.maxrev)
363 364 for rev, a1, a2, b1, b2 in reversed(self.fixups):
364 365 blines = self.targetlines[b1:b2]
365 366 if self.ui.debugflag:
366 367 idx = (max(rev - 1, 0)) // 2
367 368 self.ui.write(
368 369 _(b'%s: chunk %d:%d -> %d lines\n')
369 370 % (node.short(self.fctxs[idx].node()), a1, a2, len(blines))
370 371 )
371 372 self.linelog.replacelines(rev, a1, a2, b1, b2)
372 373 if self.opts.get(b'edit_lines', False):
373 374 self.finalcontents = self._checkoutlinelogwithedits()
374 375 else:
375 376 self.finalcontents = self._checkoutlinelog()
376 377
377 378 def getfinalcontent(self, fctx):
378 379 """(fctx) -> str. get modified file content for a given filecontext"""
379 380 idx = self.fctxs.index(fctx)
380 381 return self.finalcontents[idx]
381 382
382 383 def _analysediffchunk(self, chunk, annotated):
383 384 """analyse a different chunk and return new fixups found
384 385
385 386 return [] if no lines from the chunk can be safely applied.
386 387
387 388 the chunk (or lines) cannot be safely applied, if, for example:
388 389 - the modified (deleted) lines belong to a public changeset
389 390 (self.fctxs[0])
390 391 - the chunk is a pure insertion and the adjacent lines (at most 2
391 392 lines) belong to different non-public changesets, or do not belong
392 393 to any non-public changesets.
393 394 - the chunk is modifying lines from different changesets.
394 395 in this case, if the number of lines deleted equals to the number
395 396 of lines added, assume it's a simple 1:1 map (could be wrong).
396 397 otherwise, give up.
397 398 - the chunk is modifying lines from a single non-public changeset,
398 399 but other revisions touch the area as well. i.e. the lines are
399 400 not continuous as seen from the linelog.
400 401 """
401 402 a1, a2, b1, b2 = chunk
402 403 # find involved indexes from annotate result
403 404 involved = annotated[a1:a2]
404 405 if not involved and annotated: # a1 == a2 and a is not empty
405 406 # pure insertion, check nearby lines. ignore lines belong
406 407 # to the public (first) changeset (i.e. annotated[i][0] == 1)
407 408 nearbylinenums = {a2, max(0, a1 - 1)}
408 409 involved = [
409 410 annotated[i] for i in nearbylinenums if annotated[i][0] != 1
410 411 ]
411 412 involvedrevs = list({r for r, l in involved})
412 413 newfixups = []
413 414 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
414 415 # chunk belongs to a single revision
415 416 rev = involvedrevs[0]
416 417 if rev > 1:
417 418 fixuprev = rev + 1
418 419 newfixups.append((fixuprev, a1, a2, b1, b2))
419 420 elif a2 - a1 == b2 - b1 or b1 == b2:
420 421 # 1:1 line mapping, or chunk was deleted
421 422 for i in pycompat.xrange(a1, a2):
422 423 rev, linenum = annotated[i]
423 424 if rev > 1:
424 425 if b1 == b2: # deletion, simply remove that single line
425 426 nb1 = nb2 = 0
426 427 else: # 1:1 line mapping, change the corresponding rev
427 428 nb1 = b1 + i - a1
428 429 nb2 = nb1 + 1
429 430 fixuprev = rev + 1
430 431 newfixups.append((fixuprev, i, i + 1, nb1, nb2))
431 432 return self._optimizefixups(newfixups)
432 433
433 434 @staticmethod
434 435 def _alldiffchunks(a, b, alines, blines):
435 436 """like mdiff.allblocks, but only care about differences"""
436 437 blocks = mdiff.allblocks(a, b, lines1=alines, lines2=blines)
437 438 for chunk, btype in blocks:
438 439 if btype != b'!':
439 440 continue
440 441 yield chunk
441 442
442 443 def _buildlinelog(self):
443 444 """calculate the initial linelog based on self.content{,line}s.
444 445 this is similar to running a partial "annotate".
445 446 """
446 447 llog = linelog.linelog()
447 448 a, alines = b'', []
448 449 for i in pycompat.xrange(len(self.contents)):
449 450 b, blines = self.contents[i], self.contentlines[i]
450 451 llrev = i * 2 + 1
451 452 chunks = self._alldiffchunks(a, b, alines, blines)
452 453 for a1, a2, b1, b2 in reversed(list(chunks)):
453 454 llog.replacelines(llrev, a1, a2, b1, b2)
454 455 a, alines = b, blines
455 456 return llog
456 457
457 458 def _checkoutlinelog(self):
458 459 """() -> [str]. check out file contents from linelog"""
459 460 contents = []
460 461 for i in pycompat.xrange(len(self.contents)):
461 462 rev = (i + 1) * 2
462 463 self.linelog.annotate(rev)
463 464 content = b''.join(map(self._getline, self.linelog.annotateresult))
464 465 contents.append(content)
465 466 return contents
466 467
467 468 def _checkoutlinelogwithedits(self):
468 469 """() -> [str]. prompt all lines for edit"""
469 470 alllines = self.linelog.getalllines()
470 471 # header
471 472 editortext = (
472 473 _(
473 474 b'HG: editing %s\nHG: "y" means the line to the right '
474 475 b'exists in the changeset to the top\nHG:\n'
475 476 )
476 477 % self.fctxs[-1].path()
477 478 )
478 479 # [(idx, fctx)]. hide the dummy emptyfilecontext
479 480 visiblefctxs = [
480 481 (i, f)
481 482 for i, f in enumerate(self.fctxs)
482 483 if not isinstance(f, emptyfilecontext)
483 484 ]
484 485 for i, (j, f) in enumerate(visiblefctxs):
485 486 editortext += _(b'HG: %s/%s %s %s\n') % (
486 487 b'|' * i,
487 488 b'-' * (len(visiblefctxs) - i + 1),
488 489 node.short(f.node()),
489 490 f.description().split(b'\n', 1)[0],
490 491 )
491 492 editortext += _(b'HG: %s\n') % (b'|' * len(visiblefctxs))
492 493 # figure out the lifetime of a line, this is relatively inefficient,
493 494 # but probably fine
494 495 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
495 496 for i, f in visiblefctxs:
496 497 self.linelog.annotate((i + 1) * 2)
497 498 for l in self.linelog.annotateresult:
498 499 lineset[l].add(i)
499 500 # append lines
500 501 for l in alllines:
501 502 editortext += b' %s : %s' % (
502 503 b''.join(
503 504 [
504 505 (b'y' if i in lineset[l] else b' ')
505 506 for i, _f in visiblefctxs
506 507 ]
507 508 ),
508 509 self._getline(l),
509 510 )
510 511 # run editor
511 512 editedtext = self.ui.edit(editortext, b'', action=b'absorb')
512 513 if not editedtext:
513 514 raise error.Abort(_(b'empty editor text'))
514 515 # parse edited result
515 516 contents = [b''] * len(self.fctxs)
516 517 leftpadpos = 4
517 518 colonpos = leftpadpos + len(visiblefctxs) + 1
518 519 for l in mdiff.splitnewlines(editedtext):
519 520 if l.startswith(b'HG:'):
520 521 continue
521 522 if l[colonpos - 1 : colonpos + 2] != b' : ':
522 523 raise error.Abort(_(b'malformed line: %s') % l)
523 524 linecontent = l[colonpos + 2 :]
524 525 for i, ch in enumerate(
525 526 pycompat.bytestr(l[leftpadpos : colonpos - 1])
526 527 ):
527 528 if ch == b'y':
528 529 contents[visiblefctxs[i][0]] += linecontent
529 530 # chunkstats is hard to calculate if anything changes, therefore
530 531 # set them to just a simple value (1, 1).
531 532 if editedtext != editortext:
532 533 self.chunkstats = [1, 1]
533 534 return contents
534 535
535 536 def _getline(self, lineinfo):
536 537 """((rev, linenum)) -> str. convert rev+line number to line content"""
537 538 rev, linenum = lineinfo
538 539 if rev & 1: # odd: original line taken from fctxs
539 540 return self.contentlines[rev // 2][linenum]
540 541 else: # even: fixup line from targetfctx
541 542 return self.targetlines[linenum]
542 543
543 544 def _iscontinuous(self, a1, a2, closedinterval=False):
544 545 """(a1, a2 : int) -> bool
545 546
546 547 check if these lines are continuous. i.e. no other insertions or
547 548 deletions (from other revisions) among these lines.
548 549
549 550 closedinterval decides whether a2 should be included or not. i.e. is
550 551 it [a1, a2), or [a1, a2] ?
551 552 """
552 553 if a1 >= a2:
553 554 return True
554 555 llog = self.linelog
555 556 offset1 = llog.getoffset(a1)
556 557 offset2 = llog.getoffset(a2) + int(closedinterval)
557 558 linesinbetween = llog.getalllines(offset1, offset2)
558 559 return len(linesinbetween) == a2 - a1 + int(closedinterval)
559 560
560 561 def _optimizefixups(self, fixups):
561 562 """[(rev, a1, a2, b1, b2)] -> [(rev, a1, a2, b1, b2)].
562 563 merge adjacent fixups to make them less fragmented.
563 564 """
564 565 result = []
565 566 pcurrentchunk = [[-1, -1, -1, -1, -1]]
566 567
567 568 def pushchunk():
568 569 if pcurrentchunk[0][0] != -1:
569 570 result.append(tuple(pcurrentchunk[0]))
570 571
571 572 for i, chunk in enumerate(fixups):
572 573 rev, a1, a2, b1, b2 = chunk
573 574 lastrev = pcurrentchunk[0][0]
574 575 lasta2 = pcurrentchunk[0][2]
575 576 lastb2 = pcurrentchunk[0][4]
576 577 if (
577 578 a1 == lasta2
578 579 and b1 == lastb2
579 580 and rev == lastrev
580 581 and self._iscontinuous(max(a1 - 1, 0), a1)
581 582 ):
582 583 # merge into currentchunk
583 584 pcurrentchunk[0][2] = a2
584 585 pcurrentchunk[0][4] = b2
585 586 else:
586 587 pushchunk()
587 588 pcurrentchunk[0] = list(chunk)
588 589 pushchunk()
589 590 return result
590 591
591 592 def _showchanges(self, fm, alines, blines, chunk, fixups):
592 593 def trim(line):
593 594 if line.endswith(b'\n'):
594 595 line = line[:-1]
595 596 return line
596 597
597 598 # this is not optimized for perf but _showchanges only gets executed
598 599 # with an extra command-line flag.
599 600 a1, a2, b1, b2 = chunk
600 601 aidxs, bidxs = [0] * (a2 - a1), [0] * (b2 - b1)
601 602 for idx, fa1, fa2, fb1, fb2 in fixups:
602 603 for i in pycompat.xrange(fa1, fa2):
603 604 aidxs[i - a1] = (max(idx, 1) - 1) // 2
604 605 for i in pycompat.xrange(fb1, fb2):
605 606 bidxs[i - b1] = (max(idx, 1) - 1) // 2
606 607
607 608 fm.startitem()
608 609 fm.write(
609 610 b'hunk',
610 611 b' %s\n',
611 612 b'@@ -%d,%d +%d,%d @@' % (a1, a2 - a1, b1, b2 - b1),
612 613 label=b'diff.hunk',
613 614 )
614 615 fm.data(path=self.path, linetype=b'hunk')
615 616
616 617 def writeline(idx, diffchar, line, linetype, linelabel):
617 618 fm.startitem()
618 619 node = b''
619 620 if idx:
620 621 ctx = self.fctxs[idx]
621 622 fm.context(fctx=ctx)
622 623 node = ctx.hex()
623 624 self.ctxaffected.add(ctx.changectx())
624 625 fm.write(b'node', b'%-7.7s ', node, label=b'absorb.node')
625 626 fm.write(
626 627 b'diffchar ' + linetype,
627 628 b'%s%s\n',
628 629 diffchar,
629 630 line,
630 631 label=linelabel,
631 632 )
632 633 fm.data(path=self.path, linetype=linetype)
633 634
634 635 for i in pycompat.xrange(a1, a2):
635 636 writeline(
636 637 aidxs[i - a1],
637 638 b'-',
638 639 trim(alines[i]),
639 640 b'deleted',
640 641 b'diff.deleted',
641 642 )
642 643 for i in pycompat.xrange(b1, b2):
643 644 writeline(
644 645 bidxs[i - b1],
645 646 b'+',
646 647 trim(blines[i]),
647 648 b'inserted',
648 649 b'diff.inserted',
649 650 )
650 651
651 652
652 653 class fixupstate(object):
653 654 """state needed to run absorb
654 655
655 656 internally, it keeps paths and filefixupstates.
656 657
657 658 a typical use is like filefixupstates:
658 659
659 660 1. call diffwith, to calculate fixups
660 661 2. (optionally), present fixups to the user, or edit fixups
661 662 3. call apply, to apply changes to memory
662 663 4. call commit, to commit changes to hg database
663 664 """
664 665
665 666 def __init__(self, stack, ui=None, opts=None):
666 667 """([ctx], ui or None) -> None
667 668
668 669 stack: should be linear, and sorted by topo order - oldest first.
669 670 all commits in stack are considered mutable.
670 671 """
671 672 assert stack
672 673 self.ui = ui or nullui()
673 674 self.opts = opts or {}
674 675 self.stack = stack
675 676 self.repo = stack[-1].repo().unfiltered()
676 677
677 678 # following fields will be filled later
678 679 self.paths = [] # [str]
679 680 self.status = None # ctx.status output
680 681 self.fctxmap = {} # {path: {ctx: fctx}}
681 682 self.fixupmap = {} # {path: filefixupstate}
682 683 self.replacemap = {} # {oldnode: newnode or None}
683 684 self.finalnode = None # head after all fixups
684 685 self.ctxaffected = set() # ctx that will be absorbed into
685 686
686 687 def diffwith(self, targetctx, match=None, fm=None):
687 688 """diff and prepare fixups. update self.fixupmap, self.paths"""
688 689 # only care about modified files
689 690 self.status = self.stack[-1].status(targetctx, match)
690 691 self.paths = []
691 692 # but if --edit-lines is used, the user may want to edit files
692 693 # even if they are not modified
693 694 editopt = self.opts.get(b'edit_lines')
694 695 if not self.status.modified and editopt and match:
695 696 interestingpaths = match.files()
696 697 else:
697 698 interestingpaths = self.status.modified
698 699 # prepare the filefixupstate
699 700 seenfctxs = set()
700 701 # sorting is necessary to eliminate ambiguity for the "double move"
701 702 # case: "hg cp A B; hg cp A C; hg rm A", then only "B" can affect "A".
702 703 for path in sorted(interestingpaths):
703 704 self.ui.debug(b'calculating fixups for %s\n' % path)
704 705 targetfctx = targetctx[path]
705 706 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
706 707 # ignore symbolic links or binary, or unchanged files
707 708 if any(
708 709 f.islink() or stringutil.binary(f.data())
709 710 for f in [targetfctx] + fctxs
710 711 if not isinstance(f, emptyfilecontext)
711 712 ):
712 713 continue
713 714 if targetfctx.data() == fctxs[-1].data() and not editopt:
714 715 continue
715 716 seenfctxs.update(fctxs[1:])
716 717 self.fctxmap[path] = ctx2fctx
717 718 fstate = filefixupstate(fctxs, path, ui=self.ui, opts=self.opts)
718 719 if fm is not None:
719 720 fm.startitem()
720 721 fm.plain(b'showing changes for ')
721 722 fm.write(b'path', b'%s\n', path, label=b'absorb.path')
722 723 fm.data(linetype=b'path')
723 724 fstate.diffwith(targetfctx, fm)
724 725 self.fixupmap[path] = fstate
725 726 self.paths.append(path)
726 727 self.ctxaffected.update(fstate.ctxaffected)
727 728
728 729 def apply(self):
729 730 """apply fixups to individual filefixupstates"""
730 731 for path, state in pycompat.iteritems(self.fixupmap):
731 732 if self.ui.debugflag:
732 733 self.ui.write(_(b'applying fixups to %s\n') % path)
733 734 state.apply()
734 735
735 736 @property
736 737 def chunkstats(self):
737 738 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
738 739 return {
739 740 path: state.chunkstats
740 741 for path, state in pycompat.iteritems(self.fixupmap)
741 742 }
742 743
743 744 def commit(self):
744 745 """commit changes. update self.finalnode, self.replacemap"""
745 746 with self.repo.transaction(b'absorb') as tr:
746 747 self._commitstack()
747 748 self._movebookmarks(tr)
748 749 if self.repo[b'.'].node() in self.replacemap:
749 750 self._moveworkingdirectoryparent()
750 751 self._cleanupoldcommits()
751 752 return self.finalnode
752 753
753 754 def printchunkstats(self):
754 755 """print things like '1 of 2 chunk(s) applied'"""
755 756 ui = self.ui
756 757 chunkstats = self.chunkstats
757 758 if ui.verbose:
758 759 # chunkstats for each file
759 760 for path, stat in pycompat.iteritems(chunkstats):
760 761 if stat[0]:
761 762 ui.write(
762 763 _(b'%s: %d of %d chunk(s) applied\n')
763 764 % (path, stat[0], stat[1])
764 765 )
765 766 elif not ui.quiet:
766 767 # a summary for all files
767 768 stats = chunkstats.values()
768 769 applied, total = (sum(s[i] for s in stats) for i in (0, 1))
769 770 ui.write(_(b'%d of %d chunk(s) applied\n') % (applied, total))
770 771
771 772 def _commitstack(self):
772 773 """make new commits. update self.finalnode, self.replacemap.
773 774 it is splitted from "commit" to avoid too much indentation.
774 775 """
775 776 # last node (20-char) committed by us
776 777 lastcommitted = None
777 778 # p1 which overrides the parent of the next commit, "None" means use
778 779 # the original parent unchanged
779 780 nextp1 = None
780 781 for ctx in self.stack:
781 782 memworkingcopy = self._getnewfilecontents(ctx)
782 783 if not memworkingcopy and not lastcommitted:
783 784 # nothing changed, nothing commited
784 785 nextp1 = ctx
785 786 continue
786 787 willbecomenoop = ctx.files() and self._willbecomenoop(
787 788 memworkingcopy, ctx, nextp1
788 789 )
789 790 if self.skip_empty_successor and willbecomenoop:
790 791 # changeset is no longer necessary
791 792 self.replacemap[ctx.node()] = None
792 793 msg = _(b'became empty and was dropped')
793 794 else:
794 795 # changeset needs re-commit
795 796 nodestr = self._commitsingle(memworkingcopy, ctx, p1=nextp1)
796 797 lastcommitted = self.repo[nodestr]
797 798 nextp1 = lastcommitted
798 799 self.replacemap[ctx.node()] = lastcommitted.node()
799 800 if memworkingcopy:
800 801 if willbecomenoop:
801 802 msg = _(b'%d file(s) changed, became empty as %s')
802 803 else:
803 804 msg = _(b'%d file(s) changed, became %s')
804 805 msg = msg % (
805 806 len(memworkingcopy),
806 807 self._ctx2str(lastcommitted),
807 808 )
808 809 else:
809 810 msg = _(b'became %s') % self._ctx2str(lastcommitted)
810 811 if self.ui.verbose and msg:
811 812 self.ui.write(_(b'%s: %s\n') % (self._ctx2str(ctx), msg))
812 813 self.finalnode = lastcommitted and lastcommitted.node()
813 814
814 815 def _ctx2str(self, ctx):
815 816 if self.ui.debugflag:
816 817 return b'%d:%s' % (ctx.rev(), ctx.hex())
817 818 else:
818 819 return b'%d:%s' % (ctx.rev(), node.short(ctx.node()))
819 820
820 821 def _getnewfilecontents(self, ctx):
821 822 """(ctx) -> {path: str}
822 823
823 824 fetch file contents from filefixupstates.
824 825 return the working copy overrides - files different from ctx.
825 826 """
826 827 result = {}
827 828 for path in self.paths:
828 829 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
829 830 if ctx not in ctx2fctx:
830 831 continue
831 832 fctx = ctx2fctx[ctx]
832 833 content = fctx.data()
833 834 newcontent = self.fixupmap[path].getfinalcontent(fctx)
834 835 if content != newcontent:
835 836 result[fctx.path()] = newcontent
836 837 return result
837 838
838 839 def _movebookmarks(self, tr):
839 840 repo = self.repo
840 841 needupdate = [
841 842 (name, self.replacemap[hsh])
842 843 for name, hsh in pycompat.iteritems(repo._bookmarks)
843 844 if hsh in self.replacemap
844 845 ]
845 846 changes = []
846 847 for name, hsh in needupdate:
847 848 if hsh:
848 849 changes.append((name, hsh))
849 850 if self.ui.verbose:
850 851 self.ui.write(
851 852 _(b'moving bookmark %s to %s\n') % (name, node.hex(hsh))
852 853 )
853 854 else:
854 855 changes.append((name, None))
855 856 if self.ui.verbose:
856 857 self.ui.write(_(b'deleting bookmark %s\n') % name)
857 858 repo._bookmarks.applychanges(repo, tr, changes)
858 859
859 860 def _moveworkingdirectoryparent(self):
860 861 if not self.finalnode:
861 862 # Find the latest not-{obsoleted,stripped} parent.
862 863 revs = self.repo.revs(b'max(::. - %ln)', self.replacemap.keys())
863 864 ctx = self.repo[revs.first()]
864 865 self.finalnode = ctx.node()
865 866 else:
866 867 ctx = self.repo[self.finalnode]
867 868
868 869 dirstate = self.repo.dirstate
869 870 # dirstate.rebuild invalidates fsmonitorstate, causing "hg status" to
870 871 # be slow. in absorb's case, no need to invalidate fsmonitorstate.
871 872 noop = lambda: 0
872 873 restore = noop
873 874 if util.safehasattr(dirstate, '_fsmonitorstate'):
874 875 bak = dirstate._fsmonitorstate.invalidate
875 876
876 877 def restore():
877 878 dirstate._fsmonitorstate.invalidate = bak
878 879
879 880 dirstate._fsmonitorstate.invalidate = noop
880 881 try:
881 882 with dirstate.parentchange():
882 883 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
883 884 finally:
884 885 restore()
885 886
886 887 @staticmethod
887 888 def _willbecomenoop(memworkingcopy, ctx, pctx=None):
888 889 """({path: content}, ctx, ctx) -> bool. test if a commit will be noop
889 890
890 891 if it will become an empty commit (does not change anything, after the
891 892 memworkingcopy overrides), return True. otherwise return False.
892 893 """
893 894 if not pctx:
894 895 parents = ctx.parents()
895 896 if len(parents) != 1:
896 897 return False
897 898 pctx = parents[0]
898 899 if ctx.branch() != pctx.branch():
899 900 return False
900 901 if ctx.extra().get(b'close'):
901 902 return False
902 903 # ctx changes more files (not a subset of memworkingcopy)
903 904 if not set(ctx.files()).issubset(set(memworkingcopy)):
904 905 return False
905 906 for path, content in pycompat.iteritems(memworkingcopy):
906 907 if path not in pctx or path not in ctx:
907 908 return False
908 909 fctx = ctx[path]
909 910 pfctx = pctx[path]
910 911 if pfctx.flags() != fctx.flags():
911 912 return False
912 913 if pfctx.data() != content:
913 914 return False
914 915 return True
915 916
916 917 def _commitsingle(self, memworkingcopy, ctx, p1=None):
917 918 """(ctx, {path: content}, node) -> node. make a single commit
918 919
919 920 the commit is a clone from ctx, with a (optionally) different p1, and
920 921 different file contents replaced by memworkingcopy.
921 922 """
922 923 parents = p1 and (p1, node.nullid)
923 924 extra = ctx.extra()
924 925 if self._useobsolete and self.ui.configbool(b'absorb', b'add-noise'):
925 926 extra[b'absorb_source'] = ctx.hex()
926 mctx = overlaycontext(memworkingcopy, ctx, parents, extra=extra)
927
928 desc = rewriteutil.update_hash_refs(
929 ctx.repo(),
930 ctx.description(),
931 {
932 oldnode: [newnode]
933 for oldnode, newnode in self.replacemap.items()
934 },
935 )
936 mctx = overlaycontext(
937 memworkingcopy, ctx, parents, extra=extra, desc=desc
938 )
927 939 return mctx.commit()
928 940
929 941 @util.propertycache
930 942 def _useobsolete(self):
931 943 """() -> bool"""
932 944 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
933 945
934 946 def _cleanupoldcommits(self):
935 947 replacements = {
936 948 k: ([v] if v is not None else [])
937 949 for k, v in pycompat.iteritems(self.replacemap)
938 950 }
939 951 if replacements:
940 952 scmutil.cleanupnodes(
941 953 self.repo, replacements, operation=b'absorb', fixphase=True
942 954 )
943 955
944 956 @util.propertycache
945 957 def skip_empty_successor(self):
946 958 return rewriteutil.skip_empty_successor(self.ui, b'absorb')
947 959
948 960
949 961 def _parsechunk(hunk):
950 962 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
951 963 if type(hunk) not in (crecord.uihunk, patch.recordhunk):
952 964 return None, None
953 965 path = hunk.header.filename()
954 966 a1 = hunk.fromline + len(hunk.before) - 1
955 967 # remove before and after context
956 968 hunk.before = hunk.after = []
957 969 buf = util.stringio()
958 970 hunk.write(buf)
959 971 patchlines = mdiff.splitnewlines(buf.getvalue())
960 972 # hunk.prettystr() will update hunk.removed
961 973 a2 = a1 + hunk.removed
962 974 blines = [l[1:] for l in patchlines[1:] if not l.startswith(b'-')]
963 975 return path, (a1, a2, blines)
964 976
965 977
966 978 def overlaydiffcontext(ctx, chunks):
967 979 """(ctx, [crecord.uihunk]) -> memctx
968 980
969 981 return a memctx with some [1] patches (chunks) applied to ctx.
970 982 [1]: modifications are handled. renames, mode changes, etc. are ignored.
971 983 """
972 984 # sadly the applying-patch logic is hardly reusable, and messy:
973 985 # 1. the core logic "_applydiff" is too heavy - it writes .rej files, it
974 986 # needs a file stream of a patch and will re-parse it, while we have
975 987 # structured hunk objects at hand.
976 988 # 2. a lot of different implementations about "chunk" (patch.hunk,
977 989 # patch.recordhunk, crecord.uihunk)
978 990 # as we only care about applying changes to modified files, no mode
979 991 # change, no binary diff, and no renames, it's probably okay to
980 992 # re-invent the logic using much simpler code here.
981 993 memworkingcopy = {} # {path: content}
982 994 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
983 995 for path, info in map(_parsechunk, chunks):
984 996 if not path or not info:
985 997 continue
986 998 patchmap[path].append(info)
987 999 for path, patches in pycompat.iteritems(patchmap):
988 1000 if path not in ctx or not patches:
989 1001 continue
990 1002 patches.sort(reverse=True)
991 1003 lines = mdiff.splitnewlines(ctx[path].data())
992 1004 for a1, a2, blines in patches:
993 1005 lines[a1:a2] = blines
994 1006 memworkingcopy[path] = b''.join(lines)
995 1007 return overlaycontext(memworkingcopy, ctx)
996 1008
997 1009
998 1010 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
999 1011 """pick fixup chunks from targetctx, apply them to stack.
1000 1012
1001 1013 if targetctx is None, the working copy context will be used.
1002 1014 if stack is None, the current draft stack will be used.
1003 1015 return fixupstate.
1004 1016 """
1005 1017 if stack is None:
1006 1018 limit = ui.configint(b'absorb', b'max-stack-size')
1007 1019 headctx = repo[b'.']
1008 1020 if len(headctx.parents()) > 1:
1009 1021 raise error.Abort(_(b'cannot absorb into a merge'))
1010 1022 stack = getdraftstack(headctx, limit)
1011 1023 if limit and len(stack) >= limit:
1012 1024 ui.warn(
1013 1025 _(
1014 1026 b'absorb: only the recent %d changesets will '
1015 1027 b'be analysed\n'
1016 1028 )
1017 1029 % limit
1018 1030 )
1019 1031 if not stack:
1020 1032 raise error.Abort(_(b'no mutable changeset to change'))
1021 1033 if targetctx is None: # default to working copy
1022 1034 targetctx = repo[None]
1023 1035 if pats is None:
1024 1036 pats = ()
1025 1037 if opts is None:
1026 1038 opts = {}
1027 1039 state = fixupstate(stack, ui=ui, opts=opts)
1028 1040 matcher = scmutil.match(targetctx, pats, opts)
1029 1041 if opts.get(b'interactive'):
1030 1042 diff = patch.diff(repo, stack[-1].node(), targetctx.node(), matcher)
1031 1043 origchunks = patch.parsepatch(diff)
1032 1044 chunks = cmdutil.recordfilter(ui, origchunks, matcher)[0]
1033 1045 targetctx = overlaydiffcontext(stack[-1], chunks)
1034 1046 fm = None
1035 1047 if opts.get(b'print_changes') or not opts.get(b'apply_changes'):
1036 1048 fm = ui.formatter(b'absorb', opts)
1037 1049 state.diffwith(targetctx, matcher, fm)
1038 1050 if fm is not None:
1039 1051 fm.startitem()
1040 1052 fm.write(
1041 1053 b"count", b"\n%d changesets affected\n", len(state.ctxaffected)
1042 1054 )
1043 1055 fm.data(linetype=b'summary')
1044 1056 for ctx in reversed(stack):
1045 1057 if ctx not in state.ctxaffected:
1046 1058 continue
1047 1059 fm.startitem()
1048 1060 fm.context(ctx=ctx)
1049 1061 fm.data(linetype=b'changeset')
1050 1062 fm.write(b'node', b'%-7.7s ', ctx.hex(), label=b'absorb.node')
1051 1063 descfirstline = ctx.description().splitlines()[0]
1052 1064 fm.write(
1053 1065 b'descfirstline',
1054 1066 b'%s\n',
1055 1067 descfirstline,
1056 1068 label=b'absorb.description',
1057 1069 )
1058 1070 fm.end()
1059 1071 if not opts.get(b'dry_run'):
1060 1072 if (
1061 1073 not opts.get(b'apply_changes')
1062 1074 and state.ctxaffected
1063 1075 and ui.promptchoice(
1064 1076 b"apply changes (y/N)? $$ &Yes $$ &No", default=1
1065 1077 )
1066 1078 ):
1067 1079 raise error.Abort(_(b'absorb cancelled\n'))
1068 1080
1069 1081 state.apply()
1070 1082 if state.commit():
1071 1083 state.printchunkstats()
1072 1084 elif not ui.quiet:
1073 1085 ui.write(_(b'nothing applied\n'))
1074 1086 return state
1075 1087
1076 1088
1077 1089 @command(
1078 1090 b'absorb',
1079 1091 [
1080 1092 (
1081 1093 b'a',
1082 1094 b'apply-changes',
1083 1095 None,
1084 1096 _(b'apply changes without prompting for confirmation'),
1085 1097 ),
1086 1098 (
1087 1099 b'p',
1088 1100 b'print-changes',
1089 1101 None,
1090 1102 _(b'always print which changesets are modified by which changes'),
1091 1103 ),
1092 1104 (
1093 1105 b'i',
1094 1106 b'interactive',
1095 1107 None,
1096 1108 _(b'interactively select which chunks to apply'),
1097 1109 ),
1098 1110 (
1099 1111 b'e',
1100 1112 b'edit-lines',
1101 1113 None,
1102 1114 _(
1103 1115 b'edit what lines belong to which changesets before commit '
1104 1116 b'(EXPERIMENTAL)'
1105 1117 ),
1106 1118 ),
1107 1119 ]
1108 1120 + commands.dryrunopts
1109 1121 + commands.templateopts
1110 1122 + commands.walkopts,
1111 1123 _(b'hg absorb [OPTION] [FILE]...'),
1112 1124 helpcategory=command.CATEGORY_COMMITTING,
1113 1125 helpbasic=True,
1114 1126 )
1115 1127 def absorbcmd(ui, repo, *pats, **opts):
1116 1128 """incorporate corrections into the stack of draft changesets
1117 1129
1118 1130 absorb analyzes each change in your working directory and attempts to
1119 1131 amend the changed lines into the changesets in your stack that first
1120 1132 introduced those lines.
1121 1133
1122 1134 If absorb cannot find an unambiguous changeset to amend for a change,
1123 1135 that change will be left in the working directory, untouched. They can be
1124 1136 observed by :hg:`status` or :hg:`diff` afterwards. In other words,
1125 1137 absorb does not write to the working directory.
1126 1138
1127 1139 Changesets outside the revset `::. and not public() and not merge()` will
1128 1140 not be changed.
1129 1141
1130 1142 Changesets that become empty after applying the changes will be deleted.
1131 1143
1132 1144 By default, absorb will show what it plans to do and prompt for
1133 1145 confirmation. If you are confident that the changes will be absorbed
1134 1146 to the correct place, run :hg:`absorb -a` to apply the changes
1135 1147 immediately.
1136 1148
1137 1149 Returns 0 on success, 1 if all chunks were ignored and nothing amended.
1138 1150 """
1139 1151 opts = pycompat.byteskwargs(opts)
1140 1152
1141 1153 with repo.wlock(), repo.lock():
1142 1154 if not opts[b'dry_run']:
1143 1155 cmdutil.checkunfinished(repo)
1144 1156
1145 1157 state = absorb(ui, repo, pats=pats, opts=opts)
1146 1158 if sum(s[0] for s in state.chunkstats.values()) == 0:
1147 1159 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 533 2:30970dbf7b40: 2 file(s) changed, became empty as 7:df6574ae635c
534 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 $ hg commit -m foo # will become empty
606 $ hg commit -m 'foo (child of 0cde1ae39321)' # will become empty
607 607 $ hg branch bar -q
608 $ hg commit -m bar # is already empty
608 $ hg commit -m 'bar (child of e969dc86aefc)' # 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 1:795dfb1adcef: 2 file(s) changed, became 4:a8740537aa53
614 2:b02935f68891: 2 file(s) changed, became 5:59533e01c707
615 $ hg log -T '{rev} (branch: {branch}) {desc}\n' -G --stat
616 @ 5 (branch: bar) bar
613 1:e969dc86aefc: 2 file(s) changed, became 4:8fc6b2cb43a5
614 2:0298954ced32: 2 file(s) changed, became 5:ca8386dc4e2c
615 $ hg log -T '{rev}:{node|short} (branch: {branch}) {desc}\n' -G --stat
616 @ 5:ca8386dc4e2c (branch: bar) bar (child of 8fc6b2cb43a5)
617 617 |
618 o 4 (branch: foo) foo
618 o 4:8fc6b2cb43a5 (branch: foo) foo (child of fc7fcdd90fdb)
619 619 |
620 o 3 (branch: default) a
620 o 3:fc7fcdd90fdb (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