##// END OF EJS Templates
histedit: limit cleanup of histedit-last-edit.txt to success
timeless -
r27546:c00924c5 default
parent child Browse files
Show More
@@ -1,1458 +1,1458 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description
39 # r, roll = like fold, but discard this commit's description
40 # d, drop = remove commit from history
40 # d, drop = remove commit from history
41 # m, mess = edit commit message without changing commit content
41 # m, mess = edit commit message without changing commit content
42 #
42 #
43
43
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 for each revision in your history. For example, if you had meant to add gamma
45 for each revision in your history. For example, if you had meant to add gamma
46 before beta, and then wanted to add delta in the same revision as beta, you
46 before beta, and then wanted to add delta in the same revision as beta, you
47 would reorganize the file to look like this::
47 would reorganize the file to look like this::
48
48
49 pick 030b686bedc4 Add gamma
49 pick 030b686bedc4 Add gamma
50 pick c561b4e977df Add beta
50 pick c561b4e977df Add beta
51 fold 7c2fd3b9020c Add delta
51 fold 7c2fd3b9020c Add delta
52
52
53 # Edit history between c561b4e977df and 7c2fd3b9020c
53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 #
54 #
55 # Commits are listed from least to most recent
55 # Commits are listed from least to most recent
56 #
56 #
57 # Commands:
57 # Commands:
58 # p, pick = use commit
58 # p, pick = use commit
59 # e, edit = use commit, but stop for amending
59 # e, edit = use commit, but stop for amending
60 # f, fold = use commit, but combine it with the one above
60 # f, fold = use commit, but combine it with the one above
61 # r, roll = like fold, but discard this commit's description
61 # r, roll = like fold, but discard this commit's description
62 # d, drop = remove commit from history
62 # d, drop = remove commit from history
63 # m, mess = edit commit message without changing commit content
63 # m, mess = edit commit message without changing commit content
64 #
64 #
65
65
66 At which point you close the editor and ``histedit`` starts working. When you
66 At which point you close the editor and ``histedit`` starts working. When you
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 those revisions together, offering you a chance to clean up the commit message::
68 those revisions together, offering you a chance to clean up the commit message::
69
69
70 Add beta
70 Add beta
71 ***
71 ***
72 Add delta
72 Add delta
73
73
74 Edit the commit message to your liking, then close the editor. For
74 Edit the commit message to your liking, then close the editor. For
75 this example, let's assume that the commit message was changed to
75 this example, let's assume that the commit message was changed to
76 ``Add beta and delta.`` After histedit has run and had a chance to
76 ``Add beta and delta.`` After histedit has run and had a chance to
77 remove any old or temporary revisions it needed, the history looks
77 remove any old or temporary revisions it needed, the history looks
78 like this::
78 like this::
79
79
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 | Add beta and delta.
81 | Add beta and delta.
82 |
82 |
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 | Add gamma
84 | Add gamma
85 |
85 |
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 Add alpha
87 Add alpha
88
88
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 ones) until after it has completed all the editing operations, so it will
90 ones) until after it has completed all the editing operations, so it will
91 probably perform several strip operations when it's done. For the above example,
91 probably perform several strip operations when it's done. For the above example,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 so you might need to be a little patient. You can choose to keep the original
93 so you might need to be a little patient. You can choose to keep the original
94 revisions by passing the ``--keep`` flag.
94 revisions by passing the ``--keep`` flag.
95
95
96 The ``edit`` operation will drop you back to a command prompt,
96 The ``edit`` operation will drop you back to a command prompt,
97 allowing you to edit files freely, or even use ``hg record`` to commit
97 allowing you to edit files freely, or even use ``hg record`` to commit
98 some changes as a separate commit. When you're done, any remaining
98 some changes as a separate commit. When you're done, any remaining
99 uncommitted changes will be committed as well. When done, run ``hg
99 uncommitted changes will be committed as well. When done, run ``hg
100 histedit --continue`` to finish this step. You'll be prompted for a
100 histedit --continue`` to finish this step. You'll be prompted for a
101 new commit message, but the default commit message will be the
101 new commit message, but the default commit message will be the
102 original message for the ``edit`` ed revision.
102 original message for the ``edit`` ed revision.
103
103
104 The ``message`` operation will give you a chance to revise a commit
104 The ``message`` operation will give you a chance to revise a commit
105 message without changing the contents. It's a shortcut for doing
105 message without changing the contents. It's a shortcut for doing
106 ``edit`` immediately followed by `hg histedit --continue``.
106 ``edit`` immediately followed by `hg histedit --continue``.
107
107
108 If ``histedit`` encounters a conflict when moving a revision (while
108 If ``histedit`` encounters a conflict when moving a revision (while
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 ``edit`` with the difference that it won't prompt you for a commit
110 ``edit`` with the difference that it won't prompt you for a commit
111 message when done. If you decide at this point that you don't like how
111 message when done. If you decide at this point that you don't like how
112 much work it will be to rearrange history, or that you made a mistake,
112 much work it will be to rearrange history, or that you made a mistake,
113 you can use ``hg histedit --abort`` to abandon the new changes you
113 you can use ``hg histedit --abort`` to abandon the new changes you
114 have made and return to the state before you attempted to edit your
114 have made and return to the state before you attempted to edit your
115 history.
115 history.
116
116
117 If we clone the histedit-ed example repository above and add four more
117 If we clone the histedit-ed example repository above and add four more
118 changes, such that we have the following history::
118 changes, such that we have the following history::
119
119
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 | Add theta
121 | Add theta
122 |
122 |
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 | Add eta
124 | Add eta
125 |
125 |
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 | Add zeta
127 | Add zeta
128 |
128 |
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 | Add epsilon
130 | Add epsilon
131 |
131 |
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 | Add beta and delta.
133 | Add beta and delta.
134 |
134 |
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 | Add gamma
136 | Add gamma
137 |
137 |
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 Add alpha
139 Add alpha
140
140
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 as running ``hg histedit 836302820282``. If you need plan to push to a
142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 repository that Mercurial does not detect to be related to the source
143 repository that Mercurial does not detect to be related to the source
144 repo, you can add a ``--force`` option.
144 repo, you can add a ``--force`` option.
145
145
146 Config
146 Config
147 ------
147 ------
148
148
149 Histedit rule lines are truncated to 80 characters by default. You
149 Histedit rule lines are truncated to 80 characters by default. You
150 can customize this behavior by setting a different length in your
150 can customize this behavior by setting a different length in your
151 configuration file::
151 configuration file::
152
152
153 [histedit]
153 [histedit]
154 linelen = 120 # truncate rule lines at 120 characters
154 linelen = 120 # truncate rule lines at 120 characters
155
155
156 ``hg histedit`` attempts to automatically choose an appropriate base
156 ``hg histedit`` attempts to automatically choose an appropriate base
157 revision to use. To change which base revision is used, define a
157 revision to use. To change which base revision is used, define a
158 revset in your configuration file::
158 revset in your configuration file::
159
159
160 [histedit]
160 [histedit]
161 defaultrev = only(.) & draft()
161 defaultrev = only(.) & draft()
162
162
163 By default each edited revision needs to be present in histedit commands.
163 By default each edited revision needs to be present in histedit commands.
164 To remove revision you need to use ``drop`` operation. You can configure
164 To remove revision you need to use ``drop`` operation. You can configure
165 the drop to be implicit for missing commits by adding:
165 the drop to be implicit for missing commits by adding:
166
166
167 [histedit]
167 [histedit]
168 dropmissing = True
168 dropmissing = True
169
169
170 """
170 """
171
171
172 import pickle
172 import pickle
173 import errno
173 import errno
174 import os
174 import os
175 import sys
175 import sys
176
176
177 from mercurial import bundle2
177 from mercurial import bundle2
178 from mercurial import cmdutil
178 from mercurial import cmdutil
179 from mercurial import discovery
179 from mercurial import discovery
180 from mercurial import error
180 from mercurial import error
181 from mercurial import copies
181 from mercurial import copies
182 from mercurial import context
182 from mercurial import context
183 from mercurial import destutil
183 from mercurial import destutil
184 from mercurial import exchange
184 from mercurial import exchange
185 from mercurial import extensions
185 from mercurial import extensions
186 from mercurial import hg
186 from mercurial import hg
187 from mercurial import node
187 from mercurial import node
188 from mercurial import repair
188 from mercurial import repair
189 from mercurial import scmutil
189 from mercurial import scmutil
190 from mercurial import util
190 from mercurial import util
191 from mercurial import obsolete
191 from mercurial import obsolete
192 from mercurial import merge as mergemod
192 from mercurial import merge as mergemod
193 from mercurial.lock import release
193 from mercurial.lock import release
194 from mercurial.i18n import _
194 from mercurial.i18n import _
195
195
196 cmdtable = {}
196 cmdtable = {}
197 command = cmdutil.command(cmdtable)
197 command = cmdutil.command(cmdtable)
198
198
199 class _constraints(object):
199 class _constraints(object):
200 # aborts if there are multiple rules for one node
200 # aborts if there are multiple rules for one node
201 noduplicates = 'noduplicates'
201 noduplicates = 'noduplicates'
202 # abort if the node does belong to edited stack
202 # abort if the node does belong to edited stack
203 forceother = 'forceother'
203 forceother = 'forceother'
204 # abort if the node doesn't belong to edited stack
204 # abort if the node doesn't belong to edited stack
205 noother = 'noother'
205 noother = 'noother'
206
206
207 @classmethod
207 @classmethod
208 def known(cls):
208 def known(cls):
209 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
209 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
210
210
211 # Note for extension authors: ONLY specify testedwith = 'internal' for
211 # Note for extension authors: ONLY specify testedwith = 'internal' for
212 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
212 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
213 # be specifying the version(s) of Mercurial they are tested with, or
213 # be specifying the version(s) of Mercurial they are tested with, or
214 # leave the attribute unspecified.
214 # leave the attribute unspecified.
215 testedwith = 'internal'
215 testedwith = 'internal'
216
216
217 # i18n: command names and abbreviations must remain untranslated
217 # i18n: command names and abbreviations must remain untranslated
218 editcomment = _("""# Edit history between %s and %s
218 editcomment = _("""# Edit history between %s and %s
219 #
219 #
220 # Commits are listed from least to most recent
220 # Commits are listed from least to most recent
221 #
221 #
222 # Commands:
222 # Commands:
223 # p, pick = use commit
223 # p, pick = use commit
224 # e, edit = use commit, but stop for amending
224 # e, edit = use commit, but stop for amending
225 # f, fold = use commit, but combine it with the one above
225 # f, fold = use commit, but combine it with the one above
226 # r, roll = like fold, but discard this commit's description
226 # r, roll = like fold, but discard this commit's description
227 # d, drop = remove commit from history
227 # d, drop = remove commit from history
228 # m, mess = edit commit message without changing commit content
228 # m, mess = edit commit message without changing commit content
229 #
229 #
230 """)
230 """)
231
231
232 class histeditstate(object):
232 class histeditstate(object):
233 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
233 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
234 topmost=None, replacements=None, lock=None, wlock=None):
234 topmost=None, replacements=None, lock=None, wlock=None):
235 self.repo = repo
235 self.repo = repo
236 self.actions = actions
236 self.actions = actions
237 self.keep = keep
237 self.keep = keep
238 self.topmost = topmost
238 self.topmost = topmost
239 self.parentctxnode = parentctxnode
239 self.parentctxnode = parentctxnode
240 self.lock = lock
240 self.lock = lock
241 self.wlock = wlock
241 self.wlock = wlock
242 self.backupfile = None
242 self.backupfile = None
243 if replacements is None:
243 if replacements is None:
244 self.replacements = []
244 self.replacements = []
245 else:
245 else:
246 self.replacements = replacements
246 self.replacements = replacements
247
247
248 def read(self):
248 def read(self):
249 """Load histedit state from disk and set fields appropriately."""
249 """Load histedit state from disk and set fields appropriately."""
250 try:
250 try:
251 state = self.repo.vfs.read('histedit-state')
251 state = self.repo.vfs.read('histedit-state')
252 except IOError as err:
252 except IOError as err:
253 if err.errno != errno.ENOENT:
253 if err.errno != errno.ENOENT:
254 raise
254 raise
255 raise error.Abort(_('no histedit in progress'))
255 raise error.Abort(_('no histedit in progress'))
256
256
257 if state.startswith('v1\n'):
257 if state.startswith('v1\n'):
258 data = self._load()
258 data = self._load()
259 parentctxnode, rules, keep, topmost, replacements, backupfile = data
259 parentctxnode, rules, keep, topmost, replacements, backupfile = data
260 else:
260 else:
261 data = pickle.loads(state)
261 data = pickle.loads(state)
262 parentctxnode, rules, keep, topmost, replacements = data
262 parentctxnode, rules, keep, topmost, replacements = data
263 backupfile = None
263 backupfile = None
264
264
265 self.parentctxnode = parentctxnode
265 self.parentctxnode = parentctxnode
266 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
266 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
267 actions = parserules(rules, self)
267 actions = parserules(rules, self)
268 self.actions = actions
268 self.actions = actions
269 self.keep = keep
269 self.keep = keep
270 self.topmost = topmost
270 self.topmost = topmost
271 self.replacements = replacements
271 self.replacements = replacements
272 self.backupfile = backupfile
272 self.backupfile = backupfile
273
273
274 def write(self):
274 def write(self):
275 fp = self.repo.vfs('histedit-state', 'w')
275 fp = self.repo.vfs('histedit-state', 'w')
276 fp.write('v1\n')
276 fp.write('v1\n')
277 fp.write('%s\n' % node.hex(self.parentctxnode))
277 fp.write('%s\n' % node.hex(self.parentctxnode))
278 fp.write('%s\n' % node.hex(self.topmost))
278 fp.write('%s\n' % node.hex(self.topmost))
279 fp.write('%s\n' % self.keep)
279 fp.write('%s\n' % self.keep)
280 fp.write('%d\n' % len(self.actions))
280 fp.write('%d\n' % len(self.actions))
281 for action in self.actions:
281 for action in self.actions:
282 fp.write('%s\n' % action.tostate())
282 fp.write('%s\n' % action.tostate())
283 fp.write('%d\n' % len(self.replacements))
283 fp.write('%d\n' % len(self.replacements))
284 for replacement in self.replacements:
284 for replacement in self.replacements:
285 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
285 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
286 for r in replacement[1])))
286 for r in replacement[1])))
287 backupfile = self.backupfile
287 backupfile = self.backupfile
288 if not backupfile:
288 if not backupfile:
289 backupfile = ''
289 backupfile = ''
290 fp.write('%s\n' % backupfile)
290 fp.write('%s\n' % backupfile)
291 fp.close()
291 fp.close()
292
292
293 def _load(self):
293 def _load(self):
294 fp = self.repo.vfs('histedit-state', 'r')
294 fp = self.repo.vfs('histedit-state', 'r')
295 lines = [l[:-1] for l in fp.readlines()]
295 lines = [l[:-1] for l in fp.readlines()]
296
296
297 index = 0
297 index = 0
298 lines[index] # version number
298 lines[index] # version number
299 index += 1
299 index += 1
300
300
301 parentctxnode = node.bin(lines[index])
301 parentctxnode = node.bin(lines[index])
302 index += 1
302 index += 1
303
303
304 topmost = node.bin(lines[index])
304 topmost = node.bin(lines[index])
305 index += 1
305 index += 1
306
306
307 keep = lines[index] == 'True'
307 keep = lines[index] == 'True'
308 index += 1
308 index += 1
309
309
310 # Rules
310 # Rules
311 rules = []
311 rules = []
312 rulelen = int(lines[index])
312 rulelen = int(lines[index])
313 index += 1
313 index += 1
314 for i in xrange(rulelen):
314 for i in xrange(rulelen):
315 ruleaction = lines[index]
315 ruleaction = lines[index]
316 index += 1
316 index += 1
317 rule = lines[index]
317 rule = lines[index]
318 index += 1
318 index += 1
319 rules.append((ruleaction, rule))
319 rules.append((ruleaction, rule))
320
320
321 # Replacements
321 # Replacements
322 replacements = []
322 replacements = []
323 replacementlen = int(lines[index])
323 replacementlen = int(lines[index])
324 index += 1
324 index += 1
325 for i in xrange(replacementlen):
325 for i in xrange(replacementlen):
326 replacement = lines[index]
326 replacement = lines[index]
327 original = node.bin(replacement[:40])
327 original = node.bin(replacement[:40])
328 succ = [node.bin(replacement[i:i + 40]) for i in
328 succ = [node.bin(replacement[i:i + 40]) for i in
329 range(40, len(replacement), 40)]
329 range(40, len(replacement), 40)]
330 replacements.append((original, succ))
330 replacements.append((original, succ))
331 index += 1
331 index += 1
332
332
333 backupfile = lines[index]
333 backupfile = lines[index]
334 index += 1
334 index += 1
335
335
336 fp.close()
336 fp.close()
337
337
338 return parentctxnode, rules, keep, topmost, replacements, backupfile
338 return parentctxnode, rules, keep, topmost, replacements, backupfile
339
339
340 def clear(self):
340 def clear(self):
341 if self.inprogress():
341 if self.inprogress():
342 self.repo.vfs.unlink('histedit-state')
342 self.repo.vfs.unlink('histedit-state')
343
343
344 def inprogress(self):
344 def inprogress(self):
345 return self.repo.vfs.exists('histedit-state')
345 return self.repo.vfs.exists('histedit-state')
346
346
347
347
348 class histeditaction(object):
348 class histeditaction(object):
349 def __init__(self, state, node):
349 def __init__(self, state, node):
350 self.state = state
350 self.state = state
351 self.repo = state.repo
351 self.repo = state.repo
352 self.node = node
352 self.node = node
353
353
354 @classmethod
354 @classmethod
355 def fromrule(cls, state, rule):
355 def fromrule(cls, state, rule):
356 """Parses the given rule, returning an instance of the histeditaction.
356 """Parses the given rule, returning an instance of the histeditaction.
357 """
357 """
358 rulehash = rule.strip().split(' ', 1)[0]
358 rulehash = rule.strip().split(' ', 1)[0]
359 return cls(state, node.bin(rulehash))
359 return cls(state, node.bin(rulehash))
360
360
361 def verify(self, prev):
361 def verify(self, prev):
362 """ Verifies semantic correctness of the rule"""
362 """ Verifies semantic correctness of the rule"""
363 repo = self.repo
363 repo = self.repo
364 ha = node.hex(self.node)
364 ha = node.hex(self.node)
365 try:
365 try:
366 self.node = repo[ha].node()
366 self.node = repo[ha].node()
367 except error.RepoError:
367 except error.RepoError:
368 raise error.ParseError(_('unknown changeset %s listed')
368 raise error.ParseError(_('unknown changeset %s listed')
369 % ha[:12])
369 % ha[:12])
370
370
371 def torule(self):
371 def torule(self):
372 """build a histedit rule line for an action
372 """build a histedit rule line for an action
373
373
374 by default lines are in the form:
374 by default lines are in the form:
375 <hash> <rev> <summary>
375 <hash> <rev> <summary>
376 """
376 """
377 ctx = self.repo[self.node]
377 ctx = self.repo[self.node]
378 summary = ''
378 summary = ''
379 if ctx.description():
379 if ctx.description():
380 summary = ctx.description().splitlines()[0]
380 summary = ctx.description().splitlines()[0]
381 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
381 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
382 # trim to 75 columns by default so it's not stupidly wide in my editor
382 # trim to 75 columns by default so it's not stupidly wide in my editor
383 # (the 5 more are left for verb)
383 # (the 5 more are left for verb)
384 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
384 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
385 maxlen = max(maxlen, 22) # avoid truncating hash
385 maxlen = max(maxlen, 22) # avoid truncating hash
386 return util.ellipsis(line, maxlen)
386 return util.ellipsis(line, maxlen)
387
387
388 def tostate(self):
388 def tostate(self):
389 """Print an action in format used by histedit state files
389 """Print an action in format used by histedit state files
390 (the first line is a verb, the remainder is the second)
390 (the first line is a verb, the remainder is the second)
391 """
391 """
392 return "%s\n%s" % (self.verb, node.hex(self.node))
392 return "%s\n%s" % (self.verb, node.hex(self.node))
393
393
394 def constraints(self):
394 def constraints(self):
395 """Return a set of constrains that this action should be verified for
395 """Return a set of constrains that this action should be verified for
396 """
396 """
397 return set([_constraints.noduplicates, _constraints.noother])
397 return set([_constraints.noduplicates, _constraints.noother])
398
398
399 def nodetoverify(self):
399 def nodetoverify(self):
400 """Returns a node associated with the action that will be used for
400 """Returns a node associated with the action that will be used for
401 verification purposes.
401 verification purposes.
402
402
403 If the action doesn't correspond to node it should return None
403 If the action doesn't correspond to node it should return None
404 """
404 """
405 return self.node
405 return self.node
406
406
407 def run(self):
407 def run(self):
408 """Runs the action. The default behavior is simply apply the action's
408 """Runs the action. The default behavior is simply apply the action's
409 rulectx onto the current parentctx."""
409 rulectx onto the current parentctx."""
410 self.applychange()
410 self.applychange()
411 self.continuedirty()
411 self.continuedirty()
412 return self.continueclean()
412 return self.continueclean()
413
413
414 def applychange(self):
414 def applychange(self):
415 """Applies the changes from this action's rulectx onto the current
415 """Applies the changes from this action's rulectx onto the current
416 parentctx, but does not commit them."""
416 parentctx, but does not commit them."""
417 repo = self.repo
417 repo = self.repo
418 rulectx = repo[self.node]
418 rulectx = repo[self.node]
419 hg.update(repo, self.state.parentctxnode, quietempty=True)
419 hg.update(repo, self.state.parentctxnode, quietempty=True)
420 stats = applychanges(repo.ui, repo, rulectx, {})
420 stats = applychanges(repo.ui, repo, rulectx, {})
421 if stats and stats[3] > 0:
421 if stats and stats[3] > 0:
422 raise error.InterventionRequired(_('Fix up the change and run '
422 raise error.InterventionRequired(_('Fix up the change and run '
423 'hg histedit --continue'))
423 'hg histedit --continue'))
424
424
425 def continuedirty(self):
425 def continuedirty(self):
426 """Continues the action when changes have been applied to the working
426 """Continues the action when changes have been applied to the working
427 copy. The default behavior is to commit the dirty changes."""
427 copy. The default behavior is to commit the dirty changes."""
428 repo = self.repo
428 repo = self.repo
429 rulectx = repo[self.node]
429 rulectx = repo[self.node]
430
430
431 editor = self.commiteditor()
431 editor = self.commiteditor()
432 commit = commitfuncfor(repo, rulectx)
432 commit = commitfuncfor(repo, rulectx)
433
433
434 commit(text=rulectx.description(), user=rulectx.user(),
434 commit(text=rulectx.description(), user=rulectx.user(),
435 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
435 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
436
436
437 def commiteditor(self):
437 def commiteditor(self):
438 """The editor to be used to edit the commit message."""
438 """The editor to be used to edit the commit message."""
439 return False
439 return False
440
440
441 def continueclean(self):
441 def continueclean(self):
442 """Continues the action when the working copy is clean. The default
442 """Continues the action when the working copy is clean. The default
443 behavior is to accept the current commit as the new version of the
443 behavior is to accept the current commit as the new version of the
444 rulectx."""
444 rulectx."""
445 ctx = self.repo['.']
445 ctx = self.repo['.']
446 if ctx.node() == self.state.parentctxnode:
446 if ctx.node() == self.state.parentctxnode:
447 self.repo.ui.warn(_('%s: empty changeset\n') %
447 self.repo.ui.warn(_('%s: empty changeset\n') %
448 node.short(self.node))
448 node.short(self.node))
449 return ctx, [(self.node, tuple())]
449 return ctx, [(self.node, tuple())]
450 if ctx.node() == self.node:
450 if ctx.node() == self.node:
451 # Nothing changed
451 # Nothing changed
452 return ctx, []
452 return ctx, []
453 return ctx, [(self.node, (ctx.node(),))]
453 return ctx, [(self.node, (ctx.node(),))]
454
454
455 def commitfuncfor(repo, src):
455 def commitfuncfor(repo, src):
456 """Build a commit function for the replacement of <src>
456 """Build a commit function for the replacement of <src>
457
457
458 This function ensure we apply the same treatment to all changesets.
458 This function ensure we apply the same treatment to all changesets.
459
459
460 - Add a 'histedit_source' entry in extra.
460 - Add a 'histedit_source' entry in extra.
461
461
462 Note that fold has its own separated logic because its handling is a bit
462 Note that fold has its own separated logic because its handling is a bit
463 different and not easily factored out of the fold method.
463 different and not easily factored out of the fold method.
464 """
464 """
465 phasemin = src.phase()
465 phasemin = src.phase()
466 def commitfunc(**kwargs):
466 def commitfunc(**kwargs):
467 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
467 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
468 try:
468 try:
469 repo.ui.setconfig('phases', 'new-commit', phasemin,
469 repo.ui.setconfig('phases', 'new-commit', phasemin,
470 'histedit')
470 'histedit')
471 extra = kwargs.get('extra', {}).copy()
471 extra = kwargs.get('extra', {}).copy()
472 extra['histedit_source'] = src.hex()
472 extra['histedit_source'] = src.hex()
473 kwargs['extra'] = extra
473 kwargs['extra'] = extra
474 return repo.commit(**kwargs)
474 return repo.commit(**kwargs)
475 finally:
475 finally:
476 repo.ui.restoreconfig(phasebackup)
476 repo.ui.restoreconfig(phasebackup)
477 return commitfunc
477 return commitfunc
478
478
479 def applychanges(ui, repo, ctx, opts):
479 def applychanges(ui, repo, ctx, opts):
480 """Merge changeset from ctx (only) in the current working directory"""
480 """Merge changeset from ctx (only) in the current working directory"""
481 wcpar = repo.dirstate.parents()[0]
481 wcpar = repo.dirstate.parents()[0]
482 if ctx.p1().node() == wcpar:
482 if ctx.p1().node() == wcpar:
483 # edits are "in place" we do not need to make any merge,
483 # edits are "in place" we do not need to make any merge,
484 # just applies changes on parent for edition
484 # just applies changes on parent for edition
485 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
485 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
486 stats = None
486 stats = None
487 else:
487 else:
488 try:
488 try:
489 # ui.forcemerge is an internal variable, do not document
489 # ui.forcemerge is an internal variable, do not document
490 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
490 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
491 'histedit')
491 'histedit')
492 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
492 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
493 finally:
493 finally:
494 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
494 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
495 return stats
495 return stats
496
496
497 def collapse(repo, first, last, commitopts, skipprompt=False):
497 def collapse(repo, first, last, commitopts, skipprompt=False):
498 """collapse the set of revisions from first to last as new one.
498 """collapse the set of revisions from first to last as new one.
499
499
500 Expected commit options are:
500 Expected commit options are:
501 - message
501 - message
502 - date
502 - date
503 - username
503 - username
504 Commit message is edited in all cases.
504 Commit message is edited in all cases.
505
505
506 This function works in memory."""
506 This function works in memory."""
507 ctxs = list(repo.set('%d::%d', first, last))
507 ctxs = list(repo.set('%d::%d', first, last))
508 if not ctxs:
508 if not ctxs:
509 return None
509 return None
510 for c in ctxs:
510 for c in ctxs:
511 if not c.mutable():
511 if not c.mutable():
512 raise error.ParseError(
512 raise error.ParseError(
513 _("cannot fold into public change %s") % node.short(c.node()))
513 _("cannot fold into public change %s") % node.short(c.node()))
514 base = first.parents()[0]
514 base = first.parents()[0]
515
515
516 # commit a new version of the old changeset, including the update
516 # commit a new version of the old changeset, including the update
517 # collect all files which might be affected
517 # collect all files which might be affected
518 files = set()
518 files = set()
519 for ctx in ctxs:
519 for ctx in ctxs:
520 files.update(ctx.files())
520 files.update(ctx.files())
521
521
522 # Recompute copies (avoid recording a -> b -> a)
522 # Recompute copies (avoid recording a -> b -> a)
523 copied = copies.pathcopies(base, last)
523 copied = copies.pathcopies(base, last)
524
524
525 # prune files which were reverted by the updates
525 # prune files which were reverted by the updates
526 def samefile(f):
526 def samefile(f):
527 if f in last.manifest():
527 if f in last.manifest():
528 a = last.filectx(f)
528 a = last.filectx(f)
529 if f in base.manifest():
529 if f in base.manifest():
530 b = base.filectx(f)
530 b = base.filectx(f)
531 return (a.data() == b.data()
531 return (a.data() == b.data()
532 and a.flags() == b.flags())
532 and a.flags() == b.flags())
533 else:
533 else:
534 return False
534 return False
535 else:
535 else:
536 return f not in base.manifest()
536 return f not in base.manifest()
537 files = [f for f in files if not samefile(f)]
537 files = [f for f in files if not samefile(f)]
538 # commit version of these files as defined by head
538 # commit version of these files as defined by head
539 headmf = last.manifest()
539 headmf = last.manifest()
540 def filectxfn(repo, ctx, path):
540 def filectxfn(repo, ctx, path):
541 if path in headmf:
541 if path in headmf:
542 fctx = last[path]
542 fctx = last[path]
543 flags = fctx.flags()
543 flags = fctx.flags()
544 mctx = context.memfilectx(repo,
544 mctx = context.memfilectx(repo,
545 fctx.path(), fctx.data(),
545 fctx.path(), fctx.data(),
546 islink='l' in flags,
546 islink='l' in flags,
547 isexec='x' in flags,
547 isexec='x' in flags,
548 copied=copied.get(path))
548 copied=copied.get(path))
549 return mctx
549 return mctx
550 return None
550 return None
551
551
552 if commitopts.get('message'):
552 if commitopts.get('message'):
553 message = commitopts['message']
553 message = commitopts['message']
554 else:
554 else:
555 message = first.description()
555 message = first.description()
556 user = commitopts.get('user')
556 user = commitopts.get('user')
557 date = commitopts.get('date')
557 date = commitopts.get('date')
558 extra = commitopts.get('extra')
558 extra = commitopts.get('extra')
559
559
560 parents = (first.p1().node(), first.p2().node())
560 parents = (first.p1().node(), first.p2().node())
561 editor = None
561 editor = None
562 if not skipprompt:
562 if not skipprompt:
563 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
563 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
564 new = context.memctx(repo,
564 new = context.memctx(repo,
565 parents=parents,
565 parents=parents,
566 text=message,
566 text=message,
567 files=files,
567 files=files,
568 filectxfn=filectxfn,
568 filectxfn=filectxfn,
569 user=user,
569 user=user,
570 date=date,
570 date=date,
571 extra=extra,
571 extra=extra,
572 editor=editor)
572 editor=editor)
573 return repo.commitctx(new)
573 return repo.commitctx(new)
574
574
575 def _isdirtywc(repo):
575 def _isdirtywc(repo):
576 return repo[None].dirty(missing=True)
576 return repo[None].dirty(missing=True)
577
577
578 def abortdirty():
578 def abortdirty():
579 raise error.Abort(_('working copy has pending changes'),
579 raise error.Abort(_('working copy has pending changes'),
580 hint=_('amend, commit, or revert them and run histedit '
580 hint=_('amend, commit, or revert them and run histedit '
581 '--continue, or abort with histedit --abort'))
581 '--continue, or abort with histedit --abort'))
582
582
583
583
584 actiontable = {}
584 actiontable = {}
585 actionlist = []
585 actionlist = []
586
586
587 def addhisteditaction(verbs):
587 def addhisteditaction(verbs):
588 def wrap(cls):
588 def wrap(cls):
589 cls.verb = verbs[0]
589 cls.verb = verbs[0]
590 for verb in verbs:
590 for verb in verbs:
591 actiontable[verb] = cls
591 actiontable[verb] = cls
592 actionlist.append(cls)
592 actionlist.append(cls)
593 return cls
593 return cls
594 return wrap
594 return wrap
595
595
596
596
597 @addhisteditaction(['pick', 'p'])
597 @addhisteditaction(['pick', 'p'])
598 class pick(histeditaction):
598 class pick(histeditaction):
599 def run(self):
599 def run(self):
600 rulectx = self.repo[self.node]
600 rulectx = self.repo[self.node]
601 if rulectx.parents()[0].node() == self.state.parentctxnode:
601 if rulectx.parents()[0].node() == self.state.parentctxnode:
602 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
602 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
603 return rulectx, []
603 return rulectx, []
604
604
605 return super(pick, self).run()
605 return super(pick, self).run()
606
606
607 @addhisteditaction(['edit', 'e'])
607 @addhisteditaction(['edit', 'e'])
608 class edit(histeditaction):
608 class edit(histeditaction):
609 def run(self):
609 def run(self):
610 repo = self.repo
610 repo = self.repo
611 rulectx = repo[self.node]
611 rulectx = repo[self.node]
612 hg.update(repo, self.state.parentctxnode, quietempty=True)
612 hg.update(repo, self.state.parentctxnode, quietempty=True)
613 applychanges(repo.ui, repo, rulectx, {})
613 applychanges(repo.ui, repo, rulectx, {})
614 raise error.InterventionRequired(
614 raise error.InterventionRequired(
615 _('Make changes as needed, you may commit or record as needed '
615 _('Make changes as needed, you may commit or record as needed '
616 'now.\nWhen you are finished, run hg histedit --continue to '
616 'now.\nWhen you are finished, run hg histedit --continue to '
617 'resume.'))
617 'resume.'))
618
618
619 def commiteditor(self):
619 def commiteditor(self):
620 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
620 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
621
621
622 @addhisteditaction(['fold', 'f'])
622 @addhisteditaction(['fold', 'f'])
623 class fold(histeditaction):
623 class fold(histeditaction):
624 def verify(self, prev):
624 def verify(self, prev):
625 """ Verifies semantic correctness of the fold rule"""
625 """ Verifies semantic correctness of the fold rule"""
626 super(fold, self).verify(prev)
626 super(fold, self).verify(prev)
627 repo = self.repo
627 repo = self.repo
628 state = self.state
628 state = self.state
629 if not prev:
629 if not prev:
630 c = repo[self.node].parents()[0]
630 c = repo[self.node].parents()[0]
631 elif not prev.verb in ('pick', 'base'):
631 elif not prev.verb in ('pick', 'base'):
632 return
632 return
633 else:
633 else:
634 c = repo[prev.node]
634 c = repo[prev.node]
635 if not c.mutable():
635 if not c.mutable():
636 raise error.ParseError(
636 raise error.ParseError(
637 _("cannot fold into public change %s") % node.short(c.node()))
637 _("cannot fold into public change %s") % node.short(c.node()))
638
638
639
639
640 def continuedirty(self):
640 def continuedirty(self):
641 repo = self.repo
641 repo = self.repo
642 rulectx = repo[self.node]
642 rulectx = repo[self.node]
643
643
644 commit = commitfuncfor(repo, rulectx)
644 commit = commitfuncfor(repo, rulectx)
645 commit(text='fold-temp-revision %s' % node.short(self.node),
645 commit(text='fold-temp-revision %s' % node.short(self.node),
646 user=rulectx.user(), date=rulectx.date(),
646 user=rulectx.user(), date=rulectx.date(),
647 extra=rulectx.extra())
647 extra=rulectx.extra())
648
648
649 def continueclean(self):
649 def continueclean(self):
650 repo = self.repo
650 repo = self.repo
651 ctx = repo['.']
651 ctx = repo['.']
652 rulectx = repo[self.node]
652 rulectx = repo[self.node]
653 parentctxnode = self.state.parentctxnode
653 parentctxnode = self.state.parentctxnode
654 if ctx.node() == parentctxnode:
654 if ctx.node() == parentctxnode:
655 repo.ui.warn(_('%s: empty changeset\n') %
655 repo.ui.warn(_('%s: empty changeset\n') %
656 node.short(self.node))
656 node.short(self.node))
657 return ctx, [(self.node, (parentctxnode,))]
657 return ctx, [(self.node, (parentctxnode,))]
658
658
659 parentctx = repo[parentctxnode]
659 parentctx = repo[parentctxnode]
660 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
660 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
661 parentctx))
661 parentctx))
662 if not newcommits:
662 if not newcommits:
663 repo.ui.warn(_('%s: cannot fold - working copy is not a '
663 repo.ui.warn(_('%s: cannot fold - working copy is not a '
664 'descendant of previous commit %s\n') %
664 'descendant of previous commit %s\n') %
665 (node.short(self.node), node.short(parentctxnode)))
665 (node.short(self.node), node.short(parentctxnode)))
666 return ctx, [(self.node, (ctx.node(),))]
666 return ctx, [(self.node, (ctx.node(),))]
667
667
668 middlecommits = newcommits.copy()
668 middlecommits = newcommits.copy()
669 middlecommits.discard(ctx.node())
669 middlecommits.discard(ctx.node())
670
670
671 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
671 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
672 middlecommits)
672 middlecommits)
673
673
674 def skipprompt(self):
674 def skipprompt(self):
675 """Returns true if the rule should skip the message editor.
675 """Returns true if the rule should skip the message editor.
676
676
677 For example, 'fold' wants to show an editor, but 'rollup'
677 For example, 'fold' wants to show an editor, but 'rollup'
678 doesn't want to.
678 doesn't want to.
679 """
679 """
680 return False
680 return False
681
681
682 def mergedescs(self):
682 def mergedescs(self):
683 """Returns true if the rule should merge messages of multiple changes.
683 """Returns true if the rule should merge messages of multiple changes.
684
684
685 This exists mainly so that 'rollup' rules can be a subclass of
685 This exists mainly so that 'rollup' rules can be a subclass of
686 'fold'.
686 'fold'.
687 """
687 """
688 return True
688 return True
689
689
690 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
690 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
691 parent = ctx.parents()[0].node()
691 parent = ctx.parents()[0].node()
692 hg.update(repo, parent)
692 hg.update(repo, parent)
693 ### prepare new commit data
693 ### prepare new commit data
694 commitopts = {}
694 commitopts = {}
695 commitopts['user'] = ctx.user()
695 commitopts['user'] = ctx.user()
696 # commit message
696 # commit message
697 if not self.mergedescs():
697 if not self.mergedescs():
698 newmessage = ctx.description()
698 newmessage = ctx.description()
699 else:
699 else:
700 newmessage = '\n***\n'.join(
700 newmessage = '\n***\n'.join(
701 [ctx.description()] +
701 [ctx.description()] +
702 [repo[r].description() for r in internalchanges] +
702 [repo[r].description() for r in internalchanges] +
703 [oldctx.description()]) + '\n'
703 [oldctx.description()]) + '\n'
704 commitopts['message'] = newmessage
704 commitopts['message'] = newmessage
705 # date
705 # date
706 commitopts['date'] = max(ctx.date(), oldctx.date())
706 commitopts['date'] = max(ctx.date(), oldctx.date())
707 extra = ctx.extra().copy()
707 extra = ctx.extra().copy()
708 # histedit_source
708 # histedit_source
709 # note: ctx is likely a temporary commit but that the best we can do
709 # note: ctx is likely a temporary commit but that the best we can do
710 # here. This is sufficient to solve issue3681 anyway.
710 # here. This is sufficient to solve issue3681 anyway.
711 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
711 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
712 commitopts['extra'] = extra
712 commitopts['extra'] = extra
713 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
713 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
714 try:
714 try:
715 phasemin = max(ctx.phase(), oldctx.phase())
715 phasemin = max(ctx.phase(), oldctx.phase())
716 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
716 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
717 n = collapse(repo, ctx, repo[newnode], commitopts,
717 n = collapse(repo, ctx, repo[newnode], commitopts,
718 skipprompt=self.skipprompt())
718 skipprompt=self.skipprompt())
719 finally:
719 finally:
720 repo.ui.restoreconfig(phasebackup)
720 repo.ui.restoreconfig(phasebackup)
721 if n is None:
721 if n is None:
722 return ctx, []
722 return ctx, []
723 hg.update(repo, n)
723 hg.update(repo, n)
724 replacements = [(oldctx.node(), (newnode,)),
724 replacements = [(oldctx.node(), (newnode,)),
725 (ctx.node(), (n,)),
725 (ctx.node(), (n,)),
726 (newnode, (n,)),
726 (newnode, (n,)),
727 ]
727 ]
728 for ich in internalchanges:
728 for ich in internalchanges:
729 replacements.append((ich, (n,)))
729 replacements.append((ich, (n,)))
730 return repo[n], replacements
730 return repo[n], replacements
731
731
732 class base(histeditaction):
732 class base(histeditaction):
733 def constraints(self):
733 def constraints(self):
734 return set([_constraints.forceother])
734 return set([_constraints.forceother])
735
735
736 def run(self):
736 def run(self):
737 if self.repo['.'].node() != self.node:
737 if self.repo['.'].node() != self.node:
738 mergemod.update(self.repo, self.node, False, True)
738 mergemod.update(self.repo, self.node, False, True)
739 # branchmerge, force)
739 # branchmerge, force)
740 return self.continueclean()
740 return self.continueclean()
741
741
742 def continuedirty(self):
742 def continuedirty(self):
743 abortdirty()
743 abortdirty()
744
744
745 def continueclean(self):
745 def continueclean(self):
746 basectx = self.repo['.']
746 basectx = self.repo['.']
747 return basectx, []
747 return basectx, []
748
748
749 @addhisteditaction(['_multifold'])
749 @addhisteditaction(['_multifold'])
750 class _multifold(fold):
750 class _multifold(fold):
751 """fold subclass used for when multiple folds happen in a row
751 """fold subclass used for when multiple folds happen in a row
752
752
753 We only want to fire the editor for the folded message once when
753 We only want to fire the editor for the folded message once when
754 (say) four changes are folded down into a single change. This is
754 (say) four changes are folded down into a single change. This is
755 similar to rollup, but we should preserve both messages so that
755 similar to rollup, but we should preserve both messages so that
756 when the last fold operation runs we can show the user all the
756 when the last fold operation runs we can show the user all the
757 commit messages in their editor.
757 commit messages in their editor.
758 """
758 """
759 def skipprompt(self):
759 def skipprompt(self):
760 return True
760 return True
761
761
762 @addhisteditaction(["roll", "r"])
762 @addhisteditaction(["roll", "r"])
763 class rollup(fold):
763 class rollup(fold):
764 def mergedescs(self):
764 def mergedescs(self):
765 return False
765 return False
766
766
767 def skipprompt(self):
767 def skipprompt(self):
768 return True
768 return True
769
769
770 @addhisteditaction(["drop", "d"])
770 @addhisteditaction(["drop", "d"])
771 class drop(histeditaction):
771 class drop(histeditaction):
772 def run(self):
772 def run(self):
773 parentctx = self.repo[self.state.parentctxnode]
773 parentctx = self.repo[self.state.parentctxnode]
774 return parentctx, [(self.node, tuple())]
774 return parentctx, [(self.node, tuple())]
775
775
776 @addhisteditaction(["mess", "m"])
776 @addhisteditaction(["mess", "m"])
777 class message(histeditaction):
777 class message(histeditaction):
778 def commiteditor(self):
778 def commiteditor(self):
779 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
779 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
780
780
781 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
781 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
782 """utility function to find the first outgoing changeset
782 """utility function to find the first outgoing changeset
783
783
784 Used by initialization code"""
784 Used by initialization code"""
785 if opts is None:
785 if opts is None:
786 opts = {}
786 opts = {}
787 dest = ui.expandpath(remote or 'default-push', remote or 'default')
787 dest = ui.expandpath(remote or 'default-push', remote or 'default')
788 dest, revs = hg.parseurl(dest, None)[:2]
788 dest, revs = hg.parseurl(dest, None)[:2]
789 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
789 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
790
790
791 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
791 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
792 other = hg.peer(repo, opts, dest)
792 other = hg.peer(repo, opts, dest)
793
793
794 if revs:
794 if revs:
795 revs = [repo.lookup(rev) for rev in revs]
795 revs = [repo.lookup(rev) for rev in revs]
796
796
797 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
797 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
798 if not outgoing.missing:
798 if not outgoing.missing:
799 raise error.Abort(_('no outgoing ancestors'))
799 raise error.Abort(_('no outgoing ancestors'))
800 roots = list(repo.revs("roots(%ln)", outgoing.missing))
800 roots = list(repo.revs("roots(%ln)", outgoing.missing))
801 if 1 < len(roots):
801 if 1 < len(roots):
802 msg = _('there are ambiguous outgoing revisions')
802 msg = _('there are ambiguous outgoing revisions')
803 hint = _('see "hg help histedit" for more detail')
803 hint = _('see "hg help histedit" for more detail')
804 raise error.Abort(msg, hint=hint)
804 raise error.Abort(msg, hint=hint)
805 return repo.lookup(roots[0])
805 return repo.lookup(roots[0])
806
806
807
807
808 @command('histedit',
808 @command('histedit',
809 [('', 'commands', '',
809 [('', 'commands', '',
810 _('read history edits from the specified file'), _('FILE')),
810 _('read history edits from the specified file'), _('FILE')),
811 ('c', 'continue', False, _('continue an edit already in progress')),
811 ('c', 'continue', False, _('continue an edit already in progress')),
812 ('', 'edit-plan', False, _('edit remaining actions list')),
812 ('', 'edit-plan', False, _('edit remaining actions list')),
813 ('k', 'keep', False,
813 ('k', 'keep', False,
814 _("don't strip old nodes after edit is complete")),
814 _("don't strip old nodes after edit is complete")),
815 ('', 'abort', False, _('abort an edit in progress')),
815 ('', 'abort', False, _('abort an edit in progress')),
816 ('o', 'outgoing', False, _('changesets not found in destination')),
816 ('o', 'outgoing', False, _('changesets not found in destination')),
817 ('f', 'force', False,
817 ('f', 'force', False,
818 _('force outgoing even for unrelated repositories')),
818 _('force outgoing even for unrelated repositories')),
819 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
819 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
820 _("[ANCESTOR] | --outgoing [URL]"))
820 _("[ANCESTOR] | --outgoing [URL]"))
821 def histedit(ui, repo, *freeargs, **opts):
821 def histedit(ui, repo, *freeargs, **opts):
822 """interactively edit changeset history
822 """interactively edit changeset history
823
823
824 This command edits changesets between an ANCESTOR and the parent of
824 This command edits changesets between an ANCESTOR and the parent of
825 the working directory.
825 the working directory.
826
826
827 The value from the "histedit.defaultrev" config option is used as a
827 The value from the "histedit.defaultrev" config option is used as a
828 revset to select the base revision when ANCESTOR is not specified.
828 revset to select the base revision when ANCESTOR is not specified.
829 The first revision returned by the revset is used. By default, this
829 The first revision returned by the revset is used. By default, this
830 selects the editable history that is unique to the ancestry of the
830 selects the editable history that is unique to the ancestry of the
831 working directory.
831 working directory.
832
832
833 With --outgoing, this edits changesets not found in the
833 With --outgoing, this edits changesets not found in the
834 destination repository. If URL of the destination is omitted, the
834 destination repository. If URL of the destination is omitted, the
835 'default-push' (or 'default') path will be used.
835 'default-push' (or 'default') path will be used.
836
836
837 For safety, this command is also aborted if there are ambiguous
837 For safety, this command is also aborted if there are ambiguous
838 outgoing revisions which may confuse users: for example, if there
838 outgoing revisions which may confuse users: for example, if there
839 are multiple branches containing outgoing revisions.
839 are multiple branches containing outgoing revisions.
840
840
841 Use "min(outgoing() and ::.)" or similar revset specification
841 Use "min(outgoing() and ::.)" or similar revset specification
842 instead of --outgoing to specify edit target revision exactly in
842 instead of --outgoing to specify edit target revision exactly in
843 such ambiguous situation. See :hg:`help revsets` for detail about
843 such ambiguous situation. See :hg:`help revsets` for detail about
844 selecting revisions.
844 selecting revisions.
845
845
846 .. container:: verbose
846 .. container:: verbose
847
847
848 Examples:
848 Examples:
849
849
850 - A number of changes have been made.
850 - A number of changes have been made.
851 Revision 3 is no longer needed.
851 Revision 3 is no longer needed.
852
852
853 Start history editing from revision 3::
853 Start history editing from revision 3::
854
854
855 hg histedit -r 3
855 hg histedit -r 3
856
856
857 An editor opens, containing the list of revisions,
857 An editor opens, containing the list of revisions,
858 with specific actions specified::
858 with specific actions specified::
859
859
860 pick 5339bf82f0ca 3 Zworgle the foobar
860 pick 5339bf82f0ca 3 Zworgle the foobar
861 pick 8ef592ce7cc4 4 Bedazzle the zerlog
861 pick 8ef592ce7cc4 4 Bedazzle the zerlog
862 pick 0a9639fcda9d 5 Morgify the cromulancy
862 pick 0a9639fcda9d 5 Morgify the cromulancy
863
863
864 Additional information about the possible actions
864 Additional information about the possible actions
865 to take appears below the list of revisions.
865 to take appears below the list of revisions.
866
866
867 To remove revision 3 from the history,
867 To remove revision 3 from the history,
868 its action (at the beginning of the relevant line)
868 its action (at the beginning of the relevant line)
869 is changed to 'drop'::
869 is changed to 'drop'::
870
870
871 drop 5339bf82f0ca 3 Zworgle the foobar
871 drop 5339bf82f0ca 3 Zworgle the foobar
872 pick 8ef592ce7cc4 4 Bedazzle the zerlog
872 pick 8ef592ce7cc4 4 Bedazzle the zerlog
873 pick 0a9639fcda9d 5 Morgify the cromulancy
873 pick 0a9639fcda9d 5 Morgify the cromulancy
874
874
875 - A number of changes have been made.
875 - A number of changes have been made.
876 Revision 2 and 4 need to be swapped.
876 Revision 2 and 4 need to be swapped.
877
877
878 Start history editing from revision 2::
878 Start history editing from revision 2::
879
879
880 hg histedit -r 2
880 hg histedit -r 2
881
881
882 An editor opens, containing the list of revisions,
882 An editor opens, containing the list of revisions,
883 with specific actions specified::
883 with specific actions specified::
884
884
885 pick 252a1af424ad 2 Blorb a morgwazzle
885 pick 252a1af424ad 2 Blorb a morgwazzle
886 pick 5339bf82f0ca 3 Zworgle the foobar
886 pick 5339bf82f0ca 3 Zworgle the foobar
887 pick 8ef592ce7cc4 4 Bedazzle the zerlog
887 pick 8ef592ce7cc4 4 Bedazzle the zerlog
888
888
889 To swap revision 2 and 4, its lines are swapped
889 To swap revision 2 and 4, its lines are swapped
890 in the editor::
890 in the editor::
891
891
892 pick 8ef592ce7cc4 4 Bedazzle the zerlog
892 pick 8ef592ce7cc4 4 Bedazzle the zerlog
893 pick 5339bf82f0ca 3 Zworgle the foobar
893 pick 5339bf82f0ca 3 Zworgle the foobar
894 pick 252a1af424ad 2 Blorb a morgwazzle
894 pick 252a1af424ad 2 Blorb a morgwazzle
895
895
896 Returns 0 on success, 1 if user intervention is required (not only
896 Returns 0 on success, 1 if user intervention is required (not only
897 for intentional "edit" command, but also for resolving unexpected
897 for intentional "edit" command, but also for resolving unexpected
898 conflicts).
898 conflicts).
899 """
899 """
900 state = histeditstate(repo)
900 state = histeditstate(repo)
901 try:
901 try:
902 state.wlock = repo.wlock()
902 state.wlock = repo.wlock()
903 state.lock = repo.lock()
903 state.lock = repo.lock()
904 _histedit(ui, repo, state, *freeargs, **opts)
904 _histedit(ui, repo, state, *freeargs, **opts)
905 finally:
905 finally:
906 release(state.lock, state.wlock)
906 release(state.lock, state.wlock)
907
907
908 def _histedit(ui, repo, state, *freeargs, **opts):
908 def _histedit(ui, repo, state, *freeargs, **opts):
909 # TODO only abort if we try to histedit mq patches, not just
909 # TODO only abort if we try to histedit mq patches, not just
910 # blanket if mq patches are applied somewhere
910 # blanket if mq patches are applied somewhere
911 mq = getattr(repo, 'mq', None)
911 mq = getattr(repo, 'mq', None)
912 if mq and mq.applied:
912 if mq and mq.applied:
913 raise error.Abort(_('source has mq patches applied'))
913 raise error.Abort(_('source has mq patches applied'))
914
914
915 # basic argument incompatibility processing
915 # basic argument incompatibility processing
916 outg = opts.get('outgoing')
916 outg = opts.get('outgoing')
917 cont = opts.get('continue')
917 cont = opts.get('continue')
918 editplan = opts.get('edit_plan')
918 editplan = opts.get('edit_plan')
919 abort = opts.get('abort')
919 abort = opts.get('abort')
920 force = opts.get('force')
920 force = opts.get('force')
921 rules = opts.get('commands', '')
921 rules = opts.get('commands', '')
922 revs = opts.get('rev', [])
922 revs = opts.get('rev', [])
923 goal = 'new' # This invocation goal, in new, continue, abort
923 goal = 'new' # This invocation goal, in new, continue, abort
924 if force and not outg:
924 if force and not outg:
925 raise error.Abort(_('--force only allowed with --outgoing'))
925 raise error.Abort(_('--force only allowed with --outgoing'))
926 if cont:
926 if cont:
927 if any((outg, abort, revs, freeargs, rules, editplan)):
927 if any((outg, abort, revs, freeargs, rules, editplan)):
928 raise error.Abort(_('no arguments allowed with --continue'))
928 raise error.Abort(_('no arguments allowed with --continue'))
929 goal = 'continue'
929 goal = 'continue'
930 elif abort:
930 elif abort:
931 if any((outg, revs, freeargs, rules, editplan)):
931 if any((outg, revs, freeargs, rules, editplan)):
932 raise error.Abort(_('no arguments allowed with --abort'))
932 raise error.Abort(_('no arguments allowed with --abort'))
933 goal = 'abort'
933 goal = 'abort'
934 elif editplan:
934 elif editplan:
935 if any((outg, revs, freeargs)):
935 if any((outg, revs, freeargs)):
936 raise error.Abort(_('only --commands argument allowed with '
936 raise error.Abort(_('only --commands argument allowed with '
937 '--edit-plan'))
937 '--edit-plan'))
938 goal = 'edit-plan'
938 goal = 'edit-plan'
939 else:
939 else:
940 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
940 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
941 raise error.Abort(_('history edit already in progress, try '
941 raise error.Abort(_('history edit already in progress, try '
942 '--continue or --abort'))
942 '--continue or --abort'))
943 if outg:
943 if outg:
944 if revs:
944 if revs:
945 raise error.Abort(_('no revisions allowed with --outgoing'))
945 raise error.Abort(_('no revisions allowed with --outgoing'))
946 if len(freeargs) > 1:
946 if len(freeargs) > 1:
947 raise error.Abort(
947 raise error.Abort(
948 _('only one repo argument allowed with --outgoing'))
948 _('only one repo argument allowed with --outgoing'))
949 else:
949 else:
950 revs.extend(freeargs)
950 revs.extend(freeargs)
951 if len(revs) == 0:
951 if len(revs) == 0:
952 defaultrev = destutil.desthistedit(ui, repo)
952 defaultrev = destutil.desthistedit(ui, repo)
953 if defaultrev is not None:
953 if defaultrev is not None:
954 revs.append(defaultrev)
954 revs.append(defaultrev)
955
955
956 if len(revs) != 1:
956 if len(revs) != 1:
957 raise error.Abort(
957 raise error.Abort(
958 _('histedit requires exactly one ancestor revision'))
958 _('histedit requires exactly one ancestor revision'))
959
959
960
960
961 replacements = []
961 replacements = []
962 state.keep = opts.get('keep', False)
962 state.keep = opts.get('keep', False)
963 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
963 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
964
964
965 # rebuild state
965 # rebuild state
966 if goal == 'continue':
966 if goal == 'continue':
967 state.read()
967 state.read()
968 state = bootstrapcontinue(ui, state, opts)
968 state = bootstrapcontinue(ui, state, opts)
969 elif goal == 'edit-plan':
969 elif goal == 'edit-plan':
970 state.read()
970 state.read()
971 if not rules:
971 if not rules:
972 comment = editcomment % (node.short(state.parentctxnode),
972 comment = editcomment % (node.short(state.parentctxnode),
973 node.short(state.topmost))
973 node.short(state.topmost))
974 rules = ruleeditor(repo, ui, state.actions, comment)
974 rules = ruleeditor(repo, ui, state.actions, comment)
975 else:
975 else:
976 if rules == '-':
976 if rules == '-':
977 f = sys.stdin
977 f = sys.stdin
978 else:
978 else:
979 f = open(rules)
979 f = open(rules)
980 rules = f.read()
980 rules = f.read()
981 f.close()
981 f.close()
982 actions = parserules(rules, state)
982 actions = parserules(rules, state)
983 ctxs = [repo[act.nodetoverify()] \
983 ctxs = [repo[act.nodetoverify()] \
984 for act in state.actions if act.nodetoverify()]
984 for act in state.actions if act.nodetoverify()]
985 warnverifyactions(ui, repo, actions, state, ctxs)
985 warnverifyactions(ui, repo, actions, state, ctxs)
986 state.actions = actions
986 state.actions = actions
987 state.write()
987 state.write()
988 return
988 return
989 elif goal == 'abort':
989 elif goal == 'abort':
990 try:
990 try:
991 state.read()
991 state.read()
992 tmpnodes, leafs = newnodestoabort(state)
992 tmpnodes, leafs = newnodestoabort(state)
993 ui.debug('restore wc to old parent %s\n'
993 ui.debug('restore wc to old parent %s\n'
994 % node.short(state.topmost))
994 % node.short(state.topmost))
995
995
996 # Recover our old commits if necessary
996 # Recover our old commits if necessary
997 if not state.topmost in repo and state.backupfile:
997 if not state.topmost in repo and state.backupfile:
998 backupfile = repo.join(state.backupfile)
998 backupfile = repo.join(state.backupfile)
999 f = hg.openpath(ui, backupfile)
999 f = hg.openpath(ui, backupfile)
1000 gen = exchange.readbundle(ui, f, backupfile)
1000 gen = exchange.readbundle(ui, f, backupfile)
1001 tr = repo.transaction('histedit.abort')
1001 tr = repo.transaction('histedit.abort')
1002 try:
1002 try:
1003 if not isinstance(gen, bundle2.unbundle20):
1003 if not isinstance(gen, bundle2.unbundle20):
1004 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1004 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1005 if isinstance(gen, bundle2.unbundle20):
1005 if isinstance(gen, bundle2.unbundle20):
1006 bundle2.applybundle(repo, gen, tr,
1006 bundle2.applybundle(repo, gen, tr,
1007 source='histedit',
1007 source='histedit',
1008 url='bundle:' + backupfile)
1008 url='bundle:' + backupfile)
1009 tr.close()
1009 tr.close()
1010 finally:
1010 finally:
1011 tr.release()
1011 tr.release()
1012
1012
1013 os.remove(backupfile)
1013 os.remove(backupfile)
1014
1014
1015 # check whether we should update away
1015 # check whether we should update away
1016 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1016 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1017 state.parentctxnode, leafs | tmpnodes):
1017 state.parentctxnode, leafs | tmpnodes):
1018 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1018 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1019 cleanupnode(ui, repo, 'created', tmpnodes)
1019 cleanupnode(ui, repo, 'created', tmpnodes)
1020 cleanupnode(ui, repo, 'temp', leafs)
1020 cleanupnode(ui, repo, 'temp', leafs)
1021 except Exception:
1021 except Exception:
1022 if state.inprogress():
1022 if state.inprogress():
1023 ui.warn(_('warning: encountered an exception during histedit '
1023 ui.warn(_('warning: encountered an exception during histedit '
1024 '--abort; the repository may not have been completely '
1024 '--abort; the repository may not have been completely '
1025 'cleaned up\n'))
1025 'cleaned up\n'))
1026 raise
1026 raise
1027 finally:
1027 finally:
1028 state.clear()
1028 state.clear()
1029 return
1029 return
1030 else:
1030 else:
1031 cmdutil.checkunfinished(repo)
1031 cmdutil.checkunfinished(repo)
1032 cmdutil.bailifchanged(repo)
1032 cmdutil.bailifchanged(repo)
1033
1033
1034 if repo.vfs.exists('histedit-last-edit.txt'):
1035 repo.vfs.unlink('histedit-last-edit.txt')
1036 topmost, empty = repo.dirstate.parents()
1034 topmost, empty = repo.dirstate.parents()
1037 if outg:
1035 if outg:
1038 if freeargs:
1036 if freeargs:
1039 remote = freeargs[0]
1037 remote = freeargs[0]
1040 else:
1038 else:
1041 remote = None
1039 remote = None
1042 root = findoutgoing(ui, repo, remote, force, opts)
1040 root = findoutgoing(ui, repo, remote, force, opts)
1043 else:
1041 else:
1044 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1042 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1045 if len(rr) != 1:
1043 if len(rr) != 1:
1046 raise error.Abort(_('The specified revisions must have '
1044 raise error.Abort(_('The specified revisions must have '
1047 'exactly one common root'))
1045 'exactly one common root'))
1048 root = rr[0].node()
1046 root = rr[0].node()
1049
1047
1050 revs = between(repo, root, topmost, state.keep)
1048 revs = between(repo, root, topmost, state.keep)
1051 if not revs:
1049 if not revs:
1052 raise error.Abort(_('%s is not an ancestor of working directory') %
1050 raise error.Abort(_('%s is not an ancestor of working directory') %
1053 node.short(root))
1051 node.short(root))
1054
1052
1055 ctxs = [repo[r] for r in revs]
1053 ctxs = [repo[r] for r in revs]
1056 if not rules:
1054 if not rules:
1057 comment = editcomment % (node.short(root), node.short(topmost))
1055 comment = editcomment % (node.short(root), node.short(topmost))
1058 actions = [pick(state, r) for r in revs]
1056 actions = [pick(state, r) for r in revs]
1059 rules = ruleeditor(repo, ui, actions, comment)
1057 rules = ruleeditor(repo, ui, actions, comment)
1060 else:
1058 else:
1061 if rules == '-':
1059 if rules == '-':
1062 f = sys.stdin
1060 f = sys.stdin
1063 else:
1061 else:
1064 f = open(rules)
1062 f = open(rules)
1065 rules = f.read()
1063 rules = f.read()
1066 f.close()
1064 f.close()
1067 actions = parserules(rules, state)
1065 actions = parserules(rules, state)
1068 warnverifyactions(ui, repo, actions, state, ctxs)
1066 warnverifyactions(ui, repo, actions, state, ctxs)
1069
1067
1070 parentctxnode = repo[root].parents()[0].node()
1068 parentctxnode = repo[root].parents()[0].node()
1071
1069
1072 state.parentctxnode = parentctxnode
1070 state.parentctxnode = parentctxnode
1073 state.actions = actions
1071 state.actions = actions
1074 state.topmost = topmost
1072 state.topmost = topmost
1075 state.replacements = replacements
1073 state.replacements = replacements
1076
1074
1077 # Create a backup so we can always abort completely.
1075 # Create a backup so we can always abort completely.
1078 backupfile = None
1076 backupfile = None
1079 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1077 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1080 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1078 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1081 'histedit')
1079 'histedit')
1082 state.backupfile = backupfile
1080 state.backupfile = backupfile
1083
1081
1084 # preprocess rules so that we can hide inner folds from the user
1082 # preprocess rules so that we can hide inner folds from the user
1085 # and only show one editor
1083 # and only show one editor
1086 actions = state.actions[:]
1084 actions = state.actions[:]
1087 for idx, (action, nextact) in enumerate(
1085 for idx, (action, nextact) in enumerate(
1088 zip(actions, actions[1:] + [None])):
1086 zip(actions, actions[1:] + [None])):
1089 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1087 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1090 state.actions[idx].__class__ = _multifold
1088 state.actions[idx].__class__ = _multifold
1091
1089
1092 total = len(state.actions)
1090 total = len(state.actions)
1093 pos = 0
1091 pos = 0
1094 while state.actions:
1092 while state.actions:
1095 state.write()
1093 state.write()
1096 actobj = state.actions.pop(0)
1094 actobj = state.actions.pop(0)
1097 pos += 1
1095 pos += 1
1098 ui.progress(_("editing"), pos, actobj.torule(),
1096 ui.progress(_("editing"), pos, actobj.torule(),
1099 _('changes'), total)
1097 _('changes'), total)
1100 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1098 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1101 actobj.torule()))
1099 actobj.torule()))
1102 parentctx, replacement_ = actobj.run()
1100 parentctx, replacement_ = actobj.run()
1103 state.parentctxnode = parentctx.node()
1101 state.parentctxnode = parentctx.node()
1104 state.replacements.extend(replacement_)
1102 state.replacements.extend(replacement_)
1105 state.write()
1103 state.write()
1106 ui.progress(_("editing"), None)
1104 ui.progress(_("editing"), None)
1107
1105
1108 hg.update(repo, state.parentctxnode, quietempty=True)
1106 hg.update(repo, state.parentctxnode, quietempty=True)
1109
1107
1110 mapping, tmpnodes, created, ntm = processreplacement(state)
1108 mapping, tmpnodes, created, ntm = processreplacement(state)
1111 if mapping:
1109 if mapping:
1112 for prec, succs in mapping.iteritems():
1110 for prec, succs in mapping.iteritems():
1113 if not succs:
1111 if not succs:
1114 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1112 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1115 else:
1113 else:
1116 ui.debug('histedit: %s is replaced by %s\n' % (
1114 ui.debug('histedit: %s is replaced by %s\n' % (
1117 node.short(prec), node.short(succs[0])))
1115 node.short(prec), node.short(succs[0])))
1118 if len(succs) > 1:
1116 if len(succs) > 1:
1119 m = 'histedit: %s'
1117 m = 'histedit: %s'
1120 for n in succs[1:]:
1118 for n in succs[1:]:
1121 ui.debug(m % node.short(n))
1119 ui.debug(m % node.short(n))
1122
1120
1123 if supportsmarkers:
1121 if supportsmarkers:
1124 # Only create markers if the temp nodes weren't already removed.
1122 # Only create markers if the temp nodes weren't already removed.
1125 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1123 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1126 if t in repo))
1124 if t in repo))
1127 else:
1125 else:
1128 cleanupnode(ui, repo, 'temp', tmpnodes)
1126 cleanupnode(ui, repo, 'temp', tmpnodes)
1129
1127
1130 if not state.keep:
1128 if not state.keep:
1131 if mapping:
1129 if mapping:
1132 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1130 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1133 # TODO update mq state
1131 # TODO update mq state
1134 if supportsmarkers:
1132 if supportsmarkers:
1135 markers = []
1133 markers = []
1136 # sort by revision number because it sound "right"
1134 # sort by revision number because it sound "right"
1137 for prec in sorted(mapping, key=repo.changelog.rev):
1135 for prec in sorted(mapping, key=repo.changelog.rev):
1138 succs = mapping[prec]
1136 succs = mapping[prec]
1139 markers.append((repo[prec],
1137 markers.append((repo[prec],
1140 tuple(repo[s] for s in succs)))
1138 tuple(repo[s] for s in succs)))
1141 if markers:
1139 if markers:
1142 obsolete.createmarkers(repo, markers)
1140 obsolete.createmarkers(repo, markers)
1143 else:
1141 else:
1144 cleanupnode(ui, repo, 'replaced', mapping)
1142 cleanupnode(ui, repo, 'replaced', mapping)
1145
1143
1146 state.clear()
1144 state.clear()
1147 if os.path.exists(repo.sjoin('undo')):
1145 if os.path.exists(repo.sjoin('undo')):
1148 os.unlink(repo.sjoin('undo'))
1146 os.unlink(repo.sjoin('undo'))
1147 if repo.vfs.exists('histedit-last-edit.txt'):
1148 repo.vfs.unlink('histedit-last-edit.txt')
1149
1149
1150 def bootstrapcontinue(ui, state, opts):
1150 def bootstrapcontinue(ui, state, opts):
1151 repo = state.repo
1151 repo = state.repo
1152 if state.actions:
1152 if state.actions:
1153 actobj = state.actions.pop(0)
1153 actobj = state.actions.pop(0)
1154
1154
1155 if _isdirtywc(repo):
1155 if _isdirtywc(repo):
1156 actobj.continuedirty()
1156 actobj.continuedirty()
1157 if _isdirtywc(repo):
1157 if _isdirtywc(repo):
1158 abortdirty()
1158 abortdirty()
1159
1159
1160 parentctx, replacements = actobj.continueclean()
1160 parentctx, replacements = actobj.continueclean()
1161
1161
1162 state.parentctxnode = parentctx.node()
1162 state.parentctxnode = parentctx.node()
1163 state.replacements.extend(replacements)
1163 state.replacements.extend(replacements)
1164
1164
1165 return state
1165 return state
1166
1166
1167 def between(repo, old, new, keep):
1167 def between(repo, old, new, keep):
1168 """select and validate the set of revision to edit
1168 """select and validate the set of revision to edit
1169
1169
1170 When keep is false, the specified set can't have children."""
1170 When keep is false, the specified set can't have children."""
1171 ctxs = list(repo.set('%n::%n', old, new))
1171 ctxs = list(repo.set('%n::%n', old, new))
1172 if ctxs and not keep:
1172 if ctxs and not keep:
1173 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1173 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1174 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1174 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1175 raise error.Abort(_('cannot edit history that would orphan nodes'))
1175 raise error.Abort(_('cannot edit history that would orphan nodes'))
1176 if repo.revs('(%ld) and merge()', ctxs):
1176 if repo.revs('(%ld) and merge()', ctxs):
1177 raise error.Abort(_('cannot edit history that contains merges'))
1177 raise error.Abort(_('cannot edit history that contains merges'))
1178 root = ctxs[0] # list is already sorted by repo.set
1178 root = ctxs[0] # list is already sorted by repo.set
1179 if not root.mutable():
1179 if not root.mutable():
1180 raise error.Abort(_('cannot edit public changeset: %s') % root,
1180 raise error.Abort(_('cannot edit public changeset: %s') % root,
1181 hint=_('see "hg help phases" for details'))
1181 hint=_('see "hg help phases" for details'))
1182 return [c.node() for c in ctxs]
1182 return [c.node() for c in ctxs]
1183
1183
1184 def ruleeditor(repo, ui, actions, editcomment=""):
1184 def ruleeditor(repo, ui, actions, editcomment=""):
1185 """open an editor to edit rules
1185 """open an editor to edit rules
1186
1186
1187 rules are in the format [ [act, ctx], ...] like in state.rules
1187 rules are in the format [ [act, ctx], ...] like in state.rules
1188 """
1188 """
1189 rules = '\n'.join([act.torule() for act in actions])
1189 rules = '\n'.join([act.torule() for act in actions])
1190 rules += '\n\n'
1190 rules += '\n\n'
1191 rules += editcomment
1191 rules += editcomment
1192 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1192 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1193
1193
1194 # Save edit rules in .hg/histedit-last-edit.txt in case
1194 # Save edit rules in .hg/histedit-last-edit.txt in case
1195 # the user needs to ask for help after something
1195 # the user needs to ask for help after something
1196 # surprising happens.
1196 # surprising happens.
1197 f = open(repo.join('histedit-last-edit.txt'), 'w')
1197 f = open(repo.join('histedit-last-edit.txt'), 'w')
1198 f.write(rules)
1198 f.write(rules)
1199 f.close()
1199 f.close()
1200
1200
1201 return rules
1201 return rules
1202
1202
1203 def parserules(rules, state):
1203 def parserules(rules, state):
1204 """Read the histedit rules string and return list of action objects """
1204 """Read the histedit rules string and return list of action objects """
1205 rules = [l for l in (r.strip() for r in rules.splitlines())
1205 rules = [l for l in (r.strip() for r in rules.splitlines())
1206 if l and not l.startswith('#')]
1206 if l and not l.startswith('#')]
1207 actions = []
1207 actions = []
1208 for r in rules:
1208 for r in rules:
1209 if ' ' not in r:
1209 if ' ' not in r:
1210 raise error.ParseError(_('malformed line "%s"') % r)
1210 raise error.ParseError(_('malformed line "%s"') % r)
1211 verb, rest = r.split(' ', 1)
1211 verb, rest = r.split(' ', 1)
1212
1212
1213 if verb not in actiontable:
1213 if verb not in actiontable:
1214 raise error.ParseError(_('unknown action "%s"') % verb)
1214 raise error.ParseError(_('unknown action "%s"') % verb)
1215
1215
1216 action = actiontable[verb].fromrule(state, rest)
1216 action = actiontable[verb].fromrule(state, rest)
1217 actions.append(action)
1217 actions.append(action)
1218 return actions
1218 return actions
1219
1219
1220 def warnverifyactions(ui, repo, actions, state, ctxs):
1220 def warnverifyactions(ui, repo, actions, state, ctxs):
1221 try:
1221 try:
1222 verifyactions(actions, state, ctxs)
1222 verifyactions(actions, state, ctxs)
1223 except error.ParseError:
1223 except error.ParseError:
1224 if repo.vfs.exists('histedit-last-edit.txt'):
1224 if repo.vfs.exists('histedit-last-edit.txt'):
1225 ui.warn(_('warning: histedit rules saved '
1225 ui.warn(_('warning: histedit rules saved '
1226 'to: .hg/histedit-last-edit.txt\n'))
1226 'to: .hg/histedit-last-edit.txt\n'))
1227 raise
1227 raise
1228
1228
1229 def verifyactions(actions, state, ctxs):
1229 def verifyactions(actions, state, ctxs):
1230 """Verify that there exists exactly one action per given changeset and
1230 """Verify that there exists exactly one action per given changeset and
1231 other constraints.
1231 other constraints.
1232
1232
1233 Will abort if there are to many or too few rules, a malformed rule,
1233 Will abort if there are to many or too few rules, a malformed rule,
1234 or a rule on a changeset outside of the user-given range.
1234 or a rule on a changeset outside of the user-given range.
1235 """
1235 """
1236 expected = set(c.hex() for c in ctxs)
1236 expected = set(c.hex() for c in ctxs)
1237 seen = set()
1237 seen = set()
1238 prev = None
1238 prev = None
1239 for action in actions:
1239 for action in actions:
1240 action.verify(prev)
1240 action.verify(prev)
1241 prev = action
1241 prev = action
1242 constraints = action.constraints()
1242 constraints = action.constraints()
1243 for constraint in constraints:
1243 for constraint in constraints:
1244 if constraint not in _constraints.known():
1244 if constraint not in _constraints.known():
1245 raise error.ParseError(_('unknown constraint "%s"') %
1245 raise error.ParseError(_('unknown constraint "%s"') %
1246 constraint)
1246 constraint)
1247
1247
1248 nodetoverify = action.nodetoverify()
1248 nodetoverify = action.nodetoverify()
1249 if nodetoverify is not None:
1249 if nodetoverify is not None:
1250 ha = node.hex(nodetoverify)
1250 ha = node.hex(nodetoverify)
1251 if _constraints.noother in constraints and ha not in expected:
1251 if _constraints.noother in constraints and ha not in expected:
1252 raise error.ParseError(
1252 raise error.ParseError(
1253 _('may not use "%s" with changesets '
1253 _('may not use "%s" with changesets '
1254 'other than the ones listed') % action.verb)
1254 'other than the ones listed') % action.verb)
1255 if _constraints.forceother in constraints and ha in expected:
1255 if _constraints.forceother in constraints and ha in expected:
1256 raise error.ParseError(
1256 raise error.ParseError(
1257 _('may not use "%s" with changesets '
1257 _('may not use "%s" with changesets '
1258 'within the edited list') % action.verb)
1258 'within the edited list') % action.verb)
1259 if _constraints.noduplicates in constraints and ha in seen:
1259 if _constraints.noduplicates in constraints and ha in seen:
1260 raise error.ParseError(_(
1260 raise error.ParseError(_(
1261 'duplicated command for changeset %s') %
1261 'duplicated command for changeset %s') %
1262 ha[:12])
1262 ha[:12])
1263 seen.add(ha)
1263 seen.add(ha)
1264 missing = sorted(expected - seen) # sort to stabilize output
1264 missing = sorted(expected - seen) # sort to stabilize output
1265
1265
1266 if state.repo.ui.configbool('histedit', 'dropmissing'):
1266 if state.repo.ui.configbool('histedit', 'dropmissing'):
1267 drops = [drop(state, node.bin(n)) for n in missing]
1267 drops = [drop(state, node.bin(n)) for n in missing]
1268 # put the in the beginning so they execute immediately and
1268 # put the in the beginning so they execute immediately and
1269 # don't show in the edit-plan in the future
1269 # don't show in the edit-plan in the future
1270 actions[:0] = drops
1270 actions[:0] = drops
1271 elif missing:
1271 elif missing:
1272 raise error.ParseError(_('missing rules for changeset %s') %
1272 raise error.ParseError(_('missing rules for changeset %s') %
1273 missing[0][:12],
1273 missing[0][:12],
1274 hint=_('use "drop %s" to discard, see also: '
1274 hint=_('use "drop %s" to discard, see also: '
1275 '"hg help -e histedit.config"') % missing[0][:12])
1275 '"hg help -e histedit.config"') % missing[0][:12])
1276
1276
1277 def newnodestoabort(state):
1277 def newnodestoabort(state):
1278 """process the list of replacements to return
1278 """process the list of replacements to return
1279
1279
1280 1) the list of final node
1280 1) the list of final node
1281 2) the list of temporary node
1281 2) the list of temporary node
1282
1282
1283 This meant to be used on abort as less data are required in this case.
1283 This meant to be used on abort as less data are required in this case.
1284 """
1284 """
1285 replacements = state.replacements
1285 replacements = state.replacements
1286 allsuccs = set()
1286 allsuccs = set()
1287 replaced = set()
1287 replaced = set()
1288 for rep in replacements:
1288 for rep in replacements:
1289 allsuccs.update(rep[1])
1289 allsuccs.update(rep[1])
1290 replaced.add(rep[0])
1290 replaced.add(rep[0])
1291 newnodes = allsuccs - replaced
1291 newnodes = allsuccs - replaced
1292 tmpnodes = allsuccs & replaced
1292 tmpnodes = allsuccs & replaced
1293 return newnodes, tmpnodes
1293 return newnodes, tmpnodes
1294
1294
1295
1295
1296 def processreplacement(state):
1296 def processreplacement(state):
1297 """process the list of replacements to return
1297 """process the list of replacements to return
1298
1298
1299 1) the final mapping between original and created nodes
1299 1) the final mapping between original and created nodes
1300 2) the list of temporary node created by histedit
1300 2) the list of temporary node created by histedit
1301 3) the list of new commit created by histedit"""
1301 3) the list of new commit created by histedit"""
1302 replacements = state.replacements
1302 replacements = state.replacements
1303 allsuccs = set()
1303 allsuccs = set()
1304 replaced = set()
1304 replaced = set()
1305 fullmapping = {}
1305 fullmapping = {}
1306 # initialize basic set
1306 # initialize basic set
1307 # fullmapping records all operations recorded in replacement
1307 # fullmapping records all operations recorded in replacement
1308 for rep in replacements:
1308 for rep in replacements:
1309 allsuccs.update(rep[1])
1309 allsuccs.update(rep[1])
1310 replaced.add(rep[0])
1310 replaced.add(rep[0])
1311 fullmapping.setdefault(rep[0], set()).update(rep[1])
1311 fullmapping.setdefault(rep[0], set()).update(rep[1])
1312 new = allsuccs - replaced
1312 new = allsuccs - replaced
1313 tmpnodes = allsuccs & replaced
1313 tmpnodes = allsuccs & replaced
1314 # Reduce content fullmapping into direct relation between original nodes
1314 # Reduce content fullmapping into direct relation between original nodes
1315 # and final node created during history edition
1315 # and final node created during history edition
1316 # Dropped changeset are replaced by an empty list
1316 # Dropped changeset are replaced by an empty list
1317 toproceed = set(fullmapping)
1317 toproceed = set(fullmapping)
1318 final = {}
1318 final = {}
1319 while toproceed:
1319 while toproceed:
1320 for x in list(toproceed):
1320 for x in list(toproceed):
1321 succs = fullmapping[x]
1321 succs = fullmapping[x]
1322 for s in list(succs):
1322 for s in list(succs):
1323 if s in toproceed:
1323 if s in toproceed:
1324 # non final node with unknown closure
1324 # non final node with unknown closure
1325 # We can't process this now
1325 # We can't process this now
1326 break
1326 break
1327 elif s in final:
1327 elif s in final:
1328 # non final node, replace with closure
1328 # non final node, replace with closure
1329 succs.remove(s)
1329 succs.remove(s)
1330 succs.update(final[s])
1330 succs.update(final[s])
1331 else:
1331 else:
1332 final[x] = succs
1332 final[x] = succs
1333 toproceed.remove(x)
1333 toproceed.remove(x)
1334 # remove tmpnodes from final mapping
1334 # remove tmpnodes from final mapping
1335 for n in tmpnodes:
1335 for n in tmpnodes:
1336 del final[n]
1336 del final[n]
1337 # we expect all changes involved in final to exist in the repo
1337 # we expect all changes involved in final to exist in the repo
1338 # turn `final` into list (topologically sorted)
1338 # turn `final` into list (topologically sorted)
1339 nm = state.repo.changelog.nodemap
1339 nm = state.repo.changelog.nodemap
1340 for prec, succs in final.items():
1340 for prec, succs in final.items():
1341 final[prec] = sorted(succs, key=nm.get)
1341 final[prec] = sorted(succs, key=nm.get)
1342
1342
1343 # computed topmost element (necessary for bookmark)
1343 # computed topmost element (necessary for bookmark)
1344 if new:
1344 if new:
1345 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1345 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1346 elif not final:
1346 elif not final:
1347 # Nothing rewritten at all. we won't need `newtopmost`
1347 # Nothing rewritten at all. we won't need `newtopmost`
1348 # It is the same as `oldtopmost` and `processreplacement` know it
1348 # It is the same as `oldtopmost` and `processreplacement` know it
1349 newtopmost = None
1349 newtopmost = None
1350 else:
1350 else:
1351 # every body died. The newtopmost is the parent of the root.
1351 # every body died. The newtopmost is the parent of the root.
1352 r = state.repo.changelog.rev
1352 r = state.repo.changelog.rev
1353 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1353 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1354
1354
1355 return final, tmpnodes, new, newtopmost
1355 return final, tmpnodes, new, newtopmost
1356
1356
1357 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1357 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1358 """Move bookmark from old to newly created node"""
1358 """Move bookmark from old to newly created node"""
1359 if not mapping:
1359 if not mapping:
1360 # if nothing got rewritten there is not purpose for this function
1360 # if nothing got rewritten there is not purpose for this function
1361 return
1361 return
1362 moves = []
1362 moves = []
1363 for bk, old in sorted(repo._bookmarks.iteritems()):
1363 for bk, old in sorted(repo._bookmarks.iteritems()):
1364 if old == oldtopmost:
1364 if old == oldtopmost:
1365 # special case ensure bookmark stay on tip.
1365 # special case ensure bookmark stay on tip.
1366 #
1366 #
1367 # This is arguably a feature and we may only want that for the
1367 # This is arguably a feature and we may only want that for the
1368 # active bookmark. But the behavior is kept compatible with the old
1368 # active bookmark. But the behavior is kept compatible with the old
1369 # version for now.
1369 # version for now.
1370 moves.append((bk, newtopmost))
1370 moves.append((bk, newtopmost))
1371 continue
1371 continue
1372 base = old
1372 base = old
1373 new = mapping.get(base, None)
1373 new = mapping.get(base, None)
1374 if new is None:
1374 if new is None:
1375 continue
1375 continue
1376 while not new:
1376 while not new:
1377 # base is killed, trying with parent
1377 # base is killed, trying with parent
1378 base = repo[base].p1().node()
1378 base = repo[base].p1().node()
1379 new = mapping.get(base, (base,))
1379 new = mapping.get(base, (base,))
1380 # nothing to move
1380 # nothing to move
1381 moves.append((bk, new[-1]))
1381 moves.append((bk, new[-1]))
1382 if moves:
1382 if moves:
1383 lock = tr = None
1383 lock = tr = None
1384 try:
1384 try:
1385 lock = repo.lock()
1385 lock = repo.lock()
1386 tr = repo.transaction('histedit')
1386 tr = repo.transaction('histedit')
1387 marks = repo._bookmarks
1387 marks = repo._bookmarks
1388 for mark, new in moves:
1388 for mark, new in moves:
1389 old = marks[mark]
1389 old = marks[mark]
1390 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1390 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1391 % (mark, node.short(old), node.short(new)))
1391 % (mark, node.short(old), node.short(new)))
1392 marks[mark] = new
1392 marks[mark] = new
1393 marks.recordchange(tr)
1393 marks.recordchange(tr)
1394 tr.close()
1394 tr.close()
1395 finally:
1395 finally:
1396 release(tr, lock)
1396 release(tr, lock)
1397
1397
1398 def cleanupnode(ui, repo, name, nodes):
1398 def cleanupnode(ui, repo, name, nodes):
1399 """strip a group of nodes from the repository
1399 """strip a group of nodes from the repository
1400
1400
1401 The set of node to strip may contains unknown nodes."""
1401 The set of node to strip may contains unknown nodes."""
1402 ui.debug('should strip %s nodes %s\n' %
1402 ui.debug('should strip %s nodes %s\n' %
1403 (name, ', '.join([node.short(n) for n in nodes])))
1403 (name, ', '.join([node.short(n) for n in nodes])))
1404 lock = None
1404 lock = None
1405 try:
1405 try:
1406 lock = repo.lock()
1406 lock = repo.lock()
1407 # do not let filtering get in the way of the cleanse
1407 # do not let filtering get in the way of the cleanse
1408 # we should probably get rid of obsolescence marker created during the
1408 # we should probably get rid of obsolescence marker created during the
1409 # histedit, but we currently do not have such information.
1409 # histedit, but we currently do not have such information.
1410 repo = repo.unfiltered()
1410 repo = repo.unfiltered()
1411 # Find all nodes that need to be stripped
1411 # Find all nodes that need to be stripped
1412 # (we use %lr instead of %ln to silently ignore unknown items)
1412 # (we use %lr instead of %ln to silently ignore unknown items)
1413 nm = repo.changelog.nodemap
1413 nm = repo.changelog.nodemap
1414 nodes = sorted(n for n in nodes if n in nm)
1414 nodes = sorted(n for n in nodes if n in nm)
1415 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1415 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1416 for c in roots:
1416 for c in roots:
1417 # We should process node in reverse order to strip tip most first.
1417 # We should process node in reverse order to strip tip most first.
1418 # but this trigger a bug in changegroup hook.
1418 # but this trigger a bug in changegroup hook.
1419 # This would reduce bundle overhead
1419 # This would reduce bundle overhead
1420 repair.strip(ui, repo, c)
1420 repair.strip(ui, repo, c)
1421 finally:
1421 finally:
1422 release(lock)
1422 release(lock)
1423
1423
1424 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1424 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1425 if isinstance(nodelist, str):
1425 if isinstance(nodelist, str):
1426 nodelist = [nodelist]
1426 nodelist = [nodelist]
1427 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1427 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1428 state = histeditstate(repo)
1428 state = histeditstate(repo)
1429 state.read()
1429 state.read()
1430 histedit_nodes = set([action.nodetoverify() for action
1430 histedit_nodes = set([action.nodetoverify() for action
1431 in state.actions if action.nodetoverify()])
1431 in state.actions if action.nodetoverify()])
1432 strip_nodes = set([repo[n].node() for n in nodelist])
1432 strip_nodes = set([repo[n].node() for n in nodelist])
1433 common_nodes = histedit_nodes & strip_nodes
1433 common_nodes = histedit_nodes & strip_nodes
1434 if common_nodes:
1434 if common_nodes:
1435 raise error.Abort(_("histedit in progress, can't strip %s")
1435 raise error.Abort(_("histedit in progress, can't strip %s")
1436 % ', '.join(node.short(x) for x in common_nodes))
1436 % ', '.join(node.short(x) for x in common_nodes))
1437 return orig(ui, repo, nodelist, *args, **kwargs)
1437 return orig(ui, repo, nodelist, *args, **kwargs)
1438
1438
1439 extensions.wrapfunction(repair, 'strip', stripwrapper)
1439 extensions.wrapfunction(repair, 'strip', stripwrapper)
1440
1440
1441 def summaryhook(ui, repo):
1441 def summaryhook(ui, repo):
1442 if not os.path.exists(repo.join('histedit-state')):
1442 if not os.path.exists(repo.join('histedit-state')):
1443 return
1443 return
1444 state = histeditstate(repo)
1444 state = histeditstate(repo)
1445 state.read()
1445 state.read()
1446 if state.actions:
1446 if state.actions:
1447 # i18n: column positioning for "hg summary"
1447 # i18n: column positioning for "hg summary"
1448 ui.write(_('hist: %s (histedit --continue)\n') %
1448 ui.write(_('hist: %s (histedit --continue)\n') %
1449 (ui.label(_('%d remaining'), 'histedit.remaining') %
1449 (ui.label(_('%d remaining'), 'histedit.remaining') %
1450 len(state.actions)))
1450 len(state.actions)))
1451
1451
1452 def extsetup(ui):
1452 def extsetup(ui):
1453 cmdutil.summaryhooks.add('histedit', summaryhook)
1453 cmdutil.summaryhooks.add('histedit', summaryhook)
1454 cmdutil.unfinishedstates.append(
1454 cmdutil.unfinishedstates.append(
1455 ['histedit-state', False, True, _('histedit in progress'),
1455 ['histedit-state', False, True, _('histedit in progress'),
1456 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1456 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1457 if ui.configbool("experimental", "histeditng"):
1457 if ui.configbool("experimental", "histeditng"):
1458 globals()['base'] = addhisteditaction(['base', 'b'])(base)
1458 globals()['base'] = addhisteditaction(['base', 'b'])(base)
@@ -1,443 +1,456 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > histedit=
5 > histedit=
6 > EOF
6 > EOF
7
7
8 $ initrepo ()
8 $ initrepo ()
9 > {
9 > {
10 > hg init r
10 > hg init r
11 > cd r
11 > cd r
12 > for x in a b c d e f ; do
12 > for x in a b c d e f ; do
13 > echo $x > $x
13 > echo $x > $x
14 > hg add $x
14 > hg add $x
15 > hg ci -m $x
15 > hg ci -m $x
16 > done
16 > done
17 > }
17 > }
18
18
19 $ initrepo
19 $ initrepo
20
20
21 log before edit
21 log before edit
22 $ hg log --graph
22 $ hg log --graph
23 @ changeset: 5:652413bf663e
23 @ changeset: 5:652413bf663e
24 | tag: tip
24 | tag: tip
25 | user: test
25 | user: test
26 | date: Thu Jan 01 00:00:00 1970 +0000
26 | date: Thu Jan 01 00:00:00 1970 +0000
27 | summary: f
27 | summary: f
28 |
28 |
29 o changeset: 4:e860deea161a
29 o changeset: 4:e860deea161a
30 | user: test
30 | user: test
31 | date: Thu Jan 01 00:00:00 1970 +0000
31 | date: Thu Jan 01 00:00:00 1970 +0000
32 | summary: e
32 | summary: e
33 |
33 |
34 o changeset: 3:055a42cdd887
34 o changeset: 3:055a42cdd887
35 | user: test
35 | user: test
36 | date: Thu Jan 01 00:00:00 1970 +0000
36 | date: Thu Jan 01 00:00:00 1970 +0000
37 | summary: d
37 | summary: d
38 |
38 |
39 o changeset: 2:177f92b77385
39 o changeset: 2:177f92b77385
40 | user: test
40 | user: test
41 | date: Thu Jan 01 00:00:00 1970 +0000
41 | date: Thu Jan 01 00:00:00 1970 +0000
42 | summary: c
42 | summary: c
43 |
43 |
44 o changeset: 1:d2ae7f538514
44 o changeset: 1:d2ae7f538514
45 | user: test
45 | user: test
46 | date: Thu Jan 01 00:00:00 1970 +0000
46 | date: Thu Jan 01 00:00:00 1970 +0000
47 | summary: b
47 | summary: b
48 |
48 |
49 o changeset: 0:cb9a9f314b8b
49 o changeset: 0:cb9a9f314b8b
50 user: test
50 user: test
51 date: Thu Jan 01 00:00:00 1970 +0000
51 date: Thu Jan 01 00:00:00 1970 +0000
52 summary: a
52 summary: a
53
53
54
54
55 show the edit commands offered
55 show the edit commands offered
56 $ HGEDITOR=cat hg histedit 177f92b77385
56 $ HGEDITOR=cat hg histedit 177f92b77385
57 pick 177f92b77385 2 c
57 pick 177f92b77385 2 c
58 pick 055a42cdd887 3 d
58 pick 055a42cdd887 3 d
59 pick e860deea161a 4 e
59 pick e860deea161a 4 e
60 pick 652413bf663e 5 f
60 pick 652413bf663e 5 f
61
61
62 # Edit history between 177f92b77385 and 652413bf663e
62 # Edit history between 177f92b77385 and 652413bf663e
63 #
63 #
64 # Commits are listed from least to most recent
64 # Commits are listed from least to most recent
65 #
65 #
66 # Commands:
66 # Commands:
67 # p, pick = use commit
67 # p, pick = use commit
68 # e, edit = use commit, but stop for amending
68 # e, edit = use commit, but stop for amending
69 # f, fold = use commit, but combine it with the one above
69 # f, fold = use commit, but combine it with the one above
70 # r, roll = like fold, but discard this commit's description
70 # r, roll = like fold, but discard this commit's description
71 # d, drop = remove commit from history
71 # d, drop = remove commit from history
72 # m, mess = edit commit message without changing commit content
72 # m, mess = edit commit message without changing commit content
73 #
73 #
74
74
75 edit the history
75 edit the history
76 (use a hacky editor to check histedit-last-edit.txt backup)
76 (use a hacky editor to check histedit-last-edit.txt backup)
77
77
78 $ EDITED="$TESTTMP/editedhistory"
78 $ EDITED="$TESTTMP/editedhistory"
79 $ cat > $EDITED <<EOF
79 $ cat > $EDITED <<EOF
80 > edit 177f92b77385 c
81 > pick e860deea161a e
82 > pick 652413bf663e f
83 > pick 055a42cdd887 d
84 > EOF
85 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
86 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
87 Make changes as needed, you may commit or record as needed now.
88 When you are finished, run hg histedit --continue to resume.
89
90 rules should end up in .hg/histedit-last-edit.txt:
91 $ cat .hg/histedit-last-edit.txt
92 edit 177f92b77385 c
93 pick e860deea161a e
94 pick 652413bf663e f
95 pick 055a42cdd887 d
96
97 $ hg histedit --abort
98 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
99 $ cat > $EDITED <<EOF
80 > pick 177f92b77385 c
100 > pick 177f92b77385 c
81 > pick e860deea161a e
101 > pick e860deea161a e
82 > pick 652413bf663e f
102 > pick 652413bf663e f
83 > pick 055a42cdd887 d
103 > pick 055a42cdd887 d
84 > EOF
104 > EOF
85 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
105 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
86 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
106 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
87
107
88 rules should end up in .hg/histedit-last-edit.txt:
89 $ cat .hg/histedit-last-edit.txt
90 pick 177f92b77385 c
91 pick e860deea161a e
92 pick 652413bf663e f
93 pick 055a42cdd887 d
94
95 log after edit
108 log after edit
96 $ hg log --graph
109 $ hg log --graph
97 @ changeset: 5:07114f51870f
110 @ changeset: 5:07114f51870f
98 | tag: tip
111 | tag: tip
99 | user: test
112 | user: test
100 | date: Thu Jan 01 00:00:00 1970 +0000
113 | date: Thu Jan 01 00:00:00 1970 +0000
101 | summary: d
114 | summary: d
102 |
115 |
103 o changeset: 4:8ade9693061e
116 o changeset: 4:8ade9693061e
104 | user: test
117 | user: test
105 | date: Thu Jan 01 00:00:00 1970 +0000
118 | date: Thu Jan 01 00:00:00 1970 +0000
106 | summary: f
119 | summary: f
107 |
120 |
108 o changeset: 3:d8249471110a
121 o changeset: 3:d8249471110a
109 | user: test
122 | user: test
110 | date: Thu Jan 01 00:00:00 1970 +0000
123 | date: Thu Jan 01 00:00:00 1970 +0000
111 | summary: e
124 | summary: e
112 |
125 |
113 o changeset: 2:177f92b77385
126 o changeset: 2:177f92b77385
114 | user: test
127 | user: test
115 | date: Thu Jan 01 00:00:00 1970 +0000
128 | date: Thu Jan 01 00:00:00 1970 +0000
116 | summary: c
129 | summary: c
117 |
130 |
118 o changeset: 1:d2ae7f538514
131 o changeset: 1:d2ae7f538514
119 | user: test
132 | user: test
120 | date: Thu Jan 01 00:00:00 1970 +0000
133 | date: Thu Jan 01 00:00:00 1970 +0000
121 | summary: b
134 | summary: b
122 |
135 |
123 o changeset: 0:cb9a9f314b8b
136 o changeset: 0:cb9a9f314b8b
124 user: test
137 user: test
125 date: Thu Jan 01 00:00:00 1970 +0000
138 date: Thu Jan 01 00:00:00 1970 +0000
126 summary: a
139 summary: a
127
140
128
141
129 put things back
142 put things back
130
143
131 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF | fixbundle
144 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF | fixbundle
132 > pick 177f92b77385 c
145 > pick 177f92b77385 c
133 > pick 07114f51870f d
146 > pick 07114f51870f d
134 > pick d8249471110a e
147 > pick d8249471110a e
135 > pick 8ade9693061e f
148 > pick 8ade9693061e f
136 > EOF
149 > EOF
137 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
150 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
138
151
139 $ hg log --graph
152 $ hg log --graph
140 @ changeset: 5:7eca9b5b1148
153 @ changeset: 5:7eca9b5b1148
141 | tag: tip
154 | tag: tip
142 | user: test
155 | user: test
143 | date: Thu Jan 01 00:00:00 1970 +0000
156 | date: Thu Jan 01 00:00:00 1970 +0000
144 | summary: f
157 | summary: f
145 |
158 |
146 o changeset: 4:915da888f2de
159 o changeset: 4:915da888f2de
147 | user: test
160 | user: test
148 | date: Thu Jan 01 00:00:00 1970 +0000
161 | date: Thu Jan 01 00:00:00 1970 +0000
149 | summary: e
162 | summary: e
150 |
163 |
151 o changeset: 3:10517e47bbbb
164 o changeset: 3:10517e47bbbb
152 | user: test
165 | user: test
153 | date: Thu Jan 01 00:00:00 1970 +0000
166 | date: Thu Jan 01 00:00:00 1970 +0000
154 | summary: d
167 | summary: d
155 |
168 |
156 o changeset: 2:177f92b77385
169 o changeset: 2:177f92b77385
157 | user: test
170 | user: test
158 | date: Thu Jan 01 00:00:00 1970 +0000
171 | date: Thu Jan 01 00:00:00 1970 +0000
159 | summary: c
172 | summary: c
160 |
173 |
161 o changeset: 1:d2ae7f538514
174 o changeset: 1:d2ae7f538514
162 | user: test
175 | user: test
163 | date: Thu Jan 01 00:00:00 1970 +0000
176 | date: Thu Jan 01 00:00:00 1970 +0000
164 | summary: b
177 | summary: b
165 |
178 |
166 o changeset: 0:cb9a9f314b8b
179 o changeset: 0:cb9a9f314b8b
167 user: test
180 user: test
168 date: Thu Jan 01 00:00:00 1970 +0000
181 date: Thu Jan 01 00:00:00 1970 +0000
169 summary: a
182 summary: a
170
183
171
184
172 slightly different this time
185 slightly different this time
173
186
174 $ hg histedit 177f92b77385 --commands - << EOF 2>&1 | fixbundle
187 $ hg histedit 177f92b77385 --commands - << EOF 2>&1 | fixbundle
175 > pick 10517e47bbbb d
188 > pick 10517e47bbbb d
176 > pick 7eca9b5b1148 f
189 > pick 7eca9b5b1148 f
177 > pick 915da888f2de e
190 > pick 915da888f2de e
178 > pick 177f92b77385 c
191 > pick 177f92b77385 c
179 > EOF
192 > EOF
180 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
193 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
181 $ hg log --graph
194 $ hg log --graph
182 @ changeset: 5:38b92f448761
195 @ changeset: 5:38b92f448761
183 | tag: tip
196 | tag: tip
184 | user: test
197 | user: test
185 | date: Thu Jan 01 00:00:00 1970 +0000
198 | date: Thu Jan 01 00:00:00 1970 +0000
186 | summary: c
199 | summary: c
187 |
200 |
188 o changeset: 4:de71b079d9ce
201 o changeset: 4:de71b079d9ce
189 | user: test
202 | user: test
190 | date: Thu Jan 01 00:00:00 1970 +0000
203 | date: Thu Jan 01 00:00:00 1970 +0000
191 | summary: e
204 | summary: e
192 |
205 |
193 o changeset: 3:be9ae3a309c6
206 o changeset: 3:be9ae3a309c6
194 | user: test
207 | user: test
195 | date: Thu Jan 01 00:00:00 1970 +0000
208 | date: Thu Jan 01 00:00:00 1970 +0000
196 | summary: f
209 | summary: f
197 |
210 |
198 o changeset: 2:799205341b6b
211 o changeset: 2:799205341b6b
199 | user: test
212 | user: test
200 | date: Thu Jan 01 00:00:00 1970 +0000
213 | date: Thu Jan 01 00:00:00 1970 +0000
201 | summary: d
214 | summary: d
202 |
215 |
203 o changeset: 1:d2ae7f538514
216 o changeset: 1:d2ae7f538514
204 | user: test
217 | user: test
205 | date: Thu Jan 01 00:00:00 1970 +0000
218 | date: Thu Jan 01 00:00:00 1970 +0000
206 | summary: b
219 | summary: b
207 |
220 |
208 o changeset: 0:cb9a9f314b8b
221 o changeset: 0:cb9a9f314b8b
209 user: test
222 user: test
210 date: Thu Jan 01 00:00:00 1970 +0000
223 date: Thu Jan 01 00:00:00 1970 +0000
211 summary: a
224 summary: a
212
225
213
226
214 keep prevents stripping dead revs
227 keep prevents stripping dead revs
215 $ hg histedit 799205341b6b --keep --commands - 2>&1 << EOF | fixbundle
228 $ hg histedit 799205341b6b --keep --commands - 2>&1 << EOF | fixbundle
216 > pick 799205341b6b d
229 > pick 799205341b6b d
217 > pick be9ae3a309c6 f
230 > pick be9ae3a309c6 f
218 > pick 38b92f448761 c
231 > pick 38b92f448761 c
219 > pick de71b079d9ce e
232 > pick de71b079d9ce e
220 > EOF
233 > EOF
221 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
234 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
222 $ hg log --graph
235 $ hg log --graph
223 @ changeset: 7:803ef1c6fcfd
236 @ changeset: 7:803ef1c6fcfd
224 | tag: tip
237 | tag: tip
225 | user: test
238 | user: test
226 | date: Thu Jan 01 00:00:00 1970 +0000
239 | date: Thu Jan 01 00:00:00 1970 +0000
227 | summary: e
240 | summary: e
228 |
241 |
229 o changeset: 6:ece0b8d93dda
242 o changeset: 6:ece0b8d93dda
230 | parent: 3:be9ae3a309c6
243 | parent: 3:be9ae3a309c6
231 | user: test
244 | user: test
232 | date: Thu Jan 01 00:00:00 1970 +0000
245 | date: Thu Jan 01 00:00:00 1970 +0000
233 | summary: c
246 | summary: c
234 |
247 |
235 | o changeset: 5:38b92f448761
248 | o changeset: 5:38b92f448761
236 | | user: test
249 | | user: test
237 | | date: Thu Jan 01 00:00:00 1970 +0000
250 | | date: Thu Jan 01 00:00:00 1970 +0000
238 | | summary: c
251 | | summary: c
239 | |
252 | |
240 | o changeset: 4:de71b079d9ce
253 | o changeset: 4:de71b079d9ce
241 |/ user: test
254 |/ user: test
242 | date: Thu Jan 01 00:00:00 1970 +0000
255 | date: Thu Jan 01 00:00:00 1970 +0000
243 | summary: e
256 | summary: e
244 |
257 |
245 o changeset: 3:be9ae3a309c6
258 o changeset: 3:be9ae3a309c6
246 | user: test
259 | user: test
247 | date: Thu Jan 01 00:00:00 1970 +0000
260 | date: Thu Jan 01 00:00:00 1970 +0000
248 | summary: f
261 | summary: f
249 |
262 |
250 o changeset: 2:799205341b6b
263 o changeset: 2:799205341b6b
251 | user: test
264 | user: test
252 | date: Thu Jan 01 00:00:00 1970 +0000
265 | date: Thu Jan 01 00:00:00 1970 +0000
253 | summary: d
266 | summary: d
254 |
267 |
255 o changeset: 1:d2ae7f538514
268 o changeset: 1:d2ae7f538514
256 | user: test
269 | user: test
257 | date: Thu Jan 01 00:00:00 1970 +0000
270 | date: Thu Jan 01 00:00:00 1970 +0000
258 | summary: b
271 | summary: b
259 |
272 |
260 o changeset: 0:cb9a9f314b8b
273 o changeset: 0:cb9a9f314b8b
261 user: test
274 user: test
262 date: Thu Jan 01 00:00:00 1970 +0000
275 date: Thu Jan 01 00:00:00 1970 +0000
263 summary: a
276 summary: a
264
277
265
278
266 try with --rev
279 try with --rev
267 $ hg histedit --commands - --rev -2 2>&1 <<EOF | fixbundle
280 $ hg histedit --commands - --rev -2 2>&1 <<EOF | fixbundle
268 > pick de71b079d9ce e
281 > pick de71b079d9ce e
269 > pick 38b92f448761 c
282 > pick 38b92f448761 c
270 > EOF
283 > EOF
271 hg: parse error: may not use "pick" with changesets other than the ones listed
284 hg: parse error: may not use "pick" with changesets other than the ones listed
272 $ hg log --graph
285 $ hg log --graph
273 @ changeset: 7:803ef1c6fcfd
286 @ changeset: 7:803ef1c6fcfd
274 | tag: tip
287 | tag: tip
275 | user: test
288 | user: test
276 | date: Thu Jan 01 00:00:00 1970 +0000
289 | date: Thu Jan 01 00:00:00 1970 +0000
277 | summary: e
290 | summary: e
278 |
291 |
279 o changeset: 6:ece0b8d93dda
292 o changeset: 6:ece0b8d93dda
280 | parent: 3:be9ae3a309c6
293 | parent: 3:be9ae3a309c6
281 | user: test
294 | user: test
282 | date: Thu Jan 01 00:00:00 1970 +0000
295 | date: Thu Jan 01 00:00:00 1970 +0000
283 | summary: c
296 | summary: c
284 |
297 |
285 | o changeset: 5:38b92f448761
298 | o changeset: 5:38b92f448761
286 | | user: test
299 | | user: test
287 | | date: Thu Jan 01 00:00:00 1970 +0000
300 | | date: Thu Jan 01 00:00:00 1970 +0000
288 | | summary: c
301 | | summary: c
289 | |
302 | |
290 | o changeset: 4:de71b079d9ce
303 | o changeset: 4:de71b079d9ce
291 |/ user: test
304 |/ user: test
292 | date: Thu Jan 01 00:00:00 1970 +0000
305 | date: Thu Jan 01 00:00:00 1970 +0000
293 | summary: e
306 | summary: e
294 |
307 |
295 o changeset: 3:be9ae3a309c6
308 o changeset: 3:be9ae3a309c6
296 | user: test
309 | user: test
297 | date: Thu Jan 01 00:00:00 1970 +0000
310 | date: Thu Jan 01 00:00:00 1970 +0000
298 | summary: f
311 | summary: f
299 |
312 |
300 o changeset: 2:799205341b6b
313 o changeset: 2:799205341b6b
301 | user: test
314 | user: test
302 | date: Thu Jan 01 00:00:00 1970 +0000
315 | date: Thu Jan 01 00:00:00 1970 +0000
303 | summary: d
316 | summary: d
304 |
317 |
305 o changeset: 1:d2ae7f538514
318 o changeset: 1:d2ae7f538514
306 | user: test
319 | user: test
307 | date: Thu Jan 01 00:00:00 1970 +0000
320 | date: Thu Jan 01 00:00:00 1970 +0000
308 | summary: b
321 | summary: b
309 |
322 |
310 o changeset: 0:cb9a9f314b8b
323 o changeset: 0:cb9a9f314b8b
311 user: test
324 user: test
312 date: Thu Jan 01 00:00:00 1970 +0000
325 date: Thu Jan 01 00:00:00 1970 +0000
313 summary: a
326 summary: a
314
327
315 Verify that revsetalias entries work with histedit:
328 Verify that revsetalias entries work with histedit:
316 $ cat >> $HGRCPATH <<EOF
329 $ cat >> $HGRCPATH <<EOF
317 > [revsetalias]
330 > [revsetalias]
318 > grandparent(ARG) = p1(p1(ARG))
331 > grandparent(ARG) = p1(p1(ARG))
319 > EOF
332 > EOF
320 $ echo extra commit >> c
333 $ echo extra commit >> c
321 $ hg ci -m 'extra commit to c'
334 $ hg ci -m 'extra commit to c'
322 $ HGEDITOR=cat hg histedit 'grandparent(.)'
335 $ HGEDITOR=cat hg histedit 'grandparent(.)'
323 pick ece0b8d93dda 6 c
336 pick ece0b8d93dda 6 c
324 pick 803ef1c6fcfd 7 e
337 pick 803ef1c6fcfd 7 e
325 pick 9c863c565126 8 extra commit to c
338 pick 9c863c565126 8 extra commit to c
326
339
327 # Edit history between ece0b8d93dda and 9c863c565126
340 # Edit history between ece0b8d93dda and 9c863c565126
328 #
341 #
329 # Commits are listed from least to most recent
342 # Commits are listed from least to most recent
330 #
343 #
331 # Commands:
344 # Commands:
332 # p, pick = use commit
345 # p, pick = use commit
333 # e, edit = use commit, but stop for amending
346 # e, edit = use commit, but stop for amending
334 # f, fold = use commit, but combine it with the one above
347 # f, fold = use commit, but combine it with the one above
335 # r, roll = like fold, but discard this commit's description
348 # r, roll = like fold, but discard this commit's description
336 # d, drop = remove commit from history
349 # d, drop = remove commit from history
337 # m, mess = edit commit message without changing commit content
350 # m, mess = edit commit message without changing commit content
338 #
351 #
339
352
340 should also work if a commit message is missing
353 should also work if a commit message is missing
341 $ BUNDLE="$TESTDIR/missing-comment.hg"
354 $ BUNDLE="$TESTDIR/missing-comment.hg"
342 $ hg init missing
355 $ hg init missing
343 $ cd missing
356 $ cd missing
344 $ hg unbundle $BUNDLE
357 $ hg unbundle $BUNDLE
345 adding changesets
358 adding changesets
346 adding manifests
359 adding manifests
347 adding file changes
360 adding file changes
348 added 3 changesets with 3 changes to 1 files
361 added 3 changesets with 3 changes to 1 files
349 (run 'hg update' to get a working copy)
362 (run 'hg update' to get a working copy)
350 $ hg co tip
363 $ hg co tip
351 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
364 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
352 $ hg log --graph
365 $ hg log --graph
353 @ changeset: 2:bd22688093b3
366 @ changeset: 2:bd22688093b3
354 | tag: tip
367 | tag: tip
355 | user: Robert Altman <robert.altman@telventDTN.com>
368 | user: Robert Altman <robert.altman@telventDTN.com>
356 | date: Mon Nov 28 16:40:04 2011 +0000
369 | date: Mon Nov 28 16:40:04 2011 +0000
357 | summary: Update file.
370 | summary: Update file.
358 |
371 |
359 o changeset: 1:3b3e956f9171
372 o changeset: 1:3b3e956f9171
360 | user: Robert Altman <robert.altman@telventDTN.com>
373 | user: Robert Altman <robert.altman@telventDTN.com>
361 | date: Mon Nov 28 16:37:57 2011 +0000
374 | date: Mon Nov 28 16:37:57 2011 +0000
362 |
375 |
363 o changeset: 0:141947992243
376 o changeset: 0:141947992243
364 user: Robert Altman <robert.altman@telventDTN.com>
377 user: Robert Altman <robert.altman@telventDTN.com>
365 date: Mon Nov 28 16:35:28 2011 +0000
378 date: Mon Nov 28 16:35:28 2011 +0000
366 summary: Checked in text file
379 summary: Checked in text file
367
380
368 $ hg histedit 0
381 $ hg histedit 0
369 $ cd ..
382 $ cd ..
370
383
371 $ cd ..
384 $ cd ..
372
385
373
386
374 Test to make sure folding renames doesn't cause bogus conflicts (issue4251):
387 Test to make sure folding renames doesn't cause bogus conflicts (issue4251):
375 $ hg init issue4251
388 $ hg init issue4251
376 $ cd issue4251
389 $ cd issue4251
377
390
378 $ mkdir initial-dir
391 $ mkdir initial-dir
379 $ echo foo > initial-dir/initial-file
392 $ echo foo > initial-dir/initial-file
380 $ hg add initial-dir/initial-file
393 $ hg add initial-dir/initial-file
381 $ hg commit -m "initial commit"
394 $ hg commit -m "initial commit"
382
395
383 Move the file to a new directory, and in the same commit, change its content:
396 Move the file to a new directory, and in the same commit, change its content:
384 $ mkdir another-dir
397 $ mkdir another-dir
385 $ hg mv initial-dir/initial-file another-dir/
398 $ hg mv initial-dir/initial-file another-dir/
386 $ echo changed > another-dir/initial-file
399 $ echo changed > another-dir/initial-file
387 $ hg commit -m "moved and changed"
400 $ hg commit -m "moved and changed"
388
401
389 Rename the file:
402 Rename the file:
390 $ hg mv another-dir/initial-file another-dir/renamed-file
403 $ hg mv another-dir/initial-file another-dir/renamed-file
391 $ hg commit -m "renamed"
404 $ hg commit -m "renamed"
392
405
393 Now, let's try to fold the second commit into the first:
406 Now, let's try to fold the second commit into the first:
394 $ cat > editor.sh <<EOF
407 $ cat > editor.sh <<EOF
395 > #!/bin/sh
408 > #!/bin/sh
396 > cat > \$1 <<ENDOF
409 > cat > \$1 <<ENDOF
397 > pick b0f4233702ca 0 initial commit
410 > pick b0f4233702ca 0 initial commit
398 > fold 5e8704a8f2d2 1 moved and changed
411 > fold 5e8704a8f2d2 1 moved and changed
399 > pick 40e7299e8fa7 2 renamed
412 > pick 40e7299e8fa7 2 renamed
400 > ENDOF
413 > ENDOF
401 > EOF
414 > EOF
402
415
403 $ HGEDITOR="sh ./editor.sh" hg histedit 0
416 $ HGEDITOR="sh ./editor.sh" hg histedit 0
404 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
417 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
405 adding another-dir/initial-file (glob)
418 adding another-dir/initial-file (glob)
406 removing initial-dir/initial-file (glob)
419 removing initial-dir/initial-file (glob)
407 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
420 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
408 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
421 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
409 saved backup bundle to $TESTTMP/issue4251/.hg/strip-backup/*-backup.hg (glob)
422 saved backup bundle to $TESTTMP/issue4251/.hg/strip-backup/*-backup.hg (glob)
410 saved backup bundle to $TESTTMP/issue4251/.hg/strip-backup/*-backup.hg (glob)
423 saved backup bundle to $TESTTMP/issue4251/.hg/strip-backup/*-backup.hg (glob)
411
424
412 $ hg --config diff.git=yes export 0
425 $ hg --config diff.git=yes export 0
413 # HG changeset patch
426 # HG changeset patch
414 # User test
427 # User test
415 # Date 0 0
428 # Date 0 0
416 # Thu Jan 01 00:00:00 1970 +0000
429 # Thu Jan 01 00:00:00 1970 +0000
417 # Node ID fffadc26f8f85623ce60b028a3f1ccc3730f8530
430 # Node ID fffadc26f8f85623ce60b028a3f1ccc3730f8530
418 # Parent 0000000000000000000000000000000000000000
431 # Parent 0000000000000000000000000000000000000000
419 pick b0f4233702ca 0 initial commit
432 pick b0f4233702ca 0 initial commit
420 fold 5e8704a8f2d2 1 moved and changed
433 fold 5e8704a8f2d2 1 moved and changed
421 pick 40e7299e8fa7 2 renamed
434 pick 40e7299e8fa7 2 renamed
422
435
423 diff --git a/another-dir/initial-file b/another-dir/initial-file
436 diff --git a/another-dir/initial-file b/another-dir/initial-file
424 new file mode 100644
437 new file mode 100644
425 --- /dev/null
438 --- /dev/null
426 +++ b/another-dir/initial-file
439 +++ b/another-dir/initial-file
427 @@ -0,0 +1,1 @@
440 @@ -0,0 +1,1 @@
428 +changed
441 +changed
429
442
430 $ hg --config diff.git=yes export 1
443 $ hg --config diff.git=yes export 1
431 # HG changeset patch
444 # HG changeset patch
432 # User test
445 # User test
433 # Date 0 0
446 # Date 0 0
434 # Thu Jan 01 00:00:00 1970 +0000
447 # Thu Jan 01 00:00:00 1970 +0000
435 # Node ID 9b730d82b00af8a2766facebfa47cc124405a118
448 # Node ID 9b730d82b00af8a2766facebfa47cc124405a118
436 # Parent fffadc26f8f85623ce60b028a3f1ccc3730f8530
449 # Parent fffadc26f8f85623ce60b028a3f1ccc3730f8530
437 renamed
450 renamed
438
451
439 diff --git a/another-dir/initial-file b/another-dir/renamed-file
452 diff --git a/another-dir/initial-file b/another-dir/renamed-file
440 rename from another-dir/initial-file
453 rename from another-dir/initial-file
441 rename to another-dir/renamed-file
454 rename to another-dir/renamed-file
442
455
443 $ cd ..
456 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now