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