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