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