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