##// END OF EJS Templates
histedit: move outgoing processing to its own function...
Pierre-Yves David -
r19021:26b41a90 default
parent child Browse files
Show More
@@ -1,845 +1,853 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df 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 c561b4e977df and 7c2fd3b9020c
50 # Edit history between c561b4e977df 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 histedit-ed example repository above and add four more
111 If we clone the histedit-ed example repository above and add four more
112 changes, such that we have the following history::
112 changes, such that 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 import sys
146 import sys
147
147
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 obsolete
159 from mercurial import obsolete
160 from mercurial import merge as mergemod
160 from mercurial import merge as mergemod
161 from mercurial.i18n import _
161 from mercurial.i18n import _
162
162
163 cmdtable = {}
163 cmdtable = {}
164 command = cmdutil.command(cmdtable)
164 command = cmdutil.command(cmdtable)
165
165
166 testedwith = 'internal'
166 testedwith = 'internal'
167
167
168 # i18n: command names and abbreviations must remain untranslated
168 # i18n: command names and abbreviations must remain untranslated
169 editcomment = _("""# Edit history between %s and %s
169 editcomment = _("""# Edit history between %s and %s
170 #
170 #
171 # Commands:
171 # Commands:
172 # p, pick = use commit
172 # p, pick = use commit
173 # e, edit = use commit, but stop for amending
173 # e, edit = use commit, but stop for amending
174 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 # f, fold = use commit, but fold into previous commit (combines N and N-1)
175 # d, drop = remove commit from history
175 # d, drop = remove commit from history
176 # m, mess = edit message without changing commit content
176 # m, mess = edit message without changing commit content
177 #
177 #
178 """)
178 """)
179
179
180 def commitfuncfor(repo, src):
180 def commitfuncfor(repo, src):
181 """Build a commit function for the replacement of <src>
181 """Build a commit function for the replacement of <src>
182
182
183 This function ensure we apply the same treatment to all changesets.
183 This function ensure we apply the same treatment to all changesets.
184
184
185 - Add a 'histedit_source' entry in extra.
185 - Add a 'histedit_source' entry in extra.
186
186
187 Note that fold have its own separated logic because its handling is a bit
187 Note that fold have its own separated logic because its handling is a bit
188 different and not easily factored out of the fold method.
188 different and not easily factored out of the fold method.
189 """
189 """
190 phasemin = src.phase()
190 phasemin = src.phase()
191 def commitfunc(**kwargs):
191 def commitfunc(**kwargs):
192 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
192 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
193 try:
193 try:
194 repo.ui.setconfig('phases', 'new-commit', phasemin)
194 repo.ui.setconfig('phases', 'new-commit', phasemin)
195 extra = kwargs.get('extra', {}).copy()
195 extra = kwargs.get('extra', {}).copy()
196 extra['histedit_source'] = src.hex()
196 extra['histedit_source'] = src.hex()
197 kwargs['extra'] = extra
197 kwargs['extra'] = extra
198 return repo.commit(**kwargs)
198 return repo.commit(**kwargs)
199 finally:
199 finally:
200 repo.ui.restoreconfig(phasebackup)
200 repo.ui.restoreconfig(phasebackup)
201 return commitfunc
201 return commitfunc
202
202
203
203
204
204
205 def applychanges(ui, repo, ctx, opts):
205 def applychanges(ui, repo, ctx, opts):
206 """Merge changeset from ctx (only) in the current working directory"""
206 """Merge changeset from ctx (only) in the current working directory"""
207 wcpar = repo.dirstate.parents()[0]
207 wcpar = repo.dirstate.parents()[0]
208 if ctx.p1().node() == wcpar:
208 if ctx.p1().node() == wcpar:
209 # edition ar "in place" we do not need to make any merge,
209 # edition ar "in place" we do not need to make any merge,
210 # just applies changes on parent for edition
210 # just applies changes on parent for edition
211 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
211 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
212 stats = None
212 stats = None
213 else:
213 else:
214 try:
214 try:
215 # ui.forcemerge is an internal variable, do not document
215 # ui.forcemerge is an internal variable, do not document
216 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
216 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
217 stats = mergemod.update(repo, ctx.node(), True, True, False,
217 stats = mergemod.update(repo, ctx.node(), True, True, False,
218 ctx.p1().node())
218 ctx.p1().node())
219 finally:
219 finally:
220 repo.ui.setconfig('ui', 'forcemerge', '')
220 repo.ui.setconfig('ui', 'forcemerge', '')
221 repo.setparents(wcpar, node.nullid)
221 repo.setparents(wcpar, node.nullid)
222 repo.dirstate.write()
222 repo.dirstate.write()
223 # fix up dirstate for copies and renames
223 # fix up dirstate for copies and renames
224 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
224 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
225 return stats
225 return stats
226
226
227 def collapse(repo, first, last, commitopts):
227 def collapse(repo, first, last, commitopts):
228 """collapse the set of revisions from first to last as new one.
228 """collapse the set of revisions from first to last as new one.
229
229
230 Expected commit options are:
230 Expected commit options are:
231 - message
231 - message
232 - date
232 - date
233 - username
233 - username
234 Commit message is edited in all cases.
234 Commit message is edited in all cases.
235
235
236 This function works in memory."""
236 This function works in memory."""
237 ctxs = list(repo.set('%d::%d', first, last))
237 ctxs = list(repo.set('%d::%d', first, last))
238 if not ctxs:
238 if not ctxs:
239 return None
239 return None
240 base = first.parents()[0]
240 base = first.parents()[0]
241
241
242 # commit a new version of the old changeset, including the update
242 # commit a new version of the old changeset, including the update
243 # collect all files which might be affected
243 # collect all files which might be affected
244 files = set()
244 files = set()
245 for ctx in ctxs:
245 for ctx in ctxs:
246 files.update(ctx.files())
246 files.update(ctx.files())
247
247
248 # Recompute copies (avoid recording a -> b -> a)
248 # Recompute copies (avoid recording a -> b -> a)
249 copied = copies.pathcopies(first, last)
249 copied = copies.pathcopies(first, last)
250
250
251 # prune files which were reverted by the updates
251 # prune files which were reverted by the updates
252 def samefile(f):
252 def samefile(f):
253 if f in last.manifest():
253 if f in last.manifest():
254 a = last.filectx(f)
254 a = last.filectx(f)
255 if f in base.manifest():
255 if f in base.manifest():
256 b = base.filectx(f)
256 b = base.filectx(f)
257 return (a.data() == b.data()
257 return (a.data() == b.data()
258 and a.flags() == b.flags())
258 and a.flags() == b.flags())
259 else:
259 else:
260 return False
260 return False
261 else:
261 else:
262 return f not in base.manifest()
262 return f not in base.manifest()
263 files = [f for f in files if not samefile(f)]
263 files = [f for f in files if not samefile(f)]
264 # commit version of these files as defined by head
264 # commit version of these files as defined by head
265 headmf = last.manifest()
265 headmf = last.manifest()
266 def filectxfn(repo, ctx, path):
266 def filectxfn(repo, ctx, path):
267 if path in headmf:
267 if path in headmf:
268 fctx = last[path]
268 fctx = last[path]
269 flags = fctx.flags()
269 flags = fctx.flags()
270 mctx = context.memfilectx(fctx.path(), fctx.data(),
270 mctx = context.memfilectx(fctx.path(), fctx.data(),
271 islink='l' in flags,
271 islink='l' in flags,
272 isexec='x' in flags,
272 isexec='x' in flags,
273 copied=copied.get(path))
273 copied=copied.get(path))
274 return mctx
274 return mctx
275 raise IOError()
275 raise IOError()
276
276
277 if commitopts.get('message'):
277 if commitopts.get('message'):
278 message = commitopts['message']
278 message = commitopts['message']
279 else:
279 else:
280 message = first.description()
280 message = first.description()
281 user = commitopts.get('user')
281 user = commitopts.get('user')
282 date = commitopts.get('date')
282 date = commitopts.get('date')
283 extra = commitopts.get('extra')
283 extra = commitopts.get('extra')
284
284
285 parents = (first.p1().node(), first.p2().node())
285 parents = (first.p1().node(), first.p2().node())
286 new = context.memctx(repo,
286 new = context.memctx(repo,
287 parents=parents,
287 parents=parents,
288 text=message,
288 text=message,
289 files=files,
289 files=files,
290 filectxfn=filectxfn,
290 filectxfn=filectxfn,
291 user=user,
291 user=user,
292 date=date,
292 date=date,
293 extra=extra)
293 extra=extra)
294 new._text = cmdutil.commitforceeditor(repo, new, [])
294 new._text = cmdutil.commitforceeditor(repo, new, [])
295 return repo.commitctx(new)
295 return repo.commitctx(new)
296
296
297 def pick(ui, repo, ctx, ha, opts):
297 def pick(ui, repo, ctx, ha, opts):
298 oldctx = repo[ha]
298 oldctx = repo[ha]
299 if oldctx.parents()[0] == ctx:
299 if oldctx.parents()[0] == ctx:
300 ui.debug('node %s unchanged\n' % ha)
300 ui.debug('node %s unchanged\n' % ha)
301 return oldctx, []
301 return oldctx, []
302 hg.update(repo, ctx.node())
302 hg.update(repo, ctx.node())
303 stats = applychanges(ui, repo, oldctx, opts)
303 stats = applychanges(ui, repo, oldctx, opts)
304 if stats and stats[3] > 0:
304 if stats and stats[3] > 0:
305 raise error.InterventionRequired(_('Fix up the change and run '
305 raise error.InterventionRequired(_('Fix up the change and run '
306 'hg histedit --continue'))
306 'hg histedit --continue'))
307 # drop the second merge parent
307 # drop the second merge parent
308 commit = commitfuncfor(repo, oldctx)
308 commit = commitfuncfor(repo, oldctx)
309 n = commit(text=oldctx.description(), user=oldctx.user(),
309 n = commit(text=oldctx.description(), user=oldctx.user(),
310 date=oldctx.date(), extra=oldctx.extra())
310 date=oldctx.date(), extra=oldctx.extra())
311 if n is None:
311 if n is None:
312 ui.warn(_('%s: empty changeset\n')
312 ui.warn(_('%s: empty changeset\n')
313 % node.hex(ha))
313 % node.hex(ha))
314 return ctx, []
314 return ctx, []
315 new = repo[n]
315 new = repo[n]
316 return new, [(oldctx.node(), (n,))]
316 return new, [(oldctx.node(), (n,))]
317
317
318
318
319 def edit(ui, repo, ctx, ha, opts):
319 def edit(ui, repo, ctx, ha, opts):
320 oldctx = repo[ha]
320 oldctx = repo[ha]
321 hg.update(repo, ctx.node())
321 hg.update(repo, ctx.node())
322 applychanges(ui, repo, oldctx, opts)
322 applychanges(ui, repo, oldctx, opts)
323 raise error.InterventionRequired(
323 raise error.InterventionRequired(
324 _('Make changes as needed, you may commit or record as needed now.\n'
324 _('Make changes as needed, you may commit or record as needed now.\n'
325 'When you are finished, run hg histedit --continue to resume.'))
325 'When you are finished, run hg histedit --continue to resume.'))
326
326
327 def fold(ui, repo, ctx, ha, opts):
327 def fold(ui, repo, ctx, ha, opts):
328 oldctx = repo[ha]
328 oldctx = repo[ha]
329 hg.update(repo, ctx.node())
329 hg.update(repo, ctx.node())
330 stats = applychanges(ui, repo, oldctx, opts)
330 stats = applychanges(ui, repo, oldctx, opts)
331 if stats and stats[3] > 0:
331 if stats and stats[3] > 0:
332 raise error.InterventionRequired(
332 raise error.InterventionRequired(
333 _('Fix up the change and run hg histedit --continue'))
333 _('Fix up the change and run hg histedit --continue'))
334 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
334 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
335 date=oldctx.date(), extra=oldctx.extra())
335 date=oldctx.date(), extra=oldctx.extra())
336 if n is None:
336 if n is None:
337 ui.warn(_('%s: empty changeset')
337 ui.warn(_('%s: empty changeset')
338 % node.hex(ha))
338 % node.hex(ha))
339 return ctx, []
339 return ctx, []
340 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
340 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
341
341
342 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
342 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
343 parent = ctx.parents()[0].node()
343 parent = ctx.parents()[0].node()
344 hg.update(repo, parent)
344 hg.update(repo, parent)
345 ### prepare new commit data
345 ### prepare new commit data
346 commitopts = opts.copy()
346 commitopts = opts.copy()
347 # username
347 # username
348 if ctx.user() == oldctx.user():
348 if ctx.user() == oldctx.user():
349 username = ctx.user()
349 username = ctx.user()
350 else:
350 else:
351 username = ui.username()
351 username = ui.username()
352 commitopts['user'] = username
352 commitopts['user'] = username
353 # commit message
353 # commit message
354 newmessage = '\n***\n'.join(
354 newmessage = '\n***\n'.join(
355 [ctx.description()] +
355 [ctx.description()] +
356 [repo[r].description() for r in internalchanges] +
356 [repo[r].description() for r in internalchanges] +
357 [oldctx.description()]) + '\n'
357 [oldctx.description()]) + '\n'
358 commitopts['message'] = newmessage
358 commitopts['message'] = newmessage
359 # date
359 # date
360 commitopts['date'] = max(ctx.date(), oldctx.date())
360 commitopts['date'] = max(ctx.date(), oldctx.date())
361 extra = ctx.extra().copy()
361 extra = ctx.extra().copy()
362 # histedit_source
362 # histedit_source
363 # note: ctx is likely a temporary commit but that the best we can do here
363 # note: ctx is likely a temporary commit but that the best we can do here
364 # This is sufficient to solve issue3681 anyway
364 # This is sufficient to solve issue3681 anyway
365 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
365 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
366 commitopts['extra'] = extra
366 commitopts['extra'] = extra
367 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
367 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
368 try:
368 try:
369 phasemin = max(ctx.phase(), oldctx.phase())
369 phasemin = max(ctx.phase(), oldctx.phase())
370 repo.ui.setconfig('phases', 'new-commit', phasemin)
370 repo.ui.setconfig('phases', 'new-commit', phasemin)
371 n = collapse(repo, ctx, repo[newnode], commitopts)
371 n = collapse(repo, ctx, repo[newnode], commitopts)
372 finally:
372 finally:
373 repo.ui.restoreconfig(phasebackup)
373 repo.ui.restoreconfig(phasebackup)
374 if n is None:
374 if n is None:
375 return ctx, []
375 return ctx, []
376 hg.update(repo, n)
376 hg.update(repo, n)
377 replacements = [(oldctx.node(), (newnode,)),
377 replacements = [(oldctx.node(), (newnode,)),
378 (ctx.node(), (n,)),
378 (ctx.node(), (n,)),
379 (newnode, (n,)),
379 (newnode, (n,)),
380 ]
380 ]
381 for ich in internalchanges:
381 for ich in internalchanges:
382 replacements.append((ich, (n,)))
382 replacements.append((ich, (n,)))
383 return repo[n], replacements
383 return repo[n], replacements
384
384
385 def drop(ui, repo, ctx, ha, opts):
385 def drop(ui, repo, ctx, ha, opts):
386 return ctx, [(repo[ha].node(), ())]
386 return ctx, [(repo[ha].node(), ())]
387
387
388
388
389 def message(ui, repo, ctx, ha, opts):
389 def message(ui, repo, ctx, ha, opts):
390 oldctx = repo[ha]
390 oldctx = repo[ha]
391 hg.update(repo, ctx.node())
391 hg.update(repo, ctx.node())
392 stats = applychanges(ui, repo, oldctx, opts)
392 stats = applychanges(ui, repo, oldctx, opts)
393 if stats and stats[3] > 0:
393 if stats and stats[3] > 0:
394 raise error.InterventionRequired(
394 raise error.InterventionRequired(
395 _('Fix up the change and run hg histedit --continue'))
395 _('Fix up the change and run hg histedit --continue'))
396 message = oldctx.description() + '\n'
396 message = oldctx.description() + '\n'
397 message = ui.edit(message, ui.username())
397 message = ui.edit(message, ui.username())
398 commit = commitfuncfor(repo, oldctx)
398 commit = commitfuncfor(repo, oldctx)
399 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
399 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
400 extra=oldctx.extra())
400 extra=oldctx.extra())
401 newctx = repo[new]
401 newctx = repo[new]
402 if oldctx.node() != newctx.node():
402 if oldctx.node() != newctx.node():
403 return newctx, [(oldctx.node(), (new,))]
403 return newctx, [(oldctx.node(), (new,))]
404 # We didn't make an edit, so just indicate no replaced nodes
404 # We didn't make an edit, so just indicate no replaced nodes
405 return newctx, []
405 return newctx, []
406
406
407 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
408 """utility function to find the first outgoing changeset
409
410 Used by initialisation code"""
411 dest = ui.expandpath(remote or 'default-push', remote or 'default')
412 dest, revs = hg.parseurl(dest, None)[:2]
413 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
414
415 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
416 other = hg.peer(repo, opts, dest)
417
418 if revs:
419 revs = [repo.lookup(rev) for rev in revs]
420
421 # hexlify nodes from outgoing, because we're going to parse
422 # parent[0] using revsingle below, and if the binary hash
423 # contains special revset characters like ":" the revset
424 # parser can choke.
425 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
426 if not outgoing.missing:
427 raise util.Abort(_('no outgoing ancestors'))
428 return outgoing.missing[0]
429
407 actiontable = {'p': pick,
430 actiontable = {'p': pick,
408 'pick': pick,
431 'pick': pick,
409 'e': edit,
432 'e': edit,
410 'edit': edit,
433 'edit': edit,
411 'f': fold,
434 'f': fold,
412 'fold': fold,
435 'fold': fold,
413 'd': drop,
436 'd': drop,
414 'drop': drop,
437 'drop': drop,
415 'm': message,
438 'm': message,
416 'mess': message,
439 'mess': message,
417 }
440 }
418
441
419 @command('histedit',
442 @command('histedit',
420 [('', 'commands', '',
443 [('', 'commands', '',
421 _('Read history edits from the specified file.')),
444 _('Read history edits from the specified file.')),
422 ('c', 'continue', False, _('continue an edit already in progress')),
445 ('c', 'continue', False, _('continue an edit already in progress')),
423 ('k', 'keep', False,
446 ('k', 'keep', False,
424 _("don't strip old nodes after edit is complete")),
447 _("don't strip old nodes after edit is complete")),
425 ('', 'abort', False, _('abort an edit in progress')),
448 ('', 'abort', False, _('abort an edit in progress')),
426 ('o', 'outgoing', False, _('changesets not found in destination')),
449 ('o', 'outgoing', False, _('changesets not found in destination')),
427 ('f', 'force', False,
450 ('f', 'force', False,
428 _('force outgoing even for unrelated repositories')),
451 _('force outgoing even for unrelated repositories')),
429 ('r', 'rev', [], _('first revision to be edited'))],
452 ('r', 'rev', [], _('first revision to be edited'))],
430 _("[PARENT]"))
453 _("[PARENT]"))
431 def histedit(ui, repo, *freeargs, **opts):
454 def histedit(ui, repo, *freeargs, **opts):
432 """interactively edit changeset history
455 """interactively edit changeset history
433 """
456 """
434 # TODO only abort if we try and histedit mq patches, not just
457 # TODO only abort if we try and histedit mq patches, not just
435 # blanket if mq patches are applied somewhere
458 # blanket if mq patches are applied somewhere
436 mq = getattr(repo, 'mq', None)
459 mq = getattr(repo, 'mq', None)
437 if mq and mq.applied:
460 if mq and mq.applied:
438 raise util.Abort(_('source has mq patches applied'))
461 raise util.Abort(_('source has mq patches applied'))
439
462
440 # basic argument incompatibility processing
463 # basic argument incompatibility processing
441 outg = opts.get('outgoing')
464 outg = opts.get('outgoing')
442 cont = opts.get('continue')
465 cont = opts.get('continue')
443 abort = opts.get('abort')
466 abort = opts.get('abort')
444 force = opts.get('force')
467 force = opts.get('force')
445 rules = opts.get('commands', '')
468 rules = opts.get('commands', '')
446 revs = opts.get('rev', [])
469 revs = opts.get('rev', [])
447 goal = 'new' # This invocation goal, in new, continue, abort
470 goal = 'new' # This invocation goal, in new, continue, abort
448 if force and not outg:
471 if force and not outg:
449 raise util.Abort(_('--force only allowed with --outgoing'))
472 raise util.Abort(_('--force only allowed with --outgoing'))
450 if cont:
473 if cont:
451 if util.any((outg, abort, revs, freeargs, rules)):
474 if util.any((outg, abort, revs, freeargs, rules)):
452 raise util.Abort(_('no arguments allowed with --continue'))
475 raise util.Abort(_('no arguments allowed with --continue'))
453 goal = 'continue'
476 goal = 'continue'
454 elif abort:
477 elif abort:
455 if util.any((outg, revs, freeargs, rules)):
478 if util.any((outg, revs, freeargs, rules)):
456 raise util.Abort(_('no arguments allowed with --abort'))
479 raise util.Abort(_('no arguments allowed with --abort'))
457 goal = 'abort'
480 goal = 'abort'
458 else:
481 else:
459 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
482 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
460 raise util.Abort(_('history edit already in progress, try '
483 raise util.Abort(_('history edit already in progress, try '
461 '--continue or --abort'))
484 '--continue or --abort'))
462 if outg:
485 if outg:
463 if revs:
486 if revs:
464 raise util.Abort(_('no revisions allowed with --outgoing'))
487 raise util.Abort(_('no revisions allowed with --outgoing'))
465 if len(freeargs) > 1:
488 if len(freeargs) > 1:
466 raise util.Abort(
489 raise util.Abort(
467 _('only one repo argument allowed with --outgoing'))
490 _('only one repo argument allowed with --outgoing'))
468 else:
491 else:
469 parent = list(freeargs) + opts.get('rev', [])
492 revs.extend(freeargs)
470 if len(parent) != 1:
493 if len(revs) != 1:
471 raise util.Abort(
494 raise util.Abort(
472 _('histedit requires exactly one parent revision'))
495 _('histedit requires exactly one parent revision'))
473
496
474 if opts.get('outgoing'):
475 if freeargs:
476 parent = freeargs[0]
477
478 dest = ui.expandpath(parent or 'default-push', parent or 'default')
479 dest, revs = hg.parseurl(dest, None)[:2]
480 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
481
482 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
483 other = hg.peer(repo, opts, dest)
484
485 if revs:
486 revs = [repo.lookup(rev) for rev in revs]
487
488 # hexlify nodes from outgoing, because we're going to parse
489 # parent[0] using revsingle below, and if the binary hash
490 # contains special revset characters like ":" the revset
491 # parser can choke.
492 parent = [node.hex(n) for n in discovery.findcommonoutgoing(
493 repo, other, revs, force=force).missing[0:1]]
494 if not parent:
495 raise util.Abort(_('no outgoing ancestors'))
496
497
497 if goal == 'continue':
498 if goal == 'continue':
498 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
499 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
499 currentparent, wantnull = repo.dirstate.parents()
500 currentparent, wantnull = repo.dirstate.parents()
500 parentctx = repo[parentctxnode]
501 parentctx = repo[parentctxnode]
501 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
502 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
502 replacements.extend(repl)
503 replacements.extend(repl)
503 elif goal == 'abort':
504 elif goal == 'abort':
504 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
505 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
505 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
506 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
506 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
507 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
507 hg.clean(repo, topmost)
508 hg.clean(repo, topmost)
508 cleanupnode(ui, repo, 'created', tmpnodes)
509 cleanupnode(ui, repo, 'created', tmpnodes)
509 cleanupnode(ui, repo, 'temp', leafs)
510 cleanupnode(ui, repo, 'temp', leafs)
510 os.unlink(os.path.join(repo.path, 'histedit-state'))
511 os.unlink(os.path.join(repo.path, 'histedit-state'))
511 return
512 return
512 else:
513 else:
513 cmdutil.bailifchanged(repo)
514 cmdutil.bailifchanged(repo)
514
515
515 topmost, empty = repo.dirstate.parents()
516 topmost, empty = repo.dirstate.parents()
516
517 if outg:
517 parent = scmutil.revsingle(repo, parent[0]).node()
518 if freeargs:
519 remote = freeargs[0]
520 else:
521 remote = None
522 root = findoutgoing(ui, repo, remote, force, opts)
523 else:
524 root = revs[0]
525 root = scmutil.revsingle(repo, root).node()
518
526
519 keep = opts.get('keep', False)
527 keep = opts.get('keep', False)
520 revs = between(repo, parent, topmost, keep)
528 revs = between(repo, root, topmost, keep)
521 if not revs:
529 if not revs:
522 raise util.Abort(_('%s is not an ancestor of working directory') %
530 raise util.Abort(_('%s is not an ancestor of working directory') %
523 node.short(parent))
531 node.short(root))
524
532
525 ctxs = [repo[r] for r in revs]
533 ctxs = [repo[r] for r in revs]
526 if not rules:
534 if not rules:
527 rules = '\n'.join([makedesc(c) for c in ctxs])
535 rules = '\n'.join([makedesc(c) for c in ctxs])
528 rules += '\n\n'
536 rules += '\n\n'
529 rules += editcomment % (node.short(parent), node.short(topmost))
537 rules += editcomment % (node.short(root), node.short(topmost))
530 rules = ui.edit(rules, ui.username())
538 rules = ui.edit(rules, ui.username())
531 # Save edit rules in .hg/histedit-last-edit.txt in case
539 # Save edit rules in .hg/histedit-last-edit.txt in case
532 # the user needs to ask for help after something
540 # the user needs to ask for help after something
533 # surprising happens.
541 # surprising happens.
534 f = open(repo.join('histedit-last-edit.txt'), 'w')
542 f = open(repo.join('histedit-last-edit.txt'), 'w')
535 f.write(rules)
543 f.write(rules)
536 f.close()
544 f.close()
537 else:
545 else:
538 if rules == '-':
546 if rules == '-':
539 f = sys.stdin
547 f = sys.stdin
540 else:
548 else:
541 f = open(rules)
549 f = open(rules)
542 rules = f.read()
550 rules = f.read()
543 f.close()
551 f.close()
544 rules = [l for l in (r.strip() for r in rules.splitlines())
552 rules = [l for l in (r.strip() for r in rules.splitlines())
545 if l and not l[0] == '#']
553 if l and not l[0] == '#']
546 rules = verifyrules(rules, repo, ctxs)
554 rules = verifyrules(rules, repo, ctxs)
547
555
548 parentctx = repo[parent].parents()[0]
556 parentctx = repo[root].parents()[0]
549 keep = opts.get('keep', False)
557 keep = opts.get('keep', False)
550 replacements = []
558 replacements = []
551
559
552
560
553 while rules:
561 while rules:
554 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
562 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
555 action, ha = rules.pop(0)
563 action, ha = rules.pop(0)
556 ui.debug('histedit: processing %s %s\n' % (action, ha))
564 ui.debug('histedit: processing %s %s\n' % (action, ha))
557 actfunc = actiontable[action]
565 actfunc = actiontable[action]
558 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
566 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
559 replacements.extend(replacement_)
567 replacements.extend(replacement_)
560
568
561 hg.update(repo, parentctx.node())
569 hg.update(repo, parentctx.node())
562
570
563 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
571 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
564 if mapping:
572 if mapping:
565 for prec, succs in mapping.iteritems():
573 for prec, succs in mapping.iteritems():
566 if not succs:
574 if not succs:
567 ui.debug('histedit: %s is dropped\n' % node.short(prec))
575 ui.debug('histedit: %s is dropped\n' % node.short(prec))
568 else:
576 else:
569 ui.debug('histedit: %s is replaced by %s\n' % (
577 ui.debug('histedit: %s is replaced by %s\n' % (
570 node.short(prec), node.short(succs[0])))
578 node.short(prec), node.short(succs[0])))
571 if len(succs) > 1:
579 if len(succs) > 1:
572 m = 'histedit: %s'
580 m = 'histedit: %s'
573 for n in succs[1:]:
581 for n in succs[1:]:
574 ui.debug(m % node.short(n))
582 ui.debug(m % node.short(n))
575
583
576 if not keep:
584 if not keep:
577 if mapping:
585 if mapping:
578 movebookmarks(ui, repo, mapping, topmost, ntm)
586 movebookmarks(ui, repo, mapping, topmost, ntm)
579 # TODO update mq state
587 # TODO update mq state
580 if obsolete._enabled:
588 if obsolete._enabled:
581 markers = []
589 markers = []
582 # sort by revision number because it sound "right"
590 # sort by revision number because it sound "right"
583 for prec in sorted(mapping, key=repo.changelog.rev):
591 for prec in sorted(mapping, key=repo.changelog.rev):
584 succs = mapping[prec]
592 succs = mapping[prec]
585 markers.append((repo[prec],
593 markers.append((repo[prec],
586 tuple(repo[s] for s in succs)))
594 tuple(repo[s] for s in succs)))
587 if markers:
595 if markers:
588 obsolete.createmarkers(repo, markers)
596 obsolete.createmarkers(repo, markers)
589 else:
597 else:
590 cleanupnode(ui, repo, 'replaced', mapping)
598 cleanupnode(ui, repo, 'replaced', mapping)
591
599
592 cleanupnode(ui, repo, 'temp', tmpnodes)
600 cleanupnode(ui, repo, 'temp', tmpnodes)
593 os.unlink(os.path.join(repo.path, 'histedit-state'))
601 os.unlink(os.path.join(repo.path, 'histedit-state'))
594 if os.path.exists(repo.sjoin('undo')):
602 if os.path.exists(repo.sjoin('undo')):
595 os.unlink(repo.sjoin('undo'))
603 os.unlink(repo.sjoin('undo'))
596
604
597
605
598 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
606 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
599 action, currentnode = rules.pop(0)
607 action, currentnode = rules.pop(0)
600 ctx = repo[currentnode]
608 ctx = repo[currentnode]
601 # is there any new commit between the expected parent and "."
609 # is there any new commit between the expected parent and "."
602 #
610 #
603 # note: does not take non linear new change in account (but previous
611 # note: does not take non linear new change in account (but previous
604 # implementation didn't used them anyway (issue3655)
612 # implementation didn't used them anyway (issue3655)
605 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
613 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
606 if parentctx.node() != node.nullid:
614 if parentctx.node() != node.nullid:
607 if not newchildren:
615 if not newchildren:
608 # `parentctxnode` should match but no result. This means that
616 # `parentctxnode` should match but no result. This means that
609 # currentnode is not a descendant from parentctxnode.
617 # currentnode is not a descendant from parentctxnode.
610 msg = _('%s is not an ancestor of working directory')
618 msg = _('%s is not an ancestor of working directory')
611 hint = _('update to %s or descendant and run "hg histedit '
619 hint = _('update to %s or descendant and run "hg histedit '
612 '--continue" again') % parentctx
620 '--continue" again') % parentctx
613 raise util.Abort(msg % parentctx, hint=hint)
621 raise util.Abort(msg % parentctx, hint=hint)
614 newchildren.pop(0) # remove parentctxnode
622 newchildren.pop(0) # remove parentctxnode
615 # Commit dirty working directory if necessary
623 # Commit dirty working directory if necessary
616 new = None
624 new = None
617 m, a, r, d = repo.status()[:4]
625 m, a, r, d = repo.status()[:4]
618 if m or a or r or d:
626 if m or a or r or d:
619 # prepare the message for the commit to comes
627 # prepare the message for the commit to comes
620 if action in ('f', 'fold'):
628 if action in ('f', 'fold'):
621 message = 'fold-temp-revision %s' % currentnode
629 message = 'fold-temp-revision %s' % currentnode
622 else:
630 else:
623 message = ctx.description() + '\n'
631 message = ctx.description() + '\n'
624 if action in ('e', 'edit', 'm', 'mess'):
632 if action in ('e', 'edit', 'm', 'mess'):
625 editor = cmdutil.commitforceeditor
633 editor = cmdutil.commitforceeditor
626 else:
634 else:
627 editor = False
635 editor = False
628 commit = commitfuncfor(repo, ctx)
636 commit = commitfuncfor(repo, ctx)
629 new = commit(text=message, user=ctx.user(),
637 new = commit(text=message, user=ctx.user(),
630 date=ctx.date(), extra=ctx.extra(),
638 date=ctx.date(), extra=ctx.extra(),
631 editor=editor)
639 editor=editor)
632 if new is not None:
640 if new is not None:
633 newchildren.append(new)
641 newchildren.append(new)
634
642
635 replacements = []
643 replacements = []
636 # track replacements
644 # track replacements
637 if ctx.node() not in newchildren:
645 if ctx.node() not in newchildren:
638 # note: new children may be empty when the changeset is dropped.
646 # note: new children may be empty when the changeset is dropped.
639 # this happen e.g during conflicting pick where we revert content
647 # this happen e.g during conflicting pick where we revert content
640 # to parent.
648 # to parent.
641 replacements.append((ctx.node(), tuple(newchildren)))
649 replacements.append((ctx.node(), tuple(newchildren)))
642
650
643 if action in ('f', 'fold'):
651 if action in ('f', 'fold'):
644 if newchildren:
652 if newchildren:
645 # finalize fold operation if applicable
653 # finalize fold operation if applicable
646 if new is None:
654 if new is None:
647 new = newchildren[-1]
655 new = newchildren[-1]
648 else:
656 else:
649 newchildren.pop() # remove new from internal changes
657 newchildren.pop() # remove new from internal changes
650 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
658 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
651 newchildren)
659 newchildren)
652 replacements.extend(repl)
660 replacements.extend(repl)
653 else:
661 else:
654 # newchildren is empty if the fold did not result in any commit
662 # newchildren is empty if the fold did not result in any commit
655 # this happen when all folded change are discarded during the
663 # this happen when all folded change are discarded during the
656 # merge.
664 # merge.
657 replacements.append((ctx.node(), (parentctx.node(),)))
665 replacements.append((ctx.node(), (parentctx.node(),)))
658 elif newchildren:
666 elif newchildren:
659 # otherwise update "parentctx" before proceeding to further operation
667 # otherwise update "parentctx" before proceeding to further operation
660 parentctx = repo[newchildren[-1]]
668 parentctx = repo[newchildren[-1]]
661 return parentctx, replacements
669 return parentctx, replacements
662
670
663
671
664 def between(repo, old, new, keep):
672 def between(repo, old, new, keep):
665 """select and validate the set of revision to edit
673 """select and validate the set of revision to edit
666
674
667 When keep is false, the specified set can't have children."""
675 When keep is false, the specified set can't have children."""
668 ctxs = list(repo.set('%n::%n', old, new))
676 ctxs = list(repo.set('%n::%n', old, new))
669 if ctxs and not keep:
677 if ctxs and not keep:
670 if (not obsolete._enabled and
678 if (not obsolete._enabled and
671 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
679 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
672 raise util.Abort(_('cannot edit history that would orphan nodes'))
680 raise util.Abort(_('cannot edit history that would orphan nodes'))
673 root = ctxs[0] # list is already sorted by repo.set
681 root = ctxs[0] # list is already sorted by repo.set
674 if not root.phase():
682 if not root.phase():
675 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
683 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
676 return [c.node() for c in ctxs]
684 return [c.node() for c in ctxs]
677
685
678
686
679 def writestate(repo, parentnode, rules, keep, topmost, replacements):
687 def writestate(repo, parentnode, rules, keep, topmost, replacements):
680 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
688 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
681 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
689 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
682 fp.close()
690 fp.close()
683
691
684 def readstate(repo):
692 def readstate(repo):
685 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
693 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
686 """
694 """
687 fp = open(os.path.join(repo.path, 'histedit-state'))
695 fp = open(os.path.join(repo.path, 'histedit-state'))
688 return pickle.load(fp)
696 return pickle.load(fp)
689
697
690
698
691 def makedesc(c):
699 def makedesc(c):
692 """build a initial action line for a ctx `c`
700 """build a initial action line for a ctx `c`
693
701
694 line are in the form:
702 line are in the form:
695
703
696 pick <hash> <rev> <summary>
704 pick <hash> <rev> <summary>
697 """
705 """
698 summary = ''
706 summary = ''
699 if c.description():
707 if c.description():
700 summary = c.description().splitlines()[0]
708 summary = c.description().splitlines()[0]
701 line = 'pick %s %d %s' % (c, c.rev(), summary)
709 line = 'pick %s %d %s' % (c, c.rev(), summary)
702 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
710 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
703
711
704 def verifyrules(rules, repo, ctxs):
712 def verifyrules(rules, repo, ctxs):
705 """Verify that there exists exactly one edit rule per given changeset.
713 """Verify that there exists exactly one edit rule per given changeset.
706
714
707 Will abort if there are to many or too few rules, a malformed rule,
715 Will abort if there are to many or too few rules, a malformed rule,
708 or a rule on a changeset outside of the user-given range.
716 or a rule on a changeset outside of the user-given range.
709 """
717 """
710 parsed = []
718 parsed = []
711 if len(rules) != len(ctxs):
719 if len(rules) != len(ctxs):
712 raise util.Abort(_('must specify a rule for each changeset once'))
720 raise util.Abort(_('must specify a rule for each changeset once'))
713 for r in rules:
721 for r in rules:
714 if ' ' not in r:
722 if ' ' not in r:
715 raise util.Abort(_('malformed line "%s"') % r)
723 raise util.Abort(_('malformed line "%s"') % r)
716 action, rest = r.split(' ', 1)
724 action, rest = r.split(' ', 1)
717 if ' ' in rest.strip():
725 if ' ' in rest.strip():
718 ha, rest = rest.split(' ', 1)
726 ha, rest = rest.split(' ', 1)
719 else:
727 else:
720 ha = r.strip()
728 ha = r.strip()
721 try:
729 try:
722 if repo[ha] not in ctxs:
730 if repo[ha] not in ctxs:
723 raise util.Abort(
731 raise util.Abort(
724 _('may not use changesets other than the ones listed'))
732 _('may not use changesets other than the ones listed'))
725 except error.RepoError:
733 except error.RepoError:
726 raise util.Abort(_('unknown changeset %s listed') % ha)
734 raise util.Abort(_('unknown changeset %s listed') % ha)
727 if action not in actiontable:
735 if action not in actiontable:
728 raise util.Abort(_('unknown action "%s"') % action)
736 raise util.Abort(_('unknown action "%s"') % action)
729 parsed.append([action, ha])
737 parsed.append([action, ha])
730 return parsed
738 return parsed
731
739
732 def processreplacement(repo, replacements):
740 def processreplacement(repo, replacements):
733 """process the list of replacements to return
741 """process the list of replacements to return
734
742
735 1) the final mapping between original and created nodes
743 1) the final mapping between original and created nodes
736 2) the list of temporary node created by histedit
744 2) the list of temporary node created by histedit
737 3) the list of new commit created by histedit"""
745 3) the list of new commit created by histedit"""
738 allsuccs = set()
746 allsuccs = set()
739 replaced = set()
747 replaced = set()
740 fullmapping = {}
748 fullmapping = {}
741 # initialise basic set
749 # initialise basic set
742 # fullmapping record all operation recorded in replacement
750 # fullmapping record all operation recorded in replacement
743 for rep in replacements:
751 for rep in replacements:
744 allsuccs.update(rep[1])
752 allsuccs.update(rep[1])
745 replaced.add(rep[0])
753 replaced.add(rep[0])
746 fullmapping.setdefault(rep[0], set()).update(rep[1])
754 fullmapping.setdefault(rep[0], set()).update(rep[1])
747 new = allsuccs - replaced
755 new = allsuccs - replaced
748 tmpnodes = allsuccs & replaced
756 tmpnodes = allsuccs & replaced
749 # Reduce content fullmapping into direct relation between original nodes
757 # Reduce content fullmapping into direct relation between original nodes
750 # and final node created during history edition
758 # and final node created during history edition
751 # Dropped changeset are replaced by an empty list
759 # Dropped changeset are replaced by an empty list
752 toproceed = set(fullmapping)
760 toproceed = set(fullmapping)
753 final = {}
761 final = {}
754 while toproceed:
762 while toproceed:
755 for x in list(toproceed):
763 for x in list(toproceed):
756 succs = fullmapping[x]
764 succs = fullmapping[x]
757 for s in list(succs):
765 for s in list(succs):
758 if s in toproceed:
766 if s in toproceed:
759 # non final node with unknown closure
767 # non final node with unknown closure
760 # We can't process this now
768 # We can't process this now
761 break
769 break
762 elif s in final:
770 elif s in final:
763 # non final node, replace with closure
771 # non final node, replace with closure
764 succs.remove(s)
772 succs.remove(s)
765 succs.update(final[s])
773 succs.update(final[s])
766 else:
774 else:
767 final[x] = succs
775 final[x] = succs
768 toproceed.remove(x)
776 toproceed.remove(x)
769 # remove tmpnodes from final mapping
777 # remove tmpnodes from final mapping
770 for n in tmpnodes:
778 for n in tmpnodes:
771 del final[n]
779 del final[n]
772 # we expect all changes involved in final to exist in the repo
780 # we expect all changes involved in final to exist in the repo
773 # turn `final` into list (topologically sorted)
781 # turn `final` into list (topologically sorted)
774 nm = repo.changelog.nodemap
782 nm = repo.changelog.nodemap
775 for prec, succs in final.items():
783 for prec, succs in final.items():
776 final[prec] = sorted(succs, key=nm.get)
784 final[prec] = sorted(succs, key=nm.get)
777
785
778 # computed topmost element (necessary for bookmark)
786 # computed topmost element (necessary for bookmark)
779 if new:
787 if new:
780 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
788 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
781 elif not final:
789 elif not final:
782 # Nothing rewritten at all. we won't need `newtopmost`
790 # Nothing rewritten at all. we won't need `newtopmost`
783 # It is the same as `oldtopmost` and `processreplacement` know it
791 # It is the same as `oldtopmost` and `processreplacement` know it
784 newtopmost = None
792 newtopmost = None
785 else:
793 else:
786 # every body died. The newtopmost is the parent of the root.
794 # every body died. The newtopmost is the parent of the root.
787 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
795 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
788
796
789 return final, tmpnodes, new, newtopmost
797 return final, tmpnodes, new, newtopmost
790
798
791 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
799 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
792 """Move bookmark from old to newly created node"""
800 """Move bookmark from old to newly created node"""
793 if not mapping:
801 if not mapping:
794 # if nothing got rewritten there is not purpose for this function
802 # if nothing got rewritten there is not purpose for this function
795 return
803 return
796 moves = []
804 moves = []
797 for bk, old in sorted(repo._bookmarks.iteritems()):
805 for bk, old in sorted(repo._bookmarks.iteritems()):
798 if old == oldtopmost:
806 if old == oldtopmost:
799 # special case ensure bookmark stay on tip.
807 # special case ensure bookmark stay on tip.
800 #
808 #
801 # This is arguably a feature and we may only want that for the
809 # This is arguably a feature and we may only want that for the
802 # active bookmark. But the behavior is kept compatible with the old
810 # active bookmark. But the behavior is kept compatible with the old
803 # version for now.
811 # version for now.
804 moves.append((bk, newtopmost))
812 moves.append((bk, newtopmost))
805 continue
813 continue
806 base = old
814 base = old
807 new = mapping.get(base, None)
815 new = mapping.get(base, None)
808 if new is None:
816 if new is None:
809 continue
817 continue
810 while not new:
818 while not new:
811 # base is killed, trying with parent
819 # base is killed, trying with parent
812 base = repo[base].p1().node()
820 base = repo[base].p1().node()
813 new = mapping.get(base, (base,))
821 new = mapping.get(base, (base,))
814 # nothing to move
822 # nothing to move
815 moves.append((bk, new[-1]))
823 moves.append((bk, new[-1]))
816 if moves:
824 if moves:
817 marks = repo._bookmarks
825 marks = repo._bookmarks
818 for mark, new in moves:
826 for mark, new in moves:
819 old = marks[mark]
827 old = marks[mark]
820 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
828 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
821 % (mark, node.short(old), node.short(new)))
829 % (mark, node.short(old), node.short(new)))
822 marks[mark] = new
830 marks[mark] = new
823 marks.write()
831 marks.write()
824
832
825 def cleanupnode(ui, repo, name, nodes):
833 def cleanupnode(ui, repo, name, nodes):
826 """strip a group of nodes from the repository
834 """strip a group of nodes from the repository
827
835
828 The set of node to strip may contains unknown nodes."""
836 The set of node to strip may contains unknown nodes."""
829 ui.debug('should strip %s nodes %s\n' %
837 ui.debug('should strip %s nodes %s\n' %
830 (name, ', '.join([node.short(n) for n in nodes])))
838 (name, ', '.join([node.short(n) for n in nodes])))
831 lock = None
839 lock = None
832 try:
840 try:
833 lock = repo.lock()
841 lock = repo.lock()
834 # Find all node that need to be stripped
842 # Find all node that need to be stripped
835 # (we hg %lr instead of %ln to silently ignore unknown item
843 # (we hg %lr instead of %ln to silently ignore unknown item
836 nm = repo.changelog.nodemap
844 nm = repo.changelog.nodemap
837 nodes = [n for n in nodes if n in nm]
845 nodes = [n for n in nodes if n in nm]
838 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
846 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
839 for c in roots:
847 for c in roots:
840 # We should process node in reverse order to strip tip most first.
848 # We should process node in reverse order to strip tip most first.
841 # but this trigger a bug in changegroup hook.
849 # but this trigger a bug in changegroup hook.
842 # This would reduce bundle overhead
850 # This would reduce bundle overhead
843 repair.strip(ui, repo, c)
851 repair.strip(ui, repo, c)
844 finally:
852 finally:
845 lockmod.release(lock)
853 lockmod.release(lock)
General Comments 0
You need to be logged in to leave comments. Login now