##// END OF EJS Templates
histedit: fix new nodes computation with --continue (issue3534)...
Patrick Mezard -
r17242:33612108 stable
parent child Browse files
Show More
@@ -1,695 +1,702 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between 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
36 # f, fold = use commit, but fold into previous commit
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 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
40 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
41
41
42 In this file, lines beginning with ``#`` are ignored. You must specify a rule
42 In this file, lines beginning with ``#`` are ignored. You must specify a rule
43 for each revision in your history. For example, if you had meant to add gamma
43 for each revision in your history. For example, if you had meant to add gamma
44 before beta, and then wanted to add delta in the same revision as beta, you
44 before beta, and then wanted to add delta in the same revision as beta, you
45 would reorganize the file to look like this::
45 would reorganize the file to look like this::
46
46
47 pick 030b686bedc4 Add gamma
47 pick 030b686bedc4 Add gamma
48 pick c561b4e977df Add beta
48 pick c561b4e977df Add beta
49 fold 7c2fd3b9020c Add delta
49 fold 7c2fd3b9020c Add delta
50
50
51 # Edit history between 633536316234 and 7c2fd3b9020c
51 # Edit history between 633536316234 and 7c2fd3b9020c
52 #
52 #
53 # Commands:
53 # Commands:
54 # p, pick = use commit
54 # p, pick = use commit
55 # e, edit = use commit, but stop for amending
55 # e, edit = use commit, but stop for amending
56 # f, fold = use commit, but fold into previous commit
56 # f, fold = use commit, but fold into previous commit
57 # d, drop = remove commit from history
57 # d, drop = remove commit from history
58 # m, mess = edit message without changing commit content
58 # m, mess = edit message without changing commit content
59 #
59 #
60 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
61
61
62 At which point you close the editor and ``histedit`` starts working. When you
62 At which point you close the editor and ``histedit`` starts working. When you
63 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
63 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
64 those revisions together, offering you a chance to clean up the commit message::
64 those revisions together, offering you a chance to clean up the commit message::
65
65
66 Add beta
66 Add beta
67 ***
67 ***
68 Add delta
68 Add delta
69
69
70 Edit the commit message to your liking, then close the editor. For
70 Edit the commit message to your liking, then close the editor. For
71 this example, let's assume that the commit message was changed to
71 this example, let's assume that the commit message was changed to
72 ``Add beta and delta.`` After histedit has run and had a chance to
72 ``Add beta and delta.`` After histedit has run and had a chance to
73 remove any old or temporary revisions it needed, the history looks
73 remove any old or temporary revisions it needed, the history looks
74 like this::
74 like this::
75
75
76 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
76 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
77 | Add beta and delta.
77 | Add beta and delta.
78 |
78 |
79 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
79 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
80 | Add gamma
80 | Add gamma
81 |
81 |
82 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
82 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
83 Add alpha
83 Add alpha
84
84
85 Note that ``histedit`` does *not* remove any revisions (even its own temporary
85 Note that ``histedit`` does *not* remove any revisions (even its own temporary
86 ones) until after it has completed all the editing operations, so it will
86 ones) until after it has completed all the editing operations, so it will
87 probably perform several strip operations when it's done. For the above example,
87 probably perform several strip operations when it's done. For the above example,
88 it had to run strip twice. Strip can be slow depending on a variety of factors,
88 it had to run strip twice. Strip can be slow depending on a variety of factors,
89 so you might need to be a little patient. You can choose to keep the original
89 so you might need to be a little patient. You can choose to keep the original
90 revisions by passing the ``--keep`` flag.
90 revisions by passing the ``--keep`` flag.
91
91
92 The ``edit`` operation will drop you back to a command prompt,
92 The ``edit`` operation will drop you back to a command prompt,
93 allowing you to edit files freely, or even use ``hg record`` to commit
93 allowing you to edit files freely, or even use ``hg record`` to commit
94 some changes as a separate commit. When you're done, any remaining
94 some changes as a separate commit. When you're done, any remaining
95 uncommitted changes will be committed as well. When done, run ``hg
95 uncommitted changes will be committed as well. When done, run ``hg
96 histedit --continue`` to finish this step. You'll be prompted for a
96 histedit --continue`` to finish this step. You'll be prompted for a
97 new commit message, but the default commit message will be the
97 new commit message, but the default commit message will be the
98 original message for the ``edit`` ed revision.
98 original message for the ``edit`` ed revision.
99
99
100 The ``message`` operation will give you a chance to revise a commit
100 The ``message`` operation will give you a chance to revise a commit
101 message without changing the contents. It's a shortcut for doing
101 message without changing the contents. It's a shortcut for doing
102 ``edit`` immediately followed by `hg histedit --continue``.
102 ``edit`` immediately followed by `hg histedit --continue``.
103
103
104 If ``histedit`` encounters a conflict when moving a revision (while
104 If ``histedit`` encounters a conflict when moving a revision (while
105 handling ``pick`` or ``fold``), it'll stop in a similar manner to
105 handling ``pick`` or ``fold``), it'll stop in a similar manner to
106 ``edit`` with the difference that it won't prompt you for a commit
106 ``edit`` with the difference that it won't prompt you for a commit
107 message when done. If you decide at this point that you don't like how
107 message when done. If you decide at this point that you don't like how
108 much work it will be to rearrange history, or that you made a mistake,
108 much work it will be to rearrange history, or that you made a mistake,
109 you can use ``hg histedit --abort`` to abandon the new changes you
109 you can use ``hg histedit --abort`` to abandon the new changes you
110 have made and return to the state before you attempted to edit your
110 have made and return to the state before you attempted to edit your
111 history.
111 history.
112
112
113 If we clone the example repository above and add three more changes, such that
113 If we clone the example repository above and add three more changes, such that
114 we have the following history::
114 we have the following history::
115
115
116 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
116 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
117 | Add theta
117 | Add theta
118 |
118 |
119 o 5 140988835471 2009-04-27 18:04 -0500 stefan
119 o 5 140988835471 2009-04-27 18:04 -0500 stefan
120 | Add eta
120 | Add eta
121 |
121 |
122 o 4 122930637314 2009-04-27 18:04 -0500 stefan
122 o 4 122930637314 2009-04-27 18:04 -0500 stefan
123 | Add zeta
123 | Add zeta
124 |
124 |
125 o 3 836302820282 2009-04-27 18:04 -0500 stefan
125 o 3 836302820282 2009-04-27 18:04 -0500 stefan
126 | Add epsilon
126 | Add epsilon
127 |
127 |
128 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
128 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
129 | Add beta and delta.
129 | Add beta and delta.
130 |
130 |
131 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
131 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
132 | Add gamma
132 | Add gamma
133 |
133 |
134 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
134 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
135 Add alpha
135 Add alpha
136
136
137 If you run ``hg histedit --outgoing`` on the clone then it is the same
137 If you run ``hg histedit --outgoing`` on the clone then it is the same
138 as running ``hg histedit 836302820282``. If you need plan to push to a
138 as running ``hg histedit 836302820282``. If you need plan to push to a
139 repository that Mercurial does not detect to be related to the source
139 repository that Mercurial does not detect to be related to the source
140 repo, you can add a ``--force`` option.
140 repo, you can add a ``--force`` option.
141 """
141 """
142
142
143 try:
143 try:
144 import cPickle as pickle
144 import cPickle as pickle
145 except ImportError:
145 except ImportError:
146 import pickle
146 import pickle
147 import tempfile
147 import tempfile
148 import os
148 import os
149
149
150 from mercurial import bookmarks
150 from mercurial import bookmarks
151 from mercurial import cmdutil
151 from mercurial import cmdutil
152 from mercurial import discovery
152 from mercurial import discovery
153 from mercurial import error
153 from mercurial import error
154 from mercurial import hg
154 from mercurial import hg
155 from mercurial import node
155 from mercurial import node
156 from mercurial import patch
156 from mercurial import patch
157 from mercurial import repair
157 from mercurial import repair
158 from mercurial import scmutil
158 from mercurial import scmutil
159 from mercurial import util
159 from mercurial import util
160 from mercurial.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 editcomment = """
167 editcomment = """
168
168
169 # Edit history between %s and %s
169 # Edit history between %s and %s
170 #
170 #
171 # Commands:
171 # Commands:
172 # p, pick = use commit
172 # p, pick = use commit
173 # e, edit = use commit, but stop for amending
173 # e, edit = use commit, but stop for amending
174 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 # f, fold = use commit, but fold into previous commit (combines N and N-1)
175 # d, drop = remove commit from history
175 # d, drop = remove commit from history
176 # m, mess = edit message without changing commit content
176 # m, mess = edit message without changing commit content
177 #
177 #
178 """
178 """
179
179
180 def between(repo, old, new, keep):
180 def between(repo, old, new, keep):
181 revs = [old]
181 revs = [old]
182 current = old
182 current = old
183 while current != new:
183 while current != new:
184 ctx = repo[current]
184 ctx = repo[current]
185 if not keep and len(ctx.children()) > 1:
185 if not keep and len(ctx.children()) > 1:
186 raise util.Abort(_('cannot edit history that would orphan nodes'))
186 raise util.Abort(_('cannot edit history that would orphan nodes'))
187 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
187 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
188 raise util.Abort(_("can't edit history with merges"))
188 raise util.Abort(_("can't edit history with merges"))
189 if not ctx.children():
189 if not ctx.children():
190 current = new
190 current = new
191 else:
191 else:
192 current = ctx.children()[0].node()
192 current = ctx.children()[0].node()
193 revs.append(current)
193 revs.append(current)
194 if len(repo[current].children()) and not keep:
194 if len(repo[current].children()) and not keep:
195 raise util.Abort(_('cannot edit history that would orphan nodes'))
195 raise util.Abort(_('cannot edit history that would orphan nodes'))
196 return revs
196 return revs
197
197
198
198
199 def pick(ui, repo, ctx, ha, opts):
199 def pick(ui, repo, ctx, ha, opts):
200 oldctx = repo[ha]
200 oldctx = repo[ha]
201 if oldctx.parents()[0] == ctx:
201 if oldctx.parents()[0] == ctx:
202 ui.debug('node %s unchanged\n' % ha)
202 ui.debug('node %s unchanged\n' % ha)
203 return oldctx, [], [], []
203 return oldctx, [], [], []
204 hg.update(repo, ctx.node())
204 hg.update(repo, ctx.node())
205 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
205 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
206 fp = os.fdopen(fd, 'w')
206 fp = os.fdopen(fd, 'w')
207 diffopts = patch.diffopts(ui, opts)
207 diffopts = patch.diffopts(ui, opts)
208 diffopts.git = True
208 diffopts.git = True
209 diffopts.ignorews = False
209 diffopts.ignorews = False
210 diffopts.ignorewsamount = False
210 diffopts.ignorewsamount = False
211 diffopts.ignoreblanklines = False
211 diffopts.ignoreblanklines = False
212 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
212 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
213 for chunk in gen:
213 for chunk in gen:
214 fp.write(chunk)
214 fp.write(chunk)
215 fp.close()
215 fp.close()
216 try:
216 try:
217 files = set()
217 files = set()
218 try:
218 try:
219 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
219 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
220 if not files:
220 if not files:
221 ui.warn(_('%s: empty changeset')
221 ui.warn(_('%s: empty changeset')
222 % node.hex(ha))
222 % node.hex(ha))
223 return ctx, [], [], []
223 return ctx, [], [], []
224 finally:
224 finally:
225 os.unlink(patchfile)
225 os.unlink(patchfile)
226 except Exception:
226 except Exception:
227 raise util.Abort(_('Fix up the change and run '
227 raise util.Abort(_('Fix up the change and run '
228 'hg histedit --continue'))
228 'hg histedit --continue'))
229 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
229 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
230 date=oldctx.date(), extra=oldctx.extra())
230 date=oldctx.date(), extra=oldctx.extra())
231 return repo[n], [n], [oldctx.node()], []
231 return repo[n], [n], [oldctx.node()], []
232
232
233
233
234 def edit(ui, repo, ctx, ha, opts):
234 def edit(ui, repo, ctx, ha, opts):
235 oldctx = repo[ha]
235 oldctx = repo[ha]
236 hg.update(repo, ctx.node())
236 hg.update(repo, ctx.node())
237 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
237 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
238 fp = os.fdopen(fd, 'w')
238 fp = os.fdopen(fd, 'w')
239 diffopts = patch.diffopts(ui, opts)
239 diffopts = patch.diffopts(ui, opts)
240 diffopts.git = True
240 diffopts.git = True
241 diffopts.ignorews = False
241 diffopts.ignorews = False
242 diffopts.ignorewsamount = False
242 diffopts.ignorewsamount = False
243 diffopts.ignoreblanklines = False
243 diffopts.ignoreblanklines = False
244 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
244 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
245 for chunk in gen:
245 for chunk in gen:
246 fp.write(chunk)
246 fp.write(chunk)
247 fp.close()
247 fp.close()
248 try:
248 try:
249 files = set()
249 files = set()
250 try:
250 try:
251 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
251 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
252 finally:
252 finally:
253 os.unlink(patchfile)
253 os.unlink(patchfile)
254 except Exception:
254 except Exception:
255 pass
255 pass
256 raise util.Abort(_('Make changes as needed, you may commit or record as '
256 raise util.Abort(_('Make changes as needed, you may commit or record as '
257 'needed now.\nWhen you are finished, run hg'
257 'needed now.\nWhen you are finished, run hg'
258 ' histedit --continue to resume.'))
258 ' histedit --continue to resume.'))
259
259
260 def fold(ui, repo, ctx, ha, opts):
260 def fold(ui, repo, ctx, ha, opts):
261 oldctx = repo[ha]
261 oldctx = repo[ha]
262 hg.update(repo, ctx.node())
262 hg.update(repo, ctx.node())
263 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
263 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
264 fp = os.fdopen(fd, 'w')
264 fp = os.fdopen(fd, 'w')
265 diffopts = patch.diffopts(ui, opts)
265 diffopts = patch.diffopts(ui, opts)
266 diffopts.git = True
266 diffopts.git = True
267 diffopts.ignorews = False
267 diffopts.ignorews = False
268 diffopts.ignorewsamount = False
268 diffopts.ignorewsamount = False
269 diffopts.ignoreblanklines = False
269 diffopts.ignoreblanklines = False
270 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
270 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
271 for chunk in gen:
271 for chunk in gen:
272 fp.write(chunk)
272 fp.write(chunk)
273 fp.close()
273 fp.close()
274 try:
274 try:
275 files = set()
275 files = set()
276 try:
276 try:
277 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
277 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
278 if not files:
278 if not files:
279 ui.warn(_('%s: empty changeset')
279 ui.warn(_('%s: empty changeset')
280 % node.hex(ha))
280 % node.hex(ha))
281 return ctx, [], [], []
281 return ctx, [], [], []
282 finally:
282 finally:
283 os.unlink(patchfile)
283 os.unlink(patchfile)
284 except Exception:
284 except Exception:
285 raise util.Abort(_('Fix up the change and run '
285 raise util.Abort(_('Fix up the change and run '
286 'hg histedit --continue'))
286 'hg histedit --continue'))
287 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
287 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
288 date=oldctx.date(), extra=oldctx.extra())
288 date=oldctx.date(), extra=oldctx.extra())
289 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
289 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
290
290
291 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
291 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
292 parent = ctx.parents()[0].node()
292 parent = ctx.parents()[0].node()
293 hg.update(repo, parent)
293 hg.update(repo, parent)
294 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
294 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
295 fp = os.fdopen(fd, 'w')
295 fp = os.fdopen(fd, 'w')
296 diffopts = patch.diffopts(ui, opts)
296 diffopts = patch.diffopts(ui, opts)
297 diffopts.git = True
297 diffopts.git = True
298 diffopts.ignorews = False
298 diffopts.ignorews = False
299 diffopts.ignorewsamount = False
299 diffopts.ignorewsamount = False
300 diffopts.ignoreblanklines = False
300 diffopts.ignoreblanklines = False
301 gen = patch.diff(repo, parent, newnode, opts=diffopts)
301 gen = patch.diff(repo, parent, newnode, opts=diffopts)
302 for chunk in gen:
302 for chunk in gen:
303 fp.write(chunk)
303 fp.write(chunk)
304 fp.close()
304 fp.close()
305 files = set()
305 files = set()
306 try:
306 try:
307 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
307 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
308 finally:
308 finally:
309 os.unlink(patchfile)
309 os.unlink(patchfile)
310 newmessage = '\n***\n'.join(
310 newmessage = '\n***\n'.join(
311 [ctx.description()] +
311 [ctx.description()] +
312 [repo[r].description() for r in internalchanges] +
312 [repo[r].description() for r in internalchanges] +
313 [oldctx.description()]) + '\n'
313 [oldctx.description()]) + '\n'
314 # If the changesets are from the same author, keep it.
314 # If the changesets are from the same author, keep it.
315 if ctx.user() == oldctx.user():
315 if ctx.user() == oldctx.user():
316 username = ctx.user()
316 username = ctx.user()
317 else:
317 else:
318 username = ui.username()
318 username = ui.username()
319 newmessage = ui.edit(newmessage, username)
319 newmessage = ui.edit(newmessage, username)
320 n = repo.commit(text=newmessage, user=username,
320 n = repo.commit(text=newmessage, user=username,
321 date=max(ctx.date(), oldctx.date()), extra=oldctx.extra())
321 date=max(ctx.date(), oldctx.date()), extra=oldctx.extra())
322 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
322 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
323
323
324 def drop(ui, repo, ctx, ha, opts):
324 def drop(ui, repo, ctx, ha, opts):
325 return ctx, [], [repo[ha].node()], []
325 return ctx, [], [repo[ha].node()], []
326
326
327
327
328 def message(ui, repo, ctx, ha, opts):
328 def message(ui, repo, ctx, ha, opts):
329 oldctx = repo[ha]
329 oldctx = repo[ha]
330 hg.update(repo, ctx.node())
330 hg.update(repo, ctx.node())
331 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
331 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
332 fp = os.fdopen(fd, 'w')
332 fp = os.fdopen(fd, 'w')
333 diffopts = patch.diffopts(ui, opts)
333 diffopts = patch.diffopts(ui, opts)
334 diffopts.git = True
334 diffopts.git = True
335 diffopts.ignorews = False
335 diffopts.ignorews = False
336 diffopts.ignorewsamount = False
336 diffopts.ignorewsamount = False
337 diffopts.ignoreblanklines = False
337 diffopts.ignoreblanklines = False
338 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
338 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
339 for chunk in gen:
339 for chunk in gen:
340 fp.write(chunk)
340 fp.write(chunk)
341 fp.close()
341 fp.close()
342 try:
342 try:
343 files = set()
343 files = set()
344 try:
344 try:
345 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
345 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
346 finally:
346 finally:
347 os.unlink(patchfile)
347 os.unlink(patchfile)
348 except Exception:
348 except Exception:
349 raise util.Abort(_('Fix up the change and run '
349 raise util.Abort(_('Fix up the change and run '
350 'hg histedit --continue'))
350 'hg histedit --continue'))
351 message = oldctx.description()
351 message = oldctx.description()
352 message = ui.edit(message, ui.username())
352 message = ui.edit(message, ui.username())
353 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
353 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
354 extra=oldctx.extra())
354 extra=oldctx.extra())
355 newctx = repo[new]
355 newctx = repo[new]
356 if oldctx.node() != newctx.node():
356 if oldctx.node() != newctx.node():
357 return newctx, [new], [oldctx.node()], []
357 return newctx, [new], [oldctx.node()], []
358 # We didn't make an edit, so just indicate no replaced nodes
358 # We didn't make an edit, so just indicate no replaced nodes
359 return newctx, [new], [], []
359 return newctx, [new], [], []
360
360
361
361
362 def makedesc(c):
362 def makedesc(c):
363 summary = ''
363 summary = ''
364 if c.description():
364 if c.description():
365 summary = c.description().splitlines()[0]
365 summary = c.description().splitlines()[0]
366 line = 'pick %s %d %s' % (c.hex()[:12], c.rev(), summary)
366 line = 'pick %s %d %s' % (c.hex()[:12], c.rev(), summary)
367 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
367 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
368
368
369 actiontable = {'p': pick,
369 actiontable = {'p': pick,
370 'pick': pick,
370 'pick': pick,
371 'e': edit,
371 'e': edit,
372 'edit': edit,
372 'edit': edit,
373 'f': fold,
373 'f': fold,
374 'fold': fold,
374 'fold': fold,
375 'd': drop,
375 'd': drop,
376 'drop': drop,
376 'drop': drop,
377 'm': message,
377 'm': message,
378 'mess': message,
378 'mess': message,
379 }
379 }
380
380
381 @command('histedit',
381 @command('histedit',
382 [('', 'commands', '',
382 [('', 'commands', '',
383 _('Read history edits from the specified file.')),
383 _('Read history edits from the specified file.')),
384 ('c', 'continue', False, _('continue an edit already in progress')),
384 ('c', 'continue', False, _('continue an edit already in progress')),
385 ('k', 'keep', False,
385 ('k', 'keep', False,
386 _("don't strip old nodes after edit is complete")),
386 _("don't strip old nodes after edit is complete")),
387 ('', 'abort', False, _('abort an edit in progress')),
387 ('', 'abort', False, _('abort an edit in progress')),
388 ('o', 'outgoing', False, _('changesets not found in destination')),
388 ('o', 'outgoing', False, _('changesets not found in destination')),
389 ('f', 'force', False,
389 ('f', 'force', False,
390 _('force outgoing even for unrelated repositories')),
390 _('force outgoing even for unrelated repositories')),
391 ('r', 'rev', [], _('first revision to be edited'))],
391 ('r', 'rev', [], _('first revision to be edited'))],
392 _("[PARENT]"))
392 _("[PARENT]"))
393 def histedit(ui, repo, *parent, **opts):
393 def histedit(ui, repo, *parent, **opts):
394 """interactively edit changeset history
394 """interactively edit changeset history
395 """
395 """
396 # TODO only abort if we try and histedit mq patches, not just
396 # TODO only abort if we try and histedit mq patches, not just
397 # blanket if mq patches are applied somewhere
397 # blanket if mq patches are applied somewhere
398 mq = getattr(repo, 'mq', None)
398 mq = getattr(repo, 'mq', None)
399 if mq and mq.applied:
399 if mq and mq.applied:
400 raise util.Abort(_('source has mq patches applied'))
400 raise util.Abort(_('source has mq patches applied'))
401
401
402 parent = list(parent) + opts.get('rev', [])
402 parent = list(parent) + opts.get('rev', [])
403 if opts.get('outgoing'):
403 if opts.get('outgoing'):
404 if len(parent) > 1:
404 if len(parent) > 1:
405 raise util.Abort(
405 raise util.Abort(
406 _('only one repo argument allowed with --outgoing'))
406 _('only one repo argument allowed with --outgoing'))
407 elif parent:
407 elif parent:
408 parent = parent[0]
408 parent = parent[0]
409
409
410 dest = ui.expandpath(parent or 'default-push', parent or 'default')
410 dest = ui.expandpath(parent or 'default-push', parent or 'default')
411 dest, revs = hg.parseurl(dest, None)[:2]
411 dest, revs = hg.parseurl(dest, None)[:2]
412 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
412 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
413
413
414 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
414 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
415 other = hg.peer(repo, opts, dest)
415 other = hg.peer(repo, opts, dest)
416
416
417 if revs:
417 if revs:
418 revs = [repo.lookup(rev) for rev in revs]
418 revs = [repo.lookup(rev) for rev in revs]
419
419
420 parent = discovery.findcommonoutgoing(
420 parent = discovery.findcommonoutgoing(
421 repo, other, [], force=opts.get('force')).missing[0:1]
421 repo, other, [], force=opts.get('force')).missing[0:1]
422 else:
422 else:
423 if opts.get('force'):
423 if opts.get('force'):
424 raise util.Abort(_('--force only allowed with --outgoing'))
424 raise util.Abort(_('--force only allowed with --outgoing'))
425
425
426 if opts.get('continue', False):
426 if opts.get('continue', False):
427 if len(parent) != 0:
427 if len(parent) != 0:
428 raise util.Abort(_('no arguments allowed with --continue'))
428 raise util.Abort(_('no arguments allowed with --continue'))
429 (parentctxnode, created, replaced,
429 (parentctxnode, created, replaced,
430 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
430 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
431 currentparent, wantnull = repo.dirstate.parents()
431 currentparent, wantnull = repo.dirstate.parents()
432 parentctx = repo[parentctxnode]
432 parentctx = repo[parentctxnode]
433 # discover any nodes the user has added in the interim
433 # existing is the list of revisions initially considered by
434 newchildren = [c for c in parentctx.children()
434 # histedit. Here we use it to list new changesets, descendants
435 if c.node() not in existing]
435 # of parentctx without an 'existing' changeset in-between. We
436 # also have to exclude 'existing' changesets which were
437 # previously dropped.
438 descendants = set(c.node() for c in
439 repo.set('(%n::) - %n', parentctxnode, parentctxnode))
440 existing = set(existing)
441 notdropped = set(n for n in existing if n in descendants and
442 (n not in replacemap or replacemap[n] in descendants))
443 # Discover any nodes the user has added in the interim. We can
444 # miss changesets which were dropped and recreated the same.
445 newchildren = list(c.node() for c in repo.set(
446 'sort(%ln - (%ln or %ln::))', descendants, existing, notdropped))
436 action, currentnode = rules.pop(0)
447 action, currentnode = rules.pop(0)
437 while newchildren:
438 if action in ('f', 'fold'):
448 if action in ('f', 'fold'):
439 tmpnodes.extend([n.node() for n in newchildren])
449 tmpnodes.extend(newchildren)
440 else:
450 else:
441 created.extend([n.node() for n in newchildren])
451 created.extend(newchildren)
442 filtered = []
452
443 for r in newchildren:
444 filtered += [c for c in r.children() if c.node not in existing]
445 newchildren = filtered
446 m, a, r, d = repo.status()[:4]
453 m, a, r, d = repo.status()[:4]
447 oldctx = repo[currentnode]
454 oldctx = repo[currentnode]
448 message = oldctx.description()
455 message = oldctx.description()
449 if action in ('e', 'edit', 'm', 'mess'):
456 if action in ('e', 'edit', 'm', 'mess'):
450 message = ui.edit(message, ui.username())
457 message = ui.edit(message, ui.username())
451 elif action in ('f', 'fold'):
458 elif action in ('f', 'fold'):
452 message = 'fold-temp-revision %s' % currentnode
459 message = 'fold-temp-revision %s' % currentnode
453 new = None
460 new = None
454 if m or a or r or d:
461 if m or a or r or d:
455 new = repo.commit(text=message, user=oldctx.user(),
462 new = repo.commit(text=message, user=oldctx.user(),
456 date=oldctx.date(), extra=oldctx.extra())
463 date=oldctx.date(), extra=oldctx.extra())
457
464
458 # If we're resuming a fold and we have new changes, mark the
465 # If we're resuming a fold and we have new changes, mark the
459 # replacements and finish the fold. If not, it's more like a
466 # replacements and finish the fold. If not, it's more like a
460 # drop of the changesets that disappeared, and we can skip
467 # drop of the changesets that disappeared, and we can skip
461 # this step.
468 # this step.
462 if action in ('f', 'fold') and (new or newchildren):
469 if action in ('f', 'fold') and (new or newchildren):
463 if new:
470 if new:
464 tmpnodes.append(new)
471 tmpnodes.append(new)
465 else:
472 else:
466 new = newchildren[-1]
473 new = newchildren[-1]
467 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
474 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
468 ui, repo, parentctx, oldctx, new, opts, newchildren)
475 ui, repo, parentctx, oldctx, new, opts, newchildren)
469 replaced.extend(replaced_)
476 replaced.extend(replaced_)
470 created.extend(created_)
477 created.extend(created_)
471 tmpnodes.extend(tmpnodes_)
478 tmpnodes.extend(tmpnodes_)
472 elif action not in ('d', 'drop'):
479 elif action not in ('d', 'drop'):
473 if new != oldctx.node():
480 if new != oldctx.node():
474 replaced.append(oldctx.node())
481 replaced.append(oldctx.node())
475 if new:
482 if new:
476 if new != oldctx.node():
483 if new != oldctx.node():
477 created.append(new)
484 created.append(new)
478 parentctx = repo[new]
485 parentctx = repo[new]
479
486
480 elif opts.get('abort', False):
487 elif opts.get('abort', False):
481 if len(parent) != 0:
488 if len(parent) != 0:
482 raise util.Abort(_('no arguments allowed with --abort'))
489 raise util.Abort(_('no arguments allowed with --abort'))
483 (parentctxnode, created, replaced, tmpnodes,
490 (parentctxnode, created, replaced, tmpnodes,
484 existing, rules, keep, tip, replacemap) = readstate(repo)
491 existing, rules, keep, tip, replacemap) = readstate(repo)
485 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
492 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
486 hg.clean(repo, tip)
493 hg.clean(repo, tip)
487 ui.debug('should strip created nodes %s\n' %
494 ui.debug('should strip created nodes %s\n' %
488 ', '.join([node.hex(n)[:12] for n in created]))
495 ', '.join([node.hex(n)[:12] for n in created]))
489 ui.debug('should strip temp nodes %s\n' %
496 ui.debug('should strip temp nodes %s\n' %
490 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
497 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
491 for nodes in (created, tmpnodes):
498 for nodes in (created, tmpnodes):
492 for n in reversed(nodes):
499 for n in reversed(nodes):
493 try:
500 try:
494 repair.strip(ui, repo, n)
501 repair.strip(ui, repo, n)
495 except error.LookupError:
502 except error.LookupError:
496 pass
503 pass
497 os.unlink(os.path.join(repo.path, 'histedit-state'))
504 os.unlink(os.path.join(repo.path, 'histedit-state'))
498 return
505 return
499 else:
506 else:
500 cmdutil.bailifchanged(repo)
507 cmdutil.bailifchanged(repo)
501 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
508 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
502 raise util.Abort(_('history edit already in progress, try '
509 raise util.Abort(_('history edit already in progress, try '
503 '--continue or --abort'))
510 '--continue or --abort'))
504
511
505 tip, empty = repo.dirstate.parents()
512 tip, empty = repo.dirstate.parents()
506
513
507
514
508 if len(parent) != 1:
515 if len(parent) != 1:
509 raise util.Abort(_('histedit requires exactly one parent revision'))
516 raise util.Abort(_('histedit requires exactly one parent revision'))
510 parent = scmutil.revsingle(repo, parent[0]).node()
517 parent = scmutil.revsingle(repo, parent[0]).node()
511
518
512 keep = opts.get('keep', False)
519 keep = opts.get('keep', False)
513 revs = between(repo, parent, tip, keep)
520 revs = between(repo, parent, tip, keep)
514
521
515 ctxs = [repo[r] for r in revs]
522 ctxs = [repo[r] for r in revs]
516 existing = [r.node() for r in ctxs]
523 existing = [r.node() for r in ctxs]
517 rules = opts.get('commands', '')
524 rules = opts.get('commands', '')
518 if not rules:
525 if not rules:
519 rules = '\n'.join([makedesc(c) for c in ctxs])
526 rules = '\n'.join([makedesc(c) for c in ctxs])
520 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
527 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
521 rules = ui.edit(rules, ui.username())
528 rules = ui.edit(rules, ui.username())
522 # Save edit rules in .hg/histedit-last-edit.txt in case
529 # Save edit rules in .hg/histedit-last-edit.txt in case
523 # the user needs to ask for help after something
530 # the user needs to ask for help after something
524 # surprising happens.
531 # surprising happens.
525 f = open(repo.join('histedit-last-edit.txt'), 'w')
532 f = open(repo.join('histedit-last-edit.txt'), 'w')
526 f.write(rules)
533 f.write(rules)
527 f.close()
534 f.close()
528 else:
535 else:
529 f = open(rules)
536 f = open(rules)
530 rules = f.read()
537 rules = f.read()
531 f.close()
538 f.close()
532 rules = [l for l in (r.strip() for r in rules.splitlines())
539 rules = [l for l in (r.strip() for r in rules.splitlines())
533 if l and not l[0] == '#']
540 if l and not l[0] == '#']
534 rules = verifyrules(rules, repo, ctxs)
541 rules = verifyrules(rules, repo, ctxs)
535
542
536 parentctx = repo[parent].parents()[0]
543 parentctx = repo[parent].parents()[0]
537 keep = opts.get('keep', False)
544 keep = opts.get('keep', False)
538 replaced = []
545 replaced = []
539 replacemap = {}
546 replacemap = {}
540 tmpnodes = []
547 tmpnodes = []
541 created = []
548 created = []
542
549
543
550
544 while rules:
551 while rules:
545 writestate(repo, parentctx.node(), created, replaced,
552 writestate(repo, parentctx.node(), created, replaced,
546 tmpnodes, existing, rules, keep, tip, replacemap)
553 tmpnodes, existing, rules, keep, tip, replacemap)
547 action, ha = rules.pop(0)
554 action, ha = rules.pop(0)
548 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
555 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
549 ui, repo, parentctx, ha, opts)
556 ui, repo, parentctx, ha, opts)
550
557
551 if replaced_:
558 if replaced_:
552 clen, rlen = len(created_), len(replaced_)
559 clen, rlen = len(created_), len(replaced_)
553 if clen == rlen == 1:
560 if clen == rlen == 1:
554 ui.debug('histedit: exact replacement of %s with %s\n' % (
561 ui.debug('histedit: exact replacement of %s with %s\n' % (
555 node.short(replaced_[0]), node.short(created_[0])))
562 node.short(replaced_[0]), node.short(created_[0])))
556
563
557 replacemap[replaced_[0]] = created_[0]
564 replacemap[replaced_[0]] = created_[0]
558 elif clen > rlen:
565 elif clen > rlen:
559 assert rlen == 1, ('unexpected replacement of '
566 assert rlen == 1, ('unexpected replacement of '
560 '%d changes with %d changes' % (rlen, clen))
567 '%d changes with %d changes' % (rlen, clen))
561 # made more changesets than we're replacing
568 # made more changesets than we're replacing
562 # TODO synthesize patch names for created patches
569 # TODO synthesize patch names for created patches
563 replacemap[replaced_[0]] = created_[-1]
570 replacemap[replaced_[0]] = created_[-1]
564 ui.debug('histedit: created many, assuming %s replaced by %s' %
571 ui.debug('histedit: created many, assuming %s replaced by %s' %
565 (node.short(replaced_[0]), node.short(created_[-1])))
572 (node.short(replaced_[0]), node.short(created_[-1])))
566 elif rlen > clen:
573 elif rlen > clen:
567 if not created_:
574 if not created_:
568 # This must be a drop. Try and put our metadata on
575 # This must be a drop. Try and put our metadata on
569 # the parent change.
576 # the parent change.
570 assert rlen == 1
577 assert rlen == 1
571 r = replaced_[0]
578 r = replaced_[0]
572 ui.debug('histedit: %s seems replaced with nothing, '
579 ui.debug('histedit: %s seems replaced with nothing, '
573 'finding a parent\n' % (node.short(r)))
580 'finding a parent\n' % (node.short(r)))
574 pctx = repo[r].parents()[0]
581 pctx = repo[r].parents()[0]
575 if pctx.node() in replacemap:
582 if pctx.node() in replacemap:
576 ui.debug('histedit: parent is already replaced\n')
583 ui.debug('histedit: parent is already replaced\n')
577 replacemap[r] = replacemap[pctx.node()]
584 replacemap[r] = replacemap[pctx.node()]
578 else:
585 else:
579 replacemap[r] = pctx.node()
586 replacemap[r] = pctx.node()
580 ui.debug('histedit: %s best replaced by %s\n' % (
587 ui.debug('histedit: %s best replaced by %s\n' % (
581 node.short(r), node.short(replacemap[r])))
588 node.short(r), node.short(replacemap[r])))
582 else:
589 else:
583 assert len(created_) == 1
590 assert len(created_) == 1
584 for r in replaced_:
591 for r in replaced_:
585 ui.debug('histedit: %s replaced by %s\n' % (
592 ui.debug('histedit: %s replaced by %s\n' % (
586 node.short(r), node.short(created_[0])))
593 node.short(r), node.short(created_[0])))
587 replacemap[r] = created_[0]
594 replacemap[r] = created_[0]
588 else:
595 else:
589 assert False, (
596 assert False, (
590 'Unhandled case in replacement mapping! '
597 'Unhandled case in replacement mapping! '
591 'replacing %d changes with %d changes' % (rlen, clen))
598 'replacing %d changes with %d changes' % (rlen, clen))
592 created.extend(created_)
599 created.extend(created_)
593 replaced.extend(replaced_)
600 replaced.extend(replaced_)
594 tmpnodes.extend(tmpnodes_)
601 tmpnodes.extend(tmpnodes_)
595
602
596 hg.update(repo, parentctx.node())
603 hg.update(repo, parentctx.node())
597
604
598 if not keep:
605 if not keep:
599 if replacemap:
606 if replacemap:
600 ui.note(_('histedit: Should update metadata for the following '
607 ui.note(_('histedit: Should update metadata for the following '
601 'changes:\n'))
608 'changes:\n'))
602
609
603 def copybms(old, new):
610 def copybms(old, new):
604 if old in tmpnodes or old in created:
611 if old in tmpnodes or old in created:
605 # can't have any metadata we'd want to update
612 # can't have any metadata we'd want to update
606 return
613 return
607 while new in replacemap:
614 while new in replacemap:
608 new = replacemap[new]
615 new = replacemap[new]
609 ui.note(_('histedit: %s to %s\n') % (node.short(old),
616 ui.note(_('histedit: %s to %s\n') % (node.short(old),
610 node.short(new)))
617 node.short(new)))
611 octx = repo[old]
618 octx = repo[old]
612 marks = octx.bookmarks()
619 marks = octx.bookmarks()
613 if marks:
620 if marks:
614 ui.note(_('histedit: moving bookmarks %s\n') %
621 ui.note(_('histedit: moving bookmarks %s\n') %
615 ', '.join(marks))
622 ', '.join(marks))
616 for mark in marks:
623 for mark in marks:
617 repo._bookmarks[mark] = new
624 repo._bookmarks[mark] = new
618 bookmarks.write(repo)
625 bookmarks.write(repo)
619
626
620 # We assume that bookmarks on the tip should remain
627 # We assume that bookmarks on the tip should remain
621 # tipmost, but bookmarks on non-tip changesets should go
628 # tipmost, but bookmarks on non-tip changesets should go
622 # to their most reasonable successor. As a result, find
629 # to their most reasonable successor. As a result, find
623 # the old tip and new tip and copy those bookmarks first,
630 # the old tip and new tip and copy those bookmarks first,
624 # then do the rest of the bookmark copies.
631 # then do the rest of the bookmark copies.
625 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
632 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
626 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
633 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
627 copybms(oldtip, newtip)
634 copybms(oldtip, newtip)
628
635
629 for old, new in sorted(replacemap.iteritems()):
636 for old, new in sorted(replacemap.iteritems()):
630 copybms(old, new)
637 copybms(old, new)
631 # TODO update mq state
638 # TODO update mq state
632
639
633 ui.debug('should strip replaced nodes %s\n' %
640 ui.debug('should strip replaced nodes %s\n' %
634 ', '.join([node.hex(n)[:12] for n in replaced]))
641 ', '.join([node.hex(n)[:12] for n in replaced]))
635 for n in sorted(replaced, key=lambda x: repo[x].rev()):
642 for n in sorted(replaced, key=lambda x: repo[x].rev()):
636 try:
643 try:
637 repair.strip(ui, repo, n)
644 repair.strip(ui, repo, n)
638 except error.LookupError:
645 except error.LookupError:
639 pass
646 pass
640
647
641 ui.debug('should strip temp nodes %s\n' %
648 ui.debug('should strip temp nodes %s\n' %
642 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
649 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
643 for n in reversed(tmpnodes):
650 for n in reversed(tmpnodes):
644 try:
651 try:
645 repair.strip(ui, repo, n)
652 repair.strip(ui, repo, n)
646 except error.LookupError:
653 except error.LookupError:
647 pass
654 pass
648 os.unlink(os.path.join(repo.path, 'histedit-state'))
655 os.unlink(os.path.join(repo.path, 'histedit-state'))
649 if os.path.exists(repo.sjoin('undo')):
656 if os.path.exists(repo.sjoin('undo')):
650 os.unlink(repo.sjoin('undo'))
657 os.unlink(repo.sjoin('undo'))
651
658
652
659
653 def writestate(repo, parentctxnode, created, replaced,
660 def writestate(repo, parentctxnode, created, replaced,
654 tmpnodes, existing, rules, keep, oldtip, replacemap):
661 tmpnodes, existing, rules, keep, oldtip, replacemap):
655 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
662 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
656 pickle.dump((parentctxnode, created, replaced,
663 pickle.dump((parentctxnode, created, replaced,
657 tmpnodes, existing, rules, keep, oldtip, replacemap),
664 tmpnodes, existing, rules, keep, oldtip, replacemap),
658 fp)
665 fp)
659 fp.close()
666 fp.close()
660
667
661 def readstate(repo):
668 def readstate(repo):
662 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
669 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
663 keep, oldtip, replacemap ).
670 keep, oldtip, replacemap ).
664 """
671 """
665 fp = open(os.path.join(repo.path, 'histedit-state'))
672 fp = open(os.path.join(repo.path, 'histedit-state'))
666 return pickle.load(fp)
673 return pickle.load(fp)
667
674
668
675
669 def verifyrules(rules, repo, ctxs):
676 def verifyrules(rules, repo, ctxs):
670 """Verify that there exists exactly one edit rule per given changeset.
677 """Verify that there exists exactly one edit rule per given changeset.
671
678
672 Will abort if there are to many or too few rules, a malformed rule,
679 Will abort if there are to many or too few rules, a malformed rule,
673 or a rule on a changeset outside of the user-given range.
680 or a rule on a changeset outside of the user-given range.
674 """
681 """
675 parsed = []
682 parsed = []
676 if len(rules) != len(ctxs):
683 if len(rules) != len(ctxs):
677 raise util.Abort(_('must specify a rule for each changeset once'))
684 raise util.Abort(_('must specify a rule for each changeset once'))
678 for r in rules:
685 for r in rules:
679 if ' ' not in r:
686 if ' ' not in r:
680 raise util.Abort(_('malformed line "%s"') % r)
687 raise util.Abort(_('malformed line "%s"') % r)
681 action, rest = r.split(' ', 1)
688 action, rest = r.split(' ', 1)
682 if ' ' in rest.strip():
689 if ' ' in rest.strip():
683 ha, rest = rest.split(' ', 1)
690 ha, rest = rest.split(' ', 1)
684 else:
691 else:
685 ha = r.strip()
692 ha = r.strip()
686 try:
693 try:
687 if repo[ha] not in ctxs:
694 if repo[ha] not in ctxs:
688 raise util.Abort(
695 raise util.Abort(
689 _('may not use changesets other than the ones listed'))
696 _('may not use changesets other than the ones listed'))
690 except error.RepoError:
697 except error.RepoError:
691 raise util.Abort(_('unknown changeset %s listed') % ha)
698 raise util.Abort(_('unknown changeset %s listed') % ha)
692 if action not in actiontable:
699 if action not in actiontable:
693 raise util.Abort(_('unknown action "%s"') % action)
700 raise util.Abort(_('unknown action "%s"') % action)
694 parsed.append([action, ha])
701 parsed.append([action, ha])
695 return parsed
702 return parsed
@@ -1,185 +1,238 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > graphlog=
5 > graphlog=
6 > histedit=
6 > histedit=
7 > EOF
7 > EOF
8
8
9 $ EDITED="$TESTTMP/editedhistory"
9 $ EDITED="$TESTTMP/editedhistory"
10 $ cat > $EDITED <<EOF
10 $ cat > $EDITED <<EOF
11 > pick e860deea161a e
11 > pick e860deea161a e
12 > pick 652413bf663e f
12 > pick 652413bf663e f
13 > fold 177f92b77385 c
13 > fold 177f92b77385 c
14 > pick 055a42cdd887 d
14 > pick 055a42cdd887 d
15 > EOF
15 > EOF
16 $ initrepo ()
16 $ initrepo ()
17 > {
17 > {
18 > hg init r
18 > hg init r
19 > cd r
19 > cd r
20 > for x in a b c d e f ; do
20 > for x in a b c d e f ; do
21 > echo $x > $x
21 > echo $x > $x
22 > hg add $x
22 > hg add $x
23 > hg ci -m $x
23 > hg ci -m $x
24 > done
24 > done
25 > }
25 > }
26
26
27 $ initrepo
27 $ initrepo
28
28
29 log before edit
29 log before edit
30 $ hg log --graph
30 $ hg log --graph
31 @ changeset: 5:652413bf663e
31 @ changeset: 5:652413bf663e
32 | tag: tip
32 | tag: tip
33 | user: test
33 | user: test
34 | date: Thu Jan 01 00:00:00 1970 +0000
34 | date: Thu Jan 01 00:00:00 1970 +0000
35 | summary: f
35 | summary: f
36 |
36 |
37 o changeset: 4:e860deea161a
37 o changeset: 4:e860deea161a
38 | user: test
38 | user: test
39 | date: Thu Jan 01 00:00:00 1970 +0000
39 | date: Thu Jan 01 00:00:00 1970 +0000
40 | summary: e
40 | summary: e
41 |
41 |
42 o changeset: 3:055a42cdd887
42 o changeset: 3:055a42cdd887
43 | user: test
43 | user: test
44 | date: Thu Jan 01 00:00:00 1970 +0000
44 | date: Thu Jan 01 00:00:00 1970 +0000
45 | summary: d
45 | summary: d
46 |
46 |
47 o changeset: 2:177f92b77385
47 o changeset: 2:177f92b77385
48 | user: test
48 | user: test
49 | date: Thu Jan 01 00:00:00 1970 +0000
49 | date: Thu Jan 01 00:00:00 1970 +0000
50 | summary: c
50 | summary: c
51 |
51 |
52 o changeset: 1:d2ae7f538514
52 o changeset: 1:d2ae7f538514
53 | user: test
53 | user: test
54 | date: Thu Jan 01 00:00:00 1970 +0000
54 | date: Thu Jan 01 00:00:00 1970 +0000
55 | summary: b
55 | summary: b
56 |
56 |
57 o changeset: 0:cb9a9f314b8b
57 o changeset: 0:cb9a9f314b8b
58 user: test
58 user: test
59 date: Thu Jan 01 00:00:00 1970 +0000
59 date: Thu Jan 01 00:00:00 1970 +0000
60 summary: a
60 summary: a
61
61
62
62
63 edit the history
63 edit the history
64 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
64 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
65 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
65 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
66 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
66 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
68 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
69 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
69 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
70 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
70 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
71
71
72 log after edit
72 log after edit
73 $ hg log --graph
73 $ hg log --graph
74 @ changeset: 4:82b0c1ff1777
74 @ changeset: 4:82b0c1ff1777
75 | tag: tip
75 | tag: tip
76 | user: test
76 | user: test
77 | date: Thu Jan 01 00:00:00 1970 +0000
77 | date: Thu Jan 01 00:00:00 1970 +0000
78 | summary: d
78 | summary: d
79 |
79 |
80 o changeset: 3:150aafb44a91
80 o changeset: 3:150aafb44a91
81 | user: test
81 | user: test
82 | date: Thu Jan 01 00:00:00 1970 +0000
82 | date: Thu Jan 01 00:00:00 1970 +0000
83 | summary: pick e860deea161a e
83 | summary: pick e860deea161a e
84 |
84 |
85 o changeset: 2:493dc0964412
85 o changeset: 2:493dc0964412
86 | user: test
86 | user: test
87 | date: Thu Jan 01 00:00:00 1970 +0000
87 | date: Thu Jan 01 00:00:00 1970 +0000
88 | summary: e
88 | summary: e
89 |
89 |
90 o changeset: 1:d2ae7f538514
90 o changeset: 1:d2ae7f538514
91 | user: test
91 | user: test
92 | date: Thu Jan 01 00:00:00 1970 +0000
92 | date: Thu Jan 01 00:00:00 1970 +0000
93 | summary: b
93 | summary: b
94 |
94 |
95 o changeset: 0:cb9a9f314b8b
95 o changeset: 0:cb9a9f314b8b
96 user: test
96 user: test
97 date: Thu Jan 01 00:00:00 1970 +0000
97 date: Thu Jan 01 00:00:00 1970 +0000
98 summary: a
98 summary: a
99
99
100
100
101 post-fold manifest
101 post-fold manifest
102 $ hg manifest
102 $ hg manifest
103 a
103 a
104 b
104 b
105 c
105 c
106 d
106 d
107 e
107 e
108 f
108 f
109
109
110 $ cd ..
110 $ cd ..
111
111
112 folding and creating no new change doesn't break:
112 folding and creating no new change doesn't break:
113 $ mkdir fold-to-empty-test
113 $ mkdir fold-to-empty-test
114 $ cd fold-to-empty-test
114 $ cd fold-to-empty-test
115 $ hg init
115 $ hg init
116 $ printf "1\n2\n3\n" > file
116 $ printf "1\n2\n3\n" > file
117 $ hg add file
117 $ hg add file
118 $ hg commit -m '1+2+3'
118 $ hg commit -m '1+2+3'
119 $ echo 4 >> file
119 $ echo 4 >> file
120 $ hg commit -m '+4'
120 $ hg commit -m '+4'
121 $ echo 5 >> file
121 $ echo 5 >> file
122 $ hg commit -m '+5'
122 $ hg commit -m '+5'
123 $ echo 6 >> file
123 $ echo 6 >> file
124 $ hg commit -m '+6'
124 $ hg commit -m '+6'
125 $ hg log --graph
125 $ hg log --graph
126 @ changeset: 3:251d831eeec5
126 @ changeset: 3:251d831eeec5
127 | tag: tip
127 | tag: tip
128 | user: test
128 | user: test
129 | date: Thu Jan 01 00:00:00 1970 +0000
129 | date: Thu Jan 01 00:00:00 1970 +0000
130 | summary: +6
130 | summary: +6
131 |
131 |
132 o changeset: 2:888f9082bf99
132 o changeset: 2:888f9082bf99
133 | user: test
133 | user: test
134 | date: Thu Jan 01 00:00:00 1970 +0000
134 | date: Thu Jan 01 00:00:00 1970 +0000
135 | summary: +5
135 | summary: +5
136 |
136 |
137 o changeset: 1:617f94f13c0f
137 o changeset: 1:617f94f13c0f
138 | user: test
138 | user: test
139 | date: Thu Jan 01 00:00:00 1970 +0000
139 | date: Thu Jan 01 00:00:00 1970 +0000
140 | summary: +4
140 | summary: +4
141 |
141 |
142 o changeset: 0:0189ba417d34
142 o changeset: 0:0189ba417d34
143 user: test
143 user: test
144 date: Thu Jan 01 00:00:00 1970 +0000
144 date: Thu Jan 01 00:00:00 1970 +0000
145 summary: 1+2+3
145 summary: 1+2+3
146
146
147
147
148 $ cat > editor.py <<EOF
148 $ cat > editor.py <<EOF
149 > import re, sys
149 > import re, sys
150 > rules = sys.argv[1]
150 > rules = sys.argv[1]
151 > data = open(rules).read()
151 > data = open(rules).read()
152 > data = re.sub(r'pick ([0-9a-f]{12} 2 \+5)', r'drop \1', data)
152 > data = re.sub(r'pick ([0-9a-f]{12} 2 \+5)', r'drop \1', data)
153 > data = re.sub(r'pick ([0-9a-f]{12} 2 \+6)', r'fold \1', data)
153 > data = re.sub(r'pick ([0-9a-f]{12} 2 \+6)', r'fold \1', data)
154 > open(rules, 'w').write(data)
154 > open(rules, 'w').write(data)
155 > EOF
155 > EOF
156
156
157 $ HGEDITOR='python editor.py' hg histedit 1
157 $ HGEDITOR='python editor.py' hg histedit 1
158 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
158 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
159 patching file file
159 patching file file
160 Hunk #1 FAILED at 2
160 Hunk #1 FAILED at 2
161 1 out of 1 hunks FAILED -- saving rejects to file file.rej
161 1 out of 1 hunks FAILED -- saving rejects to file file.rej
162 abort: Fix up the change and run hg histedit --continue
162 abort: Fix up the change and run hg histedit --continue
163 [255]
163 [255]
164 There were conflicts, but we'll continue without resolving. This
164 There were conflicts, but we'll continue without resolving. This
165 should effectively drop the changes from +6.
165 should effectively drop the changes from +6.
166 $ hg status
166 $ hg status
167 ? editor.py
167 ? editor.py
168 ? file.rej
168 ? file.rej
169 $ hg histedit --continue
169 $ hg histedit --continue
170 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
170 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
171 saved backup bundle to $TESTTMP/*-backup.hg (glob)
171 saved backup bundle to $TESTTMP/*-backup.hg (glob)
172 $ hg log --graph
172 $ hg log --graph
173 @ changeset: 1:617f94f13c0f
173 @ changeset: 1:617f94f13c0f
174 | tag: tip
174 | tag: tip
175 | user: test
175 | user: test
176 | date: Thu Jan 01 00:00:00 1970 +0000
176 | date: Thu Jan 01 00:00:00 1970 +0000
177 | summary: +4
177 | summary: +4
178 |
178 |
179 o changeset: 0:0189ba417d34
179 o changeset: 0:0189ba417d34
180 user: test
180 user: test
181 date: Thu Jan 01 00:00:00 1970 +0000
181 date: Thu Jan 01 00:00:00 1970 +0000
182 summary: 1+2+3
182 summary: 1+2+3
183
183
184
184
185 $ cd ..
185 $ cd ..
186
187 Test corner case where folded revision is separated from its parent by a
188 dropped revision.
189
190
191 $ hg init fold-with-dropped
192 $ cd fold-with-dropped
193 $ printf "1\n2\n3\n" > file
194 $ hg commit -Am '1+2+3'
195 adding file
196 $ echo 4 >> file
197 $ hg commit -m '+4'
198 $ echo 5 >> file
199 $ hg commit -m '+5'
200 $ echo 6 >> file
201 $ hg commit -m '+6'
202 $ hg log -G --template '{rev}:{node|short} {desc|firstline}\n'
203 @ 3:251d831eeec5 +6
204 |
205 o 2:888f9082bf99 +5
206 |
207 o 1:617f94f13c0f +4
208 |
209 o 0:0189ba417d34 1+2+3
210
211 $ EDITED=`pwd`/../editcommands
212 $ cat > $EDITED <<EOF
213 > pick 617f94f13c0f 1 +4
214 > drop 888f9082bf99 2 +5
215 > fold 251d831eeec5 3 +6
216 > EOF
217 $ HGEDITOR="cat $EDITED >" hg histedit 1
218 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
219 patching file file
220 Hunk #1 FAILED at 2
221 1 out of 1 hunks FAILED -- saving rejects to file file.rej
222 abort: Fix up the change and run hg histedit --continue
223 [255]
224 $ echo 5 >> file
225 $ hg commit -m '+5.2'
226 created new head
227 $ echo 6 >> file
228 $ HGEDITOR=cat hg histedit --continue
229 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
230 +4
231 ***
232 +5.2
233 ***
234 +6
235 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
236 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-backup.hg (glob)
237 $ cd ..
238
General Comments 0
You need to be logged in to leave comments. Login now