##// END OF EJS Templates
histedit: preserve initial author on fold (issue4296)...
Martin von Zweigbergk -
r22147:9ac98c2a stable
parent child Browse files
Show More
@@ -1,929 +1,924
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
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 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # d, drop = remove commit from history
39 # d, drop = remove commit from history
40 # m, mess = edit message without changing commit content
40 # m, mess = edit message without changing commit content
41 #
41 #
42
42
43 In this file, lines beginning with ``#`` are ignored. You must specify a rule
43 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 for each revision in your history. For example, if you had meant to add gamma
44 for each revision in your history. For example, if you had meant to add gamma
45 before beta, and then wanted to add delta in the same revision as beta, you
45 before beta, and then wanted to add delta in the same revision as beta, you
46 would reorganize the file to look like this::
46 would reorganize the file to look like this::
47
47
48 pick 030b686bedc4 Add gamma
48 pick 030b686bedc4 Add gamma
49 pick c561b4e977df Add beta
49 pick c561b4e977df Add beta
50 fold 7c2fd3b9020c Add delta
50 fold 7c2fd3b9020c Add delta
51
51
52 # Edit history between c561b4e977df and 7c2fd3b9020c
52 # Edit history between c561b4e977df and 7c2fd3b9020c
53 #
53 #
54 # Commits are listed from least to most recent
54 # Commits are listed from least to most recent
55 #
55 #
56 # Commands:
56 # Commands:
57 # p, pick = use commit
57 # p, pick = use commit
58 # e, edit = use commit, but stop for amending
58 # e, edit = use commit, but stop for amending
59 # f, fold = use commit, but combine it with the one above
59 # f, fold = use commit, but combine it with the one above
60 # d, drop = remove commit from history
60 # d, drop = remove commit from history
61 # m, mess = edit message without changing commit content
61 # m, mess = edit message without changing commit content
62 #
62 #
63
63
64 At which point you close the editor and ``histedit`` starts working. When you
64 At which point you close the editor and ``histedit`` starts working. When you
65 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
65 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
66 those revisions together, offering you a chance to clean up the commit message::
66 those revisions together, offering you a chance to clean up the commit message::
67
67
68 Add beta
68 Add beta
69 ***
69 ***
70 Add delta
70 Add delta
71
71
72 Edit the commit message to your liking, then close the editor. For
72 Edit the commit message to your liking, then close the editor. For
73 this example, let's assume that the commit message was changed to
73 this example, let's assume that the commit message was changed to
74 ``Add beta and delta.`` After histedit has run and had a chance to
74 ``Add beta and delta.`` After histedit has run and had a chance to
75 remove any old or temporary revisions it needed, the history looks
75 remove any old or temporary revisions it needed, the history looks
76 like this::
76 like this::
77
77
78 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
78 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
79 | Add beta and delta.
79 | Add beta and delta.
80 |
80 |
81 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
81 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
82 | Add gamma
82 | Add gamma
83 |
83 |
84 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
84 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
85 Add alpha
85 Add alpha
86
86
87 Note that ``histedit`` does *not* remove any revisions (even its own temporary
87 Note that ``histedit`` does *not* remove any revisions (even its own temporary
88 ones) until after it has completed all the editing operations, so it will
88 ones) until after it has completed all the editing operations, so it will
89 probably perform several strip operations when it's done. For the above example,
89 probably perform several strip operations when it's done. For the above example,
90 it had to run strip twice. Strip can be slow depending on a variety of factors,
90 it had to run strip twice. Strip can be slow depending on a variety of factors,
91 so you might need to be a little patient. You can choose to keep the original
91 so you might need to be a little patient. You can choose to keep the original
92 revisions by passing the ``--keep`` flag.
92 revisions by passing the ``--keep`` flag.
93
93
94 The ``edit`` operation will drop you back to a command prompt,
94 The ``edit`` operation will drop you back to a command prompt,
95 allowing you to edit files freely, or even use ``hg record`` to commit
95 allowing you to edit files freely, or even use ``hg record`` to commit
96 some changes as a separate commit. When you're done, any remaining
96 some changes as a separate commit. When you're done, any remaining
97 uncommitted changes will be committed as well. When done, run ``hg
97 uncommitted changes will be committed as well. When done, run ``hg
98 histedit --continue`` to finish this step. You'll be prompted for a
98 histedit --continue`` to finish this step. You'll be prompted for a
99 new commit message, but the default commit message will be the
99 new commit message, but the default commit message will be the
100 original message for the ``edit`` ed revision.
100 original message for the ``edit`` ed revision.
101
101
102 The ``message`` operation will give you a chance to revise a commit
102 The ``message`` operation will give you a chance to revise a commit
103 message without changing the contents. It's a shortcut for doing
103 message without changing the contents. It's a shortcut for doing
104 ``edit`` immediately followed by `hg histedit --continue``.
104 ``edit`` immediately followed by `hg histedit --continue``.
105
105
106 If ``histedit`` encounters a conflict when moving a revision (while
106 If ``histedit`` encounters a conflict when moving a revision (while
107 handling ``pick`` or ``fold``), it'll stop in a similar manner to
107 handling ``pick`` or ``fold``), it'll stop in a similar manner to
108 ``edit`` with the difference that it won't prompt you for a commit
108 ``edit`` with the difference that it won't prompt you for a commit
109 message when done. If you decide at this point that you don't like how
109 message when done. If you decide at this point that you don't like how
110 much work it will be to rearrange history, or that you made a mistake,
110 much work it will be to rearrange history, or that you made a mistake,
111 you can use ``hg histedit --abort`` to abandon the new changes you
111 you can use ``hg histedit --abort`` to abandon the new changes you
112 have made and return to the state before you attempted to edit your
112 have made and return to the state before you attempted to edit your
113 history.
113 history.
114
114
115 If we clone the histedit-ed example repository above and add four more
115 If we clone the histedit-ed example repository above and add four more
116 changes, such that we have the following history::
116 changes, such that we have the following history::
117
117
118 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
118 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
119 | Add theta
119 | Add theta
120 |
120 |
121 o 5 140988835471 2009-04-27 18:04 -0500 stefan
121 o 5 140988835471 2009-04-27 18:04 -0500 stefan
122 | Add eta
122 | Add eta
123 |
123 |
124 o 4 122930637314 2009-04-27 18:04 -0500 stefan
124 o 4 122930637314 2009-04-27 18:04 -0500 stefan
125 | Add zeta
125 | Add zeta
126 |
126 |
127 o 3 836302820282 2009-04-27 18:04 -0500 stefan
127 o 3 836302820282 2009-04-27 18:04 -0500 stefan
128 | Add epsilon
128 | Add epsilon
129 |
129 |
130 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
130 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
131 | Add beta and delta.
131 | Add beta and delta.
132 |
132 |
133 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
133 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
134 | Add gamma
134 | Add gamma
135 |
135 |
136 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
136 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
137 Add alpha
137 Add alpha
138
138
139 If you run ``hg histedit --outgoing`` on the clone then it is the same
139 If you run ``hg histedit --outgoing`` on the clone then it is the same
140 as running ``hg histedit 836302820282``. If you need plan to push to a
140 as running ``hg histedit 836302820282``. If you need plan to push to a
141 repository that Mercurial does not detect to be related to the source
141 repository that Mercurial does not detect to be related to the source
142 repo, you can add a ``--force`` option.
142 repo, you can add a ``--force`` option.
143 """
143 """
144
144
145 try:
145 try:
146 import cPickle as pickle
146 import cPickle as pickle
147 pickle.dump # import now
147 pickle.dump # import now
148 except ImportError:
148 except ImportError:
149 import pickle
149 import pickle
150 import os
150 import os
151 import sys
151 import sys
152
152
153 from mercurial import cmdutil
153 from mercurial import cmdutil
154 from mercurial import discovery
154 from mercurial import discovery
155 from mercurial import error
155 from mercurial import error
156 from mercurial import copies
156 from mercurial import copies
157 from mercurial import context
157 from mercurial import context
158 from mercurial import hg
158 from mercurial import hg
159 from mercurial import node
159 from mercurial import node
160 from mercurial import repair
160 from mercurial import repair
161 from mercurial import scmutil
161 from mercurial import scmutil
162 from mercurial import util
162 from mercurial import util
163 from mercurial import obsolete
163 from mercurial import obsolete
164 from mercurial import merge as mergemod
164 from mercurial import merge as mergemod
165 from mercurial.lock import release
165 from mercurial.lock import release
166 from mercurial.i18n import _
166 from mercurial.i18n import _
167
167
168 cmdtable = {}
168 cmdtable = {}
169 command = cmdutil.command(cmdtable)
169 command = cmdutil.command(cmdtable)
170
170
171 testedwith = 'internal'
171 testedwith = 'internal'
172
172
173 # i18n: command names and abbreviations must remain untranslated
173 # i18n: command names and abbreviations must remain untranslated
174 editcomment = _("""# Edit history between %s and %s
174 editcomment = _("""# Edit history between %s and %s
175 #
175 #
176 # Commits are listed from least to most recent
176 # Commits are listed from least to most recent
177 #
177 #
178 # Commands:
178 # Commands:
179 # p, pick = use commit
179 # p, pick = use commit
180 # e, edit = use commit, but stop for amending
180 # e, edit = use commit, but stop for amending
181 # f, fold = use commit, but combine it with the one above
181 # f, fold = use commit, but combine it with the one above
182 # d, drop = remove commit from history
182 # d, drop = remove commit from history
183 # m, mess = edit message without changing commit content
183 # m, mess = edit message without changing commit content
184 #
184 #
185 """)
185 """)
186
186
187 def commitfuncfor(repo, src):
187 def commitfuncfor(repo, src):
188 """Build a commit function for the replacement of <src>
188 """Build a commit function for the replacement of <src>
189
189
190 This function ensure we apply the same treatment to all changesets.
190 This function ensure we apply the same treatment to all changesets.
191
191
192 - Add a 'histedit_source' entry in extra.
192 - Add a 'histedit_source' entry in extra.
193
193
194 Note that fold have its own separated logic because its handling is a bit
194 Note that fold have its own separated logic because its handling is a bit
195 different and not easily factored out of the fold method.
195 different and not easily factored out of the fold method.
196 """
196 """
197 phasemin = src.phase()
197 phasemin = src.phase()
198 def commitfunc(**kwargs):
198 def commitfunc(**kwargs):
199 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
199 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
200 try:
200 try:
201 repo.ui.setconfig('phases', 'new-commit', phasemin,
201 repo.ui.setconfig('phases', 'new-commit', phasemin,
202 'histedit')
202 'histedit')
203 extra = kwargs.get('extra', {}).copy()
203 extra = kwargs.get('extra', {}).copy()
204 extra['histedit_source'] = src.hex()
204 extra['histedit_source'] = src.hex()
205 kwargs['extra'] = extra
205 kwargs['extra'] = extra
206 return repo.commit(**kwargs)
206 return repo.commit(**kwargs)
207 finally:
207 finally:
208 repo.ui.restoreconfig(phasebackup)
208 repo.ui.restoreconfig(phasebackup)
209 return commitfunc
209 return commitfunc
210
210
211
211
212
212
213 def applychanges(ui, repo, ctx, opts):
213 def applychanges(ui, repo, ctx, opts):
214 """Merge changeset from ctx (only) in the current working directory"""
214 """Merge changeset from ctx (only) in the current working directory"""
215 wcpar = repo.dirstate.parents()[0]
215 wcpar = repo.dirstate.parents()[0]
216 if ctx.p1().node() == wcpar:
216 if ctx.p1().node() == wcpar:
217 # edition ar "in place" we do not need to make any merge,
217 # edition ar "in place" we do not need to make any merge,
218 # just applies changes on parent for edition
218 # just applies changes on parent for edition
219 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
219 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
220 stats = None
220 stats = None
221 else:
221 else:
222 try:
222 try:
223 # ui.forcemerge is an internal variable, do not document
223 # ui.forcemerge is an internal variable, do not document
224 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
224 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
225 'histedit')
225 'histedit')
226 stats = mergemod.update(repo, ctx.node(), True, True, False,
226 stats = mergemod.update(repo, ctx.node(), True, True, False,
227 ctx.p1().node())
227 ctx.p1().node())
228 finally:
228 finally:
229 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
229 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
230 repo.setparents(wcpar, node.nullid)
230 repo.setparents(wcpar, node.nullid)
231 repo.dirstate.write()
231 repo.dirstate.write()
232 # fix up dirstate for copies and renames
232 # fix up dirstate for copies and renames
233 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
233 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
234 return stats
234 return stats
235
235
236 def collapse(repo, first, last, commitopts):
236 def collapse(repo, first, last, commitopts):
237 """collapse the set of revisions from first to last as new one.
237 """collapse the set of revisions from first to last as new one.
238
238
239 Expected commit options are:
239 Expected commit options are:
240 - message
240 - message
241 - date
241 - date
242 - username
242 - username
243 Commit message is edited in all cases.
243 Commit message is edited in all cases.
244
244
245 This function works in memory."""
245 This function works in memory."""
246 ctxs = list(repo.set('%d::%d', first, last))
246 ctxs = list(repo.set('%d::%d', first, last))
247 if not ctxs:
247 if not ctxs:
248 return None
248 return None
249 base = first.parents()[0]
249 base = first.parents()[0]
250
250
251 # commit a new version of the old changeset, including the update
251 # commit a new version of the old changeset, including the update
252 # collect all files which might be affected
252 # collect all files which might be affected
253 files = set()
253 files = set()
254 for ctx in ctxs:
254 for ctx in ctxs:
255 files.update(ctx.files())
255 files.update(ctx.files())
256
256
257 # Recompute copies (avoid recording a -> b -> a)
257 # Recompute copies (avoid recording a -> b -> a)
258 copied = copies.pathcopies(base, last)
258 copied = copies.pathcopies(base, last)
259
259
260 # prune files which were reverted by the updates
260 # prune files which were reverted by the updates
261 def samefile(f):
261 def samefile(f):
262 if f in last.manifest():
262 if f in last.manifest():
263 a = last.filectx(f)
263 a = last.filectx(f)
264 if f in base.manifest():
264 if f in base.manifest():
265 b = base.filectx(f)
265 b = base.filectx(f)
266 return (a.data() == b.data()
266 return (a.data() == b.data()
267 and a.flags() == b.flags())
267 and a.flags() == b.flags())
268 else:
268 else:
269 return False
269 return False
270 else:
270 else:
271 return f not in base.manifest()
271 return f not in base.manifest()
272 files = [f for f in files if not samefile(f)]
272 files = [f for f in files if not samefile(f)]
273 # commit version of these files as defined by head
273 # commit version of these files as defined by head
274 headmf = last.manifest()
274 headmf = last.manifest()
275 def filectxfn(repo, ctx, path):
275 def filectxfn(repo, ctx, path):
276 if path in headmf:
276 if path in headmf:
277 fctx = last[path]
277 fctx = last[path]
278 flags = fctx.flags()
278 flags = fctx.flags()
279 mctx = context.memfilectx(repo,
279 mctx = context.memfilectx(repo,
280 fctx.path(), fctx.data(),
280 fctx.path(), fctx.data(),
281 islink='l' in flags,
281 islink='l' in flags,
282 isexec='x' in flags,
282 isexec='x' in flags,
283 copied=copied.get(path))
283 copied=copied.get(path))
284 return mctx
284 return mctx
285 raise IOError()
285 raise IOError()
286
286
287 if commitopts.get('message'):
287 if commitopts.get('message'):
288 message = commitopts['message']
288 message = commitopts['message']
289 else:
289 else:
290 message = first.description()
290 message = first.description()
291 user = commitopts.get('user')
291 user = commitopts.get('user')
292 date = commitopts.get('date')
292 date = commitopts.get('date')
293 extra = commitopts.get('extra')
293 extra = commitopts.get('extra')
294
294
295 parents = (first.p1().node(), first.p2().node())
295 parents = (first.p1().node(), first.p2().node())
296 new = context.memctx(repo,
296 new = context.memctx(repo,
297 parents=parents,
297 parents=parents,
298 text=message,
298 text=message,
299 files=files,
299 files=files,
300 filectxfn=filectxfn,
300 filectxfn=filectxfn,
301 user=user,
301 user=user,
302 date=date,
302 date=date,
303 extra=extra,
303 extra=extra,
304 editor=cmdutil.getcommiteditor(edit=True))
304 editor=cmdutil.getcommiteditor(edit=True))
305 return repo.commitctx(new)
305 return repo.commitctx(new)
306
306
307 def pick(ui, repo, ctx, ha, opts):
307 def pick(ui, repo, ctx, ha, opts):
308 oldctx = repo[ha]
308 oldctx = repo[ha]
309 if oldctx.parents()[0] == ctx:
309 if oldctx.parents()[0] == ctx:
310 ui.debug('node %s unchanged\n' % ha)
310 ui.debug('node %s unchanged\n' % ha)
311 return oldctx, []
311 return oldctx, []
312 hg.update(repo, ctx.node())
312 hg.update(repo, ctx.node())
313 stats = applychanges(ui, repo, oldctx, opts)
313 stats = applychanges(ui, repo, oldctx, opts)
314 if stats and stats[3] > 0:
314 if stats and stats[3] > 0:
315 raise error.InterventionRequired(_('Fix up the change and run '
315 raise error.InterventionRequired(_('Fix up the change and run '
316 'hg histedit --continue'))
316 'hg histedit --continue'))
317 # drop the second merge parent
317 # drop the second merge parent
318 commit = commitfuncfor(repo, oldctx)
318 commit = commitfuncfor(repo, oldctx)
319 n = commit(text=oldctx.description(), user=oldctx.user(),
319 n = commit(text=oldctx.description(), user=oldctx.user(),
320 date=oldctx.date(), extra=oldctx.extra())
320 date=oldctx.date(), extra=oldctx.extra())
321 if n is None:
321 if n is None:
322 ui.warn(_('%s: empty changeset\n')
322 ui.warn(_('%s: empty changeset\n')
323 % node.hex(ha))
323 % node.hex(ha))
324 return ctx, []
324 return ctx, []
325 new = repo[n]
325 new = repo[n]
326 return new, [(oldctx.node(), (n,))]
326 return new, [(oldctx.node(), (n,))]
327
327
328
328
329 def edit(ui, repo, ctx, ha, opts):
329 def edit(ui, repo, ctx, ha, opts):
330 oldctx = repo[ha]
330 oldctx = repo[ha]
331 hg.update(repo, ctx.node())
331 hg.update(repo, ctx.node())
332 applychanges(ui, repo, oldctx, opts)
332 applychanges(ui, repo, oldctx, opts)
333 raise error.InterventionRequired(
333 raise error.InterventionRequired(
334 _('Make changes as needed, you may commit or record as needed now.\n'
334 _('Make changes as needed, you may commit or record as needed now.\n'
335 'When you are finished, run hg histedit --continue to resume.'))
335 'When you are finished, run hg histedit --continue to resume.'))
336
336
337 def fold(ui, repo, ctx, ha, opts):
337 def fold(ui, repo, ctx, ha, opts):
338 oldctx = repo[ha]
338 oldctx = repo[ha]
339 hg.update(repo, ctx.node())
339 hg.update(repo, ctx.node())
340 stats = applychanges(ui, repo, oldctx, opts)
340 stats = applychanges(ui, repo, oldctx, opts)
341 if stats and stats[3] > 0:
341 if stats and stats[3] > 0:
342 raise error.InterventionRequired(
342 raise error.InterventionRequired(
343 _('Fix up the change and run hg histedit --continue'))
343 _('Fix up the change and run hg histedit --continue'))
344 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
344 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
345 date=oldctx.date(), extra=oldctx.extra())
345 date=oldctx.date(), extra=oldctx.extra())
346 if n is None:
346 if n is None:
347 ui.warn(_('%s: empty changeset')
347 ui.warn(_('%s: empty changeset')
348 % node.hex(ha))
348 % node.hex(ha))
349 return ctx, []
349 return ctx, []
350 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
350 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
351
351
352 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
352 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
353 parent = ctx.parents()[0].node()
353 parent = ctx.parents()[0].node()
354 hg.update(repo, parent)
354 hg.update(repo, parent)
355 ### prepare new commit data
355 ### prepare new commit data
356 commitopts = opts.copy()
356 commitopts = opts.copy()
357 # username
357 commitopts['user'] = ctx.user()
358 if ctx.user() == oldctx.user():
359 username = ctx.user()
360 else:
361 username = ui.username()
362 commitopts['user'] = username
363 # commit message
358 # commit message
364 newmessage = '\n***\n'.join(
359 newmessage = '\n***\n'.join(
365 [ctx.description()] +
360 [ctx.description()] +
366 [repo[r].description() for r in internalchanges] +
361 [repo[r].description() for r in internalchanges] +
367 [oldctx.description()]) + '\n'
362 [oldctx.description()]) + '\n'
368 commitopts['message'] = newmessage
363 commitopts['message'] = newmessage
369 # date
364 # date
370 commitopts['date'] = max(ctx.date(), oldctx.date())
365 commitopts['date'] = max(ctx.date(), oldctx.date())
371 extra = ctx.extra().copy()
366 extra = ctx.extra().copy()
372 # histedit_source
367 # histedit_source
373 # note: ctx is likely a temporary commit but that the best we can do here
368 # note: ctx is likely a temporary commit but that the best we can do here
374 # This is sufficient to solve issue3681 anyway
369 # This is sufficient to solve issue3681 anyway
375 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
370 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
376 commitopts['extra'] = extra
371 commitopts['extra'] = extra
377 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
372 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
378 try:
373 try:
379 phasemin = max(ctx.phase(), oldctx.phase())
374 phasemin = max(ctx.phase(), oldctx.phase())
380 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
375 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
381 n = collapse(repo, ctx, repo[newnode], commitopts)
376 n = collapse(repo, ctx, repo[newnode], commitopts)
382 finally:
377 finally:
383 repo.ui.restoreconfig(phasebackup)
378 repo.ui.restoreconfig(phasebackup)
384 if n is None:
379 if n is None:
385 return ctx, []
380 return ctx, []
386 hg.update(repo, n)
381 hg.update(repo, n)
387 replacements = [(oldctx.node(), (newnode,)),
382 replacements = [(oldctx.node(), (newnode,)),
388 (ctx.node(), (n,)),
383 (ctx.node(), (n,)),
389 (newnode, (n,)),
384 (newnode, (n,)),
390 ]
385 ]
391 for ich in internalchanges:
386 for ich in internalchanges:
392 replacements.append((ich, (n,)))
387 replacements.append((ich, (n,)))
393 return repo[n], replacements
388 return repo[n], replacements
394
389
395 def drop(ui, repo, ctx, ha, opts):
390 def drop(ui, repo, ctx, ha, opts):
396 return ctx, [(repo[ha].node(), ())]
391 return ctx, [(repo[ha].node(), ())]
397
392
398
393
399 def message(ui, repo, ctx, ha, opts):
394 def message(ui, repo, ctx, ha, opts):
400 oldctx = repo[ha]
395 oldctx = repo[ha]
401 hg.update(repo, ctx.node())
396 hg.update(repo, ctx.node())
402 stats = applychanges(ui, repo, oldctx, opts)
397 stats = applychanges(ui, repo, oldctx, opts)
403 if stats and stats[3] > 0:
398 if stats and stats[3] > 0:
404 raise error.InterventionRequired(
399 raise error.InterventionRequired(
405 _('Fix up the change and run hg histedit --continue'))
400 _('Fix up the change and run hg histedit --continue'))
406 message = oldctx.description()
401 message = oldctx.description()
407 commit = commitfuncfor(repo, oldctx)
402 commit = commitfuncfor(repo, oldctx)
408 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
403 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
409 extra=oldctx.extra(),
404 extra=oldctx.extra(),
410 editor=cmdutil.getcommiteditor(edit=True))
405 editor=cmdutil.getcommiteditor(edit=True))
411 newctx = repo[new]
406 newctx = repo[new]
412 if oldctx.node() != newctx.node():
407 if oldctx.node() != newctx.node():
413 return newctx, [(oldctx.node(), (new,))]
408 return newctx, [(oldctx.node(), (new,))]
414 # We didn't make an edit, so just indicate no replaced nodes
409 # We didn't make an edit, so just indicate no replaced nodes
415 return newctx, []
410 return newctx, []
416
411
417 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
412 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
418 """utility function to find the first outgoing changeset
413 """utility function to find the first outgoing changeset
419
414
420 Used by initialisation code"""
415 Used by initialisation code"""
421 dest = ui.expandpath(remote or 'default-push', remote or 'default')
416 dest = ui.expandpath(remote or 'default-push', remote or 'default')
422 dest, revs = hg.parseurl(dest, None)[:2]
417 dest, revs = hg.parseurl(dest, None)[:2]
423 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
418 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
424
419
425 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
420 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
426 other = hg.peer(repo, opts, dest)
421 other = hg.peer(repo, opts, dest)
427
422
428 if revs:
423 if revs:
429 revs = [repo.lookup(rev) for rev in revs]
424 revs = [repo.lookup(rev) for rev in revs]
430
425
431 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
426 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
432 if not outgoing.missing:
427 if not outgoing.missing:
433 raise util.Abort(_('no outgoing ancestors'))
428 raise util.Abort(_('no outgoing ancestors'))
434 roots = list(repo.revs("roots(%ln)", outgoing.missing))
429 roots = list(repo.revs("roots(%ln)", outgoing.missing))
435 if 1 < len(roots):
430 if 1 < len(roots):
436 msg = _('there are ambiguous outgoing revisions')
431 msg = _('there are ambiguous outgoing revisions')
437 hint = _('see "hg help histedit" for more detail')
432 hint = _('see "hg help histedit" for more detail')
438 raise util.Abort(msg, hint=hint)
433 raise util.Abort(msg, hint=hint)
439 return repo.lookup(roots[0])
434 return repo.lookup(roots[0])
440
435
441 actiontable = {'p': pick,
436 actiontable = {'p': pick,
442 'pick': pick,
437 'pick': pick,
443 'e': edit,
438 'e': edit,
444 'edit': edit,
439 'edit': edit,
445 'f': fold,
440 'f': fold,
446 'fold': fold,
441 'fold': fold,
447 'd': drop,
442 'd': drop,
448 'drop': drop,
443 'drop': drop,
449 'm': message,
444 'm': message,
450 'mess': message,
445 'mess': message,
451 }
446 }
452
447
453 @command('histedit',
448 @command('histedit',
454 [('', 'commands', '',
449 [('', 'commands', '',
455 _('Read history edits from the specified file.')),
450 _('Read history edits from the specified file.')),
456 ('c', 'continue', False, _('continue an edit already in progress')),
451 ('c', 'continue', False, _('continue an edit already in progress')),
457 ('k', 'keep', False,
452 ('k', 'keep', False,
458 _("don't strip old nodes after edit is complete")),
453 _("don't strip old nodes after edit is complete")),
459 ('', 'abort', False, _('abort an edit in progress')),
454 ('', 'abort', False, _('abort an edit in progress')),
460 ('o', 'outgoing', False, _('changesets not found in destination')),
455 ('o', 'outgoing', False, _('changesets not found in destination')),
461 ('f', 'force', False,
456 ('f', 'force', False,
462 _('force outgoing even for unrelated repositories')),
457 _('force outgoing even for unrelated repositories')),
463 ('r', 'rev', [], _('first revision to be edited'))],
458 ('r', 'rev', [], _('first revision to be edited'))],
464 _("ANCESTOR | --outgoing [URL]"))
459 _("ANCESTOR | --outgoing [URL]"))
465 def histedit(ui, repo, *freeargs, **opts):
460 def histedit(ui, repo, *freeargs, **opts):
466 """interactively edit changeset history
461 """interactively edit changeset history
467
462
468 This command edits changesets between ANCESTOR and the parent of
463 This command edits changesets between ANCESTOR and the parent of
469 the working directory.
464 the working directory.
470
465
471 With --outgoing, this edits changesets not found in the
466 With --outgoing, this edits changesets not found in the
472 destination repository. If URL of the destination is omitted, the
467 destination repository. If URL of the destination is omitted, the
473 'default-push' (or 'default') path will be used.
468 'default-push' (or 'default') path will be used.
474
469
475 For safety, this command is aborted, also if there are ambiguous
470 For safety, this command is aborted, also if there are ambiguous
476 outgoing revisions which may confuse users: for example, there are
471 outgoing revisions which may confuse users: for example, there are
477 multiple branches containing outgoing revisions.
472 multiple branches containing outgoing revisions.
478
473
479 Use "min(outgoing() and ::.)" or similar revset specification
474 Use "min(outgoing() and ::.)" or similar revset specification
480 instead of --outgoing to specify edit target revision exactly in
475 instead of --outgoing to specify edit target revision exactly in
481 such ambiguous situation. See :hg:`help revsets` for detail about
476 such ambiguous situation. See :hg:`help revsets` for detail about
482 selecting revisions.
477 selecting revisions.
483
478
484 Returns 0 on success, 1 if user intervention is required (not only
479 Returns 0 on success, 1 if user intervention is required (not only
485 for intentional "edit" command, but also for resolving unexpected
480 for intentional "edit" command, but also for resolving unexpected
486 conflicts).
481 conflicts).
487 """
482 """
488 lock = wlock = None
483 lock = wlock = None
489 try:
484 try:
490 wlock = repo.wlock()
485 wlock = repo.wlock()
491 lock = repo.lock()
486 lock = repo.lock()
492 _histedit(ui, repo, *freeargs, **opts)
487 _histedit(ui, repo, *freeargs, **opts)
493 finally:
488 finally:
494 release(lock, wlock)
489 release(lock, wlock)
495
490
496 def _histedit(ui, repo, *freeargs, **opts):
491 def _histedit(ui, repo, *freeargs, **opts):
497 # TODO only abort if we try and histedit mq patches, not just
492 # TODO only abort if we try and histedit mq patches, not just
498 # blanket if mq patches are applied somewhere
493 # blanket if mq patches are applied somewhere
499 mq = getattr(repo, 'mq', None)
494 mq = getattr(repo, 'mq', None)
500 if mq and mq.applied:
495 if mq and mq.applied:
501 raise util.Abort(_('source has mq patches applied'))
496 raise util.Abort(_('source has mq patches applied'))
502
497
503 # basic argument incompatibility processing
498 # basic argument incompatibility processing
504 outg = opts.get('outgoing')
499 outg = opts.get('outgoing')
505 cont = opts.get('continue')
500 cont = opts.get('continue')
506 abort = opts.get('abort')
501 abort = opts.get('abort')
507 force = opts.get('force')
502 force = opts.get('force')
508 rules = opts.get('commands', '')
503 rules = opts.get('commands', '')
509 revs = opts.get('rev', [])
504 revs = opts.get('rev', [])
510 goal = 'new' # This invocation goal, in new, continue, abort
505 goal = 'new' # This invocation goal, in new, continue, abort
511 if force and not outg:
506 if force and not outg:
512 raise util.Abort(_('--force only allowed with --outgoing'))
507 raise util.Abort(_('--force only allowed with --outgoing'))
513 if cont:
508 if cont:
514 if util.any((outg, abort, revs, freeargs, rules)):
509 if util.any((outg, abort, revs, freeargs, rules)):
515 raise util.Abort(_('no arguments allowed with --continue'))
510 raise util.Abort(_('no arguments allowed with --continue'))
516 goal = 'continue'
511 goal = 'continue'
517 elif abort:
512 elif abort:
518 if util.any((outg, revs, freeargs, rules)):
513 if util.any((outg, revs, freeargs, rules)):
519 raise util.Abort(_('no arguments allowed with --abort'))
514 raise util.Abort(_('no arguments allowed with --abort'))
520 goal = 'abort'
515 goal = 'abort'
521 else:
516 else:
522 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
517 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
523 raise util.Abort(_('history edit already in progress, try '
518 raise util.Abort(_('history edit already in progress, try '
524 '--continue or --abort'))
519 '--continue or --abort'))
525 if outg:
520 if outg:
526 if revs:
521 if revs:
527 raise util.Abort(_('no revisions allowed with --outgoing'))
522 raise util.Abort(_('no revisions allowed with --outgoing'))
528 if len(freeargs) > 1:
523 if len(freeargs) > 1:
529 raise util.Abort(
524 raise util.Abort(
530 _('only one repo argument allowed with --outgoing'))
525 _('only one repo argument allowed with --outgoing'))
531 else:
526 else:
532 revs.extend(freeargs)
527 revs.extend(freeargs)
533 if len(revs) != 1:
528 if len(revs) != 1:
534 raise util.Abort(
529 raise util.Abort(
535 _('histedit requires exactly one ancestor revision'))
530 _('histedit requires exactly one ancestor revision'))
536
531
537
532
538 if goal == 'continue':
533 if goal == 'continue':
539 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
534 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
540 parentctx = repo[parentctxnode]
535 parentctx = repo[parentctxnode]
541 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
536 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
542 replacements.extend(repl)
537 replacements.extend(repl)
543 elif goal == 'abort':
538 elif goal == 'abort':
544 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
539 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
545 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
540 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
546 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
541 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
547 # check whether we should update away
542 # check whether we should update away
548 parentnodes = [c.node() for c in repo[None].parents()]
543 parentnodes = [c.node() for c in repo[None].parents()]
549 for n in leafs | set([parentctxnode]):
544 for n in leafs | set([parentctxnode]):
550 if n in parentnodes:
545 if n in parentnodes:
551 hg.clean(repo, topmost)
546 hg.clean(repo, topmost)
552 break
547 break
553 else:
548 else:
554 pass
549 pass
555 cleanupnode(ui, repo, 'created', tmpnodes)
550 cleanupnode(ui, repo, 'created', tmpnodes)
556 cleanupnode(ui, repo, 'temp', leafs)
551 cleanupnode(ui, repo, 'temp', leafs)
557 os.unlink(os.path.join(repo.path, 'histedit-state'))
552 os.unlink(os.path.join(repo.path, 'histedit-state'))
558 return
553 return
559 else:
554 else:
560 cmdutil.checkunfinished(repo)
555 cmdutil.checkunfinished(repo)
561 cmdutil.bailifchanged(repo)
556 cmdutil.bailifchanged(repo)
562
557
563 topmost, empty = repo.dirstate.parents()
558 topmost, empty = repo.dirstate.parents()
564 if outg:
559 if outg:
565 if freeargs:
560 if freeargs:
566 remote = freeargs[0]
561 remote = freeargs[0]
567 else:
562 else:
568 remote = None
563 remote = None
569 root = findoutgoing(ui, repo, remote, force, opts)
564 root = findoutgoing(ui, repo, remote, force, opts)
570 else:
565 else:
571 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
566 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
572 if len(rr) != 1:
567 if len(rr) != 1:
573 raise util.Abort(_('The specified revisions must have '
568 raise util.Abort(_('The specified revisions must have '
574 'exactly one common root'))
569 'exactly one common root'))
575 root = rr[0].node()
570 root = rr[0].node()
576
571
577 keep = opts.get('keep', False)
572 keep = opts.get('keep', False)
578 revs = between(repo, root, topmost, keep)
573 revs = between(repo, root, topmost, keep)
579 if not revs:
574 if not revs:
580 raise util.Abort(_('%s is not an ancestor of working directory') %
575 raise util.Abort(_('%s is not an ancestor of working directory') %
581 node.short(root))
576 node.short(root))
582
577
583 ctxs = [repo[r] for r in revs]
578 ctxs = [repo[r] for r in revs]
584 if not rules:
579 if not rules:
585 rules = '\n'.join([makedesc(c) for c in ctxs])
580 rules = '\n'.join([makedesc(c) for c in ctxs])
586 rules += '\n\n'
581 rules += '\n\n'
587 rules += editcomment % (node.short(root), node.short(topmost))
582 rules += editcomment % (node.short(root), node.short(topmost))
588 rules = ui.edit(rules, ui.username())
583 rules = ui.edit(rules, ui.username())
589 # Save edit rules in .hg/histedit-last-edit.txt in case
584 # Save edit rules in .hg/histedit-last-edit.txt in case
590 # the user needs to ask for help after something
585 # the user needs to ask for help after something
591 # surprising happens.
586 # surprising happens.
592 f = open(repo.join('histedit-last-edit.txt'), 'w')
587 f = open(repo.join('histedit-last-edit.txt'), 'w')
593 f.write(rules)
588 f.write(rules)
594 f.close()
589 f.close()
595 else:
590 else:
596 if rules == '-':
591 if rules == '-':
597 f = sys.stdin
592 f = sys.stdin
598 else:
593 else:
599 f = open(rules)
594 f = open(rules)
600 rules = f.read()
595 rules = f.read()
601 f.close()
596 f.close()
602 rules = [l for l in (r.strip() for r in rules.splitlines())
597 rules = [l for l in (r.strip() for r in rules.splitlines())
603 if l and not l[0] == '#']
598 if l and not l[0] == '#']
604 rules = verifyrules(rules, repo, ctxs)
599 rules = verifyrules(rules, repo, ctxs)
605
600
606 parentctx = repo[root].parents()[0]
601 parentctx = repo[root].parents()[0]
607 keep = opts.get('keep', False)
602 keep = opts.get('keep', False)
608 replacements = []
603 replacements = []
609
604
610
605
611 while rules:
606 while rules:
612 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
607 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
613 action, ha = rules.pop(0)
608 action, ha = rules.pop(0)
614 ui.debug('histedit: processing %s %s\n' % (action, ha))
609 ui.debug('histedit: processing %s %s\n' % (action, ha))
615 actfunc = actiontable[action]
610 actfunc = actiontable[action]
616 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
611 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
617 replacements.extend(replacement_)
612 replacements.extend(replacement_)
618
613
619 hg.update(repo, parentctx.node())
614 hg.update(repo, parentctx.node())
620
615
621 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
616 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
622 if mapping:
617 if mapping:
623 for prec, succs in mapping.iteritems():
618 for prec, succs in mapping.iteritems():
624 if not succs:
619 if not succs:
625 ui.debug('histedit: %s is dropped\n' % node.short(prec))
620 ui.debug('histedit: %s is dropped\n' % node.short(prec))
626 else:
621 else:
627 ui.debug('histedit: %s is replaced by %s\n' % (
622 ui.debug('histedit: %s is replaced by %s\n' % (
628 node.short(prec), node.short(succs[0])))
623 node.short(prec), node.short(succs[0])))
629 if len(succs) > 1:
624 if len(succs) > 1:
630 m = 'histedit: %s'
625 m = 'histedit: %s'
631 for n in succs[1:]:
626 for n in succs[1:]:
632 ui.debug(m % node.short(n))
627 ui.debug(m % node.short(n))
633
628
634 if not keep:
629 if not keep:
635 if mapping:
630 if mapping:
636 movebookmarks(ui, repo, mapping, topmost, ntm)
631 movebookmarks(ui, repo, mapping, topmost, ntm)
637 # TODO update mq state
632 # TODO update mq state
638 if obsolete._enabled:
633 if obsolete._enabled:
639 markers = []
634 markers = []
640 # sort by revision number because it sound "right"
635 # sort by revision number because it sound "right"
641 for prec in sorted(mapping, key=repo.changelog.rev):
636 for prec in sorted(mapping, key=repo.changelog.rev):
642 succs = mapping[prec]
637 succs = mapping[prec]
643 markers.append((repo[prec],
638 markers.append((repo[prec],
644 tuple(repo[s] for s in succs)))
639 tuple(repo[s] for s in succs)))
645 if markers:
640 if markers:
646 obsolete.createmarkers(repo, markers)
641 obsolete.createmarkers(repo, markers)
647 else:
642 else:
648 cleanupnode(ui, repo, 'replaced', mapping)
643 cleanupnode(ui, repo, 'replaced', mapping)
649
644
650 cleanupnode(ui, repo, 'temp', tmpnodes)
645 cleanupnode(ui, repo, 'temp', tmpnodes)
651 os.unlink(os.path.join(repo.path, 'histedit-state'))
646 os.unlink(os.path.join(repo.path, 'histedit-state'))
652 if os.path.exists(repo.sjoin('undo')):
647 if os.path.exists(repo.sjoin('undo')):
653 os.unlink(repo.sjoin('undo'))
648 os.unlink(repo.sjoin('undo'))
654
649
655 def gatherchildren(repo, ctx):
650 def gatherchildren(repo, ctx):
656 # is there any new commit between the expected parent and "."
651 # is there any new commit between the expected parent and "."
657 #
652 #
658 # note: does not take non linear new change in account (but previous
653 # note: does not take non linear new change in account (but previous
659 # implementation didn't used them anyway (issue3655)
654 # implementation didn't used them anyway (issue3655)
660 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
655 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
661 if ctx.node() != node.nullid:
656 if ctx.node() != node.nullid:
662 if not newchildren:
657 if not newchildren:
663 # `ctx` should match but no result. This means that
658 # `ctx` should match but no result. This means that
664 # currentnode is not a descendant from ctx.
659 # currentnode is not a descendant from ctx.
665 msg = _('%s is not an ancestor of working directory')
660 msg = _('%s is not an ancestor of working directory')
666 hint = _('use "histedit --abort" to clear broken state')
661 hint = _('use "histedit --abort" to clear broken state')
667 raise util.Abort(msg % ctx, hint=hint)
662 raise util.Abort(msg % ctx, hint=hint)
668 newchildren.pop(0) # remove ctx
663 newchildren.pop(0) # remove ctx
669 return newchildren
664 return newchildren
670
665
671 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
666 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
672 action, currentnode = rules.pop(0)
667 action, currentnode = rules.pop(0)
673 ctx = repo[currentnode]
668 ctx = repo[currentnode]
674
669
675 newchildren = gatherchildren(repo, parentctx)
670 newchildren = gatherchildren(repo, parentctx)
676
671
677 # Commit dirty working directory if necessary
672 # Commit dirty working directory if necessary
678 new = None
673 new = None
679 m, a, r, d = repo.status()[:4]
674 m, a, r, d = repo.status()[:4]
680 if m or a or r or d:
675 if m or a or r or d:
681 # prepare the message for the commit to comes
676 # prepare the message for the commit to comes
682 if action in ('f', 'fold'):
677 if action in ('f', 'fold'):
683 message = 'fold-temp-revision %s' % currentnode
678 message = 'fold-temp-revision %s' % currentnode
684 else:
679 else:
685 message = ctx.description()
680 message = ctx.description()
686 editopt = action in ('e', 'edit', 'm', 'mess')
681 editopt = action in ('e', 'edit', 'm', 'mess')
687 editor = cmdutil.getcommiteditor(edit=editopt)
682 editor = cmdutil.getcommiteditor(edit=editopt)
688 commit = commitfuncfor(repo, ctx)
683 commit = commitfuncfor(repo, ctx)
689 new = commit(text=message, user=ctx.user(),
684 new = commit(text=message, user=ctx.user(),
690 date=ctx.date(), extra=ctx.extra(),
685 date=ctx.date(), extra=ctx.extra(),
691 editor=editor)
686 editor=editor)
692 if new is not None:
687 if new is not None:
693 newchildren.append(new)
688 newchildren.append(new)
694
689
695 replacements = []
690 replacements = []
696 # track replacements
691 # track replacements
697 if ctx.node() not in newchildren:
692 if ctx.node() not in newchildren:
698 # note: new children may be empty when the changeset is dropped.
693 # note: new children may be empty when the changeset is dropped.
699 # this happen e.g during conflicting pick where we revert content
694 # this happen e.g during conflicting pick where we revert content
700 # to parent.
695 # to parent.
701 replacements.append((ctx.node(), tuple(newchildren)))
696 replacements.append((ctx.node(), tuple(newchildren)))
702
697
703 if action in ('f', 'fold'):
698 if action in ('f', 'fold'):
704 if newchildren:
699 if newchildren:
705 # finalize fold operation if applicable
700 # finalize fold operation if applicable
706 if new is None:
701 if new is None:
707 new = newchildren[-1]
702 new = newchildren[-1]
708 else:
703 else:
709 newchildren.pop() # remove new from internal changes
704 newchildren.pop() # remove new from internal changes
710 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
705 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
711 newchildren)
706 newchildren)
712 replacements.extend(repl)
707 replacements.extend(repl)
713 else:
708 else:
714 # newchildren is empty if the fold did not result in any commit
709 # newchildren is empty if the fold did not result in any commit
715 # this happen when all folded change are discarded during the
710 # this happen when all folded change are discarded during the
716 # merge.
711 # merge.
717 replacements.append((ctx.node(), (parentctx.node(),)))
712 replacements.append((ctx.node(), (parentctx.node(),)))
718 elif newchildren:
713 elif newchildren:
719 # otherwise update "parentctx" before proceeding to further operation
714 # otherwise update "parentctx" before proceeding to further operation
720 parentctx = repo[newchildren[-1]]
715 parentctx = repo[newchildren[-1]]
721 return parentctx, replacements
716 return parentctx, replacements
722
717
723
718
724 def between(repo, old, new, keep):
719 def between(repo, old, new, keep):
725 """select and validate the set of revision to edit
720 """select and validate the set of revision to edit
726
721
727 When keep is false, the specified set can't have children."""
722 When keep is false, the specified set can't have children."""
728 ctxs = list(repo.set('%n::%n', old, new))
723 ctxs = list(repo.set('%n::%n', old, new))
729 if ctxs and not keep:
724 if ctxs and not keep:
730 if (not obsolete._enabled and
725 if (not obsolete._enabled and
731 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
726 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
732 raise util.Abort(_('cannot edit history that would orphan nodes'))
727 raise util.Abort(_('cannot edit history that would orphan nodes'))
733 if repo.revs('(%ld) and merge()', ctxs):
728 if repo.revs('(%ld) and merge()', ctxs):
734 raise util.Abort(_('cannot edit history that contains merges'))
729 raise util.Abort(_('cannot edit history that contains merges'))
735 root = ctxs[0] # list is already sorted by repo.set
730 root = ctxs[0] # list is already sorted by repo.set
736 if not root.phase():
731 if not root.phase():
737 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
732 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
738 return [c.node() for c in ctxs]
733 return [c.node() for c in ctxs]
739
734
740
735
741 def writestate(repo, parentnode, rules, keep, topmost, replacements):
736 def writestate(repo, parentnode, rules, keep, topmost, replacements):
742 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
737 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
743 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
738 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
744 fp.close()
739 fp.close()
745
740
746 def readstate(repo):
741 def readstate(repo):
747 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
742 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
748 """
743 """
749 fp = open(os.path.join(repo.path, 'histedit-state'))
744 fp = open(os.path.join(repo.path, 'histedit-state'))
750 return pickle.load(fp)
745 return pickle.load(fp)
751
746
752
747
753 def makedesc(c):
748 def makedesc(c):
754 """build a initial action line for a ctx `c`
749 """build a initial action line for a ctx `c`
755
750
756 line are in the form:
751 line are in the form:
757
752
758 pick <hash> <rev> <summary>
753 pick <hash> <rev> <summary>
759 """
754 """
760 summary = ''
755 summary = ''
761 if c.description():
756 if c.description():
762 summary = c.description().splitlines()[0]
757 summary = c.description().splitlines()[0]
763 line = 'pick %s %d %s' % (c, c.rev(), summary)
758 line = 'pick %s %d %s' % (c, c.rev(), summary)
764 # trim to 80 columns so it's not stupidly wide in my editor
759 # trim to 80 columns so it's not stupidly wide in my editor
765 return util.ellipsis(line, 80)
760 return util.ellipsis(line, 80)
766
761
767 def verifyrules(rules, repo, ctxs):
762 def verifyrules(rules, repo, ctxs):
768 """Verify that there exists exactly one edit rule per given changeset.
763 """Verify that there exists exactly one edit rule per given changeset.
769
764
770 Will abort if there are to many or too few rules, a malformed rule,
765 Will abort if there are to many or too few rules, a malformed rule,
771 or a rule on a changeset outside of the user-given range.
766 or a rule on a changeset outside of the user-given range.
772 """
767 """
773 parsed = []
768 parsed = []
774 expected = set(str(c) for c in ctxs)
769 expected = set(str(c) for c in ctxs)
775 seen = set()
770 seen = set()
776 for r in rules:
771 for r in rules:
777 if ' ' not in r:
772 if ' ' not in r:
778 raise util.Abort(_('malformed line "%s"') % r)
773 raise util.Abort(_('malformed line "%s"') % r)
779 action, rest = r.split(' ', 1)
774 action, rest = r.split(' ', 1)
780 ha = rest.strip().split(' ', 1)[0]
775 ha = rest.strip().split(' ', 1)[0]
781 try:
776 try:
782 ha = str(repo[ha]) # ensure its a short hash
777 ha = str(repo[ha]) # ensure its a short hash
783 except error.RepoError:
778 except error.RepoError:
784 raise util.Abort(_('unknown changeset %s listed') % ha)
779 raise util.Abort(_('unknown changeset %s listed') % ha)
785 if ha not in expected:
780 if ha not in expected:
786 raise util.Abort(
781 raise util.Abort(
787 _('may not use changesets other than the ones listed'))
782 _('may not use changesets other than the ones listed'))
788 if ha in seen:
783 if ha in seen:
789 raise util.Abort(_('duplicated command for changeset %s') % ha)
784 raise util.Abort(_('duplicated command for changeset %s') % ha)
790 seen.add(ha)
785 seen.add(ha)
791 if action not in actiontable:
786 if action not in actiontable:
792 raise util.Abort(_('unknown action "%s"') % action)
787 raise util.Abort(_('unknown action "%s"') % action)
793 parsed.append([action, ha])
788 parsed.append([action, ha])
794 missing = sorted(expected - seen) # sort to stabilize output
789 missing = sorted(expected - seen) # sort to stabilize output
795 if missing:
790 if missing:
796 raise util.Abort(_('missing rules for changeset %s') % missing[0],
791 raise util.Abort(_('missing rules for changeset %s') % missing[0],
797 hint=_('do you want to use the drop action?'))
792 hint=_('do you want to use the drop action?'))
798 return parsed
793 return parsed
799
794
800 def processreplacement(repo, replacements):
795 def processreplacement(repo, replacements):
801 """process the list of replacements to return
796 """process the list of replacements to return
802
797
803 1) the final mapping between original and created nodes
798 1) the final mapping between original and created nodes
804 2) the list of temporary node created by histedit
799 2) the list of temporary node created by histedit
805 3) the list of new commit created by histedit"""
800 3) the list of new commit created by histedit"""
806 allsuccs = set()
801 allsuccs = set()
807 replaced = set()
802 replaced = set()
808 fullmapping = {}
803 fullmapping = {}
809 # initialise basic set
804 # initialise basic set
810 # fullmapping record all operation recorded in replacement
805 # fullmapping record all operation recorded in replacement
811 for rep in replacements:
806 for rep in replacements:
812 allsuccs.update(rep[1])
807 allsuccs.update(rep[1])
813 replaced.add(rep[0])
808 replaced.add(rep[0])
814 fullmapping.setdefault(rep[0], set()).update(rep[1])
809 fullmapping.setdefault(rep[0], set()).update(rep[1])
815 new = allsuccs - replaced
810 new = allsuccs - replaced
816 tmpnodes = allsuccs & replaced
811 tmpnodes = allsuccs & replaced
817 # Reduce content fullmapping into direct relation between original nodes
812 # Reduce content fullmapping into direct relation between original nodes
818 # and final node created during history edition
813 # and final node created during history edition
819 # Dropped changeset are replaced by an empty list
814 # Dropped changeset are replaced by an empty list
820 toproceed = set(fullmapping)
815 toproceed = set(fullmapping)
821 final = {}
816 final = {}
822 while toproceed:
817 while toproceed:
823 for x in list(toproceed):
818 for x in list(toproceed):
824 succs = fullmapping[x]
819 succs = fullmapping[x]
825 for s in list(succs):
820 for s in list(succs):
826 if s in toproceed:
821 if s in toproceed:
827 # non final node with unknown closure
822 # non final node with unknown closure
828 # We can't process this now
823 # We can't process this now
829 break
824 break
830 elif s in final:
825 elif s in final:
831 # non final node, replace with closure
826 # non final node, replace with closure
832 succs.remove(s)
827 succs.remove(s)
833 succs.update(final[s])
828 succs.update(final[s])
834 else:
829 else:
835 final[x] = succs
830 final[x] = succs
836 toproceed.remove(x)
831 toproceed.remove(x)
837 # remove tmpnodes from final mapping
832 # remove tmpnodes from final mapping
838 for n in tmpnodes:
833 for n in tmpnodes:
839 del final[n]
834 del final[n]
840 # we expect all changes involved in final to exist in the repo
835 # we expect all changes involved in final to exist in the repo
841 # turn `final` into list (topologically sorted)
836 # turn `final` into list (topologically sorted)
842 nm = repo.changelog.nodemap
837 nm = repo.changelog.nodemap
843 for prec, succs in final.items():
838 for prec, succs in final.items():
844 final[prec] = sorted(succs, key=nm.get)
839 final[prec] = sorted(succs, key=nm.get)
845
840
846 # computed topmost element (necessary for bookmark)
841 # computed topmost element (necessary for bookmark)
847 if new:
842 if new:
848 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
843 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
849 elif not final:
844 elif not final:
850 # Nothing rewritten at all. we won't need `newtopmost`
845 # Nothing rewritten at all. we won't need `newtopmost`
851 # It is the same as `oldtopmost` and `processreplacement` know it
846 # It is the same as `oldtopmost` and `processreplacement` know it
852 newtopmost = None
847 newtopmost = None
853 else:
848 else:
854 # every body died. The newtopmost is the parent of the root.
849 # every body died. The newtopmost is the parent of the root.
855 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
850 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
856
851
857 return final, tmpnodes, new, newtopmost
852 return final, tmpnodes, new, newtopmost
858
853
859 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
854 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
860 """Move bookmark from old to newly created node"""
855 """Move bookmark from old to newly created node"""
861 if not mapping:
856 if not mapping:
862 # if nothing got rewritten there is not purpose for this function
857 # if nothing got rewritten there is not purpose for this function
863 return
858 return
864 moves = []
859 moves = []
865 for bk, old in sorted(repo._bookmarks.iteritems()):
860 for bk, old in sorted(repo._bookmarks.iteritems()):
866 if old == oldtopmost:
861 if old == oldtopmost:
867 # special case ensure bookmark stay on tip.
862 # special case ensure bookmark stay on tip.
868 #
863 #
869 # This is arguably a feature and we may only want that for the
864 # This is arguably a feature and we may only want that for the
870 # active bookmark. But the behavior is kept compatible with the old
865 # active bookmark. But the behavior is kept compatible with the old
871 # version for now.
866 # version for now.
872 moves.append((bk, newtopmost))
867 moves.append((bk, newtopmost))
873 continue
868 continue
874 base = old
869 base = old
875 new = mapping.get(base, None)
870 new = mapping.get(base, None)
876 if new is None:
871 if new is None:
877 continue
872 continue
878 while not new:
873 while not new:
879 # base is killed, trying with parent
874 # base is killed, trying with parent
880 base = repo[base].p1().node()
875 base = repo[base].p1().node()
881 new = mapping.get(base, (base,))
876 new = mapping.get(base, (base,))
882 # nothing to move
877 # nothing to move
883 moves.append((bk, new[-1]))
878 moves.append((bk, new[-1]))
884 if moves:
879 if moves:
885 marks = repo._bookmarks
880 marks = repo._bookmarks
886 for mark, new in moves:
881 for mark, new in moves:
887 old = marks[mark]
882 old = marks[mark]
888 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
883 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
889 % (mark, node.short(old), node.short(new)))
884 % (mark, node.short(old), node.short(new)))
890 marks[mark] = new
885 marks[mark] = new
891 marks.write()
886 marks.write()
892
887
893 def cleanupnode(ui, repo, name, nodes):
888 def cleanupnode(ui, repo, name, nodes):
894 """strip a group of nodes from the repository
889 """strip a group of nodes from the repository
895
890
896 The set of node to strip may contains unknown nodes."""
891 The set of node to strip may contains unknown nodes."""
897 ui.debug('should strip %s nodes %s\n' %
892 ui.debug('should strip %s nodes %s\n' %
898 (name, ', '.join([node.short(n) for n in nodes])))
893 (name, ', '.join([node.short(n) for n in nodes])))
899 lock = None
894 lock = None
900 try:
895 try:
901 lock = repo.lock()
896 lock = repo.lock()
902 # Find all node that need to be stripped
897 # Find all node that need to be stripped
903 # (we hg %lr instead of %ln to silently ignore unknown item
898 # (we hg %lr instead of %ln to silently ignore unknown item
904 nm = repo.changelog.nodemap
899 nm = repo.changelog.nodemap
905 nodes = [n for n in nodes if n in nm]
900 nodes = [n for n in nodes if n in nm]
906 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
901 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
907 for c in roots:
902 for c in roots:
908 # We should process node in reverse order to strip tip most first.
903 # We should process node in reverse order to strip tip most first.
909 # but this trigger a bug in changegroup hook.
904 # but this trigger a bug in changegroup hook.
910 # This would reduce bundle overhead
905 # This would reduce bundle overhead
911 repair.strip(ui, repo, c)
906 repair.strip(ui, repo, c)
912 finally:
907 finally:
913 release(lock)
908 release(lock)
914
909
915 def summaryhook(ui, repo):
910 def summaryhook(ui, repo):
916 if not os.path.exists(repo.join('histedit-state')):
911 if not os.path.exists(repo.join('histedit-state')):
917 return
912 return
918 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
913 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
919 if rules:
914 if rules:
920 # i18n: column positioning for "hg summary"
915 # i18n: column positioning for "hg summary"
921 ui.write(_('hist: %s (histedit --continue)\n') %
916 ui.write(_('hist: %s (histedit --continue)\n') %
922 (ui.label(_('%d remaining'), 'histedit.remaining') %
917 (ui.label(_('%d remaining'), 'histedit.remaining') %
923 len(rules)))
918 len(rules)))
924
919
925 def extsetup(ui):
920 def extsetup(ui):
926 cmdutil.summaryhooks.add('histedit', summaryhook)
921 cmdutil.summaryhooks.add('histedit', summaryhook)
927 cmdutil.unfinishedstates.append(
922 cmdutil.unfinishedstates.append(
928 ['histedit-state', False, True, _('histedit in progress'),
923 ['histedit-state', False, True, _('histedit in progress'),
929 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
924 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,366 +1,404
1 Test histedit extension: Fold commands
1 Test histedit extension: Fold commands
2 ======================================
2 ======================================
3
3
4 This test file is dedicated to testing the fold command in non conflicting
4 This test file is dedicated to testing the fold command in non conflicting
5 case.
5 case.
6
6
7 Initialization
7 Initialization
8 ---------------
8 ---------------
9
9
10
10
11 $ . "$TESTDIR/histedit-helpers.sh"
11 $ . "$TESTDIR/histedit-helpers.sh"
12
12
13 $ cat >> $HGRCPATH <<EOF
13 $ cat >> $HGRCPATH <<EOF
14 > [alias]
14 > [alias]
15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
16 > [extensions]
16 > [extensions]
17 > histedit=
17 > histedit=
18 > EOF
18 > EOF
19
19
20
20
21 Simple folding
21 Simple folding
22 --------------------
22 --------------------
23 $ initrepo ()
23 $ initrepo ()
24 > {
24 > {
25 > hg init r
25 > hg init r
26 > cd r
26 > cd r
27 > for x in a b c d e f ; do
27 > for x in a b c d e f ; do
28 > echo $x > $x
28 > echo $x > $x
29 > hg add $x
29 > hg add $x
30 > hg ci -m $x
30 > hg ci -m $x
31 > done
31 > done
32 > }
32 > }
33
33
34 $ initrepo
34 $ initrepo
35
35
36 log before edit
36 log before edit
37 $ hg logt --graph
37 $ hg logt --graph
38 @ 5:652413bf663e f
38 @ 5:652413bf663e f
39 |
39 |
40 o 4:e860deea161a e
40 o 4:e860deea161a e
41 |
41 |
42 o 3:055a42cdd887 d
42 o 3:055a42cdd887 d
43 |
43 |
44 o 2:177f92b77385 c
44 o 2:177f92b77385 c
45 |
45 |
46 o 1:d2ae7f538514 b
46 o 1:d2ae7f538514 b
47 |
47 |
48 o 0:cb9a9f314b8b a
48 o 0:cb9a9f314b8b a
49
49
50
50
51 $ hg histedit 177f92b77385 --commands - 2>&1 <<EOF | fixbundle
51 $ hg histedit 177f92b77385 --commands - 2>&1 <<EOF | fixbundle
52 > pick e860deea161a e
52 > pick e860deea161a e
53 > pick 652413bf663e f
53 > pick 652413bf663e f
54 > fold 177f92b77385 c
54 > fold 177f92b77385 c
55 > pick 055a42cdd887 d
55 > pick 055a42cdd887 d
56 > EOF
56 > EOF
57 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
57 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
58 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
58 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
59 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
59 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
60 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
61 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
61 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
63 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
63 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
64
64
65 log after edit
65 log after edit
66 $ hg logt --graph
66 $ hg logt --graph
67 @ 4:9c277da72c9b d
67 @ 4:9c277da72c9b d
68 |
68 |
69 o 3:6de59d13424a f
69 o 3:6de59d13424a f
70 |
70 |
71 o 2:ee283cb5f2d5 e
71 o 2:ee283cb5f2d5 e
72 |
72 |
73 o 1:d2ae7f538514 b
73 o 1:d2ae7f538514 b
74 |
74 |
75 o 0:cb9a9f314b8b a
75 o 0:cb9a9f314b8b a
76
76
77
77
78 post-fold manifest
78 post-fold manifest
79 $ hg manifest
79 $ hg manifest
80 a
80 a
81 b
81 b
82 c
82 c
83 d
83 d
84 e
84 e
85 f
85 f
86
86
87
87
88 check histedit_source
88 check histedit_source
89
89
90 $ hg log --debug --rev 3
90 $ hg log --debug --rev 3
91 changeset: 3:6de59d13424a8a13acd3e975514aed29dd0d9b2d
91 changeset: 3:6de59d13424a8a13acd3e975514aed29dd0d9b2d
92 phase: draft
92 phase: draft
93 parent: 2:ee283cb5f2d5955443f23a27b697a04339e9a39a
93 parent: 2:ee283cb5f2d5955443f23a27b697a04339e9a39a
94 parent: -1:0000000000000000000000000000000000000000
94 parent: -1:0000000000000000000000000000000000000000
95 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
95 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
96 user: test
96 user: test
97 date: Thu Jan 01 00:00:00 1970 +0000
97 date: Thu Jan 01 00:00:00 1970 +0000
98 files+: c f
98 files+: c f
99 extra: branch=default
99 extra: branch=default
100 extra: histedit_source=a4f7421b80f79fcc59fff01bcbf4a53d127dd6d3,177f92b773850b59254aa5e923436f921b55483b
100 extra: histedit_source=a4f7421b80f79fcc59fff01bcbf4a53d127dd6d3,177f92b773850b59254aa5e923436f921b55483b
101 description:
101 description:
102 f
102 f
103 ***
103 ***
104 c
104 c
105
105
106
106
107
107
108 check saving last-message.txt
108 check saving last-message.txt
109
109
110 $ cat > $TESTTMP/abortfolding.py <<EOF
110 $ cat > $TESTTMP/abortfolding.py <<EOF
111 > from mercurial import util
111 > from mercurial import util
112 > def abortfolding(ui, repo, hooktype, **kwargs):
112 > def abortfolding(ui, repo, hooktype, **kwargs):
113 > ctx = repo[kwargs.get('node')]
113 > ctx = repo[kwargs.get('node')]
114 > if set(ctx.files()) == set(['c', 'd', 'f']):
114 > if set(ctx.files()) == set(['c', 'd', 'f']):
115 > return True # abort folding commit only
115 > return True # abort folding commit only
116 > ui.warn('allow non-folding commit\\n')
116 > ui.warn('allow non-folding commit\\n')
117 > EOF
117 > EOF
118 $ cat > .hg/hgrc <<EOF
118 $ cat > .hg/hgrc <<EOF
119 > [hooks]
119 > [hooks]
120 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
120 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
121 > EOF
121 > EOF
122
122
123 $ cat > $TESTTMP/editor.sh << EOF
123 $ cat > $TESTTMP/editor.sh << EOF
124 > echo "==== before editing"
124 > echo "==== before editing"
125 > cat \$1
125 > cat \$1
126 > echo "===="
126 > echo "===="
127 > echo "check saving last-message.txt" >> \$1
127 > echo "check saving last-message.txt" >> \$1
128 > EOF
128 > EOF
129
129
130 $ rm -f .hg/last-message.txt
130 $ rm -f .hg/last-message.txt
131 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 6de59d13424a --commands - 2>&1 <<EOF | fixbundle
131 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 6de59d13424a --commands - 2>&1 <<EOF | fixbundle
132 > pick 6de59d13424a f
132 > pick 6de59d13424a f
133 > fold 9c277da72c9b d
133 > fold 9c277da72c9b d
134 > EOF
134 > EOF
135 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
135 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
136 allow non-folding commit
136 allow non-folding commit
137 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
137 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
138 ==== before editing
138 ==== before editing
139 f
139 f
140 ***
140 ***
141 c
141 c
142 ***
142 ***
143 d
143 d
144
144
145
145
146
146
147 HG: Enter commit message. Lines beginning with 'HG:' are removed.
147 HG: Enter commit message. Lines beginning with 'HG:' are removed.
148 HG: Leave message empty to abort commit.
148 HG: Leave message empty to abort commit.
149 HG: --
149 HG: --
150 HG: user: test
150 HG: user: test
151 HG: branch 'default'
151 HG: branch 'default'
152 HG: changed c
152 HG: changed c
153 HG: changed d
153 HG: changed d
154 HG: changed f
154 HG: changed f
155 ====
155 ====
156 transaction abort!
156 transaction abort!
157 rollback completed
157 rollback completed
158 abort: pretxncommit.abortfolding hook failed
158 abort: pretxncommit.abortfolding hook failed
159
159
160 $ cat .hg/last-message.txt
160 $ cat .hg/last-message.txt
161 f
161 f
162 ***
162 ***
163 c
163 c
164 ***
164 ***
165 d
165 d
166
166
167
167
168
168
169 check saving last-message.txt
169 check saving last-message.txt
170
170
171 $ cd ..
171 $ cd ..
172 $ rm -r r
173
174 folding preserves initial author
175 --------------------------------
176
177 $ initrepo
178
179 $ hg ci --user "someone else" --amend --quiet
180
181 tip before edit
182 $ hg log --rev .
183 changeset: 5:a00ad806cb55
184 tag: tip
185 user: someone else
186 date: Thu Jan 01 00:00:00 1970 +0000
187 summary: f
188
189
190 $ hg histedit e860deea161a --commands - 2>&1 <<EOF | fixbundle
191 > pick e860deea161a e
192 > fold a00ad806cb55 f
193 > EOF
194 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
195 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
196 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
197 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
198
199 tip after edit
200 $ hg log --rev .
201 changeset: 4:698d4e8040a1
202 tag: tip
203 user: test
204 date: Thu Jan 01 00:00:00 1970 +0000
205 summary: e
206
207
208 $ cd ..
209 $ rm -r r
172
210
173 folding and creating no new change doesn't break:
211 folding and creating no new change doesn't break:
174 -------------------------------------------------
212 -------------------------------------------------
175
213
176 folded content is dropped during a merge. The folded commit should properly disappear.
214 folded content is dropped during a merge. The folded commit should properly disappear.
177
215
178 $ mkdir fold-to-empty-test
216 $ mkdir fold-to-empty-test
179 $ cd fold-to-empty-test
217 $ cd fold-to-empty-test
180 $ hg init
218 $ hg init
181 $ printf "1\n2\n3\n" > file
219 $ printf "1\n2\n3\n" > file
182 $ hg add file
220 $ hg add file
183 $ hg commit -m '1+2+3'
221 $ hg commit -m '1+2+3'
184 $ echo 4 >> file
222 $ echo 4 >> file
185 $ hg commit -m '+4'
223 $ hg commit -m '+4'
186 $ echo 5 >> file
224 $ echo 5 >> file
187 $ hg commit -m '+5'
225 $ hg commit -m '+5'
188 $ echo 6 >> file
226 $ echo 6 >> file
189 $ hg commit -m '+6'
227 $ hg commit -m '+6'
190 $ hg logt --graph
228 $ hg logt --graph
191 @ 3:251d831eeec5 +6
229 @ 3:251d831eeec5 +6
192 |
230 |
193 o 2:888f9082bf99 +5
231 o 2:888f9082bf99 +5
194 |
232 |
195 o 1:617f94f13c0f +4
233 o 1:617f94f13c0f +4
196 |
234 |
197 o 0:0189ba417d34 1+2+3
235 o 0:0189ba417d34 1+2+3
198
236
199
237
200 $ hg histedit 1 --commands - << EOF
238 $ hg histedit 1 --commands - << EOF
201 > pick 617f94f13c0f 1 +4
239 > pick 617f94f13c0f 1 +4
202 > drop 888f9082bf99 2 +5
240 > drop 888f9082bf99 2 +5
203 > fold 251d831eeec5 3 +6
241 > fold 251d831eeec5 3 +6
204 > EOF
242 > EOF
205 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
243 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
206 merging file
244 merging file
207 warning: conflicts during merge.
245 warning: conflicts during merge.
208 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
246 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
209 Fix up the change and run hg histedit --continue
247 Fix up the change and run hg histedit --continue
210 [1]
248 [1]
211 There were conflicts, we keep P1 content. This
249 There were conflicts, we keep P1 content. This
212 should effectively drop the changes from +6.
250 should effectively drop the changes from +6.
213 $ hg status
251 $ hg status
214 M file
252 M file
215 ? file.orig
253 ? file.orig
216 $ hg resolve -l
254 $ hg resolve -l
217 U file
255 U file
218 $ hg revert -r 'p1()' file
256 $ hg revert -r 'p1()' file
219 $ hg resolve --mark file
257 $ hg resolve --mark file
220 (no more unresolved files)
258 (no more unresolved files)
221 $ hg histedit --continue
259 $ hg histedit --continue
222 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
260 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
223 saved backup bundle to $TESTTMP/*-backup.hg (glob)
261 saved backup bundle to $TESTTMP/*-backup.hg (glob)
224 $ hg logt --graph
262 $ hg logt --graph
225 @ 1:617f94f13c0f +4
263 @ 1:617f94f13c0f +4
226 |
264 |
227 o 0:0189ba417d34 1+2+3
265 o 0:0189ba417d34 1+2+3
228
266
229
267
230 $ cd ..
268 $ cd ..
231
269
232
270
233 Test fold through dropped
271 Test fold through dropped
234 -------------------------
272 -------------------------
235
273
236
274
237 Test corner case where folded revision is separated from its parent by a
275 Test corner case where folded revision is separated from its parent by a
238 dropped revision.
276 dropped revision.
239
277
240
278
241 $ hg init fold-with-dropped
279 $ hg init fold-with-dropped
242 $ cd fold-with-dropped
280 $ cd fold-with-dropped
243 $ printf "1\n2\n3\n" > file
281 $ printf "1\n2\n3\n" > file
244 $ hg commit -Am '1+2+3'
282 $ hg commit -Am '1+2+3'
245 adding file
283 adding file
246 $ echo 4 >> file
284 $ echo 4 >> file
247 $ hg commit -m '+4'
285 $ hg commit -m '+4'
248 $ echo 5 >> file
286 $ echo 5 >> file
249 $ hg commit -m '+5'
287 $ hg commit -m '+5'
250 $ echo 6 >> file
288 $ echo 6 >> file
251 $ hg commit -m '+6'
289 $ hg commit -m '+6'
252 $ hg logt -G
290 $ hg logt -G
253 @ 3:251d831eeec5 +6
291 @ 3:251d831eeec5 +6
254 |
292 |
255 o 2:888f9082bf99 +5
293 o 2:888f9082bf99 +5
256 |
294 |
257 o 1:617f94f13c0f +4
295 o 1:617f94f13c0f +4
258 |
296 |
259 o 0:0189ba417d34 1+2+3
297 o 0:0189ba417d34 1+2+3
260
298
261 $ hg histedit 1 --commands - << EOF
299 $ hg histedit 1 --commands - << EOF
262 > pick 617f94f13c0f 1 +4
300 > pick 617f94f13c0f 1 +4
263 > drop 888f9082bf99 2 +5
301 > drop 888f9082bf99 2 +5
264 > fold 251d831eeec5 3 +6
302 > fold 251d831eeec5 3 +6
265 > EOF
303 > EOF
266 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
304 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
267 merging file
305 merging file
268 warning: conflicts during merge.
306 warning: conflicts during merge.
269 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
307 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
270 Fix up the change and run hg histedit --continue
308 Fix up the change and run hg histedit --continue
271 [1]
309 [1]
272 $ cat > file << EOF
310 $ cat > file << EOF
273 > 1
311 > 1
274 > 2
312 > 2
275 > 3
313 > 3
276 > 4
314 > 4
277 > 5
315 > 5
278 > EOF
316 > EOF
279 $ hg resolve --mark file
317 $ hg resolve --mark file
280 (no more unresolved files)
318 (no more unresolved files)
281 $ hg commit -m '+5.2'
319 $ hg commit -m '+5.2'
282 created new head
320 created new head
283 $ echo 6 >> file
321 $ echo 6 >> file
284 $ HGEDITOR=cat hg histedit --continue
322 $ HGEDITOR=cat hg histedit --continue
285 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
323 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
286 +4
324 +4
287 ***
325 ***
288 +5.2
326 +5.2
289 ***
327 ***
290 +6
328 +6
291
329
292
330
293
331
294 HG: Enter commit message. Lines beginning with 'HG:' are removed.
332 HG: Enter commit message. Lines beginning with 'HG:' are removed.
295 HG: Leave message empty to abort commit.
333 HG: Leave message empty to abort commit.
296 HG: --
334 HG: --
297 HG: user: test
335 HG: user: test
298 HG: branch 'default'
336 HG: branch 'default'
299 HG: changed file
337 HG: changed file
300 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
338 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
301 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
339 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
302 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-backup.hg (glob)
340 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-backup.hg (glob)
303 $ hg logt -G
341 $ hg logt -G
304 @ 1:10c647b2cdd5 +4
342 @ 1:10c647b2cdd5 +4
305 |
343 |
306 o 0:0189ba417d34 1+2+3
344 o 0:0189ba417d34 1+2+3
307
345
308 $ hg export tip
346 $ hg export tip
309 # HG changeset patch
347 # HG changeset patch
310 # User test
348 # User test
311 # Date 0 0
349 # Date 0 0
312 # Thu Jan 01 00:00:00 1970 +0000
350 # Thu Jan 01 00:00:00 1970 +0000
313 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
351 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
314 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
352 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
315 +4
353 +4
316 ***
354 ***
317 +5.2
355 +5.2
318 ***
356 ***
319 +6
357 +6
320
358
321 diff -r 0189ba417d34 -r 10c647b2cdd5 file
359 diff -r 0189ba417d34 -r 10c647b2cdd5 file
322 --- a/file Thu Jan 01 00:00:00 1970 +0000
360 --- a/file Thu Jan 01 00:00:00 1970 +0000
323 +++ b/file Thu Jan 01 00:00:00 1970 +0000
361 +++ b/file Thu Jan 01 00:00:00 1970 +0000
324 @@ -1,3 +1,6 @@
362 @@ -1,3 +1,6 @@
325 1
363 1
326 2
364 2
327 3
365 3
328 +4
366 +4
329 +5
367 +5
330 +6
368 +6
331 $ cd ..
369 $ cd ..
332
370
333
371
334 Folding with initial rename (issue3729)
372 Folding with initial rename (issue3729)
335 ---------------------------------------
373 ---------------------------------------
336
374
337 $ hg init fold-rename
375 $ hg init fold-rename
338 $ cd fold-rename
376 $ cd fold-rename
339 $ echo a > a.txt
377 $ echo a > a.txt
340 $ hg add a.txt
378 $ hg add a.txt
341 $ hg commit -m a
379 $ hg commit -m a
342 $ hg rename a.txt b.txt
380 $ hg rename a.txt b.txt
343 $ hg commit -m rename
381 $ hg commit -m rename
344 $ echo b >> b.txt
382 $ echo b >> b.txt
345 $ hg commit -m b
383 $ hg commit -m b
346
384
347 $ hg logt --follow b.txt
385 $ hg logt --follow b.txt
348 2:e0371e0426bc b
386 2:e0371e0426bc b
349 1:1c4f440a8085 rename
387 1:1c4f440a8085 rename
350 0:6c795aa153cb a
388 0:6c795aa153cb a
351
389
352 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
390 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
353 > pick 1c4f440a8085 rename
391 > pick 1c4f440a8085 rename
354 > fold e0371e0426bc b
392 > fold e0371e0426bc b
355 > EOF
393 > EOF
356 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
394 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
357 reverting b.txt
395 reverting b.txt
358 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
396 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
359 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
397 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
360 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
398 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
361
399
362 $ hg logt --follow b.txt
400 $ hg logt --follow b.txt
363 1:cf858d235c76 rename
401 1:cf858d235c76 rename
364 0:6c795aa153cb a
402 0:6c795aa153cb a
365
403
366 $ cd ..
404 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now