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