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