##// END OF EJS Templates
histedit: move `between function` outside the action logic...
Pierre-Yves David -
r17642:bea381c1 default
parent child Browse files
Show More
@@ -1,670 +1,670
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 633536316234 and 7c2fd3b9020c
31 # Edit history between 633536316234 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 633536316234 and 7c2fd3b9020c
50 # Edit history between 633536316234 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 example repository above and add three more changes, such that
111 If we clone the example repository above and add three more changes, such that
112 we have the following history::
112 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 tempfile
145 import tempfile
146 import os
146 import os
147
147
148 from mercurial import bookmarks
148 from mercurial import bookmarks
149 from mercurial import cmdutil
149 from mercurial import cmdutil
150 from mercurial import discovery
150 from mercurial import discovery
151 from mercurial import error
151 from mercurial import error
152 from mercurial import 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 patch
155 from mercurial import patch
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.i18n import _
159 from mercurial.i18n import _
160
160
161 cmdtable = {}
161 cmdtable = {}
162 command = cmdutil.command(cmdtable)
162 command = cmdutil.command(cmdtable)
163
163
164 testedwith = 'internal'
164 testedwith = 'internal'
165
165
166 # i18n: command names and abbreviations must remain untranslated
166 # i18n: command names and abbreviations must remain untranslated
167 editcomment = _("""# Edit history between %s and %s
167 editcomment = _("""# Edit history between %s and %s
168 #
168 #
169 # Commands:
169 # Commands:
170 # p, pick = use commit
170 # p, pick = use commit
171 # e, edit = use commit, but stop for amending
171 # e, edit = use commit, but stop for amending
172 # f, fold = use commit, but fold into previous commit (combines N and N-1)
172 # f, fold = use commit, but fold into previous commit (combines N and N-1)
173 # d, drop = remove commit from history
173 # d, drop = remove commit from history
174 # m, mess = edit message without changing commit content
174 # m, mess = edit message without changing commit content
175 #
175 #
176 """)
176 """)
177
177
178 def foldchanges(ui, repo, node1, node2, opts):
178 def foldchanges(ui, repo, node1, node2, opts):
179 """Produce a new changeset that represents the diff from node1 to node2."""
179 """Produce a new changeset that represents the diff from node1 to node2."""
180 try:
180 try:
181 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
181 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
182 fp = os.fdopen(fd, 'w')
182 fp = os.fdopen(fd, 'w')
183 diffopts = patch.diffopts(ui, opts)
183 diffopts = patch.diffopts(ui, opts)
184 diffopts.git = True
184 diffopts.git = True
185 diffopts.ignorews = False
185 diffopts.ignorews = False
186 diffopts.ignorewsamount = False
186 diffopts.ignorewsamount = False
187 diffopts.ignoreblanklines = False
187 diffopts.ignoreblanklines = False
188 gen = patch.diff(repo, node1, node2, opts=diffopts)
188 gen = patch.diff(repo, node1, node2, opts=diffopts)
189 for chunk in gen:
189 for chunk in gen:
190 fp.write(chunk)
190 fp.write(chunk)
191 fp.close()
191 fp.close()
192 files = set()
192 files = set()
193 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
193 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
194 finally:
194 finally:
195 os.unlink(patchfile)
195 os.unlink(patchfile)
196 return files
196 return files
197
197
198 def between(repo, old, new, keep):
199 """select and validate the set of revision to edit
200
201 When keep is false, the specified set can't have children."""
202 revs = [old]
203 current = old
204 while current != new:
205 ctx = repo[current]
206 if not keep and len(ctx.children()) > 1:
207 raise util.Abort(_('cannot edit history that would orphan nodes'))
208 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
209 raise util.Abort(_("can't edit history with merges"))
210 if not ctx.children():
211 current = new
212 else:
213 current = ctx.children()[0].node()
214 revs.append(current)
215 if len(repo[current].children()) and not keep:
216 raise util.Abort(_('cannot edit history that would orphan nodes'))
217 return revs
218
219
220 def pick(ui, repo, ctx, ha, opts):
198 def pick(ui, repo, ctx, ha, opts):
221 oldctx = repo[ha]
199 oldctx = repo[ha]
222 if oldctx.parents()[0] == ctx:
200 if oldctx.parents()[0] == ctx:
223 ui.debug('node %s unchanged\n' % ha)
201 ui.debug('node %s unchanged\n' % ha)
224 return oldctx, [], [], []
202 return oldctx, [], [], []
225 hg.update(repo, ctx.node())
203 hg.update(repo, ctx.node())
226 try:
204 try:
227 files = foldchanges(ui, repo, oldctx.p1().node() , ha, opts)
205 files = foldchanges(ui, repo, oldctx.p1().node() , ha, opts)
228 if not files:
206 if not files:
229 ui.warn(_('%s: empty changeset')
207 ui.warn(_('%s: empty changeset')
230 % node.hex(ha))
208 % node.hex(ha))
231 return ctx, [], [], []
209 return ctx, [], [], []
232 except Exception:
210 except Exception:
233 raise util.Abort(_('Fix up the change and run '
211 raise util.Abort(_('Fix up the change and run '
234 'hg histedit --continue'))
212 'hg histedit --continue'))
235 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
213 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
236 date=oldctx.date(), extra=oldctx.extra())
214 date=oldctx.date(), extra=oldctx.extra())
237 return repo[n], [n], [oldctx.node()], []
215 return repo[n], [n], [oldctx.node()], []
238
216
239
217
240 def edit(ui, repo, ctx, ha, opts):
218 def edit(ui, repo, ctx, ha, opts):
241 oldctx = repo[ha]
219 oldctx = repo[ha]
242 hg.update(repo, ctx.node())
220 hg.update(repo, ctx.node())
243 try:
221 try:
244 foldchanges(ui, repo, oldctx.p1().node() , ha, opts)
222 foldchanges(ui, repo, oldctx.p1().node() , ha, opts)
245 except Exception:
223 except Exception:
246 pass
224 pass
247 raise util.Abort(_('Make changes as needed, you may commit or record as '
225 raise util.Abort(_('Make changes as needed, you may commit or record as '
248 'needed now.\nWhen you are finished, run hg'
226 'needed now.\nWhen you are finished, run hg'
249 ' histedit --continue to resume.'))
227 ' histedit --continue to resume.'))
250
228
251 def fold(ui, repo, ctx, ha, opts):
229 def fold(ui, repo, ctx, ha, opts):
252 oldctx = repo[ha]
230 oldctx = repo[ha]
253 hg.update(repo, ctx.node())
231 hg.update(repo, ctx.node())
254 try:
232 try:
255 files = foldchanges(ui, repo, oldctx.p1().node() , ha, opts)
233 files = foldchanges(ui, repo, oldctx.p1().node() , ha, opts)
256 if not files:
234 if not files:
257 ui.warn(_('%s: empty changeset')
235 ui.warn(_('%s: empty changeset')
258 % node.hex(ha))
236 % node.hex(ha))
259 return ctx, [], [], []
237 return ctx, [], [], []
260 except Exception:
238 except Exception:
261 raise util.Abort(_('Fix up the change and run '
239 raise util.Abort(_('Fix up the change and run '
262 'hg histedit --continue'))
240 'hg histedit --continue'))
263 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
241 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
264 date=oldctx.date(), extra=oldctx.extra())
242 date=oldctx.date(), extra=oldctx.extra())
265 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
243 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
266
244
267 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
245 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
268 parent = ctx.parents()[0].node()
246 parent = ctx.parents()[0].node()
269 hg.update(repo, parent)
247 hg.update(repo, parent)
270 foldchanges(ui, repo, parent, newnode, opts)
248 foldchanges(ui, repo, parent, newnode, opts)
271 newmessage = '\n***\n'.join(
249 newmessage = '\n***\n'.join(
272 [ctx.description()] +
250 [ctx.description()] +
273 [repo[r].description() for r in internalchanges] +
251 [repo[r].description() for r in internalchanges] +
274 [oldctx.description()]) + '\n'
252 [oldctx.description()]) + '\n'
275 # If the changesets are from the same author, keep it.
253 # If the changesets are from the same author, keep it.
276 if ctx.user() == oldctx.user():
254 if ctx.user() == oldctx.user():
277 username = ctx.user()
255 username = ctx.user()
278 else:
256 else:
279 username = ui.username()
257 username = ui.username()
280 newmessage = ui.edit(newmessage, username)
258 newmessage = ui.edit(newmessage, username)
281 n = repo.commit(text=newmessage, user=username,
259 n = repo.commit(text=newmessage, user=username,
282 date=max(ctx.date(), oldctx.date()), extra=oldctx.extra())
260 date=max(ctx.date(), oldctx.date()), extra=oldctx.extra())
283 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
261 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
284
262
285 def drop(ui, repo, ctx, ha, opts):
263 def drop(ui, repo, ctx, ha, opts):
286 return ctx, [], [repo[ha].node()], []
264 return ctx, [], [repo[ha].node()], []
287
265
288
266
289 def message(ui, repo, ctx, ha, opts):
267 def message(ui, repo, ctx, ha, opts):
290 oldctx = repo[ha]
268 oldctx = repo[ha]
291 hg.update(repo, ctx.node())
269 hg.update(repo, ctx.node())
292 try:
270 try:
293 foldchanges(ui, repo, oldctx.p1().node() , ha, opts)
271 foldchanges(ui, repo, oldctx.p1().node() , ha, opts)
294 except Exception:
272 except Exception:
295 raise util.Abort(_('Fix up the change and run '
273 raise util.Abort(_('Fix up the change and run '
296 'hg histedit --continue'))
274 'hg histedit --continue'))
297 message = oldctx.description() + '\n'
275 message = oldctx.description() + '\n'
298 message = ui.edit(message, ui.username())
276 message = ui.edit(message, ui.username())
299 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
277 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
300 extra=oldctx.extra())
278 extra=oldctx.extra())
301 newctx = repo[new]
279 newctx = repo[new]
302 if oldctx.node() != newctx.node():
280 if oldctx.node() != newctx.node():
303 return newctx, [new], [oldctx.node()], []
281 return newctx, [new], [oldctx.node()], []
304 # We didn't make an edit, so just indicate no replaced nodes
282 # We didn't make an edit, so just indicate no replaced nodes
305 return newctx, [new], [], []
283 return newctx, [new], [], []
306
284
307
285
308 def makedesc(c):
286 def makedesc(c):
309 """build a initial action line for a ctx `c`
287 """build a initial action line for a ctx `c`
310
288
311 line are in the form:
289 line are in the form:
312
290
313 pick <hash> <rev> <summary>
291 pick <hash> <rev> <summary>
314 """
292 """
315 summary = ''
293 summary = ''
316 if c.description():
294 if c.description():
317 summary = c.description().splitlines()[0]
295 summary = c.description().splitlines()[0]
318 line = 'pick %s %d %s' % (c.hex()[:12], c.rev(), summary)
296 line = 'pick %s %d %s' % (c.hex()[:12], c.rev(), summary)
319 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
297 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
320
298
321 actiontable = {'p': pick,
299 actiontable = {'p': pick,
322 'pick': pick,
300 'pick': pick,
323 'e': edit,
301 'e': edit,
324 'edit': edit,
302 'edit': edit,
325 'f': fold,
303 'f': fold,
326 'fold': fold,
304 'fold': fold,
327 'd': drop,
305 'd': drop,
328 'drop': drop,
306 'drop': drop,
329 'm': message,
307 'm': message,
330 'mess': message,
308 'mess': message,
331 }
309 }
332
310
333 @command('histedit',
311 @command('histedit',
334 [('', 'commands', '',
312 [('', 'commands', '',
335 _('Read history edits from the specified file.')),
313 _('Read history edits from the specified file.')),
336 ('c', 'continue', False, _('continue an edit already in progress')),
314 ('c', 'continue', False, _('continue an edit already in progress')),
337 ('k', 'keep', False,
315 ('k', 'keep', False,
338 _("don't strip old nodes after edit is complete")),
316 _("don't strip old nodes after edit is complete")),
339 ('', 'abort', False, _('abort an edit in progress')),
317 ('', 'abort', False, _('abort an edit in progress')),
340 ('o', 'outgoing', False, _('changesets not found in destination')),
318 ('o', 'outgoing', False, _('changesets not found in destination')),
341 ('f', 'force', False,
319 ('f', 'force', False,
342 _('force outgoing even for unrelated repositories')),
320 _('force outgoing even for unrelated repositories')),
343 ('r', 'rev', [], _('first revision to be edited'))],
321 ('r', 'rev', [], _('first revision to be edited'))],
344 _("[PARENT]"))
322 _("[PARENT]"))
345 def histedit(ui, repo, *parent, **opts):
323 def histedit(ui, repo, *parent, **opts):
346 """interactively edit changeset history
324 """interactively edit changeset history
347 """
325 """
348 # TODO only abort if we try and histedit mq patches, not just
326 # TODO only abort if we try and histedit mq patches, not just
349 # blanket if mq patches are applied somewhere
327 # blanket if mq patches are applied somewhere
350 mq = getattr(repo, 'mq', None)
328 mq = getattr(repo, 'mq', None)
351 if mq and mq.applied:
329 if mq and mq.applied:
352 raise util.Abort(_('source has mq patches applied'))
330 raise util.Abort(_('source has mq patches applied'))
353
331
354 parent = list(parent) + opts.get('rev', [])
332 parent = list(parent) + opts.get('rev', [])
355 if opts.get('outgoing'):
333 if opts.get('outgoing'):
356 if len(parent) > 1:
334 if len(parent) > 1:
357 raise util.Abort(
335 raise util.Abort(
358 _('only one repo argument allowed with --outgoing'))
336 _('only one repo argument allowed with --outgoing'))
359 elif parent:
337 elif parent:
360 parent = parent[0]
338 parent = parent[0]
361
339
362 dest = ui.expandpath(parent or 'default-push', parent or 'default')
340 dest = ui.expandpath(parent or 'default-push', parent or 'default')
363 dest, revs = hg.parseurl(dest, None)[:2]
341 dest, revs = hg.parseurl(dest, None)[:2]
364 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
342 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
365
343
366 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
344 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
367 other = hg.peer(repo, opts, dest)
345 other = hg.peer(repo, opts, dest)
368
346
369 if revs:
347 if revs:
370 revs = [repo.lookup(rev) for rev in revs]
348 revs = [repo.lookup(rev) for rev in revs]
371
349
372 parent = discovery.findcommonoutgoing(
350 parent = discovery.findcommonoutgoing(
373 repo, other, [], force=opts.get('force')).missing[0:1]
351 repo, other, [], force=opts.get('force')).missing[0:1]
374 else:
352 else:
375 if opts.get('force'):
353 if opts.get('force'):
376 raise util.Abort(_('--force only allowed with --outgoing'))
354 raise util.Abort(_('--force only allowed with --outgoing'))
377
355
378 if opts.get('continue', False):
356 if opts.get('continue', False):
379 if len(parent) != 0:
357 if len(parent) != 0:
380 raise util.Abort(_('no arguments allowed with --continue'))
358 raise util.Abort(_('no arguments allowed with --continue'))
381 (parentctxnode, created, replaced,
359 (parentctxnode, created, replaced,
382 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
360 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
383 currentparent, wantnull = repo.dirstate.parents()
361 currentparent, wantnull = repo.dirstate.parents()
384 parentctx = repo[parentctxnode]
362 parentctx = repo[parentctxnode]
385 # existing is the list of revisions initially considered by
363 # existing is the list of revisions initially considered by
386 # histedit. Here we use it to list new changesets, descendants
364 # histedit. Here we use it to list new changesets, descendants
387 # of parentctx without an 'existing' changeset in-between. We
365 # of parentctx without an 'existing' changeset in-between. We
388 # also have to exclude 'existing' changesets which were
366 # also have to exclude 'existing' changesets which were
389 # previously dropped.
367 # previously dropped.
390 descendants = set(c.node() for c in
368 descendants = set(c.node() for c in
391 repo.set('(%n::) - %n', parentctxnode, parentctxnode))
369 repo.set('(%n::) - %n', parentctxnode, parentctxnode))
392 existing = set(existing)
370 existing = set(existing)
393 notdropped = set(n for n in existing if n in descendants and
371 notdropped = set(n for n in existing if n in descendants and
394 (n not in replacemap or replacemap[n] in descendants))
372 (n not in replacemap or replacemap[n] in descendants))
395 # Discover any nodes the user has added in the interim. We can
373 # Discover any nodes the user has added in the interim. We can
396 # miss changesets which were dropped and recreated the same.
374 # miss changesets which were dropped and recreated the same.
397 newchildren = list(c.node() for c in repo.set(
375 newchildren = list(c.node() for c in repo.set(
398 'sort(%ln - (%ln or %ln::))', descendants, existing, notdropped))
376 'sort(%ln - (%ln or %ln::))', descendants, existing, notdropped))
399 action, currentnode = rules.pop(0)
377 action, currentnode = rules.pop(0)
400 if action in ('f', 'fold'):
378 if action in ('f', 'fold'):
401 tmpnodes.extend(newchildren)
379 tmpnodes.extend(newchildren)
402 else:
380 else:
403 created.extend(newchildren)
381 created.extend(newchildren)
404
382
405 m, a, r, d = repo.status()[:4]
383 m, a, r, d = repo.status()[:4]
406 oldctx = repo[currentnode]
384 oldctx = repo[currentnode]
407 message = oldctx.description() + '\n'
385 message = oldctx.description() + '\n'
408 if action in ('e', 'edit', 'm', 'mess'):
386 if action in ('e', 'edit', 'm', 'mess'):
409 message = ui.edit(message, ui.username())
387 message = ui.edit(message, ui.username())
410 elif action in ('f', 'fold'):
388 elif action in ('f', 'fold'):
411 message = 'fold-temp-revision %s' % currentnode
389 message = 'fold-temp-revision %s' % currentnode
412 new = None
390 new = None
413 if m or a or r or d:
391 if m or a or r or d:
414 new = repo.commit(text=message, user=oldctx.user(),
392 new = repo.commit(text=message, user=oldctx.user(),
415 date=oldctx.date(), extra=oldctx.extra())
393 date=oldctx.date(), extra=oldctx.extra())
416
394
417 # If we're resuming a fold and we have new changes, mark the
395 # If we're resuming a fold and we have new changes, mark the
418 # replacements and finish the fold. If not, it's more like a
396 # replacements and finish the fold. If not, it's more like a
419 # drop of the changesets that disappeared, and we can skip
397 # drop of the changesets that disappeared, and we can skip
420 # this step.
398 # this step.
421 if action in ('f', 'fold') and (new or newchildren):
399 if action in ('f', 'fold') and (new or newchildren):
422 if new:
400 if new:
423 tmpnodes.append(new)
401 tmpnodes.append(new)
424 else:
402 else:
425 new = newchildren[-1]
403 new = newchildren[-1]
426 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
404 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
427 ui, repo, parentctx, oldctx, new, opts, newchildren)
405 ui, repo, parentctx, oldctx, new, opts, newchildren)
428 replaced.extend(replaced_)
406 replaced.extend(replaced_)
429 created.extend(created_)
407 created.extend(created_)
430 tmpnodes.extend(tmpnodes_)
408 tmpnodes.extend(tmpnodes_)
431 elif action not in ('d', 'drop'):
409 elif action not in ('d', 'drop'):
432 if new != oldctx.node():
410 if new != oldctx.node():
433 replaced.append(oldctx.node())
411 replaced.append(oldctx.node())
434 if new:
412 if new:
435 if new != oldctx.node():
413 if new != oldctx.node():
436 created.append(new)
414 created.append(new)
437 parentctx = repo[new]
415 parentctx = repo[new]
438
416
439 elif opts.get('abort', False):
417 elif opts.get('abort', False):
440 if len(parent) != 0:
418 if len(parent) != 0:
441 raise util.Abort(_('no arguments allowed with --abort'))
419 raise util.Abort(_('no arguments allowed with --abort'))
442 (parentctxnode, created, replaced, tmpnodes,
420 (parentctxnode, created, replaced, tmpnodes,
443 existing, rules, keep, tip, replacemap) = readstate(repo)
421 existing, rules, keep, tip, replacemap) = readstate(repo)
444 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
422 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
445 hg.clean(repo, tip)
423 hg.clean(repo, tip)
446 ui.debug('should strip created nodes %s\n' %
424 ui.debug('should strip created nodes %s\n' %
447 ', '.join([node.hex(n)[:12] for n in created]))
425 ', '.join([node.hex(n)[:12] for n in created]))
448 ui.debug('should strip temp nodes %s\n' %
426 ui.debug('should strip temp nodes %s\n' %
449 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
427 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
450 for nodes in (created, tmpnodes):
428 for nodes in (created, tmpnodes):
451 lock = None
429 lock = None
452 try:
430 try:
453 lock = repo.lock()
431 lock = repo.lock()
454 for n in reversed(nodes):
432 for n in reversed(nodes):
455 try:
433 try:
456 repair.strip(ui, repo, n)
434 repair.strip(ui, repo, n)
457 except error.LookupError:
435 except error.LookupError:
458 pass
436 pass
459 finally:
437 finally:
460 lockmod.release(lock)
438 lockmod.release(lock)
461 os.unlink(os.path.join(repo.path, 'histedit-state'))
439 os.unlink(os.path.join(repo.path, 'histedit-state'))
462 return
440 return
463 else:
441 else:
464 cmdutil.bailifchanged(repo)
442 cmdutil.bailifchanged(repo)
465 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
443 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
466 raise util.Abort(_('history edit already in progress, try '
444 raise util.Abort(_('history edit already in progress, try '
467 '--continue or --abort'))
445 '--continue or --abort'))
468
446
469 tip, empty = repo.dirstate.parents()
447 tip, empty = repo.dirstate.parents()
470
448
471
449
472 if len(parent) != 1:
450 if len(parent) != 1:
473 raise util.Abort(_('histedit requires exactly one parent revision'))
451 raise util.Abort(_('histedit requires exactly one parent revision'))
474 parent = scmutil.revsingle(repo, parent[0]).node()
452 parent = scmutil.revsingle(repo, parent[0]).node()
475
453
476 keep = opts.get('keep', False)
454 keep = opts.get('keep', False)
477 revs = between(repo, parent, tip, keep)
455 revs = between(repo, parent, tip, keep)
478
456
479 ctxs = [repo[r] for r in revs]
457 ctxs = [repo[r] for r in revs]
480 existing = [r.node() for r in ctxs]
458 existing = [r.node() for r in ctxs]
481 rules = opts.get('commands', '')
459 rules = opts.get('commands', '')
482 if not rules:
460 if not rules:
483 rules = '\n'.join([makedesc(c) for c in ctxs])
461 rules = '\n'.join([makedesc(c) for c in ctxs])
484 rules += '\n\n'
462 rules += '\n\n'
485 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
463 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
486 rules = ui.edit(rules, ui.username())
464 rules = ui.edit(rules, ui.username())
487 # Save edit rules in .hg/histedit-last-edit.txt in case
465 # Save edit rules in .hg/histedit-last-edit.txt in case
488 # the user needs to ask for help after something
466 # the user needs to ask for help after something
489 # surprising happens.
467 # surprising happens.
490 f = open(repo.join('histedit-last-edit.txt'), 'w')
468 f = open(repo.join('histedit-last-edit.txt'), 'w')
491 f.write(rules)
469 f.write(rules)
492 f.close()
470 f.close()
493 else:
471 else:
494 f = open(rules)
472 f = open(rules)
495 rules = f.read()
473 rules = f.read()
496 f.close()
474 f.close()
497 rules = [l for l in (r.strip() for r in rules.splitlines())
475 rules = [l for l in (r.strip() for r in rules.splitlines())
498 if l and not l[0] == '#']
476 if l and not l[0] == '#']
499 rules = verifyrules(rules, repo, ctxs)
477 rules = verifyrules(rules, repo, ctxs)
500
478
501 parentctx = repo[parent].parents()[0]
479 parentctx = repo[parent].parents()[0]
502 keep = opts.get('keep', False)
480 keep = opts.get('keep', False)
503 replaced = []
481 replaced = []
504 replacemap = {}
482 replacemap = {}
505 tmpnodes = []
483 tmpnodes = []
506 created = []
484 created = []
507
485
508
486
509 while rules:
487 while rules:
510 writestate(repo, parentctx.node(), created, replaced,
488 writestate(repo, parentctx.node(), created, replaced,
511 tmpnodes, existing, rules, keep, tip, replacemap)
489 tmpnodes, existing, rules, keep, tip, replacemap)
512 action, ha = rules.pop(0)
490 action, ha = rules.pop(0)
513 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
491 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
514 ui, repo, parentctx, ha, opts)
492 ui, repo, parentctx, ha, opts)
515
493
516 if replaced_:
494 if replaced_:
517 clen, rlen = len(created_), len(replaced_)
495 clen, rlen = len(created_), len(replaced_)
518 if clen == rlen == 1:
496 if clen == rlen == 1:
519 ui.debug('histedit: exact replacement of %s with %s\n' % (
497 ui.debug('histedit: exact replacement of %s with %s\n' % (
520 node.short(replaced_[0]), node.short(created_[0])))
498 node.short(replaced_[0]), node.short(created_[0])))
521
499
522 replacemap[replaced_[0]] = created_[0]
500 replacemap[replaced_[0]] = created_[0]
523 elif clen > rlen:
501 elif clen > rlen:
524 assert rlen == 1, ('unexpected replacement of '
502 assert rlen == 1, ('unexpected replacement of '
525 '%d changes with %d changes' % (rlen, clen))
503 '%d changes with %d changes' % (rlen, clen))
526 # made more changesets than we're replacing
504 # made more changesets than we're replacing
527 # TODO synthesize patch names for created patches
505 # TODO synthesize patch names for created patches
528 replacemap[replaced_[0]] = created_[-1]
506 replacemap[replaced_[0]] = created_[-1]
529 ui.debug('histedit: created many, assuming %s replaced by %s' %
507 ui.debug('histedit: created many, assuming %s replaced by %s' %
530 (node.short(replaced_[0]), node.short(created_[-1])))
508 (node.short(replaced_[0]), node.short(created_[-1])))
531 elif rlen > clen:
509 elif rlen > clen:
532 if not created_:
510 if not created_:
533 # This must be a drop. Try and put our metadata on
511 # This must be a drop. Try and put our metadata on
534 # the parent change.
512 # the parent change.
535 assert rlen == 1
513 assert rlen == 1
536 r = replaced_[0]
514 r = replaced_[0]
537 ui.debug('histedit: %s seems replaced with nothing, '
515 ui.debug('histedit: %s seems replaced with nothing, '
538 'finding a parent\n' % (node.short(r)))
516 'finding a parent\n' % (node.short(r)))
539 pctx = repo[r].parents()[0]
517 pctx = repo[r].parents()[0]
540 if pctx.node() in replacemap:
518 if pctx.node() in replacemap:
541 ui.debug('histedit: parent is already replaced\n')
519 ui.debug('histedit: parent is already replaced\n')
542 replacemap[r] = replacemap[pctx.node()]
520 replacemap[r] = replacemap[pctx.node()]
543 else:
521 else:
544 replacemap[r] = pctx.node()
522 replacemap[r] = pctx.node()
545 ui.debug('histedit: %s best replaced by %s\n' % (
523 ui.debug('histedit: %s best replaced by %s\n' % (
546 node.short(r), node.short(replacemap[r])))
524 node.short(r), node.short(replacemap[r])))
547 else:
525 else:
548 assert len(created_) == 1
526 assert len(created_) == 1
549 for r in replaced_:
527 for r in replaced_:
550 ui.debug('histedit: %s replaced by %s\n' % (
528 ui.debug('histedit: %s replaced by %s\n' % (
551 node.short(r), node.short(created_[0])))
529 node.short(r), node.short(created_[0])))
552 replacemap[r] = created_[0]
530 replacemap[r] = created_[0]
553 else:
531 else:
554 assert False, (
532 assert False, (
555 'Unhandled case in replacement mapping! '
533 'Unhandled case in replacement mapping! '
556 'replacing %d changes with %d changes' % (rlen, clen))
534 'replacing %d changes with %d changes' % (rlen, clen))
557 created.extend(created_)
535 created.extend(created_)
558 replaced.extend(replaced_)
536 replaced.extend(replaced_)
559 tmpnodes.extend(tmpnodes_)
537 tmpnodes.extend(tmpnodes_)
560
538
561 hg.update(repo, parentctx.node())
539 hg.update(repo, parentctx.node())
562
540
563 if not keep:
541 if not keep:
564 if replacemap:
542 if replacemap:
565 ui.note(_('histedit: Should update metadata for the following '
543 ui.note(_('histedit: Should update metadata for the following '
566 'changes:\n'))
544 'changes:\n'))
567
545
568 def copybms(old, new):
546 def copybms(old, new):
569 if old in tmpnodes or old in created:
547 if old in tmpnodes or old in created:
570 # can't have any metadata we'd want to update
548 # can't have any metadata we'd want to update
571 return
549 return
572 while new in replacemap:
550 while new in replacemap:
573 new = replacemap[new]
551 new = replacemap[new]
574 ui.note(_('histedit: %s to %s\n') % (node.short(old),
552 ui.note(_('histedit: %s to %s\n') % (node.short(old),
575 node.short(new)))
553 node.short(new)))
576 octx = repo[old]
554 octx = repo[old]
577 marks = octx.bookmarks()
555 marks = octx.bookmarks()
578 if marks:
556 if marks:
579 ui.note(_('histedit: moving bookmarks %s\n') %
557 ui.note(_('histedit: moving bookmarks %s\n') %
580 ', '.join(marks))
558 ', '.join(marks))
581 for mark in marks:
559 for mark in marks:
582 repo._bookmarks[mark] = new
560 repo._bookmarks[mark] = new
583 bookmarks.write(repo)
561 bookmarks.write(repo)
584
562
585 # We assume that bookmarks on the tip should remain
563 # We assume that bookmarks on the tip should remain
586 # tipmost, but bookmarks on non-tip changesets should go
564 # tipmost, but bookmarks on non-tip changesets should go
587 # to their most reasonable successor. As a result, find
565 # to their most reasonable successor. As a result, find
588 # the old tip and new tip and copy those bookmarks first,
566 # the old tip and new tip and copy those bookmarks first,
589 # then do the rest of the bookmark copies.
567 # then do the rest of the bookmark copies.
590 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
568 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
591 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
569 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
592 copybms(oldtip, newtip)
570 copybms(oldtip, newtip)
593
571
594 for old, new in sorted(replacemap.iteritems()):
572 for old, new in sorted(replacemap.iteritems()):
595 copybms(old, new)
573 copybms(old, new)
596 # TODO update mq state
574 # TODO update mq state
597
575
598 ui.debug('should strip replaced nodes %s\n' %
576 ui.debug('should strip replaced nodes %s\n' %
599 ', '.join([node.hex(n)[:12] for n in replaced]))
577 ', '.join([node.hex(n)[:12] for n in replaced]))
600 lock = None
578 lock = None
601 try:
579 try:
602 lock = repo.lock()
580 lock = repo.lock()
603 for n in sorted(replaced, key=lambda x: repo[x].rev()):
581 for n in sorted(replaced, key=lambda x: repo[x].rev()):
604 try:
582 try:
605 repair.strip(ui, repo, n)
583 repair.strip(ui, repo, n)
606 except error.LookupError:
584 except error.LookupError:
607 pass
585 pass
608 finally:
586 finally:
609 lockmod.release(lock)
587 lockmod.release(lock)
610
588
611 ui.debug('should strip temp nodes %s\n' %
589 ui.debug('should strip temp nodes %s\n' %
612 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
590 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
613 lock = None
591 lock = None
614 try:
592 try:
615 lock = repo.lock()
593 lock = repo.lock()
616 for n in reversed(tmpnodes):
594 for n in reversed(tmpnodes):
617 try:
595 try:
618 repair.strip(ui, repo, n)
596 repair.strip(ui, repo, n)
619 except error.LookupError:
597 except error.LookupError:
620 pass
598 pass
621 finally:
599 finally:
622 lockmod.release(lock)
600 lockmod.release(lock)
623 os.unlink(os.path.join(repo.path, 'histedit-state'))
601 os.unlink(os.path.join(repo.path, 'histedit-state'))
624 if os.path.exists(repo.sjoin('undo')):
602 if os.path.exists(repo.sjoin('undo')):
625 os.unlink(repo.sjoin('undo'))
603 os.unlink(repo.sjoin('undo'))
626
604
627
605
606 def between(repo, old, new, keep):
607 """select and validate the set of revision to edit
608
609 When keep is false, the specified set can't have children."""
610 revs = [old]
611 current = old
612 while current != new:
613 ctx = repo[current]
614 if not keep and len(ctx.children()) > 1:
615 raise util.Abort(_('cannot edit history that would orphan nodes'))
616 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
617 raise util.Abort(_("can't edit history with merges"))
618 if not ctx.children():
619 current = new
620 else:
621 current = ctx.children()[0].node()
622 revs.append(current)
623 if len(repo[current].children()) and not keep:
624 raise util.Abort(_('cannot edit history that would orphan nodes'))
625 return revs
626
627
628 def writestate(repo, parentctxnode, created, replaced,
628 def writestate(repo, parentctxnode, created, replaced,
629 tmpnodes, existing, rules, keep, oldtip, replacemap):
629 tmpnodes, existing, rules, keep, oldtip, replacemap):
630 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
630 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
631 pickle.dump((parentctxnode, created, replaced,
631 pickle.dump((parentctxnode, created, replaced,
632 tmpnodes, existing, rules, keep, oldtip, replacemap),
632 tmpnodes, existing, rules, keep, oldtip, replacemap),
633 fp)
633 fp)
634 fp.close()
634 fp.close()
635
635
636 def readstate(repo):
636 def readstate(repo):
637 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
637 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
638 keep, oldtip, replacemap ).
638 keep, oldtip, replacemap ).
639 """
639 """
640 fp = open(os.path.join(repo.path, 'histedit-state'))
640 fp = open(os.path.join(repo.path, 'histedit-state'))
641 return pickle.load(fp)
641 return pickle.load(fp)
642
642
643
643
644 def verifyrules(rules, repo, ctxs):
644 def verifyrules(rules, repo, ctxs):
645 """Verify that there exists exactly one edit rule per given changeset.
645 """Verify that there exists exactly one edit rule per given changeset.
646
646
647 Will abort if there are to many or too few rules, a malformed rule,
647 Will abort if there are to many or too few rules, a malformed rule,
648 or a rule on a changeset outside of the user-given range.
648 or a rule on a changeset outside of the user-given range.
649 """
649 """
650 parsed = []
650 parsed = []
651 if len(rules) != len(ctxs):
651 if len(rules) != len(ctxs):
652 raise util.Abort(_('must specify a rule for each changeset once'))
652 raise util.Abort(_('must specify a rule for each changeset once'))
653 for r in rules:
653 for r in rules:
654 if ' ' not in r:
654 if ' ' not in r:
655 raise util.Abort(_('malformed line "%s"') % r)
655 raise util.Abort(_('malformed line "%s"') % r)
656 action, rest = r.split(' ', 1)
656 action, rest = r.split(' ', 1)
657 if ' ' in rest.strip():
657 if ' ' in rest.strip():
658 ha, rest = rest.split(' ', 1)
658 ha, rest = rest.split(' ', 1)
659 else:
659 else:
660 ha = r.strip()
660 ha = r.strip()
661 try:
661 try:
662 if repo[ha] not in ctxs:
662 if repo[ha] not in ctxs:
663 raise util.Abort(
663 raise util.Abort(
664 _('may not use changesets other than the ones listed'))
664 _('may not use changesets other than the ones listed'))
665 except error.RepoError:
665 except error.RepoError:
666 raise util.Abort(_('unknown changeset %s listed') % ha)
666 raise util.Abort(_('unknown changeset %s listed') % ha)
667 if action not in actiontable:
667 if action not in actiontable:
668 raise util.Abort(_('unknown action "%s"') % action)
668 raise util.Abort(_('unknown action "%s"') % action)
669 parsed.append([action, ha])
669 parsed.append([action, ha])
670 return parsed
670 return parsed
General Comments 0
You need to be logged in to leave comments. Login now