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