##// END OF EJS Templates
histedit: handle multiple spaces between action and hash (issue3893)...
Pierre-Yves David -
r19039:41669a18 default
parent child Browse files
Show More
@@ -1,853 +1,850 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={}):
407 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
408 """utility function to find the first outgoing changeset
408 """utility function to find the first outgoing changeset
409
409
410 Used by initialisation code"""
410 Used by initialisation code"""
411 dest = ui.expandpath(remote or 'default-push', remote or 'default')
411 dest = ui.expandpath(remote or 'default-push', remote or 'default')
412 dest, revs = hg.parseurl(dest, None)[:2]
412 dest, revs = hg.parseurl(dest, None)[:2]
413 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
413 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
414
414
415 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
415 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
416 other = hg.peer(repo, opts, dest)
416 other = hg.peer(repo, opts, dest)
417
417
418 if revs:
418 if revs:
419 revs = [repo.lookup(rev) for rev in revs]
419 revs = [repo.lookup(rev) for rev in revs]
420
420
421 # hexlify nodes from outgoing, because we're going to parse
421 # hexlify nodes from outgoing, because we're going to parse
422 # parent[0] using revsingle below, and if the binary hash
422 # parent[0] using revsingle below, and if the binary hash
423 # contains special revset characters like ":" the revset
423 # contains special revset characters like ":" the revset
424 # parser can choke.
424 # parser can choke.
425 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
425 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
426 if not outgoing.missing:
426 if not outgoing.missing:
427 raise util.Abort(_('no outgoing ancestors'))
427 raise util.Abort(_('no outgoing ancestors'))
428 return outgoing.missing[0]
428 return outgoing.missing[0]
429
429
430 actiontable = {'p': pick,
430 actiontable = {'p': pick,
431 'pick': pick,
431 'pick': pick,
432 'e': edit,
432 'e': edit,
433 'edit': edit,
433 'edit': edit,
434 'f': fold,
434 'f': fold,
435 'fold': fold,
435 'fold': fold,
436 'd': drop,
436 'd': drop,
437 'drop': drop,
437 'drop': drop,
438 'm': message,
438 'm': message,
439 'mess': message,
439 'mess': message,
440 }
440 }
441
441
442 @command('histedit',
442 @command('histedit',
443 [('', 'commands', '',
443 [('', 'commands', '',
444 _('Read history edits from the specified file.')),
444 _('Read history edits from the specified file.')),
445 ('c', 'continue', False, _('continue an edit already in progress')),
445 ('c', 'continue', False, _('continue an edit already in progress')),
446 ('k', 'keep', False,
446 ('k', 'keep', False,
447 _("don't strip old nodes after edit is complete")),
447 _("don't strip old nodes after edit is complete")),
448 ('', 'abort', False, _('abort an edit in progress')),
448 ('', 'abort', False, _('abort an edit in progress')),
449 ('o', 'outgoing', False, _('changesets not found in destination')),
449 ('o', 'outgoing', False, _('changesets not found in destination')),
450 ('f', 'force', False,
450 ('f', 'force', False,
451 _('force outgoing even for unrelated repositories')),
451 _('force outgoing even for unrelated repositories')),
452 ('r', 'rev', [], _('first revision to be edited'))],
452 ('r', 'rev', [], _('first revision to be edited'))],
453 _("[PARENT]"))
453 _("[PARENT]"))
454 def histedit(ui, repo, *freeargs, **opts):
454 def histedit(ui, repo, *freeargs, **opts):
455 """interactively edit changeset history
455 """interactively edit changeset history
456 """
456 """
457 # 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
458 # blanket if mq patches are applied somewhere
458 # blanket if mq patches are applied somewhere
459 mq = getattr(repo, 'mq', None)
459 mq = getattr(repo, 'mq', None)
460 if mq and mq.applied:
460 if mq and mq.applied:
461 raise util.Abort(_('source has mq patches applied'))
461 raise util.Abort(_('source has mq patches applied'))
462
462
463 # basic argument incompatibility processing
463 # basic argument incompatibility processing
464 outg = opts.get('outgoing')
464 outg = opts.get('outgoing')
465 cont = opts.get('continue')
465 cont = opts.get('continue')
466 abort = opts.get('abort')
466 abort = opts.get('abort')
467 force = opts.get('force')
467 force = opts.get('force')
468 rules = opts.get('commands', '')
468 rules = opts.get('commands', '')
469 revs = opts.get('rev', [])
469 revs = opts.get('rev', [])
470 goal = 'new' # This invocation goal, in new, continue, abort
470 goal = 'new' # This invocation goal, in new, continue, abort
471 if force and not outg:
471 if force and not outg:
472 raise util.Abort(_('--force only allowed with --outgoing'))
472 raise util.Abort(_('--force only allowed with --outgoing'))
473 if cont:
473 if cont:
474 if util.any((outg, abort, revs, freeargs, rules)):
474 if util.any((outg, abort, revs, freeargs, rules)):
475 raise util.Abort(_('no arguments allowed with --continue'))
475 raise util.Abort(_('no arguments allowed with --continue'))
476 goal = 'continue'
476 goal = 'continue'
477 elif abort:
477 elif abort:
478 if util.any((outg, revs, freeargs, rules)):
478 if util.any((outg, revs, freeargs, rules)):
479 raise util.Abort(_('no arguments allowed with --abort'))
479 raise util.Abort(_('no arguments allowed with --abort'))
480 goal = 'abort'
480 goal = 'abort'
481 else:
481 else:
482 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
482 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
483 raise util.Abort(_('history edit already in progress, try '
483 raise util.Abort(_('history edit already in progress, try '
484 '--continue or --abort'))
484 '--continue or --abort'))
485 if outg:
485 if outg:
486 if revs:
486 if revs:
487 raise util.Abort(_('no revisions allowed with --outgoing'))
487 raise util.Abort(_('no revisions allowed with --outgoing'))
488 if len(freeargs) > 1:
488 if len(freeargs) > 1:
489 raise util.Abort(
489 raise util.Abort(
490 _('only one repo argument allowed with --outgoing'))
490 _('only one repo argument allowed with --outgoing'))
491 else:
491 else:
492 revs.extend(freeargs)
492 revs.extend(freeargs)
493 if len(revs) != 1:
493 if len(revs) != 1:
494 raise util.Abort(
494 raise util.Abort(
495 _('histedit requires exactly one parent revision'))
495 _('histedit requires exactly one parent revision'))
496
496
497
497
498 if goal == 'continue':
498 if goal == 'continue':
499 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
499 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
500 currentparent, wantnull = repo.dirstate.parents()
500 currentparent, wantnull = repo.dirstate.parents()
501 parentctx = repo[parentctxnode]
501 parentctx = repo[parentctxnode]
502 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
502 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
503 replacements.extend(repl)
503 replacements.extend(repl)
504 elif goal == 'abort':
504 elif goal == 'abort':
505 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
505 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
506 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
506 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
507 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))
508 hg.clean(repo, topmost)
508 hg.clean(repo, topmost)
509 cleanupnode(ui, repo, 'created', tmpnodes)
509 cleanupnode(ui, repo, 'created', tmpnodes)
510 cleanupnode(ui, repo, 'temp', leafs)
510 cleanupnode(ui, repo, 'temp', leafs)
511 os.unlink(os.path.join(repo.path, 'histedit-state'))
511 os.unlink(os.path.join(repo.path, 'histedit-state'))
512 return
512 return
513 else:
513 else:
514 cmdutil.bailifchanged(repo)
514 cmdutil.bailifchanged(repo)
515
515
516 topmost, empty = repo.dirstate.parents()
516 topmost, empty = repo.dirstate.parents()
517 if outg:
517 if outg:
518 if freeargs:
518 if freeargs:
519 remote = freeargs[0]
519 remote = freeargs[0]
520 else:
520 else:
521 remote = None
521 remote = None
522 root = findoutgoing(ui, repo, remote, force, opts)
522 root = findoutgoing(ui, repo, remote, force, opts)
523 else:
523 else:
524 root = revs[0]
524 root = revs[0]
525 root = scmutil.revsingle(repo, root).node()
525 root = scmutil.revsingle(repo, root).node()
526
526
527 keep = opts.get('keep', False)
527 keep = opts.get('keep', False)
528 revs = between(repo, root, topmost, keep)
528 revs = between(repo, root, topmost, keep)
529 if not revs:
529 if not revs:
530 raise util.Abort(_('%s is not an ancestor of working directory') %
530 raise util.Abort(_('%s is not an ancestor of working directory') %
531 node.short(root))
531 node.short(root))
532
532
533 ctxs = [repo[r] for r in revs]
533 ctxs = [repo[r] for r in revs]
534 if not rules:
534 if not rules:
535 rules = '\n'.join([makedesc(c) for c in ctxs])
535 rules = '\n'.join([makedesc(c) for c in ctxs])
536 rules += '\n\n'
536 rules += '\n\n'
537 rules += editcomment % (node.short(root), node.short(topmost))
537 rules += editcomment % (node.short(root), node.short(topmost))
538 rules = ui.edit(rules, ui.username())
538 rules = ui.edit(rules, ui.username())
539 # Save edit rules in .hg/histedit-last-edit.txt in case
539 # Save edit rules in .hg/histedit-last-edit.txt in case
540 # the user needs to ask for help after something
540 # the user needs to ask for help after something
541 # surprising happens.
541 # surprising happens.
542 f = open(repo.join('histedit-last-edit.txt'), 'w')
542 f = open(repo.join('histedit-last-edit.txt'), 'w')
543 f.write(rules)
543 f.write(rules)
544 f.close()
544 f.close()
545 else:
545 else:
546 if rules == '-':
546 if rules == '-':
547 f = sys.stdin
547 f = sys.stdin
548 else:
548 else:
549 f = open(rules)
549 f = open(rules)
550 rules = f.read()
550 rules = f.read()
551 f.close()
551 f.close()
552 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())
553 if l and not l[0] == '#']
553 if l and not l[0] == '#']
554 rules = verifyrules(rules, repo, ctxs)
554 rules = verifyrules(rules, repo, ctxs)
555
555
556 parentctx = repo[root].parents()[0]
556 parentctx = repo[root].parents()[0]
557 keep = opts.get('keep', False)
557 keep = opts.get('keep', False)
558 replacements = []
558 replacements = []
559
559
560
560
561 while rules:
561 while rules:
562 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
562 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
563 action, ha = rules.pop(0)
563 action, ha = rules.pop(0)
564 ui.debug('histedit: processing %s %s\n' % (action, ha))
564 ui.debug('histedit: processing %s %s\n' % (action, ha))
565 actfunc = actiontable[action]
565 actfunc = actiontable[action]
566 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
566 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
567 replacements.extend(replacement_)
567 replacements.extend(replacement_)
568
568
569 hg.update(repo, parentctx.node())
569 hg.update(repo, parentctx.node())
570
570
571 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
571 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
572 if mapping:
572 if mapping:
573 for prec, succs in mapping.iteritems():
573 for prec, succs in mapping.iteritems():
574 if not succs:
574 if not succs:
575 ui.debug('histedit: %s is dropped\n' % node.short(prec))
575 ui.debug('histedit: %s is dropped\n' % node.short(prec))
576 else:
576 else:
577 ui.debug('histedit: %s is replaced by %s\n' % (
577 ui.debug('histedit: %s is replaced by %s\n' % (
578 node.short(prec), node.short(succs[0])))
578 node.short(prec), node.short(succs[0])))
579 if len(succs) > 1:
579 if len(succs) > 1:
580 m = 'histedit: %s'
580 m = 'histedit: %s'
581 for n in succs[1:]:
581 for n in succs[1:]:
582 ui.debug(m % node.short(n))
582 ui.debug(m % node.short(n))
583
583
584 if not keep:
584 if not keep:
585 if mapping:
585 if mapping:
586 movebookmarks(ui, repo, mapping, topmost, ntm)
586 movebookmarks(ui, repo, mapping, topmost, ntm)
587 # TODO update mq state
587 # TODO update mq state
588 if obsolete._enabled:
588 if obsolete._enabled:
589 markers = []
589 markers = []
590 # sort by revision number because it sound "right"
590 # sort by revision number because it sound "right"
591 for prec in sorted(mapping, key=repo.changelog.rev):
591 for prec in sorted(mapping, key=repo.changelog.rev):
592 succs = mapping[prec]
592 succs = mapping[prec]
593 markers.append((repo[prec],
593 markers.append((repo[prec],
594 tuple(repo[s] for s in succs)))
594 tuple(repo[s] for s in succs)))
595 if markers:
595 if markers:
596 obsolete.createmarkers(repo, markers)
596 obsolete.createmarkers(repo, markers)
597 else:
597 else:
598 cleanupnode(ui, repo, 'replaced', mapping)
598 cleanupnode(ui, repo, 'replaced', mapping)
599
599
600 cleanupnode(ui, repo, 'temp', tmpnodes)
600 cleanupnode(ui, repo, 'temp', tmpnodes)
601 os.unlink(os.path.join(repo.path, 'histedit-state'))
601 os.unlink(os.path.join(repo.path, 'histedit-state'))
602 if os.path.exists(repo.sjoin('undo')):
602 if os.path.exists(repo.sjoin('undo')):
603 os.unlink(repo.sjoin('undo'))
603 os.unlink(repo.sjoin('undo'))
604
604
605
605
606 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
606 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
607 action, currentnode = rules.pop(0)
607 action, currentnode = rules.pop(0)
608 ctx = repo[currentnode]
608 ctx = repo[currentnode]
609 # is there any new commit between the expected parent and "."
609 # is there any new commit between the expected parent and "."
610 #
610 #
611 # 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
612 # implementation didn't used them anyway (issue3655)
612 # implementation didn't used them anyway (issue3655)
613 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
613 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
614 if parentctx.node() != node.nullid:
614 if parentctx.node() != node.nullid:
615 if not newchildren:
615 if not newchildren:
616 # `parentctxnode` should match but no result. This means that
616 # `parentctxnode` should match but no result. This means that
617 # currentnode is not a descendant from parentctxnode.
617 # currentnode is not a descendant from parentctxnode.
618 msg = _('%s is not an ancestor of working directory')
618 msg = _('%s is not an ancestor of working directory')
619 hint = _('update to %s or descendant and run "hg histedit '
619 hint = _('update to %s or descendant and run "hg histedit '
620 '--continue" again') % parentctx
620 '--continue" again') % parentctx
621 raise util.Abort(msg % parentctx, hint=hint)
621 raise util.Abort(msg % parentctx, hint=hint)
622 newchildren.pop(0) # remove parentctxnode
622 newchildren.pop(0) # remove parentctxnode
623 # Commit dirty working directory if necessary
623 # Commit dirty working directory if necessary
624 new = None
624 new = None
625 m, a, r, d = repo.status()[:4]
625 m, a, r, d = repo.status()[:4]
626 if m or a or r or d:
626 if m or a or r or d:
627 # prepare the message for the commit to comes
627 # prepare the message for the commit to comes
628 if action in ('f', 'fold'):
628 if action in ('f', 'fold'):
629 message = 'fold-temp-revision %s' % currentnode
629 message = 'fold-temp-revision %s' % currentnode
630 else:
630 else:
631 message = ctx.description() + '\n'
631 message = ctx.description() + '\n'
632 if action in ('e', 'edit', 'm', 'mess'):
632 if action in ('e', 'edit', 'm', 'mess'):
633 editor = cmdutil.commitforceeditor
633 editor = cmdutil.commitforceeditor
634 else:
634 else:
635 editor = False
635 editor = False
636 commit = commitfuncfor(repo, ctx)
636 commit = commitfuncfor(repo, ctx)
637 new = commit(text=message, user=ctx.user(),
637 new = commit(text=message, user=ctx.user(),
638 date=ctx.date(), extra=ctx.extra(),
638 date=ctx.date(), extra=ctx.extra(),
639 editor=editor)
639 editor=editor)
640 if new is not None:
640 if new is not None:
641 newchildren.append(new)
641 newchildren.append(new)
642
642
643 replacements = []
643 replacements = []
644 # track replacements
644 # track replacements
645 if ctx.node() not in newchildren:
645 if ctx.node() not in newchildren:
646 # note: new children may be empty when the changeset is dropped.
646 # note: new children may be empty when the changeset is dropped.
647 # this happen e.g during conflicting pick where we revert content
647 # this happen e.g during conflicting pick where we revert content
648 # to parent.
648 # to parent.
649 replacements.append((ctx.node(), tuple(newchildren)))
649 replacements.append((ctx.node(), tuple(newchildren)))
650
650
651 if action in ('f', 'fold'):
651 if action in ('f', 'fold'):
652 if newchildren:
652 if newchildren:
653 # finalize fold operation if applicable
653 # finalize fold operation if applicable
654 if new is None:
654 if new is None:
655 new = newchildren[-1]
655 new = newchildren[-1]
656 else:
656 else:
657 newchildren.pop() # remove new from internal changes
657 newchildren.pop() # remove new from internal changes
658 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
658 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
659 newchildren)
659 newchildren)
660 replacements.extend(repl)
660 replacements.extend(repl)
661 else:
661 else:
662 # 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
663 # this happen when all folded change are discarded during the
663 # this happen when all folded change are discarded during the
664 # merge.
664 # merge.
665 replacements.append((ctx.node(), (parentctx.node(),)))
665 replacements.append((ctx.node(), (parentctx.node(),)))
666 elif newchildren:
666 elif newchildren:
667 # otherwise update "parentctx" before proceeding to further operation
667 # otherwise update "parentctx" before proceeding to further operation
668 parentctx = repo[newchildren[-1]]
668 parentctx = repo[newchildren[-1]]
669 return parentctx, replacements
669 return parentctx, replacements
670
670
671
671
672 def between(repo, old, new, keep):
672 def between(repo, old, new, keep):
673 """select and validate the set of revision to edit
673 """select and validate the set of revision to edit
674
674
675 When keep is false, the specified set can't have children."""
675 When keep is false, the specified set can't have children."""
676 ctxs = list(repo.set('%n::%n', old, new))
676 ctxs = list(repo.set('%n::%n', old, new))
677 if ctxs and not keep:
677 if ctxs and not keep:
678 if (not obsolete._enabled and
678 if (not obsolete._enabled and
679 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
679 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
680 raise util.Abort(_('cannot edit history that would orphan nodes'))
680 raise util.Abort(_('cannot edit history that would orphan nodes'))
681 root = ctxs[0] # list is already sorted by repo.set
681 root = ctxs[0] # list is already sorted by repo.set
682 if not root.phase():
682 if not root.phase():
683 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
683 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
684 return [c.node() for c in ctxs]
684 return [c.node() for c in ctxs]
685
685
686
686
687 def writestate(repo, parentnode, rules, keep, topmost, replacements):
687 def writestate(repo, parentnode, rules, keep, topmost, replacements):
688 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
688 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
689 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
689 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
690 fp.close()
690 fp.close()
691
691
692 def readstate(repo):
692 def readstate(repo):
693 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
693 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
694 """
694 """
695 fp = open(os.path.join(repo.path, 'histedit-state'))
695 fp = open(os.path.join(repo.path, 'histedit-state'))
696 return pickle.load(fp)
696 return pickle.load(fp)
697
697
698
698
699 def makedesc(c):
699 def makedesc(c):
700 """build a initial action line for a ctx `c`
700 """build a initial action line for a ctx `c`
701
701
702 line are in the form:
702 line are in the form:
703
703
704 pick <hash> <rev> <summary>
704 pick <hash> <rev> <summary>
705 """
705 """
706 summary = ''
706 summary = ''
707 if c.description():
707 if c.description():
708 summary = c.description().splitlines()[0]
708 summary = c.description().splitlines()[0]
709 line = 'pick %s %d %s' % (c, c.rev(), summary)
709 line = 'pick %s %d %s' % (c, c.rev(), summary)
710 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
711
711
712 def verifyrules(rules, repo, ctxs):
712 def verifyrules(rules, repo, ctxs):
713 """Verify that there exists exactly one edit rule per given changeset.
713 """Verify that there exists exactly one edit rule per given changeset.
714
714
715 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,
716 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.
717 """
717 """
718 parsed = []
718 parsed = []
719 if len(rules) != len(ctxs):
719 if len(rules) != len(ctxs):
720 raise util.Abort(_('must specify a rule for each changeset once'))
720 raise util.Abort(_('must specify a rule for each changeset once'))
721 for r in rules:
721 for r in rules:
722 if ' ' not in r:
722 if ' ' not in r:
723 raise util.Abort(_('malformed line "%s"') % r)
723 raise util.Abort(_('malformed line "%s"') % r)
724 action, rest = r.split(' ', 1)
724 action, rest = r.split(' ', 1)
725 if ' ' in rest.strip():
725 ha = rest.strip().split(' ', 1)[0]
726 ha, rest = rest.split(' ', 1)
727 else:
728 ha = r.strip()
729 try:
726 try:
730 if repo[ha] not in ctxs:
727 if repo[ha] not in ctxs:
731 raise util.Abort(
728 raise util.Abort(
732 _('may not use changesets other than the ones listed'))
729 _('may not use changesets other than the ones listed'))
733 except error.RepoError:
730 except error.RepoError:
734 raise util.Abort(_('unknown changeset %s listed') % ha)
731 raise util.Abort(_('unknown changeset %s listed') % ha)
735 if action not in actiontable:
732 if action not in actiontable:
736 raise util.Abort(_('unknown action "%s"') % action)
733 raise util.Abort(_('unknown action "%s"') % action)
737 parsed.append([action, ha])
734 parsed.append([action, ha])
738 return parsed
735 return parsed
739
736
740 def processreplacement(repo, replacements):
737 def processreplacement(repo, replacements):
741 """process the list of replacements to return
738 """process the list of replacements to return
742
739
743 1) the final mapping between original and created nodes
740 1) the final mapping between original and created nodes
744 2) the list of temporary node created by histedit
741 2) the list of temporary node created by histedit
745 3) the list of new commit created by histedit"""
742 3) the list of new commit created by histedit"""
746 allsuccs = set()
743 allsuccs = set()
747 replaced = set()
744 replaced = set()
748 fullmapping = {}
745 fullmapping = {}
749 # initialise basic set
746 # initialise basic set
750 # fullmapping record all operation recorded in replacement
747 # fullmapping record all operation recorded in replacement
751 for rep in replacements:
748 for rep in replacements:
752 allsuccs.update(rep[1])
749 allsuccs.update(rep[1])
753 replaced.add(rep[0])
750 replaced.add(rep[0])
754 fullmapping.setdefault(rep[0], set()).update(rep[1])
751 fullmapping.setdefault(rep[0], set()).update(rep[1])
755 new = allsuccs - replaced
752 new = allsuccs - replaced
756 tmpnodes = allsuccs & replaced
753 tmpnodes = allsuccs & replaced
757 # Reduce content fullmapping into direct relation between original nodes
754 # Reduce content fullmapping into direct relation between original nodes
758 # and final node created during history edition
755 # and final node created during history edition
759 # Dropped changeset are replaced by an empty list
756 # Dropped changeset are replaced by an empty list
760 toproceed = set(fullmapping)
757 toproceed = set(fullmapping)
761 final = {}
758 final = {}
762 while toproceed:
759 while toproceed:
763 for x in list(toproceed):
760 for x in list(toproceed):
764 succs = fullmapping[x]
761 succs = fullmapping[x]
765 for s in list(succs):
762 for s in list(succs):
766 if s in toproceed:
763 if s in toproceed:
767 # non final node with unknown closure
764 # non final node with unknown closure
768 # We can't process this now
765 # We can't process this now
769 break
766 break
770 elif s in final:
767 elif s in final:
771 # non final node, replace with closure
768 # non final node, replace with closure
772 succs.remove(s)
769 succs.remove(s)
773 succs.update(final[s])
770 succs.update(final[s])
774 else:
771 else:
775 final[x] = succs
772 final[x] = succs
776 toproceed.remove(x)
773 toproceed.remove(x)
777 # remove tmpnodes from final mapping
774 # remove tmpnodes from final mapping
778 for n in tmpnodes:
775 for n in tmpnodes:
779 del final[n]
776 del final[n]
780 # we expect all changes involved in final to exist in the repo
777 # we expect all changes involved in final to exist in the repo
781 # turn `final` into list (topologically sorted)
778 # turn `final` into list (topologically sorted)
782 nm = repo.changelog.nodemap
779 nm = repo.changelog.nodemap
783 for prec, succs in final.items():
780 for prec, succs in final.items():
784 final[prec] = sorted(succs, key=nm.get)
781 final[prec] = sorted(succs, key=nm.get)
785
782
786 # computed topmost element (necessary for bookmark)
783 # computed topmost element (necessary for bookmark)
787 if new:
784 if new:
788 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
785 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
789 elif not final:
786 elif not final:
790 # Nothing rewritten at all. we won't need `newtopmost`
787 # Nothing rewritten at all. we won't need `newtopmost`
791 # It is the same as `oldtopmost` and `processreplacement` know it
788 # It is the same as `oldtopmost` and `processreplacement` know it
792 newtopmost = None
789 newtopmost = None
793 else:
790 else:
794 # every body died. The newtopmost is the parent of the root.
791 # every body died. The newtopmost is the parent of the root.
795 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
792 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
796
793
797 return final, tmpnodes, new, newtopmost
794 return final, tmpnodes, new, newtopmost
798
795
799 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
796 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
800 """Move bookmark from old to newly created node"""
797 """Move bookmark from old to newly created node"""
801 if not mapping:
798 if not mapping:
802 # if nothing got rewritten there is not purpose for this function
799 # if nothing got rewritten there is not purpose for this function
803 return
800 return
804 moves = []
801 moves = []
805 for bk, old in sorted(repo._bookmarks.iteritems()):
802 for bk, old in sorted(repo._bookmarks.iteritems()):
806 if old == oldtopmost:
803 if old == oldtopmost:
807 # special case ensure bookmark stay on tip.
804 # special case ensure bookmark stay on tip.
808 #
805 #
809 # This is arguably a feature and we may only want that for the
806 # This is arguably a feature and we may only want that for the
810 # active bookmark. But the behavior is kept compatible with the old
807 # active bookmark. But the behavior is kept compatible with the old
811 # version for now.
808 # version for now.
812 moves.append((bk, newtopmost))
809 moves.append((bk, newtopmost))
813 continue
810 continue
814 base = old
811 base = old
815 new = mapping.get(base, None)
812 new = mapping.get(base, None)
816 if new is None:
813 if new is None:
817 continue
814 continue
818 while not new:
815 while not new:
819 # base is killed, trying with parent
816 # base is killed, trying with parent
820 base = repo[base].p1().node()
817 base = repo[base].p1().node()
821 new = mapping.get(base, (base,))
818 new = mapping.get(base, (base,))
822 # nothing to move
819 # nothing to move
823 moves.append((bk, new[-1]))
820 moves.append((bk, new[-1]))
824 if moves:
821 if moves:
825 marks = repo._bookmarks
822 marks = repo._bookmarks
826 for mark, new in moves:
823 for mark, new in moves:
827 old = marks[mark]
824 old = marks[mark]
828 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
825 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
829 % (mark, node.short(old), node.short(new)))
826 % (mark, node.short(old), node.short(new)))
830 marks[mark] = new
827 marks[mark] = new
831 marks.write()
828 marks.write()
832
829
833 def cleanupnode(ui, repo, name, nodes):
830 def cleanupnode(ui, repo, name, nodes):
834 """strip a group of nodes from the repository
831 """strip a group of nodes from the repository
835
832
836 The set of node to strip may contains unknown nodes."""
833 The set of node to strip may contains unknown nodes."""
837 ui.debug('should strip %s nodes %s\n' %
834 ui.debug('should strip %s nodes %s\n' %
838 (name, ', '.join([node.short(n) for n in nodes])))
835 (name, ', '.join([node.short(n) for n in nodes])))
839 lock = None
836 lock = None
840 try:
837 try:
841 lock = repo.lock()
838 lock = repo.lock()
842 # Find all node that need to be stripped
839 # Find all node that need to be stripped
843 # (we hg %lr instead of %ln to silently ignore unknown item
840 # (we hg %lr instead of %ln to silently ignore unknown item
844 nm = repo.changelog.nodemap
841 nm = repo.changelog.nodemap
845 nodes = [n for n in nodes if n in nm]
842 nodes = [n for n in nodes if n in nm]
846 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
843 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
847 for c in roots:
844 for c in roots:
848 # We should process node in reverse order to strip tip most first.
845 # We should process node in reverse order to strip tip most first.
849 # but this trigger a bug in changegroup hook.
846 # but this trigger a bug in changegroup hook.
850 # This would reduce bundle overhead
847 # This would reduce bundle overhead
851 repair.strip(ui, repo, c)
848 repair.strip(ui, repo, c)
852 finally:
849 finally:
853 lockmod.release(lock)
850 lockmod.release(lock)
@@ -1,70 +1,101 b''
1 Test argument handling and various data parsing
1 Test argument handling and various data parsing
2 ==================================================
2 ==================================================
3
3
4
4
5 Enable extensions used by this test.
5 Enable extensions used by this test.
6 $ cat >>$HGRCPATH <<EOF
6 $ cat >>$HGRCPATH <<EOF
7 > [extensions]
7 > [extensions]
8 > histedit=
8 > histedit=
9 > EOF
9 > EOF
10
10
11 Repo setup.
11 Repo setup.
12 $ hg init foo
12 $ hg init foo
13 $ cd foo
13 $ cd foo
14 $ echo alpha >> alpha
14 $ echo alpha >> alpha
15 $ hg addr
15 $ hg addr
16 adding alpha
16 adding alpha
17 $ hg ci -m one
17 $ hg ci -m one
18 $ echo alpha >> alpha
18 $ echo alpha >> alpha
19 $ hg ci -m two
19 $ hg ci -m two
20 $ echo alpha >> alpha
20 $ echo alpha >> alpha
21 $ hg ci -m three
21 $ hg ci -m three
22 $ echo alpha >> alpha
22 $ echo alpha >> alpha
23 $ hg ci -m four
23 $ hg ci -m four
24 $ echo alpha >> alpha
24 $ echo alpha >> alpha
25 $ hg ci -m five
25 $ hg ci -m five
26
26
27 $ hg log --style compact --graph
27 $ hg log --style compact --graph
28 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
28 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
29 | five
29 | five
30 |
30 |
31 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
31 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
32 | four
32 | four
33 |
33 |
34 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
34 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
35 | three
35 | three
36 |
36 |
37 o 1 579e40513370 1970-01-01 00:00 +0000 test
37 o 1 579e40513370 1970-01-01 00:00 +0000 test
38 | two
38 | two
39 |
39 |
40 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
40 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
41 one
41 one
42
42
43
43
44 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
44 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
45 --------------------------------------------------------------------
45 --------------------------------------------------------------------
46
46
47 $ HGEDITOR=cat hg histedit "tip^^"
47 $ HGEDITOR=cat hg histedit "tip^^"
48 pick eb57da33312f 2 three
48 pick eb57da33312f 2 three
49 pick c8e68270e35a 3 four
49 pick c8e68270e35a 3 four
50 pick 08d98a8350f3 4 five
50 pick 08d98a8350f3 4 five
51
51
52 # Edit history between eb57da33312f and 08d98a8350f3
52 # Edit history between eb57da33312f and 08d98a8350f3
53 #
53 #
54 # Commands:
54 # Commands:
55 # p, pick = use commit
55 # p, pick = use commit
56 # e, edit = use commit, but stop for amending
56 # e, edit = use commit, but stop for amending
57 # f, fold = use commit, but fold into previous commit (combines N and N-1)
57 # f, fold = use commit, but fold into previous commit (combines N and N-1)
58 # d, drop = remove commit from history
58 # d, drop = remove commit from history
59 # m, mess = edit message without changing commit content
59 # m, mess = edit message without changing commit content
60 #
60 #
61 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
61 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
62
62
63 Run on a revision not ancestors of the current working directory.
63 Run on a revision not ancestors of the current working directory.
64 --------------------------------------------------------------------
64 --------------------------------------------------------------------
65
65
66 $ hg up 2
66 $ hg up 2
67 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 $ hg histedit -r 4
68 $ hg histedit -r 4
69 abort: 08d98a8350f3 is not an ancestor of working directory
69 abort: 08d98a8350f3 is not an ancestor of working directory
70 [255]
70 [255]
71 $ hg up --quiet
72
73 Test short version of command
74 ---------------------------------------
75
76 Note: we use varying amounts of white space between command name and changeset
77 short hash. This tests issue3893.
78
79 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
80 > pick eb57da33312f 2 three
81 > p c8e68270e35a 3 four
82 > f 08d98a8350f3 4 five
83 > EOF
84 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
85 reverting alpha
86 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
87 four
88 ***
89 five
90
91
92
93 HG: Enter commit message. Lines beginning with 'HG:' are removed.
94 HG: Leave message empty to abort commit.
95 HG: --
96 HG: user: test
97 HG: branch 'default'
98 HG: changed alpha
99 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
100 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
101 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/*-backup.hg (glob)
General Comments 0
You need to be logged in to leave comments. Login now