##// END OF EJS Templates
histedit, i18n: replace '+' with concatenation to make hggettext happy
Wagner Bruna -
r21175:dae36d3e stable
parent child Browse files
Show More
@@ -1,930 +1,930 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 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # d, drop = remove commit from history
39 # d, drop = remove commit from history
40 # m, mess = edit message without changing commit content
40 # m, mess = edit message without changing commit content
41 #
41 #
42
42
43 In this file, lines beginning with ``#`` are ignored. You must specify a rule
43 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 for each revision in your history. For example, if you had meant to add gamma
44 for each revision in your history. For example, if you had meant to add gamma
45 before beta, and then wanted to add delta in the same revision as beta, you
45 before beta, and then wanted to add delta in the same revision as beta, you
46 would reorganize the file to look like this::
46 would reorganize the file to look like this::
47
47
48 pick 030b686bedc4 Add gamma
48 pick 030b686bedc4 Add gamma
49 pick c561b4e977df Add beta
49 pick c561b4e977df Add beta
50 fold 7c2fd3b9020c Add delta
50 fold 7c2fd3b9020c Add delta
51
51
52 # Edit history between c561b4e977df and 7c2fd3b9020c
52 # Edit history between c561b4e977df and 7c2fd3b9020c
53 #
53 #
54 # Commits are listed from least to most recent
54 # Commits are listed from least to most recent
55 #
55 #
56 # Commands:
56 # Commands:
57 # p, pick = use commit
57 # p, pick = use commit
58 # e, edit = use commit, but stop for amending
58 # e, edit = use commit, but stop for amending
59 # f, fold = use commit, but combine it with the one above
59 # f, fold = use commit, but combine it with the one above
60 # d, drop = remove commit from history
60 # d, drop = remove commit from history
61 # m, mess = edit message without changing commit content
61 # m, mess = edit message without changing commit content
62 #
62 #
63
63
64 At which point you close the editor and ``histedit`` starts working. When you
64 At which point you close the editor and ``histedit`` starts working. When you
65 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
65 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
66 those revisions together, offering you a chance to clean up the commit message::
66 those revisions together, offering you a chance to clean up the commit message::
67
67
68 Add beta
68 Add beta
69 ***
69 ***
70 Add delta
70 Add delta
71
71
72 Edit the commit message to your liking, then close the editor. For
72 Edit the commit message to your liking, then close the editor. For
73 this example, let's assume that the commit message was changed to
73 this example, let's assume that the commit message was changed to
74 ``Add beta and delta.`` After histedit has run and had a chance to
74 ``Add beta and delta.`` After histedit has run and had a chance to
75 remove any old or temporary revisions it needed, the history looks
75 remove any old or temporary revisions it needed, the history looks
76 like this::
76 like this::
77
77
78 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
78 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
79 | Add beta and delta.
79 | Add beta and delta.
80 |
80 |
81 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
81 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
82 | Add gamma
82 | Add gamma
83 |
83 |
84 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
84 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
85 Add alpha
85 Add alpha
86
86
87 Note that ``histedit`` does *not* remove any revisions (even its own temporary
87 Note that ``histedit`` does *not* remove any revisions (even its own temporary
88 ones) until after it has completed all the editing operations, so it will
88 ones) until after it has completed all the editing operations, so it will
89 probably perform several strip operations when it's done. For the above example,
89 probably perform several strip operations when it's done. For the above example,
90 it had to run strip twice. Strip can be slow depending on a variety of factors,
90 it had to run strip twice. Strip can be slow depending on a variety of factors,
91 so you might need to be a little patient. You can choose to keep the original
91 so you might need to be a little patient. You can choose to keep the original
92 revisions by passing the ``--keep`` flag.
92 revisions by passing the ``--keep`` flag.
93
93
94 The ``edit`` operation will drop you back to a command prompt,
94 The ``edit`` operation will drop you back to a command prompt,
95 allowing you to edit files freely, or even use ``hg record`` to commit
95 allowing you to edit files freely, or even use ``hg record`` to commit
96 some changes as a separate commit. When you're done, any remaining
96 some changes as a separate commit. When you're done, any remaining
97 uncommitted changes will be committed as well. When done, run ``hg
97 uncommitted changes will be committed as well. When done, run ``hg
98 histedit --continue`` to finish this step. You'll be prompted for a
98 histedit --continue`` to finish this step. You'll be prompted for a
99 new commit message, but the default commit message will be the
99 new commit message, but the default commit message will be the
100 original message for the ``edit`` ed revision.
100 original message for the ``edit`` ed revision.
101
101
102 The ``message`` operation will give you a chance to revise a commit
102 The ``message`` operation will give you a chance to revise a commit
103 message without changing the contents. It's a shortcut for doing
103 message without changing the contents. It's a shortcut for doing
104 ``edit`` immediately followed by `hg histedit --continue``.
104 ``edit`` immediately followed by `hg histedit --continue``.
105
105
106 If ``histedit`` encounters a conflict when moving a revision (while
106 If ``histedit`` encounters a conflict when moving a revision (while
107 handling ``pick`` or ``fold``), it'll stop in a similar manner to
107 handling ``pick`` or ``fold``), it'll stop in a similar manner to
108 ``edit`` with the difference that it won't prompt you for a commit
108 ``edit`` with the difference that it won't prompt you for a commit
109 message when done. If you decide at this point that you don't like how
109 message when done. If you decide at this point that you don't like how
110 much work it will be to rearrange history, or that you made a mistake,
110 much work it will be to rearrange history, or that you made a mistake,
111 you can use ``hg histedit --abort`` to abandon the new changes you
111 you can use ``hg histedit --abort`` to abandon the new changes you
112 have made and return to the state before you attempted to edit your
112 have made and return to the state before you attempted to edit your
113 history.
113 history.
114
114
115 If we clone the histedit-ed example repository above and add four more
115 If we clone the histedit-ed example repository above and add four more
116 changes, such that we have the following history::
116 changes, such that we have the following history::
117
117
118 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
118 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
119 | Add theta
119 | Add theta
120 |
120 |
121 o 5 140988835471 2009-04-27 18:04 -0500 stefan
121 o 5 140988835471 2009-04-27 18:04 -0500 stefan
122 | Add eta
122 | Add eta
123 |
123 |
124 o 4 122930637314 2009-04-27 18:04 -0500 stefan
124 o 4 122930637314 2009-04-27 18:04 -0500 stefan
125 | Add zeta
125 | Add zeta
126 |
126 |
127 o 3 836302820282 2009-04-27 18:04 -0500 stefan
127 o 3 836302820282 2009-04-27 18:04 -0500 stefan
128 | Add epsilon
128 | Add epsilon
129 |
129 |
130 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
130 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
131 | Add beta and delta.
131 | Add beta and delta.
132 |
132 |
133 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
133 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
134 | Add gamma
134 | Add gamma
135 |
135 |
136 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
136 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
137 Add alpha
137 Add alpha
138
138
139 If you run ``hg histedit --outgoing`` on the clone then it is the same
139 If you run ``hg histedit --outgoing`` on the clone then it is the same
140 as running ``hg histedit 836302820282``. If you need plan to push to a
140 as running ``hg histedit 836302820282``. If you need plan to push to a
141 repository that Mercurial does not detect to be related to the source
141 repository that Mercurial does not detect to be related to the source
142 repo, you can add a ``--force`` option.
142 repo, you can add a ``--force`` option.
143 """
143 """
144
144
145 try:
145 try:
146 import cPickle as pickle
146 import cPickle as pickle
147 pickle.dump # import now
147 pickle.dump # import now
148 except ImportError:
148 except ImportError:
149 import pickle
149 import pickle
150 import os
150 import os
151 import sys
151 import sys
152
152
153 from mercurial import cmdutil
153 from mercurial import cmdutil
154 from mercurial import discovery
154 from mercurial import discovery
155 from mercurial import error
155 from mercurial import error
156 from mercurial import copies
156 from mercurial import copies
157 from mercurial import context
157 from mercurial import context
158 from mercurial import hg
158 from mercurial import hg
159 from mercurial import node
159 from mercurial import node
160 from mercurial import repair
160 from mercurial import repair
161 from mercurial import util
161 from mercurial import util
162 from mercurial import obsolete
162 from mercurial import obsolete
163 from mercurial import merge as mergemod
163 from mercurial import merge as mergemod
164 from mercurial.lock import release
164 from mercurial.lock import release
165 from mercurial.i18n import _
165 from mercurial.i18n import _
166
166
167 cmdtable = {}
167 cmdtable = {}
168 command = cmdutil.command(cmdtable)
168 command = cmdutil.command(cmdtable)
169
169
170 testedwith = 'internal'
170 testedwith = 'internal'
171
171
172 # i18n: command names and abbreviations must remain untranslated
172 # i18n: command names and abbreviations must remain untranslated
173 editcomment = _("""# Edit history between %s and %s
173 editcomment = _("""# Edit history between %s and %s
174 #
174 #
175 # Commits are listed from least to most recent
175 # Commits are listed from least to most recent
176 #
176 #
177 # Commands:
177 # Commands:
178 # p, pick = use commit
178 # p, pick = use commit
179 # e, edit = use commit, but stop for amending
179 # e, edit = use commit, but stop for amending
180 # f, fold = use commit, but combine it with the one above
180 # f, fold = use commit, but combine it with the one above
181 # d, drop = remove commit from history
181 # d, drop = remove commit from history
182 # m, mess = edit message without changing commit content
182 # m, mess = edit message without changing commit content
183 #
183 #
184 """)
184 """)
185
185
186 def commitfuncfor(repo, src):
186 def commitfuncfor(repo, src):
187 """Build a commit function for the replacement of <src>
187 """Build a commit function for the replacement of <src>
188
188
189 This function ensure we apply the same treatment to all changesets.
189 This function ensure we apply the same treatment to all changesets.
190
190
191 - Add a 'histedit_source' entry in extra.
191 - Add a 'histedit_source' entry in extra.
192
192
193 Note that fold have its own separated logic because its handling is a bit
193 Note that fold have its own separated logic because its handling is a bit
194 different and not easily factored out of the fold method.
194 different and not easily factored out of the fold method.
195 """
195 """
196 phasemin = src.phase()
196 phasemin = src.phase()
197 def commitfunc(**kwargs):
197 def commitfunc(**kwargs):
198 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
198 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
199 try:
199 try:
200 repo.ui.setconfig('phases', 'new-commit', phasemin,
200 repo.ui.setconfig('phases', 'new-commit', phasemin,
201 'histedit')
201 'histedit')
202 extra = kwargs.get('extra', {}).copy()
202 extra = kwargs.get('extra', {}).copy()
203 extra['histedit_source'] = src.hex()
203 extra['histedit_source'] = src.hex()
204 kwargs['extra'] = extra
204 kwargs['extra'] = extra
205 return repo.commit(**kwargs)
205 return repo.commit(**kwargs)
206 finally:
206 finally:
207 repo.ui.restoreconfig(phasebackup)
207 repo.ui.restoreconfig(phasebackup)
208 return commitfunc
208 return commitfunc
209
209
210
210
211
211
212 def applychanges(ui, repo, ctx, opts):
212 def applychanges(ui, repo, ctx, opts):
213 """Merge changeset from ctx (only) in the current working directory"""
213 """Merge changeset from ctx (only) in the current working directory"""
214 wcpar = repo.dirstate.parents()[0]
214 wcpar = repo.dirstate.parents()[0]
215 if ctx.p1().node() == wcpar:
215 if ctx.p1().node() == wcpar:
216 # edition ar "in place" we do not need to make any merge,
216 # edition ar "in place" we do not need to make any merge,
217 # just applies changes on parent for edition
217 # just applies changes on parent for edition
218 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
218 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
219 stats = None
219 stats = None
220 else:
220 else:
221 try:
221 try:
222 # ui.forcemerge is an internal variable, do not document
222 # ui.forcemerge is an internal variable, do not document
223 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
223 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
224 'histedit')
224 'histedit')
225 stats = mergemod.update(repo, ctx.node(), True, True, False,
225 stats = mergemod.update(repo, ctx.node(), True, True, False,
226 ctx.p1().node())
226 ctx.p1().node())
227 finally:
227 finally:
228 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
228 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
229 repo.setparents(wcpar, node.nullid)
229 repo.setparents(wcpar, node.nullid)
230 repo.dirstate.write()
230 repo.dirstate.write()
231 # fix up dirstate for copies and renames
231 # fix up dirstate for copies and renames
232 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
232 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
233 return stats
233 return stats
234
234
235 def collapse(repo, first, last, commitopts):
235 def collapse(repo, first, last, commitopts):
236 """collapse the set of revisions from first to last as new one.
236 """collapse the set of revisions from first to last as new one.
237
237
238 Expected commit options are:
238 Expected commit options are:
239 - message
239 - message
240 - date
240 - date
241 - username
241 - username
242 Commit message is edited in all cases.
242 Commit message is edited in all cases.
243
243
244 This function works in memory."""
244 This function works in memory."""
245 ctxs = list(repo.set('%d::%d', first, last))
245 ctxs = list(repo.set('%d::%d', first, last))
246 if not ctxs:
246 if not ctxs:
247 return None
247 return None
248 base = first.parents()[0]
248 base = first.parents()[0]
249
249
250 # commit a new version of the old changeset, including the update
250 # commit a new version of the old changeset, including the update
251 # collect all files which might be affected
251 # collect all files which might be affected
252 files = set()
252 files = set()
253 for ctx in ctxs:
253 for ctx in ctxs:
254 files.update(ctx.files())
254 files.update(ctx.files())
255
255
256 # Recompute copies (avoid recording a -> b -> a)
256 # Recompute copies (avoid recording a -> b -> a)
257 copied = copies.pathcopies(base, last)
257 copied = copies.pathcopies(base, last)
258
258
259 # prune files which were reverted by the updates
259 # prune files which were reverted by the updates
260 def samefile(f):
260 def samefile(f):
261 if f in last.manifest():
261 if f in last.manifest():
262 a = last.filectx(f)
262 a = last.filectx(f)
263 if f in base.manifest():
263 if f in base.manifest():
264 b = base.filectx(f)
264 b = base.filectx(f)
265 return (a.data() == b.data()
265 return (a.data() == b.data()
266 and a.flags() == b.flags())
266 and a.flags() == b.flags())
267 else:
267 else:
268 return False
268 return False
269 else:
269 else:
270 return f not in base.manifest()
270 return f not in base.manifest()
271 files = [f for f in files if not samefile(f)]
271 files = [f for f in files if not samefile(f)]
272 # commit version of these files as defined by head
272 # commit version of these files as defined by head
273 headmf = last.manifest()
273 headmf = last.manifest()
274 def filectxfn(repo, ctx, path):
274 def filectxfn(repo, ctx, path):
275 if path in headmf:
275 if path in headmf:
276 fctx = last[path]
276 fctx = last[path]
277 flags = fctx.flags()
277 flags = fctx.flags()
278 mctx = context.memfilectx(fctx.path(), fctx.data(),
278 mctx = context.memfilectx(fctx.path(), fctx.data(),
279 islink='l' in flags,
279 islink='l' in flags,
280 isexec='x' in flags,
280 isexec='x' in flags,
281 copied=copied.get(path))
281 copied=copied.get(path))
282 return mctx
282 return mctx
283 raise IOError()
283 raise IOError()
284
284
285 if commitopts.get('message'):
285 if commitopts.get('message'):
286 message = commitopts['message']
286 message = commitopts['message']
287 else:
287 else:
288 message = first.description()
288 message = first.description()
289 user = commitopts.get('user')
289 user = commitopts.get('user')
290 date = commitopts.get('date')
290 date = commitopts.get('date')
291 extra = commitopts.get('extra')
291 extra = commitopts.get('extra')
292
292
293 parents = (first.p1().node(), first.p2().node())
293 parents = (first.p1().node(), first.p2().node())
294 new = context.memctx(repo,
294 new = context.memctx(repo,
295 parents=parents,
295 parents=parents,
296 text=message,
296 text=message,
297 files=files,
297 files=files,
298 filectxfn=filectxfn,
298 filectxfn=filectxfn,
299 user=user,
299 user=user,
300 date=date,
300 date=date,
301 extra=extra)
301 extra=extra)
302 new._text = cmdutil.commitforceeditor(repo, new, [])
302 new._text = cmdutil.commitforceeditor(repo, new, [])
303 repo.savecommitmessage(new.description())
303 repo.savecommitmessage(new.description())
304 return repo.commitctx(new)
304 return repo.commitctx(new)
305
305
306 def pick(ui, repo, ctx, ha, opts):
306 def pick(ui, repo, ctx, ha, opts):
307 oldctx = repo[ha]
307 oldctx = repo[ha]
308 if oldctx.parents()[0] == ctx:
308 if oldctx.parents()[0] == ctx:
309 ui.debug('node %s unchanged\n' % ha)
309 ui.debug('node %s unchanged\n' % ha)
310 return oldctx, []
310 return oldctx, []
311 hg.update(repo, ctx.node())
311 hg.update(repo, ctx.node())
312 stats = applychanges(ui, repo, oldctx, opts)
312 stats = applychanges(ui, repo, oldctx, opts)
313 if stats and stats[3] > 0:
313 if stats and stats[3] > 0:
314 raise error.InterventionRequired(_('Fix up the change and run '
314 raise error.InterventionRequired(_('Fix up the change and run '
315 'hg histedit --continue'))
315 'hg histedit --continue'))
316 # drop the second merge parent
316 # drop the second merge parent
317 commit = commitfuncfor(repo, oldctx)
317 commit = commitfuncfor(repo, oldctx)
318 n = commit(text=oldctx.description(), user=oldctx.user(),
318 n = commit(text=oldctx.description(), user=oldctx.user(),
319 date=oldctx.date(), extra=oldctx.extra())
319 date=oldctx.date(), extra=oldctx.extra())
320 if n is None:
320 if n is None:
321 ui.warn(_('%s: empty changeset\n')
321 ui.warn(_('%s: empty changeset\n')
322 % node.hex(ha))
322 % node.hex(ha))
323 return ctx, []
323 return ctx, []
324 new = repo[n]
324 new = repo[n]
325 return new, [(oldctx.node(), (n,))]
325 return new, [(oldctx.node(), (n,))]
326
326
327
327
328 def edit(ui, repo, ctx, ha, opts):
328 def edit(ui, repo, ctx, ha, opts):
329 oldctx = repo[ha]
329 oldctx = repo[ha]
330 hg.update(repo, ctx.node())
330 hg.update(repo, ctx.node())
331 applychanges(ui, repo, oldctx, opts)
331 applychanges(ui, repo, oldctx, opts)
332 raise error.InterventionRequired(
332 raise error.InterventionRequired(
333 _('Make changes as needed, you may commit or record as needed now.\n'
333 _('Make changes as needed, you may commit or record as needed now.\n'
334 'When you are finished, run hg histedit --continue to resume.'))
334 'When you are finished, run hg histedit --continue to resume.'))
335
335
336 def fold(ui, repo, ctx, ha, opts):
336 def fold(ui, repo, ctx, ha, opts):
337 oldctx = repo[ha]
337 oldctx = repo[ha]
338 hg.update(repo, ctx.node())
338 hg.update(repo, ctx.node())
339 stats = applychanges(ui, repo, oldctx, opts)
339 stats = applychanges(ui, repo, oldctx, opts)
340 if stats and stats[3] > 0:
340 if stats and stats[3] > 0:
341 raise error.InterventionRequired(
341 raise error.InterventionRequired(
342 _('Fix up the change and run hg histedit --continue'))
342 _('Fix up the change and run hg histedit --continue'))
343 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
343 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
344 date=oldctx.date(), extra=oldctx.extra())
344 date=oldctx.date(), extra=oldctx.extra())
345 if n is None:
345 if n is None:
346 ui.warn(_('%s: empty changeset')
346 ui.warn(_('%s: empty changeset')
347 % node.hex(ha))
347 % node.hex(ha))
348 return ctx, []
348 return ctx, []
349 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
349 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
350
350
351 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
351 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
352 parent = ctx.parents()[0].node()
352 parent = ctx.parents()[0].node()
353 hg.update(repo, parent)
353 hg.update(repo, parent)
354 ### prepare new commit data
354 ### prepare new commit data
355 commitopts = opts.copy()
355 commitopts = opts.copy()
356 # username
356 # username
357 if ctx.user() == oldctx.user():
357 if ctx.user() == oldctx.user():
358 username = ctx.user()
358 username = ctx.user()
359 else:
359 else:
360 username = ui.username()
360 username = ui.username()
361 commitopts['user'] = username
361 commitopts['user'] = username
362 # commit message
362 # commit message
363 newmessage = '\n***\n'.join(
363 newmessage = '\n***\n'.join(
364 [ctx.description()] +
364 [ctx.description()] +
365 [repo[r].description() for r in internalchanges] +
365 [repo[r].description() for r in internalchanges] +
366 [oldctx.description()]) + '\n'
366 [oldctx.description()]) + '\n'
367 commitopts['message'] = newmessage
367 commitopts['message'] = newmessage
368 # date
368 # date
369 commitopts['date'] = max(ctx.date(), oldctx.date())
369 commitopts['date'] = max(ctx.date(), oldctx.date())
370 extra = ctx.extra().copy()
370 extra = ctx.extra().copy()
371 # histedit_source
371 # histedit_source
372 # note: ctx is likely a temporary commit but that the best we can do here
372 # note: ctx is likely a temporary commit but that the best we can do here
373 # This is sufficient to solve issue3681 anyway
373 # This is sufficient to solve issue3681 anyway
374 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
374 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
375 commitopts['extra'] = extra
375 commitopts['extra'] = extra
376 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
376 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
377 try:
377 try:
378 phasemin = max(ctx.phase(), oldctx.phase())
378 phasemin = max(ctx.phase(), oldctx.phase())
379 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
379 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
380 n = collapse(repo, ctx, repo[newnode], commitopts)
380 n = collapse(repo, ctx, repo[newnode], commitopts)
381 finally:
381 finally:
382 repo.ui.restoreconfig(phasebackup)
382 repo.ui.restoreconfig(phasebackup)
383 if n is None:
383 if n is None:
384 return ctx, []
384 return ctx, []
385 hg.update(repo, n)
385 hg.update(repo, n)
386 replacements = [(oldctx.node(), (newnode,)),
386 replacements = [(oldctx.node(), (newnode,)),
387 (ctx.node(), (n,)),
387 (ctx.node(), (n,)),
388 (newnode, (n,)),
388 (newnode, (n,)),
389 ]
389 ]
390 for ich in internalchanges:
390 for ich in internalchanges:
391 replacements.append((ich, (n,)))
391 replacements.append((ich, (n,)))
392 return repo[n], replacements
392 return repo[n], replacements
393
393
394 def drop(ui, repo, ctx, ha, opts):
394 def drop(ui, repo, ctx, ha, opts):
395 return ctx, [(repo[ha].node(), ())]
395 return ctx, [(repo[ha].node(), ())]
396
396
397
397
398 def message(ui, repo, ctx, ha, opts):
398 def message(ui, repo, ctx, ha, opts):
399 oldctx = repo[ha]
399 oldctx = repo[ha]
400 hg.update(repo, ctx.node())
400 hg.update(repo, ctx.node())
401 stats = applychanges(ui, repo, oldctx, opts)
401 stats = applychanges(ui, repo, oldctx, opts)
402 if stats and stats[3] > 0:
402 if stats and stats[3] > 0:
403 raise error.InterventionRequired(
403 raise error.InterventionRequired(
404 _('Fix up the change and run hg histedit --continue'))
404 _('Fix up the change and run hg histedit --continue'))
405 message = oldctx.description() + '\n'
405 message = oldctx.description() + '\n'
406 message = ui.edit(message, ui.username())
406 message = ui.edit(message, ui.username())
407 repo.savecommitmessage(message)
407 repo.savecommitmessage(message)
408 commit = commitfuncfor(repo, oldctx)
408 commit = commitfuncfor(repo, oldctx)
409 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
409 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
410 extra=oldctx.extra())
410 extra=oldctx.extra())
411 newctx = repo[new]
411 newctx = repo[new]
412 if oldctx.node() != newctx.node():
412 if oldctx.node() != newctx.node():
413 return newctx, [(oldctx.node(), (new,))]
413 return newctx, [(oldctx.node(), (new,))]
414 # We didn't make an edit, so just indicate no replaced nodes
414 # We didn't make an edit, so just indicate no replaced nodes
415 return newctx, []
415 return newctx, []
416
416
417 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
417 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
418 """utility function to find the first outgoing changeset
418 """utility function to find the first outgoing changeset
419
419
420 Used by initialisation code"""
420 Used by initialisation code"""
421 dest = ui.expandpath(remote or 'default-push', remote or 'default')
421 dest = ui.expandpath(remote or 'default-push', remote or 'default')
422 dest, revs = hg.parseurl(dest, None)[:2]
422 dest, revs = hg.parseurl(dest, None)[:2]
423 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
423 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
424
424
425 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
425 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
426 other = hg.peer(repo, opts, dest)
426 other = hg.peer(repo, opts, dest)
427
427
428 if revs:
428 if revs:
429 revs = [repo.lookup(rev) for rev in revs]
429 revs = [repo.lookup(rev) for rev in revs]
430
430
431 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
431 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
432 if not outgoing.missing:
432 if not outgoing.missing:
433 raise util.Abort(_('no outgoing ancestors'))
433 raise util.Abort(_('no outgoing ancestors'))
434 roots = list(repo.revs("roots(%ln)", outgoing.missing))
434 roots = list(repo.revs("roots(%ln)", outgoing.missing))
435 if 1 < len(roots):
435 if 1 < len(roots):
436 msg = _('there are ambiguous outgoing revisions')
436 msg = _('there are ambiguous outgoing revisions')
437 hint = _('see "hg help histedit" for more detail')
437 hint = _('see "hg help histedit" for more detail')
438 raise util.Abort(msg, hint=hint)
438 raise util.Abort(msg, hint=hint)
439 return repo.lookup(roots[0])
439 return repo.lookup(roots[0])
440
440
441 actiontable = {'p': pick,
441 actiontable = {'p': pick,
442 'pick': pick,
442 'pick': pick,
443 'e': edit,
443 'e': edit,
444 'edit': edit,
444 'edit': edit,
445 'f': fold,
445 'f': fold,
446 'fold': fold,
446 'fold': fold,
447 'd': drop,
447 'd': drop,
448 'drop': drop,
448 'drop': drop,
449 'm': message,
449 'm': message,
450 'mess': message,
450 'mess': message,
451 }
451 }
452
452
453 @command('histedit',
453 @command('histedit',
454 [('', 'commands', '',
454 [('', 'commands', '',
455 _('Read history edits from the specified file.')),
455 _('Read history edits from the specified file.')),
456 ('c', 'continue', False, _('continue an edit already in progress')),
456 ('c', 'continue', False, _('continue an edit already in progress')),
457 ('k', 'keep', False,
457 ('k', 'keep', False,
458 _("don't strip old nodes after edit is complete")),
458 _("don't strip old nodes after edit is complete")),
459 ('', 'abort', False, _('abort an edit in progress')),
459 ('', 'abort', False, _('abort an edit in progress')),
460 ('o', 'outgoing', False, _('changesets not found in destination')),
460 ('o', 'outgoing', False, _('changesets not found in destination')),
461 ('f', 'force', False,
461 ('f', 'force', False,
462 _('force outgoing even for unrelated repositories')),
462 _('force outgoing even for unrelated repositories')),
463 ('r', 'rev', [], _('first revision to be edited'))],
463 ('r', 'rev', [], _('first revision to be edited'))],
464 _("ANCESTOR | --outgoing [URL]"))
464 _("ANCESTOR | --outgoing [URL]"))
465 def histedit(ui, repo, *freeargs, **opts):
465 def histedit(ui, repo, *freeargs, **opts):
466 """interactively edit changeset history
466 """interactively edit changeset history
467
467
468 This command edits changesets between ANCESTOR and the parent of
468 This command edits changesets between ANCESTOR and the parent of
469 the working directory.
469 the working directory.
470
470
471 With --outgoing, this edits changesets not found in the
471 With --outgoing, this edits changesets not found in the
472 destination repository. If URL of the destination is omitted, the
472 destination repository. If URL of the destination is omitted, the
473 'default-push' (or 'default') path will be used.
473 'default-push' (or 'default') path will be used.
474
474
475 For safety, this command is aborted, also if there are ambiguous
475 For safety, this command is aborted, also if there are ambiguous
476 outgoing revisions which may confuse users: for example, there are
476 outgoing revisions which may confuse users: for example, there are
477 multiple branches containing outgoing revisions.
477 multiple branches containing outgoing revisions.
478
478
479 Use "min(outgoing() and ::.)" or similar revset specification
479 Use "min(outgoing() and ::.)" or similar revset specification
480 instead of --outgoing to specify edit target revision exactly in
480 instead of --outgoing to specify edit target revision exactly in
481 such ambiguous situation. See :hg:`help revsets` for detail about
481 such ambiguous situation. See :hg:`help revsets` for detail about
482 selecting revisions.
482 selecting revisions.
483
483
484 Returns 0 on success, 1 if user intervention is required (not only
484 Returns 0 on success, 1 if user intervention is required (not only
485 for intentional "edit" command, but also for resolving unexpected
485 for intentional "edit" command, but also for resolving unexpected
486 conflicts).
486 conflicts).
487 """
487 """
488 lock = wlock = None
488 lock = wlock = None
489 try:
489 try:
490 wlock = repo.wlock()
490 wlock = repo.wlock()
491 lock = repo.lock()
491 lock = repo.lock()
492 _histedit(ui, repo, *freeargs, **opts)
492 _histedit(ui, repo, *freeargs, **opts)
493 finally:
493 finally:
494 release(lock, wlock)
494 release(lock, wlock)
495
495
496 def _histedit(ui, repo, *freeargs, **opts):
496 def _histedit(ui, repo, *freeargs, **opts):
497 # TODO only abort if we try and histedit mq patches, not just
497 # TODO only abort if we try and histedit mq patches, not just
498 # blanket if mq patches are applied somewhere
498 # blanket if mq patches are applied somewhere
499 mq = getattr(repo, 'mq', None)
499 mq = getattr(repo, 'mq', None)
500 if mq and mq.applied:
500 if mq and mq.applied:
501 raise util.Abort(_('source has mq patches applied'))
501 raise util.Abort(_('source has mq patches applied'))
502
502
503 # basic argument incompatibility processing
503 # basic argument incompatibility processing
504 outg = opts.get('outgoing')
504 outg = opts.get('outgoing')
505 cont = opts.get('continue')
505 cont = opts.get('continue')
506 abort = opts.get('abort')
506 abort = opts.get('abort')
507 force = opts.get('force')
507 force = opts.get('force')
508 rules = opts.get('commands', '')
508 rules = opts.get('commands', '')
509 revs = opts.get('rev', [])
509 revs = opts.get('rev', [])
510 goal = 'new' # This invocation goal, in new, continue, abort
510 goal = 'new' # This invocation goal, in new, continue, abort
511 if force and not outg:
511 if force and not outg:
512 raise util.Abort(_('--force only allowed with --outgoing'))
512 raise util.Abort(_('--force only allowed with --outgoing'))
513 if cont:
513 if cont:
514 if util.any((outg, abort, revs, freeargs, rules)):
514 if util.any((outg, abort, revs, freeargs, rules)):
515 raise util.Abort(_('no arguments allowed with --continue'))
515 raise util.Abort(_('no arguments allowed with --continue'))
516 goal = 'continue'
516 goal = 'continue'
517 elif abort:
517 elif abort:
518 if util.any((outg, revs, freeargs, rules)):
518 if util.any((outg, revs, freeargs, rules)):
519 raise util.Abort(_('no arguments allowed with --abort'))
519 raise util.Abort(_('no arguments allowed with --abort'))
520 goal = 'abort'
520 goal = 'abort'
521 else:
521 else:
522 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
522 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
523 raise util.Abort(_('history edit already in progress, try '
523 raise util.Abort(_('history edit already in progress, try '
524 '--continue or --abort'))
524 '--continue or --abort'))
525 if outg:
525 if outg:
526 if revs:
526 if revs:
527 raise util.Abort(_('no revisions allowed with --outgoing'))
527 raise util.Abort(_('no revisions allowed with --outgoing'))
528 if len(freeargs) > 1:
528 if len(freeargs) > 1:
529 raise util.Abort(
529 raise util.Abort(
530 _('only one repo argument allowed with --outgoing'))
530 _('only one repo argument allowed with --outgoing'))
531 else:
531 else:
532 revs.extend(freeargs)
532 revs.extend(freeargs)
533 if len(revs) != 1:
533 if len(revs) != 1:
534 raise util.Abort(
534 raise util.Abort(
535 _('histedit requires exactly one ancestor revision'))
535 _('histedit requires exactly one ancestor revision'))
536
536
537
537
538 if goal == 'continue':
538 if goal == 'continue':
539 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
539 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
540 parentctx = repo[parentctxnode]
540 parentctx = repo[parentctxnode]
541 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
541 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
542 replacements.extend(repl)
542 replacements.extend(repl)
543 elif goal == 'abort':
543 elif goal == 'abort':
544 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
544 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
545 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
545 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
546 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
546 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
547 # check whether we should update away
547 # check whether we should update away
548 parentnodes = [c.node() for c in repo[None].parents()]
548 parentnodes = [c.node() for c in repo[None].parents()]
549 for n in leafs | set([parentctxnode]):
549 for n in leafs | set([parentctxnode]):
550 if n in parentnodes:
550 if n in parentnodes:
551 hg.clean(repo, topmost)
551 hg.clean(repo, topmost)
552 break
552 break
553 else:
553 else:
554 pass
554 pass
555 cleanupnode(ui, repo, 'created', tmpnodes)
555 cleanupnode(ui, repo, 'created', tmpnodes)
556 cleanupnode(ui, repo, 'temp', leafs)
556 cleanupnode(ui, repo, 'temp', leafs)
557 os.unlink(os.path.join(repo.path, 'histedit-state'))
557 os.unlink(os.path.join(repo.path, 'histedit-state'))
558 return
558 return
559 else:
559 else:
560 cmdutil.checkunfinished(repo)
560 cmdutil.checkunfinished(repo)
561 cmdutil.bailifchanged(repo)
561 cmdutil.bailifchanged(repo)
562
562
563 topmost, empty = repo.dirstate.parents()
563 topmost, empty = repo.dirstate.parents()
564 if outg:
564 if outg:
565 if freeargs:
565 if freeargs:
566 remote = freeargs[0]
566 remote = freeargs[0]
567 else:
567 else:
568 remote = None
568 remote = None
569 root = findoutgoing(ui, repo, remote, force, opts)
569 root = findoutgoing(ui, repo, remote, force, opts)
570 else:
570 else:
571 rootrevs = list(repo.set('roots(%lr)', revs))
571 rootrevs = list(repo.set('roots(%lr)', revs))
572 if len(rootrevs) != 1:
572 if len(rootrevs) != 1:
573 raise util.Abort(_('The specified revisions must have ' +
573 raise util.Abort(_('The specified revisions must have '
574 'exactly one common root'))
574 'exactly one common root'))
575 root = rootrevs[0].node()
575 root = rootrevs[0].node()
576
576
577 keep = opts.get('keep', False)
577 keep = opts.get('keep', False)
578 revs = between(repo, root, topmost, keep)
578 revs = between(repo, root, topmost, keep)
579 if not revs:
579 if not revs:
580 raise util.Abort(_('%s is not an ancestor of working directory') %
580 raise util.Abort(_('%s is not an ancestor of working directory') %
581 node.short(root))
581 node.short(root))
582
582
583 ctxs = [repo[r] for r in revs]
583 ctxs = [repo[r] for r in revs]
584 if not rules:
584 if not rules:
585 rules = '\n'.join([makedesc(c) for c in ctxs])
585 rules = '\n'.join([makedesc(c) for c in ctxs])
586 rules += '\n\n'
586 rules += '\n\n'
587 rules += editcomment % (node.short(root), node.short(topmost))
587 rules += editcomment % (node.short(root), node.short(topmost))
588 rules = ui.edit(rules, ui.username())
588 rules = ui.edit(rules, ui.username())
589 # Save edit rules in .hg/histedit-last-edit.txt in case
589 # Save edit rules in .hg/histedit-last-edit.txt in case
590 # the user needs to ask for help after something
590 # the user needs to ask for help after something
591 # surprising happens.
591 # surprising happens.
592 f = open(repo.join('histedit-last-edit.txt'), 'w')
592 f = open(repo.join('histedit-last-edit.txt'), 'w')
593 f.write(rules)
593 f.write(rules)
594 f.close()
594 f.close()
595 else:
595 else:
596 if rules == '-':
596 if rules == '-':
597 f = sys.stdin
597 f = sys.stdin
598 else:
598 else:
599 f = open(rules)
599 f = open(rules)
600 rules = f.read()
600 rules = f.read()
601 f.close()
601 f.close()
602 rules = [l for l in (r.strip() for r in rules.splitlines())
602 rules = [l for l in (r.strip() for r in rules.splitlines())
603 if l and not l[0] == '#']
603 if l and not l[0] == '#']
604 rules = verifyrules(rules, repo, ctxs)
604 rules = verifyrules(rules, repo, ctxs)
605
605
606 parentctx = repo[root].parents()[0]
606 parentctx = repo[root].parents()[0]
607 keep = opts.get('keep', False)
607 keep = opts.get('keep', False)
608 replacements = []
608 replacements = []
609
609
610
610
611 while rules:
611 while rules:
612 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
612 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
613 action, ha = rules.pop(0)
613 action, ha = rules.pop(0)
614 ui.debug('histedit: processing %s %s\n' % (action, ha))
614 ui.debug('histedit: processing %s %s\n' % (action, ha))
615 actfunc = actiontable[action]
615 actfunc = actiontable[action]
616 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
616 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
617 replacements.extend(replacement_)
617 replacements.extend(replacement_)
618
618
619 hg.update(repo, parentctx.node())
619 hg.update(repo, parentctx.node())
620
620
621 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
621 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
622 if mapping:
622 if mapping:
623 for prec, succs in mapping.iteritems():
623 for prec, succs in mapping.iteritems():
624 if not succs:
624 if not succs:
625 ui.debug('histedit: %s is dropped\n' % node.short(prec))
625 ui.debug('histedit: %s is dropped\n' % node.short(prec))
626 else:
626 else:
627 ui.debug('histedit: %s is replaced by %s\n' % (
627 ui.debug('histedit: %s is replaced by %s\n' % (
628 node.short(prec), node.short(succs[0])))
628 node.short(prec), node.short(succs[0])))
629 if len(succs) > 1:
629 if len(succs) > 1:
630 m = 'histedit: %s'
630 m = 'histedit: %s'
631 for n in succs[1:]:
631 for n in succs[1:]:
632 ui.debug(m % node.short(n))
632 ui.debug(m % node.short(n))
633
633
634 if not keep:
634 if not keep:
635 if mapping:
635 if mapping:
636 movebookmarks(ui, repo, mapping, topmost, ntm)
636 movebookmarks(ui, repo, mapping, topmost, ntm)
637 # TODO update mq state
637 # TODO update mq state
638 if obsolete._enabled:
638 if obsolete._enabled:
639 markers = []
639 markers = []
640 # sort by revision number because it sound "right"
640 # sort by revision number because it sound "right"
641 for prec in sorted(mapping, key=repo.changelog.rev):
641 for prec in sorted(mapping, key=repo.changelog.rev):
642 succs = mapping[prec]
642 succs = mapping[prec]
643 markers.append((repo[prec],
643 markers.append((repo[prec],
644 tuple(repo[s] for s in succs)))
644 tuple(repo[s] for s in succs)))
645 if markers:
645 if markers:
646 obsolete.createmarkers(repo, markers)
646 obsolete.createmarkers(repo, markers)
647 else:
647 else:
648 cleanupnode(ui, repo, 'replaced', mapping)
648 cleanupnode(ui, repo, 'replaced', mapping)
649
649
650 cleanupnode(ui, repo, 'temp', tmpnodes)
650 cleanupnode(ui, repo, 'temp', tmpnodes)
651 os.unlink(os.path.join(repo.path, 'histedit-state'))
651 os.unlink(os.path.join(repo.path, 'histedit-state'))
652 if os.path.exists(repo.sjoin('undo')):
652 if os.path.exists(repo.sjoin('undo')):
653 os.unlink(repo.sjoin('undo'))
653 os.unlink(repo.sjoin('undo'))
654
654
655 def gatherchildren(repo, ctx):
655 def gatherchildren(repo, ctx):
656 # is there any new commit between the expected parent and "."
656 # is there any new commit between the expected parent and "."
657 #
657 #
658 # note: does not take non linear new change in account (but previous
658 # note: does not take non linear new change in account (but previous
659 # implementation didn't used them anyway (issue3655)
659 # implementation didn't used them anyway (issue3655)
660 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
660 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
661 if ctx.node() != node.nullid:
661 if ctx.node() != node.nullid:
662 if not newchildren:
662 if not newchildren:
663 # `ctx` should match but no result. This means that
663 # `ctx` should match but no result. This means that
664 # currentnode is not a descendant from ctx.
664 # currentnode is not a descendant from ctx.
665 msg = _('%s is not an ancestor of working directory')
665 msg = _('%s is not an ancestor of working directory')
666 hint = _('use "histedit --abort" to clear broken state')
666 hint = _('use "histedit --abort" to clear broken state')
667 raise util.Abort(msg % ctx, hint=hint)
667 raise util.Abort(msg % ctx, hint=hint)
668 newchildren.pop(0) # remove ctx
668 newchildren.pop(0) # remove ctx
669 return newchildren
669 return newchildren
670
670
671 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
671 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
672 action, currentnode = rules.pop(0)
672 action, currentnode = rules.pop(0)
673 ctx = repo[currentnode]
673 ctx = repo[currentnode]
674
674
675 newchildren = gatherchildren(repo, parentctx)
675 newchildren = gatherchildren(repo, parentctx)
676
676
677 # Commit dirty working directory if necessary
677 # Commit dirty working directory if necessary
678 new = None
678 new = None
679 m, a, r, d = repo.status()[:4]
679 m, a, r, d = repo.status()[:4]
680 if m or a or r or d:
680 if m or a or r or d:
681 # prepare the message for the commit to comes
681 # prepare the message for the commit to comes
682 if action in ('f', 'fold'):
682 if action in ('f', 'fold'):
683 message = 'fold-temp-revision %s' % currentnode
683 message = 'fold-temp-revision %s' % currentnode
684 else:
684 else:
685 message = ctx.description() + '\n'
685 message = ctx.description() + '\n'
686 if action in ('e', 'edit', 'm', 'mess'):
686 if action in ('e', 'edit', 'm', 'mess'):
687 editor = cmdutil.commitforceeditor
687 editor = cmdutil.commitforceeditor
688 else:
688 else:
689 editor = False
689 editor = False
690 commit = commitfuncfor(repo, ctx)
690 commit = commitfuncfor(repo, ctx)
691 new = commit(text=message, user=ctx.user(),
691 new = commit(text=message, user=ctx.user(),
692 date=ctx.date(), extra=ctx.extra(),
692 date=ctx.date(), extra=ctx.extra(),
693 editor=editor)
693 editor=editor)
694 if new is not None:
694 if new is not None:
695 newchildren.append(new)
695 newchildren.append(new)
696
696
697 replacements = []
697 replacements = []
698 # track replacements
698 # track replacements
699 if ctx.node() not in newchildren:
699 if ctx.node() not in newchildren:
700 # note: new children may be empty when the changeset is dropped.
700 # note: new children may be empty when the changeset is dropped.
701 # this happen e.g during conflicting pick where we revert content
701 # this happen e.g during conflicting pick where we revert content
702 # to parent.
702 # to parent.
703 replacements.append((ctx.node(), tuple(newchildren)))
703 replacements.append((ctx.node(), tuple(newchildren)))
704
704
705 if action in ('f', 'fold'):
705 if action in ('f', 'fold'):
706 if newchildren:
706 if newchildren:
707 # finalize fold operation if applicable
707 # finalize fold operation if applicable
708 if new is None:
708 if new is None:
709 new = newchildren[-1]
709 new = newchildren[-1]
710 else:
710 else:
711 newchildren.pop() # remove new from internal changes
711 newchildren.pop() # remove new from internal changes
712 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
712 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
713 newchildren)
713 newchildren)
714 replacements.extend(repl)
714 replacements.extend(repl)
715 else:
715 else:
716 # newchildren is empty if the fold did not result in any commit
716 # newchildren is empty if the fold did not result in any commit
717 # this happen when all folded change are discarded during the
717 # this happen when all folded change are discarded during the
718 # merge.
718 # merge.
719 replacements.append((ctx.node(), (parentctx.node(),)))
719 replacements.append((ctx.node(), (parentctx.node(),)))
720 elif newchildren:
720 elif newchildren:
721 # otherwise update "parentctx" before proceeding to further operation
721 # otherwise update "parentctx" before proceeding to further operation
722 parentctx = repo[newchildren[-1]]
722 parentctx = repo[newchildren[-1]]
723 return parentctx, replacements
723 return parentctx, replacements
724
724
725
725
726 def between(repo, old, new, keep):
726 def between(repo, old, new, keep):
727 """select and validate the set of revision to edit
727 """select and validate the set of revision to edit
728
728
729 When keep is false, the specified set can't have children."""
729 When keep is false, the specified set can't have children."""
730 ctxs = list(repo.set('%n::%n', old, new))
730 ctxs = list(repo.set('%n::%n', old, new))
731 if ctxs and not keep:
731 if ctxs and not keep:
732 if (not obsolete._enabled and
732 if (not obsolete._enabled and
733 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
733 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
734 raise util.Abort(_('cannot edit history that would orphan nodes'))
734 raise util.Abort(_('cannot edit history that would orphan nodes'))
735 if repo.revs('(%ld) and merge()', ctxs):
735 if repo.revs('(%ld) and merge()', ctxs):
736 raise util.Abort(_('cannot edit history that contains merges'))
736 raise util.Abort(_('cannot edit history that contains merges'))
737 root = ctxs[0] # list is already sorted by repo.set
737 root = ctxs[0] # list is already sorted by repo.set
738 if not root.phase():
738 if not root.phase():
739 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
739 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
740 return [c.node() for c in ctxs]
740 return [c.node() for c in ctxs]
741
741
742
742
743 def writestate(repo, parentnode, rules, keep, topmost, replacements):
743 def writestate(repo, parentnode, rules, keep, topmost, replacements):
744 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
744 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
745 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
745 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
746 fp.close()
746 fp.close()
747
747
748 def readstate(repo):
748 def readstate(repo):
749 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
749 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
750 """
750 """
751 fp = open(os.path.join(repo.path, 'histedit-state'))
751 fp = open(os.path.join(repo.path, 'histedit-state'))
752 return pickle.load(fp)
752 return pickle.load(fp)
753
753
754
754
755 def makedesc(c):
755 def makedesc(c):
756 """build a initial action line for a ctx `c`
756 """build a initial action line for a ctx `c`
757
757
758 line are in the form:
758 line are in the form:
759
759
760 pick <hash> <rev> <summary>
760 pick <hash> <rev> <summary>
761 """
761 """
762 summary = ''
762 summary = ''
763 if c.description():
763 if c.description():
764 summary = c.description().splitlines()[0]
764 summary = c.description().splitlines()[0]
765 line = 'pick %s %d %s' % (c, c.rev(), summary)
765 line = 'pick %s %d %s' % (c, c.rev(), summary)
766 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
766 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
767
767
768 def verifyrules(rules, repo, ctxs):
768 def verifyrules(rules, repo, ctxs):
769 """Verify that there exists exactly one edit rule per given changeset.
769 """Verify that there exists exactly one edit rule per given changeset.
770
770
771 Will abort if there are to many or too few rules, a malformed rule,
771 Will abort if there are to many or too few rules, a malformed rule,
772 or a rule on a changeset outside of the user-given range.
772 or a rule on a changeset outside of the user-given range.
773 """
773 """
774 parsed = []
774 parsed = []
775 expected = set(str(c) for c in ctxs)
775 expected = set(str(c) for c in ctxs)
776 seen = set()
776 seen = set()
777 for r in rules:
777 for r in rules:
778 if ' ' not in r:
778 if ' ' not in r:
779 raise util.Abort(_('malformed line "%s"') % r)
779 raise util.Abort(_('malformed line "%s"') % r)
780 action, rest = r.split(' ', 1)
780 action, rest = r.split(' ', 1)
781 ha = rest.strip().split(' ', 1)[0]
781 ha = rest.strip().split(' ', 1)[0]
782 try:
782 try:
783 ha = str(repo[ha]) # ensure its a short hash
783 ha = str(repo[ha]) # ensure its a short hash
784 except error.RepoError:
784 except error.RepoError:
785 raise util.Abort(_('unknown changeset %s listed') % ha)
785 raise util.Abort(_('unknown changeset %s listed') % ha)
786 if ha not in expected:
786 if ha not in expected:
787 raise util.Abort(
787 raise util.Abort(
788 _('may not use changesets other than the ones listed'))
788 _('may not use changesets other than the ones listed'))
789 if ha in seen:
789 if ha in seen:
790 raise util.Abort(_('duplicated command for changeset %s') % ha)
790 raise util.Abort(_('duplicated command for changeset %s') % ha)
791 seen.add(ha)
791 seen.add(ha)
792 if action not in actiontable:
792 if action not in actiontable:
793 raise util.Abort(_('unknown action "%s"') % action)
793 raise util.Abort(_('unknown action "%s"') % action)
794 parsed.append([action, ha])
794 parsed.append([action, ha])
795 missing = sorted(expected - seen) # sort to stabilize output
795 missing = sorted(expected - seen) # sort to stabilize output
796 if missing:
796 if missing:
797 raise util.Abort(_('missing rules for changeset %s') % missing[0],
797 raise util.Abort(_('missing rules for changeset %s') % missing[0],
798 hint=_('do you want to use the drop action?'))
798 hint=_('do you want to use the drop action?'))
799 return parsed
799 return parsed
800
800
801 def processreplacement(repo, replacements):
801 def processreplacement(repo, replacements):
802 """process the list of replacements to return
802 """process the list of replacements to return
803
803
804 1) the final mapping between original and created nodes
804 1) the final mapping between original and created nodes
805 2) the list of temporary node created by histedit
805 2) the list of temporary node created by histedit
806 3) the list of new commit created by histedit"""
806 3) the list of new commit created by histedit"""
807 allsuccs = set()
807 allsuccs = set()
808 replaced = set()
808 replaced = set()
809 fullmapping = {}
809 fullmapping = {}
810 # initialise basic set
810 # initialise basic set
811 # fullmapping record all operation recorded in replacement
811 # fullmapping record all operation recorded in replacement
812 for rep in replacements:
812 for rep in replacements:
813 allsuccs.update(rep[1])
813 allsuccs.update(rep[1])
814 replaced.add(rep[0])
814 replaced.add(rep[0])
815 fullmapping.setdefault(rep[0], set()).update(rep[1])
815 fullmapping.setdefault(rep[0], set()).update(rep[1])
816 new = allsuccs - replaced
816 new = allsuccs - replaced
817 tmpnodes = allsuccs & replaced
817 tmpnodes = allsuccs & replaced
818 # Reduce content fullmapping into direct relation between original nodes
818 # Reduce content fullmapping into direct relation between original nodes
819 # and final node created during history edition
819 # and final node created during history edition
820 # Dropped changeset are replaced by an empty list
820 # Dropped changeset are replaced by an empty list
821 toproceed = set(fullmapping)
821 toproceed = set(fullmapping)
822 final = {}
822 final = {}
823 while toproceed:
823 while toproceed:
824 for x in list(toproceed):
824 for x in list(toproceed):
825 succs = fullmapping[x]
825 succs = fullmapping[x]
826 for s in list(succs):
826 for s in list(succs):
827 if s in toproceed:
827 if s in toproceed:
828 # non final node with unknown closure
828 # non final node with unknown closure
829 # We can't process this now
829 # We can't process this now
830 break
830 break
831 elif s in final:
831 elif s in final:
832 # non final node, replace with closure
832 # non final node, replace with closure
833 succs.remove(s)
833 succs.remove(s)
834 succs.update(final[s])
834 succs.update(final[s])
835 else:
835 else:
836 final[x] = succs
836 final[x] = succs
837 toproceed.remove(x)
837 toproceed.remove(x)
838 # remove tmpnodes from final mapping
838 # remove tmpnodes from final mapping
839 for n in tmpnodes:
839 for n in tmpnodes:
840 del final[n]
840 del final[n]
841 # we expect all changes involved in final to exist in the repo
841 # we expect all changes involved in final to exist in the repo
842 # turn `final` into list (topologically sorted)
842 # turn `final` into list (topologically sorted)
843 nm = repo.changelog.nodemap
843 nm = repo.changelog.nodemap
844 for prec, succs in final.items():
844 for prec, succs in final.items():
845 final[prec] = sorted(succs, key=nm.get)
845 final[prec] = sorted(succs, key=nm.get)
846
846
847 # computed topmost element (necessary for bookmark)
847 # computed topmost element (necessary for bookmark)
848 if new:
848 if new:
849 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
849 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
850 elif not final:
850 elif not final:
851 # Nothing rewritten at all. we won't need `newtopmost`
851 # Nothing rewritten at all. we won't need `newtopmost`
852 # It is the same as `oldtopmost` and `processreplacement` know it
852 # It is the same as `oldtopmost` and `processreplacement` know it
853 newtopmost = None
853 newtopmost = None
854 else:
854 else:
855 # every body died. The newtopmost is the parent of the root.
855 # every body died. The newtopmost is the parent of the root.
856 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
856 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
857
857
858 return final, tmpnodes, new, newtopmost
858 return final, tmpnodes, new, newtopmost
859
859
860 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
860 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
861 """Move bookmark from old to newly created node"""
861 """Move bookmark from old to newly created node"""
862 if not mapping:
862 if not mapping:
863 # if nothing got rewritten there is not purpose for this function
863 # if nothing got rewritten there is not purpose for this function
864 return
864 return
865 moves = []
865 moves = []
866 for bk, old in sorted(repo._bookmarks.iteritems()):
866 for bk, old in sorted(repo._bookmarks.iteritems()):
867 if old == oldtopmost:
867 if old == oldtopmost:
868 # special case ensure bookmark stay on tip.
868 # special case ensure bookmark stay on tip.
869 #
869 #
870 # This is arguably a feature and we may only want that for the
870 # This is arguably a feature and we may only want that for the
871 # active bookmark. But the behavior is kept compatible with the old
871 # active bookmark. But the behavior is kept compatible with the old
872 # version for now.
872 # version for now.
873 moves.append((bk, newtopmost))
873 moves.append((bk, newtopmost))
874 continue
874 continue
875 base = old
875 base = old
876 new = mapping.get(base, None)
876 new = mapping.get(base, None)
877 if new is None:
877 if new is None:
878 continue
878 continue
879 while not new:
879 while not new:
880 # base is killed, trying with parent
880 # base is killed, trying with parent
881 base = repo[base].p1().node()
881 base = repo[base].p1().node()
882 new = mapping.get(base, (base,))
882 new = mapping.get(base, (base,))
883 # nothing to move
883 # nothing to move
884 moves.append((bk, new[-1]))
884 moves.append((bk, new[-1]))
885 if moves:
885 if moves:
886 marks = repo._bookmarks
886 marks = repo._bookmarks
887 for mark, new in moves:
887 for mark, new in moves:
888 old = marks[mark]
888 old = marks[mark]
889 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
889 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
890 % (mark, node.short(old), node.short(new)))
890 % (mark, node.short(old), node.short(new)))
891 marks[mark] = new
891 marks[mark] = new
892 marks.write()
892 marks.write()
893
893
894 def cleanupnode(ui, repo, name, nodes):
894 def cleanupnode(ui, repo, name, nodes):
895 """strip a group of nodes from the repository
895 """strip a group of nodes from the repository
896
896
897 The set of node to strip may contains unknown nodes."""
897 The set of node to strip may contains unknown nodes."""
898 ui.debug('should strip %s nodes %s\n' %
898 ui.debug('should strip %s nodes %s\n' %
899 (name, ', '.join([node.short(n) for n in nodes])))
899 (name, ', '.join([node.short(n) for n in nodes])))
900 lock = None
900 lock = None
901 try:
901 try:
902 lock = repo.lock()
902 lock = repo.lock()
903 # Find all node that need to be stripped
903 # Find all node that need to be stripped
904 # (we hg %lr instead of %ln to silently ignore unknown item
904 # (we hg %lr instead of %ln to silently ignore unknown item
905 nm = repo.changelog.nodemap
905 nm = repo.changelog.nodemap
906 nodes = [n for n in nodes if n in nm]
906 nodes = [n for n in nodes if n in nm]
907 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
907 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
908 for c in roots:
908 for c in roots:
909 # We should process node in reverse order to strip tip most first.
909 # We should process node in reverse order to strip tip most first.
910 # but this trigger a bug in changegroup hook.
910 # but this trigger a bug in changegroup hook.
911 # This would reduce bundle overhead
911 # This would reduce bundle overhead
912 repair.strip(ui, repo, c)
912 repair.strip(ui, repo, c)
913 finally:
913 finally:
914 release(lock)
914 release(lock)
915
915
916 def summaryhook(ui, repo):
916 def summaryhook(ui, repo):
917 if not os.path.exists(repo.join('histedit-state')):
917 if not os.path.exists(repo.join('histedit-state')):
918 return
918 return
919 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
919 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
920 if rules:
920 if rules:
921 # i18n: column positioning for "hg summary"
921 # i18n: column positioning for "hg summary"
922 ui.write(_('hist: %s (histedit --continue)\n') %
922 ui.write(_('hist: %s (histedit --continue)\n') %
923 (ui.label(_('%d remaining'), 'histedit.remaining') %
923 (ui.label(_('%d remaining'), 'histedit.remaining') %
924 len(rules)))
924 len(rules)))
925
925
926 def extsetup(ui):
926 def extsetup(ui):
927 cmdutil.summaryhooks.add('histedit', summaryhook)
927 cmdutil.summaryhooks.add('histedit', summaryhook)
928 cmdutil.unfinishedstates.append(
928 cmdutil.unfinishedstates.append(
929 ['histedit-state', False, True, _('histedit in progress'),
929 ['histedit-state', False, True, _('histedit in progress'),
930 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
930 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now