##// END OF EJS Templates
bundle2: use "else" instead of checking condition again
Martin von Zweigbergk -
r32881:f65786a0 default
parent child Browse files
Show More
@@ -1,1676 +1,1676 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description and date
39 # r, roll = like fold, but discard this commit's description and date
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 and date
61 # r, roll = like fold, but discard this commit's description and date
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. The date used
74 Edit the commit message to your liking, then close the editor. The date used
75 for the commit will be the later of the two commits' dates. For this example,
75 for the commit will be the later of the two commits' dates. For this example,
76 let's assume that the commit message was changed to ``Add beta and delta.``
76 let's assume that the commit message was changed to ``Add beta and delta.``
77 After histedit has run and had a chance to remove any old or temporary
77 After histedit has run and had a chance to remove any old or temporary
78 revisions it needed, the history looks 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. If there are uncommitted
100 histedit --continue`` to finish this step. If there are uncommitted
101 changes, you'll be prompted for a new commit message, but the default
101 changes, you'll be prompted for a new commit message, but the default
102 commit message will be the original message for the ``edit`` ed
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 revision, and the date of the original commit will be preserved.
104
104
105 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
106 message without changing the contents. It's a shortcut for doing
106 message without changing the contents. It's a shortcut for doing
107 ``edit`` immediately followed by `hg histedit --continue``.
107 ``edit`` immediately followed by `hg histedit --continue``.
108
108
109 If ``histedit`` encounters a conflict when moving a revision (while
109 If ``histedit`` encounters a conflict when moving a revision (while
110 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
111 ``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
112 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
113 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,
114 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
115 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
116 history.
116 history.
117
117
118 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
119 changes, such that we have the following history::
119 changes, such that we have the following history::
120
120
121 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
122 | Add theta
122 | Add theta
123 |
123 |
124 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 o 5 140988835471 2009-04-27 18:04 -0500 stefan
125 | Add eta
125 | Add eta
126 |
126 |
127 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 o 4 122930637314 2009-04-27 18:04 -0500 stefan
128 | Add zeta
128 | Add zeta
129 |
129 |
130 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 o 3 836302820282 2009-04-27 18:04 -0500 stefan
131 | Add epsilon
131 | Add epsilon
132 |
132 |
133 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
134 | Add beta and delta.
134 | Add beta and delta.
135 |
135 |
136 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
137 | Add gamma
137 | Add gamma
138 |
138 |
139 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
140 Add alpha
140 Add alpha
141
141
142 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
143 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
144 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
145 repo, you can add a ``--force`` option.
145 repo, you can add a ``--force`` option.
146
146
147 Config
147 Config
148 ------
148 ------
149
149
150 Histedit rule lines are truncated to 80 characters by default. You
150 Histedit rule lines are truncated to 80 characters by default. You
151 can customize this behavior by setting a different length in your
151 can customize this behavior by setting a different length in your
152 configuration file::
152 configuration file::
153
153
154 [histedit]
154 [histedit]
155 linelen = 120 # truncate rule lines at 120 characters
155 linelen = 120 # truncate rule lines at 120 characters
156
156
157 ``hg histedit`` attempts to automatically choose an appropriate base
157 ``hg histedit`` attempts to automatically choose an appropriate base
158 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
159 revset in your configuration file::
159 revset in your configuration file::
160
160
161 [histedit]
161 [histedit]
162 defaultrev = only(.) & draft()
162 defaultrev = only(.) & draft()
163
163
164 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.
165 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
166 the drop to be implicit for missing commits by adding::
166 the drop to be implicit for missing commits by adding::
167
167
168 [histedit]
168 [histedit]
169 dropmissing = True
169 dropmissing = True
170
170
171 By default, histedit will close the transaction after each action. For
171 By default, histedit will close the transaction after each action. For
172 performance purposes, you can configure histedit to use a single transaction
172 performance purposes, you can configure histedit to use a single transaction
173 across the entire histedit. WARNING: This setting introduces a significant risk
173 across the entire histedit. WARNING: This setting introduces a significant risk
174 of losing the work you've done in a histedit if the histedit aborts
174 of losing the work you've done in a histedit if the histedit aborts
175 unexpectedly::
175 unexpectedly::
176
176
177 [histedit]
177 [histedit]
178 singletransaction = True
178 singletransaction = True
179
179
180 """
180 """
181
181
182 from __future__ import absolute_import
182 from __future__ import absolute_import
183
183
184 import errno
184 import errno
185 import os
185 import os
186
186
187 from mercurial.i18n import _
187 from mercurial.i18n import _
188 from mercurial import (
188 from mercurial import (
189 bundle2,
189 bundle2,
190 cmdutil,
190 cmdutil,
191 context,
191 context,
192 copies,
192 copies,
193 destutil,
193 destutil,
194 discovery,
194 discovery,
195 error,
195 error,
196 exchange,
196 exchange,
197 extensions,
197 extensions,
198 hg,
198 hg,
199 lock,
199 lock,
200 merge as mergemod,
200 merge as mergemod,
201 mergeutil,
201 mergeutil,
202 node,
202 node,
203 obsolete,
203 obsolete,
204 registrar,
204 registrar,
205 repair,
205 repair,
206 scmutil,
206 scmutil,
207 util,
207 util,
208 )
208 )
209
209
210 pickle = util.pickle
210 pickle = util.pickle
211 release = lock.release
211 release = lock.release
212 cmdtable = {}
212 cmdtable = {}
213 command = registrar.command(cmdtable)
213 command = registrar.command(cmdtable)
214
214
215 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
215 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
216 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
216 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
217 # be specifying the version(s) of Mercurial they are tested with, or
217 # be specifying the version(s) of Mercurial they are tested with, or
218 # leave the attribute unspecified.
218 # leave the attribute unspecified.
219 testedwith = 'ships-with-hg-core'
219 testedwith = 'ships-with-hg-core'
220
220
221 actiontable = {}
221 actiontable = {}
222 primaryactions = set()
222 primaryactions = set()
223 secondaryactions = set()
223 secondaryactions = set()
224 tertiaryactions = set()
224 tertiaryactions = set()
225 internalactions = set()
225 internalactions = set()
226
226
227 def geteditcomment(ui, first, last):
227 def geteditcomment(ui, first, last):
228 """ construct the editor comment
228 """ construct the editor comment
229 The comment includes::
229 The comment includes::
230 - an intro
230 - an intro
231 - sorted primary commands
231 - sorted primary commands
232 - sorted short commands
232 - sorted short commands
233 - sorted long commands
233 - sorted long commands
234 - additional hints
234 - additional hints
235
235
236 Commands are only included once.
236 Commands are only included once.
237 """
237 """
238 intro = _("""Edit history between %s and %s
238 intro = _("""Edit history between %s and %s
239
239
240 Commits are listed from least to most recent
240 Commits are listed from least to most recent
241
241
242 You can reorder changesets by reordering the lines
242 You can reorder changesets by reordering the lines
243
243
244 Commands:
244 Commands:
245 """)
245 """)
246 actions = []
246 actions = []
247 def addverb(v):
247 def addverb(v):
248 a = actiontable[v]
248 a = actiontable[v]
249 lines = a.message.split("\n")
249 lines = a.message.split("\n")
250 if len(a.verbs):
250 if len(a.verbs):
251 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
251 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
252 actions.append(" %s = %s" % (v, lines[0]))
252 actions.append(" %s = %s" % (v, lines[0]))
253 actions.extend([' %s' for l in lines[1:]])
253 actions.extend([' %s' for l in lines[1:]])
254
254
255 for v in (
255 for v in (
256 sorted(primaryactions) +
256 sorted(primaryactions) +
257 sorted(secondaryactions) +
257 sorted(secondaryactions) +
258 sorted(tertiaryactions)
258 sorted(tertiaryactions)
259 ):
259 ):
260 addverb(v)
260 addverb(v)
261 actions.append('')
261 actions.append('')
262
262
263 hints = []
263 hints = []
264 if ui.configbool('histedit', 'dropmissing'):
264 if ui.configbool('histedit', 'dropmissing'):
265 hints.append("Deleting a changeset from the list "
265 hints.append("Deleting a changeset from the list "
266 "will DISCARD it from the edited history!")
266 "will DISCARD it from the edited history!")
267
267
268 lines = (intro % (first, last)).split('\n') + actions + hints
268 lines = (intro % (first, last)).split('\n') + actions + hints
269
269
270 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
270 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
271
271
272 class histeditstate(object):
272 class histeditstate(object):
273 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
273 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
274 topmost=None, replacements=None, lock=None, wlock=None):
274 topmost=None, replacements=None, lock=None, wlock=None):
275 self.repo = repo
275 self.repo = repo
276 self.actions = actions
276 self.actions = actions
277 self.keep = keep
277 self.keep = keep
278 self.topmost = topmost
278 self.topmost = topmost
279 self.parentctxnode = parentctxnode
279 self.parentctxnode = parentctxnode
280 self.lock = lock
280 self.lock = lock
281 self.wlock = wlock
281 self.wlock = wlock
282 self.backupfile = None
282 self.backupfile = None
283 self.tr = None
283 self.tr = None
284 if replacements is None:
284 if replacements is None:
285 self.replacements = []
285 self.replacements = []
286 else:
286 else:
287 self.replacements = replacements
287 self.replacements = replacements
288
288
289 def read(self):
289 def read(self):
290 """Load histedit state from disk and set fields appropriately."""
290 """Load histedit state from disk and set fields appropriately."""
291 try:
291 try:
292 state = self.repo.vfs.read('histedit-state')
292 state = self.repo.vfs.read('histedit-state')
293 except IOError as err:
293 except IOError as err:
294 if err.errno != errno.ENOENT:
294 if err.errno != errno.ENOENT:
295 raise
295 raise
296 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
296 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
297
297
298 if state.startswith('v1\n'):
298 if state.startswith('v1\n'):
299 data = self._load()
299 data = self._load()
300 parentctxnode, rules, keep, topmost, replacements, backupfile = data
300 parentctxnode, rules, keep, topmost, replacements, backupfile = data
301 else:
301 else:
302 data = pickle.loads(state)
302 data = pickle.loads(state)
303 parentctxnode, rules, keep, topmost, replacements = data
303 parentctxnode, rules, keep, topmost, replacements = data
304 backupfile = None
304 backupfile = None
305
305
306 self.parentctxnode = parentctxnode
306 self.parentctxnode = parentctxnode
307 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
307 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
308 actions = parserules(rules, self)
308 actions = parserules(rules, self)
309 self.actions = actions
309 self.actions = actions
310 self.keep = keep
310 self.keep = keep
311 self.topmost = topmost
311 self.topmost = topmost
312 self.replacements = replacements
312 self.replacements = replacements
313 self.backupfile = backupfile
313 self.backupfile = backupfile
314
314
315 def write(self, tr=None):
315 def write(self, tr=None):
316 if tr:
316 if tr:
317 tr.addfilegenerator('histedit-state', ('histedit-state',),
317 tr.addfilegenerator('histedit-state', ('histedit-state',),
318 self._write, location='plain')
318 self._write, location='plain')
319 else:
319 else:
320 with self.repo.vfs("histedit-state", "w") as f:
320 with self.repo.vfs("histedit-state", "w") as f:
321 self._write(f)
321 self._write(f)
322
322
323 def _write(self, fp):
323 def _write(self, fp):
324 fp.write('v1\n')
324 fp.write('v1\n')
325 fp.write('%s\n' % node.hex(self.parentctxnode))
325 fp.write('%s\n' % node.hex(self.parentctxnode))
326 fp.write('%s\n' % node.hex(self.topmost))
326 fp.write('%s\n' % node.hex(self.topmost))
327 fp.write('%s\n' % self.keep)
327 fp.write('%s\n' % self.keep)
328 fp.write('%d\n' % len(self.actions))
328 fp.write('%d\n' % len(self.actions))
329 for action in self.actions:
329 for action in self.actions:
330 fp.write('%s\n' % action.tostate())
330 fp.write('%s\n' % action.tostate())
331 fp.write('%d\n' % len(self.replacements))
331 fp.write('%d\n' % len(self.replacements))
332 for replacement in self.replacements:
332 for replacement in self.replacements:
333 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
333 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
334 for r in replacement[1])))
334 for r in replacement[1])))
335 backupfile = self.backupfile
335 backupfile = self.backupfile
336 if not backupfile:
336 if not backupfile:
337 backupfile = ''
337 backupfile = ''
338 fp.write('%s\n' % backupfile)
338 fp.write('%s\n' % backupfile)
339
339
340 def _load(self):
340 def _load(self):
341 fp = self.repo.vfs('histedit-state', 'r')
341 fp = self.repo.vfs('histedit-state', 'r')
342 lines = [l[:-1] for l in fp.readlines()]
342 lines = [l[:-1] for l in fp.readlines()]
343
343
344 index = 0
344 index = 0
345 lines[index] # version number
345 lines[index] # version number
346 index += 1
346 index += 1
347
347
348 parentctxnode = node.bin(lines[index])
348 parentctxnode = node.bin(lines[index])
349 index += 1
349 index += 1
350
350
351 topmost = node.bin(lines[index])
351 topmost = node.bin(lines[index])
352 index += 1
352 index += 1
353
353
354 keep = lines[index] == 'True'
354 keep = lines[index] == 'True'
355 index += 1
355 index += 1
356
356
357 # Rules
357 # Rules
358 rules = []
358 rules = []
359 rulelen = int(lines[index])
359 rulelen = int(lines[index])
360 index += 1
360 index += 1
361 for i in xrange(rulelen):
361 for i in xrange(rulelen):
362 ruleaction = lines[index]
362 ruleaction = lines[index]
363 index += 1
363 index += 1
364 rule = lines[index]
364 rule = lines[index]
365 index += 1
365 index += 1
366 rules.append((ruleaction, rule))
366 rules.append((ruleaction, rule))
367
367
368 # Replacements
368 # Replacements
369 replacements = []
369 replacements = []
370 replacementlen = int(lines[index])
370 replacementlen = int(lines[index])
371 index += 1
371 index += 1
372 for i in xrange(replacementlen):
372 for i in xrange(replacementlen):
373 replacement = lines[index]
373 replacement = lines[index]
374 original = node.bin(replacement[:40])
374 original = node.bin(replacement[:40])
375 succ = [node.bin(replacement[i:i + 40]) for i in
375 succ = [node.bin(replacement[i:i + 40]) for i in
376 range(40, len(replacement), 40)]
376 range(40, len(replacement), 40)]
377 replacements.append((original, succ))
377 replacements.append((original, succ))
378 index += 1
378 index += 1
379
379
380 backupfile = lines[index]
380 backupfile = lines[index]
381 index += 1
381 index += 1
382
382
383 fp.close()
383 fp.close()
384
384
385 return parentctxnode, rules, keep, topmost, replacements, backupfile
385 return parentctxnode, rules, keep, topmost, replacements, backupfile
386
386
387 def clear(self):
387 def clear(self):
388 if self.inprogress():
388 if self.inprogress():
389 self.repo.vfs.unlink('histedit-state')
389 self.repo.vfs.unlink('histedit-state')
390
390
391 def inprogress(self):
391 def inprogress(self):
392 return self.repo.vfs.exists('histedit-state')
392 return self.repo.vfs.exists('histedit-state')
393
393
394
394
395 class histeditaction(object):
395 class histeditaction(object):
396 def __init__(self, state, node):
396 def __init__(self, state, node):
397 self.state = state
397 self.state = state
398 self.repo = state.repo
398 self.repo = state.repo
399 self.node = node
399 self.node = node
400
400
401 @classmethod
401 @classmethod
402 def fromrule(cls, state, rule):
402 def fromrule(cls, state, rule):
403 """Parses the given rule, returning an instance of the histeditaction.
403 """Parses the given rule, returning an instance of the histeditaction.
404 """
404 """
405 rulehash = rule.strip().split(' ', 1)[0]
405 rulehash = rule.strip().split(' ', 1)[0]
406 try:
406 try:
407 rev = node.bin(rulehash)
407 rev = node.bin(rulehash)
408 except TypeError:
408 except TypeError:
409 raise error.ParseError("invalid changeset %s" % rulehash)
409 raise error.ParseError("invalid changeset %s" % rulehash)
410 return cls(state, rev)
410 return cls(state, rev)
411
411
412 def verify(self, prev, expected, seen):
412 def verify(self, prev, expected, seen):
413 """ Verifies semantic correctness of the rule"""
413 """ Verifies semantic correctness of the rule"""
414 repo = self.repo
414 repo = self.repo
415 ha = node.hex(self.node)
415 ha = node.hex(self.node)
416 try:
416 try:
417 self.node = repo[ha].node()
417 self.node = repo[ha].node()
418 except error.RepoError:
418 except error.RepoError:
419 raise error.ParseError(_('unknown changeset %s listed')
419 raise error.ParseError(_('unknown changeset %s listed')
420 % ha[:12])
420 % ha[:12])
421 if self.node is not None:
421 if self.node is not None:
422 self._verifynodeconstraints(prev, expected, seen)
422 self._verifynodeconstraints(prev, expected, seen)
423
423
424 def _verifynodeconstraints(self, prev, expected, seen):
424 def _verifynodeconstraints(self, prev, expected, seen):
425 # by default command need a node in the edited list
425 # by default command need a node in the edited list
426 if self.node not in expected:
426 if self.node not in expected:
427 raise error.ParseError(_('%s "%s" changeset was not a candidate')
427 raise error.ParseError(_('%s "%s" changeset was not a candidate')
428 % (self.verb, node.short(self.node)),
428 % (self.verb, node.short(self.node)),
429 hint=_('only use listed changesets'))
429 hint=_('only use listed changesets'))
430 # and only one command per node
430 # and only one command per node
431 if self.node in seen:
431 if self.node in seen:
432 raise error.ParseError(_('duplicated command for changeset %s') %
432 raise error.ParseError(_('duplicated command for changeset %s') %
433 node.short(self.node))
433 node.short(self.node))
434
434
435 def torule(self):
435 def torule(self):
436 """build a histedit rule line for an action
436 """build a histedit rule line for an action
437
437
438 by default lines are in the form:
438 by default lines are in the form:
439 <hash> <rev> <summary>
439 <hash> <rev> <summary>
440 """
440 """
441 ctx = self.repo[self.node]
441 ctx = self.repo[self.node]
442 summary = _getsummary(ctx)
442 summary = _getsummary(ctx)
443 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
443 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
444 # trim to 75 columns by default so it's not stupidly wide in my editor
444 # trim to 75 columns by default so it's not stupidly wide in my editor
445 # (the 5 more are left for verb)
445 # (the 5 more are left for verb)
446 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
446 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
447 maxlen = max(maxlen, 22) # avoid truncating hash
447 maxlen = max(maxlen, 22) # avoid truncating hash
448 return util.ellipsis(line, maxlen)
448 return util.ellipsis(line, maxlen)
449
449
450 def tostate(self):
450 def tostate(self):
451 """Print an action in format used by histedit state files
451 """Print an action in format used by histedit state files
452 (the first line is a verb, the remainder is the second)
452 (the first line is a verb, the remainder is the second)
453 """
453 """
454 return "%s\n%s" % (self.verb, node.hex(self.node))
454 return "%s\n%s" % (self.verb, node.hex(self.node))
455
455
456 def run(self):
456 def run(self):
457 """Runs the action. The default behavior is simply apply the action's
457 """Runs the action. The default behavior is simply apply the action's
458 rulectx onto the current parentctx."""
458 rulectx onto the current parentctx."""
459 self.applychange()
459 self.applychange()
460 self.continuedirty()
460 self.continuedirty()
461 return self.continueclean()
461 return self.continueclean()
462
462
463 def applychange(self):
463 def applychange(self):
464 """Applies the changes from this action's rulectx onto the current
464 """Applies the changes from this action's rulectx onto the current
465 parentctx, but does not commit them."""
465 parentctx, but does not commit them."""
466 repo = self.repo
466 repo = self.repo
467 rulectx = repo[self.node]
467 rulectx = repo[self.node]
468 repo.ui.pushbuffer(error=True, labeled=True)
468 repo.ui.pushbuffer(error=True, labeled=True)
469 hg.update(repo, self.state.parentctxnode, quietempty=True)
469 hg.update(repo, self.state.parentctxnode, quietempty=True)
470 stats = applychanges(repo.ui, repo, rulectx, {})
470 stats = applychanges(repo.ui, repo, rulectx, {})
471 if stats and stats[3] > 0:
471 if stats and stats[3] > 0:
472 buf = repo.ui.popbuffer()
472 buf = repo.ui.popbuffer()
473 repo.ui.write(*buf)
473 repo.ui.write(*buf)
474 raise error.InterventionRequired(
474 raise error.InterventionRequired(
475 _('Fix up the change (%s %s)') %
475 _('Fix up the change (%s %s)') %
476 (self.verb, node.short(self.node)),
476 (self.verb, node.short(self.node)),
477 hint=_('hg histedit --continue to resume'))
477 hint=_('hg histedit --continue to resume'))
478 else:
478 else:
479 repo.ui.popbuffer()
479 repo.ui.popbuffer()
480
480
481 def continuedirty(self):
481 def continuedirty(self):
482 """Continues the action when changes have been applied to the working
482 """Continues the action when changes have been applied to the working
483 copy. The default behavior is to commit the dirty changes."""
483 copy. The default behavior is to commit the dirty changes."""
484 repo = self.repo
484 repo = self.repo
485 rulectx = repo[self.node]
485 rulectx = repo[self.node]
486
486
487 editor = self.commiteditor()
487 editor = self.commiteditor()
488 commit = commitfuncfor(repo, rulectx)
488 commit = commitfuncfor(repo, rulectx)
489
489
490 commit(text=rulectx.description(), user=rulectx.user(),
490 commit(text=rulectx.description(), user=rulectx.user(),
491 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
491 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
492
492
493 def commiteditor(self):
493 def commiteditor(self):
494 """The editor to be used to edit the commit message."""
494 """The editor to be used to edit the commit message."""
495 return False
495 return False
496
496
497 def continueclean(self):
497 def continueclean(self):
498 """Continues the action when the working copy is clean. The default
498 """Continues the action when the working copy is clean. The default
499 behavior is to accept the current commit as the new version of the
499 behavior is to accept the current commit as the new version of the
500 rulectx."""
500 rulectx."""
501 ctx = self.repo['.']
501 ctx = self.repo['.']
502 if ctx.node() == self.state.parentctxnode:
502 if ctx.node() == self.state.parentctxnode:
503 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
503 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
504 node.short(self.node))
504 node.short(self.node))
505 return ctx, [(self.node, tuple())]
505 return ctx, [(self.node, tuple())]
506 if ctx.node() == self.node:
506 if ctx.node() == self.node:
507 # Nothing changed
507 # Nothing changed
508 return ctx, []
508 return ctx, []
509 return ctx, [(self.node, (ctx.node(),))]
509 return ctx, [(self.node, (ctx.node(),))]
510
510
511 def commitfuncfor(repo, src):
511 def commitfuncfor(repo, src):
512 """Build a commit function for the replacement of <src>
512 """Build a commit function for the replacement of <src>
513
513
514 This function ensure we apply the same treatment to all changesets.
514 This function ensure we apply the same treatment to all changesets.
515
515
516 - Add a 'histedit_source' entry in extra.
516 - Add a 'histedit_source' entry in extra.
517
517
518 Note that fold has its own separated logic because its handling is a bit
518 Note that fold has its own separated logic because its handling is a bit
519 different and not easily factored out of the fold method.
519 different and not easily factored out of the fold method.
520 """
520 """
521 phasemin = src.phase()
521 phasemin = src.phase()
522 def commitfunc(**kwargs):
522 def commitfunc(**kwargs):
523 overrides = {('phases', 'new-commit'): phasemin}
523 overrides = {('phases', 'new-commit'): phasemin}
524 with repo.ui.configoverride(overrides, 'histedit'):
524 with repo.ui.configoverride(overrides, 'histedit'):
525 extra = kwargs.get('extra', {}).copy()
525 extra = kwargs.get('extra', {}).copy()
526 extra['histedit_source'] = src.hex()
526 extra['histedit_source'] = src.hex()
527 kwargs['extra'] = extra
527 kwargs['extra'] = extra
528 return repo.commit(**kwargs)
528 return repo.commit(**kwargs)
529 return commitfunc
529 return commitfunc
530
530
531 def applychanges(ui, repo, ctx, opts):
531 def applychanges(ui, repo, ctx, opts):
532 """Merge changeset from ctx (only) in the current working directory"""
532 """Merge changeset from ctx (only) in the current working directory"""
533 wcpar = repo.dirstate.parents()[0]
533 wcpar = repo.dirstate.parents()[0]
534 if ctx.p1().node() == wcpar:
534 if ctx.p1().node() == wcpar:
535 # edits are "in place" we do not need to make any merge,
535 # edits are "in place" we do not need to make any merge,
536 # just applies changes on parent for editing
536 # just applies changes on parent for editing
537 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
537 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
538 stats = None
538 stats = None
539 else:
539 else:
540 try:
540 try:
541 # ui.forcemerge is an internal variable, do not document
541 # ui.forcemerge is an internal variable, do not document
542 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
542 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
543 'histedit')
543 'histedit')
544 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
544 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
545 finally:
545 finally:
546 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
546 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
547 return stats
547 return stats
548
548
549 def collapse(repo, first, last, commitopts, skipprompt=False):
549 def collapse(repo, first, last, commitopts, skipprompt=False):
550 """collapse the set of revisions from first to last as new one.
550 """collapse the set of revisions from first to last as new one.
551
551
552 Expected commit options are:
552 Expected commit options are:
553 - message
553 - message
554 - date
554 - date
555 - username
555 - username
556 Commit message is edited in all cases.
556 Commit message is edited in all cases.
557
557
558 This function works in memory."""
558 This function works in memory."""
559 ctxs = list(repo.set('%d::%d', first, last))
559 ctxs = list(repo.set('%d::%d', first, last))
560 if not ctxs:
560 if not ctxs:
561 return None
561 return None
562 for c in ctxs:
562 for c in ctxs:
563 if not c.mutable():
563 if not c.mutable():
564 raise error.ParseError(
564 raise error.ParseError(
565 _("cannot fold into public change %s") % node.short(c.node()))
565 _("cannot fold into public change %s") % node.short(c.node()))
566 base = first.parents()[0]
566 base = first.parents()[0]
567
567
568 # commit a new version of the old changeset, including the update
568 # commit a new version of the old changeset, including the update
569 # collect all files which might be affected
569 # collect all files which might be affected
570 files = set()
570 files = set()
571 for ctx in ctxs:
571 for ctx in ctxs:
572 files.update(ctx.files())
572 files.update(ctx.files())
573
573
574 # Recompute copies (avoid recording a -> b -> a)
574 # Recompute copies (avoid recording a -> b -> a)
575 copied = copies.pathcopies(base, last)
575 copied = copies.pathcopies(base, last)
576
576
577 # prune files which were reverted by the updates
577 # prune files which were reverted by the updates
578 files = [f for f in files if not cmdutil.samefile(f, last, base)]
578 files = [f for f in files if not cmdutil.samefile(f, last, base)]
579 # commit version of these files as defined by head
579 # commit version of these files as defined by head
580 headmf = last.manifest()
580 headmf = last.manifest()
581 def filectxfn(repo, ctx, path):
581 def filectxfn(repo, ctx, path):
582 if path in headmf:
582 if path in headmf:
583 fctx = last[path]
583 fctx = last[path]
584 flags = fctx.flags()
584 flags = fctx.flags()
585 mctx = context.memfilectx(repo,
585 mctx = context.memfilectx(repo,
586 fctx.path(), fctx.data(),
586 fctx.path(), fctx.data(),
587 islink='l' in flags,
587 islink='l' in flags,
588 isexec='x' in flags,
588 isexec='x' in flags,
589 copied=copied.get(path))
589 copied=copied.get(path))
590 return mctx
590 return mctx
591 return None
591 return None
592
592
593 if commitopts.get('message'):
593 if commitopts.get('message'):
594 message = commitopts['message']
594 message = commitopts['message']
595 else:
595 else:
596 message = first.description()
596 message = first.description()
597 user = commitopts.get('user')
597 user = commitopts.get('user')
598 date = commitopts.get('date')
598 date = commitopts.get('date')
599 extra = commitopts.get('extra')
599 extra = commitopts.get('extra')
600
600
601 parents = (first.p1().node(), first.p2().node())
601 parents = (first.p1().node(), first.p2().node())
602 editor = None
602 editor = None
603 if not skipprompt:
603 if not skipprompt:
604 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
604 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
605 new = context.memctx(repo,
605 new = context.memctx(repo,
606 parents=parents,
606 parents=parents,
607 text=message,
607 text=message,
608 files=files,
608 files=files,
609 filectxfn=filectxfn,
609 filectxfn=filectxfn,
610 user=user,
610 user=user,
611 date=date,
611 date=date,
612 extra=extra,
612 extra=extra,
613 editor=editor)
613 editor=editor)
614 return repo.commitctx(new)
614 return repo.commitctx(new)
615
615
616 def _isdirtywc(repo):
616 def _isdirtywc(repo):
617 return repo[None].dirty(missing=True)
617 return repo[None].dirty(missing=True)
618
618
619 def abortdirty():
619 def abortdirty():
620 raise error.Abort(_('working copy has pending changes'),
620 raise error.Abort(_('working copy has pending changes'),
621 hint=_('amend, commit, or revert them and run histedit '
621 hint=_('amend, commit, or revert them and run histedit '
622 '--continue, or abort with histedit --abort'))
622 '--continue, or abort with histedit --abort'))
623
623
624 def action(verbs, message, priority=False, internal=False):
624 def action(verbs, message, priority=False, internal=False):
625 def wrap(cls):
625 def wrap(cls):
626 assert not priority or not internal
626 assert not priority or not internal
627 verb = verbs[0]
627 verb = verbs[0]
628 if priority:
628 if priority:
629 primaryactions.add(verb)
629 primaryactions.add(verb)
630 elif internal:
630 elif internal:
631 internalactions.add(verb)
631 internalactions.add(verb)
632 elif len(verbs) > 1:
632 elif len(verbs) > 1:
633 secondaryactions.add(verb)
633 secondaryactions.add(verb)
634 else:
634 else:
635 tertiaryactions.add(verb)
635 tertiaryactions.add(verb)
636
636
637 cls.verb = verb
637 cls.verb = verb
638 cls.verbs = verbs
638 cls.verbs = verbs
639 cls.message = message
639 cls.message = message
640 for verb in verbs:
640 for verb in verbs:
641 actiontable[verb] = cls
641 actiontable[verb] = cls
642 return cls
642 return cls
643 return wrap
643 return wrap
644
644
645 @action(['pick', 'p'],
645 @action(['pick', 'p'],
646 _('use commit'),
646 _('use commit'),
647 priority=True)
647 priority=True)
648 class pick(histeditaction):
648 class pick(histeditaction):
649 def run(self):
649 def run(self):
650 rulectx = self.repo[self.node]
650 rulectx = self.repo[self.node]
651 if rulectx.parents()[0].node() == self.state.parentctxnode:
651 if rulectx.parents()[0].node() == self.state.parentctxnode:
652 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
652 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
653 return rulectx, []
653 return rulectx, []
654
654
655 return super(pick, self).run()
655 return super(pick, self).run()
656
656
657 @action(['edit', 'e'],
657 @action(['edit', 'e'],
658 _('use commit, but stop for amending'),
658 _('use commit, but stop for amending'),
659 priority=True)
659 priority=True)
660 class edit(histeditaction):
660 class edit(histeditaction):
661 def run(self):
661 def run(self):
662 repo = self.repo
662 repo = self.repo
663 rulectx = repo[self.node]
663 rulectx = repo[self.node]
664 hg.update(repo, self.state.parentctxnode, quietempty=True)
664 hg.update(repo, self.state.parentctxnode, quietempty=True)
665 applychanges(repo.ui, repo, rulectx, {})
665 applychanges(repo.ui, repo, rulectx, {})
666 raise error.InterventionRequired(
666 raise error.InterventionRequired(
667 _('Editing (%s), you may commit or record as needed now.')
667 _('Editing (%s), you may commit or record as needed now.')
668 % node.short(self.node),
668 % node.short(self.node),
669 hint=_('hg histedit --continue to resume'))
669 hint=_('hg histedit --continue to resume'))
670
670
671 def commiteditor(self):
671 def commiteditor(self):
672 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
672 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
673
673
674 @action(['fold', 'f'],
674 @action(['fold', 'f'],
675 _('use commit, but combine it with the one above'))
675 _('use commit, but combine it with the one above'))
676 class fold(histeditaction):
676 class fold(histeditaction):
677 def verify(self, prev, expected, seen):
677 def verify(self, prev, expected, seen):
678 """ Verifies semantic correctness of the fold rule"""
678 """ Verifies semantic correctness of the fold rule"""
679 super(fold, self).verify(prev, expected, seen)
679 super(fold, self).verify(prev, expected, seen)
680 repo = self.repo
680 repo = self.repo
681 if not prev:
681 if not prev:
682 c = repo[self.node].parents()[0]
682 c = repo[self.node].parents()[0]
683 elif not prev.verb in ('pick', 'base'):
683 elif not prev.verb in ('pick', 'base'):
684 return
684 return
685 else:
685 else:
686 c = repo[prev.node]
686 c = repo[prev.node]
687 if not c.mutable():
687 if not c.mutable():
688 raise error.ParseError(
688 raise error.ParseError(
689 _("cannot fold into public change %s") % node.short(c.node()))
689 _("cannot fold into public change %s") % node.short(c.node()))
690
690
691
691
692 def continuedirty(self):
692 def continuedirty(self):
693 repo = self.repo
693 repo = self.repo
694 rulectx = repo[self.node]
694 rulectx = repo[self.node]
695
695
696 commit = commitfuncfor(repo, rulectx)
696 commit = commitfuncfor(repo, rulectx)
697 commit(text='fold-temp-revision %s' % node.short(self.node),
697 commit(text='fold-temp-revision %s' % node.short(self.node),
698 user=rulectx.user(), date=rulectx.date(),
698 user=rulectx.user(), date=rulectx.date(),
699 extra=rulectx.extra())
699 extra=rulectx.extra())
700
700
701 def continueclean(self):
701 def continueclean(self):
702 repo = self.repo
702 repo = self.repo
703 ctx = repo['.']
703 ctx = repo['.']
704 rulectx = repo[self.node]
704 rulectx = repo[self.node]
705 parentctxnode = self.state.parentctxnode
705 parentctxnode = self.state.parentctxnode
706 if ctx.node() == parentctxnode:
706 if ctx.node() == parentctxnode:
707 repo.ui.warn(_('%s: empty changeset\n') %
707 repo.ui.warn(_('%s: empty changeset\n') %
708 node.short(self.node))
708 node.short(self.node))
709 return ctx, [(self.node, (parentctxnode,))]
709 return ctx, [(self.node, (parentctxnode,))]
710
710
711 parentctx = repo[parentctxnode]
711 parentctx = repo[parentctxnode]
712 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
712 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
713 parentctx))
713 parentctx))
714 if not newcommits:
714 if not newcommits:
715 repo.ui.warn(_('%s: cannot fold - working copy is not a '
715 repo.ui.warn(_('%s: cannot fold - working copy is not a '
716 'descendant of previous commit %s\n') %
716 'descendant of previous commit %s\n') %
717 (node.short(self.node), node.short(parentctxnode)))
717 (node.short(self.node), node.short(parentctxnode)))
718 return ctx, [(self.node, (ctx.node(),))]
718 return ctx, [(self.node, (ctx.node(),))]
719
719
720 middlecommits = newcommits.copy()
720 middlecommits = newcommits.copy()
721 middlecommits.discard(ctx.node())
721 middlecommits.discard(ctx.node())
722
722
723 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
723 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
724 middlecommits)
724 middlecommits)
725
725
726 def skipprompt(self):
726 def skipprompt(self):
727 """Returns true if the rule should skip the message editor.
727 """Returns true if the rule should skip the message editor.
728
728
729 For example, 'fold' wants to show an editor, but 'rollup'
729 For example, 'fold' wants to show an editor, but 'rollup'
730 doesn't want to.
730 doesn't want to.
731 """
731 """
732 return False
732 return False
733
733
734 def mergedescs(self):
734 def mergedescs(self):
735 """Returns true if the rule should merge messages of multiple changes.
735 """Returns true if the rule should merge messages of multiple changes.
736
736
737 This exists mainly so that 'rollup' rules can be a subclass of
737 This exists mainly so that 'rollup' rules can be a subclass of
738 'fold'.
738 'fold'.
739 """
739 """
740 return True
740 return True
741
741
742 def firstdate(self):
742 def firstdate(self):
743 """Returns true if the rule should preserve the date of the first
743 """Returns true if the rule should preserve the date of the first
744 change.
744 change.
745
745
746 This exists mainly so that 'rollup' rules can be a subclass of
746 This exists mainly so that 'rollup' rules can be a subclass of
747 'fold'.
747 'fold'.
748 """
748 """
749 return False
749 return False
750
750
751 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
751 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
752 parent = ctx.parents()[0].node()
752 parent = ctx.parents()[0].node()
753 repo.ui.pushbuffer()
753 repo.ui.pushbuffer()
754 hg.update(repo, parent)
754 hg.update(repo, parent)
755 repo.ui.popbuffer()
755 repo.ui.popbuffer()
756 ### prepare new commit data
756 ### prepare new commit data
757 commitopts = {}
757 commitopts = {}
758 commitopts['user'] = ctx.user()
758 commitopts['user'] = ctx.user()
759 # commit message
759 # commit message
760 if not self.mergedescs():
760 if not self.mergedescs():
761 newmessage = ctx.description()
761 newmessage = ctx.description()
762 else:
762 else:
763 newmessage = '\n***\n'.join(
763 newmessage = '\n***\n'.join(
764 [ctx.description()] +
764 [ctx.description()] +
765 [repo[r].description() for r in internalchanges] +
765 [repo[r].description() for r in internalchanges] +
766 [oldctx.description()]) + '\n'
766 [oldctx.description()]) + '\n'
767 commitopts['message'] = newmessage
767 commitopts['message'] = newmessage
768 # date
768 # date
769 if self.firstdate():
769 if self.firstdate():
770 commitopts['date'] = ctx.date()
770 commitopts['date'] = ctx.date()
771 else:
771 else:
772 commitopts['date'] = max(ctx.date(), oldctx.date())
772 commitopts['date'] = max(ctx.date(), oldctx.date())
773 extra = ctx.extra().copy()
773 extra = ctx.extra().copy()
774 # histedit_source
774 # histedit_source
775 # note: ctx is likely a temporary commit but that the best we can do
775 # note: ctx is likely a temporary commit but that the best we can do
776 # here. This is sufficient to solve issue3681 anyway.
776 # here. This is sufficient to solve issue3681 anyway.
777 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
777 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
778 commitopts['extra'] = extra
778 commitopts['extra'] = extra
779 phasemin = max(ctx.phase(), oldctx.phase())
779 phasemin = max(ctx.phase(), oldctx.phase())
780 overrides = {('phases', 'new-commit'): phasemin}
780 overrides = {('phases', 'new-commit'): phasemin}
781 with repo.ui.configoverride(overrides, 'histedit'):
781 with repo.ui.configoverride(overrides, 'histedit'):
782 n = collapse(repo, ctx, repo[newnode], commitopts,
782 n = collapse(repo, ctx, repo[newnode], commitopts,
783 skipprompt=self.skipprompt())
783 skipprompt=self.skipprompt())
784 if n is None:
784 if n is None:
785 return ctx, []
785 return ctx, []
786 repo.ui.pushbuffer()
786 repo.ui.pushbuffer()
787 hg.update(repo, n)
787 hg.update(repo, n)
788 repo.ui.popbuffer()
788 repo.ui.popbuffer()
789 replacements = [(oldctx.node(), (newnode,)),
789 replacements = [(oldctx.node(), (newnode,)),
790 (ctx.node(), (n,)),
790 (ctx.node(), (n,)),
791 (newnode, (n,)),
791 (newnode, (n,)),
792 ]
792 ]
793 for ich in internalchanges:
793 for ich in internalchanges:
794 replacements.append((ich, (n,)))
794 replacements.append((ich, (n,)))
795 return repo[n], replacements
795 return repo[n], replacements
796
796
797 class base(histeditaction):
797 class base(histeditaction):
798
798
799 def run(self):
799 def run(self):
800 if self.repo['.'].node() != self.node:
800 if self.repo['.'].node() != self.node:
801 mergemod.update(self.repo, self.node, False, True)
801 mergemod.update(self.repo, self.node, False, True)
802 # branchmerge, force)
802 # branchmerge, force)
803 return self.continueclean()
803 return self.continueclean()
804
804
805 def continuedirty(self):
805 def continuedirty(self):
806 abortdirty()
806 abortdirty()
807
807
808 def continueclean(self):
808 def continueclean(self):
809 basectx = self.repo['.']
809 basectx = self.repo['.']
810 return basectx, []
810 return basectx, []
811
811
812 def _verifynodeconstraints(self, prev, expected, seen):
812 def _verifynodeconstraints(self, prev, expected, seen):
813 # base can only be use with a node not in the edited set
813 # base can only be use with a node not in the edited set
814 if self.node in expected:
814 if self.node in expected:
815 msg = _('%s "%s" changeset was an edited list candidate')
815 msg = _('%s "%s" changeset was an edited list candidate')
816 raise error.ParseError(
816 raise error.ParseError(
817 msg % (self.verb, node.short(self.node)),
817 msg % (self.verb, node.short(self.node)),
818 hint=_('base must only use unlisted changesets'))
818 hint=_('base must only use unlisted changesets'))
819
819
820 @action(['_multifold'],
820 @action(['_multifold'],
821 _(
821 _(
822 """fold subclass used for when multiple folds happen in a row
822 """fold subclass used for when multiple folds happen in a row
823
823
824 We only want to fire the editor for the folded message once when
824 We only want to fire the editor for the folded message once when
825 (say) four changes are folded down into a single change. This is
825 (say) four changes are folded down into a single change. This is
826 similar to rollup, but we should preserve both messages so that
826 similar to rollup, but we should preserve both messages so that
827 when the last fold operation runs we can show the user all the
827 when the last fold operation runs we can show the user all the
828 commit messages in their editor.
828 commit messages in their editor.
829 """),
829 """),
830 internal=True)
830 internal=True)
831 class _multifold(fold):
831 class _multifold(fold):
832 def skipprompt(self):
832 def skipprompt(self):
833 return True
833 return True
834
834
835 @action(["roll", "r"],
835 @action(["roll", "r"],
836 _("like fold, but discard this commit's description and date"))
836 _("like fold, but discard this commit's description and date"))
837 class rollup(fold):
837 class rollup(fold):
838 def mergedescs(self):
838 def mergedescs(self):
839 return False
839 return False
840
840
841 def skipprompt(self):
841 def skipprompt(self):
842 return True
842 return True
843
843
844 def firstdate(self):
844 def firstdate(self):
845 return True
845 return True
846
846
847 @action(["drop", "d"],
847 @action(["drop", "d"],
848 _('remove commit from history'))
848 _('remove commit from history'))
849 class drop(histeditaction):
849 class drop(histeditaction):
850 def run(self):
850 def run(self):
851 parentctx = self.repo[self.state.parentctxnode]
851 parentctx = self.repo[self.state.parentctxnode]
852 return parentctx, [(self.node, tuple())]
852 return parentctx, [(self.node, tuple())]
853
853
854 @action(["mess", "m"],
854 @action(["mess", "m"],
855 _('edit commit message without changing commit content'),
855 _('edit commit message without changing commit content'),
856 priority=True)
856 priority=True)
857 class message(histeditaction):
857 class message(histeditaction):
858 def commiteditor(self):
858 def commiteditor(self):
859 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
859 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
860
860
861 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
861 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
862 """utility function to find the first outgoing changeset
862 """utility function to find the first outgoing changeset
863
863
864 Used by initialization code"""
864 Used by initialization code"""
865 if opts is None:
865 if opts is None:
866 opts = {}
866 opts = {}
867 dest = ui.expandpath(remote or 'default-push', remote or 'default')
867 dest = ui.expandpath(remote or 'default-push', remote or 'default')
868 dest, revs = hg.parseurl(dest, None)[:2]
868 dest, revs = hg.parseurl(dest, None)[:2]
869 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
869 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
870
870
871 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
871 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
872 other = hg.peer(repo, opts, dest)
872 other = hg.peer(repo, opts, dest)
873
873
874 if revs:
874 if revs:
875 revs = [repo.lookup(rev) for rev in revs]
875 revs = [repo.lookup(rev) for rev in revs]
876
876
877 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
877 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
878 if not outgoing.missing:
878 if not outgoing.missing:
879 raise error.Abort(_('no outgoing ancestors'))
879 raise error.Abort(_('no outgoing ancestors'))
880 roots = list(repo.revs("roots(%ln)", outgoing.missing))
880 roots = list(repo.revs("roots(%ln)", outgoing.missing))
881 if 1 < len(roots):
881 if 1 < len(roots):
882 msg = _('there are ambiguous outgoing revisions')
882 msg = _('there are ambiguous outgoing revisions')
883 hint = _("see 'hg help histedit' for more detail")
883 hint = _("see 'hg help histedit' for more detail")
884 raise error.Abort(msg, hint=hint)
884 raise error.Abort(msg, hint=hint)
885 return repo.lookup(roots[0])
885 return repo.lookup(roots[0])
886
886
887
887
888 @command('histedit',
888 @command('histedit',
889 [('', 'commands', '',
889 [('', 'commands', '',
890 _('read history edits from the specified file'), _('FILE')),
890 _('read history edits from the specified file'), _('FILE')),
891 ('c', 'continue', False, _('continue an edit already in progress')),
891 ('c', 'continue', False, _('continue an edit already in progress')),
892 ('', 'edit-plan', False, _('edit remaining actions list')),
892 ('', 'edit-plan', False, _('edit remaining actions list')),
893 ('k', 'keep', False,
893 ('k', 'keep', False,
894 _("don't strip old nodes after edit is complete")),
894 _("don't strip old nodes after edit is complete")),
895 ('', 'abort', False, _('abort an edit in progress')),
895 ('', 'abort', False, _('abort an edit in progress')),
896 ('o', 'outgoing', False, _('changesets not found in destination')),
896 ('o', 'outgoing', False, _('changesets not found in destination')),
897 ('f', 'force', False,
897 ('f', 'force', False,
898 _('force outgoing even for unrelated repositories')),
898 _('force outgoing even for unrelated repositories')),
899 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
899 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
900 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
900 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
901 def histedit(ui, repo, *freeargs, **opts):
901 def histedit(ui, repo, *freeargs, **opts):
902 """interactively edit changeset history
902 """interactively edit changeset history
903
903
904 This command lets you edit a linear series of changesets (up to
904 This command lets you edit a linear series of changesets (up to
905 and including the working directory, which should be clean).
905 and including the working directory, which should be clean).
906 You can:
906 You can:
907
907
908 - `pick` to [re]order a changeset
908 - `pick` to [re]order a changeset
909
909
910 - `drop` to omit changeset
910 - `drop` to omit changeset
911
911
912 - `mess` to reword the changeset commit message
912 - `mess` to reword the changeset commit message
913
913
914 - `fold` to combine it with the preceding changeset (using the later date)
914 - `fold` to combine it with the preceding changeset (using the later date)
915
915
916 - `roll` like fold, but discarding this commit's description and date
916 - `roll` like fold, but discarding this commit's description and date
917
917
918 - `edit` to edit this changeset (preserving date)
918 - `edit` to edit this changeset (preserving date)
919
919
920 There are a number of ways to select the root changeset:
920 There are a number of ways to select the root changeset:
921
921
922 - Specify ANCESTOR directly
922 - Specify ANCESTOR directly
923
923
924 - Use --outgoing -- it will be the first linear changeset not
924 - Use --outgoing -- it will be the first linear changeset not
925 included in destination. (See :hg:`help config.paths.default-push`)
925 included in destination. (See :hg:`help config.paths.default-push`)
926
926
927 - Otherwise, the value from the "histedit.defaultrev" config option
927 - Otherwise, the value from the "histedit.defaultrev" config option
928 is used as a revset to select the base revision when ANCESTOR is not
928 is used as a revset to select the base revision when ANCESTOR is not
929 specified. The first revision returned by the revset is used. By
929 specified. The first revision returned by the revset is used. By
930 default, this selects the editable history that is unique to the
930 default, this selects the editable history that is unique to the
931 ancestry of the working directory.
931 ancestry of the working directory.
932
932
933 .. container:: verbose
933 .. container:: verbose
934
934
935 If you use --outgoing, this command will abort if there are ambiguous
935 If you use --outgoing, this command will abort if there are ambiguous
936 outgoing revisions. For example, if there are multiple branches
936 outgoing revisions. For example, if there are multiple branches
937 containing outgoing revisions.
937 containing outgoing revisions.
938
938
939 Use "min(outgoing() and ::.)" or similar revset specification
939 Use "min(outgoing() and ::.)" or similar revset specification
940 instead of --outgoing to specify edit target revision exactly in
940 instead of --outgoing to specify edit target revision exactly in
941 such ambiguous situation. See :hg:`help revsets` for detail about
941 such ambiguous situation. See :hg:`help revsets` for detail about
942 selecting revisions.
942 selecting revisions.
943
943
944 .. container:: verbose
944 .. container:: verbose
945
945
946 Examples:
946 Examples:
947
947
948 - A number of changes have been made.
948 - A number of changes have been made.
949 Revision 3 is no longer needed.
949 Revision 3 is no longer needed.
950
950
951 Start history editing from revision 3::
951 Start history editing from revision 3::
952
952
953 hg histedit -r 3
953 hg histedit -r 3
954
954
955 An editor opens, containing the list of revisions,
955 An editor opens, containing the list of revisions,
956 with specific actions specified::
956 with specific actions specified::
957
957
958 pick 5339bf82f0ca 3 Zworgle the foobar
958 pick 5339bf82f0ca 3 Zworgle the foobar
959 pick 8ef592ce7cc4 4 Bedazzle the zerlog
959 pick 8ef592ce7cc4 4 Bedazzle the zerlog
960 pick 0a9639fcda9d 5 Morgify the cromulancy
960 pick 0a9639fcda9d 5 Morgify the cromulancy
961
961
962 Additional information about the possible actions
962 Additional information about the possible actions
963 to take appears below the list of revisions.
963 to take appears below the list of revisions.
964
964
965 To remove revision 3 from the history,
965 To remove revision 3 from the history,
966 its action (at the beginning of the relevant line)
966 its action (at the beginning of the relevant line)
967 is changed to 'drop'::
967 is changed to 'drop'::
968
968
969 drop 5339bf82f0ca 3 Zworgle the foobar
969 drop 5339bf82f0ca 3 Zworgle the foobar
970 pick 8ef592ce7cc4 4 Bedazzle the zerlog
970 pick 8ef592ce7cc4 4 Bedazzle the zerlog
971 pick 0a9639fcda9d 5 Morgify the cromulancy
971 pick 0a9639fcda9d 5 Morgify the cromulancy
972
972
973 - A number of changes have been made.
973 - A number of changes have been made.
974 Revision 2 and 4 need to be swapped.
974 Revision 2 and 4 need to be swapped.
975
975
976 Start history editing from revision 2::
976 Start history editing from revision 2::
977
977
978 hg histedit -r 2
978 hg histedit -r 2
979
979
980 An editor opens, containing the list of revisions,
980 An editor opens, containing the list of revisions,
981 with specific actions specified::
981 with specific actions specified::
982
982
983 pick 252a1af424ad 2 Blorb a morgwazzle
983 pick 252a1af424ad 2 Blorb a morgwazzle
984 pick 5339bf82f0ca 3 Zworgle the foobar
984 pick 5339bf82f0ca 3 Zworgle the foobar
985 pick 8ef592ce7cc4 4 Bedazzle the zerlog
985 pick 8ef592ce7cc4 4 Bedazzle the zerlog
986
986
987 To swap revision 2 and 4, its lines are swapped
987 To swap revision 2 and 4, its lines are swapped
988 in the editor::
988 in the editor::
989
989
990 pick 8ef592ce7cc4 4 Bedazzle the zerlog
990 pick 8ef592ce7cc4 4 Bedazzle the zerlog
991 pick 5339bf82f0ca 3 Zworgle the foobar
991 pick 5339bf82f0ca 3 Zworgle the foobar
992 pick 252a1af424ad 2 Blorb a morgwazzle
992 pick 252a1af424ad 2 Blorb a morgwazzle
993
993
994 Returns 0 on success, 1 if user intervention is required (not only
994 Returns 0 on success, 1 if user intervention is required (not only
995 for intentional "edit" command, but also for resolving unexpected
995 for intentional "edit" command, but also for resolving unexpected
996 conflicts).
996 conflicts).
997 """
997 """
998 state = histeditstate(repo)
998 state = histeditstate(repo)
999 try:
999 try:
1000 state.wlock = repo.wlock()
1000 state.wlock = repo.wlock()
1001 state.lock = repo.lock()
1001 state.lock = repo.lock()
1002 _histedit(ui, repo, state, *freeargs, **opts)
1002 _histedit(ui, repo, state, *freeargs, **opts)
1003 finally:
1003 finally:
1004 release(state.lock, state.wlock)
1004 release(state.lock, state.wlock)
1005
1005
1006 goalcontinue = 'continue'
1006 goalcontinue = 'continue'
1007 goalabort = 'abort'
1007 goalabort = 'abort'
1008 goaleditplan = 'edit-plan'
1008 goaleditplan = 'edit-plan'
1009 goalnew = 'new'
1009 goalnew = 'new'
1010
1010
1011 def _getgoal(opts):
1011 def _getgoal(opts):
1012 if opts.get('continue'):
1012 if opts.get('continue'):
1013 return goalcontinue
1013 return goalcontinue
1014 if opts.get('abort'):
1014 if opts.get('abort'):
1015 return goalabort
1015 return goalabort
1016 if opts.get('edit_plan'):
1016 if opts.get('edit_plan'):
1017 return goaleditplan
1017 return goaleditplan
1018 return goalnew
1018 return goalnew
1019
1019
1020 def _readfile(ui, path):
1020 def _readfile(ui, path):
1021 if path == '-':
1021 if path == '-':
1022 with ui.timeblockedsection('histedit'):
1022 with ui.timeblockedsection('histedit'):
1023 return ui.fin.read()
1023 return ui.fin.read()
1024 else:
1024 else:
1025 with open(path, 'rb') as f:
1025 with open(path, 'rb') as f:
1026 return f.read()
1026 return f.read()
1027
1027
1028 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1028 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1029 # TODO only abort if we try to histedit mq patches, not just
1029 # TODO only abort if we try to histedit mq patches, not just
1030 # blanket if mq patches are applied somewhere
1030 # blanket if mq patches are applied somewhere
1031 mq = getattr(repo, 'mq', None)
1031 mq = getattr(repo, 'mq', None)
1032 if mq and mq.applied:
1032 if mq and mq.applied:
1033 raise error.Abort(_('source has mq patches applied'))
1033 raise error.Abort(_('source has mq patches applied'))
1034
1034
1035 # basic argument incompatibility processing
1035 # basic argument incompatibility processing
1036 outg = opts.get('outgoing')
1036 outg = opts.get('outgoing')
1037 editplan = opts.get('edit_plan')
1037 editplan = opts.get('edit_plan')
1038 abort = opts.get('abort')
1038 abort = opts.get('abort')
1039 force = opts.get('force')
1039 force = opts.get('force')
1040 if force and not outg:
1040 if force and not outg:
1041 raise error.Abort(_('--force only allowed with --outgoing'))
1041 raise error.Abort(_('--force only allowed with --outgoing'))
1042 if goal == 'continue':
1042 if goal == 'continue':
1043 if any((outg, abort, revs, freeargs, rules, editplan)):
1043 if any((outg, abort, revs, freeargs, rules, editplan)):
1044 raise error.Abort(_('no arguments allowed with --continue'))
1044 raise error.Abort(_('no arguments allowed with --continue'))
1045 elif goal == 'abort':
1045 elif goal == 'abort':
1046 if any((outg, revs, freeargs, rules, editplan)):
1046 if any((outg, revs, freeargs, rules, editplan)):
1047 raise error.Abort(_('no arguments allowed with --abort'))
1047 raise error.Abort(_('no arguments allowed with --abort'))
1048 elif goal == 'edit-plan':
1048 elif goal == 'edit-plan':
1049 if any((outg, revs, freeargs)):
1049 if any((outg, revs, freeargs)):
1050 raise error.Abort(_('only --commands argument allowed with '
1050 raise error.Abort(_('only --commands argument allowed with '
1051 '--edit-plan'))
1051 '--edit-plan'))
1052 else:
1052 else:
1053 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1053 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1054 raise error.Abort(_('history edit already in progress, try '
1054 raise error.Abort(_('history edit already in progress, try '
1055 '--continue or --abort'))
1055 '--continue or --abort'))
1056 if outg:
1056 if outg:
1057 if revs:
1057 if revs:
1058 raise error.Abort(_('no revisions allowed with --outgoing'))
1058 raise error.Abort(_('no revisions allowed with --outgoing'))
1059 if len(freeargs) > 1:
1059 if len(freeargs) > 1:
1060 raise error.Abort(
1060 raise error.Abort(
1061 _('only one repo argument allowed with --outgoing'))
1061 _('only one repo argument allowed with --outgoing'))
1062 else:
1062 else:
1063 revs.extend(freeargs)
1063 revs.extend(freeargs)
1064 if len(revs) == 0:
1064 if len(revs) == 0:
1065 defaultrev = destutil.desthistedit(ui, repo)
1065 defaultrev = destutil.desthistedit(ui, repo)
1066 if defaultrev is not None:
1066 if defaultrev is not None:
1067 revs.append(defaultrev)
1067 revs.append(defaultrev)
1068
1068
1069 if len(revs) != 1:
1069 if len(revs) != 1:
1070 raise error.Abort(
1070 raise error.Abort(
1071 _('histedit requires exactly one ancestor revision'))
1071 _('histedit requires exactly one ancestor revision'))
1072
1072
1073 def _histedit(ui, repo, state, *freeargs, **opts):
1073 def _histedit(ui, repo, state, *freeargs, **opts):
1074 goal = _getgoal(opts)
1074 goal = _getgoal(opts)
1075 revs = opts.get('rev', [])
1075 revs = opts.get('rev', [])
1076 rules = opts.get('commands', '')
1076 rules = opts.get('commands', '')
1077 state.keep = opts.get('keep', False)
1077 state.keep = opts.get('keep', False)
1078
1078
1079 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1079 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1080
1080
1081 # rebuild state
1081 # rebuild state
1082 if goal == goalcontinue:
1082 if goal == goalcontinue:
1083 state.read()
1083 state.read()
1084 state = bootstrapcontinue(ui, state, opts)
1084 state = bootstrapcontinue(ui, state, opts)
1085 elif goal == goaleditplan:
1085 elif goal == goaleditplan:
1086 _edithisteditplan(ui, repo, state, rules)
1086 _edithisteditplan(ui, repo, state, rules)
1087 return
1087 return
1088 elif goal == goalabort:
1088 elif goal == goalabort:
1089 _aborthistedit(ui, repo, state)
1089 _aborthistedit(ui, repo, state)
1090 return
1090 return
1091 else:
1091 else:
1092 # goal == goalnew
1092 # goal == goalnew
1093 _newhistedit(ui, repo, state, revs, freeargs, opts)
1093 _newhistedit(ui, repo, state, revs, freeargs, opts)
1094
1094
1095 _continuehistedit(ui, repo, state)
1095 _continuehistedit(ui, repo, state)
1096 _finishhistedit(ui, repo, state)
1096 _finishhistedit(ui, repo, state)
1097
1097
1098 def _continuehistedit(ui, repo, state):
1098 def _continuehistedit(ui, repo, state):
1099 """This function runs after either:
1099 """This function runs after either:
1100 - bootstrapcontinue (if the goal is 'continue')
1100 - bootstrapcontinue (if the goal is 'continue')
1101 - _newhistedit (if the goal is 'new')
1101 - _newhistedit (if the goal is 'new')
1102 """
1102 """
1103 # preprocess rules so that we can hide inner folds from the user
1103 # preprocess rules so that we can hide inner folds from the user
1104 # and only show one editor
1104 # and only show one editor
1105 actions = state.actions[:]
1105 actions = state.actions[:]
1106 for idx, (action, nextact) in enumerate(
1106 for idx, (action, nextact) in enumerate(
1107 zip(actions, actions[1:] + [None])):
1107 zip(actions, actions[1:] + [None])):
1108 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1108 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1109 state.actions[idx].__class__ = _multifold
1109 state.actions[idx].__class__ = _multifold
1110
1110
1111 total = len(state.actions)
1111 total = len(state.actions)
1112 pos = 0
1112 pos = 0
1113 state.tr = None
1113 state.tr = None
1114
1114
1115 # Force an initial state file write, so the user can run --abort/continue
1115 # Force an initial state file write, so the user can run --abort/continue
1116 # even if there's an exception before the first transaction serialize.
1116 # even if there's an exception before the first transaction serialize.
1117 state.write()
1117 state.write()
1118 try:
1118 try:
1119 # Don't use singletransaction by default since it rolls the entire
1119 # Don't use singletransaction by default since it rolls the entire
1120 # transaction back if an unexpected exception happens (like a
1120 # transaction back if an unexpected exception happens (like a
1121 # pretxncommit hook throws, or the user aborts the commit msg editor).
1121 # pretxncommit hook throws, or the user aborts the commit msg editor).
1122 if ui.configbool("histedit", "singletransaction", False):
1122 if ui.configbool("histedit", "singletransaction", False):
1123 # Don't use a 'with' for the transaction, since actions may close
1123 # Don't use a 'with' for the transaction, since actions may close
1124 # and reopen a transaction. For example, if the action executes an
1124 # and reopen a transaction. For example, if the action executes an
1125 # external process it may choose to commit the transaction first.
1125 # external process it may choose to commit the transaction first.
1126 state.tr = repo.transaction('histedit')
1126 state.tr = repo.transaction('histedit')
1127
1127
1128 while state.actions:
1128 while state.actions:
1129 state.write(tr=state.tr)
1129 state.write(tr=state.tr)
1130 actobj = state.actions[0]
1130 actobj = state.actions[0]
1131 pos += 1
1131 pos += 1
1132 ui.progress(_("editing"), pos, actobj.torule(),
1132 ui.progress(_("editing"), pos, actobj.torule(),
1133 _('changes'), total)
1133 _('changes'), total)
1134 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1134 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1135 actobj.torule()))
1135 actobj.torule()))
1136 parentctx, replacement_ = actobj.run()
1136 parentctx, replacement_ = actobj.run()
1137 state.parentctxnode = parentctx.node()
1137 state.parentctxnode = parentctx.node()
1138 state.replacements.extend(replacement_)
1138 state.replacements.extend(replacement_)
1139 state.actions.pop(0)
1139 state.actions.pop(0)
1140
1140
1141 if state.tr is not None:
1141 if state.tr is not None:
1142 state.tr.close()
1142 state.tr.close()
1143 except error.InterventionRequired:
1143 except error.InterventionRequired:
1144 if state.tr is not None:
1144 if state.tr is not None:
1145 state.tr.close()
1145 state.tr.close()
1146 raise
1146 raise
1147 except Exception:
1147 except Exception:
1148 if state.tr is not None:
1148 if state.tr is not None:
1149 state.tr.abort()
1149 state.tr.abort()
1150 raise
1150 raise
1151
1151
1152 state.write()
1152 state.write()
1153 ui.progress(_("editing"), None)
1153 ui.progress(_("editing"), None)
1154
1154
1155 def _finishhistedit(ui, repo, state):
1155 def _finishhistedit(ui, repo, state):
1156 """This action runs when histedit is finishing its session"""
1156 """This action runs when histedit is finishing its session"""
1157 repo.ui.pushbuffer()
1157 repo.ui.pushbuffer()
1158 hg.update(repo, state.parentctxnode, quietempty=True)
1158 hg.update(repo, state.parentctxnode, quietempty=True)
1159 repo.ui.popbuffer()
1159 repo.ui.popbuffer()
1160
1160
1161 mapping, tmpnodes, created, ntm = processreplacement(state)
1161 mapping, tmpnodes, created, ntm = processreplacement(state)
1162 if mapping:
1162 if mapping:
1163 for prec, succs in mapping.iteritems():
1163 for prec, succs in mapping.iteritems():
1164 if not succs:
1164 if not succs:
1165 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1165 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1166 else:
1166 else:
1167 ui.debug('histedit: %s is replaced by %s\n' % (
1167 ui.debug('histedit: %s is replaced by %s\n' % (
1168 node.short(prec), node.short(succs[0])))
1168 node.short(prec), node.short(succs[0])))
1169 if len(succs) > 1:
1169 if len(succs) > 1:
1170 m = 'histedit: %s'
1170 m = 'histedit: %s'
1171 for n in succs[1:]:
1171 for n in succs[1:]:
1172 ui.debug(m % node.short(n))
1172 ui.debug(m % node.short(n))
1173
1173
1174 safecleanupnode(ui, repo, 'temp', tmpnodes)
1174 safecleanupnode(ui, repo, 'temp', tmpnodes)
1175
1175
1176 if not state.keep:
1176 if not state.keep:
1177 if mapping:
1177 if mapping:
1178 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1178 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1179 # TODO update mq state
1179 # TODO update mq state
1180 safecleanupnode(ui, repo, 'replaced', mapping)
1180 safecleanupnode(ui, repo, 'replaced', mapping)
1181
1181
1182 state.clear()
1182 state.clear()
1183 if os.path.exists(repo.sjoin('undo')):
1183 if os.path.exists(repo.sjoin('undo')):
1184 os.unlink(repo.sjoin('undo'))
1184 os.unlink(repo.sjoin('undo'))
1185 if repo.vfs.exists('histedit-last-edit.txt'):
1185 if repo.vfs.exists('histedit-last-edit.txt'):
1186 repo.vfs.unlink('histedit-last-edit.txt')
1186 repo.vfs.unlink('histedit-last-edit.txt')
1187
1187
1188 def _aborthistedit(ui, repo, state):
1188 def _aborthistedit(ui, repo, state):
1189 try:
1189 try:
1190 state.read()
1190 state.read()
1191 __, leafs, tmpnodes, __ = processreplacement(state)
1191 __, leafs, tmpnodes, __ = processreplacement(state)
1192 ui.debug('restore wc to old parent %s\n'
1192 ui.debug('restore wc to old parent %s\n'
1193 % node.short(state.topmost))
1193 % node.short(state.topmost))
1194
1194
1195 # Recover our old commits if necessary
1195 # Recover our old commits if necessary
1196 if not state.topmost in repo and state.backupfile:
1196 if not state.topmost in repo and state.backupfile:
1197 backupfile = repo.vfs.join(state.backupfile)
1197 backupfile = repo.vfs.join(state.backupfile)
1198 f = hg.openpath(ui, backupfile)
1198 f = hg.openpath(ui, backupfile)
1199 gen = exchange.readbundle(ui, f, backupfile)
1199 gen = exchange.readbundle(ui, f, backupfile)
1200 with repo.transaction('histedit.abort') as tr:
1200 with repo.transaction('histedit.abort') as tr:
1201 if not isinstance(gen, bundle2.unbundle20):
1201 if not isinstance(gen, bundle2.unbundle20):
1202 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1202 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1203 if isinstance(gen, bundle2.unbundle20):
1203 else:
1204 bundle2.applybundle(repo, gen, tr,
1204 bundle2.applybundle(repo, gen, tr,
1205 source='histedit',
1205 source='histedit',
1206 url='bundle:' + backupfile)
1206 url='bundle:' + backupfile)
1207
1207
1208 os.remove(backupfile)
1208 os.remove(backupfile)
1209
1209
1210 # check whether we should update away
1210 # check whether we should update away
1211 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1211 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1212 state.parentctxnode, leafs | tmpnodes):
1212 state.parentctxnode, leafs | tmpnodes):
1213 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1213 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1214 cleanupnode(ui, repo, 'created', tmpnodes)
1214 cleanupnode(ui, repo, 'created', tmpnodes)
1215 cleanupnode(ui, repo, 'temp', leafs)
1215 cleanupnode(ui, repo, 'temp', leafs)
1216 except Exception:
1216 except Exception:
1217 if state.inprogress():
1217 if state.inprogress():
1218 ui.warn(_('warning: encountered an exception during histedit '
1218 ui.warn(_('warning: encountered an exception during histedit '
1219 '--abort; the repository may not have been completely '
1219 '--abort; the repository may not have been completely '
1220 'cleaned up\n'))
1220 'cleaned up\n'))
1221 raise
1221 raise
1222 finally:
1222 finally:
1223 state.clear()
1223 state.clear()
1224
1224
1225 def _edithisteditplan(ui, repo, state, rules):
1225 def _edithisteditplan(ui, repo, state, rules):
1226 state.read()
1226 state.read()
1227 if not rules:
1227 if not rules:
1228 comment = geteditcomment(ui,
1228 comment = geteditcomment(ui,
1229 node.short(state.parentctxnode),
1229 node.short(state.parentctxnode),
1230 node.short(state.topmost))
1230 node.short(state.topmost))
1231 rules = ruleeditor(repo, ui, state.actions, comment)
1231 rules = ruleeditor(repo, ui, state.actions, comment)
1232 else:
1232 else:
1233 rules = _readfile(ui, rules)
1233 rules = _readfile(ui, rules)
1234 actions = parserules(rules, state)
1234 actions = parserules(rules, state)
1235 ctxs = [repo[act.node] \
1235 ctxs = [repo[act.node] \
1236 for act in state.actions if act.node]
1236 for act in state.actions if act.node]
1237 warnverifyactions(ui, repo, actions, state, ctxs)
1237 warnverifyactions(ui, repo, actions, state, ctxs)
1238 state.actions = actions
1238 state.actions = actions
1239 state.write()
1239 state.write()
1240
1240
1241 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1241 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1242 outg = opts.get('outgoing')
1242 outg = opts.get('outgoing')
1243 rules = opts.get('commands', '')
1243 rules = opts.get('commands', '')
1244 force = opts.get('force')
1244 force = opts.get('force')
1245
1245
1246 cmdutil.checkunfinished(repo)
1246 cmdutil.checkunfinished(repo)
1247 cmdutil.bailifchanged(repo)
1247 cmdutil.bailifchanged(repo)
1248
1248
1249 topmost, empty = repo.dirstate.parents()
1249 topmost, empty = repo.dirstate.parents()
1250 if outg:
1250 if outg:
1251 if freeargs:
1251 if freeargs:
1252 remote = freeargs[0]
1252 remote = freeargs[0]
1253 else:
1253 else:
1254 remote = None
1254 remote = None
1255 root = findoutgoing(ui, repo, remote, force, opts)
1255 root = findoutgoing(ui, repo, remote, force, opts)
1256 else:
1256 else:
1257 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1257 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1258 if len(rr) != 1:
1258 if len(rr) != 1:
1259 raise error.Abort(_('The specified revisions must have '
1259 raise error.Abort(_('The specified revisions must have '
1260 'exactly one common root'))
1260 'exactly one common root'))
1261 root = rr[0].node()
1261 root = rr[0].node()
1262
1262
1263 revs = between(repo, root, topmost, state.keep)
1263 revs = between(repo, root, topmost, state.keep)
1264 if not revs:
1264 if not revs:
1265 raise error.Abort(_('%s is not an ancestor of working directory') %
1265 raise error.Abort(_('%s is not an ancestor of working directory') %
1266 node.short(root))
1266 node.short(root))
1267
1267
1268 ctxs = [repo[r] for r in revs]
1268 ctxs = [repo[r] for r in revs]
1269 if not rules:
1269 if not rules:
1270 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1270 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1271 actions = [pick(state, r) for r in revs]
1271 actions = [pick(state, r) for r in revs]
1272 rules = ruleeditor(repo, ui, actions, comment)
1272 rules = ruleeditor(repo, ui, actions, comment)
1273 else:
1273 else:
1274 rules = _readfile(ui, rules)
1274 rules = _readfile(ui, rules)
1275 actions = parserules(rules, state)
1275 actions = parserules(rules, state)
1276 warnverifyactions(ui, repo, actions, state, ctxs)
1276 warnverifyactions(ui, repo, actions, state, ctxs)
1277
1277
1278 parentctxnode = repo[root].parents()[0].node()
1278 parentctxnode = repo[root].parents()[0].node()
1279
1279
1280 state.parentctxnode = parentctxnode
1280 state.parentctxnode = parentctxnode
1281 state.actions = actions
1281 state.actions = actions
1282 state.topmost = topmost
1282 state.topmost = topmost
1283 state.replacements = []
1283 state.replacements = []
1284
1284
1285 # Create a backup so we can always abort completely.
1285 # Create a backup so we can always abort completely.
1286 backupfile = None
1286 backupfile = None
1287 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1287 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1288 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1288 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1289 'histedit')
1289 'histedit')
1290 state.backupfile = backupfile
1290 state.backupfile = backupfile
1291
1291
1292 def _getsummary(ctx):
1292 def _getsummary(ctx):
1293 # a common pattern is to extract the summary but default to the empty
1293 # a common pattern is to extract the summary but default to the empty
1294 # string
1294 # string
1295 summary = ctx.description() or ''
1295 summary = ctx.description() or ''
1296 if summary:
1296 if summary:
1297 summary = summary.splitlines()[0]
1297 summary = summary.splitlines()[0]
1298 return summary
1298 return summary
1299
1299
1300 def bootstrapcontinue(ui, state, opts):
1300 def bootstrapcontinue(ui, state, opts):
1301 repo = state.repo
1301 repo = state.repo
1302
1302
1303 ms = mergemod.mergestate.read(repo)
1303 ms = mergemod.mergestate.read(repo)
1304 mergeutil.checkunresolved(ms)
1304 mergeutil.checkunresolved(ms)
1305
1305
1306 if state.actions:
1306 if state.actions:
1307 actobj = state.actions.pop(0)
1307 actobj = state.actions.pop(0)
1308
1308
1309 if _isdirtywc(repo):
1309 if _isdirtywc(repo):
1310 actobj.continuedirty()
1310 actobj.continuedirty()
1311 if _isdirtywc(repo):
1311 if _isdirtywc(repo):
1312 abortdirty()
1312 abortdirty()
1313
1313
1314 parentctx, replacements = actobj.continueclean()
1314 parentctx, replacements = actobj.continueclean()
1315
1315
1316 state.parentctxnode = parentctx.node()
1316 state.parentctxnode = parentctx.node()
1317 state.replacements.extend(replacements)
1317 state.replacements.extend(replacements)
1318
1318
1319 return state
1319 return state
1320
1320
1321 def between(repo, old, new, keep):
1321 def between(repo, old, new, keep):
1322 """select and validate the set of revision to edit
1322 """select and validate the set of revision to edit
1323
1323
1324 When keep is false, the specified set can't have children."""
1324 When keep is false, the specified set can't have children."""
1325 ctxs = list(repo.set('%n::%n', old, new))
1325 ctxs = list(repo.set('%n::%n', old, new))
1326 if ctxs and not keep:
1326 if ctxs and not keep:
1327 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1327 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1328 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1328 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1329 raise error.Abort(_('can only histedit a changeset together '
1329 raise error.Abort(_('can only histedit a changeset together '
1330 'with all its descendants'))
1330 'with all its descendants'))
1331 if repo.revs('(%ld) and merge()', ctxs):
1331 if repo.revs('(%ld) and merge()', ctxs):
1332 raise error.Abort(_('cannot edit history that contains merges'))
1332 raise error.Abort(_('cannot edit history that contains merges'))
1333 root = ctxs[0] # list is already sorted by repo.set
1333 root = ctxs[0] # list is already sorted by repo.set
1334 if not root.mutable():
1334 if not root.mutable():
1335 raise error.Abort(_('cannot edit public changeset: %s') % root,
1335 raise error.Abort(_('cannot edit public changeset: %s') % root,
1336 hint=_("see 'hg help phases' for details"))
1336 hint=_("see 'hg help phases' for details"))
1337 return [c.node() for c in ctxs]
1337 return [c.node() for c in ctxs]
1338
1338
1339 def ruleeditor(repo, ui, actions, editcomment=""):
1339 def ruleeditor(repo, ui, actions, editcomment=""):
1340 """open an editor to edit rules
1340 """open an editor to edit rules
1341
1341
1342 rules are in the format [ [act, ctx], ...] like in state.rules
1342 rules are in the format [ [act, ctx], ...] like in state.rules
1343 """
1343 """
1344 if repo.ui.configbool("experimental", "histedit.autoverb"):
1344 if repo.ui.configbool("experimental", "histedit.autoverb"):
1345 newact = util.sortdict()
1345 newact = util.sortdict()
1346 for act in actions:
1346 for act in actions:
1347 ctx = repo[act.node]
1347 ctx = repo[act.node]
1348 summary = _getsummary(ctx)
1348 summary = _getsummary(ctx)
1349 fword = summary.split(' ', 1)[0].lower()
1349 fword = summary.split(' ', 1)[0].lower()
1350 added = False
1350 added = False
1351
1351
1352 # if it doesn't end with the special character '!' just skip this
1352 # if it doesn't end with the special character '!' just skip this
1353 if fword.endswith('!'):
1353 if fword.endswith('!'):
1354 fword = fword[:-1]
1354 fword = fword[:-1]
1355 if fword in primaryactions | secondaryactions | tertiaryactions:
1355 if fword in primaryactions | secondaryactions | tertiaryactions:
1356 act.verb = fword
1356 act.verb = fword
1357 # get the target summary
1357 # get the target summary
1358 tsum = summary[len(fword) + 1:].lstrip()
1358 tsum = summary[len(fword) + 1:].lstrip()
1359 # safe but slow: reverse iterate over the actions so we
1359 # safe but slow: reverse iterate over the actions so we
1360 # don't clash on two commits having the same summary
1360 # don't clash on two commits having the same summary
1361 for na, l in reversed(list(newact.iteritems())):
1361 for na, l in reversed(list(newact.iteritems())):
1362 actx = repo[na.node]
1362 actx = repo[na.node]
1363 asum = _getsummary(actx)
1363 asum = _getsummary(actx)
1364 if asum == tsum:
1364 if asum == tsum:
1365 added = True
1365 added = True
1366 l.append(act)
1366 l.append(act)
1367 break
1367 break
1368
1368
1369 if not added:
1369 if not added:
1370 newact[act] = []
1370 newact[act] = []
1371
1371
1372 # copy over and flatten the new list
1372 # copy over and flatten the new list
1373 actions = []
1373 actions = []
1374 for na, l in newact.iteritems():
1374 for na, l in newact.iteritems():
1375 actions.append(na)
1375 actions.append(na)
1376 actions += l
1376 actions += l
1377
1377
1378 rules = '\n'.join([act.torule() for act in actions])
1378 rules = '\n'.join([act.torule() for act in actions])
1379 rules += '\n\n'
1379 rules += '\n\n'
1380 rules += editcomment
1380 rules += editcomment
1381 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1381 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1382 repopath=repo.path)
1382 repopath=repo.path)
1383
1383
1384 # Save edit rules in .hg/histedit-last-edit.txt in case
1384 # Save edit rules in .hg/histedit-last-edit.txt in case
1385 # the user needs to ask for help after something
1385 # the user needs to ask for help after something
1386 # surprising happens.
1386 # surprising happens.
1387 f = open(repo.vfs.join('histedit-last-edit.txt'), 'w')
1387 f = open(repo.vfs.join('histedit-last-edit.txt'), 'w')
1388 f.write(rules)
1388 f.write(rules)
1389 f.close()
1389 f.close()
1390
1390
1391 return rules
1391 return rules
1392
1392
1393 def parserules(rules, state):
1393 def parserules(rules, state):
1394 """Read the histedit rules string and return list of action objects """
1394 """Read the histedit rules string and return list of action objects """
1395 rules = [l for l in (r.strip() for r in rules.splitlines())
1395 rules = [l for l in (r.strip() for r in rules.splitlines())
1396 if l and not l.startswith('#')]
1396 if l and not l.startswith('#')]
1397 actions = []
1397 actions = []
1398 for r in rules:
1398 for r in rules:
1399 if ' ' not in r:
1399 if ' ' not in r:
1400 raise error.ParseError(_('malformed line "%s"') % r)
1400 raise error.ParseError(_('malformed line "%s"') % r)
1401 verb, rest = r.split(' ', 1)
1401 verb, rest = r.split(' ', 1)
1402
1402
1403 if verb not in actiontable:
1403 if verb not in actiontable:
1404 raise error.ParseError(_('unknown action "%s"') % verb)
1404 raise error.ParseError(_('unknown action "%s"') % verb)
1405
1405
1406 action = actiontable[verb].fromrule(state, rest)
1406 action = actiontable[verb].fromrule(state, rest)
1407 actions.append(action)
1407 actions.append(action)
1408 return actions
1408 return actions
1409
1409
1410 def warnverifyactions(ui, repo, actions, state, ctxs):
1410 def warnverifyactions(ui, repo, actions, state, ctxs):
1411 try:
1411 try:
1412 verifyactions(actions, state, ctxs)
1412 verifyactions(actions, state, ctxs)
1413 except error.ParseError:
1413 except error.ParseError:
1414 if repo.vfs.exists('histedit-last-edit.txt'):
1414 if repo.vfs.exists('histedit-last-edit.txt'):
1415 ui.warn(_('warning: histedit rules saved '
1415 ui.warn(_('warning: histedit rules saved '
1416 'to: .hg/histedit-last-edit.txt\n'))
1416 'to: .hg/histedit-last-edit.txt\n'))
1417 raise
1417 raise
1418
1418
1419 def verifyactions(actions, state, ctxs):
1419 def verifyactions(actions, state, ctxs):
1420 """Verify that there exists exactly one action per given changeset and
1420 """Verify that there exists exactly one action per given changeset and
1421 other constraints.
1421 other constraints.
1422
1422
1423 Will abort if there are to many or too few rules, a malformed rule,
1423 Will abort if there are to many or too few rules, a malformed rule,
1424 or a rule on a changeset outside of the user-given range.
1424 or a rule on a changeset outside of the user-given range.
1425 """
1425 """
1426 expected = set(c.node() for c in ctxs)
1426 expected = set(c.node() for c in ctxs)
1427 seen = set()
1427 seen = set()
1428 prev = None
1428 prev = None
1429 for action in actions:
1429 for action in actions:
1430 action.verify(prev, expected, seen)
1430 action.verify(prev, expected, seen)
1431 prev = action
1431 prev = action
1432 if action.node is not None:
1432 if action.node is not None:
1433 seen.add(action.node)
1433 seen.add(action.node)
1434 missing = sorted(expected - seen) # sort to stabilize output
1434 missing = sorted(expected - seen) # sort to stabilize output
1435
1435
1436 if state.repo.ui.configbool('histedit', 'dropmissing'):
1436 if state.repo.ui.configbool('histedit', 'dropmissing'):
1437 if len(actions) == 0:
1437 if len(actions) == 0:
1438 raise error.ParseError(_('no rules provided'),
1438 raise error.ParseError(_('no rules provided'),
1439 hint=_('use strip extension to remove commits'))
1439 hint=_('use strip extension to remove commits'))
1440
1440
1441 drops = [drop(state, n) for n in missing]
1441 drops = [drop(state, n) for n in missing]
1442 # put the in the beginning so they execute immediately and
1442 # put the in the beginning so they execute immediately and
1443 # don't show in the edit-plan in the future
1443 # don't show in the edit-plan in the future
1444 actions[:0] = drops
1444 actions[:0] = drops
1445 elif missing:
1445 elif missing:
1446 raise error.ParseError(_('missing rules for changeset %s') %
1446 raise error.ParseError(_('missing rules for changeset %s') %
1447 node.short(missing[0]),
1447 node.short(missing[0]),
1448 hint=_('use "drop %s" to discard, see also: '
1448 hint=_('use "drop %s" to discard, see also: '
1449 "'hg help -e histedit.config'")
1449 "'hg help -e histedit.config'")
1450 % node.short(missing[0]))
1450 % node.short(missing[0]))
1451
1451
1452 def adjustreplacementsfrommarkers(repo, oldreplacements):
1452 def adjustreplacementsfrommarkers(repo, oldreplacements):
1453 """Adjust replacements from obsolescence markers
1453 """Adjust replacements from obsolescence markers
1454
1454
1455 Replacements structure is originally generated based on
1455 Replacements structure is originally generated based on
1456 histedit's state and does not account for changes that are
1456 histedit's state and does not account for changes that are
1457 not recorded there. This function fixes that by adding
1457 not recorded there. This function fixes that by adding
1458 data read from obsolescence markers"""
1458 data read from obsolescence markers"""
1459 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1459 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1460 return oldreplacements
1460 return oldreplacements
1461
1461
1462 unfi = repo.unfiltered()
1462 unfi = repo.unfiltered()
1463 nm = unfi.changelog.nodemap
1463 nm = unfi.changelog.nodemap
1464 obsstore = repo.obsstore
1464 obsstore = repo.obsstore
1465 newreplacements = list(oldreplacements)
1465 newreplacements = list(oldreplacements)
1466 oldsuccs = [r[1] for r in oldreplacements]
1466 oldsuccs = [r[1] for r in oldreplacements]
1467 # successors that have already been added to succstocheck once
1467 # successors that have already been added to succstocheck once
1468 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1468 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1469 succstocheck = list(seensuccs)
1469 succstocheck = list(seensuccs)
1470 while succstocheck:
1470 while succstocheck:
1471 n = succstocheck.pop()
1471 n = succstocheck.pop()
1472 missing = nm.get(n) is None
1472 missing = nm.get(n) is None
1473 markers = obsstore.successors.get(n, ())
1473 markers = obsstore.successors.get(n, ())
1474 if missing and not markers:
1474 if missing and not markers:
1475 # dead end, mark it as such
1475 # dead end, mark it as such
1476 newreplacements.append((n, ()))
1476 newreplacements.append((n, ()))
1477 for marker in markers:
1477 for marker in markers:
1478 nsuccs = marker[1]
1478 nsuccs = marker[1]
1479 newreplacements.append((n, nsuccs))
1479 newreplacements.append((n, nsuccs))
1480 for nsucc in nsuccs:
1480 for nsucc in nsuccs:
1481 if nsucc not in seensuccs:
1481 if nsucc not in seensuccs:
1482 seensuccs.add(nsucc)
1482 seensuccs.add(nsucc)
1483 succstocheck.append(nsucc)
1483 succstocheck.append(nsucc)
1484
1484
1485 return newreplacements
1485 return newreplacements
1486
1486
1487 def processreplacement(state):
1487 def processreplacement(state):
1488 """process the list of replacements to return
1488 """process the list of replacements to return
1489
1489
1490 1) the final mapping between original and created nodes
1490 1) the final mapping between original and created nodes
1491 2) the list of temporary node created by histedit
1491 2) the list of temporary node created by histedit
1492 3) the list of new commit created by histedit"""
1492 3) the list of new commit created by histedit"""
1493 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1493 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1494 allsuccs = set()
1494 allsuccs = set()
1495 replaced = set()
1495 replaced = set()
1496 fullmapping = {}
1496 fullmapping = {}
1497 # initialize basic set
1497 # initialize basic set
1498 # fullmapping records all operations recorded in replacement
1498 # fullmapping records all operations recorded in replacement
1499 for rep in replacements:
1499 for rep in replacements:
1500 allsuccs.update(rep[1])
1500 allsuccs.update(rep[1])
1501 replaced.add(rep[0])
1501 replaced.add(rep[0])
1502 fullmapping.setdefault(rep[0], set()).update(rep[1])
1502 fullmapping.setdefault(rep[0], set()).update(rep[1])
1503 new = allsuccs - replaced
1503 new = allsuccs - replaced
1504 tmpnodes = allsuccs & replaced
1504 tmpnodes = allsuccs & replaced
1505 # Reduce content fullmapping into direct relation between original nodes
1505 # Reduce content fullmapping into direct relation between original nodes
1506 # and final node created during history edition
1506 # and final node created during history edition
1507 # Dropped changeset are replaced by an empty list
1507 # Dropped changeset are replaced by an empty list
1508 toproceed = set(fullmapping)
1508 toproceed = set(fullmapping)
1509 final = {}
1509 final = {}
1510 while toproceed:
1510 while toproceed:
1511 for x in list(toproceed):
1511 for x in list(toproceed):
1512 succs = fullmapping[x]
1512 succs = fullmapping[x]
1513 for s in list(succs):
1513 for s in list(succs):
1514 if s in toproceed:
1514 if s in toproceed:
1515 # non final node with unknown closure
1515 # non final node with unknown closure
1516 # We can't process this now
1516 # We can't process this now
1517 break
1517 break
1518 elif s in final:
1518 elif s in final:
1519 # non final node, replace with closure
1519 # non final node, replace with closure
1520 succs.remove(s)
1520 succs.remove(s)
1521 succs.update(final[s])
1521 succs.update(final[s])
1522 else:
1522 else:
1523 final[x] = succs
1523 final[x] = succs
1524 toproceed.remove(x)
1524 toproceed.remove(x)
1525 # remove tmpnodes from final mapping
1525 # remove tmpnodes from final mapping
1526 for n in tmpnodes:
1526 for n in tmpnodes:
1527 del final[n]
1527 del final[n]
1528 # we expect all changes involved in final to exist in the repo
1528 # we expect all changes involved in final to exist in the repo
1529 # turn `final` into list (topologically sorted)
1529 # turn `final` into list (topologically sorted)
1530 nm = state.repo.changelog.nodemap
1530 nm = state.repo.changelog.nodemap
1531 for prec, succs in final.items():
1531 for prec, succs in final.items():
1532 final[prec] = sorted(succs, key=nm.get)
1532 final[prec] = sorted(succs, key=nm.get)
1533
1533
1534 # computed topmost element (necessary for bookmark)
1534 # computed topmost element (necessary for bookmark)
1535 if new:
1535 if new:
1536 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1536 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1537 elif not final:
1537 elif not final:
1538 # Nothing rewritten at all. we won't need `newtopmost`
1538 # Nothing rewritten at all. we won't need `newtopmost`
1539 # It is the same as `oldtopmost` and `processreplacement` know it
1539 # It is the same as `oldtopmost` and `processreplacement` know it
1540 newtopmost = None
1540 newtopmost = None
1541 else:
1541 else:
1542 # every body died. The newtopmost is the parent of the root.
1542 # every body died. The newtopmost is the parent of the root.
1543 r = state.repo.changelog.rev
1543 r = state.repo.changelog.rev
1544 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1544 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1545
1545
1546 return final, tmpnodes, new, newtopmost
1546 return final, tmpnodes, new, newtopmost
1547
1547
1548 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1548 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1549 """Move bookmark from old to newly created node"""
1549 """Move bookmark from old to newly created node"""
1550 if not mapping:
1550 if not mapping:
1551 # if nothing got rewritten there is not purpose for this function
1551 # if nothing got rewritten there is not purpose for this function
1552 return
1552 return
1553 moves = []
1553 moves = []
1554 for bk, old in sorted(repo._bookmarks.iteritems()):
1554 for bk, old in sorted(repo._bookmarks.iteritems()):
1555 if old == oldtopmost:
1555 if old == oldtopmost:
1556 # special case ensure bookmark stay on tip.
1556 # special case ensure bookmark stay on tip.
1557 #
1557 #
1558 # This is arguably a feature and we may only want that for the
1558 # This is arguably a feature and we may only want that for the
1559 # active bookmark. But the behavior is kept compatible with the old
1559 # active bookmark. But the behavior is kept compatible with the old
1560 # version for now.
1560 # version for now.
1561 moves.append((bk, newtopmost))
1561 moves.append((bk, newtopmost))
1562 continue
1562 continue
1563 base = old
1563 base = old
1564 new = mapping.get(base, None)
1564 new = mapping.get(base, None)
1565 if new is None:
1565 if new is None:
1566 continue
1566 continue
1567 while not new:
1567 while not new:
1568 # base is killed, trying with parent
1568 # base is killed, trying with parent
1569 base = repo[base].p1().node()
1569 base = repo[base].p1().node()
1570 new = mapping.get(base, (base,))
1570 new = mapping.get(base, (base,))
1571 # nothing to move
1571 # nothing to move
1572 moves.append((bk, new[-1]))
1572 moves.append((bk, new[-1]))
1573 if moves:
1573 if moves:
1574 lock = tr = None
1574 lock = tr = None
1575 try:
1575 try:
1576 lock = repo.lock()
1576 lock = repo.lock()
1577 tr = repo.transaction('histedit')
1577 tr = repo.transaction('histedit')
1578 marks = repo._bookmarks
1578 marks = repo._bookmarks
1579 for mark, new in moves:
1579 for mark, new in moves:
1580 old = marks[mark]
1580 old = marks[mark]
1581 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1581 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1582 % (mark, node.short(old), node.short(new)))
1582 % (mark, node.short(old), node.short(new)))
1583 marks[mark] = new
1583 marks[mark] = new
1584 marks.recordchange(tr)
1584 marks.recordchange(tr)
1585 tr.close()
1585 tr.close()
1586 finally:
1586 finally:
1587 release(tr, lock)
1587 release(tr, lock)
1588
1588
1589 def cleanupnode(ui, repo, name, nodes):
1589 def cleanupnode(ui, repo, name, nodes):
1590 """strip a group of nodes from the repository
1590 """strip a group of nodes from the repository
1591
1591
1592 The set of node to strip may contains unknown nodes."""
1592 The set of node to strip may contains unknown nodes."""
1593 ui.debug('should strip %s nodes %s\n' %
1593 ui.debug('should strip %s nodes %s\n' %
1594 (name, ', '.join([node.short(n) for n in nodes])))
1594 (name, ', '.join([node.short(n) for n in nodes])))
1595 with repo.lock():
1595 with repo.lock():
1596 # do not let filtering get in the way of the cleanse
1596 # do not let filtering get in the way of the cleanse
1597 # we should probably get rid of obsolescence marker created during the
1597 # we should probably get rid of obsolescence marker created during the
1598 # histedit, but we currently do not have such information.
1598 # histedit, but we currently do not have such information.
1599 repo = repo.unfiltered()
1599 repo = repo.unfiltered()
1600 # Find all nodes that need to be stripped
1600 # Find all nodes that need to be stripped
1601 # (we use %lr instead of %ln to silently ignore unknown items)
1601 # (we use %lr instead of %ln to silently ignore unknown items)
1602 nm = repo.changelog.nodemap
1602 nm = repo.changelog.nodemap
1603 nodes = sorted(n for n in nodes if n in nm)
1603 nodes = sorted(n for n in nodes if n in nm)
1604 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1604 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1605 for c in roots:
1605 for c in roots:
1606 # We should process node in reverse order to strip tip most first.
1606 # We should process node in reverse order to strip tip most first.
1607 # but this trigger a bug in changegroup hook.
1607 # but this trigger a bug in changegroup hook.
1608 # This would reduce bundle overhead
1608 # This would reduce bundle overhead
1609 repair.strip(ui, repo, c)
1609 repair.strip(ui, repo, c)
1610
1610
1611 def safecleanupnode(ui, repo, name, nodes):
1611 def safecleanupnode(ui, repo, name, nodes):
1612 """strip or obsolete nodes
1612 """strip or obsolete nodes
1613
1613
1614 nodes could be either a set or dict which maps to replacements.
1614 nodes could be either a set or dict which maps to replacements.
1615 nodes could be unknown (outside the repo).
1615 nodes could be unknown (outside the repo).
1616 """
1616 """
1617 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1617 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1618 if supportsmarkers:
1618 if supportsmarkers:
1619 if util.safehasattr(nodes, 'get'):
1619 if util.safehasattr(nodes, 'get'):
1620 # nodes is a dict-like mapping
1620 # nodes is a dict-like mapping
1621 # use unfiltered repo for successors in case they are hidden
1621 # use unfiltered repo for successors in case they are hidden
1622 urepo = repo.unfiltered()
1622 urepo = repo.unfiltered()
1623 def getmarker(prec):
1623 def getmarker(prec):
1624 succs = tuple(urepo[n] for n in nodes.get(prec, ()))
1624 succs = tuple(urepo[n] for n in nodes.get(prec, ()))
1625 return (repo[prec], succs)
1625 return (repo[prec], succs)
1626 else:
1626 else:
1627 # nodes is a set-like
1627 # nodes is a set-like
1628 def getmarker(prec):
1628 def getmarker(prec):
1629 return (repo[prec], ())
1629 return (repo[prec], ())
1630 # sort by revision number because it sound "right"
1630 # sort by revision number because it sound "right"
1631 sortednodes = sorted([n for n in nodes if n in repo],
1631 sortednodes = sorted([n for n in nodes if n in repo],
1632 key=repo.changelog.rev)
1632 key=repo.changelog.rev)
1633 markers = [getmarker(t) for t in sortednodes]
1633 markers = [getmarker(t) for t in sortednodes]
1634 if markers:
1634 if markers:
1635 obsolete.createmarkers(repo, markers, operation='histedit')
1635 obsolete.createmarkers(repo, markers, operation='histedit')
1636 else:
1636 else:
1637 return cleanupnode(ui, repo, name, nodes)
1637 return cleanupnode(ui, repo, name, nodes)
1638
1638
1639 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1639 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1640 if isinstance(nodelist, str):
1640 if isinstance(nodelist, str):
1641 nodelist = [nodelist]
1641 nodelist = [nodelist]
1642 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1642 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1643 state = histeditstate(repo)
1643 state = histeditstate(repo)
1644 state.read()
1644 state.read()
1645 histedit_nodes = {action.node for action
1645 histedit_nodes = {action.node for action
1646 in state.actions if action.node}
1646 in state.actions if action.node}
1647 common_nodes = histedit_nodes & set(nodelist)
1647 common_nodes = histedit_nodes & set(nodelist)
1648 if common_nodes:
1648 if common_nodes:
1649 raise error.Abort(_("histedit in progress, can't strip %s")
1649 raise error.Abort(_("histedit in progress, can't strip %s")
1650 % ', '.join(node.short(x) for x in common_nodes))
1650 % ', '.join(node.short(x) for x in common_nodes))
1651 return orig(ui, repo, nodelist, *args, **kwargs)
1651 return orig(ui, repo, nodelist, *args, **kwargs)
1652
1652
1653 extensions.wrapfunction(repair, 'strip', stripwrapper)
1653 extensions.wrapfunction(repair, 'strip', stripwrapper)
1654
1654
1655 def summaryhook(ui, repo):
1655 def summaryhook(ui, repo):
1656 if not os.path.exists(repo.vfs.join('histedit-state')):
1656 if not os.path.exists(repo.vfs.join('histedit-state')):
1657 return
1657 return
1658 state = histeditstate(repo)
1658 state = histeditstate(repo)
1659 state.read()
1659 state.read()
1660 if state.actions:
1660 if state.actions:
1661 # i18n: column positioning for "hg summary"
1661 # i18n: column positioning for "hg summary"
1662 ui.write(_('hist: %s (histedit --continue)\n') %
1662 ui.write(_('hist: %s (histedit --continue)\n') %
1663 (ui.label(_('%d remaining'), 'histedit.remaining') %
1663 (ui.label(_('%d remaining'), 'histedit.remaining') %
1664 len(state.actions)))
1664 len(state.actions)))
1665
1665
1666 def extsetup(ui):
1666 def extsetup(ui):
1667 cmdutil.summaryhooks.add('histedit', summaryhook)
1667 cmdutil.summaryhooks.add('histedit', summaryhook)
1668 cmdutil.unfinishedstates.append(
1668 cmdutil.unfinishedstates.append(
1669 ['histedit-state', False, True, _('histedit in progress'),
1669 ['histedit-state', False, True, _('histedit in progress'),
1670 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1670 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1671 cmdutil.afterresolvedstates.append(
1671 cmdutil.afterresolvedstates.append(
1672 ['histedit-state', _('hg histedit --continue')])
1672 ['histedit-state', _('hg histedit --continue')])
1673 if ui.configbool("experimental", "histeditng"):
1673 if ui.configbool("experimental", "histeditng"):
1674 globals()['base'] = action(['base', 'b'],
1674 globals()['base'] = action(['base', 'b'],
1675 _('checkout changeset and apply further changesets from there')
1675 _('checkout changeset and apply further changesets from there')
1676 )(base)
1676 )(base)
@@ -1,1049 +1,1049 b''
1 # shelve.py - save/restore working directory state
1 # shelve.py - save/restore working directory state
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
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
7
8 """save and restore changes to the working directory
8 """save and restore changes to the working directory
9
9
10 The "hg shelve" command saves changes made to the working directory
10 The "hg shelve" command saves changes made to the working directory
11 and reverts those changes, resetting the working directory to a clean
11 and reverts those changes, resetting the working directory to a clean
12 state.
12 state.
13
13
14 Later on, the "hg unshelve" command restores the changes saved by "hg
14 Later on, the "hg unshelve" command restores the changes saved by "hg
15 shelve". Changes can be restored even after updating to a different
15 shelve". Changes can be restored even after updating to a different
16 parent, in which case Mercurial's merge machinery will resolve any
16 parent, in which case Mercurial's merge machinery will resolve any
17 conflicts if necessary.
17 conflicts if necessary.
18
18
19 You can have more than one shelved change outstanding at a time; each
19 You can have more than one shelved change outstanding at a time; each
20 shelved change has a distinct name. For details, see the help for "hg
20 shelved change has a distinct name. For details, see the help for "hg
21 shelve".
21 shelve".
22 """
22 """
23 from __future__ import absolute_import
23 from __future__ import absolute_import
24
24
25 import collections
25 import collections
26 import errno
26 import errno
27 import itertools
27 import itertools
28
28
29 from mercurial.i18n import _
29 from mercurial.i18n import _
30 from mercurial import (
30 from mercurial import (
31 bookmarks,
31 bookmarks,
32 bundle2,
32 bundle2,
33 bundlerepo,
33 bundlerepo,
34 changegroup,
34 changegroup,
35 cmdutil,
35 cmdutil,
36 error,
36 error,
37 exchange,
37 exchange,
38 hg,
38 hg,
39 lock as lockmod,
39 lock as lockmod,
40 mdiff,
40 mdiff,
41 merge,
41 merge,
42 node as nodemod,
42 node as nodemod,
43 patch,
43 patch,
44 phases,
44 phases,
45 registrar,
45 registrar,
46 repair,
46 repair,
47 scmutil,
47 scmutil,
48 templatefilters,
48 templatefilters,
49 util,
49 util,
50 vfs as vfsmod,
50 vfs as vfsmod,
51 )
51 )
52
52
53 from . import (
53 from . import (
54 rebase,
54 rebase,
55 )
55 )
56
56
57 cmdtable = {}
57 cmdtable = {}
58 command = registrar.command(cmdtable)
58 command = registrar.command(cmdtable)
59 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
59 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
60 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
60 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
61 # be specifying the version(s) of Mercurial they are tested with, or
61 # be specifying the version(s) of Mercurial they are tested with, or
62 # leave the attribute unspecified.
62 # leave the attribute unspecified.
63 testedwith = 'ships-with-hg-core'
63 testedwith = 'ships-with-hg-core'
64
64
65 backupdir = 'shelve-backup'
65 backupdir = 'shelve-backup'
66 shelvedir = 'shelved'
66 shelvedir = 'shelved'
67 shelvefileextensions = ['hg', 'patch', 'oshelve']
67 shelvefileextensions = ['hg', 'patch', 'oshelve']
68 # universal extension is present in all types of shelves
68 # universal extension is present in all types of shelves
69 patchextension = 'patch'
69 patchextension = 'patch'
70
70
71 # we never need the user, so we use a
71 # we never need the user, so we use a
72 # generic user for all shelve operations
72 # generic user for all shelve operations
73 shelveuser = 'shelve@localhost'
73 shelveuser = 'shelve@localhost'
74
74
75 class shelvedfile(object):
75 class shelvedfile(object):
76 """Helper for the file storing a single shelve
76 """Helper for the file storing a single shelve
77
77
78 Handles common functions on shelve files (.hg/.patch) using
78 Handles common functions on shelve files (.hg/.patch) using
79 the vfs layer"""
79 the vfs layer"""
80 def __init__(self, repo, name, filetype=None):
80 def __init__(self, repo, name, filetype=None):
81 self.repo = repo
81 self.repo = repo
82 self.name = name
82 self.name = name
83 self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
83 self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
84 self.backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
84 self.backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
85 self.ui = self.repo.ui
85 self.ui = self.repo.ui
86 if filetype:
86 if filetype:
87 self.fname = name + '.' + filetype
87 self.fname = name + '.' + filetype
88 else:
88 else:
89 self.fname = name
89 self.fname = name
90
90
91 def exists(self):
91 def exists(self):
92 return self.vfs.exists(self.fname)
92 return self.vfs.exists(self.fname)
93
93
94 def filename(self):
94 def filename(self):
95 return self.vfs.join(self.fname)
95 return self.vfs.join(self.fname)
96
96
97 def backupfilename(self):
97 def backupfilename(self):
98 def gennames(base):
98 def gennames(base):
99 yield base
99 yield base
100 base, ext = base.rsplit('.', 1)
100 base, ext = base.rsplit('.', 1)
101 for i in itertools.count(1):
101 for i in itertools.count(1):
102 yield '%s-%d.%s' % (base, i, ext)
102 yield '%s-%d.%s' % (base, i, ext)
103
103
104 name = self.backupvfs.join(self.fname)
104 name = self.backupvfs.join(self.fname)
105 for n in gennames(name):
105 for n in gennames(name):
106 if not self.backupvfs.exists(n):
106 if not self.backupvfs.exists(n):
107 return n
107 return n
108
108
109 def movetobackup(self):
109 def movetobackup(self):
110 if not self.backupvfs.isdir():
110 if not self.backupvfs.isdir():
111 self.backupvfs.makedir()
111 self.backupvfs.makedir()
112 util.rename(self.filename(), self.backupfilename())
112 util.rename(self.filename(), self.backupfilename())
113
113
114 def stat(self):
114 def stat(self):
115 return self.vfs.stat(self.fname)
115 return self.vfs.stat(self.fname)
116
116
117 def opener(self, mode='rb'):
117 def opener(self, mode='rb'):
118 try:
118 try:
119 return self.vfs(self.fname, mode)
119 return self.vfs(self.fname, mode)
120 except IOError as err:
120 except IOError as err:
121 if err.errno != errno.ENOENT:
121 if err.errno != errno.ENOENT:
122 raise
122 raise
123 raise error.Abort(_("shelved change '%s' not found") % self.name)
123 raise error.Abort(_("shelved change '%s' not found") % self.name)
124
124
125 def applybundle(self):
125 def applybundle(self):
126 fp = self.opener()
126 fp = self.opener()
127 try:
127 try:
128 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
128 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
129 if not isinstance(gen, bundle2.unbundle20):
129 if not isinstance(gen, bundle2.unbundle20):
130 gen.apply(self.repo, 'unshelve',
130 gen.apply(self.repo, 'unshelve',
131 'bundle:' + self.vfs.join(self.fname),
131 'bundle:' + self.vfs.join(self.fname),
132 targetphase=phases.secret)
132 targetphase=phases.secret)
133 if isinstance(gen, bundle2.unbundle20):
133 else:
134 bundle2.applybundle(self.repo, gen,
134 bundle2.applybundle(self.repo, gen,
135 self.repo.currenttransaction(),
135 self.repo.currenttransaction(),
136 source='unshelve',
136 source='unshelve',
137 url='bundle:' + self.vfs.join(self.fname))
137 url='bundle:' + self.vfs.join(self.fname))
138 finally:
138 finally:
139 fp.close()
139 fp.close()
140
140
141 def bundlerepo(self):
141 def bundlerepo(self):
142 return bundlerepo.bundlerepository(self.repo.baseui, self.repo.root,
142 return bundlerepo.bundlerepository(self.repo.baseui, self.repo.root,
143 self.vfs.join(self.fname))
143 self.vfs.join(self.fname))
144 def writebundle(self, bases, node):
144 def writebundle(self, bases, node):
145 cgversion = changegroup.safeversion(self.repo)
145 cgversion = changegroup.safeversion(self.repo)
146 if cgversion == '01':
146 if cgversion == '01':
147 btype = 'HG10BZ'
147 btype = 'HG10BZ'
148 compression = None
148 compression = None
149 else:
149 else:
150 btype = 'HG20'
150 btype = 'HG20'
151 compression = 'BZ'
151 compression = 'BZ'
152
152
153 cg = changegroup.changegroupsubset(self.repo, bases, [node], 'shelve',
153 cg = changegroup.changegroupsubset(self.repo, bases, [node], 'shelve',
154 version=cgversion)
154 version=cgversion)
155 bundle2.writebundle(self.ui, cg, self.fname, btype, self.vfs,
155 bundle2.writebundle(self.ui, cg, self.fname, btype, self.vfs,
156 compression=compression)
156 compression=compression)
157
157
158 def writeobsshelveinfo(self, info):
158 def writeobsshelveinfo(self, info):
159 scmutil.simplekeyvaluefile(self.vfs, self.fname).write(info)
159 scmutil.simplekeyvaluefile(self.vfs, self.fname).write(info)
160
160
161 def readobsshelveinfo(self):
161 def readobsshelveinfo(self):
162 return scmutil.simplekeyvaluefile(self.vfs, self.fname).read()
162 return scmutil.simplekeyvaluefile(self.vfs, self.fname).read()
163
163
164 class shelvedstate(object):
164 class shelvedstate(object):
165 """Handle persistence during unshelving operations.
165 """Handle persistence during unshelving operations.
166
166
167 Handles saving and restoring a shelved state. Ensures that different
167 Handles saving and restoring a shelved state. Ensures that different
168 versions of a shelved state are possible and handles them appropriately.
168 versions of a shelved state are possible and handles them appropriately.
169 """
169 """
170 _version = 2
170 _version = 2
171 _filename = 'shelvedstate'
171 _filename = 'shelvedstate'
172 _keep = 'keep'
172 _keep = 'keep'
173 _nokeep = 'nokeep'
173 _nokeep = 'nokeep'
174 # colon is essential to differentiate from a real bookmark name
174 # colon is essential to differentiate from a real bookmark name
175 _noactivebook = ':no-active-bookmark'
175 _noactivebook = ':no-active-bookmark'
176
176
177 @classmethod
177 @classmethod
178 def _verifyandtransform(cls, d):
178 def _verifyandtransform(cls, d):
179 """Some basic shelvestate syntactic verification and transformation"""
179 """Some basic shelvestate syntactic verification and transformation"""
180 try:
180 try:
181 d['originalwctx'] = nodemod.bin(d['originalwctx'])
181 d['originalwctx'] = nodemod.bin(d['originalwctx'])
182 d['pendingctx'] = nodemod.bin(d['pendingctx'])
182 d['pendingctx'] = nodemod.bin(d['pendingctx'])
183 d['parents'] = [nodemod.bin(h)
183 d['parents'] = [nodemod.bin(h)
184 for h in d['parents'].split(' ')]
184 for h in d['parents'].split(' ')]
185 d['nodestoremove'] = [nodemod.bin(h)
185 d['nodestoremove'] = [nodemod.bin(h)
186 for h in d['nodestoremove'].split(' ')]
186 for h in d['nodestoremove'].split(' ')]
187 except (ValueError, TypeError, KeyError) as err:
187 except (ValueError, TypeError, KeyError) as err:
188 raise error.CorruptedState(str(err))
188 raise error.CorruptedState(str(err))
189
189
190 @classmethod
190 @classmethod
191 def _getversion(cls, repo):
191 def _getversion(cls, repo):
192 """Read version information from shelvestate file"""
192 """Read version information from shelvestate file"""
193 fp = repo.vfs(cls._filename)
193 fp = repo.vfs(cls._filename)
194 try:
194 try:
195 version = int(fp.readline().strip())
195 version = int(fp.readline().strip())
196 except ValueError as err:
196 except ValueError as err:
197 raise error.CorruptedState(str(err))
197 raise error.CorruptedState(str(err))
198 finally:
198 finally:
199 fp.close()
199 fp.close()
200 return version
200 return version
201
201
202 @classmethod
202 @classmethod
203 def _readold(cls, repo):
203 def _readold(cls, repo):
204 """Read the old position-based version of a shelvestate file"""
204 """Read the old position-based version of a shelvestate file"""
205 # Order is important, because old shelvestate file uses it
205 # Order is important, because old shelvestate file uses it
206 # to detemine values of fields (i.g. name is on the second line,
206 # to detemine values of fields (i.g. name is on the second line,
207 # originalwctx is on the third and so forth). Please do not change.
207 # originalwctx is on the third and so forth). Please do not change.
208 keys = ['version', 'name', 'originalwctx', 'pendingctx', 'parents',
208 keys = ['version', 'name', 'originalwctx', 'pendingctx', 'parents',
209 'nodestoremove', 'branchtorestore', 'keep', 'activebook']
209 'nodestoremove', 'branchtorestore', 'keep', 'activebook']
210 # this is executed only seldomly, so it is not a big deal
210 # this is executed only seldomly, so it is not a big deal
211 # that we open this file twice
211 # that we open this file twice
212 fp = repo.vfs(cls._filename)
212 fp = repo.vfs(cls._filename)
213 d = {}
213 d = {}
214 try:
214 try:
215 for key in keys:
215 for key in keys:
216 d[key] = fp.readline().strip()
216 d[key] = fp.readline().strip()
217 finally:
217 finally:
218 fp.close()
218 fp.close()
219 return d
219 return d
220
220
221 @classmethod
221 @classmethod
222 def load(cls, repo):
222 def load(cls, repo):
223 version = cls._getversion(repo)
223 version = cls._getversion(repo)
224 if version < cls._version:
224 if version < cls._version:
225 d = cls._readold(repo)
225 d = cls._readold(repo)
226 elif version == cls._version:
226 elif version == cls._version:
227 d = scmutil.simplekeyvaluefile(repo.vfs, cls._filename)\
227 d = scmutil.simplekeyvaluefile(repo.vfs, cls._filename)\
228 .read(firstlinenonkeyval=True)
228 .read(firstlinenonkeyval=True)
229 else:
229 else:
230 raise error.Abort(_('this version of shelve is incompatible '
230 raise error.Abort(_('this version of shelve is incompatible '
231 'with the version used in this repo'))
231 'with the version used in this repo'))
232
232
233 cls._verifyandtransform(d)
233 cls._verifyandtransform(d)
234 try:
234 try:
235 obj = cls()
235 obj = cls()
236 obj.name = d['name']
236 obj.name = d['name']
237 obj.wctx = repo[d['originalwctx']]
237 obj.wctx = repo[d['originalwctx']]
238 obj.pendingctx = repo[d['pendingctx']]
238 obj.pendingctx = repo[d['pendingctx']]
239 obj.parents = d['parents']
239 obj.parents = d['parents']
240 obj.nodestoremove = d['nodestoremove']
240 obj.nodestoremove = d['nodestoremove']
241 obj.branchtorestore = d.get('branchtorestore', '')
241 obj.branchtorestore = d.get('branchtorestore', '')
242 obj.keep = d.get('keep') == cls._keep
242 obj.keep = d.get('keep') == cls._keep
243 obj.activebookmark = ''
243 obj.activebookmark = ''
244 if d.get('activebook', '') != cls._noactivebook:
244 if d.get('activebook', '') != cls._noactivebook:
245 obj.activebookmark = d.get('activebook', '')
245 obj.activebookmark = d.get('activebook', '')
246 except (error.RepoLookupError, KeyError) as err:
246 except (error.RepoLookupError, KeyError) as err:
247 raise error.CorruptedState(str(err))
247 raise error.CorruptedState(str(err))
248
248
249 return obj
249 return obj
250
250
251 @classmethod
251 @classmethod
252 def save(cls, repo, name, originalwctx, pendingctx, nodestoremove,
252 def save(cls, repo, name, originalwctx, pendingctx, nodestoremove,
253 branchtorestore, keep=False, activebook=''):
253 branchtorestore, keep=False, activebook=''):
254 info = {
254 info = {
255 "name": name,
255 "name": name,
256 "originalwctx": nodemod.hex(originalwctx.node()),
256 "originalwctx": nodemod.hex(originalwctx.node()),
257 "pendingctx": nodemod.hex(pendingctx.node()),
257 "pendingctx": nodemod.hex(pendingctx.node()),
258 "parents": ' '.join([nodemod.hex(p)
258 "parents": ' '.join([nodemod.hex(p)
259 for p in repo.dirstate.parents()]),
259 for p in repo.dirstate.parents()]),
260 "nodestoremove": ' '.join([nodemod.hex(n)
260 "nodestoremove": ' '.join([nodemod.hex(n)
261 for n in nodestoremove]),
261 for n in nodestoremove]),
262 "branchtorestore": branchtorestore,
262 "branchtorestore": branchtorestore,
263 "keep": cls._keep if keep else cls._nokeep,
263 "keep": cls._keep if keep else cls._nokeep,
264 "activebook": activebook or cls._noactivebook
264 "activebook": activebook or cls._noactivebook
265 }
265 }
266 scmutil.simplekeyvaluefile(repo.vfs, cls._filename)\
266 scmutil.simplekeyvaluefile(repo.vfs, cls._filename)\
267 .write(info, firstline=str(cls._version))
267 .write(info, firstline=str(cls._version))
268
268
269 @classmethod
269 @classmethod
270 def clear(cls, repo):
270 def clear(cls, repo):
271 repo.vfs.unlinkpath(cls._filename, ignoremissing=True)
271 repo.vfs.unlinkpath(cls._filename, ignoremissing=True)
272
272
273 def cleanupoldbackups(repo):
273 def cleanupoldbackups(repo):
274 vfs = vfsmod.vfs(repo.vfs.join(backupdir))
274 vfs = vfsmod.vfs(repo.vfs.join(backupdir))
275 maxbackups = repo.ui.configint('shelve', 'maxbackups', 10)
275 maxbackups = repo.ui.configint('shelve', 'maxbackups', 10)
276 hgfiles = [f for f in vfs.listdir()
276 hgfiles = [f for f in vfs.listdir()
277 if f.endswith('.' + patchextension)]
277 if f.endswith('.' + patchextension)]
278 hgfiles = sorted([(vfs.stat(f).st_mtime, f) for f in hgfiles])
278 hgfiles = sorted([(vfs.stat(f).st_mtime, f) for f in hgfiles])
279 if 0 < maxbackups and maxbackups < len(hgfiles):
279 if 0 < maxbackups and maxbackups < len(hgfiles):
280 bordermtime = hgfiles[-maxbackups][0]
280 bordermtime = hgfiles[-maxbackups][0]
281 else:
281 else:
282 bordermtime = None
282 bordermtime = None
283 for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
283 for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
284 if mtime == bordermtime:
284 if mtime == bordermtime:
285 # keep it, because timestamp can't decide exact order of backups
285 # keep it, because timestamp can't decide exact order of backups
286 continue
286 continue
287 base = f[:-(1 + len(patchextension))]
287 base = f[:-(1 + len(patchextension))]
288 for ext in shelvefileextensions:
288 for ext in shelvefileextensions:
289 vfs.tryunlink(base + '.' + ext)
289 vfs.tryunlink(base + '.' + ext)
290
290
291 def _backupactivebookmark(repo):
291 def _backupactivebookmark(repo):
292 activebookmark = repo._activebookmark
292 activebookmark = repo._activebookmark
293 if activebookmark:
293 if activebookmark:
294 bookmarks.deactivate(repo)
294 bookmarks.deactivate(repo)
295 return activebookmark
295 return activebookmark
296
296
297 def _restoreactivebookmark(repo, mark):
297 def _restoreactivebookmark(repo, mark):
298 if mark:
298 if mark:
299 bookmarks.activate(repo, mark)
299 bookmarks.activate(repo, mark)
300
300
301 def _aborttransaction(repo):
301 def _aborttransaction(repo):
302 '''Abort current transaction for shelve/unshelve, but keep dirstate
302 '''Abort current transaction for shelve/unshelve, but keep dirstate
303 '''
303 '''
304 tr = repo.currenttransaction()
304 tr = repo.currenttransaction()
305 repo.dirstate.savebackup(tr, suffix='.shelve')
305 repo.dirstate.savebackup(tr, suffix='.shelve')
306 tr.abort()
306 tr.abort()
307 repo.dirstate.restorebackup(None, suffix='.shelve')
307 repo.dirstate.restorebackup(None, suffix='.shelve')
308
308
309 def createcmd(ui, repo, pats, opts):
309 def createcmd(ui, repo, pats, opts):
310 """subcommand that creates a new shelve"""
310 """subcommand that creates a new shelve"""
311 with repo.wlock():
311 with repo.wlock():
312 cmdutil.checkunfinished(repo)
312 cmdutil.checkunfinished(repo)
313 return _docreatecmd(ui, repo, pats, opts)
313 return _docreatecmd(ui, repo, pats, opts)
314
314
315 def getshelvename(repo, parent, opts):
315 def getshelvename(repo, parent, opts):
316 """Decide on the name this shelve is going to have"""
316 """Decide on the name this shelve is going to have"""
317 def gennames():
317 def gennames():
318 yield label
318 yield label
319 for i in xrange(1, 100):
319 for i in xrange(1, 100):
320 yield '%s-%02d' % (label, i)
320 yield '%s-%02d' % (label, i)
321 name = opts.get('name')
321 name = opts.get('name')
322 label = repo._activebookmark or parent.branch() or 'default'
322 label = repo._activebookmark or parent.branch() or 'default'
323 # slashes aren't allowed in filenames, therefore we rename it
323 # slashes aren't allowed in filenames, therefore we rename it
324 label = label.replace('/', '_')
324 label = label.replace('/', '_')
325 label = label.replace('\\', '_')
325 label = label.replace('\\', '_')
326 # filenames must not start with '.' as it should not be hidden
326 # filenames must not start with '.' as it should not be hidden
327 if label.startswith('.'):
327 if label.startswith('.'):
328 label = label.replace('.', '_', 1)
328 label = label.replace('.', '_', 1)
329
329
330 if name:
330 if name:
331 if shelvedfile(repo, name, patchextension).exists():
331 if shelvedfile(repo, name, patchextension).exists():
332 e = _("a shelved change named '%s' already exists") % name
332 e = _("a shelved change named '%s' already exists") % name
333 raise error.Abort(e)
333 raise error.Abort(e)
334
334
335 # ensure we are not creating a subdirectory or a hidden file
335 # ensure we are not creating a subdirectory or a hidden file
336 if '/' in name or '\\' in name:
336 if '/' in name or '\\' in name:
337 raise error.Abort(_('shelved change names can not contain slashes'))
337 raise error.Abort(_('shelved change names can not contain slashes'))
338 if name.startswith('.'):
338 if name.startswith('.'):
339 raise error.Abort(_("shelved change names can not start with '.'"))
339 raise error.Abort(_("shelved change names can not start with '.'"))
340
340
341 else:
341 else:
342 for n in gennames():
342 for n in gennames():
343 if not shelvedfile(repo, n, patchextension).exists():
343 if not shelvedfile(repo, n, patchextension).exists():
344 name = n
344 name = n
345 break
345 break
346 else:
346 else:
347 raise error.Abort(_("too many shelved changes named '%s'") % label)
347 raise error.Abort(_("too many shelved changes named '%s'") % label)
348
348
349 return name
349 return name
350
350
351 def mutableancestors(ctx):
351 def mutableancestors(ctx):
352 """return all mutable ancestors for ctx (included)
352 """return all mutable ancestors for ctx (included)
353
353
354 Much faster than the revset ancestors(ctx) & draft()"""
354 Much faster than the revset ancestors(ctx) & draft()"""
355 seen = {nodemod.nullrev}
355 seen = {nodemod.nullrev}
356 visit = collections.deque()
356 visit = collections.deque()
357 visit.append(ctx)
357 visit.append(ctx)
358 while visit:
358 while visit:
359 ctx = visit.popleft()
359 ctx = visit.popleft()
360 yield ctx.node()
360 yield ctx.node()
361 for parent in ctx.parents():
361 for parent in ctx.parents():
362 rev = parent.rev()
362 rev = parent.rev()
363 if rev not in seen:
363 if rev not in seen:
364 seen.add(rev)
364 seen.add(rev)
365 if parent.mutable():
365 if parent.mutable():
366 visit.append(parent)
366 visit.append(parent)
367
367
368 def getcommitfunc(extra, interactive, editor=False):
368 def getcommitfunc(extra, interactive, editor=False):
369 def commitfunc(ui, repo, message, match, opts):
369 def commitfunc(ui, repo, message, match, opts):
370 hasmq = util.safehasattr(repo, 'mq')
370 hasmq = util.safehasattr(repo, 'mq')
371 if hasmq:
371 if hasmq:
372 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
372 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
373 overrides = {('phases', 'new-commit'): phases.secret}
373 overrides = {('phases', 'new-commit'): phases.secret}
374 try:
374 try:
375 editor_ = False
375 editor_ = False
376 if editor:
376 if editor:
377 editor_ = cmdutil.getcommiteditor(editform='shelve.shelve',
377 editor_ = cmdutil.getcommiteditor(editform='shelve.shelve',
378 **opts)
378 **opts)
379 with repo.ui.configoverride(overrides):
379 with repo.ui.configoverride(overrides):
380 return repo.commit(message, shelveuser, opts.get('date'),
380 return repo.commit(message, shelveuser, opts.get('date'),
381 match, editor=editor_, extra=extra)
381 match, editor=editor_, extra=extra)
382 finally:
382 finally:
383 if hasmq:
383 if hasmq:
384 repo.mq.checkapplied = saved
384 repo.mq.checkapplied = saved
385
385
386 def interactivecommitfunc(ui, repo, *pats, **opts):
386 def interactivecommitfunc(ui, repo, *pats, **opts):
387 match = scmutil.match(repo['.'], pats, {})
387 match = scmutil.match(repo['.'], pats, {})
388 message = opts['message']
388 message = opts['message']
389 return commitfunc(ui, repo, message, match, opts)
389 return commitfunc(ui, repo, message, match, opts)
390
390
391 return interactivecommitfunc if interactive else commitfunc
391 return interactivecommitfunc if interactive else commitfunc
392
392
393 def _nothingtoshelvemessaging(ui, repo, pats, opts):
393 def _nothingtoshelvemessaging(ui, repo, pats, opts):
394 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
394 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
395 if stat.deleted:
395 if stat.deleted:
396 ui.status(_("nothing changed (%d missing files, see "
396 ui.status(_("nothing changed (%d missing files, see "
397 "'hg status')\n") % len(stat.deleted))
397 "'hg status')\n") % len(stat.deleted))
398 else:
398 else:
399 ui.status(_("nothing changed\n"))
399 ui.status(_("nothing changed\n"))
400
400
401 def _shelvecreatedcommit(repo, node, name):
401 def _shelvecreatedcommit(repo, node, name):
402 bases = list(mutableancestors(repo[node]))
402 bases = list(mutableancestors(repo[node]))
403 shelvedfile(repo, name, 'hg').writebundle(bases, node)
403 shelvedfile(repo, name, 'hg').writebundle(bases, node)
404 cmdutil.export(repo, [node],
404 cmdutil.export(repo, [node],
405 fp=shelvedfile(repo, name, patchextension).opener('wb'),
405 fp=shelvedfile(repo, name, patchextension).opener('wb'),
406 opts=mdiff.diffopts(git=True))
406 opts=mdiff.diffopts(git=True))
407
407
408 def _includeunknownfiles(repo, pats, opts, extra):
408 def _includeunknownfiles(repo, pats, opts, extra):
409 s = repo.status(match=scmutil.match(repo[None], pats, opts),
409 s = repo.status(match=scmutil.match(repo[None], pats, opts),
410 unknown=True)
410 unknown=True)
411 if s.unknown:
411 if s.unknown:
412 extra['shelve_unknown'] = '\0'.join(s.unknown)
412 extra['shelve_unknown'] = '\0'.join(s.unknown)
413 repo[None].add(s.unknown)
413 repo[None].add(s.unknown)
414
414
415 def _finishshelve(repo):
415 def _finishshelve(repo):
416 _aborttransaction(repo)
416 _aborttransaction(repo)
417
417
418 def _docreatecmd(ui, repo, pats, opts):
418 def _docreatecmd(ui, repo, pats, opts):
419 wctx = repo[None]
419 wctx = repo[None]
420 parents = wctx.parents()
420 parents = wctx.parents()
421 if len(parents) > 1:
421 if len(parents) > 1:
422 raise error.Abort(_('cannot shelve while merging'))
422 raise error.Abort(_('cannot shelve while merging'))
423 parent = parents[0]
423 parent = parents[0]
424 origbranch = wctx.branch()
424 origbranch = wctx.branch()
425
425
426 if parent.node() != nodemod.nullid:
426 if parent.node() != nodemod.nullid:
427 desc = "changes to: %s" % parent.description().split('\n', 1)[0]
427 desc = "changes to: %s" % parent.description().split('\n', 1)[0]
428 else:
428 else:
429 desc = '(changes in empty repository)'
429 desc = '(changes in empty repository)'
430
430
431 if not opts.get('message'):
431 if not opts.get('message'):
432 opts['message'] = desc
432 opts['message'] = desc
433
433
434 lock = tr = activebookmark = None
434 lock = tr = activebookmark = None
435 try:
435 try:
436 lock = repo.lock()
436 lock = repo.lock()
437
437
438 # use an uncommitted transaction to generate the bundle to avoid
438 # use an uncommitted transaction to generate the bundle to avoid
439 # pull races. ensure we don't print the abort message to stderr.
439 # pull races. ensure we don't print the abort message to stderr.
440 tr = repo.transaction('commit', report=lambda x: None)
440 tr = repo.transaction('commit', report=lambda x: None)
441
441
442 interactive = opts.get('interactive', False)
442 interactive = opts.get('interactive', False)
443 includeunknown = (opts.get('unknown', False) and
443 includeunknown = (opts.get('unknown', False) and
444 not opts.get('addremove', False))
444 not opts.get('addremove', False))
445
445
446 name = getshelvename(repo, parent, opts)
446 name = getshelvename(repo, parent, opts)
447 activebookmark = _backupactivebookmark(repo)
447 activebookmark = _backupactivebookmark(repo)
448 extra = {}
448 extra = {}
449 if includeunknown:
449 if includeunknown:
450 _includeunknownfiles(repo, pats, opts, extra)
450 _includeunknownfiles(repo, pats, opts, extra)
451
451
452 if _iswctxonnewbranch(repo) and not _isbareshelve(pats, opts):
452 if _iswctxonnewbranch(repo) and not _isbareshelve(pats, opts):
453 # In non-bare shelve we don't store newly created branch
453 # In non-bare shelve we don't store newly created branch
454 # at bundled commit
454 # at bundled commit
455 repo.dirstate.setbranch(repo['.'].branch())
455 repo.dirstate.setbranch(repo['.'].branch())
456
456
457 commitfunc = getcommitfunc(extra, interactive, editor=True)
457 commitfunc = getcommitfunc(extra, interactive, editor=True)
458 if not interactive:
458 if not interactive:
459 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
459 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
460 else:
460 else:
461 node = cmdutil.dorecord(ui, repo, commitfunc, None,
461 node = cmdutil.dorecord(ui, repo, commitfunc, None,
462 False, cmdutil.recordfilter, *pats,
462 False, cmdutil.recordfilter, *pats,
463 **opts)
463 **opts)
464 if not node:
464 if not node:
465 _nothingtoshelvemessaging(ui, repo, pats, opts)
465 _nothingtoshelvemessaging(ui, repo, pats, opts)
466 return 1
466 return 1
467
467
468 _shelvecreatedcommit(repo, node, name)
468 _shelvecreatedcommit(repo, node, name)
469
469
470 if ui.formatted():
470 if ui.formatted():
471 desc = util.ellipsis(desc, ui.termwidth())
471 desc = util.ellipsis(desc, ui.termwidth())
472 ui.status(_('shelved as %s\n') % name)
472 ui.status(_('shelved as %s\n') % name)
473 hg.update(repo, parent.node())
473 hg.update(repo, parent.node())
474 if origbranch != repo['.'].branch() and not _isbareshelve(pats, opts):
474 if origbranch != repo['.'].branch() and not _isbareshelve(pats, opts):
475 repo.dirstate.setbranch(origbranch)
475 repo.dirstate.setbranch(origbranch)
476
476
477 _finishshelve(repo)
477 _finishshelve(repo)
478 finally:
478 finally:
479 _restoreactivebookmark(repo, activebookmark)
479 _restoreactivebookmark(repo, activebookmark)
480 lockmod.release(tr, lock)
480 lockmod.release(tr, lock)
481
481
482 def _isbareshelve(pats, opts):
482 def _isbareshelve(pats, opts):
483 return (not pats
483 return (not pats
484 and not opts.get('interactive', False)
484 and not opts.get('interactive', False)
485 and not opts.get('include', False)
485 and not opts.get('include', False)
486 and not opts.get('exclude', False))
486 and not opts.get('exclude', False))
487
487
488 def _iswctxonnewbranch(repo):
488 def _iswctxonnewbranch(repo):
489 return repo[None].branch() != repo['.'].branch()
489 return repo[None].branch() != repo['.'].branch()
490
490
491 def cleanupcmd(ui, repo):
491 def cleanupcmd(ui, repo):
492 """subcommand that deletes all shelves"""
492 """subcommand that deletes all shelves"""
493
493
494 with repo.wlock():
494 with repo.wlock():
495 for (name, _type) in repo.vfs.readdir(shelvedir):
495 for (name, _type) in repo.vfs.readdir(shelvedir):
496 suffix = name.rsplit('.', 1)[-1]
496 suffix = name.rsplit('.', 1)[-1]
497 if suffix in shelvefileextensions:
497 if suffix in shelvefileextensions:
498 shelvedfile(repo, name).movetobackup()
498 shelvedfile(repo, name).movetobackup()
499 cleanupoldbackups(repo)
499 cleanupoldbackups(repo)
500
500
501 def deletecmd(ui, repo, pats):
501 def deletecmd(ui, repo, pats):
502 """subcommand that deletes a specific shelve"""
502 """subcommand that deletes a specific shelve"""
503 if not pats:
503 if not pats:
504 raise error.Abort(_('no shelved changes specified!'))
504 raise error.Abort(_('no shelved changes specified!'))
505 with repo.wlock():
505 with repo.wlock():
506 try:
506 try:
507 for name in pats:
507 for name in pats:
508 for suffix in shelvefileextensions:
508 for suffix in shelvefileextensions:
509 shfile = shelvedfile(repo, name, suffix)
509 shfile = shelvedfile(repo, name, suffix)
510 # patch file is necessary, as it should
510 # patch file is necessary, as it should
511 # be present for any kind of shelve,
511 # be present for any kind of shelve,
512 # but the .hg file is optional as in future we
512 # but the .hg file is optional as in future we
513 # will add obsolete shelve with does not create a
513 # will add obsolete shelve with does not create a
514 # bundle
514 # bundle
515 if shfile.exists() or suffix == patchextension:
515 if shfile.exists() or suffix == patchextension:
516 shfile.movetobackup()
516 shfile.movetobackup()
517 cleanupoldbackups(repo)
517 cleanupoldbackups(repo)
518 except OSError as err:
518 except OSError as err:
519 if err.errno != errno.ENOENT:
519 if err.errno != errno.ENOENT:
520 raise
520 raise
521 raise error.Abort(_("shelved change '%s' not found") % name)
521 raise error.Abort(_("shelved change '%s' not found") % name)
522
522
523 def listshelves(repo):
523 def listshelves(repo):
524 """return all shelves in repo as list of (time, filename)"""
524 """return all shelves in repo as list of (time, filename)"""
525 try:
525 try:
526 names = repo.vfs.readdir(shelvedir)
526 names = repo.vfs.readdir(shelvedir)
527 except OSError as err:
527 except OSError as err:
528 if err.errno != errno.ENOENT:
528 if err.errno != errno.ENOENT:
529 raise
529 raise
530 return []
530 return []
531 info = []
531 info = []
532 for (name, _type) in names:
532 for (name, _type) in names:
533 pfx, sfx = name.rsplit('.', 1)
533 pfx, sfx = name.rsplit('.', 1)
534 if not pfx or sfx != patchextension:
534 if not pfx or sfx != patchextension:
535 continue
535 continue
536 st = shelvedfile(repo, name).stat()
536 st = shelvedfile(repo, name).stat()
537 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
537 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
538 return sorted(info, reverse=True)
538 return sorted(info, reverse=True)
539
539
540 def listcmd(ui, repo, pats, opts):
540 def listcmd(ui, repo, pats, opts):
541 """subcommand that displays the list of shelves"""
541 """subcommand that displays the list of shelves"""
542 pats = set(pats)
542 pats = set(pats)
543 width = 80
543 width = 80
544 if not ui.plain():
544 if not ui.plain():
545 width = ui.termwidth()
545 width = ui.termwidth()
546 namelabel = 'shelve.newest'
546 namelabel = 'shelve.newest'
547 ui.pager('shelve')
547 ui.pager('shelve')
548 for mtime, name in listshelves(repo):
548 for mtime, name in listshelves(repo):
549 sname = util.split(name)[1]
549 sname = util.split(name)[1]
550 if pats and sname not in pats:
550 if pats and sname not in pats:
551 continue
551 continue
552 ui.write(sname, label=namelabel)
552 ui.write(sname, label=namelabel)
553 namelabel = 'shelve.name'
553 namelabel = 'shelve.name'
554 if ui.quiet:
554 if ui.quiet:
555 ui.write('\n')
555 ui.write('\n')
556 continue
556 continue
557 ui.write(' ' * (16 - len(sname)))
557 ui.write(' ' * (16 - len(sname)))
558 used = 16
558 used = 16
559 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
559 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
560 ui.write(age, label='shelve.age')
560 ui.write(age, label='shelve.age')
561 ui.write(' ' * (12 - len(age)))
561 ui.write(' ' * (12 - len(age)))
562 used += 12
562 used += 12
563 with open(name + '.' + patchextension, 'rb') as fp:
563 with open(name + '.' + patchextension, 'rb') as fp:
564 while True:
564 while True:
565 line = fp.readline()
565 line = fp.readline()
566 if not line:
566 if not line:
567 break
567 break
568 if not line.startswith('#'):
568 if not line.startswith('#'):
569 desc = line.rstrip()
569 desc = line.rstrip()
570 if ui.formatted():
570 if ui.formatted():
571 desc = util.ellipsis(desc, width - used)
571 desc = util.ellipsis(desc, width - used)
572 ui.write(desc)
572 ui.write(desc)
573 break
573 break
574 ui.write('\n')
574 ui.write('\n')
575 if not (opts['patch'] or opts['stat']):
575 if not (opts['patch'] or opts['stat']):
576 continue
576 continue
577 difflines = fp.readlines()
577 difflines = fp.readlines()
578 if opts['patch']:
578 if opts['patch']:
579 for chunk, label in patch.difflabel(iter, difflines):
579 for chunk, label in patch.difflabel(iter, difflines):
580 ui.write(chunk, label=label)
580 ui.write(chunk, label=label)
581 if opts['stat']:
581 if opts['stat']:
582 for chunk, label in patch.diffstatui(difflines, width=width):
582 for chunk, label in patch.diffstatui(difflines, width=width):
583 ui.write(chunk, label=label)
583 ui.write(chunk, label=label)
584
584
585 def patchcmds(ui, repo, pats, opts, subcommand):
585 def patchcmds(ui, repo, pats, opts, subcommand):
586 """subcommand that displays shelves"""
586 """subcommand that displays shelves"""
587 if len(pats) == 0:
587 if len(pats) == 0:
588 raise error.Abort(_("--%s expects at least one shelf") % subcommand)
588 raise error.Abort(_("--%s expects at least one shelf") % subcommand)
589
589
590 for shelfname in pats:
590 for shelfname in pats:
591 if not shelvedfile(repo, shelfname, patchextension).exists():
591 if not shelvedfile(repo, shelfname, patchextension).exists():
592 raise error.Abort(_("cannot find shelf %s") % shelfname)
592 raise error.Abort(_("cannot find shelf %s") % shelfname)
593
593
594 listcmd(ui, repo, pats, opts)
594 listcmd(ui, repo, pats, opts)
595
595
596 def checkparents(repo, state):
596 def checkparents(repo, state):
597 """check parent while resuming an unshelve"""
597 """check parent while resuming an unshelve"""
598 if state.parents != repo.dirstate.parents():
598 if state.parents != repo.dirstate.parents():
599 raise error.Abort(_('working directory parents do not match unshelve '
599 raise error.Abort(_('working directory parents do not match unshelve '
600 'state'))
600 'state'))
601
601
602 def pathtofiles(repo, files):
602 def pathtofiles(repo, files):
603 cwd = repo.getcwd()
603 cwd = repo.getcwd()
604 return [repo.pathto(f, cwd) for f in files]
604 return [repo.pathto(f, cwd) for f in files]
605
605
606 def unshelveabort(ui, repo, state, opts):
606 def unshelveabort(ui, repo, state, opts):
607 """subcommand that abort an in-progress unshelve"""
607 """subcommand that abort an in-progress unshelve"""
608 with repo.lock():
608 with repo.lock():
609 try:
609 try:
610 checkparents(repo, state)
610 checkparents(repo, state)
611
611
612 repo.vfs.rename('unshelverebasestate', 'rebasestate')
612 repo.vfs.rename('unshelverebasestate', 'rebasestate')
613 try:
613 try:
614 rebase.rebase(ui, repo, **{
614 rebase.rebase(ui, repo, **{
615 'abort' : True
615 'abort' : True
616 })
616 })
617 except Exception:
617 except Exception:
618 repo.vfs.rename('rebasestate', 'unshelverebasestate')
618 repo.vfs.rename('rebasestate', 'unshelverebasestate')
619 raise
619 raise
620
620
621 mergefiles(ui, repo, state.wctx, state.pendingctx)
621 mergefiles(ui, repo, state.wctx, state.pendingctx)
622 repair.strip(ui, repo, state.nodestoremove, backup=False,
622 repair.strip(ui, repo, state.nodestoremove, backup=False,
623 topic='shelve')
623 topic='shelve')
624 finally:
624 finally:
625 shelvedstate.clear(repo)
625 shelvedstate.clear(repo)
626 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
626 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
627
627
628 def mergefiles(ui, repo, wctx, shelvectx):
628 def mergefiles(ui, repo, wctx, shelvectx):
629 """updates to wctx and merges the changes from shelvectx into the
629 """updates to wctx and merges the changes from shelvectx into the
630 dirstate."""
630 dirstate."""
631 with ui.configoverride({('ui', 'quiet'): True}):
631 with ui.configoverride({('ui', 'quiet'): True}):
632 hg.update(repo, wctx.node())
632 hg.update(repo, wctx.node())
633 files = []
633 files = []
634 files.extend(shelvectx.files())
634 files.extend(shelvectx.files())
635 files.extend(shelvectx.parents()[0].files())
635 files.extend(shelvectx.parents()[0].files())
636
636
637 # revert will overwrite unknown files, so move them out of the way
637 # revert will overwrite unknown files, so move them out of the way
638 for file in repo.status(unknown=True).unknown:
638 for file in repo.status(unknown=True).unknown:
639 if file in files:
639 if file in files:
640 util.rename(file, scmutil.origpath(ui, repo, file))
640 util.rename(file, scmutil.origpath(ui, repo, file))
641 ui.pushbuffer(True)
641 ui.pushbuffer(True)
642 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
642 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
643 *pathtofiles(repo, files),
643 *pathtofiles(repo, files),
644 **{'no_backup': True})
644 **{'no_backup': True})
645 ui.popbuffer()
645 ui.popbuffer()
646
646
647 def restorebranch(ui, repo, branchtorestore):
647 def restorebranch(ui, repo, branchtorestore):
648 if branchtorestore and branchtorestore != repo.dirstate.branch():
648 if branchtorestore and branchtorestore != repo.dirstate.branch():
649 repo.dirstate.setbranch(branchtorestore)
649 repo.dirstate.setbranch(branchtorestore)
650 ui.status(_('marked working directory as branch %s\n')
650 ui.status(_('marked working directory as branch %s\n')
651 % branchtorestore)
651 % branchtorestore)
652
652
653 def unshelvecleanup(ui, repo, name, opts):
653 def unshelvecleanup(ui, repo, name, opts):
654 """remove related files after an unshelve"""
654 """remove related files after an unshelve"""
655 if not opts.get('keep'):
655 if not opts.get('keep'):
656 for filetype in shelvefileextensions:
656 for filetype in shelvefileextensions:
657 shfile = shelvedfile(repo, name, filetype)
657 shfile = shelvedfile(repo, name, filetype)
658 if shfile.exists():
658 if shfile.exists():
659 shfile.movetobackup()
659 shfile.movetobackup()
660 cleanupoldbackups(repo)
660 cleanupoldbackups(repo)
661
661
662 def unshelvecontinue(ui, repo, state, opts):
662 def unshelvecontinue(ui, repo, state, opts):
663 """subcommand to continue an in-progress unshelve"""
663 """subcommand to continue an in-progress unshelve"""
664 # We're finishing off a merge. First parent is our original
664 # We're finishing off a merge. First parent is our original
665 # parent, second is the temporary "fake" commit we're unshelving.
665 # parent, second is the temporary "fake" commit we're unshelving.
666 with repo.lock():
666 with repo.lock():
667 checkparents(repo, state)
667 checkparents(repo, state)
668 ms = merge.mergestate.read(repo)
668 ms = merge.mergestate.read(repo)
669 if [f for f in ms if ms[f] == 'u']:
669 if [f for f in ms if ms[f] == 'u']:
670 raise error.Abort(
670 raise error.Abort(
671 _("unresolved conflicts, can't continue"),
671 _("unresolved conflicts, can't continue"),
672 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
672 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
673
673
674 repo.vfs.rename('unshelverebasestate', 'rebasestate')
674 repo.vfs.rename('unshelverebasestate', 'rebasestate')
675 try:
675 try:
676 rebase.rebase(ui, repo, **{
676 rebase.rebase(ui, repo, **{
677 'continue' : True
677 'continue' : True
678 })
678 })
679 except Exception:
679 except Exception:
680 repo.vfs.rename('rebasestate', 'unshelverebasestate')
680 repo.vfs.rename('rebasestate', 'unshelverebasestate')
681 raise
681 raise
682
682
683 shelvectx = repo['tip']
683 shelvectx = repo['tip']
684 if not shelvectx in state.pendingctx.children():
684 if not shelvectx in state.pendingctx.children():
685 # rebase was a no-op, so it produced no child commit
685 # rebase was a no-op, so it produced no child commit
686 shelvectx = state.pendingctx
686 shelvectx = state.pendingctx
687 else:
687 else:
688 # only strip the shelvectx if the rebase produced it
688 # only strip the shelvectx if the rebase produced it
689 state.nodestoremove.append(shelvectx.node())
689 state.nodestoremove.append(shelvectx.node())
690
690
691 mergefiles(ui, repo, state.wctx, shelvectx)
691 mergefiles(ui, repo, state.wctx, shelvectx)
692 restorebranch(ui, repo, state.branchtorestore)
692 restorebranch(ui, repo, state.branchtorestore)
693
693
694 repair.strip(ui, repo, state.nodestoremove, backup=False,
694 repair.strip(ui, repo, state.nodestoremove, backup=False,
695 topic='shelve')
695 topic='shelve')
696 _restoreactivebookmark(repo, state.activebookmark)
696 _restoreactivebookmark(repo, state.activebookmark)
697 shelvedstate.clear(repo)
697 shelvedstate.clear(repo)
698 unshelvecleanup(ui, repo, state.name, opts)
698 unshelvecleanup(ui, repo, state.name, opts)
699 ui.status(_("unshelve of '%s' complete\n") % state.name)
699 ui.status(_("unshelve of '%s' complete\n") % state.name)
700
700
701 def _commitworkingcopychanges(ui, repo, opts, tmpwctx):
701 def _commitworkingcopychanges(ui, repo, opts, tmpwctx):
702 """Temporarily commit working copy changes before moving unshelve commit"""
702 """Temporarily commit working copy changes before moving unshelve commit"""
703 # Store pending changes in a commit and remember added in case a shelve
703 # Store pending changes in a commit and remember added in case a shelve
704 # contains unknown files that are part of the pending change
704 # contains unknown files that are part of the pending change
705 s = repo.status()
705 s = repo.status()
706 addedbefore = frozenset(s.added)
706 addedbefore = frozenset(s.added)
707 if not (s.modified or s.added or s.removed):
707 if not (s.modified or s.added or s.removed):
708 return tmpwctx, addedbefore
708 return tmpwctx, addedbefore
709 ui.status(_("temporarily committing pending changes "
709 ui.status(_("temporarily committing pending changes "
710 "(restore with 'hg unshelve --abort')\n"))
710 "(restore with 'hg unshelve --abort')\n"))
711 commitfunc = getcommitfunc(extra=None, interactive=False,
711 commitfunc = getcommitfunc(extra=None, interactive=False,
712 editor=False)
712 editor=False)
713 tempopts = {}
713 tempopts = {}
714 tempopts['message'] = "pending changes temporary commit"
714 tempopts['message'] = "pending changes temporary commit"
715 tempopts['date'] = opts.get('date')
715 tempopts['date'] = opts.get('date')
716 with ui.configoverride({('ui', 'quiet'): True}):
716 with ui.configoverride({('ui', 'quiet'): True}):
717 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
717 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
718 tmpwctx = repo[node]
718 tmpwctx = repo[node]
719 return tmpwctx, addedbefore
719 return tmpwctx, addedbefore
720
720
721 def _unshelverestorecommit(ui, repo, basename):
721 def _unshelverestorecommit(ui, repo, basename):
722 """Recreate commit in the repository during the unshelve"""
722 """Recreate commit in the repository during the unshelve"""
723 with ui.configoverride({('ui', 'quiet'): True}):
723 with ui.configoverride({('ui', 'quiet'): True}):
724 shelvedfile(repo, basename, 'hg').applybundle()
724 shelvedfile(repo, basename, 'hg').applybundle()
725 shelvectx = repo['tip']
725 shelvectx = repo['tip']
726 return repo, shelvectx
726 return repo, shelvectx
727
727
728 def _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev, basename, pctx,
728 def _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev, basename, pctx,
729 tmpwctx, shelvectx, branchtorestore,
729 tmpwctx, shelvectx, branchtorestore,
730 activebookmark):
730 activebookmark):
731 """Rebase restored commit from its original location to a destination"""
731 """Rebase restored commit from its original location to a destination"""
732 # If the shelve is not immediately on top of the commit
732 # If the shelve is not immediately on top of the commit
733 # we'll be merging with, rebase it to be on top.
733 # we'll be merging with, rebase it to be on top.
734 if tmpwctx.node() == shelvectx.parents()[0].node():
734 if tmpwctx.node() == shelvectx.parents()[0].node():
735 return shelvectx
735 return shelvectx
736
736
737 ui.status(_('rebasing shelved changes\n'))
737 ui.status(_('rebasing shelved changes\n'))
738 try:
738 try:
739 rebase.rebase(ui, repo, **{
739 rebase.rebase(ui, repo, **{
740 'rev': [shelvectx.rev()],
740 'rev': [shelvectx.rev()],
741 'dest': str(tmpwctx.rev()),
741 'dest': str(tmpwctx.rev()),
742 'keep': True,
742 'keep': True,
743 'tool': opts.get('tool', ''),
743 'tool': opts.get('tool', ''),
744 })
744 })
745 except error.InterventionRequired:
745 except error.InterventionRequired:
746 tr.close()
746 tr.close()
747
747
748 nodestoremove = [repo.changelog.node(rev)
748 nodestoremove = [repo.changelog.node(rev)
749 for rev in xrange(oldtiprev, len(repo))]
749 for rev in xrange(oldtiprev, len(repo))]
750 shelvedstate.save(repo, basename, pctx, tmpwctx, nodestoremove,
750 shelvedstate.save(repo, basename, pctx, tmpwctx, nodestoremove,
751 branchtorestore, opts.get('keep'), activebookmark)
751 branchtorestore, opts.get('keep'), activebookmark)
752
752
753 repo.vfs.rename('rebasestate', 'unshelverebasestate')
753 repo.vfs.rename('rebasestate', 'unshelverebasestate')
754 raise error.InterventionRequired(
754 raise error.InterventionRequired(
755 _("unresolved conflicts (see 'hg resolve', then "
755 _("unresolved conflicts (see 'hg resolve', then "
756 "'hg unshelve --continue')"))
756 "'hg unshelve --continue')"))
757
757
758 # refresh ctx after rebase completes
758 # refresh ctx after rebase completes
759 shelvectx = repo['tip']
759 shelvectx = repo['tip']
760
760
761 if not shelvectx in tmpwctx.children():
761 if not shelvectx in tmpwctx.children():
762 # rebase was a no-op, so it produced no child commit
762 # rebase was a no-op, so it produced no child commit
763 shelvectx = tmpwctx
763 shelvectx = tmpwctx
764 return shelvectx
764 return shelvectx
765
765
766 def _forgetunknownfiles(repo, shelvectx, addedbefore):
766 def _forgetunknownfiles(repo, shelvectx, addedbefore):
767 # Forget any files that were unknown before the shelve, unknown before
767 # Forget any files that were unknown before the shelve, unknown before
768 # unshelve started, but are now added.
768 # unshelve started, but are now added.
769 shelveunknown = shelvectx.extra().get('shelve_unknown')
769 shelveunknown = shelvectx.extra().get('shelve_unknown')
770 if not shelveunknown:
770 if not shelveunknown:
771 return
771 return
772 shelveunknown = frozenset(shelveunknown.split('\0'))
772 shelveunknown = frozenset(shelveunknown.split('\0'))
773 addedafter = frozenset(repo.status().added)
773 addedafter = frozenset(repo.status().added)
774 toforget = (addedafter & shelveunknown) - addedbefore
774 toforget = (addedafter & shelveunknown) - addedbefore
775 repo[None].forget(toforget)
775 repo[None].forget(toforget)
776
776
777 def _finishunshelve(repo, oldtiprev, tr, activebookmark):
777 def _finishunshelve(repo, oldtiprev, tr, activebookmark):
778 _restoreactivebookmark(repo, activebookmark)
778 _restoreactivebookmark(repo, activebookmark)
779 # The transaction aborting will strip all the commits for us,
779 # The transaction aborting will strip all the commits for us,
780 # but it doesn't update the inmemory structures, so addchangegroup
780 # but it doesn't update the inmemory structures, so addchangegroup
781 # hooks still fire and try to operate on the missing commits.
781 # hooks still fire and try to operate on the missing commits.
782 # Clean up manually to prevent this.
782 # Clean up manually to prevent this.
783 repo.unfiltered().changelog.strip(oldtiprev, tr)
783 repo.unfiltered().changelog.strip(oldtiprev, tr)
784 _aborttransaction(repo)
784 _aborttransaction(repo)
785
785
786 def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
786 def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
787 """Check potential problems which may result from working
787 """Check potential problems which may result from working
788 copy having untracked changes."""
788 copy having untracked changes."""
789 wcdeleted = set(repo.status().deleted)
789 wcdeleted = set(repo.status().deleted)
790 shelvetouched = set(shelvectx.files())
790 shelvetouched = set(shelvectx.files())
791 intersection = wcdeleted.intersection(shelvetouched)
791 intersection = wcdeleted.intersection(shelvetouched)
792 if intersection:
792 if intersection:
793 m = _("shelved change touches missing files")
793 m = _("shelved change touches missing files")
794 hint = _("run hg status to see which files are missing")
794 hint = _("run hg status to see which files are missing")
795 raise error.Abort(m, hint=hint)
795 raise error.Abort(m, hint=hint)
796
796
797 @command('unshelve',
797 @command('unshelve',
798 [('a', 'abort', None,
798 [('a', 'abort', None,
799 _('abort an incomplete unshelve operation')),
799 _('abort an incomplete unshelve operation')),
800 ('c', 'continue', None,
800 ('c', 'continue', None,
801 _('continue an incomplete unshelve operation')),
801 _('continue an incomplete unshelve operation')),
802 ('k', 'keep', None,
802 ('k', 'keep', None,
803 _('keep shelve after unshelving')),
803 _('keep shelve after unshelving')),
804 ('n', 'name', '',
804 ('n', 'name', '',
805 _('restore shelved change with given name'), _('NAME')),
805 _('restore shelved change with given name'), _('NAME')),
806 ('t', 'tool', '', _('specify merge tool')),
806 ('t', 'tool', '', _('specify merge tool')),
807 ('', 'date', '',
807 ('', 'date', '',
808 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
808 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
809 _('hg unshelve [[-n] SHELVED]'))
809 _('hg unshelve [[-n] SHELVED]'))
810 def unshelve(ui, repo, *shelved, **opts):
810 def unshelve(ui, repo, *shelved, **opts):
811 """restore a shelved change to the working directory
811 """restore a shelved change to the working directory
812
812
813 This command accepts an optional name of a shelved change to
813 This command accepts an optional name of a shelved change to
814 restore. If none is given, the most recent shelved change is used.
814 restore. If none is given, the most recent shelved change is used.
815
815
816 If a shelved change is applied successfully, the bundle that
816 If a shelved change is applied successfully, the bundle that
817 contains the shelved changes is moved to a backup location
817 contains the shelved changes is moved to a backup location
818 (.hg/shelve-backup).
818 (.hg/shelve-backup).
819
819
820 Since you can restore a shelved change on top of an arbitrary
820 Since you can restore a shelved change on top of an arbitrary
821 commit, it is possible that unshelving will result in a conflict
821 commit, it is possible that unshelving will result in a conflict
822 between your changes and the commits you are unshelving onto. If
822 between your changes and the commits you are unshelving onto. If
823 this occurs, you must resolve the conflict, then use
823 this occurs, you must resolve the conflict, then use
824 ``--continue`` to complete the unshelve operation. (The bundle
824 ``--continue`` to complete the unshelve operation. (The bundle
825 will not be moved until you successfully complete the unshelve.)
825 will not be moved until you successfully complete the unshelve.)
826
826
827 (Alternatively, you can use ``--abort`` to abandon an unshelve
827 (Alternatively, you can use ``--abort`` to abandon an unshelve
828 that causes a conflict. This reverts the unshelved changes, and
828 that causes a conflict. This reverts the unshelved changes, and
829 leaves the bundle in place.)
829 leaves the bundle in place.)
830
830
831 If bare shelved change(when no files are specified, without interactive,
831 If bare shelved change(when no files are specified, without interactive,
832 include and exclude option) was done on newly created branch it would
832 include and exclude option) was done on newly created branch it would
833 restore branch information to the working directory.
833 restore branch information to the working directory.
834
834
835 After a successful unshelve, the shelved changes are stored in a
835 After a successful unshelve, the shelved changes are stored in a
836 backup directory. Only the N most recent backups are kept. N
836 backup directory. Only the N most recent backups are kept. N
837 defaults to 10 but can be overridden using the ``shelve.maxbackups``
837 defaults to 10 but can be overridden using the ``shelve.maxbackups``
838 configuration option.
838 configuration option.
839
839
840 .. container:: verbose
840 .. container:: verbose
841
841
842 Timestamp in seconds is used to decide order of backups. More
842 Timestamp in seconds is used to decide order of backups. More
843 than ``maxbackups`` backups are kept, if same timestamp
843 than ``maxbackups`` backups are kept, if same timestamp
844 prevents from deciding exact order of them, for safety.
844 prevents from deciding exact order of them, for safety.
845 """
845 """
846 with repo.wlock():
846 with repo.wlock():
847 return _dounshelve(ui, repo, *shelved, **opts)
847 return _dounshelve(ui, repo, *shelved, **opts)
848
848
849 def _dounshelve(ui, repo, *shelved, **opts):
849 def _dounshelve(ui, repo, *shelved, **opts):
850 abortf = opts.get('abort')
850 abortf = opts.get('abort')
851 continuef = opts.get('continue')
851 continuef = opts.get('continue')
852 if not abortf and not continuef:
852 if not abortf and not continuef:
853 cmdutil.checkunfinished(repo)
853 cmdutil.checkunfinished(repo)
854 shelved = list(shelved)
854 shelved = list(shelved)
855 if opts.get("name"):
855 if opts.get("name"):
856 shelved.append(opts["name"])
856 shelved.append(opts["name"])
857
857
858 if abortf or continuef:
858 if abortf or continuef:
859 if abortf and continuef:
859 if abortf and continuef:
860 raise error.Abort(_('cannot use both abort and continue'))
860 raise error.Abort(_('cannot use both abort and continue'))
861 if shelved:
861 if shelved:
862 raise error.Abort(_('cannot combine abort/continue with '
862 raise error.Abort(_('cannot combine abort/continue with '
863 'naming a shelved change'))
863 'naming a shelved change'))
864 if abortf and opts.get('tool', False):
864 if abortf and opts.get('tool', False):
865 ui.warn(_('tool option will be ignored\n'))
865 ui.warn(_('tool option will be ignored\n'))
866
866
867 try:
867 try:
868 state = shelvedstate.load(repo)
868 state = shelvedstate.load(repo)
869 if opts.get('keep') is None:
869 if opts.get('keep') is None:
870 opts['keep'] = state.keep
870 opts['keep'] = state.keep
871 except IOError as err:
871 except IOError as err:
872 if err.errno != errno.ENOENT:
872 if err.errno != errno.ENOENT:
873 raise
873 raise
874 cmdutil.wrongtooltocontinue(repo, _('unshelve'))
874 cmdutil.wrongtooltocontinue(repo, _('unshelve'))
875 except error.CorruptedState as err:
875 except error.CorruptedState as err:
876 ui.debug(str(err) + '\n')
876 ui.debug(str(err) + '\n')
877 if continuef:
877 if continuef:
878 msg = _('corrupted shelved state file')
878 msg = _('corrupted shelved state file')
879 hint = _('please run hg unshelve --abort to abort unshelve '
879 hint = _('please run hg unshelve --abort to abort unshelve '
880 'operation')
880 'operation')
881 raise error.Abort(msg, hint=hint)
881 raise error.Abort(msg, hint=hint)
882 elif abortf:
882 elif abortf:
883 msg = _('could not read shelved state file, your working copy '
883 msg = _('could not read shelved state file, your working copy '
884 'may be in an unexpected state\nplease update to some '
884 'may be in an unexpected state\nplease update to some '
885 'commit\n')
885 'commit\n')
886 ui.warn(msg)
886 ui.warn(msg)
887 shelvedstate.clear(repo)
887 shelvedstate.clear(repo)
888 return
888 return
889
889
890 if abortf:
890 if abortf:
891 return unshelveabort(ui, repo, state, opts)
891 return unshelveabort(ui, repo, state, opts)
892 elif continuef:
892 elif continuef:
893 return unshelvecontinue(ui, repo, state, opts)
893 return unshelvecontinue(ui, repo, state, opts)
894 elif len(shelved) > 1:
894 elif len(shelved) > 1:
895 raise error.Abort(_('can only unshelve one change at a time'))
895 raise error.Abort(_('can only unshelve one change at a time'))
896 elif not shelved:
896 elif not shelved:
897 shelved = listshelves(repo)
897 shelved = listshelves(repo)
898 if not shelved:
898 if not shelved:
899 raise error.Abort(_('no shelved changes to apply!'))
899 raise error.Abort(_('no shelved changes to apply!'))
900 basename = util.split(shelved[0][1])[1]
900 basename = util.split(shelved[0][1])[1]
901 ui.status(_("unshelving change '%s'\n") % basename)
901 ui.status(_("unshelving change '%s'\n") % basename)
902 else:
902 else:
903 basename = shelved[0]
903 basename = shelved[0]
904
904
905 if not shelvedfile(repo, basename, patchextension).exists():
905 if not shelvedfile(repo, basename, patchextension).exists():
906 raise error.Abort(_("shelved change '%s' not found") % basename)
906 raise error.Abort(_("shelved change '%s' not found") % basename)
907
907
908 lock = tr = None
908 lock = tr = None
909 try:
909 try:
910 lock = repo.lock()
910 lock = repo.lock()
911 tr = repo.transaction('unshelve', report=lambda x: None)
911 tr = repo.transaction('unshelve', report=lambda x: None)
912 oldtiprev = len(repo)
912 oldtiprev = len(repo)
913
913
914 pctx = repo['.']
914 pctx = repo['.']
915 tmpwctx = pctx
915 tmpwctx = pctx
916 # The goal is to have a commit structure like so:
916 # The goal is to have a commit structure like so:
917 # ...-> pctx -> tmpwctx -> shelvectx
917 # ...-> pctx -> tmpwctx -> shelvectx
918 # where tmpwctx is an optional commit with the user's pending changes
918 # where tmpwctx is an optional commit with the user's pending changes
919 # and shelvectx is the unshelved changes. Then we merge it all down
919 # and shelvectx is the unshelved changes. Then we merge it all down
920 # to the original pctx.
920 # to the original pctx.
921
921
922 activebookmark = _backupactivebookmark(repo)
922 activebookmark = _backupactivebookmark(repo)
923 overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
923 overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
924 with ui.configoverride(overrides, 'unshelve'):
924 with ui.configoverride(overrides, 'unshelve'):
925 tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts,
925 tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts,
926 tmpwctx)
926 tmpwctx)
927 repo, shelvectx = _unshelverestorecommit(ui, repo, basename)
927 repo, shelvectx = _unshelverestorecommit(ui, repo, basename)
928 _checkunshelveuntrackedproblems(ui, repo, shelvectx)
928 _checkunshelveuntrackedproblems(ui, repo, shelvectx)
929 branchtorestore = ''
929 branchtorestore = ''
930 if shelvectx.branch() != shelvectx.p1().branch():
930 if shelvectx.branch() != shelvectx.p1().branch():
931 branchtorestore = shelvectx.branch()
931 branchtorestore = shelvectx.branch()
932
932
933 shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev,
933 shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev,
934 basename, pctx, tmpwctx,
934 basename, pctx, tmpwctx,
935 shelvectx, branchtorestore,
935 shelvectx, branchtorestore,
936 activebookmark)
936 activebookmark)
937 mergefiles(ui, repo, pctx, shelvectx)
937 mergefiles(ui, repo, pctx, shelvectx)
938 restorebranch(ui, repo, branchtorestore)
938 restorebranch(ui, repo, branchtorestore)
939 _forgetunknownfiles(repo, shelvectx, addedbefore)
939 _forgetunknownfiles(repo, shelvectx, addedbefore)
940
940
941 shelvedstate.clear(repo)
941 shelvedstate.clear(repo)
942 _finishunshelve(repo, oldtiprev, tr, activebookmark)
942 _finishunshelve(repo, oldtiprev, tr, activebookmark)
943 unshelvecleanup(ui, repo, basename, opts)
943 unshelvecleanup(ui, repo, basename, opts)
944 finally:
944 finally:
945 if tr:
945 if tr:
946 tr.release()
946 tr.release()
947 lockmod.release(lock)
947 lockmod.release(lock)
948
948
949 @command('shelve',
949 @command('shelve',
950 [('A', 'addremove', None,
950 [('A', 'addremove', None,
951 _('mark new/missing files as added/removed before shelving')),
951 _('mark new/missing files as added/removed before shelving')),
952 ('u', 'unknown', None,
952 ('u', 'unknown', None,
953 _('store unknown files in the shelve')),
953 _('store unknown files in the shelve')),
954 ('', 'cleanup', None,
954 ('', 'cleanup', None,
955 _('delete all shelved changes')),
955 _('delete all shelved changes')),
956 ('', 'date', '',
956 ('', 'date', '',
957 _('shelve with the specified commit date'), _('DATE')),
957 _('shelve with the specified commit date'), _('DATE')),
958 ('d', 'delete', None,
958 ('d', 'delete', None,
959 _('delete the named shelved change(s)')),
959 _('delete the named shelved change(s)')),
960 ('e', 'edit', False,
960 ('e', 'edit', False,
961 _('invoke editor on commit messages')),
961 _('invoke editor on commit messages')),
962 ('l', 'list', None,
962 ('l', 'list', None,
963 _('list current shelves')),
963 _('list current shelves')),
964 ('m', 'message', '',
964 ('m', 'message', '',
965 _('use text as shelve message'), _('TEXT')),
965 _('use text as shelve message'), _('TEXT')),
966 ('n', 'name', '',
966 ('n', 'name', '',
967 _('use the given name for the shelved commit'), _('NAME')),
967 _('use the given name for the shelved commit'), _('NAME')),
968 ('p', 'patch', None,
968 ('p', 'patch', None,
969 _('show patch')),
969 _('show patch')),
970 ('i', 'interactive', None,
970 ('i', 'interactive', None,
971 _('interactive mode, only works while creating a shelve')),
971 _('interactive mode, only works while creating a shelve')),
972 ('', 'stat', None,
972 ('', 'stat', None,
973 _('output diffstat-style summary of changes'))] + cmdutil.walkopts,
973 _('output diffstat-style summary of changes'))] + cmdutil.walkopts,
974 _('hg shelve [OPTION]... [FILE]...'))
974 _('hg shelve [OPTION]... [FILE]...'))
975 def shelvecmd(ui, repo, *pats, **opts):
975 def shelvecmd(ui, repo, *pats, **opts):
976 '''save and set aside changes from the working directory
976 '''save and set aside changes from the working directory
977
977
978 Shelving takes files that "hg status" reports as not clean, saves
978 Shelving takes files that "hg status" reports as not clean, saves
979 the modifications to a bundle (a shelved change), and reverts the
979 the modifications to a bundle (a shelved change), and reverts the
980 files so that their state in the working directory becomes clean.
980 files so that their state in the working directory becomes clean.
981
981
982 To restore these changes to the working directory, using "hg
982 To restore these changes to the working directory, using "hg
983 unshelve"; this will work even if you switch to a different
983 unshelve"; this will work even if you switch to a different
984 commit.
984 commit.
985
985
986 When no files are specified, "hg shelve" saves all not-clean
986 When no files are specified, "hg shelve" saves all not-clean
987 files. If specific files or directories are named, only changes to
987 files. If specific files or directories are named, only changes to
988 those files are shelved.
988 those files are shelved.
989
989
990 In bare shelve (when no files are specified, without interactive,
990 In bare shelve (when no files are specified, without interactive,
991 include and exclude option), shelving remembers information if the
991 include and exclude option), shelving remembers information if the
992 working directory was on newly created branch, in other words working
992 working directory was on newly created branch, in other words working
993 directory was on different branch than its first parent. In this
993 directory was on different branch than its first parent. In this
994 situation unshelving restores branch information to the working directory.
994 situation unshelving restores branch information to the working directory.
995
995
996 Each shelved change has a name that makes it easier to find later.
996 Each shelved change has a name that makes it easier to find later.
997 The name of a shelved change defaults to being based on the active
997 The name of a shelved change defaults to being based on the active
998 bookmark, or if there is no active bookmark, the current named
998 bookmark, or if there is no active bookmark, the current named
999 branch. To specify a different name, use ``--name``.
999 branch. To specify a different name, use ``--name``.
1000
1000
1001 To see a list of existing shelved changes, use the ``--list``
1001 To see a list of existing shelved changes, use the ``--list``
1002 option. For each shelved change, this will print its name, age,
1002 option. For each shelved change, this will print its name, age,
1003 and description; use ``--patch`` or ``--stat`` for more details.
1003 and description; use ``--patch`` or ``--stat`` for more details.
1004
1004
1005 To delete specific shelved changes, use ``--delete``. To delete
1005 To delete specific shelved changes, use ``--delete``. To delete
1006 all shelved changes, use ``--cleanup``.
1006 all shelved changes, use ``--cleanup``.
1007 '''
1007 '''
1008 allowables = [
1008 allowables = [
1009 ('addremove', {'create'}), # 'create' is pseudo action
1009 ('addremove', {'create'}), # 'create' is pseudo action
1010 ('unknown', {'create'}),
1010 ('unknown', {'create'}),
1011 ('cleanup', {'cleanup'}),
1011 ('cleanup', {'cleanup'}),
1012 # ('date', {'create'}), # ignored for passing '--date "0 0"' in tests
1012 # ('date', {'create'}), # ignored for passing '--date "0 0"' in tests
1013 ('delete', {'delete'}),
1013 ('delete', {'delete'}),
1014 ('edit', {'create'}),
1014 ('edit', {'create'}),
1015 ('list', {'list'}),
1015 ('list', {'list'}),
1016 ('message', {'create'}),
1016 ('message', {'create'}),
1017 ('name', {'create'}),
1017 ('name', {'create'}),
1018 ('patch', {'patch', 'list'}),
1018 ('patch', {'patch', 'list'}),
1019 ('stat', {'stat', 'list'}),
1019 ('stat', {'stat', 'list'}),
1020 ]
1020 ]
1021 def checkopt(opt):
1021 def checkopt(opt):
1022 if opts.get(opt):
1022 if opts.get(opt):
1023 for i, allowable in allowables:
1023 for i, allowable in allowables:
1024 if opts[i] and opt not in allowable:
1024 if opts[i] and opt not in allowable:
1025 raise error.Abort(_("options '--%s' and '--%s' may not be "
1025 raise error.Abort(_("options '--%s' and '--%s' may not be "
1026 "used together") % (opt, i))
1026 "used together") % (opt, i))
1027 return True
1027 return True
1028 if checkopt('cleanup'):
1028 if checkopt('cleanup'):
1029 if pats:
1029 if pats:
1030 raise error.Abort(_("cannot specify names when using '--cleanup'"))
1030 raise error.Abort(_("cannot specify names when using '--cleanup'"))
1031 return cleanupcmd(ui, repo)
1031 return cleanupcmd(ui, repo)
1032 elif checkopt('delete'):
1032 elif checkopt('delete'):
1033 return deletecmd(ui, repo, pats)
1033 return deletecmd(ui, repo, pats)
1034 elif checkopt('list'):
1034 elif checkopt('list'):
1035 return listcmd(ui, repo, pats, opts)
1035 return listcmd(ui, repo, pats, opts)
1036 elif checkopt('patch'):
1036 elif checkopt('patch'):
1037 return patchcmds(ui, repo, pats, opts, subcommand='patch')
1037 return patchcmds(ui, repo, pats, opts, subcommand='patch')
1038 elif checkopt('stat'):
1038 elif checkopt('stat'):
1039 return patchcmds(ui, repo, pats, opts, subcommand='stat')
1039 return patchcmds(ui, repo, pats, opts, subcommand='stat')
1040 else:
1040 else:
1041 return createcmd(ui, repo, pats, opts)
1041 return createcmd(ui, repo, pats, opts)
1042
1042
1043 def extsetup(ui):
1043 def extsetup(ui):
1044 cmdutil.unfinishedstates.append(
1044 cmdutil.unfinishedstates.append(
1045 [shelvedstate._filename, False, False,
1045 [shelvedstate._filename, False, False,
1046 _('unshelve already in progress'),
1046 _('unshelve already in progress'),
1047 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
1047 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
1048 cmdutil.afterresolvedstates.append(
1048 cmdutil.afterresolvedstates.append(
1049 [shelvedstate._filename, _('hg unshelve --continue')])
1049 [shelvedstate._filename, _('hg unshelve --continue')])
General Comments 0
You need to be logged in to leave comments. Login now