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