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