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