##// END OF EJS Templates
histedit: add obsolete support...
Pierre-Yves David -
r17759:9c7497cd default
parent child Browse files
Show More
@@ -0,0 +1,94 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
2
3 Enable obsolete
4
5 $ cat > ${TESTTMP}/obs.py << EOF
6 > import mercurial.obsolete
7 > mercurial.obsolete._enabled = True
8 > EOF
9
10 $ cat >> $HGRCPATH << EOF
11 > [ui]
12 > logtemplate= {rev}:{node|short} {desc|firstline}
13 > [phases]
14 > publish=False
15 > [extensions]'
16 > histedit=
17 >
18 > obs=${TESTTMP}/obs.py
19 > EOF
20
21 $ hg init base
22 $ cd base
23
24 $ for x in a b c d e f ; do
25 > echo $x > $x
26 > hg add $x
27 > hg ci -m $x
28 > done
29
30 $ hg log --graph
31 @ 5:652413bf663e f
32 |
33 o 4:e860deea161a e
34 |
35 o 3:055a42cdd887 d
36 |
37 o 2:177f92b77385 c
38 |
39 o 1:d2ae7f538514 b
40 |
41 o 0:cb9a9f314b8b a
42
43
44 $ HGEDITOR=cat hg histedit 1
45 pick d2ae7f538514 1 b
46 pick 177f92b77385 2 c
47 pick 055a42cdd887 3 d
48 pick e860deea161a 4 e
49 pick 652413bf663e 5 f
50
51 # Edit history between d2ae7f538514 and 652413bf663e
52 #
53 # Commands:
54 # p, pick = use commit
55 # e, edit = use commit, but stop for amending
56 # f, fold = use commit, but fold into previous commit (combines N and N-1)
57 # d, drop = remove commit from history
58 # m, mess = edit message without changing commit content
59 #
60 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
61 $ cat >> commands.txt <<EOF
62 > pick 177f92b77385 2 c
63 > drop d2ae7f538514 1 b
64 > pick 055a42cdd887 3 d
65 > fold e860deea161a 4 e
66 > pick 652413bf663e 5 f
67 > EOF
68 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
69 saved backup bundle to $TESTTMP/base/.hg/strip-backup/34a9919932c1-backup.hg (glob)
70 $ hg log --graph --hidden
71 @ 8:0efacef7cb48 f
72 |
73 o 7:ae467701c500 d
74 |
75 o 6:d36c0562f908 c
76 |
77 | x 5:652413bf663e f
78 | |
79 | x 4:e860deea161a e
80 | |
81 | x 3:055a42cdd887 d
82 | |
83 | x 2:177f92b77385 c
84 | |
85 | x 1:d2ae7f538514 b
86 |/
87 o 0:cb9a9f314b8b a
88
89 $ hg debugobsolete
90 e860deea161a2f77de56603b340ebbb4536308ae ae467701c5006bf21ffcfdb555b3d6b63280b6b7 0 {'date': '*', 'user': 'test'} (glob)
91 652413bf663ef2a641cab26574e46d5f5a64a55a 0efacef7cb481bf574f69075b82d044fdbe5c20f 0 {'date': '*': 'test'} (glob)
92 d2ae7f538514cd87c17547b0de4cea71fe1af9fb 0 {'date': '*', 'user': 'test'} (glob)
93 055a42cdd88768532f9cf79daa407fc8d138de9b ae467701c5006bf21ffcfdb555b3d6b63280b6b7 0 {'date': '*': 'test'} (glob)
94 177f92b773850b59254aa5e923436f921b55483b d36c0562f908c692f5204d606d4ff3537d41f1bf 0 {'date': '*', 'user': 'test'} (glob)
@@ -1,764 +1,773 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 633536316234 and 7c2fd3b9020c
31 # Edit history between 633536316234 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 633536316234 and 7c2fd3b9020c
50 # Edit history between 633536316234 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 example repository above and add three more changes, such that
111 If we clone the example repository above and add three more changes, such that
112 we have the following history::
112 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
146
147 from mercurial import bookmarks
147 from mercurial import bookmarks
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 merge as mergemod
160 from mercurial import merge as mergemod
160 from mercurial.i18n import _
161 from mercurial.i18n import _
161
162
162 cmdtable = {}
163 cmdtable = {}
163 command = cmdutil.command(cmdtable)
164 command = cmdutil.command(cmdtable)
164
165
165 testedwith = 'internal'
166 testedwith = 'internal'
166
167
167 # i18n: command names and abbreviations must remain untranslated
168 # i18n: command names and abbreviations must remain untranslated
168 editcomment = _("""# Edit history between %s and %s
169 editcomment = _("""# Edit history between %s and %s
169 #
170 #
170 # Commands:
171 # Commands:
171 # p, pick = use commit
172 # p, pick = use commit
172 # e, edit = use commit, but stop for amending
173 # e, edit = use commit, but stop for amending
173 # 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)
174 # d, drop = remove commit from history
175 # d, drop = remove commit from history
175 # m, mess = edit message without changing commit content
176 # m, mess = edit message without changing commit content
176 #
177 #
177 """)
178 """)
178
179
179 def applychanges(ui, repo, ctx, opts):
180 def applychanges(ui, repo, ctx, opts):
180 """Merge changeset from ctx (only) in the current working directory"""
181 """Merge changeset from ctx (only) in the current working directory"""
181 wcpar = repo.dirstate.parents()[0]
182 wcpar = repo.dirstate.parents()[0]
182 if ctx.p1().node() == wcpar:
183 if ctx.p1().node() == wcpar:
183 # edition ar "in place" we do not need to make any merge,
184 # edition ar "in place" we do not need to make any merge,
184 # just applies changes on parent for edition
185 # just applies changes on parent for edition
185 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
186 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
186 stats = None
187 stats = None
187 else:
188 else:
188 try:
189 try:
189 # ui.forcemerge is an internal variable, do not document
190 # ui.forcemerge is an internal variable, do not document
190 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
191 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
191 stats = mergemod.update(repo, ctx.node(), True, True, False,
192 stats = mergemod.update(repo, ctx.node(), True, True, False,
192 ctx.p1().node())
193 ctx.p1().node())
193 finally:
194 finally:
194 repo.ui.setconfig('ui', 'forcemerge', '')
195 repo.ui.setconfig('ui', 'forcemerge', '')
195 repo.setparents(wcpar, node.nullid)
196 repo.setparents(wcpar, node.nullid)
196 repo.dirstate.write()
197 repo.dirstate.write()
197 # fix up dirstate for copies and renames
198 # fix up dirstate for copies and renames
198 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
199 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
199 return stats
200 return stats
200
201
201 def collapse(repo, first, last, commitopts):
202 def collapse(repo, first, last, commitopts):
202 """collapse the set of revisions from first to last as new one.
203 """collapse the set of revisions from first to last as new one.
203
204
204 Expected commit options are:
205 Expected commit options are:
205 - message
206 - message
206 - date
207 - date
207 - username
208 - username
208 Commit message is edited in all cases.
209 Commit message is edited in all cases.
209
210
210 This function works in memory."""
211 This function works in memory."""
211 ctxs = list(repo.set('%d::%d', first, last))
212 ctxs = list(repo.set('%d::%d', first, last))
212 if not ctxs:
213 if not ctxs:
213 return None
214 return None
214 base = first.parents()[0]
215 base = first.parents()[0]
215
216
216 # commit a new version of the old changeset, including the update
217 # commit a new version of the old changeset, including the update
217 # collect all files which might be affected
218 # collect all files which might be affected
218 files = set()
219 files = set()
219 for ctx in ctxs:
220 for ctx in ctxs:
220 files.update(ctx.files())
221 files.update(ctx.files())
221
222
222 # Recompute copies (avoid recording a -> b -> a)
223 # Recompute copies (avoid recording a -> b -> a)
223 copied = copies.pathcopies(first, last)
224 copied = copies.pathcopies(first, last)
224
225
225 # prune files which were reverted by the updates
226 # prune files which were reverted by the updates
226 def samefile(f):
227 def samefile(f):
227 if f in last.manifest():
228 if f in last.manifest():
228 a = last.filectx(f)
229 a = last.filectx(f)
229 if f in base.manifest():
230 if f in base.manifest():
230 b = base.filectx(f)
231 b = base.filectx(f)
231 return (a.data() == b.data()
232 return (a.data() == b.data()
232 and a.flags() == b.flags())
233 and a.flags() == b.flags())
233 else:
234 else:
234 return False
235 return False
235 else:
236 else:
236 return f not in base.manifest()
237 return f not in base.manifest()
237 files = [f for f in files if not samefile(f)]
238 files = [f for f in files if not samefile(f)]
238 # commit version of these files as defined by head
239 # commit version of these files as defined by head
239 headmf = last.manifest()
240 headmf = last.manifest()
240 def filectxfn(repo, ctx, path):
241 def filectxfn(repo, ctx, path):
241 if path in headmf:
242 if path in headmf:
242 fctx = last[path]
243 fctx = last[path]
243 flags = fctx.flags()
244 flags = fctx.flags()
244 mctx = context.memfilectx(fctx.path(), fctx.data(),
245 mctx = context.memfilectx(fctx.path(), fctx.data(),
245 islink='l' in flags,
246 islink='l' in flags,
246 isexec='x' in flags,
247 isexec='x' in flags,
247 copied=copied.get(path))
248 copied=copied.get(path))
248 return mctx
249 return mctx
249 raise IOError()
250 raise IOError()
250
251
251 if commitopts.get('message'):
252 if commitopts.get('message'):
252 message = commitopts['message']
253 message = commitopts['message']
253 else:
254 else:
254 message = first.description()
255 message = first.description()
255 user = commitopts.get('user')
256 user = commitopts.get('user')
256 date = commitopts.get('date')
257 date = commitopts.get('date')
257 extra = first.extra()
258 extra = first.extra()
258
259
259 parents = (first.p1().node(), first.p2().node())
260 parents = (first.p1().node(), first.p2().node())
260 new = context.memctx(repo,
261 new = context.memctx(repo,
261 parents=parents,
262 parents=parents,
262 text=message,
263 text=message,
263 files=files,
264 files=files,
264 filectxfn=filectxfn,
265 filectxfn=filectxfn,
265 user=user,
266 user=user,
266 date=date,
267 date=date,
267 extra=extra)
268 extra=extra)
268 new._text = cmdutil.commitforceeditor(repo, new, [])
269 new._text = cmdutil.commitforceeditor(repo, new, [])
269 return repo.commitctx(new)
270 return repo.commitctx(new)
270
271
271 def pick(ui, repo, ctx, ha, opts):
272 def pick(ui, repo, ctx, ha, opts):
272 oldctx = repo[ha]
273 oldctx = repo[ha]
273 if oldctx.parents()[0] == ctx:
274 if oldctx.parents()[0] == ctx:
274 ui.debug('node %s unchanged\n' % ha)
275 ui.debug('node %s unchanged\n' % ha)
275 return oldctx, []
276 return oldctx, []
276 hg.update(repo, ctx.node())
277 hg.update(repo, ctx.node())
277 stats = applychanges(ui, repo, oldctx, opts)
278 stats = applychanges(ui, repo, oldctx, opts)
278 if stats and stats[3] > 0:
279 if stats and stats[3] > 0:
279 raise util.Abort(_('Fix up the change and run '
280 raise util.Abort(_('Fix up the change and run '
280 'hg histedit --continue'))
281 'hg histedit --continue'))
281 # drop the second merge parent
282 # drop the second merge parent
282 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
283 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
283 date=oldctx.date(), extra=oldctx.extra())
284 date=oldctx.date(), extra=oldctx.extra())
284 if n is None:
285 if n is None:
285 ui.warn(_('%s: empty changeset\n')
286 ui.warn(_('%s: empty changeset\n')
286 % node.hex(ha))
287 % node.hex(ha))
287 return ctx, []
288 return ctx, []
288 new = repo[n]
289 new = repo[n]
289 return new, [(oldctx.node(), (n,))]
290 return new, [(oldctx.node(), (n,))]
290
291
291
292
292 def edit(ui, repo, ctx, ha, opts):
293 def edit(ui, repo, ctx, ha, opts):
293 oldctx = repo[ha]
294 oldctx = repo[ha]
294 hg.update(repo, ctx.node())
295 hg.update(repo, ctx.node())
295 applychanges(ui, repo, oldctx, opts)
296 applychanges(ui, repo, oldctx, opts)
296 raise util.Abort(_('Make changes as needed, you may commit or record as '
297 raise util.Abort(_('Make changes as needed, you may commit or record as '
297 'needed now.\nWhen you are finished, run hg'
298 'needed now.\nWhen you are finished, run hg'
298 ' histedit --continue to resume.'))
299 ' histedit --continue to resume.'))
299
300
300 def fold(ui, repo, ctx, ha, opts):
301 def fold(ui, repo, ctx, ha, opts):
301 oldctx = repo[ha]
302 oldctx = repo[ha]
302 hg.update(repo, ctx.node())
303 hg.update(repo, ctx.node())
303 stats = applychanges(ui, repo, oldctx, opts)
304 stats = applychanges(ui, repo, oldctx, opts)
304 if stats and stats[3] > 0:
305 if stats and stats[3] > 0:
305 raise util.Abort(_('Fix up the change and run '
306 raise util.Abort(_('Fix up the change and run '
306 'hg histedit --continue'))
307 'hg histedit --continue'))
307 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
308 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
308 date=oldctx.date(), extra=oldctx.extra())
309 date=oldctx.date(), extra=oldctx.extra())
309 if n is None:
310 if n is None:
310 ui.warn(_('%s: empty changeset')
311 ui.warn(_('%s: empty changeset')
311 % node.hex(ha))
312 % node.hex(ha))
312 return ctx, []
313 return ctx, []
313 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
314 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
314
315
315 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
316 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
316 parent = ctx.parents()[0].node()
317 parent = ctx.parents()[0].node()
317 hg.update(repo, parent)
318 hg.update(repo, parent)
318 ### prepare new commit data
319 ### prepare new commit data
319 commitopts = opts.copy()
320 commitopts = opts.copy()
320 # username
321 # username
321 if ctx.user() == oldctx.user():
322 if ctx.user() == oldctx.user():
322 username = ctx.user()
323 username = ctx.user()
323 else:
324 else:
324 username = ui.username()
325 username = ui.username()
325 commitopts['user'] = username
326 commitopts['user'] = username
326 # commit message
327 # commit message
327 newmessage = '\n***\n'.join(
328 newmessage = '\n***\n'.join(
328 [ctx.description()] +
329 [ctx.description()] +
329 [repo[r].description() for r in internalchanges] +
330 [repo[r].description() for r in internalchanges] +
330 [oldctx.description()]) + '\n'
331 [oldctx.description()]) + '\n'
331 commitopts['message'] = newmessage
332 commitopts['message'] = newmessage
332 # date
333 # date
333 commitopts['date'] = max(ctx.date(), oldctx.date())
334 commitopts['date'] = max(ctx.date(), oldctx.date())
334 n = collapse(repo, ctx, repo[newnode], commitopts)
335 n = collapse(repo, ctx, repo[newnode], commitopts)
335 if n is None:
336 if n is None:
336 return ctx, []
337 return ctx, []
337 hg.update(repo, n)
338 hg.update(repo, n)
338 replacements = [(oldctx.node(), (newnode,)),
339 replacements = [(oldctx.node(), (newnode,)),
339 (ctx.node(), (n,)),
340 (ctx.node(), (n,)),
340 (newnode, (n,)),
341 (newnode, (n,)),
341 ]
342 ]
342 for ich in internalchanges:
343 for ich in internalchanges:
343 replacements.append((ich, (n,)))
344 replacements.append((ich, (n,)))
344 return repo[n], replacements
345 return repo[n], replacements
345
346
346 def drop(ui, repo, ctx, ha, opts):
347 def drop(ui, repo, ctx, ha, opts):
347 return ctx, [(repo[ha].node(), ())]
348 return ctx, [(repo[ha].node(), ())]
348
349
349
350
350 def message(ui, repo, ctx, ha, opts):
351 def message(ui, repo, ctx, ha, opts):
351 oldctx = repo[ha]
352 oldctx = repo[ha]
352 hg.update(repo, ctx.node())
353 hg.update(repo, ctx.node())
353 stats = applychanges(ui, repo, oldctx, opts)
354 stats = applychanges(ui, repo, oldctx, opts)
354 if stats and stats[3] > 0:
355 if stats and stats[3] > 0:
355 raise util.Abort(_('Fix up the change and run '
356 raise util.Abort(_('Fix up the change and run '
356 'hg histedit --continue'))
357 'hg histedit --continue'))
357 message = oldctx.description() + '\n'
358 message = oldctx.description() + '\n'
358 message = ui.edit(message, ui.username())
359 message = ui.edit(message, ui.username())
359 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
360 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
360 extra=oldctx.extra())
361 extra=oldctx.extra())
361 newctx = repo[new]
362 newctx = repo[new]
362 if oldctx.node() != newctx.node():
363 if oldctx.node() != newctx.node():
363 return newctx, [(oldctx.node(), (new,))]
364 return newctx, [(oldctx.node(), (new,))]
364 # We didn't make an edit, so just indicate no replaced nodes
365 # We didn't make an edit, so just indicate no replaced nodes
365 return newctx, []
366 return newctx, []
366
367
367 actiontable = {'p': pick,
368 actiontable = {'p': pick,
368 'pick': pick,
369 'pick': pick,
369 'e': edit,
370 'e': edit,
370 'edit': edit,
371 'edit': edit,
371 'f': fold,
372 'f': fold,
372 'fold': fold,
373 'fold': fold,
373 'd': drop,
374 'd': drop,
374 'drop': drop,
375 'drop': drop,
375 'm': message,
376 'm': message,
376 'mess': message,
377 'mess': message,
377 }
378 }
378
379
379 @command('histedit',
380 @command('histedit',
380 [('', 'commands', '',
381 [('', 'commands', '',
381 _('Read history edits from the specified file.')),
382 _('Read history edits from the specified file.')),
382 ('c', 'continue', False, _('continue an edit already in progress')),
383 ('c', 'continue', False, _('continue an edit already in progress')),
383 ('k', 'keep', False,
384 ('k', 'keep', False,
384 _("don't strip old nodes after edit is complete")),
385 _("don't strip old nodes after edit is complete")),
385 ('', 'abort', False, _('abort an edit in progress')),
386 ('', 'abort', False, _('abort an edit in progress')),
386 ('o', 'outgoing', False, _('changesets not found in destination')),
387 ('o', 'outgoing', False, _('changesets not found in destination')),
387 ('f', 'force', False,
388 ('f', 'force', False,
388 _('force outgoing even for unrelated repositories')),
389 _('force outgoing even for unrelated repositories')),
389 ('r', 'rev', [], _('first revision to be edited'))],
390 ('r', 'rev', [], _('first revision to be edited'))],
390 _("[PARENT]"))
391 _("[PARENT]"))
391 def histedit(ui, repo, *parent, **opts):
392 def histedit(ui, repo, *parent, **opts):
392 """interactively edit changeset history
393 """interactively edit changeset history
393 """
394 """
394 # TODO only abort if we try and histedit mq patches, not just
395 # TODO only abort if we try and histedit mq patches, not just
395 # blanket if mq patches are applied somewhere
396 # blanket if mq patches are applied somewhere
396 mq = getattr(repo, 'mq', None)
397 mq = getattr(repo, 'mq', None)
397 if mq and mq.applied:
398 if mq and mq.applied:
398 raise util.Abort(_('source has mq patches applied'))
399 raise util.Abort(_('source has mq patches applied'))
399
400
400 parent = list(parent) + opts.get('rev', [])
401 parent = list(parent) + opts.get('rev', [])
401 if opts.get('outgoing'):
402 if opts.get('outgoing'):
402 if len(parent) > 1:
403 if len(parent) > 1:
403 raise util.Abort(
404 raise util.Abort(
404 _('only one repo argument allowed with --outgoing'))
405 _('only one repo argument allowed with --outgoing'))
405 elif parent:
406 elif parent:
406 parent = parent[0]
407 parent = parent[0]
407
408
408 dest = ui.expandpath(parent or 'default-push', parent or 'default')
409 dest = ui.expandpath(parent or 'default-push', parent or 'default')
409 dest, revs = hg.parseurl(dest, None)[:2]
410 dest, revs = hg.parseurl(dest, None)[:2]
410 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
411 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
411
412
412 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
413 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
413 other = hg.peer(repo, opts, dest)
414 other = hg.peer(repo, opts, dest)
414
415
415 if revs:
416 if revs:
416 revs = [repo.lookup(rev) for rev in revs]
417 revs = [repo.lookup(rev) for rev in revs]
417
418
418 parent = discovery.findcommonoutgoing(
419 parent = discovery.findcommonoutgoing(
419 repo, other, [], force=opts.get('force')).missing[0:1]
420 repo, other, [], force=opts.get('force')).missing[0:1]
420 else:
421 else:
421 if opts.get('force'):
422 if opts.get('force'):
422 raise util.Abort(_('--force only allowed with --outgoing'))
423 raise util.Abort(_('--force only allowed with --outgoing'))
423
424
424 if opts.get('continue', False):
425 if opts.get('continue', False):
425 if len(parent) != 0:
426 if len(parent) != 0:
426 raise util.Abort(_('no arguments allowed with --continue'))
427 raise util.Abort(_('no arguments allowed with --continue'))
427 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
428 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
428 currentparent, wantnull = repo.dirstate.parents()
429 currentparent, wantnull = repo.dirstate.parents()
429 parentctx = repo[parentctxnode]
430 parentctx = repo[parentctxnode]
430 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
431 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
431 replacements.extend(repl)
432 replacements.extend(repl)
432 elif opts.get('abort', False):
433 elif opts.get('abort', False):
433 if len(parent) != 0:
434 if len(parent) != 0:
434 raise util.Abort(_('no arguments allowed with --abort'))
435 raise util.Abort(_('no arguments allowed with --abort'))
435 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
436 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
436 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
437 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
437 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
438 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
438 hg.clean(repo, topmost)
439 hg.clean(repo, topmost)
439 cleanupnode(ui, repo, 'created', tmpnodes)
440 cleanupnode(ui, repo, 'created', tmpnodes)
440 cleanupnode(ui, repo, 'temp', leafs)
441 cleanupnode(ui, repo, 'temp', leafs)
441 os.unlink(os.path.join(repo.path, 'histedit-state'))
442 os.unlink(os.path.join(repo.path, 'histedit-state'))
442 return
443 return
443 else:
444 else:
444 cmdutil.bailifchanged(repo)
445 cmdutil.bailifchanged(repo)
445 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
446 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
446 raise util.Abort(_('history edit already in progress, try '
447 raise util.Abort(_('history edit already in progress, try '
447 '--continue or --abort'))
448 '--continue or --abort'))
448
449
449 topmost, empty = repo.dirstate.parents()
450 topmost, empty = repo.dirstate.parents()
450
451
451 if len(parent) != 1:
452 if len(parent) != 1:
452 raise util.Abort(_('histedit requires exactly one parent revision'))
453 raise util.Abort(_('histedit requires exactly one parent revision'))
453 parent = scmutil.revsingle(repo, parent[0]).node()
454 parent = scmutil.revsingle(repo, parent[0]).node()
454
455
455 keep = opts.get('keep', False)
456 keep = opts.get('keep', False)
456 revs = between(repo, parent, topmost, keep)
457 revs = between(repo, parent, topmost, keep)
457
458
458 ctxs = [repo[r] for r in revs]
459 ctxs = [repo[r] for r in revs]
459 rules = opts.get('commands', '')
460 rules = opts.get('commands', '')
460 if not rules:
461 if not rules:
461 rules = '\n'.join([makedesc(c) for c in ctxs])
462 rules = '\n'.join([makedesc(c) for c in ctxs])
462 rules += '\n\n'
463 rules += '\n\n'
463 rules += editcomment % (node.short(parent), node.short(topmost))
464 rules += editcomment % (node.short(parent), node.short(topmost))
464 rules = ui.edit(rules, ui.username())
465 rules = ui.edit(rules, ui.username())
465 # Save edit rules in .hg/histedit-last-edit.txt in case
466 # Save edit rules in .hg/histedit-last-edit.txt in case
466 # the user needs to ask for help after something
467 # the user needs to ask for help after something
467 # surprising happens.
468 # surprising happens.
468 f = open(repo.join('histedit-last-edit.txt'), 'w')
469 f = open(repo.join('histedit-last-edit.txt'), 'w')
469 f.write(rules)
470 f.write(rules)
470 f.close()
471 f.close()
471 else:
472 else:
472 f = open(rules)
473 f = open(rules)
473 rules = f.read()
474 rules = f.read()
474 f.close()
475 f.close()
475 rules = [l for l in (r.strip() for r in rules.splitlines())
476 rules = [l for l in (r.strip() for r in rules.splitlines())
476 if l and not l[0] == '#']
477 if l and not l[0] == '#']
477 rules = verifyrules(rules, repo, ctxs)
478 rules = verifyrules(rules, repo, ctxs)
478
479
479 parentctx = repo[parent].parents()[0]
480 parentctx = repo[parent].parents()[0]
480 keep = opts.get('keep', False)
481 keep = opts.get('keep', False)
481 replacements = []
482 replacements = []
482
483
483
484
484 while rules:
485 while rules:
485 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
486 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
486 action, ha = rules.pop(0)
487 action, ha = rules.pop(0)
487 ui.debug('histedit: processing %s %s\n' % (action, ha))
488 ui.debug('histedit: processing %s %s\n' % (action, ha))
488 actfunc = actiontable[action]
489 actfunc = actiontable[action]
489 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
490 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
490 replacements.extend(replacement_)
491 replacements.extend(replacement_)
491
492
492 hg.update(repo, parentctx.node())
493 hg.update(repo, parentctx.node())
493
494
494 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
495 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
495 if mapping:
496 if mapping:
496 for prec, succs in mapping.iteritems():
497 for prec, succs in mapping.iteritems():
497 if not succs:
498 if not succs:
498 ui.debug('histedit: %s is dropped\n' % node.short(prec))
499 ui.debug('histedit: %s is dropped\n' % node.short(prec))
499 else:
500 else:
500 ui.debug('histedit: %s is replaced by %s\n' % (
501 ui.debug('histedit: %s is replaced by %s\n' % (
501 node.short(prec), node.short(succs[0])))
502 node.short(prec), node.short(succs[0])))
502 if len(succs) > 1:
503 if len(succs) > 1:
503 m = 'histedit: %s'
504 m = 'histedit: %s'
504 for n in succs[1:]:
505 for n in succs[1:]:
505 ui.debug(m % node.short(n))
506 ui.debug(m % node.short(n))
506
507
507 if not keep:
508 if not keep:
508 if mapping:
509 if mapping:
509 movebookmarks(ui, repo, mapping, topmost, ntm)
510 movebookmarks(ui, repo, mapping, topmost, ntm)
510 # TODO update mq state
511 # TODO update mq state
511 cleanupnode(ui, repo, 'replaced', mapping)
512 if obsolete._enabled:
513 markers = []
514 for prec, succs in mapping.iteritems():
515 markers.append((repo[prec],
516 tuple(repo[s] for s in succs)))
517 if markers:
518 obsolete.createmarkers(repo, markers)
519 else:
520 cleanupnode(ui, repo, 'replaced', mapping)
512
521
513 cleanupnode(ui, repo, 'temp', tmpnodes)
522 cleanupnode(ui, repo, 'temp', tmpnodes)
514 os.unlink(os.path.join(repo.path, 'histedit-state'))
523 os.unlink(os.path.join(repo.path, 'histedit-state'))
515 if os.path.exists(repo.sjoin('undo')):
524 if os.path.exists(repo.sjoin('undo')):
516 os.unlink(repo.sjoin('undo'))
525 os.unlink(repo.sjoin('undo'))
517
526
518
527
519 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
528 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
520 action, currentnode = rules.pop(0)
529 action, currentnode = rules.pop(0)
521 ctx = repo[currentnode]
530 ctx = repo[currentnode]
522 # is there any new commit between the expected parent and "."
531 # is there any new commit between the expected parent and "."
523 #
532 #
524 # note: does not take non linear new change in account (but previous
533 # note: does not take non linear new change in account (but previous
525 # implementation didn't used them anyway (issue3655)
534 # implementation didn't used them anyway (issue3655)
526 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
535 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
527 if not newchildren:
536 if not newchildren:
528 # `parentctxnode` should match but no result. This means that
537 # `parentctxnode` should match but no result. This means that
529 # currentnode is not a descendant from parentctxnode.
538 # currentnode is not a descendant from parentctxnode.
530 msg = _('working directory parent is not a descendant of %s')
539 msg = _('working directory parent is not a descendant of %s')
531 hint = _('update to %s or descendant and run "hg histedit '
540 hint = _('update to %s or descendant and run "hg histedit '
532 '--continue" again') % parentctx
541 '--continue" again') % parentctx
533 raise util.Abort(msg % parentctx, hint=hint)
542 raise util.Abort(msg % parentctx, hint=hint)
534 newchildren.pop(0) # remove parentctxnode
543 newchildren.pop(0) # remove parentctxnode
535 # Commit dirty working directory if necessary
544 # Commit dirty working directory if necessary
536 new = None
545 new = None
537 m, a, r, d = repo.status()[:4]
546 m, a, r, d = repo.status()[:4]
538 if m or a or r or d:
547 if m or a or r or d:
539 # prepare the message for the commit to comes
548 # prepare the message for the commit to comes
540 if action in ('f', 'fold'):
549 if action in ('f', 'fold'):
541 message = 'fold-temp-revision %s' % currentnode
550 message = 'fold-temp-revision %s' % currentnode
542 else:
551 else:
543 message = ctx.description() + '\n'
552 message = ctx.description() + '\n'
544 if action in ('e', 'edit', 'm', 'mess'):
553 if action in ('e', 'edit', 'm', 'mess'):
545 editor = cmdutil.commitforceeditor
554 editor = cmdutil.commitforceeditor
546 else:
555 else:
547 editor = False
556 editor = False
548 new = repo.commit(text=message, user=ctx.user(),
557 new = repo.commit(text=message, user=ctx.user(),
549 date=ctx.date(), extra=ctx.extra(),
558 date=ctx.date(), extra=ctx.extra(),
550 editor=editor)
559 editor=editor)
551 if new is not None:
560 if new is not None:
552 newchildren.append(new)
561 newchildren.append(new)
553
562
554 replacements = []
563 replacements = []
555 # track replacements
564 # track replacements
556 if ctx.node() not in newchildren:
565 if ctx.node() not in newchildren:
557 # note: new children may be empty when the changeset is dropped.
566 # note: new children may be empty when the changeset is dropped.
558 # this happen e.g during conflicting pick where we revert content
567 # this happen e.g during conflicting pick where we revert content
559 # to parent.
568 # to parent.
560 replacements.append((ctx.node(), tuple(newchildren)))
569 replacements.append((ctx.node(), tuple(newchildren)))
561
570
562 if action in ('f', 'fold'):
571 if action in ('f', 'fold'):
563 # finalize fold operation if applicable
572 # finalize fold operation if applicable
564 if new is None:
573 if new is None:
565 new = newchildren[-1]
574 new = newchildren[-1]
566 else:
575 else:
567 newchildren.pop() # remove new from internal changes
576 newchildren.pop() # remove new from internal changes
568 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
577 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
569 newchildren)
578 newchildren)
570 replacements.extend(repl)
579 replacements.extend(repl)
571 elif newchildren:
580 elif newchildren:
572 # otherwize update "parentctx" before proceding to further operation
581 # otherwize update "parentctx" before proceding to further operation
573 parentctx = repo[newchildren[-1]]
582 parentctx = repo[newchildren[-1]]
574 return parentctx, replacements
583 return parentctx, replacements
575
584
576
585
577 def between(repo, old, new, keep):
586 def between(repo, old, new, keep):
578 """select and validate the set of revision to edit
587 """select and validate the set of revision to edit
579
588
580 When keep is false, the specified set can't have children."""
589 When keep is false, the specified set can't have children."""
581 revs = [old]
590 revs = [old]
582 current = old
591 current = old
583 while current != new:
592 while current != new:
584 ctx = repo[current]
593 ctx = repo[current]
585 if not keep and len(ctx.children()) > 1:
594 if not keep and len(ctx.children()) > 1:
586 raise util.Abort(_('cannot edit history that would orphan nodes'))
595 raise util.Abort(_('cannot edit history that would orphan nodes'))
587 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
596 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
588 raise util.Abort(_("can't edit history with merges"))
597 raise util.Abort(_("can't edit history with merges"))
589 if not ctx.children():
598 if not ctx.children():
590 current = new
599 current = new
591 else:
600 else:
592 current = ctx.children()[0].node()
601 current = ctx.children()[0].node()
593 revs.append(current)
602 revs.append(current)
594 if len(repo[current].children()) and not keep:
603 if len(repo[current].children()) and not keep:
595 raise util.Abort(_('cannot edit history that would orphan nodes'))
604 raise util.Abort(_('cannot edit history that would orphan nodes'))
596 return revs
605 return revs
597
606
598
607
599 def writestate(repo, parentnode, rules, keep, topmost, replacements):
608 def writestate(repo, parentnode, rules, keep, topmost, replacements):
600 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
609 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
601 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
610 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
602 fp.close()
611 fp.close()
603
612
604 def readstate(repo):
613 def readstate(repo):
605 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
614 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
606 """
615 """
607 fp = open(os.path.join(repo.path, 'histedit-state'))
616 fp = open(os.path.join(repo.path, 'histedit-state'))
608 return pickle.load(fp)
617 return pickle.load(fp)
609
618
610
619
611 def makedesc(c):
620 def makedesc(c):
612 """build a initial action line for a ctx `c`
621 """build a initial action line for a ctx `c`
613
622
614 line are in the form:
623 line are in the form:
615
624
616 pick <hash> <rev> <summary>
625 pick <hash> <rev> <summary>
617 """
626 """
618 summary = ''
627 summary = ''
619 if c.description():
628 if c.description():
620 summary = c.description().splitlines()[0]
629 summary = c.description().splitlines()[0]
621 line = 'pick %s %d %s' % (c, c.rev(), summary)
630 line = 'pick %s %d %s' % (c, c.rev(), summary)
622 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
631 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
623
632
624 def verifyrules(rules, repo, ctxs):
633 def verifyrules(rules, repo, ctxs):
625 """Verify that there exists exactly one edit rule per given changeset.
634 """Verify that there exists exactly one edit rule per given changeset.
626
635
627 Will abort if there are to many or too few rules, a malformed rule,
636 Will abort if there are to many or too few rules, a malformed rule,
628 or a rule on a changeset outside of the user-given range.
637 or a rule on a changeset outside of the user-given range.
629 """
638 """
630 parsed = []
639 parsed = []
631 if len(rules) != len(ctxs):
640 if len(rules) != len(ctxs):
632 raise util.Abort(_('must specify a rule for each changeset once'))
641 raise util.Abort(_('must specify a rule for each changeset once'))
633 for r in rules:
642 for r in rules:
634 if ' ' not in r:
643 if ' ' not in r:
635 raise util.Abort(_('malformed line "%s"') % r)
644 raise util.Abort(_('malformed line "%s"') % r)
636 action, rest = r.split(' ', 1)
645 action, rest = r.split(' ', 1)
637 if ' ' in rest.strip():
646 if ' ' in rest.strip():
638 ha, rest = rest.split(' ', 1)
647 ha, rest = rest.split(' ', 1)
639 else:
648 else:
640 ha = r.strip()
649 ha = r.strip()
641 try:
650 try:
642 if repo[ha] not in ctxs:
651 if repo[ha] not in ctxs:
643 raise util.Abort(
652 raise util.Abort(
644 _('may not use changesets other than the ones listed'))
653 _('may not use changesets other than the ones listed'))
645 except error.RepoError:
654 except error.RepoError:
646 raise util.Abort(_('unknown changeset %s listed') % ha)
655 raise util.Abort(_('unknown changeset %s listed') % ha)
647 if action not in actiontable:
656 if action not in actiontable:
648 raise util.Abort(_('unknown action "%s"') % action)
657 raise util.Abort(_('unknown action "%s"') % action)
649 parsed.append([action, ha])
658 parsed.append([action, ha])
650 return parsed
659 return parsed
651
660
652 def processreplacement(repo, replacements):
661 def processreplacement(repo, replacements):
653 """process the list of replacements to return
662 """process the list of replacements to return
654
663
655 1) the final mapping between original and created nodes
664 1) the final mapping between original and created nodes
656 2) the list of temporary node created by histedit
665 2) the list of temporary node created by histedit
657 3) the list of new commit created by histedit"""
666 3) the list of new commit created by histedit"""
658 allsuccs = set()
667 allsuccs = set()
659 replaced = set()
668 replaced = set()
660 fullmapping = {}
669 fullmapping = {}
661 # initialise basic set
670 # initialise basic set
662 # fullmapping record all operation recorded in replacement
671 # fullmapping record all operation recorded in replacement
663 for rep in replacements:
672 for rep in replacements:
664 allsuccs.update(rep[1])
673 allsuccs.update(rep[1])
665 replaced.add(rep[0])
674 replaced.add(rep[0])
666 fullmapping.setdefault(rep[0], set()).update(rep[1])
675 fullmapping.setdefault(rep[0], set()).update(rep[1])
667 new = allsuccs - replaced
676 new = allsuccs - replaced
668 tmpnodes = allsuccs & replaced
677 tmpnodes = allsuccs & replaced
669 # Reduce content fullmapping into direct relation between original nodes
678 # Reduce content fullmapping into direct relation between original nodes
670 # and final node created during history edition
679 # and final node created during history edition
671 # Dropped changeset are replaced by an empty list
680 # Dropped changeset are replaced by an empty list
672 toproceed = set(fullmapping)
681 toproceed = set(fullmapping)
673 final = {}
682 final = {}
674 while toproceed:
683 while toproceed:
675 for x in list(toproceed):
684 for x in list(toproceed):
676 succs = fullmapping[x]
685 succs = fullmapping[x]
677 for s in list(succs):
686 for s in list(succs):
678 if s in toproceed:
687 if s in toproceed:
679 # non final node with unknown closure
688 # non final node with unknown closure
680 # We can't process this now
689 # We can't process this now
681 break
690 break
682 elif s in final:
691 elif s in final:
683 # non final node, replace with closure
692 # non final node, replace with closure
684 succs.remove(s)
693 succs.remove(s)
685 succs.update(final[s])
694 succs.update(final[s])
686 else:
695 else:
687 final[x] = succs
696 final[x] = succs
688 toproceed.remove(x)
697 toproceed.remove(x)
689 # remove tmpnodes from final mapping
698 # remove tmpnodes from final mapping
690 for n in tmpnodes:
699 for n in tmpnodes:
691 del final[n]
700 del final[n]
692 # we expect all changes involved in final to exist in the repo
701 # we expect all changes involved in final to exist in the repo
693 # turn `final` into list (topologically sorted)
702 # turn `final` into list (topologically sorted)
694 nm = repo.changelog.nodemap
703 nm = repo.changelog.nodemap
695 for prec, succs in final.items():
704 for prec, succs in final.items():
696 final[prec] = sorted(succs, key=nm.get)
705 final[prec] = sorted(succs, key=nm.get)
697
706
698 # computed topmost element (necessary for bookmark)
707 # computed topmost element (necessary for bookmark)
699 if new:
708 if new:
700 newtopmost = max(new, key=repo.changelog.rev)
709 newtopmost = max(new, key=repo.changelog.rev)
701 elif not final:
710 elif not final:
702 # Nothing rewritten at all. we won't need `newtopmost`
711 # Nothing rewritten at all. we won't need `newtopmost`
703 # It is the same as `oldtopmost` and `processreplacement` know it
712 # It is the same as `oldtopmost` and `processreplacement` know it
704 newtopmost = None
713 newtopmost = None
705 else:
714 else:
706 # every body died. The newtopmost is the parent of the root.
715 # every body died. The newtopmost is the parent of the root.
707 newtopmost = repo[min(final, key=repo.changelog.rev)].p1().node()
716 newtopmost = repo[min(final, key=repo.changelog.rev)].p1().node()
708
717
709 return final, tmpnodes, new, newtopmost
718 return final, tmpnodes, new, newtopmost
710
719
711 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
720 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
712 """Move bookmark from old to newly created node"""
721 """Move bookmark from old to newly created node"""
713 if not mapping:
722 if not mapping:
714 # if nothing got rewritten there is not purpose for this function
723 # if nothing got rewritten there is not purpose for this function
715 return
724 return
716 moves = []
725 moves = []
717 for bk, old in repo._bookmarks.iteritems():
726 for bk, old in repo._bookmarks.iteritems():
718 if old == oldtopmost:
727 if old == oldtopmost:
719 # special case ensure bookmark stay on tip.
728 # special case ensure bookmark stay on tip.
720 #
729 #
721 # This is arguably a feature and we may only want that for the
730 # This is arguably a feature and we may only want that for the
722 # active bookmark. But the behavior is kept compatible with the old
731 # active bookmark. But the behavior is kept compatible with the old
723 # version for now.
732 # version for now.
724 moves.append((bk, newtopmost))
733 moves.append((bk, newtopmost))
725 continue
734 continue
726 base = old
735 base = old
727 new = mapping.get(base, None)
736 new = mapping.get(base, None)
728 if new is None:
737 if new is None:
729 continue
738 continue
730 while not new:
739 while not new:
731 # base is killed, trying with parent
740 # base is killed, trying with parent
732 base = repo[base].p1().node()
741 base = repo[base].p1().node()
733 new = mapping.get(base, (base,))
742 new = mapping.get(base, (base,))
734 # nothing to move
743 # nothing to move
735 moves.append((bk, new[-1]))
744 moves.append((bk, new[-1]))
736 if moves:
745 if moves:
737 for mark, new in moves:
746 for mark, new in moves:
738 old = repo._bookmarks[mark]
747 old = repo._bookmarks[mark]
739 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
748 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
740 % (mark, node.short(old), node.short(new)))
749 % (mark, node.short(old), node.short(new)))
741 repo._bookmarks[mark] = new
750 repo._bookmarks[mark] = new
742 bookmarks.write(repo)
751 bookmarks.write(repo)
743
752
744 def cleanupnode(ui, repo, name, nodes):
753 def cleanupnode(ui, repo, name, nodes):
745 """strip a group of nodes from the repository
754 """strip a group of nodes from the repository
746
755
747 The set of node to strip may contains unknown nodes."""
756 The set of node to strip may contains unknown nodes."""
748 ui.debug('should strip %s nodes %s\n' %
757 ui.debug('should strip %s nodes %s\n' %
749 (name, ', '.join([node.short(n) for n in nodes])))
758 (name, ', '.join([node.short(n) for n in nodes])))
750 lock = None
759 lock = None
751 try:
760 try:
752 lock = repo.lock()
761 lock = repo.lock()
753 # Find all node that need to be stripped
762 # Find all node that need to be stripped
754 # (we hg %lr instead of %ln to silently ignore unknown item
763 # (we hg %lr instead of %ln to silently ignore unknown item
755 nm = repo.changelog.nodemap
764 nm = repo.changelog.nodemap
756 nodes = [n for n in nodes if n in nm]
765 nodes = [n for n in nodes if n in nm]
757 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
766 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
758 for c in roots:
767 for c in roots:
759 # We should process node in reverse order to strip tip most first.
768 # We should process node in reverse order to strip tip most first.
760 # but this trigger a bug in changegroup hook.
769 # but this trigger a bug in changegroup hook.
761 # This would reduce bundle overhead
770 # This would reduce bundle overhead
762 repair.strip(ui, repo, c)
771 repair.strip(ui, repo, c)
763 finally:
772 finally:
764 lockmod.release(lock)
773 lockmod.release(lock)
General Comments 0
You need to be logged in to leave comments. Login now