##// END OF EJS Templates
histedit: end folding message with an LF...
Patrick Mezard -
r17241:c2f13180 stable
parent child Browse files
Show More
@@ -1,695 +1,695 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()])
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 # discover any nodes the user has added in the interim
434 newchildren = [c for c in parentctx.children()
434 newchildren = [c for c in parentctx.children()
435 if c.node() not in existing]
435 if c.node() not in existing]
436 action, currentnode = rules.pop(0)
436 action, currentnode = rules.pop(0)
437 while newchildren:
437 while newchildren:
438 if action in ('f', 'fold'):
438 if action in ('f', 'fold'):
439 tmpnodes.extend([n.node() for n in newchildren])
439 tmpnodes.extend([n.node() for n in newchildren])
440 else:
440 else:
441 created.extend([n.node() for n in newchildren])
441 created.extend([n.node() for n in newchildren])
442 filtered = []
442 filtered = []
443 for r in newchildren:
443 for r in newchildren:
444 filtered += [c for c in r.children() if c.node not in existing]
444 filtered += [c for c in r.children() if c.node not in existing]
445 newchildren = filtered
445 newchildren = filtered
446 m, a, r, d = repo.status()[:4]
446 m, a, r, d = repo.status()[:4]
447 oldctx = repo[currentnode]
447 oldctx = repo[currentnode]
448 message = oldctx.description()
448 message = oldctx.description()
449 if action in ('e', 'edit', 'm', 'mess'):
449 if action in ('e', 'edit', 'm', 'mess'):
450 message = ui.edit(message, ui.username())
450 message = ui.edit(message, ui.username())
451 elif action in ('f', 'fold'):
451 elif action in ('f', 'fold'):
452 message = 'fold-temp-revision %s' % currentnode
452 message = 'fold-temp-revision %s' % currentnode
453 new = None
453 new = None
454 if m or a or r or d:
454 if m or a or r or d:
455 new = repo.commit(text=message, user=oldctx.user(),
455 new = repo.commit(text=message, user=oldctx.user(),
456 date=oldctx.date(), extra=oldctx.extra())
456 date=oldctx.date(), extra=oldctx.extra())
457
457
458 # If we're resuming a fold and we have new changes, mark the
458 # 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
459 # replacements and finish the fold. If not, it's more like a
460 # drop of the changesets that disappeared, and we can skip
460 # drop of the changesets that disappeared, and we can skip
461 # this step.
461 # this step.
462 if action in ('f', 'fold') and (new or newchildren):
462 if action in ('f', 'fold') and (new or newchildren):
463 if new:
463 if new:
464 tmpnodes.append(new)
464 tmpnodes.append(new)
465 else:
465 else:
466 new = newchildren[-1]
466 new = newchildren[-1]
467 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
467 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
468 ui, repo, parentctx, oldctx, new, opts, newchildren)
468 ui, repo, parentctx, oldctx, new, opts, newchildren)
469 replaced.extend(replaced_)
469 replaced.extend(replaced_)
470 created.extend(created_)
470 created.extend(created_)
471 tmpnodes.extend(tmpnodes_)
471 tmpnodes.extend(tmpnodes_)
472 elif action not in ('d', 'drop'):
472 elif action not in ('d', 'drop'):
473 if new != oldctx.node():
473 if new != oldctx.node():
474 replaced.append(oldctx.node())
474 replaced.append(oldctx.node())
475 if new:
475 if new:
476 if new != oldctx.node():
476 if new != oldctx.node():
477 created.append(new)
477 created.append(new)
478 parentctx = repo[new]
478 parentctx = repo[new]
479
479
480 elif opts.get('abort', False):
480 elif opts.get('abort', False):
481 if len(parent) != 0:
481 if len(parent) != 0:
482 raise util.Abort(_('no arguments allowed with --abort'))
482 raise util.Abort(_('no arguments allowed with --abort'))
483 (parentctxnode, created, replaced, tmpnodes,
483 (parentctxnode, created, replaced, tmpnodes,
484 existing, rules, keep, tip, replacemap) = readstate(repo)
484 existing, rules, keep, tip, replacemap) = readstate(repo)
485 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
485 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
486 hg.clean(repo, tip)
486 hg.clean(repo, tip)
487 ui.debug('should strip created nodes %s\n' %
487 ui.debug('should strip created nodes %s\n' %
488 ', '.join([node.hex(n)[:12] for n in created]))
488 ', '.join([node.hex(n)[:12] for n in created]))
489 ui.debug('should strip temp nodes %s\n' %
489 ui.debug('should strip temp nodes %s\n' %
490 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
490 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
491 for nodes in (created, tmpnodes):
491 for nodes in (created, tmpnodes):
492 for n in reversed(nodes):
492 for n in reversed(nodes):
493 try:
493 try:
494 repair.strip(ui, repo, n)
494 repair.strip(ui, repo, n)
495 except error.LookupError:
495 except error.LookupError:
496 pass
496 pass
497 os.unlink(os.path.join(repo.path, 'histedit-state'))
497 os.unlink(os.path.join(repo.path, 'histedit-state'))
498 return
498 return
499 else:
499 else:
500 cmdutil.bailifchanged(repo)
500 cmdutil.bailifchanged(repo)
501 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
501 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
502 raise util.Abort(_('history edit already in progress, try '
502 raise util.Abort(_('history edit already in progress, try '
503 '--continue or --abort'))
503 '--continue or --abort'))
504
504
505 tip, empty = repo.dirstate.parents()
505 tip, empty = repo.dirstate.parents()
506
506
507
507
508 if len(parent) != 1:
508 if len(parent) != 1:
509 raise util.Abort(_('histedit requires exactly one parent revision'))
509 raise util.Abort(_('histedit requires exactly one parent revision'))
510 parent = scmutil.revsingle(repo, parent[0]).node()
510 parent = scmutil.revsingle(repo, parent[0]).node()
511
511
512 keep = opts.get('keep', False)
512 keep = opts.get('keep', False)
513 revs = between(repo, parent, tip, keep)
513 revs = between(repo, parent, tip, keep)
514
514
515 ctxs = [repo[r] for r in revs]
515 ctxs = [repo[r] for r in revs]
516 existing = [r.node() for r in ctxs]
516 existing = [r.node() for r in ctxs]
517 rules = opts.get('commands', '')
517 rules = opts.get('commands', '')
518 if not rules:
518 if not rules:
519 rules = '\n'.join([makedesc(c) for c in ctxs])
519 rules = '\n'.join([makedesc(c) for c in ctxs])
520 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
520 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
521 rules = ui.edit(rules, ui.username())
521 rules = ui.edit(rules, ui.username())
522 # Save edit rules in .hg/histedit-last-edit.txt in case
522 # Save edit rules in .hg/histedit-last-edit.txt in case
523 # the user needs to ask for help after something
523 # the user needs to ask for help after something
524 # surprising happens.
524 # surprising happens.
525 f = open(repo.join('histedit-last-edit.txt'), 'w')
525 f = open(repo.join('histedit-last-edit.txt'), 'w')
526 f.write(rules)
526 f.write(rules)
527 f.close()
527 f.close()
528 else:
528 else:
529 f = open(rules)
529 f = open(rules)
530 rules = f.read()
530 rules = f.read()
531 f.close()
531 f.close()
532 rules = [l for l in (r.strip() for r in rules.splitlines())
532 rules = [l for l in (r.strip() for r in rules.splitlines())
533 if l and not l[0] == '#']
533 if l and not l[0] == '#']
534 rules = verifyrules(rules, repo, ctxs)
534 rules = verifyrules(rules, repo, ctxs)
535
535
536 parentctx = repo[parent].parents()[0]
536 parentctx = repo[parent].parents()[0]
537 keep = opts.get('keep', False)
537 keep = opts.get('keep', False)
538 replaced = []
538 replaced = []
539 replacemap = {}
539 replacemap = {}
540 tmpnodes = []
540 tmpnodes = []
541 created = []
541 created = []
542
542
543
543
544 while rules:
544 while rules:
545 writestate(repo, parentctx.node(), created, replaced,
545 writestate(repo, parentctx.node(), created, replaced,
546 tmpnodes, existing, rules, keep, tip, replacemap)
546 tmpnodes, existing, rules, keep, tip, replacemap)
547 action, ha = rules.pop(0)
547 action, ha = rules.pop(0)
548 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
548 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
549 ui, repo, parentctx, ha, opts)
549 ui, repo, parentctx, ha, opts)
550
550
551 if replaced_:
551 if replaced_:
552 clen, rlen = len(created_), len(replaced_)
552 clen, rlen = len(created_), len(replaced_)
553 if clen == rlen == 1:
553 if clen == rlen == 1:
554 ui.debug('histedit: exact replacement of %s with %s\n' % (
554 ui.debug('histedit: exact replacement of %s with %s\n' % (
555 node.short(replaced_[0]), node.short(created_[0])))
555 node.short(replaced_[0]), node.short(created_[0])))
556
556
557 replacemap[replaced_[0]] = created_[0]
557 replacemap[replaced_[0]] = created_[0]
558 elif clen > rlen:
558 elif clen > rlen:
559 assert rlen == 1, ('unexpected replacement of '
559 assert rlen == 1, ('unexpected replacement of '
560 '%d changes with %d changes' % (rlen, clen))
560 '%d changes with %d changes' % (rlen, clen))
561 # made more changesets than we're replacing
561 # made more changesets than we're replacing
562 # TODO synthesize patch names for created patches
562 # TODO synthesize patch names for created patches
563 replacemap[replaced_[0]] = created_[-1]
563 replacemap[replaced_[0]] = created_[-1]
564 ui.debug('histedit: created many, assuming %s replaced by %s' %
564 ui.debug('histedit: created many, assuming %s replaced by %s' %
565 (node.short(replaced_[0]), node.short(created_[-1])))
565 (node.short(replaced_[0]), node.short(created_[-1])))
566 elif rlen > clen:
566 elif rlen > clen:
567 if not created_:
567 if not created_:
568 # This must be a drop. Try and put our metadata on
568 # This must be a drop. Try and put our metadata on
569 # the parent change.
569 # the parent change.
570 assert rlen == 1
570 assert rlen == 1
571 r = replaced_[0]
571 r = replaced_[0]
572 ui.debug('histedit: %s seems replaced with nothing, '
572 ui.debug('histedit: %s seems replaced with nothing, '
573 'finding a parent\n' % (node.short(r)))
573 'finding a parent\n' % (node.short(r)))
574 pctx = repo[r].parents()[0]
574 pctx = repo[r].parents()[0]
575 if pctx.node() in replacemap:
575 if pctx.node() in replacemap:
576 ui.debug('histedit: parent is already replaced\n')
576 ui.debug('histedit: parent is already replaced\n')
577 replacemap[r] = replacemap[pctx.node()]
577 replacemap[r] = replacemap[pctx.node()]
578 else:
578 else:
579 replacemap[r] = pctx.node()
579 replacemap[r] = pctx.node()
580 ui.debug('histedit: %s best replaced by %s\n' % (
580 ui.debug('histedit: %s best replaced by %s\n' % (
581 node.short(r), node.short(replacemap[r])))
581 node.short(r), node.short(replacemap[r])))
582 else:
582 else:
583 assert len(created_) == 1
583 assert len(created_) == 1
584 for r in replaced_:
584 for r in replaced_:
585 ui.debug('histedit: %s replaced by %s\n' % (
585 ui.debug('histedit: %s replaced by %s\n' % (
586 node.short(r), node.short(created_[0])))
586 node.short(r), node.short(created_[0])))
587 replacemap[r] = created_[0]
587 replacemap[r] = created_[0]
588 else:
588 else:
589 assert False, (
589 assert False, (
590 'Unhandled case in replacement mapping! '
590 'Unhandled case in replacement mapping! '
591 'replacing %d changes with %d changes' % (rlen, clen))
591 'replacing %d changes with %d changes' % (rlen, clen))
592 created.extend(created_)
592 created.extend(created_)
593 replaced.extend(replaced_)
593 replaced.extend(replaced_)
594 tmpnodes.extend(tmpnodes_)
594 tmpnodes.extend(tmpnodes_)
595
595
596 hg.update(repo, parentctx.node())
596 hg.update(repo, parentctx.node())
597
597
598 if not keep:
598 if not keep:
599 if replacemap:
599 if replacemap:
600 ui.note(_('histedit: Should update metadata for the following '
600 ui.note(_('histedit: Should update metadata for the following '
601 'changes:\n'))
601 'changes:\n'))
602
602
603 def copybms(old, new):
603 def copybms(old, new):
604 if old in tmpnodes or old in created:
604 if old in tmpnodes or old in created:
605 # can't have any metadata we'd want to update
605 # can't have any metadata we'd want to update
606 return
606 return
607 while new in replacemap:
607 while new in replacemap:
608 new = replacemap[new]
608 new = replacemap[new]
609 ui.note(_('histedit: %s to %s\n') % (node.short(old),
609 ui.note(_('histedit: %s to %s\n') % (node.short(old),
610 node.short(new)))
610 node.short(new)))
611 octx = repo[old]
611 octx = repo[old]
612 marks = octx.bookmarks()
612 marks = octx.bookmarks()
613 if marks:
613 if marks:
614 ui.note(_('histedit: moving bookmarks %s\n') %
614 ui.note(_('histedit: moving bookmarks %s\n') %
615 ', '.join(marks))
615 ', '.join(marks))
616 for mark in marks:
616 for mark in marks:
617 repo._bookmarks[mark] = new
617 repo._bookmarks[mark] = new
618 bookmarks.write(repo)
618 bookmarks.write(repo)
619
619
620 # We assume that bookmarks on the tip should remain
620 # We assume that bookmarks on the tip should remain
621 # tipmost, but bookmarks on non-tip changesets should go
621 # tipmost, but bookmarks on non-tip changesets should go
622 # to their most reasonable successor. As a result, find
622 # to their most reasonable successor. As a result, find
623 # the old tip and new tip and copy those bookmarks first,
623 # the old tip and new tip and copy those bookmarks first,
624 # then do the rest of the bookmark copies.
624 # then do the rest of the bookmark copies.
625 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
625 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
626 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
626 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
627 copybms(oldtip, newtip)
627 copybms(oldtip, newtip)
628
628
629 for old, new in sorted(replacemap.iteritems()):
629 for old, new in sorted(replacemap.iteritems()):
630 copybms(old, new)
630 copybms(old, new)
631 # TODO update mq state
631 # TODO update mq state
632
632
633 ui.debug('should strip replaced nodes %s\n' %
633 ui.debug('should strip replaced nodes %s\n' %
634 ', '.join([node.hex(n)[:12] for n in replaced]))
634 ', '.join([node.hex(n)[:12] for n in replaced]))
635 for n in sorted(replaced, key=lambda x: repo[x].rev()):
635 for n in sorted(replaced, key=lambda x: repo[x].rev()):
636 try:
636 try:
637 repair.strip(ui, repo, n)
637 repair.strip(ui, repo, n)
638 except error.LookupError:
638 except error.LookupError:
639 pass
639 pass
640
640
641 ui.debug('should strip temp nodes %s\n' %
641 ui.debug('should strip temp nodes %s\n' %
642 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
642 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
643 for n in reversed(tmpnodes):
643 for n in reversed(tmpnodes):
644 try:
644 try:
645 repair.strip(ui, repo, n)
645 repair.strip(ui, repo, n)
646 except error.LookupError:
646 except error.LookupError:
647 pass
647 pass
648 os.unlink(os.path.join(repo.path, 'histedit-state'))
648 os.unlink(os.path.join(repo.path, 'histedit-state'))
649 if os.path.exists(repo.sjoin('undo')):
649 if os.path.exists(repo.sjoin('undo')):
650 os.unlink(repo.sjoin('undo'))
650 os.unlink(repo.sjoin('undo'))
651
651
652
652
653 def writestate(repo, parentctxnode, created, replaced,
653 def writestate(repo, parentctxnode, created, replaced,
654 tmpnodes, existing, rules, keep, oldtip, replacemap):
654 tmpnodes, existing, rules, keep, oldtip, replacemap):
655 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
655 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
656 pickle.dump((parentctxnode, created, replaced,
656 pickle.dump((parentctxnode, created, replaced,
657 tmpnodes, existing, rules, keep, oldtip, replacemap),
657 tmpnodes, existing, rules, keep, oldtip, replacemap),
658 fp)
658 fp)
659 fp.close()
659 fp.close()
660
660
661 def readstate(repo):
661 def readstate(repo):
662 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
662 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
663 keep, oldtip, replacemap ).
663 keep, oldtip, replacemap ).
664 """
664 """
665 fp = open(os.path.join(repo.path, 'histedit-state'))
665 fp = open(os.path.join(repo.path, 'histedit-state'))
666 return pickle.load(fp)
666 return pickle.load(fp)
667
667
668
668
669 def verifyrules(rules, repo, ctxs):
669 def verifyrules(rules, repo, ctxs):
670 """Verify that there exists exactly one edit rule per given changeset.
670 """Verify that there exists exactly one edit rule per given changeset.
671
671
672 Will abort if there are to many or too few rules, a malformed rule,
672 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.
673 or a rule on a changeset outside of the user-given range.
674 """
674 """
675 parsed = []
675 parsed = []
676 if len(rules) != len(ctxs):
676 if len(rules) != len(ctxs):
677 raise util.Abort(_('must specify a rule for each changeset once'))
677 raise util.Abort(_('must specify a rule for each changeset once'))
678 for r in rules:
678 for r in rules:
679 if ' ' not in r:
679 if ' ' not in r:
680 raise util.Abort(_('malformed line "%s"') % r)
680 raise util.Abort(_('malformed line "%s"') % r)
681 action, rest = r.split(' ', 1)
681 action, rest = r.split(' ', 1)
682 if ' ' in rest.strip():
682 if ' ' in rest.strip():
683 ha, rest = rest.split(' ', 1)
683 ha, rest = rest.split(' ', 1)
684 else:
684 else:
685 ha = r.strip()
685 ha = r.strip()
686 try:
686 try:
687 if repo[ha] not in ctxs:
687 if repo[ha] not in ctxs:
688 raise util.Abort(
688 raise util.Abort(
689 _('may not use changesets other than the ones listed'))
689 _('may not use changesets other than the ones listed'))
690 except error.RepoError:
690 except error.RepoError:
691 raise util.Abort(_('unknown changeset %s listed') % ha)
691 raise util.Abort(_('unknown changeset %s listed') % ha)
692 if action not in actiontable:
692 if action not in actiontable:
693 raise util.Abort(_('unknown action "%s"') % action)
693 raise util.Abort(_('unknown action "%s"') % action)
694 parsed.append([action, ha])
694 parsed.append([action, ha])
695 return parsed
695 return parsed
@@ -1,146 +1,147 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 177f92b77385 c
11 > pick 177f92b77385 c
12 > pick 055a42cdd887 d
12 > pick 055a42cdd887 d
13 > fold bfa474341cc9 does not commute with e
13 > fold bfa474341cc9 does not commute with e
14 > pick e860deea161a e
14 > pick e860deea161a e
15 > pick 652413bf663e f
15 > pick 652413bf663e f
16 > EOF
16 > EOF
17 $ initrepo ()
17 $ initrepo ()
18 > {
18 > {
19 > hg init $1
19 > hg init $1
20 > cd $1
20 > cd $1
21 > for x in a b c d e f ; do
21 > for x in a b c d e f ; do
22 > echo $x > $x
22 > echo $x > $x
23 > hg add $x
23 > hg add $x
24 > hg ci -m $x
24 > hg ci -m $x
25 > done
25 > done
26 > echo a >> e
26 > echo a >> e
27 > hg ci -m 'does not commute with e'
27 > hg ci -m 'does not commute with e'
28 > cd ..
28 > cd ..
29 > }
29 > }
30
30
31 $ initrepo r
31 $ initrepo r
32 $ cd r
32 $ cd r
33
33
34 log before edit
34 log before edit
35 $ hg log --graph
35 $ hg log --graph
36 @ changeset: 6:bfa474341cc9
36 @ changeset: 6:bfa474341cc9
37 | tag: tip
37 | tag: tip
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: does not commute with e
40 | summary: does not commute with e
41 |
41 |
42 o changeset: 5:652413bf663e
42 o changeset: 5:652413bf663e
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: f
45 | summary: f
46 |
46 |
47 o changeset: 4:e860deea161a
47 o changeset: 4:e860deea161a
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: e
50 | summary: e
51 |
51 |
52 o changeset: 3:055a42cdd887
52 o changeset: 3:055a42cdd887
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: d
55 | summary: d
56 |
56 |
57 o changeset: 2:177f92b77385
57 o changeset: 2:177f92b77385
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: c
60 | summary: c
61 |
61 |
62 o changeset: 1:d2ae7f538514
62 o changeset: 1:d2ae7f538514
63 | user: test
63 | user: test
64 | date: Thu Jan 01 00:00:00 1970 +0000
64 | date: Thu Jan 01 00:00:00 1970 +0000
65 | summary: b
65 | summary: b
66 |
66 |
67 o changeset: 0:cb9a9f314b8b
67 o changeset: 0:cb9a9f314b8b
68 user: test
68 user: test
69 date: Thu Jan 01 00:00:00 1970 +0000
69 date: Thu Jan 01 00:00:00 1970 +0000
70 summary: a
70 summary: a
71
71
72
72
73 edit the history
73 edit the history
74 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
74 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
75 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
75 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
76 1 out of 1 hunks FAILED -- saving rejects to file e.rej
76 1 out of 1 hunks FAILED -- saving rejects to file e.rej
77 abort: Fix up the change and run hg histedit --continue
77 abort: Fix up the change and run hg histedit --continue
78
78
79 fix up
79 fix up
80 $ echo a > e
80 $ echo a > e
81 $ hg add e
81 $ hg add e
82 $ cat > cat.py <<EOF
82 $ cat > cat.py <<EOF
83 > import sys
83 > import sys
84 > print open(sys.argv[1]).read()
84 > print open(sys.argv[1]).read()
85 > print
85 > print
86 > print
86 > print
87 > EOF
87 > EOF
88 $ HGEDITOR="python cat.py" hg histedit --continue 2>&1 | fixbundle | grep -v '2 files removed'
88 $ HGEDITOR="python cat.py" hg histedit --continue 2>&1 | fixbundle | grep -v '2 files removed'
89 d
89 d
90 ***
90 ***
91 does not commute with e
91 does not commute with e
92
92
93
93
94
94 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
95 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
95 file e already exists
96 file e already exists
96 1 out of 1 hunks FAILED -- saving rejects to file e.rej
97 1 out of 1 hunks FAILED -- saving rejects to file e.rej
97 abort: Fix up the change and run hg histedit --continue
98 abort: Fix up the change and run hg histedit --continue
98
99
99 just continue this time
100 just continue this time
100 $ hg histedit --continue 2>&1 | fixbundle
101 $ hg histedit --continue 2>&1 | fixbundle
101 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
103 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
103
104
104 log after edit
105 log after edit
105 $ hg log --graph
106 $ hg log --graph
106 @ changeset: 4:f768fd60ca34
107 @ changeset: 4:f768fd60ca34
107 | tag: tip
108 | tag: tip
108 | user: test
109 | user: test
109 | date: Thu Jan 01 00:00:00 1970 +0000
110 | date: Thu Jan 01 00:00:00 1970 +0000
110 | summary: f
111 | summary: f
111 |
112 |
112 o changeset: 3:671efe372e33
113 o changeset: 3:671efe372e33
113 | user: test
114 | user: test
114 | date: Thu Jan 01 00:00:00 1970 +0000
115 | date: Thu Jan 01 00:00:00 1970 +0000
115 | summary: d
116 | summary: d
116 |
117 |
117 o changeset: 2:177f92b77385
118 o changeset: 2:177f92b77385
118 | user: test
119 | user: test
119 | date: Thu Jan 01 00:00:00 1970 +0000
120 | date: Thu Jan 01 00:00:00 1970 +0000
120 | summary: c
121 | summary: c
121 |
122 |
122 o changeset: 1:d2ae7f538514
123 o changeset: 1:d2ae7f538514
123 | user: test
124 | user: test
124 | date: Thu Jan 01 00:00:00 1970 +0000
125 | date: Thu Jan 01 00:00:00 1970 +0000
125 | summary: b
126 | summary: b
126 |
127 |
127 o changeset: 0:cb9a9f314b8b
128 o changeset: 0:cb9a9f314b8b
128 user: test
129 user: test
129 date: Thu Jan 01 00:00:00 1970 +0000
130 date: Thu Jan 01 00:00:00 1970 +0000
130 summary: a
131 summary: a
131
132
132
133
133 contents of e
134 contents of e
134 $ hg cat e
135 $ hg cat e
135 a
136 a
136
137
137 manifest
138 manifest
138 $ hg manifest
139 $ hg manifest
139 a
140 a
140 b
141 b
141 c
142 c
142 d
143 d
143 e
144 e
144 f
145 f
145
146
146 $ cd ..
147 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now