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