##// END OF EJS Templates
histedit: mark as a first party extension
Augie Fackler -
r17069:2b1c7867 default
parent child Browse files
Show More
@@ -1,563 +1,564 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 Inspired by git rebase --interactive.
9 Inspired by git rebase --interactive.
10 """
10 """
11 try:
11 try:
12 import cPickle as pickle
12 import cPickle as pickle
13 except ImportError:
13 except ImportError:
14 import pickle
14 import pickle
15 import tempfile
15 import tempfile
16 import os
16 import os
17
17
18 from mercurial import bookmarks
18 from mercurial import bookmarks
19 from mercurial import cmdutil
19 from mercurial import cmdutil
20 from mercurial import discovery
20 from mercurial import discovery
21 from mercurial import error
21 from mercurial import error
22 from mercurial import hg
22 from mercurial import hg
23 from mercurial import node
23 from mercurial import node
24 from mercurial import patch
24 from mercurial import patch
25 from mercurial import repair
25 from mercurial import repair
26 from mercurial import scmutil
26 from mercurial import scmutil
27 from mercurial import util
27 from mercurial import util
28 from mercurial.i18n import _
28 from mercurial.i18n import _
29
29
30 testedwith = 'internal'
30
31
31 editcomment = """
32 editcomment = """
32
33
33 # Edit history between %s and %s
34 # Edit history between %s and %s
34 #
35 #
35 # Commands:
36 # Commands:
36 # p, pick = use commit
37 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
38 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but fold into previous commit (combines N and N-1)
39 # f, fold = use commit, but fold into previous commit (combines N and N-1)
39 # d, drop = remove commit from history
40 # d, drop = remove commit from history
40 # m, mess = edit message without changing commit content
41 # m, mess = edit message without changing commit content
41 #
42 #
42 """
43 """
43
44
44 def between(repo, old, new, keep):
45 def between(repo, old, new, keep):
45 revs = [old]
46 revs = [old]
46 current = old
47 current = old
47 while current != new:
48 while current != new:
48 ctx = repo[current]
49 ctx = repo[current]
49 if not keep and len(ctx.children()) > 1:
50 if not keep and len(ctx.children()) > 1:
50 raise util.Abort(_('cannot edit history that would orphan nodes'))
51 raise util.Abort(_('cannot edit history that would orphan nodes'))
51 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
52 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
52 raise util.Abort(_("can't edit history with merges"))
53 raise util.Abort(_("can't edit history with merges"))
53 if not ctx.children():
54 if not ctx.children():
54 current = new
55 current = new
55 else:
56 else:
56 current = ctx.children()[0].node()
57 current = ctx.children()[0].node()
57 revs.append(current)
58 revs.append(current)
58 if len(repo[current].children()) and not keep:
59 if len(repo[current].children()) and not keep:
59 raise util.Abort(_('cannot edit history that would orphan nodes'))
60 raise util.Abort(_('cannot edit history that would orphan nodes'))
60 return revs
61 return revs
61
62
62
63
63 def pick(ui, repo, ctx, ha, opts):
64 def pick(ui, repo, ctx, ha, opts):
64 oldctx = repo[ha]
65 oldctx = repo[ha]
65 if oldctx.parents()[0] == ctx:
66 if oldctx.parents()[0] == ctx:
66 ui.debug('node %s unchanged\n' % ha)
67 ui.debug('node %s unchanged\n' % ha)
67 return oldctx, [], [], []
68 return oldctx, [], [], []
68 hg.update(repo, ctx.node())
69 hg.update(repo, ctx.node())
69 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
70 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
70 fp = os.fdopen(fd, 'w')
71 fp = os.fdopen(fd, 'w')
71 diffopts = patch.diffopts(ui, opts)
72 diffopts = patch.diffopts(ui, opts)
72 diffopts.git = True
73 diffopts.git = True
73 diffopts.ignorews = False
74 diffopts.ignorews = False
74 diffopts.ignorewsamount = False
75 diffopts.ignorewsamount = False
75 diffopts.ignoreblanklines = False
76 diffopts.ignoreblanklines = False
76 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
77 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
77 for chunk in gen:
78 for chunk in gen:
78 fp.write(chunk)
79 fp.write(chunk)
79 fp.close()
80 fp.close()
80 try:
81 try:
81 files = set()
82 files = set()
82 try:
83 try:
83 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
84 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
84 if not files:
85 if not files:
85 ui.warn(_('%s: empty changeset')
86 ui.warn(_('%s: empty changeset')
86 % node.hex(ha))
87 % node.hex(ha))
87 return ctx, [], [], []
88 return ctx, [], [], []
88 finally:
89 finally:
89 os.unlink(patchfile)
90 os.unlink(patchfile)
90 except Exception:
91 except Exception:
91 raise util.Abort(_('Fix up the change and run '
92 raise util.Abort(_('Fix up the change and run '
92 'hg histedit --continue'))
93 'hg histedit --continue'))
93 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
94 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
94 date=oldctx.date(), extra=oldctx.extra())
95 date=oldctx.date(), extra=oldctx.extra())
95 return repo[n], [n], [oldctx.node()], []
96 return repo[n], [n], [oldctx.node()], []
96
97
97
98
98 def edit(ui, repo, ctx, ha, opts):
99 def edit(ui, repo, ctx, ha, opts):
99 oldctx = repo[ha]
100 oldctx = repo[ha]
100 hg.update(repo, ctx.node())
101 hg.update(repo, ctx.node())
101 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
102 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
102 fp = os.fdopen(fd, 'w')
103 fp = os.fdopen(fd, 'w')
103 diffopts = patch.diffopts(ui, opts)
104 diffopts = patch.diffopts(ui, opts)
104 diffopts.git = True
105 diffopts.git = True
105 diffopts.ignorews = False
106 diffopts.ignorews = False
106 diffopts.ignorewsamount = False
107 diffopts.ignorewsamount = False
107 diffopts.ignoreblanklines = False
108 diffopts.ignoreblanklines = False
108 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
109 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
109 for chunk in gen:
110 for chunk in gen:
110 fp.write(chunk)
111 fp.write(chunk)
111 fp.close()
112 fp.close()
112 try:
113 try:
113 files = set()
114 files = set()
114 try:
115 try:
115 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
116 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
116 finally:
117 finally:
117 os.unlink(patchfile)
118 os.unlink(patchfile)
118 except Exception:
119 except Exception:
119 pass
120 pass
120 raise util.Abort(_('Make changes as needed, you may commit or record as '
121 raise util.Abort(_('Make changes as needed, you may commit or record as '
121 'needed now.\nWhen you are finished, run hg'
122 'needed now.\nWhen you are finished, run hg'
122 ' histedit --continue to resume.'))
123 ' histedit --continue to resume.'))
123
124
124 def fold(ui, repo, ctx, ha, opts):
125 def fold(ui, repo, ctx, ha, opts):
125 oldctx = repo[ha]
126 oldctx = repo[ha]
126 hg.update(repo, ctx.node())
127 hg.update(repo, ctx.node())
127 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
128 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
128 fp = os.fdopen(fd, 'w')
129 fp = os.fdopen(fd, 'w')
129 diffopts = patch.diffopts(ui, opts)
130 diffopts = patch.diffopts(ui, opts)
130 diffopts.git = True
131 diffopts.git = True
131 diffopts.ignorews = False
132 diffopts.ignorews = False
132 diffopts.ignorewsamount = False
133 diffopts.ignorewsamount = False
133 diffopts.ignoreblanklines = False
134 diffopts.ignoreblanklines = False
134 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
135 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
135 for chunk in gen:
136 for chunk in gen:
136 fp.write(chunk)
137 fp.write(chunk)
137 fp.close()
138 fp.close()
138 try:
139 try:
139 files = set()
140 files = set()
140 try:
141 try:
141 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
142 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
142 if not files:
143 if not files:
143 ui.warn(_('%s: empty changeset')
144 ui.warn(_('%s: empty changeset')
144 % node.hex(ha))
145 % node.hex(ha))
145 return ctx, [], [], []
146 return ctx, [], [], []
146 finally:
147 finally:
147 os.unlink(patchfile)
148 os.unlink(patchfile)
148 except Exception:
149 except Exception:
149 raise util.Abort(_('Fix up the change and run '
150 raise util.Abort(_('Fix up the change and run '
150 'hg histedit --continue'))
151 'hg histedit --continue'))
151 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
152 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
152 date=oldctx.date(), extra=oldctx.extra())
153 date=oldctx.date(), extra=oldctx.extra())
153 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
154 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
154
155
155 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
156 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
156 parent = ctx.parents()[0].node()
157 parent = ctx.parents()[0].node()
157 hg.update(repo, parent)
158 hg.update(repo, parent)
158 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
159 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
159 fp = os.fdopen(fd, 'w')
160 fp = os.fdopen(fd, 'w')
160 diffopts = patch.diffopts(ui, opts)
161 diffopts = patch.diffopts(ui, opts)
161 diffopts.git = True
162 diffopts.git = True
162 diffopts.ignorews = False
163 diffopts.ignorews = False
163 diffopts.ignorewsamount = False
164 diffopts.ignorewsamount = False
164 diffopts.ignoreblanklines = False
165 diffopts.ignoreblanklines = False
165 gen = patch.diff(repo, parent, newnode, opts=diffopts)
166 gen = patch.diff(repo, parent, newnode, opts=diffopts)
166 for chunk in gen:
167 for chunk in gen:
167 fp.write(chunk)
168 fp.write(chunk)
168 fp.close()
169 fp.close()
169 files = set()
170 files = set()
170 try:
171 try:
171 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
172 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
172 finally:
173 finally:
173 os.unlink(patchfile)
174 os.unlink(patchfile)
174 newmessage = '\n***\n'.join(
175 newmessage = '\n***\n'.join(
175 [ctx.description()] +
176 [ctx.description()] +
176 [repo[r].description() for r in internalchanges] +
177 [repo[r].description() for r in internalchanges] +
177 [oldctx.description()])
178 [oldctx.description()])
178 # If the changesets are from the same author, keep it.
179 # If the changesets are from the same author, keep it.
179 if ctx.user() == oldctx.user():
180 if ctx.user() == oldctx.user():
180 username = ctx.user()
181 username = ctx.user()
181 else:
182 else:
182 username = ui.username()
183 username = ui.username()
183 newmessage = ui.edit(newmessage, username)
184 newmessage = ui.edit(newmessage, username)
184 n = repo.commit(text=newmessage, user=username,
185 n = repo.commit(text=newmessage, user=username,
185 date=max(ctx.date(), oldctx.date()), extra=oldctx.extra())
186 date=max(ctx.date(), oldctx.date()), extra=oldctx.extra())
186 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
187 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
187
188
188 def drop(ui, repo, ctx, ha, opts):
189 def drop(ui, repo, ctx, ha, opts):
189 return ctx, [], [repo[ha].node()], []
190 return ctx, [], [repo[ha].node()], []
190
191
191
192
192 def message(ui, repo, ctx, ha, opts):
193 def message(ui, repo, ctx, ha, opts):
193 oldctx = repo[ha]
194 oldctx = repo[ha]
194 hg.update(repo, ctx.node())
195 hg.update(repo, ctx.node())
195 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
196 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
196 fp = os.fdopen(fd, 'w')
197 fp = os.fdopen(fd, 'w')
197 diffopts = patch.diffopts(ui, opts)
198 diffopts = patch.diffopts(ui, opts)
198 diffopts.git = True
199 diffopts.git = True
199 diffopts.ignorews = False
200 diffopts.ignorews = False
200 diffopts.ignorewsamount = False
201 diffopts.ignorewsamount = False
201 diffopts.ignoreblanklines = False
202 diffopts.ignoreblanklines = False
202 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
203 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
203 for chunk in gen:
204 for chunk in gen:
204 fp.write(chunk)
205 fp.write(chunk)
205 fp.close()
206 fp.close()
206 try:
207 try:
207 files = set()
208 files = set()
208 try:
209 try:
209 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
210 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
210 finally:
211 finally:
211 os.unlink(patchfile)
212 os.unlink(patchfile)
212 except Exception:
213 except Exception:
213 raise util.Abort(_('Fix up the change and run '
214 raise util.Abort(_('Fix up the change and run '
214 'hg histedit --continue'))
215 'hg histedit --continue'))
215 message = oldctx.description()
216 message = oldctx.description()
216 message = ui.edit(message, ui.username())
217 message = ui.edit(message, ui.username())
217 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
218 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
218 extra=oldctx.extra())
219 extra=oldctx.extra())
219 newctx = repo[new]
220 newctx = repo[new]
220 if oldctx.node() != newctx.node():
221 if oldctx.node() != newctx.node():
221 return newctx, [new], [oldctx.node()], []
222 return newctx, [new], [oldctx.node()], []
222 # We didn't make an edit, so just indicate no replaced nodes
223 # We didn't make an edit, so just indicate no replaced nodes
223 return newctx, [new], [], []
224 return newctx, [new], [], []
224
225
225
226
226 def makedesc(c):
227 def makedesc(c):
227 summary = ''
228 summary = ''
228 if c.description():
229 if c.description():
229 summary = c.description().splitlines()[0]
230 summary = c.description().splitlines()[0]
230 line = 'pick %s %d %s' % (c.hex()[:12], c.rev(), summary)
231 line = 'pick %s %d %s' % (c.hex()[:12], c.rev(), summary)
231 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
232 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
232
233
233 actiontable = {'p': pick,
234 actiontable = {'p': pick,
234 'pick': pick,
235 'pick': pick,
235 'e': edit,
236 'e': edit,
236 'edit': edit,
237 'edit': edit,
237 'f': fold,
238 'f': fold,
238 'fold': fold,
239 'fold': fold,
239 'd': drop,
240 'd': drop,
240 'drop': drop,
241 'drop': drop,
241 'm': message,
242 'm': message,
242 'mess': message,
243 'mess': message,
243 }
244 }
244 def histedit(ui, repo, *parent, **opts):
245 def histedit(ui, repo, *parent, **opts):
245 """hg histedit <parent>
246 """hg histedit <parent>
246 """
247 """
247 # TODO only abort if we try and histedit mq patches, not just
248 # TODO only abort if we try and histedit mq patches, not just
248 # blanket if mq patches are applied somewhere
249 # blanket if mq patches are applied somewhere
249 mq = getattr(repo, 'mq', None)
250 mq = getattr(repo, 'mq', None)
250 if mq and mq.applied:
251 if mq and mq.applied:
251 raise util.Abort(_('source has mq patches applied'))
252 raise util.Abort(_('source has mq patches applied'))
252
253
253 parent = list(parent) + opts.get('rev', [])
254 parent = list(parent) + opts.get('rev', [])
254 if opts.get('outgoing'):
255 if opts.get('outgoing'):
255 if len(parent) > 1:
256 if len(parent) > 1:
256 raise util.Abort(
257 raise util.Abort(
257 _('only one repo argument allowed with --outgoing'))
258 _('only one repo argument allowed with --outgoing'))
258 elif parent:
259 elif parent:
259 parent = parent[0]
260 parent = parent[0]
260
261
261 dest = ui.expandpath(parent or 'default-push', parent or 'default')
262 dest = ui.expandpath(parent or 'default-push', parent or 'default')
262 dest, revs = hg.parseurl(dest, None)[:2]
263 dest, revs = hg.parseurl(dest, None)[:2]
263 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
264 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
264
265
265 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
266 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
266 other = hg.repository(hg.remoteui(repo, opts), dest)
267 other = hg.repository(hg.remoteui(repo, opts), dest)
267
268
268 if revs:
269 if revs:
269 revs = [repo.lookup(rev) for rev in revs]
270 revs = [repo.lookup(rev) for rev in revs]
270
271
271 parent = discovery.findcommonoutgoing(
272 parent = discovery.findcommonoutgoing(
272 repo, other, [], force=opts.get('force')).missing[0:1]
273 repo, other, [], force=opts.get('force')).missing[0:1]
273 else:
274 else:
274 if opts.get('force'):
275 if opts.get('force'):
275 raise util.Abort(_('--force only allowed with --outgoing'))
276 raise util.Abort(_('--force only allowed with --outgoing'))
276
277
277 if opts.get('continue', False):
278 if opts.get('continue', False):
278 if len(parent) != 0:
279 if len(parent) != 0:
279 raise util.Abort(_('no arguments allowed with --continue'))
280 raise util.Abort(_('no arguments allowed with --continue'))
280 (parentctxnode, created, replaced,
281 (parentctxnode, created, replaced,
281 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
282 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
282 currentparent, wantnull = repo.dirstate.parents()
283 currentparent, wantnull = repo.dirstate.parents()
283 parentctx = repo[parentctxnode]
284 parentctx = repo[parentctxnode]
284 # discover any nodes the user has added in the interim
285 # discover any nodes the user has added in the interim
285 newchildren = [c for c in parentctx.children()
286 newchildren = [c for c in parentctx.children()
286 if c.node() not in existing]
287 if c.node() not in existing]
287 action, currentnode = rules.pop(0)
288 action, currentnode = rules.pop(0)
288 while newchildren:
289 while newchildren:
289 if action in ('f', 'fold'):
290 if action in ('f', 'fold'):
290 tmpnodes.extend([n.node() for n in newchildren])
291 tmpnodes.extend([n.node() for n in newchildren])
291 else:
292 else:
292 created.extend([n.node() for n in newchildren])
293 created.extend([n.node() for n in newchildren])
293 filtered = []
294 filtered = []
294 for r in newchildren:
295 for r in newchildren:
295 filtered += [c for c in r.children() if c.node not in existing]
296 filtered += [c for c in r.children() if c.node not in existing]
296 newchildren = filtered
297 newchildren = filtered
297 m, a, r, d = repo.status()[:4]
298 m, a, r, d = repo.status()[:4]
298 oldctx = repo[currentnode]
299 oldctx = repo[currentnode]
299 message = oldctx.description()
300 message = oldctx.description()
300 if action in ('e', 'edit', 'm', 'mess'):
301 if action in ('e', 'edit', 'm', 'mess'):
301 message = ui.edit(message, ui.username())
302 message = ui.edit(message, ui.username())
302 elif action in ('f', 'fold'):
303 elif action in ('f', 'fold'):
303 message = 'fold-temp-revision %s' % currentnode
304 message = 'fold-temp-revision %s' % currentnode
304 new = None
305 new = None
305 if m or a or r or d:
306 if m or a or r or d:
306 new = repo.commit(text=message, user=oldctx.user(),
307 new = repo.commit(text=message, user=oldctx.user(),
307 date=oldctx.date(), extra=oldctx.extra())
308 date=oldctx.date(), extra=oldctx.extra())
308
309
309 if action in ('f', 'fold'):
310 if action in ('f', 'fold'):
310 if new:
311 if new:
311 tmpnodes.append(new)
312 tmpnodes.append(new)
312 else:
313 else:
313 new = newchildren[-1]
314 new = newchildren[-1]
314 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
315 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
315 ui, repo, parentctx, oldctx, new, opts, newchildren)
316 ui, repo, parentctx, oldctx, new, opts, newchildren)
316 replaced.extend(replaced_)
317 replaced.extend(replaced_)
317 created.extend(created_)
318 created.extend(created_)
318 tmpnodes.extend(tmpnodes_)
319 tmpnodes.extend(tmpnodes_)
319 elif action not in ('d', 'drop'):
320 elif action not in ('d', 'drop'):
320 if new != oldctx.node():
321 if new != oldctx.node():
321 replaced.append(oldctx.node())
322 replaced.append(oldctx.node())
322 if new:
323 if new:
323 if new != oldctx.node():
324 if new != oldctx.node():
324 created.append(new)
325 created.append(new)
325 parentctx = repo[new]
326 parentctx = repo[new]
326
327
327 elif opts.get('abort', False):
328 elif opts.get('abort', False):
328 if len(parent) != 0:
329 if len(parent) != 0:
329 raise util.Abort(_('no arguments allowed with --abort'))
330 raise util.Abort(_('no arguments allowed with --abort'))
330 (parentctxnode, created, replaced, tmpnodes,
331 (parentctxnode, created, replaced, tmpnodes,
331 existing, rules, keep, tip, replacemap) = readstate(repo)
332 existing, rules, keep, tip, replacemap) = readstate(repo)
332 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
333 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
333 hg.clean(repo, tip)
334 hg.clean(repo, tip)
334 ui.debug('should strip created nodes %s\n' %
335 ui.debug('should strip created nodes %s\n' %
335 ', '.join([node.hex(n)[:12] for n in created]))
336 ', '.join([node.hex(n)[:12] for n in created]))
336 ui.debug('should strip temp nodes %s\n' %
337 ui.debug('should strip temp nodes %s\n' %
337 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
338 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
338 for nodes in (created, tmpnodes):
339 for nodes in (created, tmpnodes):
339 for n in reversed(nodes):
340 for n in reversed(nodes):
340 try:
341 try:
341 repair.strip(ui, repo, n)
342 repair.strip(ui, repo, n)
342 except error.LookupError:
343 except error.LookupError:
343 pass
344 pass
344 os.unlink(os.path.join(repo.path, 'histedit-state'))
345 os.unlink(os.path.join(repo.path, 'histedit-state'))
345 return
346 return
346 else:
347 else:
347 cmdutil.bailifchanged(repo)
348 cmdutil.bailifchanged(repo)
348 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
349 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
349 raise util.Abort(_('history edit already in progress, try '
350 raise util.Abort(_('history edit already in progress, try '
350 '--continue or --abort'))
351 '--continue or --abort'))
351
352
352 tip, empty = repo.dirstate.parents()
353 tip, empty = repo.dirstate.parents()
353
354
354
355
355 if len(parent) != 1:
356 if len(parent) != 1:
356 raise util.Abort(_('histedit requires exactly one parent revision'))
357 raise util.Abort(_('histedit requires exactly one parent revision'))
357 parent = scmutil.revsingle(repo, parent[0]).node()
358 parent = scmutil.revsingle(repo, parent[0]).node()
358
359
359 keep = opts.get('keep', False)
360 keep = opts.get('keep', False)
360 revs = between(repo, parent, tip, keep)
361 revs = between(repo, parent, tip, keep)
361
362
362 ctxs = [repo[r] for r in revs]
363 ctxs = [repo[r] for r in revs]
363 existing = [r.node() for r in ctxs]
364 existing = [r.node() for r in ctxs]
364 rules = opts.get('commands', '')
365 rules = opts.get('commands', '')
365 if not rules:
366 if not rules:
366 rules = '\n'.join([makedesc(c) for c in ctxs])
367 rules = '\n'.join([makedesc(c) for c in ctxs])
367 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
368 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
368 rules = ui.edit(rules, ui.username())
369 rules = ui.edit(rules, ui.username())
369 # Save edit rules in .hg/histedit-last-edit.txt in case
370 # Save edit rules in .hg/histedit-last-edit.txt in case
370 # the user needs to ask for help after something
371 # the user needs to ask for help after something
371 # surprising happens.
372 # surprising happens.
372 f = open(repo.join('histedit-last-edit.txt'), 'w')
373 f = open(repo.join('histedit-last-edit.txt'), 'w')
373 f.write(rules)
374 f.write(rules)
374 f.close()
375 f.close()
375 else:
376 else:
376 f = open(rules)
377 f = open(rules)
377 rules = f.read()
378 rules = f.read()
378 f.close()
379 f.close()
379 rules = [l for l in (r.strip() for r in rules.splitlines())
380 rules = [l for l in (r.strip() for r in rules.splitlines())
380 if l and not l[0] == '#']
381 if l and not l[0] == '#']
381 rules = verifyrules(rules, repo, ctxs)
382 rules = verifyrules(rules, repo, ctxs)
382
383
383 parentctx = repo[parent].parents()[0]
384 parentctx = repo[parent].parents()[0]
384 keep = opts.get('keep', False)
385 keep = opts.get('keep', False)
385 replaced = []
386 replaced = []
386 replacemap = {}
387 replacemap = {}
387 tmpnodes = []
388 tmpnodes = []
388 created = []
389 created = []
389
390
390
391
391 while rules:
392 while rules:
392 writestate(repo, parentctx.node(), created, replaced,
393 writestate(repo, parentctx.node(), created, replaced,
393 tmpnodes, existing, rules, keep, tip, replacemap)
394 tmpnodes, existing, rules, keep, tip, replacemap)
394 action, ha = rules.pop(0)
395 action, ha = rules.pop(0)
395 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
396 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
396 ui, repo, parentctx, ha, opts)
397 ui, repo, parentctx, ha, opts)
397
398
398 hexshort = lambda x: node.hex(x)[:12]
399 hexshort = lambda x: node.hex(x)[:12]
399
400
400 if replaced_:
401 if replaced_:
401 clen, rlen = len(created_), len(replaced_)
402 clen, rlen = len(created_), len(replaced_)
402 if clen == rlen == 1:
403 if clen == rlen == 1:
403 ui.debug('histedit: exact replacement of %s with %s\n' % (
404 ui.debug('histedit: exact replacement of %s with %s\n' % (
404 hexshort(replaced_[0]), hexshort(created_[0])))
405 hexshort(replaced_[0]), hexshort(created_[0])))
405
406
406 replacemap[replaced_[0]] = created_[0]
407 replacemap[replaced_[0]] = created_[0]
407 elif clen > rlen:
408 elif clen > rlen:
408 assert rlen == 1, ('unexpected replacement of '
409 assert rlen == 1, ('unexpected replacement of '
409 '%d changes with %d changes' % (rlen, clen))
410 '%d changes with %d changes' % (rlen, clen))
410 # made more changesets than we're replacing
411 # made more changesets than we're replacing
411 # TODO synthesize patch names for created patches
412 # TODO synthesize patch names for created patches
412 replacemap[replaced_[0]] = created_[-1]
413 replacemap[replaced_[0]] = created_[-1]
413 ui.debug('histedit: created many, assuming %s replaced by %s' %
414 ui.debug('histedit: created many, assuming %s replaced by %s' %
414 (hexshort(replaced_[0]), hexshort(created_[-1])))
415 (hexshort(replaced_[0]), hexshort(created_[-1])))
415 elif rlen > clen:
416 elif rlen > clen:
416 if not created_:
417 if not created_:
417 # This must be a drop. Try and put our metadata on
418 # This must be a drop. Try and put our metadata on
418 # the parent change.
419 # the parent change.
419 assert rlen == 1
420 assert rlen == 1
420 r = replaced_[0]
421 r = replaced_[0]
421 ui.debug('histedit: %s seems replaced with nothing, '
422 ui.debug('histedit: %s seems replaced with nothing, '
422 'finding a parent\n' % (hexshort(r)))
423 'finding a parent\n' % (hexshort(r)))
423 pctx = repo[r].parents()[0]
424 pctx = repo[r].parents()[0]
424 if pctx.node() in replacemap:
425 if pctx.node() in replacemap:
425 ui.debug('histedit: parent is already replaced\n')
426 ui.debug('histedit: parent is already replaced\n')
426 replacemap[r] = replacemap[pctx.node()]
427 replacemap[r] = replacemap[pctx.node()]
427 else:
428 else:
428 replacemap[r] = pctx.node()
429 replacemap[r] = pctx.node()
429 ui.debug('histedit: %s best replaced by %s\n' % (
430 ui.debug('histedit: %s best replaced by %s\n' % (
430 hexshort(r), hexshort(replacemap[r])))
431 hexshort(r), hexshort(replacemap[r])))
431 else:
432 else:
432 assert len(created_) == 1
433 assert len(created_) == 1
433 for r in replaced_:
434 for r in replaced_:
434 ui.debug('histedit: %s replaced by %s\n' % (
435 ui.debug('histedit: %s replaced by %s\n' % (
435 hexshort(r), hexshort(created_[0])))
436 hexshort(r), hexshort(created_[0])))
436 replacemap[r] = created_[0]
437 replacemap[r] = created_[0]
437 else:
438 else:
438 assert False, (
439 assert False, (
439 'Unhandled case in replacement mapping! '
440 'Unhandled case in replacement mapping! '
440 'replacing %d changes with %d changes' % (rlen, clen))
441 'replacing %d changes with %d changes' % (rlen, clen))
441 created.extend(created_)
442 created.extend(created_)
442 replaced.extend(replaced_)
443 replaced.extend(replaced_)
443 tmpnodes.extend(tmpnodes_)
444 tmpnodes.extend(tmpnodes_)
444
445
445 hg.update(repo, parentctx.node())
446 hg.update(repo, parentctx.node())
446
447
447 if not keep:
448 if not keep:
448 if replacemap:
449 if replacemap:
449 ui.note(_('histedit: Should update metadata for the following '
450 ui.note(_('histedit: Should update metadata for the following '
450 'changes:\n'))
451 'changes:\n'))
451
452
452 def copybms(old, new):
453 def copybms(old, new):
453 if old in tmpnodes or old in created:
454 if old in tmpnodes or old in created:
454 # can't have any metadata we'd want to update
455 # can't have any metadata we'd want to update
455 return
456 return
456 while new in replacemap:
457 while new in replacemap:
457 new = replacemap[new]
458 new = replacemap[new]
458 ui.note(_('histedit: %s to %s\n') % (hexshort(old),
459 ui.note(_('histedit: %s to %s\n') % (hexshort(old),
459 hexshort(new)))
460 hexshort(new)))
460 octx = repo[old]
461 octx = repo[old]
461 marks = octx.bookmarks()
462 marks = octx.bookmarks()
462 if marks:
463 if marks:
463 ui.note(_('histedit: moving bookmarks %s\n') %
464 ui.note(_('histedit: moving bookmarks %s\n') %
464 ', '.join(marks))
465 ', '.join(marks))
465 for mark in marks:
466 for mark in marks:
466 repo._bookmarks[mark] = new
467 repo._bookmarks[mark] = new
467 bookmarks.write(repo)
468 bookmarks.write(repo)
468
469
469 # We assume that bookmarks on the tip should remain
470 # We assume that bookmarks on the tip should remain
470 # tipmost, but bookmarks on non-tip changesets should go
471 # tipmost, but bookmarks on non-tip changesets should go
471 # to their most reasonable successor. As a result, find
472 # to their most reasonable successor. As a result, find
472 # the old tip and new tip and copy those bookmarks first,
473 # the old tip and new tip and copy those bookmarks first,
473 # then do the rest of the bookmark copies.
474 # then do the rest of the bookmark copies.
474 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
475 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
475 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
476 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
476 copybms(oldtip, newtip)
477 copybms(oldtip, newtip)
477
478
478 for old, new in replacemap.iteritems():
479 for old, new in replacemap.iteritems():
479 copybms(old, new)
480 copybms(old, new)
480 # TODO update mq state
481 # TODO update mq state
481
482
482 ui.debug('should strip replaced nodes %s\n' %
483 ui.debug('should strip replaced nodes %s\n' %
483 ', '.join([node.hex(n)[:12] for n in replaced]))
484 ', '.join([node.hex(n)[:12] for n in replaced]))
484 for n in sorted(replaced, key=lambda x: repo[x].rev()):
485 for n in sorted(replaced, key=lambda x: repo[x].rev()):
485 try:
486 try:
486 repair.strip(ui, repo, n)
487 repair.strip(ui, repo, n)
487 except error.LookupError:
488 except error.LookupError:
488 pass
489 pass
489
490
490 ui.debug('should strip temp nodes %s\n' %
491 ui.debug('should strip temp nodes %s\n' %
491 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
492 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
492 for n in reversed(tmpnodes):
493 for n in reversed(tmpnodes):
493 try:
494 try:
494 repair.strip(ui, repo, n)
495 repair.strip(ui, repo, n)
495 except error.LookupError:
496 except error.LookupError:
496 pass
497 pass
497 os.unlink(os.path.join(repo.path, 'histedit-state'))
498 os.unlink(os.path.join(repo.path, 'histedit-state'))
498 if os.path.exists(repo.sjoin('undo')):
499 if os.path.exists(repo.sjoin('undo')):
499 os.unlink(repo.sjoin('undo'))
500 os.unlink(repo.sjoin('undo'))
500
501
501
502
502 def writestate(repo, parentctxnode, created, replaced,
503 def writestate(repo, parentctxnode, created, replaced,
503 tmpnodes, existing, rules, keep, oldtip, replacemap):
504 tmpnodes, existing, rules, keep, oldtip, replacemap):
504 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
505 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
505 pickle.dump((parentctxnode, created, replaced,
506 pickle.dump((parentctxnode, created, replaced,
506 tmpnodes, existing, rules, keep, oldtip, replacemap),
507 tmpnodes, existing, rules, keep, oldtip, replacemap),
507 fp)
508 fp)
508 fp.close()
509 fp.close()
509
510
510 def readstate(repo):
511 def readstate(repo):
511 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
512 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
512 keep, oldtip, replacemap ).
513 keep, oldtip, replacemap ).
513 """
514 """
514 fp = open(os.path.join(repo.path, 'histedit-state'))
515 fp = open(os.path.join(repo.path, 'histedit-state'))
515 return pickle.load(fp)
516 return pickle.load(fp)
516
517
517
518
518 def verifyrules(rules, repo, ctxs):
519 def verifyrules(rules, repo, ctxs):
519 """Verify that there exists exactly one edit rule per given changeset.
520 """Verify that there exists exactly one edit rule per given changeset.
520
521
521 Will abort if there are to many or too few rules, a malformed rule,
522 Will abort if there are to many or too few rules, a malformed rule,
522 or a rule on a changeset outside of the user-given range.
523 or a rule on a changeset outside of the user-given range.
523 """
524 """
524 parsed = []
525 parsed = []
525 if len(rules) != len(ctxs):
526 if len(rules) != len(ctxs):
526 raise util.Abort(_('must specify a rule for each changeset once'))
527 raise util.Abort(_('must specify a rule for each changeset once'))
527 for r in rules:
528 for r in rules:
528 if ' ' not in r:
529 if ' ' not in r:
529 raise util.Abort(_('malformed line "%s"') % r)
530 raise util.Abort(_('malformed line "%s"') % r)
530 action, rest = r.split(' ', 1)
531 action, rest = r.split(' ', 1)
531 if ' ' in rest.strip():
532 if ' ' in rest.strip():
532 ha, rest = rest.split(' ', 1)
533 ha, rest = rest.split(' ', 1)
533 else:
534 else:
534 ha = r.strip()
535 ha = r.strip()
535 try:
536 try:
536 if repo[ha] not in ctxs:
537 if repo[ha] not in ctxs:
537 raise util.Abort(
538 raise util.Abort(
538 _('may not use changesets other than the ones listed'))
539 _('may not use changesets other than the ones listed'))
539 except error.RepoError:
540 except error.RepoError:
540 raise util.Abort(_('unknown changeset %s listed') % ha)
541 raise util.Abort(_('unknown changeset %s listed') % ha)
541 if action not in actiontable:
542 if action not in actiontable:
542 raise util.Abort(_('unknown action "%s"') % action)
543 raise util.Abort(_('unknown action "%s"') % action)
543 parsed.append([action, ha])
544 parsed.append([action, ha])
544 return parsed
545 return parsed
545
546
546
547
547 cmdtable = {
548 cmdtable = {
548 "histedit":
549 "histedit":
549 (histedit,
550 (histedit,
550 [('', 'commands', '', _(
551 [('', 'commands', '', _(
551 'Read history edits from the specified file.')),
552 'Read history edits from the specified file.')),
552 ('c', 'continue', False, _('continue an edit already in progress')),
553 ('c', 'continue', False, _('continue an edit already in progress')),
553 ('k', 'keep', False, _(
554 ('k', 'keep', False, _(
554 "don't strip old nodes after edit is complete")),
555 "don't strip old nodes after edit is complete")),
555 ('', 'abort', False, _('abort an edit in progress')),
556 ('', 'abort', False, _('abort an edit in progress')),
556 ('o', 'outgoing', False, _('changesets not found in destination')),
557 ('o', 'outgoing', False, _('changesets not found in destination')),
557 ('f', 'force', False, _(
558 ('f', 'force', False, _(
558 'force outgoing even for unrelated repositories')),
559 'force outgoing even for unrelated repositories')),
559 ('r', 'rev', [], _('first revision to be edited')),
560 ('r', 'rev', [], _('first revision to be edited')),
560 ],
561 ],
561 __doc__,
562 __doc__,
562 ),
563 ),
563 }
564 }
General Comments 0
You need to be logged in to leave comments. Login now