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