##// END OF EJS Templates
histedit: improve documentation and behaviour of dates...
Ben Schmidt -
r31055:f1b63ec4 default
parent child Browse files
Show More
@@ -1,1605 +1,1606
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. The date used
75 this example, let's assume that the commit message was changed to
75 for the commit will be the later of the two commits' dates. For this example,
76 ``Add beta and delta.`` After histedit has run and had a chance to
76 let's assume that the commit message was changed to ``Add beta and delta.``
77 remove any old or temporary revisions it needed, the history looks
77 After histedit has run and had a chance to remove any old or temporary
78 like this::
78 revisions it needed, the history looks 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. If there are uncommitted
101 new commit message, but the default commit message will be the
101 changes, you'll be prompted for a new commit message, but the default
102 original message for the ``edit`` ed revision.
102 commit message will be the original message for the ``edit`` ed
103 revision, and the date of the original commit will be preserved.
103
104
104 The ``message`` operation will give you a chance to revise a commit
105 The ``message`` operation will give you a chance to revise a commit
105 message without changing the contents. It's a shortcut for doing
106 message without changing the contents. It's a shortcut for doing
106 ``edit`` immediately followed by `hg histedit --continue``.
107 ``edit`` immediately followed by `hg histedit --continue``.
107
108
108 If ``histedit`` encounters a conflict when moving a revision (while
109 If ``histedit`` encounters a conflict when moving a revision (while
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 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
111 ``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
112 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,
113 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
114 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
115 have made and return to the state before you attempted to edit your
115 history.
116 history.
116
117
117 If we clone the histedit-ed example repository above and add four more
118 If we clone the histedit-ed example repository above and add four more
118 changes, such that we have the following history::
119 changes, such that we have the following history::
119
120
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 | Add theta
122 | Add theta
122 |
123 |
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 | Add eta
125 | Add eta
125 |
126 |
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 | Add zeta
128 | Add zeta
128 |
129 |
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 | Add epsilon
131 | Add epsilon
131 |
132 |
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 | Add beta and delta.
134 | Add beta and delta.
134 |
135 |
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 | Add gamma
137 | Add gamma
137 |
138 |
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 Add alpha
140 Add alpha
140
141
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 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
143 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
144 repository that Mercurial does not detect to be related to the source
144 repo, you can add a ``--force`` option.
145 repo, you can add a ``--force`` option.
145
146
146 Config
147 Config
147 ------
148 ------
148
149
149 Histedit rule lines are truncated to 80 characters by default. You
150 Histedit rule lines are truncated to 80 characters by default. You
150 can customize this behavior by setting a different length in your
151 can customize this behavior by setting a different length in your
151 configuration file::
152 configuration file::
152
153
153 [histedit]
154 [histedit]
154 linelen = 120 # truncate rule lines at 120 characters
155 linelen = 120 # truncate rule lines at 120 characters
155
156
156 ``hg histedit`` attempts to automatically choose an appropriate base
157 ``hg histedit`` attempts to automatically choose an appropriate base
157 revision to use. To change which base revision is used, define a
158 revision to use. To change which base revision is used, define a
158 revset in your configuration file::
159 revset in your configuration file::
159
160
160 [histedit]
161 [histedit]
161 defaultrev = only(.) & draft()
162 defaultrev = only(.) & draft()
162
163
163 By default each edited revision needs to be present in histedit commands.
164 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
165 To remove revision you need to use ``drop`` operation. You can configure
165 the drop to be implicit for missing commits by adding::
166 the drop to be implicit for missing commits by adding::
166
167
167 [histedit]
168 [histedit]
168 dropmissing = True
169 dropmissing = True
169
170
170 """
171 """
171
172
172 from __future__ import absolute_import
173 from __future__ import absolute_import
173
174
174 import errno
175 import errno
175 import os
176 import os
176
177
177 from mercurial.i18n import _
178 from mercurial.i18n import _
178 from mercurial import (
179 from mercurial import (
179 bundle2,
180 bundle2,
180 cmdutil,
181 cmdutil,
181 context,
182 context,
182 copies,
183 copies,
183 destutil,
184 destutil,
184 discovery,
185 discovery,
185 error,
186 error,
186 exchange,
187 exchange,
187 extensions,
188 extensions,
188 hg,
189 hg,
189 lock,
190 lock,
190 merge as mergemod,
191 merge as mergemod,
191 node,
192 node,
192 obsolete,
193 obsolete,
193 repair,
194 repair,
194 scmutil,
195 scmutil,
195 util,
196 util,
196 )
197 )
197
198
198 pickle = util.pickle
199 pickle = util.pickle
199 release = lock.release
200 release = lock.release
200 cmdtable = {}
201 cmdtable = {}
201 command = cmdutil.command(cmdtable)
202 command = cmdutil.command(cmdtable)
202
203
203 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
204 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
204 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
205 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
205 # be specifying the version(s) of Mercurial they are tested with, or
206 # be specifying the version(s) of Mercurial they are tested with, or
206 # leave the attribute unspecified.
207 # leave the attribute unspecified.
207 testedwith = 'ships-with-hg-core'
208 testedwith = 'ships-with-hg-core'
208
209
209 actiontable = {}
210 actiontable = {}
210 primaryactions = set()
211 primaryactions = set()
211 secondaryactions = set()
212 secondaryactions = set()
212 tertiaryactions = set()
213 tertiaryactions = set()
213 internalactions = set()
214 internalactions = set()
214
215
215 def geteditcomment(ui, first, last):
216 def geteditcomment(ui, first, last):
216 """ construct the editor comment
217 """ construct the editor comment
217 The comment includes::
218 The comment includes::
218 - an intro
219 - an intro
219 - sorted primary commands
220 - sorted primary commands
220 - sorted short commands
221 - sorted short commands
221 - sorted long commands
222 - sorted long commands
222 - additional hints
223 - additional hints
223
224
224 Commands are only included once.
225 Commands are only included once.
225 """
226 """
226 intro = _("""Edit history between %s and %s
227 intro = _("""Edit history between %s and %s
227
228
228 Commits are listed from least to most recent
229 Commits are listed from least to most recent
229
230
230 You can reorder changesets by reordering the lines
231 You can reorder changesets by reordering the lines
231
232
232 Commands:
233 Commands:
233 """)
234 """)
234 actions = []
235 actions = []
235 def addverb(v):
236 def addverb(v):
236 a = actiontable[v]
237 a = actiontable[v]
237 lines = a.message.split("\n")
238 lines = a.message.split("\n")
238 if len(a.verbs):
239 if len(a.verbs):
239 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
240 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
240 actions.append(" %s = %s" % (v, lines[0]))
241 actions.append(" %s = %s" % (v, lines[0]))
241 actions.extend([' %s' for l in lines[1:]])
242 actions.extend([' %s' for l in lines[1:]])
242
243
243 for v in (
244 for v in (
244 sorted(primaryactions) +
245 sorted(primaryactions) +
245 sorted(secondaryactions) +
246 sorted(secondaryactions) +
246 sorted(tertiaryactions)
247 sorted(tertiaryactions)
247 ):
248 ):
248 addverb(v)
249 addverb(v)
249 actions.append('')
250 actions.append('')
250
251
251 hints = []
252 hints = []
252 if ui.configbool('histedit', 'dropmissing'):
253 if ui.configbool('histedit', 'dropmissing'):
253 hints.append("Deleting a changeset from the list "
254 hints.append("Deleting a changeset from the list "
254 "will DISCARD it from the edited history!")
255 "will DISCARD it from the edited history!")
255
256
256 lines = (intro % (first, last)).split('\n') + actions + hints
257 lines = (intro % (first, last)).split('\n') + actions + hints
257
258
258 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
259 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
259
260
260 class histeditstate(object):
261 class histeditstate(object):
261 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
262 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
262 topmost=None, replacements=None, lock=None, wlock=None):
263 topmost=None, replacements=None, lock=None, wlock=None):
263 self.repo = repo
264 self.repo = repo
264 self.actions = actions
265 self.actions = actions
265 self.keep = keep
266 self.keep = keep
266 self.topmost = topmost
267 self.topmost = topmost
267 self.parentctxnode = parentctxnode
268 self.parentctxnode = parentctxnode
268 self.lock = lock
269 self.lock = lock
269 self.wlock = wlock
270 self.wlock = wlock
270 self.backupfile = None
271 self.backupfile = None
271 if replacements is None:
272 if replacements is None:
272 self.replacements = []
273 self.replacements = []
273 else:
274 else:
274 self.replacements = replacements
275 self.replacements = replacements
275
276
276 def read(self):
277 def read(self):
277 """Load histedit state from disk and set fields appropriately."""
278 """Load histedit state from disk and set fields appropriately."""
278 try:
279 try:
279 state = self.repo.vfs.read('histedit-state')
280 state = self.repo.vfs.read('histedit-state')
280 except IOError as err:
281 except IOError as err:
281 if err.errno != errno.ENOENT:
282 if err.errno != errno.ENOENT:
282 raise
283 raise
283 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
284 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
284
285
285 if state.startswith('v1\n'):
286 if state.startswith('v1\n'):
286 data = self._load()
287 data = self._load()
287 parentctxnode, rules, keep, topmost, replacements, backupfile = data
288 parentctxnode, rules, keep, topmost, replacements, backupfile = data
288 else:
289 else:
289 data = pickle.loads(state)
290 data = pickle.loads(state)
290 parentctxnode, rules, keep, topmost, replacements = data
291 parentctxnode, rules, keep, topmost, replacements = data
291 backupfile = None
292 backupfile = None
292
293
293 self.parentctxnode = parentctxnode
294 self.parentctxnode = parentctxnode
294 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
295 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
295 actions = parserules(rules, self)
296 actions = parserules(rules, self)
296 self.actions = actions
297 self.actions = actions
297 self.keep = keep
298 self.keep = keep
298 self.topmost = topmost
299 self.topmost = topmost
299 self.replacements = replacements
300 self.replacements = replacements
300 self.backupfile = backupfile
301 self.backupfile = backupfile
301
302
302 def write(self):
303 def write(self):
303 fp = self.repo.vfs('histedit-state', 'w')
304 fp = self.repo.vfs('histedit-state', 'w')
304 fp.write('v1\n')
305 fp.write('v1\n')
305 fp.write('%s\n' % node.hex(self.parentctxnode))
306 fp.write('%s\n' % node.hex(self.parentctxnode))
306 fp.write('%s\n' % node.hex(self.topmost))
307 fp.write('%s\n' % node.hex(self.topmost))
307 fp.write('%s\n' % self.keep)
308 fp.write('%s\n' % self.keep)
308 fp.write('%d\n' % len(self.actions))
309 fp.write('%d\n' % len(self.actions))
309 for action in self.actions:
310 for action in self.actions:
310 fp.write('%s\n' % action.tostate())
311 fp.write('%s\n' % action.tostate())
311 fp.write('%d\n' % len(self.replacements))
312 fp.write('%d\n' % len(self.replacements))
312 for replacement in self.replacements:
313 for replacement in self.replacements:
313 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
314 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
314 for r in replacement[1])))
315 for r in replacement[1])))
315 backupfile = self.backupfile
316 backupfile = self.backupfile
316 if not backupfile:
317 if not backupfile:
317 backupfile = ''
318 backupfile = ''
318 fp.write('%s\n' % backupfile)
319 fp.write('%s\n' % backupfile)
319 fp.close()
320 fp.close()
320
321
321 def _load(self):
322 def _load(self):
322 fp = self.repo.vfs('histedit-state', 'r')
323 fp = self.repo.vfs('histedit-state', 'r')
323 lines = [l[:-1] for l in fp.readlines()]
324 lines = [l[:-1] for l in fp.readlines()]
324
325
325 index = 0
326 index = 0
326 lines[index] # version number
327 lines[index] # version number
327 index += 1
328 index += 1
328
329
329 parentctxnode = node.bin(lines[index])
330 parentctxnode = node.bin(lines[index])
330 index += 1
331 index += 1
331
332
332 topmost = node.bin(lines[index])
333 topmost = node.bin(lines[index])
333 index += 1
334 index += 1
334
335
335 keep = lines[index] == 'True'
336 keep = lines[index] == 'True'
336 index += 1
337 index += 1
337
338
338 # Rules
339 # Rules
339 rules = []
340 rules = []
340 rulelen = int(lines[index])
341 rulelen = int(lines[index])
341 index += 1
342 index += 1
342 for i in xrange(rulelen):
343 for i in xrange(rulelen):
343 ruleaction = lines[index]
344 ruleaction = lines[index]
344 index += 1
345 index += 1
345 rule = lines[index]
346 rule = lines[index]
346 index += 1
347 index += 1
347 rules.append((ruleaction, rule))
348 rules.append((ruleaction, rule))
348
349
349 # Replacements
350 # Replacements
350 replacements = []
351 replacements = []
351 replacementlen = int(lines[index])
352 replacementlen = int(lines[index])
352 index += 1
353 index += 1
353 for i in xrange(replacementlen):
354 for i in xrange(replacementlen):
354 replacement = lines[index]
355 replacement = lines[index]
355 original = node.bin(replacement[:40])
356 original = node.bin(replacement[:40])
356 succ = [node.bin(replacement[i:i + 40]) for i in
357 succ = [node.bin(replacement[i:i + 40]) for i in
357 range(40, len(replacement), 40)]
358 range(40, len(replacement), 40)]
358 replacements.append((original, succ))
359 replacements.append((original, succ))
359 index += 1
360 index += 1
360
361
361 backupfile = lines[index]
362 backupfile = lines[index]
362 index += 1
363 index += 1
363
364
364 fp.close()
365 fp.close()
365
366
366 return parentctxnode, rules, keep, topmost, replacements, backupfile
367 return parentctxnode, rules, keep, topmost, replacements, backupfile
367
368
368 def clear(self):
369 def clear(self):
369 if self.inprogress():
370 if self.inprogress():
370 self.repo.vfs.unlink('histedit-state')
371 self.repo.vfs.unlink('histedit-state')
371
372
372 def inprogress(self):
373 def inprogress(self):
373 return self.repo.vfs.exists('histedit-state')
374 return self.repo.vfs.exists('histedit-state')
374
375
375
376
376 class histeditaction(object):
377 class histeditaction(object):
377 def __init__(self, state, node):
378 def __init__(self, state, node):
378 self.state = state
379 self.state = state
379 self.repo = state.repo
380 self.repo = state.repo
380 self.node = node
381 self.node = node
381
382
382 @classmethod
383 @classmethod
383 def fromrule(cls, state, rule):
384 def fromrule(cls, state, rule):
384 """Parses the given rule, returning an instance of the histeditaction.
385 """Parses the given rule, returning an instance of the histeditaction.
385 """
386 """
386 rulehash = rule.strip().split(' ', 1)[0]
387 rulehash = rule.strip().split(' ', 1)[0]
387 try:
388 try:
388 rev = node.bin(rulehash)
389 rev = node.bin(rulehash)
389 except TypeError:
390 except TypeError:
390 raise error.ParseError("invalid changeset %s" % rulehash)
391 raise error.ParseError("invalid changeset %s" % rulehash)
391 return cls(state, rev)
392 return cls(state, rev)
392
393
393 def verify(self, prev, expected, seen):
394 def verify(self, prev, expected, seen):
394 """ Verifies semantic correctness of the rule"""
395 """ Verifies semantic correctness of the rule"""
395 repo = self.repo
396 repo = self.repo
396 ha = node.hex(self.node)
397 ha = node.hex(self.node)
397 try:
398 try:
398 self.node = repo[ha].node()
399 self.node = repo[ha].node()
399 except error.RepoError:
400 except error.RepoError:
400 raise error.ParseError(_('unknown changeset %s listed')
401 raise error.ParseError(_('unknown changeset %s listed')
401 % ha[:12])
402 % ha[:12])
402 if self.node is not None:
403 if self.node is not None:
403 self._verifynodeconstraints(prev, expected, seen)
404 self._verifynodeconstraints(prev, expected, seen)
404
405
405 def _verifynodeconstraints(self, prev, expected, seen):
406 def _verifynodeconstraints(self, prev, expected, seen):
406 # by default command need a node in the edited list
407 # by default command need a node in the edited list
407 if self.node not in expected:
408 if self.node not in expected:
408 raise error.ParseError(_('%s "%s" changeset was not a candidate')
409 raise error.ParseError(_('%s "%s" changeset was not a candidate')
409 % (self.verb, node.short(self.node)),
410 % (self.verb, node.short(self.node)),
410 hint=_('only use listed changesets'))
411 hint=_('only use listed changesets'))
411 # and only one command per node
412 # and only one command per node
412 if self.node in seen:
413 if self.node in seen:
413 raise error.ParseError(_('duplicated command for changeset %s') %
414 raise error.ParseError(_('duplicated command for changeset %s') %
414 node.short(self.node))
415 node.short(self.node))
415
416
416 def torule(self):
417 def torule(self):
417 """build a histedit rule line for an action
418 """build a histedit rule line for an action
418
419
419 by default lines are in the form:
420 by default lines are in the form:
420 <hash> <rev> <summary>
421 <hash> <rev> <summary>
421 """
422 """
422 ctx = self.repo[self.node]
423 ctx = self.repo[self.node]
423 summary = _getsummary(ctx)
424 summary = _getsummary(ctx)
424 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
425 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
425 # trim to 75 columns by default so it's not stupidly wide in my editor
426 # trim to 75 columns by default so it's not stupidly wide in my editor
426 # (the 5 more are left for verb)
427 # (the 5 more are left for verb)
427 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
428 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
428 maxlen = max(maxlen, 22) # avoid truncating hash
429 maxlen = max(maxlen, 22) # avoid truncating hash
429 return util.ellipsis(line, maxlen)
430 return util.ellipsis(line, maxlen)
430
431
431 def tostate(self):
432 def tostate(self):
432 """Print an action in format used by histedit state files
433 """Print an action in format used by histedit state files
433 (the first line is a verb, the remainder is the second)
434 (the first line is a verb, the remainder is the second)
434 """
435 """
435 return "%s\n%s" % (self.verb, node.hex(self.node))
436 return "%s\n%s" % (self.verb, node.hex(self.node))
436
437
437 def run(self):
438 def run(self):
438 """Runs the action. The default behavior is simply apply the action's
439 """Runs the action. The default behavior is simply apply the action's
439 rulectx onto the current parentctx."""
440 rulectx onto the current parentctx."""
440 self.applychange()
441 self.applychange()
441 self.continuedirty()
442 self.continuedirty()
442 return self.continueclean()
443 return self.continueclean()
443
444
444 def applychange(self):
445 def applychange(self):
445 """Applies the changes from this action's rulectx onto the current
446 """Applies the changes from this action's rulectx onto the current
446 parentctx, but does not commit them."""
447 parentctx, but does not commit them."""
447 repo = self.repo
448 repo = self.repo
448 rulectx = repo[self.node]
449 rulectx = repo[self.node]
449 repo.ui.pushbuffer(error=True, labeled=True)
450 repo.ui.pushbuffer(error=True, labeled=True)
450 hg.update(repo, self.state.parentctxnode, quietempty=True)
451 hg.update(repo, self.state.parentctxnode, quietempty=True)
451 stats = applychanges(repo.ui, repo, rulectx, {})
452 stats = applychanges(repo.ui, repo, rulectx, {})
452 if stats and stats[3] > 0:
453 if stats and stats[3] > 0:
453 buf = repo.ui.popbuffer()
454 buf = repo.ui.popbuffer()
454 repo.ui.write(*buf)
455 repo.ui.write(*buf)
455 raise error.InterventionRequired(
456 raise error.InterventionRequired(
456 _('Fix up the change (%s %s)') %
457 _('Fix up the change (%s %s)') %
457 (self.verb, node.short(self.node)),
458 (self.verb, node.short(self.node)),
458 hint=_('hg histedit --continue to resume'))
459 hint=_('hg histedit --continue to resume'))
459 else:
460 else:
460 repo.ui.popbuffer()
461 repo.ui.popbuffer()
461
462
462 def continuedirty(self):
463 def continuedirty(self):
463 """Continues the action when changes have been applied to the working
464 """Continues the action when changes have been applied to the working
464 copy. The default behavior is to commit the dirty changes."""
465 copy. The default behavior is to commit the dirty changes."""
465 repo = self.repo
466 repo = self.repo
466 rulectx = repo[self.node]
467 rulectx = repo[self.node]
467
468
468 editor = self.commiteditor()
469 editor = self.commiteditor()
469 commit = commitfuncfor(repo, rulectx)
470 commit = commitfuncfor(repo, rulectx)
470
471
471 commit(text=rulectx.description(), user=rulectx.user(),
472 commit(text=rulectx.description(), user=rulectx.user(),
472 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
473 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
473
474
474 def commiteditor(self):
475 def commiteditor(self):
475 """The editor to be used to edit the commit message."""
476 """The editor to be used to edit the commit message."""
476 return False
477 return False
477
478
478 def continueclean(self):
479 def continueclean(self):
479 """Continues the action when the working copy is clean. The default
480 """Continues the action when the working copy is clean. The default
480 behavior is to accept the current commit as the new version of the
481 behavior is to accept the current commit as the new version of the
481 rulectx."""
482 rulectx."""
482 ctx = self.repo['.']
483 ctx = self.repo['.']
483 if ctx.node() == self.state.parentctxnode:
484 if ctx.node() == self.state.parentctxnode:
484 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
485 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
485 node.short(self.node))
486 node.short(self.node))
486 return ctx, [(self.node, tuple())]
487 return ctx, [(self.node, tuple())]
487 if ctx.node() == self.node:
488 if ctx.node() == self.node:
488 # Nothing changed
489 # Nothing changed
489 return ctx, []
490 return ctx, []
490 return ctx, [(self.node, (ctx.node(),))]
491 return ctx, [(self.node, (ctx.node(),))]
491
492
492 def commitfuncfor(repo, src):
493 def commitfuncfor(repo, src):
493 """Build a commit function for the replacement of <src>
494 """Build a commit function for the replacement of <src>
494
495
495 This function ensure we apply the same treatment to all changesets.
496 This function ensure we apply the same treatment to all changesets.
496
497
497 - Add a 'histedit_source' entry in extra.
498 - Add a 'histedit_source' entry in extra.
498
499
499 Note that fold has its own separated logic because its handling is a bit
500 Note that fold has its own separated logic because its handling is a bit
500 different and not easily factored out of the fold method.
501 different and not easily factored out of the fold method.
501 """
502 """
502 phasemin = src.phase()
503 phasemin = src.phase()
503 def commitfunc(**kwargs):
504 def commitfunc(**kwargs):
504 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
505 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
505 try:
506 try:
506 repo.ui.setconfig('phases', 'new-commit', phasemin,
507 repo.ui.setconfig('phases', 'new-commit', phasemin,
507 'histedit')
508 'histedit')
508 extra = kwargs.get('extra', {}).copy()
509 extra = kwargs.get('extra', {}).copy()
509 extra['histedit_source'] = src.hex()
510 extra['histedit_source'] = src.hex()
510 kwargs['extra'] = extra
511 kwargs['extra'] = extra
511 return repo.commit(**kwargs)
512 return repo.commit(**kwargs)
512 finally:
513 finally:
513 repo.ui.restoreconfig(phasebackup)
514 repo.ui.restoreconfig(phasebackup)
514 return commitfunc
515 return commitfunc
515
516
516 def applychanges(ui, repo, ctx, opts):
517 def applychanges(ui, repo, ctx, opts):
517 """Merge changeset from ctx (only) in the current working directory"""
518 """Merge changeset from ctx (only) in the current working directory"""
518 wcpar = repo.dirstate.parents()[0]
519 wcpar = repo.dirstate.parents()[0]
519 if ctx.p1().node() == wcpar:
520 if ctx.p1().node() == wcpar:
520 # edits are "in place" we do not need to make any merge,
521 # edits are "in place" we do not need to make any merge,
521 # just applies changes on parent for editing
522 # just applies changes on parent for editing
522 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
523 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
523 stats = None
524 stats = None
524 else:
525 else:
525 try:
526 try:
526 # ui.forcemerge is an internal variable, do not document
527 # ui.forcemerge is an internal variable, do not document
527 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
528 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
528 'histedit')
529 'histedit')
529 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
530 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
530 finally:
531 finally:
531 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
532 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
532 return stats
533 return stats
533
534
534 def collapse(repo, first, last, commitopts, skipprompt=False):
535 def collapse(repo, first, last, commitopts, skipprompt=False):
535 """collapse the set of revisions from first to last as new one.
536 """collapse the set of revisions from first to last as new one.
536
537
537 Expected commit options are:
538 Expected commit options are:
538 - message
539 - message
539 - date
540 - date
540 - username
541 - username
541 Commit message is edited in all cases.
542 Commit message is edited in all cases.
542
543
543 This function works in memory."""
544 This function works in memory."""
544 ctxs = list(repo.set('%d::%d', first, last))
545 ctxs = list(repo.set('%d::%d', first, last))
545 if not ctxs:
546 if not ctxs:
546 return None
547 return None
547 for c in ctxs:
548 for c in ctxs:
548 if not c.mutable():
549 if not c.mutable():
549 raise error.ParseError(
550 raise error.ParseError(
550 _("cannot fold into public change %s") % node.short(c.node()))
551 _("cannot fold into public change %s") % node.short(c.node()))
551 base = first.parents()[0]
552 base = first.parents()[0]
552
553
553 # commit a new version of the old changeset, including the update
554 # commit a new version of the old changeset, including the update
554 # collect all files which might be affected
555 # collect all files which might be affected
555 files = set()
556 files = set()
556 for ctx in ctxs:
557 for ctx in ctxs:
557 files.update(ctx.files())
558 files.update(ctx.files())
558
559
559 # Recompute copies (avoid recording a -> b -> a)
560 # Recompute copies (avoid recording a -> b -> a)
560 copied = copies.pathcopies(base, last)
561 copied = copies.pathcopies(base, last)
561
562
562 # prune files which were reverted by the updates
563 # prune files which were reverted by the updates
563 files = [f for f in files if not cmdutil.samefile(f, last, base)]
564 files = [f for f in files if not cmdutil.samefile(f, last, base)]
564 # commit version of these files as defined by head
565 # commit version of these files as defined by head
565 headmf = last.manifest()
566 headmf = last.manifest()
566 def filectxfn(repo, ctx, path):
567 def filectxfn(repo, ctx, path):
567 if path in headmf:
568 if path in headmf:
568 fctx = last[path]
569 fctx = last[path]
569 flags = fctx.flags()
570 flags = fctx.flags()
570 mctx = context.memfilectx(repo,
571 mctx = context.memfilectx(repo,
571 fctx.path(), fctx.data(),
572 fctx.path(), fctx.data(),
572 islink='l' in flags,
573 islink='l' in flags,
573 isexec='x' in flags,
574 isexec='x' in flags,
574 copied=copied.get(path))
575 copied=copied.get(path))
575 return mctx
576 return mctx
576 return None
577 return None
577
578
578 if commitopts.get('message'):
579 if commitopts.get('message'):
579 message = commitopts['message']
580 message = commitopts['message']
580 else:
581 else:
581 message = first.description()
582 message = first.description()
582 user = commitopts.get('user')
583 user = commitopts.get('user')
583 date = commitopts.get('date')
584 date = commitopts.get('date')
584 extra = commitopts.get('extra')
585 extra = commitopts.get('extra')
585
586
586 parents = (first.p1().node(), first.p2().node())
587 parents = (first.p1().node(), first.p2().node())
587 editor = None
588 editor = None
588 if not skipprompt:
589 if not skipprompt:
589 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
590 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
590 new = context.memctx(repo,
591 new = context.memctx(repo,
591 parents=parents,
592 parents=parents,
592 text=message,
593 text=message,
593 files=files,
594 files=files,
594 filectxfn=filectxfn,
595 filectxfn=filectxfn,
595 user=user,
596 user=user,
596 date=date,
597 date=date,
597 extra=extra,
598 extra=extra,
598 editor=editor)
599 editor=editor)
599 return repo.commitctx(new)
600 return repo.commitctx(new)
600
601
601 def _isdirtywc(repo):
602 def _isdirtywc(repo):
602 return repo[None].dirty(missing=True)
603 return repo[None].dirty(missing=True)
603
604
604 def abortdirty():
605 def abortdirty():
605 raise error.Abort(_('working copy has pending changes'),
606 raise error.Abort(_('working copy has pending changes'),
606 hint=_('amend, commit, or revert them and run histedit '
607 hint=_('amend, commit, or revert them and run histedit '
607 '--continue, or abort with histedit --abort'))
608 '--continue, or abort with histedit --abort'))
608
609
609 def action(verbs, message, priority=False, internal=False):
610 def action(verbs, message, priority=False, internal=False):
610 def wrap(cls):
611 def wrap(cls):
611 assert not priority or not internal
612 assert not priority or not internal
612 verb = verbs[0]
613 verb = verbs[0]
613 if priority:
614 if priority:
614 primaryactions.add(verb)
615 primaryactions.add(verb)
615 elif internal:
616 elif internal:
616 internalactions.add(verb)
617 internalactions.add(verb)
617 elif len(verbs) > 1:
618 elif len(verbs) > 1:
618 secondaryactions.add(verb)
619 secondaryactions.add(verb)
619 else:
620 else:
620 tertiaryactions.add(verb)
621 tertiaryactions.add(verb)
621
622
622 cls.verb = verb
623 cls.verb = verb
623 cls.verbs = verbs
624 cls.verbs = verbs
624 cls.message = message
625 cls.message = message
625 for verb in verbs:
626 for verb in verbs:
626 actiontable[verb] = cls
627 actiontable[verb] = cls
627 return cls
628 return cls
628 return wrap
629 return wrap
629
630
630 @action(['pick', 'p'],
631 @action(['pick', 'p'],
631 _('use commit'),
632 _('use commit'),
632 priority=True)
633 priority=True)
633 class pick(histeditaction):
634 class pick(histeditaction):
634 def run(self):
635 def run(self):
635 rulectx = self.repo[self.node]
636 rulectx = self.repo[self.node]
636 if rulectx.parents()[0].node() == self.state.parentctxnode:
637 if rulectx.parents()[0].node() == self.state.parentctxnode:
637 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
638 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
638 return rulectx, []
639 return rulectx, []
639
640
640 return super(pick, self).run()
641 return super(pick, self).run()
641
642
642 @action(['edit', 'e'],
643 @action(['edit', 'e'],
643 _('use commit, but stop for amending'),
644 _('use commit, but stop for amending'),
644 priority=True)
645 priority=True)
645 class edit(histeditaction):
646 class edit(histeditaction):
646 def run(self):
647 def run(self):
647 repo = self.repo
648 repo = self.repo
648 rulectx = repo[self.node]
649 rulectx = repo[self.node]
649 hg.update(repo, self.state.parentctxnode, quietempty=True)
650 hg.update(repo, self.state.parentctxnode, quietempty=True)
650 applychanges(repo.ui, repo, rulectx, {})
651 applychanges(repo.ui, repo, rulectx, {})
651 raise error.InterventionRequired(
652 raise error.InterventionRequired(
652 _('Editing (%s), you may commit or record as needed now.')
653 _('Editing (%s), you may commit or record as needed now.')
653 % node.short(self.node),
654 % node.short(self.node),
654 hint=_('hg histedit --continue to resume'))
655 hint=_('hg histedit --continue to resume'))
655
656
656 def commiteditor(self):
657 def commiteditor(self):
657 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
658 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
658
659
659 @action(['fold', 'f'],
660 @action(['fold', 'f'],
660 _('use commit, but combine it with the one above'))
661 _('use commit, but combine it with the one above'))
661 class fold(histeditaction):
662 class fold(histeditaction):
662 def verify(self, prev, expected, seen):
663 def verify(self, prev, expected, seen):
663 """ Verifies semantic correctness of the fold rule"""
664 """ Verifies semantic correctness of the fold rule"""
664 super(fold, self).verify(prev, expected, seen)
665 super(fold, self).verify(prev, expected, seen)
665 repo = self.repo
666 repo = self.repo
666 if not prev:
667 if not prev:
667 c = repo[self.node].parents()[0]
668 c = repo[self.node].parents()[0]
668 elif not prev.verb in ('pick', 'base'):
669 elif not prev.verb in ('pick', 'base'):
669 return
670 return
670 else:
671 else:
671 c = repo[prev.node]
672 c = repo[prev.node]
672 if not c.mutable():
673 if not c.mutable():
673 raise error.ParseError(
674 raise error.ParseError(
674 _("cannot fold into public change %s") % node.short(c.node()))
675 _("cannot fold into public change %s") % node.short(c.node()))
675
676
676
677
677 def continuedirty(self):
678 def continuedirty(self):
678 repo = self.repo
679 repo = self.repo
679 rulectx = repo[self.node]
680 rulectx = repo[self.node]
680
681
681 commit = commitfuncfor(repo, rulectx)
682 commit = commitfuncfor(repo, rulectx)
682 commit(text='fold-temp-revision %s' % node.short(self.node),
683 commit(text='fold-temp-revision %s' % node.short(self.node),
683 user=rulectx.user(), date=rulectx.date(),
684 user=rulectx.user(), date=rulectx.date(),
684 extra=rulectx.extra())
685 extra=rulectx.extra())
685
686
686 def continueclean(self):
687 def continueclean(self):
687 repo = self.repo
688 repo = self.repo
688 ctx = repo['.']
689 ctx = repo['.']
689 rulectx = repo[self.node]
690 rulectx = repo[self.node]
690 parentctxnode = self.state.parentctxnode
691 parentctxnode = self.state.parentctxnode
691 if ctx.node() == parentctxnode:
692 if ctx.node() == parentctxnode:
692 repo.ui.warn(_('%s: empty changeset\n') %
693 repo.ui.warn(_('%s: empty changeset\n') %
693 node.short(self.node))
694 node.short(self.node))
694 return ctx, [(self.node, (parentctxnode,))]
695 return ctx, [(self.node, (parentctxnode,))]
695
696
696 parentctx = repo[parentctxnode]
697 parentctx = repo[parentctxnode]
697 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
698 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
698 parentctx))
699 parentctx))
699 if not newcommits:
700 if not newcommits:
700 repo.ui.warn(_('%s: cannot fold - working copy is not a '
701 repo.ui.warn(_('%s: cannot fold - working copy is not a '
701 'descendant of previous commit %s\n') %
702 'descendant of previous commit %s\n') %
702 (node.short(self.node), node.short(parentctxnode)))
703 (node.short(self.node), node.short(parentctxnode)))
703 return ctx, [(self.node, (ctx.node(),))]
704 return ctx, [(self.node, (ctx.node(),))]
704
705
705 middlecommits = newcommits.copy()
706 middlecommits = newcommits.copy()
706 middlecommits.discard(ctx.node())
707 middlecommits.discard(ctx.node())
707
708
708 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
709 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
709 middlecommits)
710 middlecommits)
710
711
711 def skipprompt(self):
712 def skipprompt(self):
712 """Returns true if the rule should skip the message editor.
713 """Returns true if the rule should skip the message editor.
713
714
714 For example, 'fold' wants to show an editor, but 'rollup'
715 For example, 'fold' wants to show an editor, but 'rollup'
715 doesn't want to.
716 doesn't want to.
716 """
717 """
717 return False
718 return False
718
719
719 def mergedescs(self):
720 def mergedescs(self):
720 """Returns true if the rule should merge messages of multiple changes.
721 """Returns true if the rule should merge messages of multiple changes.
721
722
722 This exists mainly so that 'rollup' rules can be a subclass of
723 This exists mainly so that 'rollup' rules can be a subclass of
723 'fold'.
724 'fold'.
724 """
725 """
725 return True
726 return True
726
727
727 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
728 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
728 parent = ctx.parents()[0].node()
729 parent = ctx.parents()[0].node()
729 repo.ui.pushbuffer()
730 repo.ui.pushbuffer()
730 hg.update(repo, parent)
731 hg.update(repo, parent)
731 repo.ui.popbuffer()
732 repo.ui.popbuffer()
732 ### prepare new commit data
733 ### prepare new commit data
733 commitopts = {}
734 commitopts = {}
734 commitopts['user'] = ctx.user()
735 commitopts['user'] = ctx.user()
735 # commit message
736 # commit message
736 if not self.mergedescs():
737 if not self.mergedescs():
737 newmessage = ctx.description()
738 newmessage = ctx.description()
738 else:
739 else:
739 newmessage = '\n***\n'.join(
740 newmessage = '\n***\n'.join(
740 [ctx.description()] +
741 [ctx.description()] +
741 [repo[r].description() for r in internalchanges] +
742 [repo[r].description() for r in internalchanges] +
742 [oldctx.description()]) + '\n'
743 [oldctx.description()]) + '\n'
743 commitopts['message'] = newmessage
744 commitopts['message'] = newmessage
744 # date
745 # date
745 commitopts['date'] = max(ctx.date(), oldctx.date())
746 commitopts['date'] = max(ctx.date(), oldctx.date())
746 extra = ctx.extra().copy()
747 extra = ctx.extra().copy()
747 # histedit_source
748 # histedit_source
748 # note: ctx is likely a temporary commit but that the best we can do
749 # note: ctx is likely a temporary commit but that the best we can do
749 # here. This is sufficient to solve issue3681 anyway.
750 # here. This is sufficient to solve issue3681 anyway.
750 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
751 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
751 commitopts['extra'] = extra
752 commitopts['extra'] = extra
752 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
753 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
753 try:
754 try:
754 phasemin = max(ctx.phase(), oldctx.phase())
755 phasemin = max(ctx.phase(), oldctx.phase())
755 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
756 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
756 n = collapse(repo, ctx, repo[newnode], commitopts,
757 n = collapse(repo, ctx, repo[newnode], commitopts,
757 skipprompt=self.skipprompt())
758 skipprompt=self.skipprompt())
758 finally:
759 finally:
759 repo.ui.restoreconfig(phasebackup)
760 repo.ui.restoreconfig(phasebackup)
760 if n is None:
761 if n is None:
761 return ctx, []
762 return ctx, []
762 repo.ui.pushbuffer()
763 repo.ui.pushbuffer()
763 hg.update(repo, n)
764 hg.update(repo, n)
764 repo.ui.popbuffer()
765 repo.ui.popbuffer()
765 replacements = [(oldctx.node(), (newnode,)),
766 replacements = [(oldctx.node(), (newnode,)),
766 (ctx.node(), (n,)),
767 (ctx.node(), (n,)),
767 (newnode, (n,)),
768 (newnode, (n,)),
768 ]
769 ]
769 for ich in internalchanges:
770 for ich in internalchanges:
770 replacements.append((ich, (n,)))
771 replacements.append((ich, (n,)))
771 return repo[n], replacements
772 return repo[n], replacements
772
773
773 class base(histeditaction):
774 class base(histeditaction):
774
775
775 def run(self):
776 def run(self):
776 if self.repo['.'].node() != self.node:
777 if self.repo['.'].node() != self.node:
777 mergemod.update(self.repo, self.node, False, True)
778 mergemod.update(self.repo, self.node, False, True)
778 # branchmerge, force)
779 # branchmerge, force)
779 return self.continueclean()
780 return self.continueclean()
780
781
781 def continuedirty(self):
782 def continuedirty(self):
782 abortdirty()
783 abortdirty()
783
784
784 def continueclean(self):
785 def continueclean(self):
785 basectx = self.repo['.']
786 basectx = self.repo['.']
786 return basectx, []
787 return basectx, []
787
788
788 def _verifynodeconstraints(self, prev, expected, seen):
789 def _verifynodeconstraints(self, prev, expected, seen):
789 # base can only be use with a node not in the edited set
790 # base can only be use with a node not in the edited set
790 if self.node in expected:
791 if self.node in expected:
791 msg = _('%s "%s" changeset was an edited list candidate')
792 msg = _('%s "%s" changeset was an edited list candidate')
792 raise error.ParseError(
793 raise error.ParseError(
793 msg % (self.verb, node.short(self.node)),
794 msg % (self.verb, node.short(self.node)),
794 hint=_('base must only use unlisted changesets'))
795 hint=_('base must only use unlisted changesets'))
795
796
796 @action(['_multifold'],
797 @action(['_multifold'],
797 _(
798 _(
798 """fold subclass used for when multiple folds happen in a row
799 """fold subclass used for when multiple folds happen in a row
799
800
800 We only want to fire the editor for the folded message once when
801 We only want to fire the editor for the folded message once when
801 (say) four changes are folded down into a single change. This is
802 (say) four changes are folded down into a single change. This is
802 similar to rollup, but we should preserve both messages so that
803 similar to rollup, but we should preserve both messages so that
803 when the last fold operation runs we can show the user all the
804 when the last fold operation runs we can show the user all the
804 commit messages in their editor.
805 commit messages in their editor.
805 """),
806 """),
806 internal=True)
807 internal=True)
807 class _multifold(fold):
808 class _multifold(fold):
808 def skipprompt(self):
809 def skipprompt(self):
809 return True
810 return True
810
811
811 @action(["roll", "r"],
812 @action(["roll", "r"],
812 _("like fold, but discard this commit's description"))
813 _("like fold, but discard this commit's description"))
813 class rollup(fold):
814 class rollup(fold):
814 def mergedescs(self):
815 def mergedescs(self):
815 return False
816 return False
816
817
817 def skipprompt(self):
818 def skipprompt(self):
818 return True
819 return True
819
820
820 @action(["drop", "d"],
821 @action(["drop", "d"],
821 _('remove commit from history'))
822 _('remove commit from history'))
822 class drop(histeditaction):
823 class drop(histeditaction):
823 def run(self):
824 def run(self):
824 parentctx = self.repo[self.state.parentctxnode]
825 parentctx = self.repo[self.state.parentctxnode]
825 return parentctx, [(self.node, tuple())]
826 return parentctx, [(self.node, tuple())]
826
827
827 @action(["mess", "m"],
828 @action(["mess", "m"],
828 _('edit commit message without changing commit content'),
829 _('edit commit message without changing commit content'),
829 priority=True)
830 priority=True)
830 class message(histeditaction):
831 class message(histeditaction):
831 def commiteditor(self):
832 def commiteditor(self):
832 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
833 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
833
834
834 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
835 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
835 """utility function to find the first outgoing changeset
836 """utility function to find the first outgoing changeset
836
837
837 Used by initialization code"""
838 Used by initialization code"""
838 if opts is None:
839 if opts is None:
839 opts = {}
840 opts = {}
840 dest = ui.expandpath(remote or 'default-push', remote or 'default')
841 dest = ui.expandpath(remote or 'default-push', remote or 'default')
841 dest, revs = hg.parseurl(dest, None)[:2]
842 dest, revs = hg.parseurl(dest, None)[:2]
842 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
843 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
843
844
844 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
845 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
845 other = hg.peer(repo, opts, dest)
846 other = hg.peer(repo, opts, dest)
846
847
847 if revs:
848 if revs:
848 revs = [repo.lookup(rev) for rev in revs]
849 revs = [repo.lookup(rev) for rev in revs]
849
850
850 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
851 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
851 if not outgoing.missing:
852 if not outgoing.missing:
852 raise error.Abort(_('no outgoing ancestors'))
853 raise error.Abort(_('no outgoing ancestors'))
853 roots = list(repo.revs("roots(%ln)", outgoing.missing))
854 roots = list(repo.revs("roots(%ln)", outgoing.missing))
854 if 1 < len(roots):
855 if 1 < len(roots):
855 msg = _('there are ambiguous outgoing revisions')
856 msg = _('there are ambiguous outgoing revisions')
856 hint = _("see 'hg help histedit' for more detail")
857 hint = _("see 'hg help histedit' for more detail")
857 raise error.Abort(msg, hint=hint)
858 raise error.Abort(msg, hint=hint)
858 return repo.lookup(roots[0])
859 return repo.lookup(roots[0])
859
860
860
861
861 @command('histedit',
862 @command('histedit',
862 [('', 'commands', '',
863 [('', 'commands', '',
863 _('read history edits from the specified file'), _('FILE')),
864 _('read history edits from the specified file'), _('FILE')),
864 ('c', 'continue', False, _('continue an edit already in progress')),
865 ('c', 'continue', False, _('continue an edit already in progress')),
865 ('', 'edit-plan', False, _('edit remaining actions list')),
866 ('', 'edit-plan', False, _('edit remaining actions list')),
866 ('k', 'keep', False,
867 ('k', 'keep', False,
867 _("don't strip old nodes after edit is complete")),
868 _("don't strip old nodes after edit is complete")),
868 ('', 'abort', False, _('abort an edit in progress')),
869 ('', 'abort', False, _('abort an edit in progress')),
869 ('o', 'outgoing', False, _('changesets not found in destination')),
870 ('o', 'outgoing', False, _('changesets not found in destination')),
870 ('f', 'force', False,
871 ('f', 'force', False,
871 _('force outgoing even for unrelated repositories')),
872 _('force outgoing even for unrelated repositories')),
872 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
873 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
873 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
874 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
874 def histedit(ui, repo, *freeargs, **opts):
875 def histedit(ui, repo, *freeargs, **opts):
875 """interactively edit changeset history
876 """interactively edit changeset history
876
877
877 This command lets you edit a linear series of changesets (up to
878 This command lets you edit a linear series of changesets (up to
878 and including the working directory, which should be clean).
879 and including the working directory, which should be clean).
879 You can:
880 You can:
880
881
881 - `pick` to [re]order a changeset
882 - `pick` to [re]order a changeset
882
883
883 - `drop` to omit changeset
884 - `drop` to omit changeset
884
885
885 - `mess` to reword the changeset commit message
886 - `mess` to reword the changeset commit message
886
887
887 - `fold` to combine it with the preceding changeset
888 - `fold` to combine it with the preceding changeset (using the later date)
888
889
889 - `roll` like fold, but discarding this commit's description
890 - `roll` like fold, but discarding this commit's description
890
891
891 - `edit` to edit this changeset
892 - `edit` to edit this changeset (preserving date)
892
893
893 There are a number of ways to select the root changeset:
894 There are a number of ways to select the root changeset:
894
895
895 - Specify ANCESTOR directly
896 - Specify ANCESTOR directly
896
897
897 - Use --outgoing -- it will be the first linear changeset not
898 - Use --outgoing -- it will be the first linear changeset not
898 included in destination. (See :hg:`help config.paths.default-push`)
899 included in destination. (See :hg:`help config.paths.default-push`)
899
900
900 - Otherwise, the value from the "histedit.defaultrev" config option
901 - Otherwise, the value from the "histedit.defaultrev" config option
901 is used as a revset to select the base revision when ANCESTOR is not
902 is used as a revset to select the base revision when ANCESTOR is not
902 specified. The first revision returned by the revset is used. By
903 specified. The first revision returned by the revset is used. By
903 default, this selects the editable history that is unique to the
904 default, this selects the editable history that is unique to the
904 ancestry of the working directory.
905 ancestry of the working directory.
905
906
906 .. container:: verbose
907 .. container:: verbose
907
908
908 If you use --outgoing, this command will abort if there are ambiguous
909 If you use --outgoing, this command will abort if there are ambiguous
909 outgoing revisions. For example, if there are multiple branches
910 outgoing revisions. For example, if there are multiple branches
910 containing outgoing revisions.
911 containing outgoing revisions.
911
912
912 Use "min(outgoing() and ::.)" or similar revset specification
913 Use "min(outgoing() and ::.)" or similar revset specification
913 instead of --outgoing to specify edit target revision exactly in
914 instead of --outgoing to specify edit target revision exactly in
914 such ambiguous situation. See :hg:`help revsets` for detail about
915 such ambiguous situation. See :hg:`help revsets` for detail about
915 selecting revisions.
916 selecting revisions.
916
917
917 .. container:: verbose
918 .. container:: verbose
918
919
919 Examples:
920 Examples:
920
921
921 - A number of changes have been made.
922 - A number of changes have been made.
922 Revision 3 is no longer needed.
923 Revision 3 is no longer needed.
923
924
924 Start history editing from revision 3::
925 Start history editing from revision 3::
925
926
926 hg histedit -r 3
927 hg histedit -r 3
927
928
928 An editor opens, containing the list of revisions,
929 An editor opens, containing the list of revisions,
929 with specific actions specified::
930 with specific actions specified::
930
931
931 pick 5339bf82f0ca 3 Zworgle the foobar
932 pick 5339bf82f0ca 3 Zworgle the foobar
932 pick 8ef592ce7cc4 4 Bedazzle the zerlog
933 pick 8ef592ce7cc4 4 Bedazzle the zerlog
933 pick 0a9639fcda9d 5 Morgify the cromulancy
934 pick 0a9639fcda9d 5 Morgify the cromulancy
934
935
935 Additional information about the possible actions
936 Additional information about the possible actions
936 to take appears below the list of revisions.
937 to take appears below the list of revisions.
937
938
938 To remove revision 3 from the history,
939 To remove revision 3 from the history,
939 its action (at the beginning of the relevant line)
940 its action (at the beginning of the relevant line)
940 is changed to 'drop'::
941 is changed to 'drop'::
941
942
942 drop 5339bf82f0ca 3 Zworgle the foobar
943 drop 5339bf82f0ca 3 Zworgle the foobar
943 pick 8ef592ce7cc4 4 Bedazzle the zerlog
944 pick 8ef592ce7cc4 4 Bedazzle the zerlog
944 pick 0a9639fcda9d 5 Morgify the cromulancy
945 pick 0a9639fcda9d 5 Morgify the cromulancy
945
946
946 - A number of changes have been made.
947 - A number of changes have been made.
947 Revision 2 and 4 need to be swapped.
948 Revision 2 and 4 need to be swapped.
948
949
949 Start history editing from revision 2::
950 Start history editing from revision 2::
950
951
951 hg histedit -r 2
952 hg histedit -r 2
952
953
953 An editor opens, containing the list of revisions,
954 An editor opens, containing the list of revisions,
954 with specific actions specified::
955 with specific actions specified::
955
956
956 pick 252a1af424ad 2 Blorb a morgwazzle
957 pick 252a1af424ad 2 Blorb a morgwazzle
957 pick 5339bf82f0ca 3 Zworgle the foobar
958 pick 5339bf82f0ca 3 Zworgle the foobar
958 pick 8ef592ce7cc4 4 Bedazzle the zerlog
959 pick 8ef592ce7cc4 4 Bedazzle the zerlog
959
960
960 To swap revision 2 and 4, its lines are swapped
961 To swap revision 2 and 4, its lines are swapped
961 in the editor::
962 in the editor::
962
963
963 pick 8ef592ce7cc4 4 Bedazzle the zerlog
964 pick 8ef592ce7cc4 4 Bedazzle the zerlog
964 pick 5339bf82f0ca 3 Zworgle the foobar
965 pick 5339bf82f0ca 3 Zworgle the foobar
965 pick 252a1af424ad 2 Blorb a morgwazzle
966 pick 252a1af424ad 2 Blorb a morgwazzle
966
967
967 Returns 0 on success, 1 if user intervention is required (not only
968 Returns 0 on success, 1 if user intervention is required (not only
968 for intentional "edit" command, but also for resolving unexpected
969 for intentional "edit" command, but also for resolving unexpected
969 conflicts).
970 conflicts).
970 """
971 """
971 state = histeditstate(repo)
972 state = histeditstate(repo)
972 try:
973 try:
973 state.wlock = repo.wlock()
974 state.wlock = repo.wlock()
974 state.lock = repo.lock()
975 state.lock = repo.lock()
975 _histedit(ui, repo, state, *freeargs, **opts)
976 _histedit(ui, repo, state, *freeargs, **opts)
976 finally:
977 finally:
977 release(state.lock, state.wlock)
978 release(state.lock, state.wlock)
978
979
979 goalcontinue = 'continue'
980 goalcontinue = 'continue'
980 goalabort = 'abort'
981 goalabort = 'abort'
981 goaleditplan = 'edit-plan'
982 goaleditplan = 'edit-plan'
982 goalnew = 'new'
983 goalnew = 'new'
983
984
984 def _getgoal(opts):
985 def _getgoal(opts):
985 if opts.get('continue'):
986 if opts.get('continue'):
986 return goalcontinue
987 return goalcontinue
987 if opts.get('abort'):
988 if opts.get('abort'):
988 return goalabort
989 return goalabort
989 if opts.get('edit_plan'):
990 if opts.get('edit_plan'):
990 return goaleditplan
991 return goaleditplan
991 return goalnew
992 return goalnew
992
993
993 def _readfile(ui, path):
994 def _readfile(ui, path):
994 if path == '-':
995 if path == '-':
995 with ui.timeblockedsection('histedit'):
996 with ui.timeblockedsection('histedit'):
996 return ui.fin.read()
997 return ui.fin.read()
997 else:
998 else:
998 with open(path, 'rb') as f:
999 with open(path, 'rb') as f:
999 return f.read()
1000 return f.read()
1000
1001
1001 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1002 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1002 # TODO only abort if we try to histedit mq patches, not just
1003 # TODO only abort if we try to histedit mq patches, not just
1003 # blanket if mq patches are applied somewhere
1004 # blanket if mq patches are applied somewhere
1004 mq = getattr(repo, 'mq', None)
1005 mq = getattr(repo, 'mq', None)
1005 if mq and mq.applied:
1006 if mq and mq.applied:
1006 raise error.Abort(_('source has mq patches applied'))
1007 raise error.Abort(_('source has mq patches applied'))
1007
1008
1008 # basic argument incompatibility processing
1009 # basic argument incompatibility processing
1009 outg = opts.get('outgoing')
1010 outg = opts.get('outgoing')
1010 editplan = opts.get('edit_plan')
1011 editplan = opts.get('edit_plan')
1011 abort = opts.get('abort')
1012 abort = opts.get('abort')
1012 force = opts.get('force')
1013 force = opts.get('force')
1013 if force and not outg:
1014 if force and not outg:
1014 raise error.Abort(_('--force only allowed with --outgoing'))
1015 raise error.Abort(_('--force only allowed with --outgoing'))
1015 if goal == 'continue':
1016 if goal == 'continue':
1016 if any((outg, abort, revs, freeargs, rules, editplan)):
1017 if any((outg, abort, revs, freeargs, rules, editplan)):
1017 raise error.Abort(_('no arguments allowed with --continue'))
1018 raise error.Abort(_('no arguments allowed with --continue'))
1018 elif goal == 'abort':
1019 elif goal == 'abort':
1019 if any((outg, revs, freeargs, rules, editplan)):
1020 if any((outg, revs, freeargs, rules, editplan)):
1020 raise error.Abort(_('no arguments allowed with --abort'))
1021 raise error.Abort(_('no arguments allowed with --abort'))
1021 elif goal == 'edit-plan':
1022 elif goal == 'edit-plan':
1022 if any((outg, revs, freeargs)):
1023 if any((outg, revs, freeargs)):
1023 raise error.Abort(_('only --commands argument allowed with '
1024 raise error.Abort(_('only --commands argument allowed with '
1024 '--edit-plan'))
1025 '--edit-plan'))
1025 else:
1026 else:
1026 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1027 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1027 raise error.Abort(_('history edit already in progress, try '
1028 raise error.Abort(_('history edit already in progress, try '
1028 '--continue or --abort'))
1029 '--continue or --abort'))
1029 if outg:
1030 if outg:
1030 if revs:
1031 if revs:
1031 raise error.Abort(_('no revisions allowed with --outgoing'))
1032 raise error.Abort(_('no revisions allowed with --outgoing'))
1032 if len(freeargs) > 1:
1033 if len(freeargs) > 1:
1033 raise error.Abort(
1034 raise error.Abort(
1034 _('only one repo argument allowed with --outgoing'))
1035 _('only one repo argument allowed with --outgoing'))
1035 else:
1036 else:
1036 revs.extend(freeargs)
1037 revs.extend(freeargs)
1037 if len(revs) == 0:
1038 if len(revs) == 0:
1038 defaultrev = destutil.desthistedit(ui, repo)
1039 defaultrev = destutil.desthistedit(ui, repo)
1039 if defaultrev is not None:
1040 if defaultrev is not None:
1040 revs.append(defaultrev)
1041 revs.append(defaultrev)
1041
1042
1042 if len(revs) != 1:
1043 if len(revs) != 1:
1043 raise error.Abort(
1044 raise error.Abort(
1044 _('histedit requires exactly one ancestor revision'))
1045 _('histedit requires exactly one ancestor revision'))
1045
1046
1046 def _histedit(ui, repo, state, *freeargs, **opts):
1047 def _histedit(ui, repo, state, *freeargs, **opts):
1047 goal = _getgoal(opts)
1048 goal = _getgoal(opts)
1048 revs = opts.get('rev', [])
1049 revs = opts.get('rev', [])
1049 rules = opts.get('commands', '')
1050 rules = opts.get('commands', '')
1050 state.keep = opts.get('keep', False)
1051 state.keep = opts.get('keep', False)
1051
1052
1052 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1053 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1053
1054
1054 # rebuild state
1055 # rebuild state
1055 if goal == goalcontinue:
1056 if goal == goalcontinue:
1056 state.read()
1057 state.read()
1057 state = bootstrapcontinue(ui, state, opts)
1058 state = bootstrapcontinue(ui, state, opts)
1058 elif goal == goaleditplan:
1059 elif goal == goaleditplan:
1059 _edithisteditplan(ui, repo, state, rules)
1060 _edithisteditplan(ui, repo, state, rules)
1060 return
1061 return
1061 elif goal == goalabort:
1062 elif goal == goalabort:
1062 _aborthistedit(ui, repo, state)
1063 _aborthistedit(ui, repo, state)
1063 return
1064 return
1064 else:
1065 else:
1065 # goal == goalnew
1066 # goal == goalnew
1066 _newhistedit(ui, repo, state, revs, freeargs, opts)
1067 _newhistedit(ui, repo, state, revs, freeargs, opts)
1067
1068
1068 _continuehistedit(ui, repo, state)
1069 _continuehistedit(ui, repo, state)
1069 _finishhistedit(ui, repo, state)
1070 _finishhistedit(ui, repo, state)
1070
1071
1071 def _continuehistedit(ui, repo, state):
1072 def _continuehistedit(ui, repo, state):
1072 """This function runs after either:
1073 """This function runs after either:
1073 - bootstrapcontinue (if the goal is 'continue')
1074 - bootstrapcontinue (if the goal is 'continue')
1074 - _newhistedit (if the goal is 'new')
1075 - _newhistedit (if the goal is 'new')
1075 """
1076 """
1076 # preprocess rules so that we can hide inner folds from the user
1077 # preprocess rules so that we can hide inner folds from the user
1077 # and only show one editor
1078 # and only show one editor
1078 actions = state.actions[:]
1079 actions = state.actions[:]
1079 for idx, (action, nextact) in enumerate(
1080 for idx, (action, nextact) in enumerate(
1080 zip(actions, actions[1:] + [None])):
1081 zip(actions, actions[1:] + [None])):
1081 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1082 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1082 state.actions[idx].__class__ = _multifold
1083 state.actions[idx].__class__ = _multifold
1083
1084
1084 total = len(state.actions)
1085 total = len(state.actions)
1085 pos = 0
1086 pos = 0
1086 while state.actions:
1087 while state.actions:
1087 state.write()
1088 state.write()
1088 actobj = state.actions.pop(0)
1089 actobj = state.actions.pop(0)
1089 pos += 1
1090 pos += 1
1090 ui.progress(_("editing"), pos, actobj.torule(),
1091 ui.progress(_("editing"), pos, actobj.torule(),
1091 _('changes'), total)
1092 _('changes'), total)
1092 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1093 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1093 actobj.torule()))
1094 actobj.torule()))
1094 parentctx, replacement_ = actobj.run()
1095 parentctx, replacement_ = actobj.run()
1095 state.parentctxnode = parentctx.node()
1096 state.parentctxnode = parentctx.node()
1096 state.replacements.extend(replacement_)
1097 state.replacements.extend(replacement_)
1097 state.write()
1098 state.write()
1098 ui.progress(_("editing"), None)
1099 ui.progress(_("editing"), None)
1099
1100
1100 def _finishhistedit(ui, repo, state):
1101 def _finishhistedit(ui, repo, state):
1101 """This action runs when histedit is finishing its session"""
1102 """This action runs when histedit is finishing its session"""
1102 repo.ui.pushbuffer()
1103 repo.ui.pushbuffer()
1103 hg.update(repo, state.parentctxnode, quietempty=True)
1104 hg.update(repo, state.parentctxnode, quietempty=True)
1104 repo.ui.popbuffer()
1105 repo.ui.popbuffer()
1105
1106
1106 mapping, tmpnodes, created, ntm = processreplacement(state)
1107 mapping, tmpnodes, created, ntm = processreplacement(state)
1107 if mapping:
1108 if mapping:
1108 for prec, succs in mapping.iteritems():
1109 for prec, succs in mapping.iteritems():
1109 if not succs:
1110 if not succs:
1110 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1111 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1111 else:
1112 else:
1112 ui.debug('histedit: %s is replaced by %s\n' % (
1113 ui.debug('histedit: %s is replaced by %s\n' % (
1113 node.short(prec), node.short(succs[0])))
1114 node.short(prec), node.short(succs[0])))
1114 if len(succs) > 1:
1115 if len(succs) > 1:
1115 m = 'histedit: %s'
1116 m = 'histedit: %s'
1116 for n in succs[1:]:
1117 for n in succs[1:]:
1117 ui.debug(m % node.short(n))
1118 ui.debug(m % node.short(n))
1118
1119
1119 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1120 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1120 if supportsmarkers:
1121 if supportsmarkers:
1121 # Only create markers if the temp nodes weren't already removed.
1122 # Only create markers if the temp nodes weren't already removed.
1122 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1123 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1123 if t in repo))
1124 if t in repo))
1124 else:
1125 else:
1125 cleanupnode(ui, repo, 'temp', tmpnodes)
1126 cleanupnode(ui, repo, 'temp', tmpnodes)
1126
1127
1127 if not state.keep:
1128 if not state.keep:
1128 if mapping:
1129 if mapping:
1129 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1130 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1130 # TODO update mq state
1131 # TODO update mq state
1131 if supportsmarkers:
1132 if supportsmarkers:
1132 markers = []
1133 markers = []
1133 # sort by revision number because it sound "right"
1134 # sort by revision number because it sound "right"
1134 for prec in sorted(mapping, key=repo.changelog.rev):
1135 for prec in sorted(mapping, key=repo.changelog.rev):
1135 succs = mapping[prec]
1136 succs = mapping[prec]
1136 markers.append((repo[prec],
1137 markers.append((repo[prec],
1137 tuple(repo[s] for s in succs)))
1138 tuple(repo[s] for s in succs)))
1138 if markers:
1139 if markers:
1139 obsolete.createmarkers(repo, markers)
1140 obsolete.createmarkers(repo, markers)
1140 else:
1141 else:
1141 cleanupnode(ui, repo, 'replaced', mapping)
1142 cleanupnode(ui, repo, 'replaced', mapping)
1142
1143
1143 state.clear()
1144 state.clear()
1144 if os.path.exists(repo.sjoin('undo')):
1145 if os.path.exists(repo.sjoin('undo')):
1145 os.unlink(repo.sjoin('undo'))
1146 os.unlink(repo.sjoin('undo'))
1146 if repo.vfs.exists('histedit-last-edit.txt'):
1147 if repo.vfs.exists('histedit-last-edit.txt'):
1147 repo.vfs.unlink('histedit-last-edit.txt')
1148 repo.vfs.unlink('histedit-last-edit.txt')
1148
1149
1149 def _aborthistedit(ui, repo, state):
1150 def _aborthistedit(ui, repo, state):
1150 try:
1151 try:
1151 state.read()
1152 state.read()
1152 __, leafs, tmpnodes, __ = processreplacement(state)
1153 __, leafs, tmpnodes, __ = processreplacement(state)
1153 ui.debug('restore wc to old parent %s\n'
1154 ui.debug('restore wc to old parent %s\n'
1154 % node.short(state.topmost))
1155 % node.short(state.topmost))
1155
1156
1156 # Recover our old commits if necessary
1157 # Recover our old commits if necessary
1157 if not state.topmost in repo and state.backupfile:
1158 if not state.topmost in repo and state.backupfile:
1158 backupfile = repo.join(state.backupfile)
1159 backupfile = repo.join(state.backupfile)
1159 f = hg.openpath(ui, backupfile)
1160 f = hg.openpath(ui, backupfile)
1160 gen = exchange.readbundle(ui, f, backupfile)
1161 gen = exchange.readbundle(ui, f, backupfile)
1161 with repo.transaction('histedit.abort') as tr:
1162 with repo.transaction('histedit.abort') as tr:
1162 if not isinstance(gen, bundle2.unbundle20):
1163 if not isinstance(gen, bundle2.unbundle20):
1163 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1164 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1164 if isinstance(gen, bundle2.unbundle20):
1165 if isinstance(gen, bundle2.unbundle20):
1165 bundle2.applybundle(repo, gen, tr,
1166 bundle2.applybundle(repo, gen, tr,
1166 source='histedit',
1167 source='histedit',
1167 url='bundle:' + backupfile)
1168 url='bundle:' + backupfile)
1168
1169
1169 os.remove(backupfile)
1170 os.remove(backupfile)
1170
1171
1171 # check whether we should update away
1172 # check whether we should update away
1172 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1173 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1173 state.parentctxnode, leafs | tmpnodes):
1174 state.parentctxnode, leafs | tmpnodes):
1174 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1175 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1175 cleanupnode(ui, repo, 'created', tmpnodes)
1176 cleanupnode(ui, repo, 'created', tmpnodes)
1176 cleanupnode(ui, repo, 'temp', leafs)
1177 cleanupnode(ui, repo, 'temp', leafs)
1177 except Exception:
1178 except Exception:
1178 if state.inprogress():
1179 if state.inprogress():
1179 ui.warn(_('warning: encountered an exception during histedit '
1180 ui.warn(_('warning: encountered an exception during histedit '
1180 '--abort; the repository may not have been completely '
1181 '--abort; the repository may not have been completely '
1181 'cleaned up\n'))
1182 'cleaned up\n'))
1182 raise
1183 raise
1183 finally:
1184 finally:
1184 state.clear()
1185 state.clear()
1185
1186
1186 def _edithisteditplan(ui, repo, state, rules):
1187 def _edithisteditplan(ui, repo, state, rules):
1187 state.read()
1188 state.read()
1188 if not rules:
1189 if not rules:
1189 comment = geteditcomment(ui,
1190 comment = geteditcomment(ui,
1190 node.short(state.parentctxnode),
1191 node.short(state.parentctxnode),
1191 node.short(state.topmost))
1192 node.short(state.topmost))
1192 rules = ruleeditor(repo, ui, state.actions, comment)
1193 rules = ruleeditor(repo, ui, state.actions, comment)
1193 else:
1194 else:
1194 rules = _readfile(ui, rules)
1195 rules = _readfile(ui, rules)
1195 actions = parserules(rules, state)
1196 actions = parserules(rules, state)
1196 ctxs = [repo[act.node] \
1197 ctxs = [repo[act.node] \
1197 for act in state.actions if act.node]
1198 for act in state.actions if act.node]
1198 warnverifyactions(ui, repo, actions, state, ctxs)
1199 warnverifyactions(ui, repo, actions, state, ctxs)
1199 state.actions = actions
1200 state.actions = actions
1200 state.write()
1201 state.write()
1201
1202
1202 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1203 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1203 outg = opts.get('outgoing')
1204 outg = opts.get('outgoing')
1204 rules = opts.get('commands', '')
1205 rules = opts.get('commands', '')
1205 force = opts.get('force')
1206 force = opts.get('force')
1206
1207
1207 cmdutil.checkunfinished(repo)
1208 cmdutil.checkunfinished(repo)
1208 cmdutil.bailifchanged(repo)
1209 cmdutil.bailifchanged(repo)
1209
1210
1210 topmost, empty = repo.dirstate.parents()
1211 topmost, empty = repo.dirstate.parents()
1211 if outg:
1212 if outg:
1212 if freeargs:
1213 if freeargs:
1213 remote = freeargs[0]
1214 remote = freeargs[0]
1214 else:
1215 else:
1215 remote = None
1216 remote = None
1216 root = findoutgoing(ui, repo, remote, force, opts)
1217 root = findoutgoing(ui, repo, remote, force, opts)
1217 else:
1218 else:
1218 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1219 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1219 if len(rr) != 1:
1220 if len(rr) != 1:
1220 raise error.Abort(_('The specified revisions must have '
1221 raise error.Abort(_('The specified revisions must have '
1221 'exactly one common root'))
1222 'exactly one common root'))
1222 root = rr[0].node()
1223 root = rr[0].node()
1223
1224
1224 revs = between(repo, root, topmost, state.keep)
1225 revs = between(repo, root, topmost, state.keep)
1225 if not revs:
1226 if not revs:
1226 raise error.Abort(_('%s is not an ancestor of working directory') %
1227 raise error.Abort(_('%s is not an ancestor of working directory') %
1227 node.short(root))
1228 node.short(root))
1228
1229
1229 ctxs = [repo[r] for r in revs]
1230 ctxs = [repo[r] for r in revs]
1230 if not rules:
1231 if not rules:
1231 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1232 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1232 actions = [pick(state, r) for r in revs]
1233 actions = [pick(state, r) for r in revs]
1233 rules = ruleeditor(repo, ui, actions, comment)
1234 rules = ruleeditor(repo, ui, actions, comment)
1234 else:
1235 else:
1235 rules = _readfile(ui, rules)
1236 rules = _readfile(ui, rules)
1236 actions = parserules(rules, state)
1237 actions = parserules(rules, state)
1237 warnverifyactions(ui, repo, actions, state, ctxs)
1238 warnverifyactions(ui, repo, actions, state, ctxs)
1238
1239
1239 parentctxnode = repo[root].parents()[0].node()
1240 parentctxnode = repo[root].parents()[0].node()
1240
1241
1241 state.parentctxnode = parentctxnode
1242 state.parentctxnode = parentctxnode
1242 state.actions = actions
1243 state.actions = actions
1243 state.topmost = topmost
1244 state.topmost = topmost
1244 state.replacements = []
1245 state.replacements = []
1245
1246
1246 # Create a backup so we can always abort completely.
1247 # Create a backup so we can always abort completely.
1247 backupfile = None
1248 backupfile = None
1248 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1249 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1249 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1250 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1250 'histedit')
1251 'histedit')
1251 state.backupfile = backupfile
1252 state.backupfile = backupfile
1252
1253
1253 def _getsummary(ctx):
1254 def _getsummary(ctx):
1254 # a common pattern is to extract the summary but default to the empty
1255 # a common pattern is to extract the summary but default to the empty
1255 # string
1256 # string
1256 summary = ctx.description() or ''
1257 summary = ctx.description() or ''
1257 if summary:
1258 if summary:
1258 summary = summary.splitlines()[0]
1259 summary = summary.splitlines()[0]
1259 return summary
1260 return summary
1260
1261
1261 def bootstrapcontinue(ui, state, opts):
1262 def bootstrapcontinue(ui, state, opts):
1262 repo = state.repo
1263 repo = state.repo
1263 if state.actions:
1264 if state.actions:
1264 actobj = state.actions.pop(0)
1265 actobj = state.actions.pop(0)
1265
1266
1266 if _isdirtywc(repo):
1267 if _isdirtywc(repo):
1267 actobj.continuedirty()
1268 actobj.continuedirty()
1268 if _isdirtywc(repo):
1269 if _isdirtywc(repo):
1269 abortdirty()
1270 abortdirty()
1270
1271
1271 parentctx, replacements = actobj.continueclean()
1272 parentctx, replacements = actobj.continueclean()
1272
1273
1273 state.parentctxnode = parentctx.node()
1274 state.parentctxnode = parentctx.node()
1274 state.replacements.extend(replacements)
1275 state.replacements.extend(replacements)
1275
1276
1276 return state
1277 return state
1277
1278
1278 def between(repo, old, new, keep):
1279 def between(repo, old, new, keep):
1279 """select and validate the set of revision to edit
1280 """select and validate the set of revision to edit
1280
1281
1281 When keep is false, the specified set can't have children."""
1282 When keep is false, the specified set can't have children."""
1282 ctxs = list(repo.set('%n::%n', old, new))
1283 ctxs = list(repo.set('%n::%n', old, new))
1283 if ctxs and not keep:
1284 if ctxs and not keep:
1284 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1285 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1285 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1286 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1286 raise error.Abort(_('can only histedit a changeset together '
1287 raise error.Abort(_('can only histedit a changeset together '
1287 'with all its descendants'))
1288 'with all its descendants'))
1288 if repo.revs('(%ld) and merge()', ctxs):
1289 if repo.revs('(%ld) and merge()', ctxs):
1289 raise error.Abort(_('cannot edit history that contains merges'))
1290 raise error.Abort(_('cannot edit history that contains merges'))
1290 root = ctxs[0] # list is already sorted by repo.set
1291 root = ctxs[0] # list is already sorted by repo.set
1291 if not root.mutable():
1292 if not root.mutable():
1292 raise error.Abort(_('cannot edit public changeset: %s') % root,
1293 raise error.Abort(_('cannot edit public changeset: %s') % root,
1293 hint=_("see 'hg help phases' for details"))
1294 hint=_("see 'hg help phases' for details"))
1294 return [c.node() for c in ctxs]
1295 return [c.node() for c in ctxs]
1295
1296
1296 def ruleeditor(repo, ui, actions, editcomment=""):
1297 def ruleeditor(repo, ui, actions, editcomment=""):
1297 """open an editor to edit rules
1298 """open an editor to edit rules
1298
1299
1299 rules are in the format [ [act, ctx], ...] like in state.rules
1300 rules are in the format [ [act, ctx], ...] like in state.rules
1300 """
1301 """
1301 if repo.ui.configbool("experimental", "histedit.autoverb"):
1302 if repo.ui.configbool("experimental", "histedit.autoverb"):
1302 newact = util.sortdict()
1303 newact = util.sortdict()
1303 for act in actions:
1304 for act in actions:
1304 ctx = repo[act.node]
1305 ctx = repo[act.node]
1305 summary = _getsummary(ctx)
1306 summary = _getsummary(ctx)
1306 fword = summary.split(' ', 1)[0].lower()
1307 fword = summary.split(' ', 1)[0].lower()
1307 added = False
1308 added = False
1308
1309
1309 # if it doesn't end with the special character '!' just skip this
1310 # if it doesn't end with the special character '!' just skip this
1310 if fword.endswith('!'):
1311 if fword.endswith('!'):
1311 fword = fword[:-1]
1312 fword = fword[:-1]
1312 if fword in primaryactions | secondaryactions | tertiaryactions:
1313 if fword in primaryactions | secondaryactions | tertiaryactions:
1313 act.verb = fword
1314 act.verb = fword
1314 # get the target summary
1315 # get the target summary
1315 tsum = summary[len(fword) + 1:].lstrip()
1316 tsum = summary[len(fword) + 1:].lstrip()
1316 # safe but slow: reverse iterate over the actions so we
1317 # safe but slow: reverse iterate over the actions so we
1317 # don't clash on two commits having the same summary
1318 # don't clash on two commits having the same summary
1318 for na, l in reversed(list(newact.iteritems())):
1319 for na, l in reversed(list(newact.iteritems())):
1319 actx = repo[na.node]
1320 actx = repo[na.node]
1320 asum = _getsummary(actx)
1321 asum = _getsummary(actx)
1321 if asum == tsum:
1322 if asum == tsum:
1322 added = True
1323 added = True
1323 l.append(act)
1324 l.append(act)
1324 break
1325 break
1325
1326
1326 if not added:
1327 if not added:
1327 newact[act] = []
1328 newact[act] = []
1328
1329
1329 # copy over and flatten the new list
1330 # copy over and flatten the new list
1330 actions = []
1331 actions = []
1331 for na, l in newact.iteritems():
1332 for na, l in newact.iteritems():
1332 actions.append(na)
1333 actions.append(na)
1333 actions += l
1334 actions += l
1334
1335
1335 rules = '\n'.join([act.torule() for act in actions])
1336 rules = '\n'.join([act.torule() for act in actions])
1336 rules += '\n\n'
1337 rules += '\n\n'
1337 rules += editcomment
1338 rules += editcomment
1338 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1339 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1339 repopath=repo.path)
1340 repopath=repo.path)
1340
1341
1341 # Save edit rules in .hg/histedit-last-edit.txt in case
1342 # Save edit rules in .hg/histedit-last-edit.txt in case
1342 # the user needs to ask for help after something
1343 # the user needs to ask for help after something
1343 # surprising happens.
1344 # surprising happens.
1344 f = open(repo.join('histedit-last-edit.txt'), 'w')
1345 f = open(repo.join('histedit-last-edit.txt'), 'w')
1345 f.write(rules)
1346 f.write(rules)
1346 f.close()
1347 f.close()
1347
1348
1348 return rules
1349 return rules
1349
1350
1350 def parserules(rules, state):
1351 def parserules(rules, state):
1351 """Read the histedit rules string and return list of action objects """
1352 """Read the histedit rules string and return list of action objects """
1352 rules = [l for l in (r.strip() for r in rules.splitlines())
1353 rules = [l for l in (r.strip() for r in rules.splitlines())
1353 if l and not l.startswith('#')]
1354 if l and not l.startswith('#')]
1354 actions = []
1355 actions = []
1355 for r in rules:
1356 for r in rules:
1356 if ' ' not in r:
1357 if ' ' not in r:
1357 raise error.ParseError(_('malformed line "%s"') % r)
1358 raise error.ParseError(_('malformed line "%s"') % r)
1358 verb, rest = r.split(' ', 1)
1359 verb, rest = r.split(' ', 1)
1359
1360
1360 if verb not in actiontable:
1361 if verb not in actiontable:
1361 raise error.ParseError(_('unknown action "%s"') % verb)
1362 raise error.ParseError(_('unknown action "%s"') % verb)
1362
1363
1363 action = actiontable[verb].fromrule(state, rest)
1364 action = actiontable[verb].fromrule(state, rest)
1364 actions.append(action)
1365 actions.append(action)
1365 return actions
1366 return actions
1366
1367
1367 def warnverifyactions(ui, repo, actions, state, ctxs):
1368 def warnverifyactions(ui, repo, actions, state, ctxs):
1368 try:
1369 try:
1369 verifyactions(actions, state, ctxs)
1370 verifyactions(actions, state, ctxs)
1370 except error.ParseError:
1371 except error.ParseError:
1371 if repo.vfs.exists('histedit-last-edit.txt'):
1372 if repo.vfs.exists('histedit-last-edit.txt'):
1372 ui.warn(_('warning: histedit rules saved '
1373 ui.warn(_('warning: histedit rules saved '
1373 'to: .hg/histedit-last-edit.txt\n'))
1374 'to: .hg/histedit-last-edit.txt\n'))
1374 raise
1375 raise
1375
1376
1376 def verifyactions(actions, state, ctxs):
1377 def verifyactions(actions, state, ctxs):
1377 """Verify that there exists exactly one action per given changeset and
1378 """Verify that there exists exactly one action per given changeset and
1378 other constraints.
1379 other constraints.
1379
1380
1380 Will abort if there are to many or too few rules, a malformed rule,
1381 Will abort if there are to many or too few rules, a malformed rule,
1381 or a rule on a changeset outside of the user-given range.
1382 or a rule on a changeset outside of the user-given range.
1382 """
1383 """
1383 expected = set(c.node() for c in ctxs)
1384 expected = set(c.node() for c in ctxs)
1384 seen = set()
1385 seen = set()
1385 prev = None
1386 prev = None
1386 for action in actions:
1387 for action in actions:
1387 action.verify(prev, expected, seen)
1388 action.verify(prev, expected, seen)
1388 prev = action
1389 prev = action
1389 if action.node is not None:
1390 if action.node is not None:
1390 seen.add(action.node)
1391 seen.add(action.node)
1391 missing = sorted(expected - seen) # sort to stabilize output
1392 missing = sorted(expected - seen) # sort to stabilize output
1392
1393
1393 if state.repo.ui.configbool('histedit', 'dropmissing'):
1394 if state.repo.ui.configbool('histedit', 'dropmissing'):
1394 if len(actions) == 0:
1395 if len(actions) == 0:
1395 raise error.ParseError(_('no rules provided'),
1396 raise error.ParseError(_('no rules provided'),
1396 hint=_('use strip extension to remove commits'))
1397 hint=_('use strip extension to remove commits'))
1397
1398
1398 drops = [drop(state, n) for n in missing]
1399 drops = [drop(state, n) for n in missing]
1399 # put the in the beginning so they execute immediately and
1400 # put the in the beginning so they execute immediately and
1400 # don't show in the edit-plan in the future
1401 # don't show in the edit-plan in the future
1401 actions[:0] = drops
1402 actions[:0] = drops
1402 elif missing:
1403 elif missing:
1403 raise error.ParseError(_('missing rules for changeset %s') %
1404 raise error.ParseError(_('missing rules for changeset %s') %
1404 node.short(missing[0]),
1405 node.short(missing[0]),
1405 hint=_('use "drop %s" to discard, see also: '
1406 hint=_('use "drop %s" to discard, see also: '
1406 "'hg help -e histedit.config'")
1407 "'hg help -e histedit.config'")
1407 % node.short(missing[0]))
1408 % node.short(missing[0]))
1408
1409
1409 def adjustreplacementsfrommarkers(repo, oldreplacements):
1410 def adjustreplacementsfrommarkers(repo, oldreplacements):
1410 """Adjust replacements from obsolescence markers
1411 """Adjust replacements from obsolescence markers
1411
1412
1412 Replacements structure is originally generated based on
1413 Replacements structure is originally generated based on
1413 histedit's state and does not account for changes that are
1414 histedit's state and does not account for changes that are
1414 not recorded there. This function fixes that by adding
1415 not recorded there. This function fixes that by adding
1415 data read from obsolescence markers"""
1416 data read from obsolescence markers"""
1416 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1417 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1417 return oldreplacements
1418 return oldreplacements
1418
1419
1419 unfi = repo.unfiltered()
1420 unfi = repo.unfiltered()
1420 nm = unfi.changelog.nodemap
1421 nm = unfi.changelog.nodemap
1421 obsstore = repo.obsstore
1422 obsstore = repo.obsstore
1422 newreplacements = list(oldreplacements)
1423 newreplacements = list(oldreplacements)
1423 oldsuccs = [r[1] for r in oldreplacements]
1424 oldsuccs = [r[1] for r in oldreplacements]
1424 # successors that have already been added to succstocheck once
1425 # successors that have already been added to succstocheck once
1425 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1426 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1426 succstocheck = list(seensuccs)
1427 succstocheck = list(seensuccs)
1427 while succstocheck:
1428 while succstocheck:
1428 n = succstocheck.pop()
1429 n = succstocheck.pop()
1429 missing = nm.get(n) is None
1430 missing = nm.get(n) is None
1430 markers = obsstore.successors.get(n, ())
1431 markers = obsstore.successors.get(n, ())
1431 if missing and not markers:
1432 if missing and not markers:
1432 # dead end, mark it as such
1433 # dead end, mark it as such
1433 newreplacements.append((n, ()))
1434 newreplacements.append((n, ()))
1434 for marker in markers:
1435 for marker in markers:
1435 nsuccs = marker[1]
1436 nsuccs = marker[1]
1436 newreplacements.append((n, nsuccs))
1437 newreplacements.append((n, nsuccs))
1437 for nsucc in nsuccs:
1438 for nsucc in nsuccs:
1438 if nsucc not in seensuccs:
1439 if nsucc not in seensuccs:
1439 seensuccs.add(nsucc)
1440 seensuccs.add(nsucc)
1440 succstocheck.append(nsucc)
1441 succstocheck.append(nsucc)
1441
1442
1442 return newreplacements
1443 return newreplacements
1443
1444
1444 def processreplacement(state):
1445 def processreplacement(state):
1445 """process the list of replacements to return
1446 """process the list of replacements to return
1446
1447
1447 1) the final mapping between original and created nodes
1448 1) the final mapping between original and created nodes
1448 2) the list of temporary node created by histedit
1449 2) the list of temporary node created by histedit
1449 3) the list of new commit created by histedit"""
1450 3) the list of new commit created by histedit"""
1450 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1451 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1451 allsuccs = set()
1452 allsuccs = set()
1452 replaced = set()
1453 replaced = set()
1453 fullmapping = {}
1454 fullmapping = {}
1454 # initialize basic set
1455 # initialize basic set
1455 # fullmapping records all operations recorded in replacement
1456 # fullmapping records all operations recorded in replacement
1456 for rep in replacements:
1457 for rep in replacements:
1457 allsuccs.update(rep[1])
1458 allsuccs.update(rep[1])
1458 replaced.add(rep[0])
1459 replaced.add(rep[0])
1459 fullmapping.setdefault(rep[0], set()).update(rep[1])
1460 fullmapping.setdefault(rep[0], set()).update(rep[1])
1460 new = allsuccs - replaced
1461 new = allsuccs - replaced
1461 tmpnodes = allsuccs & replaced
1462 tmpnodes = allsuccs & replaced
1462 # Reduce content fullmapping into direct relation between original nodes
1463 # Reduce content fullmapping into direct relation between original nodes
1463 # and final node created during history edition
1464 # and final node created during history edition
1464 # Dropped changeset are replaced by an empty list
1465 # Dropped changeset are replaced by an empty list
1465 toproceed = set(fullmapping)
1466 toproceed = set(fullmapping)
1466 final = {}
1467 final = {}
1467 while toproceed:
1468 while toproceed:
1468 for x in list(toproceed):
1469 for x in list(toproceed):
1469 succs = fullmapping[x]
1470 succs = fullmapping[x]
1470 for s in list(succs):
1471 for s in list(succs):
1471 if s in toproceed:
1472 if s in toproceed:
1472 # non final node with unknown closure
1473 # non final node with unknown closure
1473 # We can't process this now
1474 # We can't process this now
1474 break
1475 break
1475 elif s in final:
1476 elif s in final:
1476 # non final node, replace with closure
1477 # non final node, replace with closure
1477 succs.remove(s)
1478 succs.remove(s)
1478 succs.update(final[s])
1479 succs.update(final[s])
1479 else:
1480 else:
1480 final[x] = succs
1481 final[x] = succs
1481 toproceed.remove(x)
1482 toproceed.remove(x)
1482 # remove tmpnodes from final mapping
1483 # remove tmpnodes from final mapping
1483 for n in tmpnodes:
1484 for n in tmpnodes:
1484 del final[n]
1485 del final[n]
1485 # we expect all changes involved in final to exist in the repo
1486 # we expect all changes involved in final to exist in the repo
1486 # turn `final` into list (topologically sorted)
1487 # turn `final` into list (topologically sorted)
1487 nm = state.repo.changelog.nodemap
1488 nm = state.repo.changelog.nodemap
1488 for prec, succs in final.items():
1489 for prec, succs in final.items():
1489 final[prec] = sorted(succs, key=nm.get)
1490 final[prec] = sorted(succs, key=nm.get)
1490
1491
1491 # computed topmost element (necessary for bookmark)
1492 # computed topmost element (necessary for bookmark)
1492 if new:
1493 if new:
1493 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1494 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1494 elif not final:
1495 elif not final:
1495 # Nothing rewritten at all. we won't need `newtopmost`
1496 # Nothing rewritten at all. we won't need `newtopmost`
1496 # It is the same as `oldtopmost` and `processreplacement` know it
1497 # It is the same as `oldtopmost` and `processreplacement` know it
1497 newtopmost = None
1498 newtopmost = None
1498 else:
1499 else:
1499 # every body died. The newtopmost is the parent of the root.
1500 # every body died. The newtopmost is the parent of the root.
1500 r = state.repo.changelog.rev
1501 r = state.repo.changelog.rev
1501 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1502 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1502
1503
1503 return final, tmpnodes, new, newtopmost
1504 return final, tmpnodes, new, newtopmost
1504
1505
1505 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1506 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1506 """Move bookmark from old to newly created node"""
1507 """Move bookmark from old to newly created node"""
1507 if not mapping:
1508 if not mapping:
1508 # if nothing got rewritten there is not purpose for this function
1509 # if nothing got rewritten there is not purpose for this function
1509 return
1510 return
1510 moves = []
1511 moves = []
1511 for bk, old in sorted(repo._bookmarks.iteritems()):
1512 for bk, old in sorted(repo._bookmarks.iteritems()):
1512 if old == oldtopmost:
1513 if old == oldtopmost:
1513 # special case ensure bookmark stay on tip.
1514 # special case ensure bookmark stay on tip.
1514 #
1515 #
1515 # This is arguably a feature and we may only want that for the
1516 # This is arguably a feature and we may only want that for the
1516 # active bookmark. But the behavior is kept compatible with the old
1517 # active bookmark. But the behavior is kept compatible with the old
1517 # version for now.
1518 # version for now.
1518 moves.append((bk, newtopmost))
1519 moves.append((bk, newtopmost))
1519 continue
1520 continue
1520 base = old
1521 base = old
1521 new = mapping.get(base, None)
1522 new = mapping.get(base, None)
1522 if new is None:
1523 if new is None:
1523 continue
1524 continue
1524 while not new:
1525 while not new:
1525 # base is killed, trying with parent
1526 # base is killed, trying with parent
1526 base = repo[base].p1().node()
1527 base = repo[base].p1().node()
1527 new = mapping.get(base, (base,))
1528 new = mapping.get(base, (base,))
1528 # nothing to move
1529 # nothing to move
1529 moves.append((bk, new[-1]))
1530 moves.append((bk, new[-1]))
1530 if moves:
1531 if moves:
1531 lock = tr = None
1532 lock = tr = None
1532 try:
1533 try:
1533 lock = repo.lock()
1534 lock = repo.lock()
1534 tr = repo.transaction('histedit')
1535 tr = repo.transaction('histedit')
1535 marks = repo._bookmarks
1536 marks = repo._bookmarks
1536 for mark, new in moves:
1537 for mark, new in moves:
1537 old = marks[mark]
1538 old = marks[mark]
1538 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1539 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1539 % (mark, node.short(old), node.short(new)))
1540 % (mark, node.short(old), node.short(new)))
1540 marks[mark] = new
1541 marks[mark] = new
1541 marks.recordchange(tr)
1542 marks.recordchange(tr)
1542 tr.close()
1543 tr.close()
1543 finally:
1544 finally:
1544 release(tr, lock)
1545 release(tr, lock)
1545
1546
1546 def cleanupnode(ui, repo, name, nodes):
1547 def cleanupnode(ui, repo, name, nodes):
1547 """strip a group of nodes from the repository
1548 """strip a group of nodes from the repository
1548
1549
1549 The set of node to strip may contains unknown nodes."""
1550 The set of node to strip may contains unknown nodes."""
1550 ui.debug('should strip %s nodes %s\n' %
1551 ui.debug('should strip %s nodes %s\n' %
1551 (name, ', '.join([node.short(n) for n in nodes])))
1552 (name, ', '.join([node.short(n) for n in nodes])))
1552 with repo.lock():
1553 with repo.lock():
1553 # do not let filtering get in the way of the cleanse
1554 # do not let filtering get in the way of the cleanse
1554 # we should probably get rid of obsolescence marker created during the
1555 # we should probably get rid of obsolescence marker created during the
1555 # histedit, but we currently do not have such information.
1556 # histedit, but we currently do not have such information.
1556 repo = repo.unfiltered()
1557 repo = repo.unfiltered()
1557 # Find all nodes that need to be stripped
1558 # Find all nodes that need to be stripped
1558 # (we use %lr instead of %ln to silently ignore unknown items)
1559 # (we use %lr instead of %ln to silently ignore unknown items)
1559 nm = repo.changelog.nodemap
1560 nm = repo.changelog.nodemap
1560 nodes = sorted(n for n in nodes if n in nm)
1561 nodes = sorted(n for n in nodes if n in nm)
1561 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1562 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1562 for c in roots:
1563 for c in roots:
1563 # We should process node in reverse order to strip tip most first.
1564 # We should process node in reverse order to strip tip most first.
1564 # but this trigger a bug in changegroup hook.
1565 # but this trigger a bug in changegroup hook.
1565 # This would reduce bundle overhead
1566 # This would reduce bundle overhead
1566 repair.strip(ui, repo, c)
1567 repair.strip(ui, repo, c)
1567
1568
1568 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1569 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1569 if isinstance(nodelist, str):
1570 if isinstance(nodelist, str):
1570 nodelist = [nodelist]
1571 nodelist = [nodelist]
1571 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1572 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1572 state = histeditstate(repo)
1573 state = histeditstate(repo)
1573 state.read()
1574 state.read()
1574 histedit_nodes = set([action.node for action
1575 histedit_nodes = set([action.node for action
1575 in state.actions if action.node])
1576 in state.actions if action.node])
1576 common_nodes = histedit_nodes & set(nodelist)
1577 common_nodes = histedit_nodes & set(nodelist)
1577 if common_nodes:
1578 if common_nodes:
1578 raise error.Abort(_("histedit in progress, can't strip %s")
1579 raise error.Abort(_("histedit in progress, can't strip %s")
1579 % ', '.join(node.short(x) for x in common_nodes))
1580 % ', '.join(node.short(x) for x in common_nodes))
1580 return orig(ui, repo, nodelist, *args, **kwargs)
1581 return orig(ui, repo, nodelist, *args, **kwargs)
1581
1582
1582 extensions.wrapfunction(repair, 'strip', stripwrapper)
1583 extensions.wrapfunction(repair, 'strip', stripwrapper)
1583
1584
1584 def summaryhook(ui, repo):
1585 def summaryhook(ui, repo):
1585 if not os.path.exists(repo.join('histedit-state')):
1586 if not os.path.exists(repo.join('histedit-state')):
1586 return
1587 return
1587 state = histeditstate(repo)
1588 state = histeditstate(repo)
1588 state.read()
1589 state.read()
1589 if state.actions:
1590 if state.actions:
1590 # i18n: column positioning for "hg summary"
1591 # i18n: column positioning for "hg summary"
1591 ui.write(_('hist: %s (histedit --continue)\n') %
1592 ui.write(_('hist: %s (histedit --continue)\n') %
1592 (ui.label(_('%d remaining'), 'histedit.remaining') %
1593 (ui.label(_('%d remaining'), 'histedit.remaining') %
1593 len(state.actions)))
1594 len(state.actions)))
1594
1595
1595 def extsetup(ui):
1596 def extsetup(ui):
1596 cmdutil.summaryhooks.add('histedit', summaryhook)
1597 cmdutil.summaryhooks.add('histedit', summaryhook)
1597 cmdutil.unfinishedstates.append(
1598 cmdutil.unfinishedstates.append(
1598 ['histedit-state', False, True, _('histedit in progress'),
1599 ['histedit-state', False, True, _('histedit in progress'),
1599 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1600 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1600 cmdutil.afterresolvedstates.append(
1601 cmdutil.afterresolvedstates.append(
1601 ['histedit-state', _('hg histedit --continue')])
1602 ['histedit-state', _('hg histedit --continue')])
1602 if ui.configbool("experimental", "histeditng"):
1603 if ui.configbool("experimental", "histeditng"):
1603 globals()['base'] = action(['base', 'b'],
1604 globals()['base'] = action(['base', 'b'],
1604 _('checkout changeset and apply further changesets from there')
1605 _('checkout changeset and apply further changesets from there')
1605 )(base)
1606 )(base)
@@ -1,336 +1,344
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 $ modwithdate ()
9 > {
10 > echo $1 > $1
11 > hg ci -m $1 -d "$2 0"
12 > }
13
8 $ initrepo ()
14 $ initrepo ()
9 > {
15 > {
10 > hg init $1
16 > hg init $1
11 > cd $1
17 > cd $1
12 > for x in a b c d e f ; do
18 > for x in a b c d e f ; do
13 > echo $x$x$x$x$x > $x
19 > echo $x$x$x$x$x > $x
14 > hg add $x
20 > hg add $x
15 > done
21 > done
16 > hg ci -m 'Initial commit'
22 > hg ci -m 'Initial commit'
17 > for x in a b c d e f ; do
23 > modwithdate a 1
18 > echo $x > $x
24 > modwithdate b 2
19 > hg ci -m $x
25 > modwithdate c 3
20 > done
26 > modwithdate d 4
27 > modwithdate e 5
28 > modwithdate f 6
21 > echo 'I can haz no commute' > e
29 > echo 'I can haz no commute' > e
22 > hg ci -m 'does not commute with e'
30 > hg ci -m 'does not commute with e' -d '7 0'
23 > cd ..
31 > cd ..
24 > }
32 > }
25
33
26 $ initrepo r
34 $ initrepo r
27 $ cd r
35 $ cd r
28 Initial generation of the command files
36 Initial generation of the command files
29
37
30 $ EDITED="$TESTTMP/editedhistory"
38 $ EDITED="$TESTTMP/editedhistory"
31 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 3 >> $EDITED
39 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 3 >> $EDITED
32 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 4 >> $EDITED
40 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 4 >> $EDITED
33 $ hg log --template 'fold {node|short} {rev} {desc}\n' -r 7 >> $EDITED
41 $ hg log --template 'fold {node|short} {rev} {desc}\n' -r 7 >> $EDITED
34 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 5 >> $EDITED
42 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 5 >> $EDITED
35 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 6 >> $EDITED
43 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 6 >> $EDITED
36 $ cat $EDITED
44 $ cat $EDITED
37 pick 65a9a84f33fd 3 c
45 pick 092e4ce14829 3 c
38 pick 00f1c5383965 4 d
46 pick ae78f4c9d74f 4 d
39 fold 39522b764e3d 7 does not commute with e
47 fold 42abbb61bede 7 does not commute with e
40 pick 7b4e2f4b7bcd 5 e
48 pick 7f3755409b00 5 e
41 pick 500cac37a696 6 f
49 pick dd184f2faeb0 6 f
42
50
43 log before edit
51 log before edit
44 $ hg log --graph
52 $ hg log --graph
45 @ changeset: 7:39522b764e3d
53 @ changeset: 7:42abbb61bede
46 | tag: tip
54 | tag: tip
47 | user: test
55 | user: test
48 | date: Thu Jan 01 00:00:00 1970 +0000
56 | date: Thu Jan 01 00:00:07 1970 +0000
49 | summary: does not commute with e
57 | summary: does not commute with e
50 |
58 |
51 o changeset: 6:500cac37a696
59 o changeset: 6:dd184f2faeb0
52 | user: test
60 | user: test
53 | date: Thu Jan 01 00:00:00 1970 +0000
61 | date: Thu Jan 01 00:00:06 1970 +0000
54 | summary: f
62 | summary: f
55 |
63 |
56 o changeset: 5:7b4e2f4b7bcd
64 o changeset: 5:7f3755409b00
57 | user: test
65 | user: test
58 | date: Thu Jan 01 00:00:00 1970 +0000
66 | date: Thu Jan 01 00:00:05 1970 +0000
59 | summary: e
67 | summary: e
60 |
68 |
61 o changeset: 4:00f1c5383965
69 o changeset: 4:ae78f4c9d74f
62 | user: test
70 | user: test
63 | date: Thu Jan 01 00:00:00 1970 +0000
71 | date: Thu Jan 01 00:00:04 1970 +0000
64 | summary: d
72 | summary: d
65 |
73 |
66 o changeset: 3:65a9a84f33fd
74 o changeset: 3:092e4ce14829
67 | user: test
75 | user: test
68 | date: Thu Jan 01 00:00:00 1970 +0000
76 | date: Thu Jan 01 00:00:03 1970 +0000
69 | summary: c
77 | summary: c
70 |
78 |
71 o changeset: 2:da6535b52e45
79 o changeset: 2:40ccdd8beb95
72 | user: test
80 | user: test
73 | date: Thu Jan 01 00:00:00 1970 +0000
81 | date: Thu Jan 01 00:00:02 1970 +0000
74 | summary: b
82 | summary: b
75 |
83 |
76 o changeset: 1:c1f09da44841
84 o changeset: 1:cd997a145b29
77 | user: test
85 | user: test
78 | date: Thu Jan 01 00:00:00 1970 +0000
86 | date: Thu Jan 01 00:00:01 1970 +0000
79 | summary: a
87 | summary: a
80 |
88 |
81 o changeset: 0:1715188a53c7
89 o changeset: 0:1715188a53c7
82 user: test
90 user: test
83 date: Thu Jan 01 00:00:00 1970 +0000
91 date: Thu Jan 01 00:00:00 1970 +0000
84 summary: Initial commit
92 summary: Initial commit
85
93
86
94
87 edit the history
95 edit the history
88 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
96 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
89 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
97 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
90 merging e
98 merging e
91 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
99 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
92 Fix up the change (fold 39522b764e3d)
100 Fix up the change (fold 42abbb61bede)
93 (hg histedit --continue to resume)
101 (hg histedit --continue to resume)
94
102
95 fix up
103 fix up
96 $ echo 'I can haz no commute' > e
104 $ echo 'I can haz no commute' > e
97 $ hg resolve --mark e
105 $ hg resolve --mark e
98 (no more unresolved files)
106 (no more unresolved files)
99 continue: hg histedit --continue
107 continue: hg histedit --continue
100 $ HGEDITOR=cat hg histedit --continue 2>&1 | fixbundle | grep -v '2 files removed'
108 $ HGEDITOR=cat hg histedit --continue 2>&1 | fixbundle | grep -v '2 files removed'
101 d
109 d
102 ***
110 ***
103 does not commute with e
111 does not commute with e
104
112
105
113
106
114
107 HG: Enter commit message. Lines beginning with 'HG:' are removed.
115 HG: Enter commit message. Lines beginning with 'HG:' are removed.
108 HG: Leave message empty to abort commit.
116 HG: Leave message empty to abort commit.
109 HG: --
117 HG: --
110 HG: user: test
118 HG: user: test
111 HG: branch 'default'
119 HG: branch 'default'
112 HG: changed d
120 HG: changed d
113 HG: changed e
121 HG: changed e
114 merging e
122 merging e
115 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
123 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
116 Fix up the change (pick 7b4e2f4b7bcd)
124 Fix up the change (pick 7f3755409b00)
117 (hg histedit --continue to resume)
125 (hg histedit --continue to resume)
118
126
119 just continue this time
127 just continue this time
120 keep the non-commuting change, and thus the pending change will be dropped
128 keep the non-commuting change, and thus the pending change will be dropped
121 $ hg revert -r 'p1()' e
129 $ hg revert -r 'p1()' e
122 $ hg resolve --mark e
130 $ hg resolve --mark e
123 (no more unresolved files)
131 (no more unresolved files)
124 continue: hg histedit --continue
132 continue: hg histedit --continue
125 $ hg diff
133 $ hg diff
126 $ hg histedit --continue 2>&1 | fixbundle
134 $ hg histedit --continue 2>&1 | fixbundle
127 7b4e2f4b7bcd: skipping changeset (no changes)
135 7f3755409b00: skipping changeset (no changes)
128
136
129 log after edit
137 log after edit
130 $ hg log --graph
138 $ hg log --graph
131 @ changeset: 5:d9cf42e54966
139 @ changeset: 5:1300355b1a54
132 | tag: tip
140 | tag: tip
133 | user: test
141 | user: test
134 | date: Thu Jan 01 00:00:00 1970 +0000
142 | date: Thu Jan 01 00:00:06 1970 +0000
135 | summary: f
143 | summary: f
136 |
144 |
137 o changeset: 4:10486af2e984
145 o changeset: 4:e2ac33269083
138 | user: test
146 | user: test
139 | date: Thu Jan 01 00:00:00 1970 +0000
147 | date: Thu Jan 01 00:00:07 1970 +0000
140 | summary: d
148 | summary: d
141 |
149 |
142 o changeset: 3:65a9a84f33fd
150 o changeset: 3:092e4ce14829
143 | user: test
151 | user: test
144 | date: Thu Jan 01 00:00:00 1970 +0000
152 | date: Thu Jan 01 00:00:03 1970 +0000
145 | summary: c
153 | summary: c
146 |
154 |
147 o changeset: 2:da6535b52e45
155 o changeset: 2:40ccdd8beb95
148 | user: test
156 | user: test
149 | date: Thu Jan 01 00:00:00 1970 +0000
157 | date: Thu Jan 01 00:00:02 1970 +0000
150 | summary: b
158 | summary: b
151 |
159 |
152 o changeset: 1:c1f09da44841
160 o changeset: 1:cd997a145b29
153 | user: test
161 | user: test
154 | date: Thu Jan 01 00:00:00 1970 +0000
162 | date: Thu Jan 01 00:00:01 1970 +0000
155 | summary: a
163 | summary: a
156 |
164 |
157 o changeset: 0:1715188a53c7
165 o changeset: 0:1715188a53c7
158 user: test
166 user: test
159 date: Thu Jan 01 00:00:00 1970 +0000
167 date: Thu Jan 01 00:00:00 1970 +0000
160 summary: Initial commit
168 summary: Initial commit
161
169
162
170
163 contents of e
171 contents of e
164 $ hg cat e
172 $ hg cat e
165 I can haz no commute
173 I can haz no commute
166
174
167 manifest
175 manifest
168 $ hg manifest
176 $ hg manifest
169 a
177 a
170 b
178 b
171 c
179 c
172 d
180 d
173 e
181 e
174 f
182 f
175
183
176 $ cd ..
184 $ cd ..
177
185
178 Repeat test using "roll", not "fold". "roll" folds in changes but drops message
186 Repeat test using "roll", not "fold". "roll" folds in changes but drops message
179
187
180 $ initrepo r2
188 $ initrepo r2
181 $ cd r2
189 $ cd r2
182
190
183 Initial generation of the command files
191 Initial generation of the command files
184
192
185 $ EDITED="$TESTTMP/editedhistory.2"
193 $ EDITED="$TESTTMP/editedhistory.2"
186 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 3 >> $EDITED
194 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 3 >> $EDITED
187 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 4 >> $EDITED
195 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 4 >> $EDITED
188 $ hg log --template 'roll {node|short} {rev} {desc}\n' -r 7 >> $EDITED
196 $ hg log --template 'roll {node|short} {rev} {desc}\n' -r 7 >> $EDITED
189 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 5 >> $EDITED
197 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 5 >> $EDITED
190 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 6 >> $EDITED
198 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 6 >> $EDITED
191 $ cat $EDITED
199 $ cat $EDITED
192 pick 65a9a84f33fd 3 c
200 pick 092e4ce14829 3 c
193 pick 00f1c5383965 4 d
201 pick ae78f4c9d74f 4 d
194 roll 39522b764e3d 7 does not commute with e
202 roll 42abbb61bede 7 does not commute with e
195 pick 7b4e2f4b7bcd 5 e
203 pick 7f3755409b00 5 e
196 pick 500cac37a696 6 f
204 pick dd184f2faeb0 6 f
197
205
198 log before edit
206 log before edit
199 $ hg log --graph
207 $ hg log --graph
200 @ changeset: 7:39522b764e3d
208 @ changeset: 7:42abbb61bede
201 | tag: tip
209 | tag: tip
202 | user: test
210 | user: test
203 | date: Thu Jan 01 00:00:00 1970 +0000
211 | date: Thu Jan 01 00:00:07 1970 +0000
204 | summary: does not commute with e
212 | summary: does not commute with e
205 |
213 |
206 o changeset: 6:500cac37a696
214 o changeset: 6:dd184f2faeb0
207 | user: test
215 | user: test
208 | date: Thu Jan 01 00:00:00 1970 +0000
216 | date: Thu Jan 01 00:00:06 1970 +0000
209 | summary: f
217 | summary: f
210 |
218 |
211 o changeset: 5:7b4e2f4b7bcd
219 o changeset: 5:7f3755409b00
212 | user: test
220 | user: test
213 | date: Thu Jan 01 00:00:00 1970 +0000
221 | date: Thu Jan 01 00:00:05 1970 +0000
214 | summary: e
222 | summary: e
215 |
223 |
216 o changeset: 4:00f1c5383965
224 o changeset: 4:ae78f4c9d74f
217 | user: test
225 | user: test
218 | date: Thu Jan 01 00:00:00 1970 +0000
226 | date: Thu Jan 01 00:00:04 1970 +0000
219 | summary: d
227 | summary: d
220 |
228 |
221 o changeset: 3:65a9a84f33fd
229 o changeset: 3:092e4ce14829
222 | user: test
230 | user: test
223 | date: Thu Jan 01 00:00:00 1970 +0000
231 | date: Thu Jan 01 00:00:03 1970 +0000
224 | summary: c
232 | summary: c
225 |
233 |
226 o changeset: 2:da6535b52e45
234 o changeset: 2:40ccdd8beb95
227 | user: test
235 | user: test
228 | date: Thu Jan 01 00:00:00 1970 +0000
236 | date: Thu Jan 01 00:00:02 1970 +0000
229 | summary: b
237 | summary: b
230 |
238 |
231 o changeset: 1:c1f09da44841
239 o changeset: 1:cd997a145b29
232 | user: test
240 | user: test
233 | date: Thu Jan 01 00:00:00 1970 +0000
241 | date: Thu Jan 01 00:00:01 1970 +0000
234 | summary: a
242 | summary: a
235 |
243 |
236 o changeset: 0:1715188a53c7
244 o changeset: 0:1715188a53c7
237 user: test
245 user: test
238 date: Thu Jan 01 00:00:00 1970 +0000
246 date: Thu Jan 01 00:00:00 1970 +0000
239 summary: Initial commit
247 summary: Initial commit
240
248
241
249
242 edit the history
250 edit the history
243 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
251 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
244 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
252 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
245 merging e
253 merging e
246 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
254 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
247 Fix up the change (roll 39522b764e3d)
255 Fix up the change (roll 42abbb61bede)
248 (hg histedit --continue to resume)
256 (hg histedit --continue to resume)
249
257
250 fix up
258 fix up
251 $ echo 'I can haz no commute' > e
259 $ echo 'I can haz no commute' > e
252 $ hg resolve --mark e
260 $ hg resolve --mark e
253 (no more unresolved files)
261 (no more unresolved files)
254 continue: hg histedit --continue
262 continue: hg histedit --continue
255 $ hg histedit --continue 2>&1 | fixbundle | grep -v '2 files removed'
263 $ hg histedit --continue 2>&1 | fixbundle | grep -v '2 files removed'
256 merging e
264 merging e
257 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
265 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
258 Fix up the change (pick 7b4e2f4b7bcd)
266 Fix up the change (pick 7f3755409b00)
259 (hg histedit --continue to resume)
267 (hg histedit --continue to resume)
260
268
261 just continue this time
269 just continue this time
262 $ hg revert -r 'p1()' e
270 $ hg revert -r 'p1()' e
263 $ hg resolve --mark e
271 $ hg resolve --mark e
264 (no more unresolved files)
272 (no more unresolved files)
265 continue: hg histedit --continue
273 continue: hg histedit --continue
266 $ hg histedit --continue 2>&1 | fixbundle
274 $ hg histedit --continue 2>&1 | fixbundle
267 7b4e2f4b7bcd: skipping changeset (no changes)
275 7f3755409b00: skipping changeset (no changes)
268
276
269 log after edit
277 log after edit
270 $ hg log --graph
278 $ hg log --graph
271 @ changeset: 5:e7c4f5d4eb75
279 @ changeset: 5:162978f027fb
272 | tag: tip
280 | tag: tip
273 | user: test
281 | user: test
274 | date: Thu Jan 01 00:00:00 1970 +0000
282 | date: Thu Jan 01 00:00:06 1970 +0000
275 | summary: f
283 | summary: f
276 |
284 |
277 o changeset: 4:803d1bb561fc
285 o changeset: 4:74e5e6c6c32f
278 | user: test
286 | user: test
279 | date: Thu Jan 01 00:00:00 1970 +0000
287 | date: Thu Jan 01 00:00:07 1970 +0000
280 | summary: d
288 | summary: d
281 |
289 |
282 o changeset: 3:65a9a84f33fd
290 o changeset: 3:092e4ce14829
283 | user: test
291 | user: test
284 | date: Thu Jan 01 00:00:00 1970 +0000
292 | date: Thu Jan 01 00:00:03 1970 +0000
285 | summary: c
293 | summary: c
286 |
294 |
287 o changeset: 2:da6535b52e45
295 o changeset: 2:40ccdd8beb95
288 | user: test
296 | user: test
289 | date: Thu Jan 01 00:00:00 1970 +0000
297 | date: Thu Jan 01 00:00:02 1970 +0000
290 | summary: b
298 | summary: b
291 |
299 |
292 o changeset: 1:c1f09da44841
300 o changeset: 1:cd997a145b29
293 | user: test
301 | user: test
294 | date: Thu Jan 01 00:00:00 1970 +0000
302 | date: Thu Jan 01 00:00:01 1970 +0000
295 | summary: a
303 | summary: a
296 |
304 |
297 o changeset: 0:1715188a53c7
305 o changeset: 0:1715188a53c7
298 user: test
306 user: test
299 date: Thu Jan 01 00:00:00 1970 +0000
307 date: Thu Jan 01 00:00:00 1970 +0000
300 summary: Initial commit
308 summary: Initial commit
301
309
302
310
303 contents of e
311 contents of e
304 $ hg cat e
312 $ hg cat e
305 I can haz no commute
313 I can haz no commute
306
314
307 manifest
315 manifest
308 $ hg manifest
316 $ hg manifest
309 a
317 a
310 b
318 b
311 c
319 c
312 d
320 d
313 e
321 e
314 f
322 f
315
323
316 description is taken from rollup target commit
324 description is taken from rollup target commit
317
325
318 $ hg log --debug --rev 4
326 $ hg log --debug --rev 4
319 changeset: 4:803d1bb561fceac3129ec778db9da249a3106fc3
327 changeset: 4:74e5e6c6c32fa39f0eeed43302fd48633ea5926f
320 phase: draft
328 phase: draft
321 parent: 3:65a9a84f33fdeb1ad5679b3941ec885d2b24027b
329 parent: 3:092e4ce14829f4974399ce4316d59f64ef0b6725
322 parent: -1:0000000000000000000000000000000000000000
330 parent: -1:0000000000000000000000000000000000000000
323 manifest: 4:b068a323d969f22af1296ec6a5ea9384cef437ac
331 manifest: 4:b068a323d969f22af1296ec6a5ea9384cef437ac
324 user: test
332 user: test
325 date: Thu Jan 01 00:00:00 1970 +0000
333 date: Thu Jan 01 00:00:07 1970 +0000
326 files: d e
334 files: d e
327 extra: branch=default
335 extra: branch=default
328 extra: histedit_source=00f1c53839651fa5c76d423606811ea5455a79d0,39522b764e3d26103f08bd1fa2ccd3e3d7dbcf4e
336 extra: histedit_source=ae78f4c9d74ffa4b6cb5045001c303fe9204e890,42abbb61bede6f4366fa1e74a664343e5d558a70
329 description:
337 description:
330 d
338 d
331
339
332
340
333
341
334 done with repo r2
342 done with repo r2
335
343
336 $ cd ..
344 $ cd ..
@@ -1,535 +1,543
1 Test histedit extension: Fold commands
1 Test histedit extension: Fold commands
2 ======================================
2 ======================================
3
3
4 This test file is dedicated to testing the fold command in non conflicting
4 This test file is dedicated to testing the fold command in non conflicting
5 case.
5 case.
6
6
7 Initialization
7 Initialization
8 ---------------
8 ---------------
9
9
10
10
11 $ . "$TESTDIR/histedit-helpers.sh"
11 $ . "$TESTDIR/histedit-helpers.sh"
12
12
13 $ cat >> $HGRCPATH <<EOF
13 $ cat >> $HGRCPATH <<EOF
14 > [alias]
14 > [alias]
15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
16 > [extensions]
16 > [extensions]
17 > histedit=
17 > histedit=
18 > EOF
18 > EOF
19
19
20
20
21 Simple folding
21 Simple folding
22 --------------------
22 --------------------
23 $ addwithdate ()
24 > {
25 > echo $1 > $1
26 > hg add $1
27 > hg ci -m $1 -d "$2 0"
28 > }
29
23 $ initrepo ()
30 $ initrepo ()
24 > {
31 > {
25 > hg init r
32 > hg init r
26 > cd r
33 > cd r
27 > for x in a b c d e f ; do
34 > addwithdate a 1
28 > echo $x > $x
35 > addwithdate b 2
29 > hg add $x
36 > addwithdate c 3
30 > hg ci -m $x
37 > addwithdate d 4
31 > done
38 > addwithdate e 5
39 > addwithdate f 6
32 > }
40 > }
33
41
34 $ initrepo
42 $ initrepo
35
43
36 log before edit
44 log before edit
37 $ hg logt --graph
45 $ hg logt --graph
38 @ 5:652413bf663e f
46 @ 5:178e35e0ce73 f
39 |
47 |
40 o 4:e860deea161a e
48 o 4:1ddb6c90f2ee e
41 |
49 |
42 o 3:055a42cdd887 d
50 o 3:532247a8969b d
43 |
51 |
44 o 2:177f92b77385 c
52 o 2:ff2c9fa2018b c
45 |
53 |
46 o 1:d2ae7f538514 b
54 o 1:97d72e5f12c7 b
47 |
55 |
48 o 0:cb9a9f314b8b a
56 o 0:8580ff50825a a
49
57
50
58
51 $ hg histedit 177f92b77385 --commands - 2>&1 <<EOF | fixbundle
59 $ hg histedit ff2c9fa2018b --commands - 2>&1 <<EOF | fixbundle
52 > pick e860deea161a e
60 > pick 1ddb6c90f2ee e
53 > pick 652413bf663e f
61 > pick 178e35e0ce73 f
54 > fold 177f92b77385 c
62 > fold ff2c9fa2018b c
55 > pick 055a42cdd887 d
63 > pick 532247a8969b d
56 > EOF
64 > EOF
57
65
58 log after edit
66 log after edit
59 $ hg logt --graph
67 $ hg logt --graph
60 @ 4:9c277da72c9b d
68 @ 4:c4d7f3def76d d
61 |
69 |
62 o 3:6de59d13424a f
70 o 3:575228819b7e f
63 |
71 |
64 o 2:ee283cb5f2d5 e
72 o 2:505a591af19e e
65 |
73 |
66 o 1:d2ae7f538514 b
74 o 1:97d72e5f12c7 b
67 |
75 |
68 o 0:cb9a9f314b8b a
76 o 0:8580ff50825a a
69
77
70
78
71 post-fold manifest
79 post-fold manifest
72 $ hg manifest
80 $ hg manifest
73 a
81 a
74 b
82 b
75 c
83 c
76 d
84 d
77 e
85 e
78 f
86 f
79
87
80
88
81 check histedit_source
89 check histedit_source, including that it uses the later date, from the first changeset
82
90
83 $ hg log --debug --rev 3
91 $ hg log --debug --rev 3
84 changeset: 3:6de59d13424a8a13acd3e975514aed29dd0d9b2d
92 changeset: 3:575228819b7e6ed69e8c0a6a383ee59a80db7358
85 phase: draft
93 phase: draft
86 parent: 2:ee283cb5f2d5955443f23a27b697a04339e9a39a
94 parent: 2:505a591af19eed18f560af827b9e03d2076773dc
87 parent: -1:0000000000000000000000000000000000000000
95 parent: -1:0000000000000000000000000000000000000000
88 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
96 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
89 user: test
97 user: test
90 date: Thu Jan 01 00:00:00 1970 +0000
98 date: Thu Jan 01 00:00:06 1970 +0000
91 files+: c f
99 files+: c f
92 extra: branch=default
100 extra: branch=default
93 extra: histedit_source=a4f7421b80f79fcc59fff01bcbf4a53d127dd6d3,177f92b773850b59254aa5e923436f921b55483b
101 extra: histedit_source=7cad1d7030207872dfd1c3a7cb430f24f2884086,ff2c9fa2018b15fa74b33363bda9527323e2a99f
94 description:
102 description:
95 f
103 f
96 ***
104 ***
97 c
105 c
98
106
99
107
100
108
101 rollup will fold without preserving the folded commit's message
109 rollup will fold without preserving the folded commit's message
102
110
103 $ OLDHGEDITOR=$HGEDITOR
111 $ OLDHGEDITOR=$HGEDITOR
104 $ HGEDITOR=false
112 $ HGEDITOR=false
105 $ hg histedit d2ae7f538514 --commands - 2>&1 <<EOF | fixbundle
113 $ hg histedit 97d72e5f12c7 --commands - 2>&1 <<EOF | fixbundle
106 > pick d2ae7f538514 b
114 > pick 97d72e5f12c7 b
107 > roll ee283cb5f2d5 e
115 > roll 505a591af19e e
108 > pick 6de59d13424a f
116 > pick 575228819b7e f
109 > pick 9c277da72c9b d
117 > pick c4d7f3def76d d
110 > EOF
118 > EOF
111
119
112 $ HGEDITOR=$OLDHGEDITOR
120 $ HGEDITOR=$OLDHGEDITOR
113
121
114 log after edit
122 log after edit
115 $ hg logt --graph
123 $ hg logt --graph
116 @ 3:c4a9eb7989fc d
124 @ 3:fb13f1f49340 d
117 |
125 |
118 o 2:8e03a72b6f83 f
126 o 2:6d4bc3727566 f
119 |
127 |
120 o 1:391ee782c689 b
128 o 1:563995ddbe65 b
121 |
129 |
122 o 0:cb9a9f314b8b a
130 o 0:8580ff50825a a
123
131
124
132
125 description is taken from rollup target commit
133 description is taken from rollup target commit
126
134
127 $ hg log --debug --rev 1
135 $ hg log --debug --rev 1
128 changeset: 1:391ee782c68930be438ccf4c6a403daedbfbffa5
136 changeset: 1:563995ddbe650c0e6b0e1c1d75f0a197b61cec50
129 phase: draft
137 phase: draft
130 parent: 0:cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
138 parent: 0:8580ff50825a50c8f716709acdf8de0deddcd6ab
131 parent: -1:0000000000000000000000000000000000000000
139 parent: -1:0000000000000000000000000000000000000000
132 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
140 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
133 user: test
141 user: test
134 date: Thu Jan 01 00:00:00 1970 +0000
142 date: Thu Jan 01 00:00:05 1970 +0000
135 files+: b e
143 files+: b e
136 extra: branch=default
144 extra: branch=default
137 extra: histedit_source=d2ae7f538514cd87c17547b0de4cea71fe1af9fb,ee283cb5f2d5955443f23a27b697a04339e9a39a
145 extra: histedit_source=97d72e5f12c7e84f85064aa72e5a297142c36ed9,505a591af19eed18f560af827b9e03d2076773dc
138 description:
146 description:
139 b
147 b
140
148
141
149
142
150
143 check saving last-message.txt
151 check saving last-message.txt
144
152
145 $ cat > $TESTTMP/abortfolding.py <<EOF
153 $ cat > $TESTTMP/abortfolding.py <<EOF
146 > from mercurial import util
154 > from mercurial import util
147 > def abortfolding(ui, repo, hooktype, **kwargs):
155 > def abortfolding(ui, repo, hooktype, **kwargs):
148 > ctx = repo[kwargs.get('node')]
156 > ctx = repo[kwargs.get('node')]
149 > if set(ctx.files()) == set(['c', 'd', 'f']):
157 > if set(ctx.files()) == set(['c', 'd', 'f']):
150 > return True # abort folding commit only
158 > return True # abort folding commit only
151 > ui.warn('allow non-folding commit\\n')
159 > ui.warn('allow non-folding commit\\n')
152 > EOF
160 > EOF
153 $ cat > .hg/hgrc <<EOF
161 $ cat > .hg/hgrc <<EOF
154 > [hooks]
162 > [hooks]
155 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
163 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
156 > EOF
164 > EOF
157
165
158 $ cat > $TESTTMP/editor.sh << EOF
166 $ cat > $TESTTMP/editor.sh << EOF
159 > echo "==== before editing"
167 > echo "==== before editing"
160 > cat \$1
168 > cat \$1
161 > echo "===="
169 > echo "===="
162 > echo "check saving last-message.txt" >> \$1
170 > echo "check saving last-message.txt" >> \$1
163 > EOF
171 > EOF
164
172
165 $ rm -f .hg/last-message.txt
173 $ rm -f .hg/last-message.txt
166 $ hg status --rev '8e03a72b6f83^1::c4a9eb7989fc'
174 $ hg status --rev '6d4bc3727566^1::fb13f1f49340'
167 A c
175 A c
168 A d
176 A d
169 A f
177 A f
170 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 8e03a72b6f83 --commands - 2>&1 <<EOF
178 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 6d4bc3727566 --commands - 2>&1 <<EOF
171 > pick 8e03a72b6f83 f
179 > pick 6d4bc3727566 f
172 > fold c4a9eb7989fc d
180 > fold fb13f1f49340 d
173 > EOF
181 > EOF
174 allow non-folding commit
182 allow non-folding commit
175 ==== before editing
183 ==== before editing
176 f
184 f
177 ***
185 ***
178 c
186 c
179 ***
187 ***
180 d
188 d
181
189
182
190
183
191
184 HG: Enter commit message. Lines beginning with 'HG:' are removed.
192 HG: Enter commit message. Lines beginning with 'HG:' are removed.
185 HG: Leave message empty to abort commit.
193 HG: Leave message empty to abort commit.
186 HG: --
194 HG: --
187 HG: user: test
195 HG: user: test
188 HG: branch 'default'
196 HG: branch 'default'
189 HG: added c
197 HG: added c
190 HG: added d
198 HG: added d
191 HG: added f
199 HG: added f
192 ====
200 ====
193 transaction abort!
201 transaction abort!
194 rollback completed
202 rollback completed
195 abort: pretxncommit.abortfolding hook failed
203 abort: pretxncommit.abortfolding hook failed
196 [255]
204 [255]
197
205
198 $ cat .hg/last-message.txt
206 $ cat .hg/last-message.txt
199 f
207 f
200 ***
208 ***
201 c
209 c
202 ***
210 ***
203 d
211 d
204
212
205
213
206
214
207 check saving last-message.txt
215 check saving last-message.txt
208
216
209 $ cd ..
217 $ cd ..
210 $ rm -r r
218 $ rm -r r
211
219
212 folding preserves initial author
220 folding preserves initial author but uses later date
213 --------------------------------
221 ----------------------------------------------------
214
222
215 $ initrepo
223 $ initrepo
216
224
217 $ hg ci --user "someone else" --amend --quiet
225 $ hg ci -d '7 0' --user "someone else" --amend --quiet
218
226
219 tip before edit
227 tip before edit
220 $ hg log --rev .
228 $ hg log --rev .
221 changeset: 5:a00ad806cb55
229 changeset: 5:10c36dd37515
222 tag: tip
230 tag: tip
223 user: someone else
231 user: someone else
224 date: Thu Jan 01 00:00:00 1970 +0000
232 date: Thu Jan 01 00:00:07 1970 +0000
225 summary: f
233 summary: f
226
234
227
235
228 $ hg --config progress.debug=1 --debug \
236 $ hg --config progress.debug=1 --debug \
229 > histedit e860deea161a --commands - 2>&1 <<EOF | \
237 > histedit 1ddb6c90f2ee --commands - 2>&1 <<EOF | \
230 > egrep 'editing|unresolved'
238 > egrep 'editing|unresolved'
231 > pick e860deea161a e
239 > pick 1ddb6c90f2ee e
232 > fold a00ad806cb55 f
240 > fold 10c36dd37515 f
233 > EOF
241 > EOF
234 editing: pick e860deea161a 4 e 1/2 changes (50.00%)
242 editing: pick 1ddb6c90f2ee 4 e 1/2 changes (50.00%)
235 editing: fold a00ad806cb55 5 f 2/2 changes (100.00%)
243 editing: fold 10c36dd37515 5 f 2/2 changes (100.00%)
236
244
237 tip after edit
245 tip after edit, which should use the later date, from the second changeset
238 $ hg log --rev .
246 $ hg log --rev .
239 changeset: 4:698d4e8040a1
247 changeset: 4:e4f3ec5d0b40
240 tag: tip
248 tag: tip
241 user: test
249 user: test
242 date: Thu Jan 01 00:00:00 1970 +0000
250 date: Thu Jan 01 00:00:07 1970 +0000
243 summary: e
251 summary: e
244
252
245
253
246 $ cd ..
254 $ cd ..
247 $ rm -r r
255 $ rm -r r
248
256
249 folding and creating no new change doesn't break:
257 folding and creating no new change doesn't break:
250 -------------------------------------------------
258 -------------------------------------------------
251
259
252 folded content is dropped during a merge. The folded commit should properly disappear.
260 folded content is dropped during a merge. The folded commit should properly disappear.
253
261
254 $ mkdir fold-to-empty-test
262 $ mkdir fold-to-empty-test
255 $ cd fold-to-empty-test
263 $ cd fold-to-empty-test
256 $ hg init
264 $ hg init
257 $ printf "1\n2\n3\n" > file
265 $ printf "1\n2\n3\n" > file
258 $ hg add file
266 $ hg add file
259 $ hg commit -m '1+2+3'
267 $ hg commit -m '1+2+3'
260 $ echo 4 >> file
268 $ echo 4 >> file
261 $ hg commit -m '+4'
269 $ hg commit -m '+4'
262 $ echo 5 >> file
270 $ echo 5 >> file
263 $ hg commit -m '+5'
271 $ hg commit -m '+5'
264 $ echo 6 >> file
272 $ echo 6 >> file
265 $ hg commit -m '+6'
273 $ hg commit -m '+6'
266 $ hg logt --graph
274 $ hg logt --graph
267 @ 3:251d831eeec5 +6
275 @ 3:251d831eeec5 +6
268 |
276 |
269 o 2:888f9082bf99 +5
277 o 2:888f9082bf99 +5
270 |
278 |
271 o 1:617f94f13c0f +4
279 o 1:617f94f13c0f +4
272 |
280 |
273 o 0:0189ba417d34 1+2+3
281 o 0:0189ba417d34 1+2+3
274
282
275
283
276 $ hg histedit 1 --commands - << EOF
284 $ hg histedit 1 --commands - << EOF
277 > pick 617f94f13c0f 1 +4
285 > pick 617f94f13c0f 1 +4
278 > drop 888f9082bf99 2 +5
286 > drop 888f9082bf99 2 +5
279 > fold 251d831eeec5 3 +6
287 > fold 251d831eeec5 3 +6
280 > EOF
288 > EOF
281 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
289 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
282 merging file
290 merging file
283 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
291 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
284 Fix up the change (fold 251d831eeec5)
292 Fix up the change (fold 251d831eeec5)
285 (hg histedit --continue to resume)
293 (hg histedit --continue to resume)
286 [1]
294 [1]
287 There were conflicts, we keep P1 content. This
295 There were conflicts, we keep P1 content. This
288 should effectively drop the changes from +6.
296 should effectively drop the changes from +6.
289 $ hg status
297 $ hg status
290 M file
298 M file
291 ? file.orig
299 ? file.orig
292 $ hg resolve -l
300 $ hg resolve -l
293 U file
301 U file
294 $ hg revert -r 'p1()' file
302 $ hg revert -r 'p1()' file
295 $ hg resolve --mark file
303 $ hg resolve --mark file
296 (no more unresolved files)
304 (no more unresolved files)
297 continue: hg histedit --continue
305 continue: hg histedit --continue
298 $ hg histedit --continue
306 $ hg histedit --continue
299 251d831eeec5: empty changeset
307 251d831eeec5: empty changeset
300 saved backup bundle to $TESTTMP/*-backup.hg (glob)
308 saved backup bundle to $TESTTMP/*-backup.hg (glob)
301 $ hg logt --graph
309 $ hg logt --graph
302 @ 1:617f94f13c0f +4
310 @ 1:617f94f13c0f +4
303 |
311 |
304 o 0:0189ba417d34 1+2+3
312 o 0:0189ba417d34 1+2+3
305
313
306
314
307 $ cd ..
315 $ cd ..
308
316
309
317
310 Test fold through dropped
318 Test fold through dropped
311 -------------------------
319 -------------------------
312
320
313
321
314 Test corner case where folded revision is separated from its parent by a
322 Test corner case where folded revision is separated from its parent by a
315 dropped revision.
323 dropped revision.
316
324
317
325
318 $ hg init fold-with-dropped
326 $ hg init fold-with-dropped
319 $ cd fold-with-dropped
327 $ cd fold-with-dropped
320 $ printf "1\n2\n3\n" > file
328 $ printf "1\n2\n3\n" > file
321 $ hg commit -Am '1+2+3'
329 $ hg commit -Am '1+2+3'
322 adding file
330 adding file
323 $ echo 4 >> file
331 $ echo 4 >> file
324 $ hg commit -m '+4'
332 $ hg commit -m '+4'
325 $ echo 5 >> file
333 $ echo 5 >> file
326 $ hg commit -m '+5'
334 $ hg commit -m '+5'
327 $ echo 6 >> file
335 $ echo 6 >> file
328 $ hg commit -m '+6'
336 $ hg commit -m '+6'
329 $ hg logt -G
337 $ hg logt -G
330 @ 3:251d831eeec5 +6
338 @ 3:251d831eeec5 +6
331 |
339 |
332 o 2:888f9082bf99 +5
340 o 2:888f9082bf99 +5
333 |
341 |
334 o 1:617f94f13c0f +4
342 o 1:617f94f13c0f +4
335 |
343 |
336 o 0:0189ba417d34 1+2+3
344 o 0:0189ba417d34 1+2+3
337
345
338 $ hg histedit 1 --commands - << EOF
346 $ hg histedit 1 --commands - << EOF
339 > pick 617f94f13c0f 1 +4
347 > pick 617f94f13c0f 1 +4
340 > drop 888f9082bf99 2 +5
348 > drop 888f9082bf99 2 +5
341 > fold 251d831eeec5 3 +6
349 > fold 251d831eeec5 3 +6
342 > EOF
350 > EOF
343 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
351 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
344 merging file
352 merging file
345 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
353 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
346 Fix up the change (fold 251d831eeec5)
354 Fix up the change (fold 251d831eeec5)
347 (hg histedit --continue to resume)
355 (hg histedit --continue to resume)
348 [1]
356 [1]
349 $ cat > file << EOF
357 $ cat > file << EOF
350 > 1
358 > 1
351 > 2
359 > 2
352 > 3
360 > 3
353 > 4
361 > 4
354 > 5
362 > 5
355 > EOF
363 > EOF
356 $ hg resolve --mark file
364 $ hg resolve --mark file
357 (no more unresolved files)
365 (no more unresolved files)
358 continue: hg histedit --continue
366 continue: hg histedit --continue
359 $ hg commit -m '+5.2'
367 $ hg commit -m '+5.2'
360 created new head
368 created new head
361 $ echo 6 >> file
369 $ echo 6 >> file
362 $ HGEDITOR=cat hg histedit --continue
370 $ HGEDITOR=cat hg histedit --continue
363 +4
371 +4
364 ***
372 ***
365 +5.2
373 +5.2
366 ***
374 ***
367 +6
375 +6
368
376
369
377
370
378
371 HG: Enter commit message. Lines beginning with 'HG:' are removed.
379 HG: Enter commit message. Lines beginning with 'HG:' are removed.
372 HG: Leave message empty to abort commit.
380 HG: Leave message empty to abort commit.
373 HG: --
381 HG: --
374 HG: user: test
382 HG: user: test
375 HG: branch 'default'
383 HG: branch 'default'
376 HG: changed file
384 HG: changed file
377 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/55c8d8dc79ce-4066cd98-backup.hg (glob)
385 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/55c8d8dc79ce-4066cd98-backup.hg (glob)
378 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-a35700fc-backup.hg (glob)
386 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-a35700fc-backup.hg (glob)
379 $ hg logt -G
387 $ hg logt -G
380 @ 1:10c647b2cdd5 +4
388 @ 1:10c647b2cdd5 +4
381 |
389 |
382 o 0:0189ba417d34 1+2+3
390 o 0:0189ba417d34 1+2+3
383
391
384 $ hg export tip
392 $ hg export tip
385 # HG changeset patch
393 # HG changeset patch
386 # User test
394 # User test
387 # Date 0 0
395 # Date 0 0
388 # Thu Jan 01 00:00:00 1970 +0000
396 # Thu Jan 01 00:00:00 1970 +0000
389 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
397 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
390 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
398 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
391 +4
399 +4
392 ***
400 ***
393 +5.2
401 +5.2
394 ***
402 ***
395 +6
403 +6
396
404
397 diff -r 0189ba417d34 -r 10c647b2cdd5 file
405 diff -r 0189ba417d34 -r 10c647b2cdd5 file
398 --- a/file Thu Jan 01 00:00:00 1970 +0000
406 --- a/file Thu Jan 01 00:00:00 1970 +0000
399 +++ b/file Thu Jan 01 00:00:00 1970 +0000
407 +++ b/file Thu Jan 01 00:00:00 1970 +0000
400 @@ -1,3 +1,6 @@
408 @@ -1,3 +1,6 @@
401 1
409 1
402 2
410 2
403 3
411 3
404 +4
412 +4
405 +5
413 +5
406 +6
414 +6
407 $ cd ..
415 $ cd ..
408
416
409
417
410 Folding with initial rename (issue3729)
418 Folding with initial rename (issue3729)
411 ---------------------------------------
419 ---------------------------------------
412
420
413 $ hg init fold-rename
421 $ hg init fold-rename
414 $ cd fold-rename
422 $ cd fold-rename
415 $ echo a > a.txt
423 $ echo a > a.txt
416 $ hg add a.txt
424 $ hg add a.txt
417 $ hg commit -m a
425 $ hg commit -m a
418 $ hg rename a.txt b.txt
426 $ hg rename a.txt b.txt
419 $ hg commit -m rename
427 $ hg commit -m rename
420 $ echo b >> b.txt
428 $ echo b >> b.txt
421 $ hg commit -m b
429 $ hg commit -m b
422
430
423 $ hg logt --follow b.txt
431 $ hg logt --follow b.txt
424 2:e0371e0426bc b
432 2:e0371e0426bc b
425 1:1c4f440a8085 rename
433 1:1c4f440a8085 rename
426 0:6c795aa153cb a
434 0:6c795aa153cb a
427
435
428 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
436 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
429 > pick 1c4f440a8085 rename
437 > pick 1c4f440a8085 rename
430 > fold e0371e0426bc b
438 > fold e0371e0426bc b
431 > EOF
439 > EOF
432
440
433 $ hg logt --follow b.txt
441 $ hg logt --follow b.txt
434 1:cf858d235c76 rename
442 1:cf858d235c76 rename
435 0:6c795aa153cb a
443 0:6c795aa153cb a
436
444
437 $ cd ..
445 $ cd ..
438
446
439 Folding with swapping
447 Folding with swapping
440 ---------------------
448 ---------------------
441
449
442 This is an excuse to test hook with histedit temporary commit (issue4422)
450 This is an excuse to test hook with histedit temporary commit (issue4422)
443
451
444
452
445 $ hg init issue4422
453 $ hg init issue4422
446 $ cd issue4422
454 $ cd issue4422
447 $ echo a > a.txt
455 $ echo a > a.txt
448 $ hg add a.txt
456 $ hg add a.txt
449 $ hg commit -m a
457 $ hg commit -m a
450 $ echo b > b.txt
458 $ echo b > b.txt
451 $ hg add b.txt
459 $ hg add b.txt
452 $ hg commit -m b
460 $ hg commit -m b
453 $ echo c > c.txt
461 $ echo c > c.txt
454 $ hg add c.txt
462 $ hg add c.txt
455 $ hg commit -m c
463 $ hg commit -m c
456
464
457 $ hg logt
465 $ hg logt
458 2:a1a953ffb4b0 c
466 2:a1a953ffb4b0 c
459 1:199b6bb90248 b
467 1:199b6bb90248 b
460 0:6c795aa153cb a
468 0:6c795aa153cb a
461
469
462 Setup the proper environment variable symbol for the platform, to be subbed
470 Setup the proper environment variable symbol for the platform, to be subbed
463 into the hook command.
471 into the hook command.
464 #if windows
472 #if windows
465 $ NODE="%HG_NODE%"
473 $ NODE="%HG_NODE%"
466 #else
474 #else
467 $ NODE="\$HG_NODE"
475 $ NODE="\$HG_NODE"
468 #endif
476 #endif
469 $ hg histedit 6c795aa153cb --config hooks.commit="echo commit $NODE" --commands - 2>&1 << EOF | fixbundle
477 $ hg histedit 6c795aa153cb --config hooks.commit="echo commit $NODE" --commands - 2>&1 << EOF | fixbundle
470 > pick 199b6bb90248 b
478 > pick 199b6bb90248 b
471 > fold a1a953ffb4b0 c
479 > fold a1a953ffb4b0 c
472 > pick 6c795aa153cb a
480 > pick 6c795aa153cb a
473 > EOF
481 > EOF
474 commit 9599899f62c05f4377548c32bf1c9f1a39634b0c
482 commit 9599899f62c05f4377548c32bf1c9f1a39634b0c
475
483
476 $ hg logt
484 $ hg logt
477 1:9599899f62c0 a
485 1:9599899f62c0 a
478 0:79b99e9c8e49 b
486 0:79b99e9c8e49 b
479
487
480 $ echo "foo" > amended.txt
488 $ echo "foo" > amended.txt
481 $ hg add amended.txt
489 $ hg add amended.txt
482 $ hg ci -q --config extensions.largefiles= --amend -I amended.txt
490 $ hg ci -q --config extensions.largefiles= --amend -I amended.txt
483
491
484 Test that folding multiple changes in a row doesn't show multiple
492 Test that folding multiple changes in a row doesn't show multiple
485 editors.
493 editors.
486
494
487 $ echo foo >> foo
495 $ echo foo >> foo
488 $ hg add foo
496 $ hg add foo
489 $ hg ci -m foo1
497 $ hg ci -m foo1
490 $ echo foo >> foo
498 $ echo foo >> foo
491 $ hg ci -m foo2
499 $ hg ci -m foo2
492 $ echo foo >> foo
500 $ echo foo >> foo
493 $ hg ci -m foo3
501 $ hg ci -m foo3
494 $ hg logt
502 $ hg logt
495 4:21679ff7675c foo3
503 4:21679ff7675c foo3
496 3:b7389cc4d66e foo2
504 3:b7389cc4d66e foo2
497 2:0e01aeef5fa8 foo1
505 2:0e01aeef5fa8 foo1
498 1:578c7455730c a
506 1:578c7455730c a
499 0:79b99e9c8e49 b
507 0:79b99e9c8e49 b
500 $ cat > "$TESTTMP/editor.sh" <<EOF
508 $ cat > "$TESTTMP/editor.sh" <<EOF
501 > echo ran editor >> "$TESTTMP/editorlog.txt"
509 > echo ran editor >> "$TESTTMP/editorlog.txt"
502 > cat \$1 >> "$TESTTMP/editorlog.txt"
510 > cat \$1 >> "$TESTTMP/editorlog.txt"
503 > echo END >> "$TESTTMP/editorlog.txt"
511 > echo END >> "$TESTTMP/editorlog.txt"
504 > echo merged foos > \$1
512 > echo merged foos > \$1
505 > EOF
513 > EOF
506 $ HGEDITOR="sh \"$TESTTMP/editor.sh\"" hg histedit 1 --commands - 2>&1 <<EOF | fixbundle
514 $ HGEDITOR="sh \"$TESTTMP/editor.sh\"" hg histedit 1 --commands - 2>&1 <<EOF | fixbundle
507 > pick 578c7455730c 1 a
515 > pick 578c7455730c 1 a
508 > pick 0e01aeef5fa8 2 foo1
516 > pick 0e01aeef5fa8 2 foo1
509 > fold b7389cc4d66e 3 foo2
517 > fold b7389cc4d66e 3 foo2
510 > fold 21679ff7675c 4 foo3
518 > fold 21679ff7675c 4 foo3
511 > EOF
519 > EOF
512 $ hg logt
520 $ hg logt
513 2:e8bedbda72c1 merged foos
521 2:e8bedbda72c1 merged foos
514 1:578c7455730c a
522 1:578c7455730c a
515 0:79b99e9c8e49 b
523 0:79b99e9c8e49 b
516 Editor should have run only once
524 Editor should have run only once
517 $ cat $TESTTMP/editorlog.txt
525 $ cat $TESTTMP/editorlog.txt
518 ran editor
526 ran editor
519 foo1
527 foo1
520 ***
528 ***
521 foo2
529 foo2
522 ***
530 ***
523 foo3
531 foo3
524
532
525
533
526
534
527 HG: Enter commit message. Lines beginning with 'HG:' are removed.
535 HG: Enter commit message. Lines beginning with 'HG:' are removed.
528 HG: Leave message empty to abort commit.
536 HG: Leave message empty to abort commit.
529 HG: --
537 HG: --
530 HG: user: test
538 HG: user: test
531 HG: branch 'default'
539 HG: branch 'default'
532 HG: added foo
540 HG: added foo
533 END
541 END
534
542
535 $ cd ..
543 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now