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