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