##// END OF EJS Templates
absorb: preserve branch-closing changesets even if empty...
Manuel Jacob -
r45517:bfef35bb default
parent child Browse files
Show More
@@ -1,1133 +1,1135
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 self._willbecomenoop(memworkingcopy, ctx, nextp1):
786 786 # changeset is no longer necessary
787 787 self.replacemap[ctx.node()] = None
788 788 msg = _(b'became empty and was dropped')
789 789 else:
790 790 # changeset needs re-commit
791 791 nodestr = self._commitsingle(memworkingcopy, ctx, p1=nextp1)
792 792 lastcommitted = self.repo[nodestr]
793 793 nextp1 = lastcommitted
794 794 self.replacemap[ctx.node()] = lastcommitted.node()
795 795 if memworkingcopy:
796 796 msg = _(b'%d file(s) changed, became %s') % (
797 797 len(memworkingcopy),
798 798 self._ctx2str(lastcommitted),
799 799 )
800 800 else:
801 801 msg = _(b'became %s') % self._ctx2str(lastcommitted)
802 802 if self.ui.verbose and msg:
803 803 self.ui.write(_(b'%s: %s\n') % (self._ctx2str(ctx), msg))
804 804 self.finalnode = lastcommitted and lastcommitted.node()
805 805
806 806 def _ctx2str(self, ctx):
807 807 if self.ui.debugflag:
808 808 return b'%d:%s' % (ctx.rev(), ctx.hex())
809 809 else:
810 810 return b'%d:%s' % (ctx.rev(), node.short(ctx.node()))
811 811
812 812 def _getnewfilecontents(self, ctx):
813 813 """(ctx) -> {path: str}
814 814
815 815 fetch file contents from filefixupstates.
816 816 return the working copy overrides - files different from ctx.
817 817 """
818 818 result = {}
819 819 for path in self.paths:
820 820 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
821 821 if ctx not in ctx2fctx:
822 822 continue
823 823 fctx = ctx2fctx[ctx]
824 824 content = fctx.data()
825 825 newcontent = self.fixupmap[path].getfinalcontent(fctx)
826 826 if content != newcontent:
827 827 result[fctx.path()] = newcontent
828 828 return result
829 829
830 830 def _movebookmarks(self, tr):
831 831 repo = self.repo
832 832 needupdate = [
833 833 (name, self.replacemap[hsh])
834 834 for name, hsh in pycompat.iteritems(repo._bookmarks)
835 835 if hsh in self.replacemap
836 836 ]
837 837 changes = []
838 838 for name, hsh in needupdate:
839 839 if hsh:
840 840 changes.append((name, hsh))
841 841 if self.ui.verbose:
842 842 self.ui.write(
843 843 _(b'moving bookmark %s to %s\n') % (name, node.hex(hsh))
844 844 )
845 845 else:
846 846 changes.append((name, None))
847 847 if self.ui.verbose:
848 848 self.ui.write(_(b'deleting bookmark %s\n') % name)
849 849 repo._bookmarks.applychanges(repo, tr, changes)
850 850
851 851 def _moveworkingdirectoryparent(self):
852 852 if not self.finalnode:
853 853 # Find the latest not-{obsoleted,stripped} parent.
854 854 revs = self.repo.revs(b'max(::. - %ln)', self.replacemap.keys())
855 855 ctx = self.repo[revs.first()]
856 856 self.finalnode = ctx.node()
857 857 else:
858 858 ctx = self.repo[self.finalnode]
859 859
860 860 dirstate = self.repo.dirstate
861 861 # dirstate.rebuild invalidates fsmonitorstate, causing "hg status" to
862 862 # be slow. in absorb's case, no need to invalidate fsmonitorstate.
863 863 noop = lambda: 0
864 864 restore = noop
865 865 if util.safehasattr(dirstate, '_fsmonitorstate'):
866 866 bak = dirstate._fsmonitorstate.invalidate
867 867
868 868 def restore():
869 869 dirstate._fsmonitorstate.invalidate = bak
870 870
871 871 dirstate._fsmonitorstate.invalidate = noop
872 872 try:
873 873 with dirstate.parentchange():
874 874 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
875 875 finally:
876 876 restore()
877 877
878 878 @staticmethod
879 879 def _willbecomenoop(memworkingcopy, ctx, pctx=None):
880 880 """({path: content}, ctx, ctx) -> bool. test if a commit will be noop
881 881
882 882 if it will become an empty commit (does not change anything, after the
883 883 memworkingcopy overrides), return True. otherwise return False.
884 884 """
885 885 if not pctx:
886 886 parents = ctx.parents()
887 887 if len(parents) != 1:
888 888 return False
889 889 pctx = parents[0]
890 890 if ctx.branch() != pctx.branch():
891 891 return False
892 if ctx.extra().get(b'close'):
893 return False
892 894 # ctx changes more files (not a subset of memworkingcopy)
893 895 if not set(ctx.files()).issubset(set(memworkingcopy)):
894 896 return False
895 897 for path, content in pycompat.iteritems(memworkingcopy):
896 898 if path not in pctx or path not in ctx:
897 899 return False
898 900 fctx = ctx[path]
899 901 pfctx = pctx[path]
900 902 if pfctx.flags() != fctx.flags():
901 903 return False
902 904 if pfctx.data() != content:
903 905 return False
904 906 return True
905 907
906 908 def _commitsingle(self, memworkingcopy, ctx, p1=None):
907 909 """(ctx, {path: content}, node) -> node. make a single commit
908 910
909 911 the commit is a clone from ctx, with a (optionally) different p1, and
910 912 different file contents replaced by memworkingcopy.
911 913 """
912 914 parents = p1 and (p1, node.nullid)
913 915 extra = ctx.extra()
914 916 if self._useobsolete and self.ui.configbool(b'absorb', b'add-noise'):
915 917 extra[b'absorb_source'] = ctx.hex()
916 918 mctx = overlaycontext(memworkingcopy, ctx, parents, extra=extra)
917 919 return mctx.commit()
918 920
919 921 @util.propertycache
920 922 def _useobsolete(self):
921 923 """() -> bool"""
922 924 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
923 925
924 926 def _cleanupoldcommits(self):
925 927 replacements = {
926 928 k: ([v] if v is not None else [])
927 929 for k, v in pycompat.iteritems(self.replacemap)
928 930 }
929 931 if replacements:
930 932 scmutil.cleanupnodes(
931 933 self.repo, replacements, operation=b'absorb', fixphase=True
932 934 )
933 935
934 936
935 937 def _parsechunk(hunk):
936 938 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
937 939 if type(hunk) not in (crecord.uihunk, patch.recordhunk):
938 940 return None, None
939 941 path = hunk.header.filename()
940 942 a1 = hunk.fromline + len(hunk.before) - 1
941 943 # remove before and after context
942 944 hunk.before = hunk.after = []
943 945 buf = util.stringio()
944 946 hunk.write(buf)
945 947 patchlines = mdiff.splitnewlines(buf.getvalue())
946 948 # hunk.prettystr() will update hunk.removed
947 949 a2 = a1 + hunk.removed
948 950 blines = [l[1:] for l in patchlines[1:] if not l.startswith(b'-')]
949 951 return path, (a1, a2, blines)
950 952
951 953
952 954 def overlaydiffcontext(ctx, chunks):
953 955 """(ctx, [crecord.uihunk]) -> memctx
954 956
955 957 return a memctx with some [1] patches (chunks) applied to ctx.
956 958 [1]: modifications are handled. renames, mode changes, etc. are ignored.
957 959 """
958 960 # sadly the applying-patch logic is hardly reusable, and messy:
959 961 # 1. the core logic "_applydiff" is too heavy - it writes .rej files, it
960 962 # needs a file stream of a patch and will re-parse it, while we have
961 963 # structured hunk objects at hand.
962 964 # 2. a lot of different implementations about "chunk" (patch.hunk,
963 965 # patch.recordhunk, crecord.uihunk)
964 966 # as we only care about applying changes to modified files, no mode
965 967 # change, no binary diff, and no renames, it's probably okay to
966 968 # re-invent the logic using much simpler code here.
967 969 memworkingcopy = {} # {path: content}
968 970 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
969 971 for path, info in map(_parsechunk, chunks):
970 972 if not path or not info:
971 973 continue
972 974 patchmap[path].append(info)
973 975 for path, patches in pycompat.iteritems(patchmap):
974 976 if path not in ctx or not patches:
975 977 continue
976 978 patches.sort(reverse=True)
977 979 lines = mdiff.splitnewlines(ctx[path].data())
978 980 for a1, a2, blines in patches:
979 981 lines[a1:a2] = blines
980 982 memworkingcopy[path] = b''.join(lines)
981 983 return overlaycontext(memworkingcopy, ctx)
982 984
983 985
984 986 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
985 987 """pick fixup chunks from targetctx, apply them to stack.
986 988
987 989 if targetctx is None, the working copy context will be used.
988 990 if stack is None, the current draft stack will be used.
989 991 return fixupstate.
990 992 """
991 993 if stack is None:
992 994 limit = ui.configint(b'absorb', b'max-stack-size')
993 995 headctx = repo[b'.']
994 996 if len(headctx.parents()) > 1:
995 997 raise error.Abort(_(b'cannot absorb into a merge'))
996 998 stack = getdraftstack(headctx, limit)
997 999 if limit and len(stack) >= limit:
998 1000 ui.warn(
999 1001 _(
1000 1002 b'absorb: only the recent %d changesets will '
1001 1003 b'be analysed\n'
1002 1004 )
1003 1005 % limit
1004 1006 )
1005 1007 if not stack:
1006 1008 raise error.Abort(_(b'no mutable changeset to change'))
1007 1009 if targetctx is None: # default to working copy
1008 1010 targetctx = repo[None]
1009 1011 if pats is None:
1010 1012 pats = ()
1011 1013 if opts is None:
1012 1014 opts = {}
1013 1015 state = fixupstate(stack, ui=ui, opts=opts)
1014 1016 matcher = scmutil.match(targetctx, pats, opts)
1015 1017 if opts.get(b'interactive'):
1016 1018 diff = patch.diff(repo, stack[-1].node(), targetctx.node(), matcher)
1017 1019 origchunks = patch.parsepatch(diff)
1018 1020 chunks = cmdutil.recordfilter(ui, origchunks, matcher)[0]
1019 1021 targetctx = overlaydiffcontext(stack[-1], chunks)
1020 1022 fm = None
1021 1023 if opts.get(b'print_changes') or not opts.get(b'apply_changes'):
1022 1024 fm = ui.formatter(b'absorb', opts)
1023 1025 state.diffwith(targetctx, matcher, fm)
1024 1026 if fm is not None:
1025 1027 fm.startitem()
1026 1028 fm.write(
1027 1029 b"count", b"\n%d changesets affected\n", len(state.ctxaffected)
1028 1030 )
1029 1031 fm.data(linetype=b'summary')
1030 1032 for ctx in reversed(stack):
1031 1033 if ctx not in state.ctxaffected:
1032 1034 continue
1033 1035 fm.startitem()
1034 1036 fm.context(ctx=ctx)
1035 1037 fm.data(linetype=b'changeset')
1036 1038 fm.write(b'node', b'%-7.7s ', ctx.hex(), label=b'absorb.node')
1037 1039 descfirstline = ctx.description().splitlines()[0]
1038 1040 fm.write(
1039 1041 b'descfirstline',
1040 1042 b'%s\n',
1041 1043 descfirstline,
1042 1044 label=b'absorb.description',
1043 1045 )
1044 1046 fm.end()
1045 1047 if not opts.get(b'dry_run'):
1046 1048 if (
1047 1049 not opts.get(b'apply_changes')
1048 1050 and state.ctxaffected
1049 1051 and ui.promptchoice(
1050 1052 b"apply changes (yn)? $$ &Yes $$ &No", default=1
1051 1053 )
1052 1054 ):
1053 1055 raise error.Abort(_(b'absorb cancelled\n'))
1054 1056
1055 1057 state.apply()
1056 1058 if state.commit():
1057 1059 state.printchunkstats()
1058 1060 elif not ui.quiet:
1059 1061 ui.write(_(b'nothing applied\n'))
1060 1062 return state
1061 1063
1062 1064
1063 1065 @command(
1064 1066 b'absorb',
1065 1067 [
1066 1068 (
1067 1069 b'a',
1068 1070 b'apply-changes',
1069 1071 None,
1070 1072 _(b'apply changes without prompting for confirmation'),
1071 1073 ),
1072 1074 (
1073 1075 b'p',
1074 1076 b'print-changes',
1075 1077 None,
1076 1078 _(b'always print which changesets are modified by which changes'),
1077 1079 ),
1078 1080 (
1079 1081 b'i',
1080 1082 b'interactive',
1081 1083 None,
1082 1084 _(b'interactively select which chunks to apply'),
1083 1085 ),
1084 1086 (
1085 1087 b'e',
1086 1088 b'edit-lines',
1087 1089 None,
1088 1090 _(
1089 1091 b'edit what lines belong to which changesets before commit '
1090 1092 b'(EXPERIMENTAL)'
1091 1093 ),
1092 1094 ),
1093 1095 ]
1094 1096 + commands.dryrunopts
1095 1097 + commands.templateopts
1096 1098 + commands.walkopts,
1097 1099 _(b'hg absorb [OPTION] [FILE]...'),
1098 1100 helpcategory=command.CATEGORY_COMMITTING,
1099 1101 helpbasic=True,
1100 1102 )
1101 1103 def absorbcmd(ui, repo, *pats, **opts):
1102 1104 """incorporate corrections into the stack of draft changesets
1103 1105
1104 1106 absorb analyzes each change in your working directory and attempts to
1105 1107 amend the changed lines into the changesets in your stack that first
1106 1108 introduced those lines.
1107 1109
1108 1110 If absorb cannot find an unambiguous changeset to amend for a change,
1109 1111 that change will be left in the working directory, untouched. They can be
1110 1112 observed by :hg:`status` or :hg:`diff` afterwards. In other words,
1111 1113 absorb does not write to the working directory.
1112 1114
1113 1115 Changesets outside the revset `::. and not public() and not merge()` will
1114 1116 not be changed.
1115 1117
1116 1118 Changesets that become empty after applying the changes will be deleted.
1117 1119
1118 1120 By default, absorb will show what it plans to do and prompt for
1119 1121 confirmation. If you are confident that the changes will be absorbed
1120 1122 to the correct place, run :hg:`absorb -a` to apply the changes
1121 1123 immediately.
1122 1124
1123 1125 Returns 0 on success, 1 if all chunks were ignored and nothing amended.
1124 1126 """
1125 1127 opts = pycompat.byteskwargs(opts)
1126 1128
1127 1129 with repo.wlock(), repo.lock():
1128 1130 if not opts[b'dry_run']:
1129 1131 cmdutil.checkunfinished(repo)
1130 1132
1131 1133 state = absorb(ui, repo, pats=pats, opts=opts)
1132 1134 if sum(s[0] for s in state.chunkstats.values()) == 0:
1133 1135 return 1
@@ -1,555 +1,589
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 (yn)? 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
557 $ cd ..
558 $ hg init repo7
559 $ cd repo7
560 $ echo a1 > a
561 $ touch b
562 $ hg commit -m a -A a b
563 $ echo b > b
564 $ hg commit -m foo --close-branch # will become empty
565 $ echo c > c
566 $ hg commit -m reopen -A c -q
567 $ hg commit -m bar --close-branch # is already empty
568 $ echo a2 > a
569 $ printf '' > b
570 $ hg absorb --apply-changes --verbose | grep became
571 0:0cde1ae39321: 1 file(s) changed, became 4:fc7fcdd90fdb
572 1:651b953d5764: 2 file(s) changed, became 5:0c9de988ecdc
573 2:76017bba73f6: 2 file(s) changed, became 6:d53ac896eb25
574 3:c7c1d67efc1d: 2 file(s) changed, became 7:66520267fe96
575 $ hg up null -q # to make visible closed heads
576 $ hg log -T '{rev} {desc}\n' -G --stat
577 _ 7 bar
578 |
579 o 6 reopen
580 | c | 1 +
581 | 1 files changed, 1 insertions(+), 0 deletions(-)
582 |
583 _ 5 foo
584 |
585 o 4 a
586 a | 1 +
587 b | 0
588 2 files changed, 1 insertions(+), 0 deletions(-)
589
General Comments 0
You need to be logged in to leave comments. Login now