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