##// END OF EJS Templates
parseurl: consistently call second output "branches"...
Martin von Zweigbergk -
r37278:3809eafe default
parent child Browse files
Show More
@@ -1,1661 +1,1661 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 # b, base = checkout changeset and apply further changesets from there
42 # b, base = checkout changeset and apply further changesets from there
43 #
43 #
44
44
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 for each revision in your history. For example, if you had meant to add gamma
46 for each revision in your history. For example, if you had meant to add gamma
47 before beta, and then wanted to add delta in the same revision as beta, you
47 before beta, and then wanted to add delta in the same revision as beta, you
48 would reorganize the file to look like this::
48 would reorganize the file to look like this::
49
49
50 pick 030b686bedc4 Add gamma
50 pick 030b686bedc4 Add gamma
51 pick c561b4e977df Add beta
51 pick c561b4e977df Add beta
52 fold 7c2fd3b9020c Add delta
52 fold 7c2fd3b9020c Add delta
53
53
54 # Edit history between c561b4e977df and 7c2fd3b9020c
54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 #
55 #
56 # Commits are listed from least to most recent
56 # Commits are listed from least to most recent
57 #
57 #
58 # Commands:
58 # Commands:
59 # p, pick = use commit
59 # p, pick = use commit
60 # e, edit = use commit, but stop for amending
60 # e, edit = use commit, but stop for amending
61 # f, fold = use commit, but combine it with the one above
61 # f, fold = use commit, but combine it with the one above
62 # r, roll = like fold, but discard this commit's description and date
62 # r, roll = like fold, but discard this commit's description and date
63 # d, drop = remove commit from history
63 # d, drop = remove commit from history
64 # m, mess = edit commit message without changing commit content
64 # m, mess = edit commit message without changing commit content
65 # b, base = checkout changeset and apply further changesets from there
65 # b, base = checkout changeset and apply further changesets from there
66 #
66 #
67
67
68 At which point you close the editor and ``histedit`` starts working. When you
68 At which point you close the editor and ``histedit`` starts working. When you
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 those revisions together, offering you a chance to clean up the commit message::
70 those revisions together, offering you a chance to clean up the commit message::
71
71
72 Add beta
72 Add beta
73 ***
73 ***
74 Add delta
74 Add delta
75
75
76 Edit the commit message to your liking, then close the editor. The date used
76 Edit the commit message to your liking, then close the editor. The date used
77 for the commit will be the later of the two commits' dates. For this example,
77 for the commit will be the later of the two commits' dates. For this example,
78 let's assume that the commit message was changed to ``Add beta and delta.``
78 let's assume that the commit message was changed to ``Add beta and delta.``
79 After histedit has run and had a chance to remove any old or temporary
79 After histedit has run and had a chance to remove any old or temporary
80 revisions it needed, the history looks like this::
80 revisions it needed, the history looks like this::
81
81
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 | Add beta and delta.
83 | Add beta and delta.
84 |
84 |
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 | Add gamma
86 | Add gamma
87 |
87 |
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 Add alpha
89 Add alpha
90
90
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 ones) until after it has completed all the editing operations, so it will
92 ones) until after it has completed all the editing operations, so it will
93 probably perform several strip operations when it's done. For the above example,
93 probably perform several strip operations when it's done. For the above example,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 so you might need to be a little patient. You can choose to keep the original
95 so you might need to be a little patient. You can choose to keep the original
96 revisions by passing the ``--keep`` flag.
96 revisions by passing the ``--keep`` flag.
97
97
98 The ``edit`` operation will drop you back to a command prompt,
98 The ``edit`` operation will drop you back to a command prompt,
99 allowing you to edit files freely, or even use ``hg record`` to commit
99 allowing you to edit files freely, or even use ``hg record`` to commit
100 some changes as a separate commit. When you're done, any remaining
100 some changes as a separate commit. When you're done, any remaining
101 uncommitted changes will be committed as well. When done, run ``hg
101 uncommitted changes will be committed as well. When done, run ``hg
102 histedit --continue`` to finish this step. If there are uncommitted
102 histedit --continue`` to finish this step. If there are uncommitted
103 changes, you'll be prompted for a new commit message, but the default
103 changes, you'll be prompted for a new commit message, but the default
104 commit message will be the original message for the ``edit`` ed
104 commit message will be the original message for the ``edit`` ed
105 revision, and the date of the original commit will be preserved.
105 revision, and the date of the original commit will be preserved.
106
106
107 The ``message`` operation will give you a chance to revise a commit
107 The ``message`` operation will give you a chance to revise a commit
108 message without changing the contents. It's a shortcut for doing
108 message without changing the contents. It's a shortcut for doing
109 ``edit`` immediately followed by `hg histedit --continue``.
109 ``edit`` immediately followed by `hg histedit --continue``.
110
110
111 If ``histedit`` encounters a conflict when moving a revision (while
111 If ``histedit`` encounters a conflict when moving a revision (while
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 ``edit`` with the difference that it won't prompt you for a commit
113 ``edit`` with the difference that it won't prompt you for a commit
114 message when done. If you decide at this point that you don't like how
114 message when done. If you decide at this point that you don't like how
115 much work it will be to rearrange history, or that you made a mistake,
115 much work it will be to rearrange history, or that you made a mistake,
116 you can use ``hg histedit --abort`` to abandon the new changes you
116 you can use ``hg histedit --abort`` to abandon the new changes you
117 have made and return to the state before you attempted to edit your
117 have made and return to the state before you attempted to edit your
118 history.
118 history.
119
119
120 If we clone the histedit-ed example repository above and add four more
120 If we clone the histedit-ed example repository above and add four more
121 changes, such that we have the following history::
121 changes, such that we have the following history::
122
122
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 | Add theta
124 | Add theta
125 |
125 |
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 | Add eta
127 | Add eta
128 |
128 |
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 | Add zeta
130 | Add zeta
131 |
131 |
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 | Add epsilon
133 | Add epsilon
134 |
134 |
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 | Add beta and delta.
136 | Add beta and delta.
137 |
137 |
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 | Add gamma
139 | Add gamma
140 |
140 |
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 Add alpha
142 Add alpha
143
143
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 as running ``hg histedit 836302820282``. If you need plan to push to a
145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 repository that Mercurial does not detect to be related to the source
146 repository that Mercurial does not detect to be related to the source
147 repo, you can add a ``--force`` option.
147 repo, you can add a ``--force`` option.
148
148
149 Config
149 Config
150 ------
150 ------
151
151
152 Histedit rule lines are truncated to 80 characters by default. You
152 Histedit rule lines are truncated to 80 characters by default. You
153 can customize this behavior by setting a different length in your
153 can customize this behavior by setting a different length in your
154 configuration file::
154 configuration file::
155
155
156 [histedit]
156 [histedit]
157 linelen = 120 # truncate rule lines at 120 characters
157 linelen = 120 # truncate rule lines at 120 characters
158
158
159 ``hg histedit`` attempts to automatically choose an appropriate base
159 ``hg histedit`` attempts to automatically choose an appropriate base
160 revision to use. To change which base revision is used, define a
160 revision to use. To change which base revision is used, define a
161 revset in your configuration file::
161 revset in your configuration file::
162
162
163 [histedit]
163 [histedit]
164 defaultrev = only(.) & draft()
164 defaultrev = only(.) & draft()
165
165
166 By default each edited revision needs to be present in histedit commands.
166 By default each edited revision needs to be present in histedit commands.
167 To remove revision you need to use ``drop`` operation. You can configure
167 To remove revision you need to use ``drop`` operation. You can configure
168 the drop to be implicit for missing commits by adding::
168 the drop to be implicit for missing commits by adding::
169
169
170 [histedit]
170 [histedit]
171 dropmissing = True
171 dropmissing = True
172
172
173 By default, histedit will close the transaction after each action. For
173 By default, histedit will close the transaction after each action. For
174 performance purposes, you can configure histedit to use a single transaction
174 performance purposes, you can configure histedit to use a single transaction
175 across the entire histedit. WARNING: This setting introduces a significant risk
175 across the entire histedit. WARNING: This setting introduces a significant risk
176 of losing the work you've done in a histedit if the histedit aborts
176 of losing the work you've done in a histedit if the histedit aborts
177 unexpectedly::
177 unexpectedly::
178
178
179 [histedit]
179 [histedit]
180 singletransaction = True
180 singletransaction = True
181
181
182 """
182 """
183
183
184 from __future__ import absolute_import
184 from __future__ import absolute_import
185
185
186 import errno
186 import errno
187 import os
187 import os
188
188
189 from mercurial.i18n import _
189 from mercurial.i18n import _
190 from mercurial import (
190 from mercurial import (
191 bundle2,
191 bundle2,
192 cmdutil,
192 cmdutil,
193 context,
193 context,
194 copies,
194 copies,
195 destutil,
195 destutil,
196 discovery,
196 discovery,
197 error,
197 error,
198 exchange,
198 exchange,
199 extensions,
199 extensions,
200 hg,
200 hg,
201 lock,
201 lock,
202 merge as mergemod,
202 merge as mergemod,
203 mergeutil,
203 mergeutil,
204 node,
204 node,
205 obsolete,
205 obsolete,
206 pycompat,
206 pycompat,
207 registrar,
207 registrar,
208 repair,
208 repair,
209 scmutil,
209 scmutil,
210 util,
210 util,
211 )
211 )
212 from mercurial.utils import (
212 from mercurial.utils import (
213 stringutil,
213 stringutil,
214 )
214 )
215
215
216 pickle = util.pickle
216 pickle = util.pickle
217 release = lock.release
217 release = lock.release
218 cmdtable = {}
218 cmdtable = {}
219 command = registrar.command(cmdtable)
219 command = registrar.command(cmdtable)
220
220
221 configtable = {}
221 configtable = {}
222 configitem = registrar.configitem(configtable)
222 configitem = registrar.configitem(configtable)
223 configitem('experimental', 'histedit.autoverb',
223 configitem('experimental', 'histedit.autoverb',
224 default=False,
224 default=False,
225 )
225 )
226 configitem('histedit', 'defaultrev',
226 configitem('histedit', 'defaultrev',
227 default=None,
227 default=None,
228 )
228 )
229 configitem('histedit', 'dropmissing',
229 configitem('histedit', 'dropmissing',
230 default=False,
230 default=False,
231 )
231 )
232 configitem('histedit', 'linelen',
232 configitem('histedit', 'linelen',
233 default=80,
233 default=80,
234 )
234 )
235 configitem('histedit', 'singletransaction',
235 configitem('histedit', 'singletransaction',
236 default=False,
236 default=False,
237 )
237 )
238
238
239 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
239 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
240 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
240 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
241 # be specifying the version(s) of Mercurial they are tested with, or
241 # be specifying the version(s) of Mercurial they are tested with, or
242 # leave the attribute unspecified.
242 # leave the attribute unspecified.
243 testedwith = 'ships-with-hg-core'
243 testedwith = 'ships-with-hg-core'
244
244
245 actiontable = {}
245 actiontable = {}
246 primaryactions = set()
246 primaryactions = set()
247 secondaryactions = set()
247 secondaryactions = set()
248 tertiaryactions = set()
248 tertiaryactions = set()
249 internalactions = set()
249 internalactions = set()
250
250
251 def geteditcomment(ui, first, last):
251 def geteditcomment(ui, first, last):
252 """ construct the editor comment
252 """ construct the editor comment
253 The comment includes::
253 The comment includes::
254 - an intro
254 - an intro
255 - sorted primary commands
255 - sorted primary commands
256 - sorted short commands
256 - sorted short commands
257 - sorted long commands
257 - sorted long commands
258 - additional hints
258 - additional hints
259
259
260 Commands are only included once.
260 Commands are only included once.
261 """
261 """
262 intro = _("""Edit history between %s and %s
262 intro = _("""Edit history between %s and %s
263
263
264 Commits are listed from least to most recent
264 Commits are listed from least to most recent
265
265
266 You can reorder changesets by reordering the lines
266 You can reorder changesets by reordering the lines
267
267
268 Commands:
268 Commands:
269 """)
269 """)
270 actions = []
270 actions = []
271 def addverb(v):
271 def addverb(v):
272 a = actiontable[v]
272 a = actiontable[v]
273 lines = a.message.split("\n")
273 lines = a.message.split("\n")
274 if len(a.verbs):
274 if len(a.verbs):
275 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
275 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
276 actions.append(" %s = %s" % (v, lines[0]))
276 actions.append(" %s = %s" % (v, lines[0]))
277 actions.extend([' %s' for l in lines[1:]])
277 actions.extend([' %s' for l in lines[1:]])
278
278
279 for v in (
279 for v in (
280 sorted(primaryactions) +
280 sorted(primaryactions) +
281 sorted(secondaryactions) +
281 sorted(secondaryactions) +
282 sorted(tertiaryactions)
282 sorted(tertiaryactions)
283 ):
283 ):
284 addverb(v)
284 addverb(v)
285 actions.append('')
285 actions.append('')
286
286
287 hints = []
287 hints = []
288 if ui.configbool('histedit', 'dropmissing'):
288 if ui.configbool('histedit', 'dropmissing'):
289 hints.append("Deleting a changeset from the list "
289 hints.append("Deleting a changeset from the list "
290 "will DISCARD it from the edited history!")
290 "will DISCARD it from the edited history!")
291
291
292 lines = (intro % (first, last)).split('\n') + actions + hints
292 lines = (intro % (first, last)).split('\n') + actions + hints
293
293
294 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
294 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
295
295
296 class histeditstate(object):
296 class histeditstate(object):
297 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
297 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
298 topmost=None, replacements=None, lock=None, wlock=None):
298 topmost=None, replacements=None, lock=None, wlock=None):
299 self.repo = repo
299 self.repo = repo
300 self.actions = actions
300 self.actions = actions
301 self.keep = keep
301 self.keep = keep
302 self.topmost = topmost
302 self.topmost = topmost
303 self.parentctxnode = parentctxnode
303 self.parentctxnode = parentctxnode
304 self.lock = lock
304 self.lock = lock
305 self.wlock = wlock
305 self.wlock = wlock
306 self.backupfile = None
306 self.backupfile = None
307 if replacements is None:
307 if replacements is None:
308 self.replacements = []
308 self.replacements = []
309 else:
309 else:
310 self.replacements = replacements
310 self.replacements = replacements
311
311
312 def read(self):
312 def read(self):
313 """Load histedit state from disk and set fields appropriately."""
313 """Load histedit state from disk and set fields appropriately."""
314 try:
314 try:
315 state = self.repo.vfs.read('histedit-state')
315 state = self.repo.vfs.read('histedit-state')
316 except IOError as err:
316 except IOError as err:
317 if err.errno != errno.ENOENT:
317 if err.errno != errno.ENOENT:
318 raise
318 raise
319 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
319 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
320
320
321 if state.startswith('v1\n'):
321 if state.startswith('v1\n'):
322 data = self._load()
322 data = self._load()
323 parentctxnode, rules, keep, topmost, replacements, backupfile = data
323 parentctxnode, rules, keep, topmost, replacements, backupfile = data
324 else:
324 else:
325 data = pickle.loads(state)
325 data = pickle.loads(state)
326 parentctxnode, rules, keep, topmost, replacements = data
326 parentctxnode, rules, keep, topmost, replacements = data
327 backupfile = None
327 backupfile = None
328
328
329 self.parentctxnode = parentctxnode
329 self.parentctxnode = parentctxnode
330 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
330 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
331 actions = parserules(rules, self)
331 actions = parserules(rules, self)
332 self.actions = actions
332 self.actions = actions
333 self.keep = keep
333 self.keep = keep
334 self.topmost = topmost
334 self.topmost = topmost
335 self.replacements = replacements
335 self.replacements = replacements
336 self.backupfile = backupfile
336 self.backupfile = backupfile
337
337
338 def write(self, tr=None):
338 def write(self, tr=None):
339 if tr:
339 if tr:
340 tr.addfilegenerator('histedit-state', ('histedit-state',),
340 tr.addfilegenerator('histedit-state', ('histedit-state',),
341 self._write, location='plain')
341 self._write, location='plain')
342 else:
342 else:
343 with self.repo.vfs("histedit-state", "w") as f:
343 with self.repo.vfs("histedit-state", "w") as f:
344 self._write(f)
344 self._write(f)
345
345
346 def _write(self, fp):
346 def _write(self, fp):
347 fp.write('v1\n')
347 fp.write('v1\n')
348 fp.write('%s\n' % node.hex(self.parentctxnode))
348 fp.write('%s\n' % node.hex(self.parentctxnode))
349 fp.write('%s\n' % node.hex(self.topmost))
349 fp.write('%s\n' % node.hex(self.topmost))
350 fp.write('%s\n' % ('True' if self.keep else 'False'))
350 fp.write('%s\n' % ('True' if self.keep else 'False'))
351 fp.write('%d\n' % len(self.actions))
351 fp.write('%d\n' % len(self.actions))
352 for action in self.actions:
352 for action in self.actions:
353 fp.write('%s\n' % action.tostate())
353 fp.write('%s\n' % action.tostate())
354 fp.write('%d\n' % len(self.replacements))
354 fp.write('%d\n' % len(self.replacements))
355 for replacement in self.replacements:
355 for replacement in self.replacements:
356 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
356 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
357 for r in replacement[1])))
357 for r in replacement[1])))
358 backupfile = self.backupfile
358 backupfile = self.backupfile
359 if not backupfile:
359 if not backupfile:
360 backupfile = ''
360 backupfile = ''
361 fp.write('%s\n' % backupfile)
361 fp.write('%s\n' % backupfile)
362
362
363 def _load(self):
363 def _load(self):
364 fp = self.repo.vfs('histedit-state', 'r')
364 fp = self.repo.vfs('histedit-state', 'r')
365 lines = [l[:-1] for l in fp.readlines()]
365 lines = [l[:-1] for l in fp.readlines()]
366
366
367 index = 0
367 index = 0
368 lines[index] # version number
368 lines[index] # version number
369 index += 1
369 index += 1
370
370
371 parentctxnode = node.bin(lines[index])
371 parentctxnode = node.bin(lines[index])
372 index += 1
372 index += 1
373
373
374 topmost = node.bin(lines[index])
374 topmost = node.bin(lines[index])
375 index += 1
375 index += 1
376
376
377 keep = lines[index] == 'True'
377 keep = lines[index] == 'True'
378 index += 1
378 index += 1
379
379
380 # Rules
380 # Rules
381 rules = []
381 rules = []
382 rulelen = int(lines[index])
382 rulelen = int(lines[index])
383 index += 1
383 index += 1
384 for i in xrange(rulelen):
384 for i in xrange(rulelen):
385 ruleaction = lines[index]
385 ruleaction = lines[index]
386 index += 1
386 index += 1
387 rule = lines[index]
387 rule = lines[index]
388 index += 1
388 index += 1
389 rules.append((ruleaction, rule))
389 rules.append((ruleaction, rule))
390
390
391 # Replacements
391 # Replacements
392 replacements = []
392 replacements = []
393 replacementlen = int(lines[index])
393 replacementlen = int(lines[index])
394 index += 1
394 index += 1
395 for i in xrange(replacementlen):
395 for i in xrange(replacementlen):
396 replacement = lines[index]
396 replacement = lines[index]
397 original = node.bin(replacement[:40])
397 original = node.bin(replacement[:40])
398 succ = [node.bin(replacement[i:i + 40]) for i in
398 succ = [node.bin(replacement[i:i + 40]) for i in
399 range(40, len(replacement), 40)]
399 range(40, len(replacement), 40)]
400 replacements.append((original, succ))
400 replacements.append((original, succ))
401 index += 1
401 index += 1
402
402
403 backupfile = lines[index]
403 backupfile = lines[index]
404 index += 1
404 index += 1
405
405
406 fp.close()
406 fp.close()
407
407
408 return parentctxnode, rules, keep, topmost, replacements, backupfile
408 return parentctxnode, rules, keep, topmost, replacements, backupfile
409
409
410 def clear(self):
410 def clear(self):
411 if self.inprogress():
411 if self.inprogress():
412 self.repo.vfs.unlink('histedit-state')
412 self.repo.vfs.unlink('histedit-state')
413
413
414 def inprogress(self):
414 def inprogress(self):
415 return self.repo.vfs.exists('histedit-state')
415 return self.repo.vfs.exists('histedit-state')
416
416
417
417
418 class histeditaction(object):
418 class histeditaction(object):
419 def __init__(self, state, node):
419 def __init__(self, state, node):
420 self.state = state
420 self.state = state
421 self.repo = state.repo
421 self.repo = state.repo
422 self.node = node
422 self.node = node
423
423
424 @classmethod
424 @classmethod
425 def fromrule(cls, state, rule):
425 def fromrule(cls, state, rule):
426 """Parses the given rule, returning an instance of the histeditaction.
426 """Parses the given rule, returning an instance of the histeditaction.
427 """
427 """
428 ruleid = rule.strip().split(' ', 1)[0]
428 ruleid = rule.strip().split(' ', 1)[0]
429 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
429 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
430 # Check for validation of rule ids and get the rulehash
430 # Check for validation of rule ids and get the rulehash
431 try:
431 try:
432 rev = node.bin(ruleid)
432 rev = node.bin(ruleid)
433 except TypeError:
433 except TypeError:
434 try:
434 try:
435 _ctx = scmutil.revsingle(state.repo, ruleid)
435 _ctx = scmutil.revsingle(state.repo, ruleid)
436 rulehash = _ctx.hex()
436 rulehash = _ctx.hex()
437 rev = node.bin(rulehash)
437 rev = node.bin(rulehash)
438 except error.RepoLookupError:
438 except error.RepoLookupError:
439 raise error.ParseError("invalid changeset %s" % ruleid)
439 raise error.ParseError("invalid changeset %s" % ruleid)
440 return cls(state, rev)
440 return cls(state, rev)
441
441
442 def verify(self, prev, expected, seen):
442 def verify(self, prev, expected, seen):
443 """ Verifies semantic correctness of the rule"""
443 """ Verifies semantic correctness of the rule"""
444 repo = self.repo
444 repo = self.repo
445 ha = node.hex(self.node)
445 ha = node.hex(self.node)
446 try:
446 try:
447 self.node = repo[ha].node()
447 self.node = repo[ha].node()
448 except error.RepoError:
448 except error.RepoError:
449 raise error.ParseError(_('unknown changeset %s listed')
449 raise error.ParseError(_('unknown changeset %s listed')
450 % ha[:12])
450 % ha[:12])
451 if self.node is not None:
451 if self.node is not None:
452 self._verifynodeconstraints(prev, expected, seen)
452 self._verifynodeconstraints(prev, expected, seen)
453
453
454 def _verifynodeconstraints(self, prev, expected, seen):
454 def _verifynodeconstraints(self, prev, expected, seen):
455 # by default command need a node in the edited list
455 # by default command need a node in the edited list
456 if self.node not in expected:
456 if self.node not in expected:
457 raise error.ParseError(_('%s "%s" changeset was not a candidate')
457 raise error.ParseError(_('%s "%s" changeset was not a candidate')
458 % (self.verb, node.short(self.node)),
458 % (self.verb, node.short(self.node)),
459 hint=_('only use listed changesets'))
459 hint=_('only use listed changesets'))
460 # and only one command per node
460 # and only one command per node
461 if self.node in seen:
461 if self.node in seen:
462 raise error.ParseError(_('duplicated command for changeset %s') %
462 raise error.ParseError(_('duplicated command for changeset %s') %
463 node.short(self.node))
463 node.short(self.node))
464
464
465 def torule(self):
465 def torule(self):
466 """build a histedit rule line for an action
466 """build a histedit rule line for an action
467
467
468 by default lines are in the form:
468 by default lines are in the form:
469 <hash> <rev> <summary>
469 <hash> <rev> <summary>
470 """
470 """
471 ctx = self.repo[self.node]
471 ctx = self.repo[self.node]
472 summary = _getsummary(ctx)
472 summary = _getsummary(ctx)
473 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
473 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
474 # trim to 75 columns by default so it's not stupidly wide in my editor
474 # trim to 75 columns by default so it's not stupidly wide in my editor
475 # (the 5 more are left for verb)
475 # (the 5 more are left for verb)
476 maxlen = self.repo.ui.configint('histedit', 'linelen')
476 maxlen = self.repo.ui.configint('histedit', 'linelen')
477 maxlen = max(maxlen, 22) # avoid truncating hash
477 maxlen = max(maxlen, 22) # avoid truncating hash
478 return stringutil.ellipsis(line, maxlen)
478 return stringutil.ellipsis(line, maxlen)
479
479
480 def tostate(self):
480 def tostate(self):
481 """Print an action in format used by histedit state files
481 """Print an action in format used by histedit state files
482 (the first line is a verb, the remainder is the second)
482 (the first line is a verb, the remainder is the second)
483 """
483 """
484 return "%s\n%s" % (self.verb, node.hex(self.node))
484 return "%s\n%s" % (self.verb, node.hex(self.node))
485
485
486 def run(self):
486 def run(self):
487 """Runs the action. The default behavior is simply apply the action's
487 """Runs the action. The default behavior is simply apply the action's
488 rulectx onto the current parentctx."""
488 rulectx onto the current parentctx."""
489 self.applychange()
489 self.applychange()
490 self.continuedirty()
490 self.continuedirty()
491 return self.continueclean()
491 return self.continueclean()
492
492
493 def applychange(self):
493 def applychange(self):
494 """Applies the changes from this action's rulectx onto the current
494 """Applies the changes from this action's rulectx onto the current
495 parentctx, but does not commit them."""
495 parentctx, but does not commit them."""
496 repo = self.repo
496 repo = self.repo
497 rulectx = repo[self.node]
497 rulectx = repo[self.node]
498 repo.ui.pushbuffer(error=True, labeled=True)
498 repo.ui.pushbuffer(error=True, labeled=True)
499 hg.update(repo, self.state.parentctxnode, quietempty=True)
499 hg.update(repo, self.state.parentctxnode, quietempty=True)
500 stats = applychanges(repo.ui, repo, rulectx, {})
500 stats = applychanges(repo.ui, repo, rulectx, {})
501 repo.dirstate.setbranch(rulectx.branch())
501 repo.dirstate.setbranch(rulectx.branch())
502 if stats.unresolvedcount:
502 if stats.unresolvedcount:
503 buf = repo.ui.popbuffer()
503 buf = repo.ui.popbuffer()
504 repo.ui.write(buf)
504 repo.ui.write(buf)
505 raise error.InterventionRequired(
505 raise error.InterventionRequired(
506 _('Fix up the change (%s %s)') %
506 _('Fix up the change (%s %s)') %
507 (self.verb, node.short(self.node)),
507 (self.verb, node.short(self.node)),
508 hint=_('hg histedit --continue to resume'))
508 hint=_('hg histedit --continue to resume'))
509 else:
509 else:
510 repo.ui.popbuffer()
510 repo.ui.popbuffer()
511
511
512 def continuedirty(self):
512 def continuedirty(self):
513 """Continues the action when changes have been applied to the working
513 """Continues the action when changes have been applied to the working
514 copy. The default behavior is to commit the dirty changes."""
514 copy. The default behavior is to commit the dirty changes."""
515 repo = self.repo
515 repo = self.repo
516 rulectx = repo[self.node]
516 rulectx = repo[self.node]
517
517
518 editor = self.commiteditor()
518 editor = self.commiteditor()
519 commit = commitfuncfor(repo, rulectx)
519 commit = commitfuncfor(repo, rulectx)
520
520
521 commit(text=rulectx.description(), user=rulectx.user(),
521 commit(text=rulectx.description(), user=rulectx.user(),
522 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
522 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
523
523
524 def commiteditor(self):
524 def commiteditor(self):
525 """The editor to be used to edit the commit message."""
525 """The editor to be used to edit the commit message."""
526 return False
526 return False
527
527
528 def continueclean(self):
528 def continueclean(self):
529 """Continues the action when the working copy is clean. The default
529 """Continues the action when the working copy is clean. The default
530 behavior is to accept the current commit as the new version of the
530 behavior is to accept the current commit as the new version of the
531 rulectx."""
531 rulectx."""
532 ctx = self.repo['.']
532 ctx = self.repo['.']
533 if ctx.node() == self.state.parentctxnode:
533 if ctx.node() == self.state.parentctxnode:
534 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
534 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
535 node.short(self.node))
535 node.short(self.node))
536 return ctx, [(self.node, tuple())]
536 return ctx, [(self.node, tuple())]
537 if ctx.node() == self.node:
537 if ctx.node() == self.node:
538 # Nothing changed
538 # Nothing changed
539 return ctx, []
539 return ctx, []
540 return ctx, [(self.node, (ctx.node(),))]
540 return ctx, [(self.node, (ctx.node(),))]
541
541
542 def commitfuncfor(repo, src):
542 def commitfuncfor(repo, src):
543 """Build a commit function for the replacement of <src>
543 """Build a commit function for the replacement of <src>
544
544
545 This function ensure we apply the same treatment to all changesets.
545 This function ensure we apply the same treatment to all changesets.
546
546
547 - Add a 'histedit_source' entry in extra.
547 - Add a 'histedit_source' entry in extra.
548
548
549 Note that fold has its own separated logic because its handling is a bit
549 Note that fold has its own separated logic because its handling is a bit
550 different and not easily factored out of the fold method.
550 different and not easily factored out of the fold method.
551 """
551 """
552 phasemin = src.phase()
552 phasemin = src.phase()
553 def commitfunc(**kwargs):
553 def commitfunc(**kwargs):
554 overrides = {('phases', 'new-commit'): phasemin}
554 overrides = {('phases', 'new-commit'): phasemin}
555 with repo.ui.configoverride(overrides, 'histedit'):
555 with repo.ui.configoverride(overrides, 'histedit'):
556 extra = kwargs.get(r'extra', {}).copy()
556 extra = kwargs.get(r'extra', {}).copy()
557 extra['histedit_source'] = src.hex()
557 extra['histedit_source'] = src.hex()
558 kwargs[r'extra'] = extra
558 kwargs[r'extra'] = extra
559 return repo.commit(**kwargs)
559 return repo.commit(**kwargs)
560 return commitfunc
560 return commitfunc
561
561
562 def applychanges(ui, repo, ctx, opts):
562 def applychanges(ui, repo, ctx, opts):
563 """Merge changeset from ctx (only) in the current working directory"""
563 """Merge changeset from ctx (only) in the current working directory"""
564 wcpar = repo.dirstate.parents()[0]
564 wcpar = repo.dirstate.parents()[0]
565 if ctx.p1().node() == wcpar:
565 if ctx.p1().node() == wcpar:
566 # edits are "in place" we do not need to make any merge,
566 # edits are "in place" we do not need to make any merge,
567 # just applies changes on parent for editing
567 # just applies changes on parent for editing
568 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
568 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
569 stats = mergemod.updateresult(0, 0, 0, 0)
569 stats = mergemod.updateresult(0, 0, 0, 0)
570 else:
570 else:
571 try:
571 try:
572 # ui.forcemerge is an internal variable, do not document
572 # ui.forcemerge is an internal variable, do not document
573 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
573 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
574 'histedit')
574 'histedit')
575 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
575 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
576 finally:
576 finally:
577 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
577 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
578 return stats
578 return stats
579
579
580 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
580 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
581 """collapse the set of revisions from first to last as new one.
581 """collapse the set of revisions from first to last as new one.
582
582
583 Expected commit options are:
583 Expected commit options are:
584 - message
584 - message
585 - date
585 - date
586 - username
586 - username
587 Commit message is edited in all cases.
587 Commit message is edited in all cases.
588
588
589 This function works in memory."""
589 This function works in memory."""
590 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
590 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
591 if not ctxs:
591 if not ctxs:
592 return None
592 return None
593 for c in ctxs:
593 for c in ctxs:
594 if not c.mutable():
594 if not c.mutable():
595 raise error.ParseError(
595 raise error.ParseError(
596 _("cannot fold into public change %s") % node.short(c.node()))
596 _("cannot fold into public change %s") % node.short(c.node()))
597 base = firstctx.parents()[0]
597 base = firstctx.parents()[0]
598
598
599 # commit a new version of the old changeset, including the update
599 # commit a new version of the old changeset, including the update
600 # collect all files which might be affected
600 # collect all files which might be affected
601 files = set()
601 files = set()
602 for ctx in ctxs:
602 for ctx in ctxs:
603 files.update(ctx.files())
603 files.update(ctx.files())
604
604
605 # Recompute copies (avoid recording a -> b -> a)
605 # Recompute copies (avoid recording a -> b -> a)
606 copied = copies.pathcopies(base, lastctx)
606 copied = copies.pathcopies(base, lastctx)
607
607
608 # prune files which were reverted by the updates
608 # prune files which were reverted by the updates
609 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
609 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
610 # commit version of these files as defined by head
610 # commit version of these files as defined by head
611 headmf = lastctx.manifest()
611 headmf = lastctx.manifest()
612 def filectxfn(repo, ctx, path):
612 def filectxfn(repo, ctx, path):
613 if path in headmf:
613 if path in headmf:
614 fctx = lastctx[path]
614 fctx = lastctx[path]
615 flags = fctx.flags()
615 flags = fctx.flags()
616 mctx = context.memfilectx(repo, ctx,
616 mctx = context.memfilectx(repo, ctx,
617 fctx.path(), fctx.data(),
617 fctx.path(), fctx.data(),
618 islink='l' in flags,
618 islink='l' in flags,
619 isexec='x' in flags,
619 isexec='x' in flags,
620 copied=copied.get(path))
620 copied=copied.get(path))
621 return mctx
621 return mctx
622 return None
622 return None
623
623
624 if commitopts.get('message'):
624 if commitopts.get('message'):
625 message = commitopts['message']
625 message = commitopts['message']
626 else:
626 else:
627 message = firstctx.description()
627 message = firstctx.description()
628 user = commitopts.get('user')
628 user = commitopts.get('user')
629 date = commitopts.get('date')
629 date = commitopts.get('date')
630 extra = commitopts.get('extra')
630 extra = commitopts.get('extra')
631
631
632 parents = (firstctx.p1().node(), firstctx.p2().node())
632 parents = (firstctx.p1().node(), firstctx.p2().node())
633 editor = None
633 editor = None
634 if not skipprompt:
634 if not skipprompt:
635 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
635 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
636 new = context.memctx(repo,
636 new = context.memctx(repo,
637 parents=parents,
637 parents=parents,
638 text=message,
638 text=message,
639 files=files,
639 files=files,
640 filectxfn=filectxfn,
640 filectxfn=filectxfn,
641 user=user,
641 user=user,
642 date=date,
642 date=date,
643 extra=extra,
643 extra=extra,
644 editor=editor)
644 editor=editor)
645 return repo.commitctx(new)
645 return repo.commitctx(new)
646
646
647 def _isdirtywc(repo):
647 def _isdirtywc(repo):
648 return repo[None].dirty(missing=True)
648 return repo[None].dirty(missing=True)
649
649
650 def abortdirty():
650 def abortdirty():
651 raise error.Abort(_('working copy has pending changes'),
651 raise error.Abort(_('working copy has pending changes'),
652 hint=_('amend, commit, or revert them and run histedit '
652 hint=_('amend, commit, or revert them and run histedit '
653 '--continue, or abort with histedit --abort'))
653 '--continue, or abort with histedit --abort'))
654
654
655 def action(verbs, message, priority=False, internal=False):
655 def action(verbs, message, priority=False, internal=False):
656 def wrap(cls):
656 def wrap(cls):
657 assert not priority or not internal
657 assert not priority or not internal
658 verb = verbs[0]
658 verb = verbs[0]
659 if priority:
659 if priority:
660 primaryactions.add(verb)
660 primaryactions.add(verb)
661 elif internal:
661 elif internal:
662 internalactions.add(verb)
662 internalactions.add(verb)
663 elif len(verbs) > 1:
663 elif len(verbs) > 1:
664 secondaryactions.add(verb)
664 secondaryactions.add(verb)
665 else:
665 else:
666 tertiaryactions.add(verb)
666 tertiaryactions.add(verb)
667
667
668 cls.verb = verb
668 cls.verb = verb
669 cls.verbs = verbs
669 cls.verbs = verbs
670 cls.message = message
670 cls.message = message
671 for verb in verbs:
671 for verb in verbs:
672 actiontable[verb] = cls
672 actiontable[verb] = cls
673 return cls
673 return cls
674 return wrap
674 return wrap
675
675
676 @action(['pick', 'p'],
676 @action(['pick', 'p'],
677 _('use commit'),
677 _('use commit'),
678 priority=True)
678 priority=True)
679 class pick(histeditaction):
679 class pick(histeditaction):
680 def run(self):
680 def run(self):
681 rulectx = self.repo[self.node]
681 rulectx = self.repo[self.node]
682 if rulectx.parents()[0].node() == self.state.parentctxnode:
682 if rulectx.parents()[0].node() == self.state.parentctxnode:
683 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
683 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
684 return rulectx, []
684 return rulectx, []
685
685
686 return super(pick, self).run()
686 return super(pick, self).run()
687
687
688 @action(['edit', 'e'],
688 @action(['edit', 'e'],
689 _('use commit, but stop for amending'),
689 _('use commit, but stop for amending'),
690 priority=True)
690 priority=True)
691 class edit(histeditaction):
691 class edit(histeditaction):
692 def run(self):
692 def run(self):
693 repo = self.repo
693 repo = self.repo
694 rulectx = repo[self.node]
694 rulectx = repo[self.node]
695 hg.update(repo, self.state.parentctxnode, quietempty=True)
695 hg.update(repo, self.state.parentctxnode, quietempty=True)
696 applychanges(repo.ui, repo, rulectx, {})
696 applychanges(repo.ui, repo, rulectx, {})
697 raise error.InterventionRequired(
697 raise error.InterventionRequired(
698 _('Editing (%s), you may commit or record as needed now.')
698 _('Editing (%s), you may commit or record as needed now.')
699 % node.short(self.node),
699 % node.short(self.node),
700 hint=_('hg histedit --continue to resume'))
700 hint=_('hg histedit --continue to resume'))
701
701
702 def commiteditor(self):
702 def commiteditor(self):
703 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
703 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
704
704
705 @action(['fold', 'f'],
705 @action(['fold', 'f'],
706 _('use commit, but combine it with the one above'))
706 _('use commit, but combine it with the one above'))
707 class fold(histeditaction):
707 class fold(histeditaction):
708 def verify(self, prev, expected, seen):
708 def verify(self, prev, expected, seen):
709 """ Verifies semantic correctness of the fold rule"""
709 """ Verifies semantic correctness of the fold rule"""
710 super(fold, self).verify(prev, expected, seen)
710 super(fold, self).verify(prev, expected, seen)
711 repo = self.repo
711 repo = self.repo
712 if not prev:
712 if not prev:
713 c = repo[self.node].parents()[0]
713 c = repo[self.node].parents()[0]
714 elif not prev.verb in ('pick', 'base'):
714 elif not prev.verb in ('pick', 'base'):
715 return
715 return
716 else:
716 else:
717 c = repo[prev.node]
717 c = repo[prev.node]
718 if not c.mutable():
718 if not c.mutable():
719 raise error.ParseError(
719 raise error.ParseError(
720 _("cannot fold into public change %s") % node.short(c.node()))
720 _("cannot fold into public change %s") % node.short(c.node()))
721
721
722
722
723 def continuedirty(self):
723 def continuedirty(self):
724 repo = self.repo
724 repo = self.repo
725 rulectx = repo[self.node]
725 rulectx = repo[self.node]
726
726
727 commit = commitfuncfor(repo, rulectx)
727 commit = commitfuncfor(repo, rulectx)
728 commit(text='fold-temp-revision %s' % node.short(self.node),
728 commit(text='fold-temp-revision %s' % node.short(self.node),
729 user=rulectx.user(), date=rulectx.date(),
729 user=rulectx.user(), date=rulectx.date(),
730 extra=rulectx.extra())
730 extra=rulectx.extra())
731
731
732 def continueclean(self):
732 def continueclean(self):
733 repo = self.repo
733 repo = self.repo
734 ctx = repo['.']
734 ctx = repo['.']
735 rulectx = repo[self.node]
735 rulectx = repo[self.node]
736 parentctxnode = self.state.parentctxnode
736 parentctxnode = self.state.parentctxnode
737 if ctx.node() == parentctxnode:
737 if ctx.node() == parentctxnode:
738 repo.ui.warn(_('%s: empty changeset\n') %
738 repo.ui.warn(_('%s: empty changeset\n') %
739 node.short(self.node))
739 node.short(self.node))
740 return ctx, [(self.node, (parentctxnode,))]
740 return ctx, [(self.node, (parentctxnode,))]
741
741
742 parentctx = repo[parentctxnode]
742 parentctx = repo[parentctxnode]
743 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
743 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
744 parentctx.rev(),
744 parentctx.rev(),
745 parentctx.rev()))
745 parentctx.rev()))
746 if not newcommits:
746 if not newcommits:
747 repo.ui.warn(_('%s: cannot fold - working copy is not a '
747 repo.ui.warn(_('%s: cannot fold - working copy is not a '
748 'descendant of previous commit %s\n') %
748 'descendant of previous commit %s\n') %
749 (node.short(self.node), node.short(parentctxnode)))
749 (node.short(self.node), node.short(parentctxnode)))
750 return ctx, [(self.node, (ctx.node(),))]
750 return ctx, [(self.node, (ctx.node(),))]
751
751
752 middlecommits = newcommits.copy()
752 middlecommits = newcommits.copy()
753 middlecommits.discard(ctx.node())
753 middlecommits.discard(ctx.node())
754
754
755 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
755 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
756 middlecommits)
756 middlecommits)
757
757
758 def skipprompt(self):
758 def skipprompt(self):
759 """Returns true if the rule should skip the message editor.
759 """Returns true if the rule should skip the message editor.
760
760
761 For example, 'fold' wants to show an editor, but 'rollup'
761 For example, 'fold' wants to show an editor, but 'rollup'
762 doesn't want to.
762 doesn't want to.
763 """
763 """
764 return False
764 return False
765
765
766 def mergedescs(self):
766 def mergedescs(self):
767 """Returns true if the rule should merge messages of multiple changes.
767 """Returns true if the rule should merge messages of multiple changes.
768
768
769 This exists mainly so that 'rollup' rules can be a subclass of
769 This exists mainly so that 'rollup' rules can be a subclass of
770 'fold'.
770 'fold'.
771 """
771 """
772 return True
772 return True
773
773
774 def firstdate(self):
774 def firstdate(self):
775 """Returns true if the rule should preserve the date of the first
775 """Returns true if the rule should preserve the date of the first
776 change.
776 change.
777
777
778 This exists mainly so that 'rollup' rules can be a subclass of
778 This exists mainly so that 'rollup' rules can be a subclass of
779 'fold'.
779 'fold'.
780 """
780 """
781 return False
781 return False
782
782
783 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
783 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
784 parent = ctx.parents()[0].node()
784 parent = ctx.parents()[0].node()
785 repo.ui.pushbuffer()
785 repo.ui.pushbuffer()
786 hg.update(repo, parent)
786 hg.update(repo, parent)
787 repo.ui.popbuffer()
787 repo.ui.popbuffer()
788 ### prepare new commit data
788 ### prepare new commit data
789 commitopts = {}
789 commitopts = {}
790 commitopts['user'] = ctx.user()
790 commitopts['user'] = ctx.user()
791 # commit message
791 # commit message
792 if not self.mergedescs():
792 if not self.mergedescs():
793 newmessage = ctx.description()
793 newmessage = ctx.description()
794 else:
794 else:
795 newmessage = '\n***\n'.join(
795 newmessage = '\n***\n'.join(
796 [ctx.description()] +
796 [ctx.description()] +
797 [repo[r].description() for r in internalchanges] +
797 [repo[r].description() for r in internalchanges] +
798 [oldctx.description()]) + '\n'
798 [oldctx.description()]) + '\n'
799 commitopts['message'] = newmessage
799 commitopts['message'] = newmessage
800 # date
800 # date
801 if self.firstdate():
801 if self.firstdate():
802 commitopts['date'] = ctx.date()
802 commitopts['date'] = ctx.date()
803 else:
803 else:
804 commitopts['date'] = max(ctx.date(), oldctx.date())
804 commitopts['date'] = max(ctx.date(), oldctx.date())
805 extra = ctx.extra().copy()
805 extra = ctx.extra().copy()
806 # histedit_source
806 # histedit_source
807 # note: ctx is likely a temporary commit but that the best we can do
807 # note: ctx is likely a temporary commit but that the best we can do
808 # here. This is sufficient to solve issue3681 anyway.
808 # here. This is sufficient to solve issue3681 anyway.
809 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
809 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
810 commitopts['extra'] = extra
810 commitopts['extra'] = extra
811 phasemin = max(ctx.phase(), oldctx.phase())
811 phasemin = max(ctx.phase(), oldctx.phase())
812 overrides = {('phases', 'new-commit'): phasemin}
812 overrides = {('phases', 'new-commit'): phasemin}
813 with repo.ui.configoverride(overrides, 'histedit'):
813 with repo.ui.configoverride(overrides, 'histedit'):
814 n = collapse(repo, ctx, repo[newnode], commitopts,
814 n = collapse(repo, ctx, repo[newnode], commitopts,
815 skipprompt=self.skipprompt())
815 skipprompt=self.skipprompt())
816 if n is None:
816 if n is None:
817 return ctx, []
817 return ctx, []
818 repo.ui.pushbuffer()
818 repo.ui.pushbuffer()
819 hg.update(repo, n)
819 hg.update(repo, n)
820 repo.ui.popbuffer()
820 repo.ui.popbuffer()
821 replacements = [(oldctx.node(), (newnode,)),
821 replacements = [(oldctx.node(), (newnode,)),
822 (ctx.node(), (n,)),
822 (ctx.node(), (n,)),
823 (newnode, (n,)),
823 (newnode, (n,)),
824 ]
824 ]
825 for ich in internalchanges:
825 for ich in internalchanges:
826 replacements.append((ich, (n,)))
826 replacements.append((ich, (n,)))
827 return repo[n], replacements
827 return repo[n], replacements
828
828
829 @action(['base', 'b'],
829 @action(['base', 'b'],
830 _('checkout changeset and apply further changesets from there'))
830 _('checkout changeset and apply further changesets from there'))
831 class base(histeditaction):
831 class base(histeditaction):
832
832
833 def run(self):
833 def run(self):
834 if self.repo['.'].node() != self.node:
834 if self.repo['.'].node() != self.node:
835 mergemod.update(self.repo, self.node, False, True)
835 mergemod.update(self.repo, self.node, False, True)
836 # branchmerge, force)
836 # branchmerge, force)
837 return self.continueclean()
837 return self.continueclean()
838
838
839 def continuedirty(self):
839 def continuedirty(self):
840 abortdirty()
840 abortdirty()
841
841
842 def continueclean(self):
842 def continueclean(self):
843 basectx = self.repo['.']
843 basectx = self.repo['.']
844 return basectx, []
844 return basectx, []
845
845
846 def _verifynodeconstraints(self, prev, expected, seen):
846 def _verifynodeconstraints(self, prev, expected, seen):
847 # base can only be use with a node not in the edited set
847 # base can only be use with a node not in the edited set
848 if self.node in expected:
848 if self.node in expected:
849 msg = _('%s "%s" changeset was an edited list candidate')
849 msg = _('%s "%s" changeset was an edited list candidate')
850 raise error.ParseError(
850 raise error.ParseError(
851 msg % (self.verb, node.short(self.node)),
851 msg % (self.verb, node.short(self.node)),
852 hint=_('base must only use unlisted changesets'))
852 hint=_('base must only use unlisted changesets'))
853
853
854 @action(['_multifold'],
854 @action(['_multifold'],
855 _(
855 _(
856 """fold subclass used for when multiple folds happen in a row
856 """fold subclass used for when multiple folds happen in a row
857
857
858 We only want to fire the editor for the folded message once when
858 We only want to fire the editor for the folded message once when
859 (say) four changes are folded down into a single change. This is
859 (say) four changes are folded down into a single change. This is
860 similar to rollup, but we should preserve both messages so that
860 similar to rollup, but we should preserve both messages so that
861 when the last fold operation runs we can show the user all the
861 when the last fold operation runs we can show the user all the
862 commit messages in their editor.
862 commit messages in their editor.
863 """),
863 """),
864 internal=True)
864 internal=True)
865 class _multifold(fold):
865 class _multifold(fold):
866 def skipprompt(self):
866 def skipprompt(self):
867 return True
867 return True
868
868
869 @action(["roll", "r"],
869 @action(["roll", "r"],
870 _("like fold, but discard this commit's description and date"))
870 _("like fold, but discard this commit's description and date"))
871 class rollup(fold):
871 class rollup(fold):
872 def mergedescs(self):
872 def mergedescs(self):
873 return False
873 return False
874
874
875 def skipprompt(self):
875 def skipprompt(self):
876 return True
876 return True
877
877
878 def firstdate(self):
878 def firstdate(self):
879 return True
879 return True
880
880
881 @action(["drop", "d"],
881 @action(["drop", "d"],
882 _('remove commit from history'))
882 _('remove commit from history'))
883 class drop(histeditaction):
883 class drop(histeditaction):
884 def run(self):
884 def run(self):
885 parentctx = self.repo[self.state.parentctxnode]
885 parentctx = self.repo[self.state.parentctxnode]
886 return parentctx, [(self.node, tuple())]
886 return parentctx, [(self.node, tuple())]
887
887
888 @action(["mess", "m"],
888 @action(["mess", "m"],
889 _('edit commit message without changing commit content'),
889 _('edit commit message without changing commit content'),
890 priority=True)
890 priority=True)
891 class message(histeditaction):
891 class message(histeditaction):
892 def commiteditor(self):
892 def commiteditor(self):
893 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
893 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
894
894
895 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
895 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
896 """utility function to find the first outgoing changeset
896 """utility function to find the first outgoing changeset
897
897
898 Used by initialization code"""
898 Used by initialization code"""
899 if opts is None:
899 if opts is None:
900 opts = {}
900 opts = {}
901 dest = ui.expandpath(remote or 'default-push', remote or 'default')
901 dest = ui.expandpath(remote or 'default-push', remote or 'default')
902 dest, revs = hg.parseurl(dest, None)[:2]
902 dest, branches = hg.parseurl(dest, None)[:2]
903 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
903 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
904
904
905 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
905 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
906 other = hg.peer(repo, opts, dest)
906 other = hg.peer(repo, opts, dest)
907
907
908 if revs:
908 if revs:
909 revs = [repo.lookup(rev) for rev in revs]
909 revs = [repo.lookup(rev) for rev in revs]
910
910
911 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
911 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
912 if not outgoing.missing:
912 if not outgoing.missing:
913 raise error.Abort(_('no outgoing ancestors'))
913 raise error.Abort(_('no outgoing ancestors'))
914 roots = list(repo.revs("roots(%ln)", outgoing.missing))
914 roots = list(repo.revs("roots(%ln)", outgoing.missing))
915 if 1 < len(roots):
915 if 1 < len(roots):
916 msg = _('there are ambiguous outgoing revisions')
916 msg = _('there are ambiguous outgoing revisions')
917 hint = _("see 'hg help histedit' for more detail")
917 hint = _("see 'hg help histedit' for more detail")
918 raise error.Abort(msg, hint=hint)
918 raise error.Abort(msg, hint=hint)
919 return repo.lookup(roots[0])
919 return repo.lookup(roots[0])
920
920
921 @command('histedit',
921 @command('histedit',
922 [('', 'commands', '',
922 [('', 'commands', '',
923 _('read history edits from the specified file'), _('FILE')),
923 _('read history edits from the specified file'), _('FILE')),
924 ('c', 'continue', False, _('continue an edit already in progress')),
924 ('c', 'continue', False, _('continue an edit already in progress')),
925 ('', 'edit-plan', False, _('edit remaining actions list')),
925 ('', 'edit-plan', False, _('edit remaining actions list')),
926 ('k', 'keep', False,
926 ('k', 'keep', False,
927 _("don't strip old nodes after edit is complete")),
927 _("don't strip old nodes after edit is complete")),
928 ('', 'abort', False, _('abort an edit in progress')),
928 ('', 'abort', False, _('abort an edit in progress')),
929 ('o', 'outgoing', False, _('changesets not found in destination')),
929 ('o', 'outgoing', False, _('changesets not found in destination')),
930 ('f', 'force', False,
930 ('f', 'force', False,
931 _('force outgoing even for unrelated repositories')),
931 _('force outgoing even for unrelated repositories')),
932 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
932 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
933 cmdutil.formatteropts,
933 cmdutil.formatteropts,
934 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
934 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
935 def histedit(ui, repo, *freeargs, **opts):
935 def histedit(ui, repo, *freeargs, **opts):
936 """interactively edit changeset history
936 """interactively edit changeset history
937
937
938 This command lets you edit a linear series of changesets (up to
938 This command lets you edit a linear series of changesets (up to
939 and including the working directory, which should be clean).
939 and including the working directory, which should be clean).
940 You can:
940 You can:
941
941
942 - `pick` to [re]order a changeset
942 - `pick` to [re]order a changeset
943
943
944 - `drop` to omit changeset
944 - `drop` to omit changeset
945
945
946 - `mess` to reword the changeset commit message
946 - `mess` to reword the changeset commit message
947
947
948 - `fold` to combine it with the preceding changeset (using the later date)
948 - `fold` to combine it with the preceding changeset (using the later date)
949
949
950 - `roll` like fold, but discarding this commit's description and date
950 - `roll` like fold, but discarding this commit's description and date
951
951
952 - `edit` to edit this changeset (preserving date)
952 - `edit` to edit this changeset (preserving date)
953
953
954 - `base` to checkout changeset and apply further changesets from there
954 - `base` to checkout changeset and apply further changesets from there
955
955
956 There are a number of ways to select the root changeset:
956 There are a number of ways to select the root changeset:
957
957
958 - Specify ANCESTOR directly
958 - Specify ANCESTOR directly
959
959
960 - Use --outgoing -- it will be the first linear changeset not
960 - Use --outgoing -- it will be the first linear changeset not
961 included in destination. (See :hg:`help config.paths.default-push`)
961 included in destination. (See :hg:`help config.paths.default-push`)
962
962
963 - Otherwise, the value from the "histedit.defaultrev" config option
963 - Otherwise, the value from the "histedit.defaultrev" config option
964 is used as a revset to select the base revision when ANCESTOR is not
964 is used as a revset to select the base revision when ANCESTOR is not
965 specified. The first revision returned by the revset is used. By
965 specified. The first revision returned by the revset is used. By
966 default, this selects the editable history that is unique to the
966 default, this selects the editable history that is unique to the
967 ancestry of the working directory.
967 ancestry of the working directory.
968
968
969 .. container:: verbose
969 .. container:: verbose
970
970
971 If you use --outgoing, this command will abort if there are ambiguous
971 If you use --outgoing, this command will abort if there are ambiguous
972 outgoing revisions. For example, if there are multiple branches
972 outgoing revisions. For example, if there are multiple branches
973 containing outgoing revisions.
973 containing outgoing revisions.
974
974
975 Use "min(outgoing() and ::.)" or similar revset specification
975 Use "min(outgoing() and ::.)" or similar revset specification
976 instead of --outgoing to specify edit target revision exactly in
976 instead of --outgoing to specify edit target revision exactly in
977 such ambiguous situation. See :hg:`help revsets` for detail about
977 such ambiguous situation. See :hg:`help revsets` for detail about
978 selecting revisions.
978 selecting revisions.
979
979
980 .. container:: verbose
980 .. container:: verbose
981
981
982 Examples:
982 Examples:
983
983
984 - A number of changes have been made.
984 - A number of changes have been made.
985 Revision 3 is no longer needed.
985 Revision 3 is no longer needed.
986
986
987 Start history editing from revision 3::
987 Start history editing from revision 3::
988
988
989 hg histedit -r 3
989 hg histedit -r 3
990
990
991 An editor opens, containing the list of revisions,
991 An editor opens, containing the list of revisions,
992 with specific actions specified::
992 with specific actions specified::
993
993
994 pick 5339bf82f0ca 3 Zworgle the foobar
994 pick 5339bf82f0ca 3 Zworgle the foobar
995 pick 8ef592ce7cc4 4 Bedazzle the zerlog
995 pick 8ef592ce7cc4 4 Bedazzle the zerlog
996 pick 0a9639fcda9d 5 Morgify the cromulancy
996 pick 0a9639fcda9d 5 Morgify the cromulancy
997
997
998 Additional information about the possible actions
998 Additional information about the possible actions
999 to take appears below the list of revisions.
999 to take appears below the list of revisions.
1000
1000
1001 To remove revision 3 from the history,
1001 To remove revision 3 from the history,
1002 its action (at the beginning of the relevant line)
1002 its action (at the beginning of the relevant line)
1003 is changed to 'drop'::
1003 is changed to 'drop'::
1004
1004
1005 drop 5339bf82f0ca 3 Zworgle the foobar
1005 drop 5339bf82f0ca 3 Zworgle the foobar
1006 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1006 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1007 pick 0a9639fcda9d 5 Morgify the cromulancy
1007 pick 0a9639fcda9d 5 Morgify the cromulancy
1008
1008
1009 - A number of changes have been made.
1009 - A number of changes have been made.
1010 Revision 2 and 4 need to be swapped.
1010 Revision 2 and 4 need to be swapped.
1011
1011
1012 Start history editing from revision 2::
1012 Start history editing from revision 2::
1013
1013
1014 hg histedit -r 2
1014 hg histedit -r 2
1015
1015
1016 An editor opens, containing the list of revisions,
1016 An editor opens, containing the list of revisions,
1017 with specific actions specified::
1017 with specific actions specified::
1018
1018
1019 pick 252a1af424ad 2 Blorb a morgwazzle
1019 pick 252a1af424ad 2 Blorb a morgwazzle
1020 pick 5339bf82f0ca 3 Zworgle the foobar
1020 pick 5339bf82f0ca 3 Zworgle the foobar
1021 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1021 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1022
1022
1023 To swap revision 2 and 4, its lines are swapped
1023 To swap revision 2 and 4, its lines are swapped
1024 in the editor::
1024 in the editor::
1025
1025
1026 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1026 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1027 pick 5339bf82f0ca 3 Zworgle the foobar
1027 pick 5339bf82f0ca 3 Zworgle the foobar
1028 pick 252a1af424ad 2 Blorb a morgwazzle
1028 pick 252a1af424ad 2 Blorb a morgwazzle
1029
1029
1030 Returns 0 on success, 1 if user intervention is required (not only
1030 Returns 0 on success, 1 if user intervention is required (not only
1031 for intentional "edit" command, but also for resolving unexpected
1031 for intentional "edit" command, but also for resolving unexpected
1032 conflicts).
1032 conflicts).
1033 """
1033 """
1034 state = histeditstate(repo)
1034 state = histeditstate(repo)
1035 try:
1035 try:
1036 state.wlock = repo.wlock()
1036 state.wlock = repo.wlock()
1037 state.lock = repo.lock()
1037 state.lock = repo.lock()
1038 _histedit(ui, repo, state, *freeargs, **opts)
1038 _histedit(ui, repo, state, *freeargs, **opts)
1039 finally:
1039 finally:
1040 release(state.lock, state.wlock)
1040 release(state.lock, state.wlock)
1041
1041
1042 goalcontinue = 'continue'
1042 goalcontinue = 'continue'
1043 goalabort = 'abort'
1043 goalabort = 'abort'
1044 goaleditplan = 'edit-plan'
1044 goaleditplan = 'edit-plan'
1045 goalnew = 'new'
1045 goalnew = 'new'
1046
1046
1047 def _getgoal(opts):
1047 def _getgoal(opts):
1048 if opts.get('continue'):
1048 if opts.get('continue'):
1049 return goalcontinue
1049 return goalcontinue
1050 if opts.get('abort'):
1050 if opts.get('abort'):
1051 return goalabort
1051 return goalabort
1052 if opts.get('edit_plan'):
1052 if opts.get('edit_plan'):
1053 return goaleditplan
1053 return goaleditplan
1054 return goalnew
1054 return goalnew
1055
1055
1056 def _readfile(ui, path):
1056 def _readfile(ui, path):
1057 if path == '-':
1057 if path == '-':
1058 with ui.timeblockedsection('histedit'):
1058 with ui.timeblockedsection('histedit'):
1059 return ui.fin.read()
1059 return ui.fin.read()
1060 else:
1060 else:
1061 with open(path, 'rb') as f:
1061 with open(path, 'rb') as f:
1062 return f.read()
1062 return f.read()
1063
1063
1064 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1064 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1065 # TODO only abort if we try to histedit mq patches, not just
1065 # TODO only abort if we try to histedit mq patches, not just
1066 # blanket if mq patches are applied somewhere
1066 # blanket if mq patches are applied somewhere
1067 mq = getattr(repo, 'mq', None)
1067 mq = getattr(repo, 'mq', None)
1068 if mq and mq.applied:
1068 if mq and mq.applied:
1069 raise error.Abort(_('source has mq patches applied'))
1069 raise error.Abort(_('source has mq patches applied'))
1070
1070
1071 # basic argument incompatibility processing
1071 # basic argument incompatibility processing
1072 outg = opts.get('outgoing')
1072 outg = opts.get('outgoing')
1073 editplan = opts.get('edit_plan')
1073 editplan = opts.get('edit_plan')
1074 abort = opts.get('abort')
1074 abort = opts.get('abort')
1075 force = opts.get('force')
1075 force = opts.get('force')
1076 if force and not outg:
1076 if force and not outg:
1077 raise error.Abort(_('--force only allowed with --outgoing'))
1077 raise error.Abort(_('--force only allowed with --outgoing'))
1078 if goal == 'continue':
1078 if goal == 'continue':
1079 if any((outg, abort, revs, freeargs, rules, editplan)):
1079 if any((outg, abort, revs, freeargs, rules, editplan)):
1080 raise error.Abort(_('no arguments allowed with --continue'))
1080 raise error.Abort(_('no arguments allowed with --continue'))
1081 elif goal == 'abort':
1081 elif goal == 'abort':
1082 if any((outg, revs, freeargs, rules, editplan)):
1082 if any((outg, revs, freeargs, rules, editplan)):
1083 raise error.Abort(_('no arguments allowed with --abort'))
1083 raise error.Abort(_('no arguments allowed with --abort'))
1084 elif goal == 'edit-plan':
1084 elif goal == 'edit-plan':
1085 if any((outg, revs, freeargs)):
1085 if any((outg, revs, freeargs)):
1086 raise error.Abort(_('only --commands argument allowed with '
1086 raise error.Abort(_('only --commands argument allowed with '
1087 '--edit-plan'))
1087 '--edit-plan'))
1088 else:
1088 else:
1089 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1089 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1090 raise error.Abort(_('history edit already in progress, try '
1090 raise error.Abort(_('history edit already in progress, try '
1091 '--continue or --abort'))
1091 '--continue or --abort'))
1092 if outg:
1092 if outg:
1093 if revs:
1093 if revs:
1094 raise error.Abort(_('no revisions allowed with --outgoing'))
1094 raise error.Abort(_('no revisions allowed with --outgoing'))
1095 if len(freeargs) > 1:
1095 if len(freeargs) > 1:
1096 raise error.Abort(
1096 raise error.Abort(
1097 _('only one repo argument allowed with --outgoing'))
1097 _('only one repo argument allowed with --outgoing'))
1098 else:
1098 else:
1099 revs.extend(freeargs)
1099 revs.extend(freeargs)
1100 if len(revs) == 0:
1100 if len(revs) == 0:
1101 defaultrev = destutil.desthistedit(ui, repo)
1101 defaultrev = destutil.desthistedit(ui, repo)
1102 if defaultrev is not None:
1102 if defaultrev is not None:
1103 revs.append(defaultrev)
1103 revs.append(defaultrev)
1104
1104
1105 if len(revs) != 1:
1105 if len(revs) != 1:
1106 raise error.Abort(
1106 raise error.Abort(
1107 _('histedit requires exactly one ancestor revision'))
1107 _('histedit requires exactly one ancestor revision'))
1108
1108
1109 def _histedit(ui, repo, state, *freeargs, **opts):
1109 def _histedit(ui, repo, state, *freeargs, **opts):
1110 opts = pycompat.byteskwargs(opts)
1110 opts = pycompat.byteskwargs(opts)
1111 fm = ui.formatter('histedit', opts)
1111 fm = ui.formatter('histedit', opts)
1112 fm.startitem()
1112 fm.startitem()
1113 goal = _getgoal(opts)
1113 goal = _getgoal(opts)
1114 revs = opts.get('rev', [])
1114 revs = opts.get('rev', [])
1115 rules = opts.get('commands', '')
1115 rules = opts.get('commands', '')
1116 state.keep = opts.get('keep', False)
1116 state.keep = opts.get('keep', False)
1117
1117
1118 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1118 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1119
1119
1120 # rebuild state
1120 # rebuild state
1121 if goal == goalcontinue:
1121 if goal == goalcontinue:
1122 state.read()
1122 state.read()
1123 state = bootstrapcontinue(ui, state, opts)
1123 state = bootstrapcontinue(ui, state, opts)
1124 elif goal == goaleditplan:
1124 elif goal == goaleditplan:
1125 _edithisteditplan(ui, repo, state, rules)
1125 _edithisteditplan(ui, repo, state, rules)
1126 return
1126 return
1127 elif goal == goalabort:
1127 elif goal == goalabort:
1128 _aborthistedit(ui, repo, state)
1128 _aborthistedit(ui, repo, state)
1129 return
1129 return
1130 else:
1130 else:
1131 # goal == goalnew
1131 # goal == goalnew
1132 _newhistedit(ui, repo, state, revs, freeargs, opts)
1132 _newhistedit(ui, repo, state, revs, freeargs, opts)
1133
1133
1134 _continuehistedit(ui, repo, state)
1134 _continuehistedit(ui, repo, state)
1135 _finishhistedit(ui, repo, state, fm)
1135 _finishhistedit(ui, repo, state, fm)
1136 fm.end()
1136 fm.end()
1137
1137
1138 def _continuehistedit(ui, repo, state):
1138 def _continuehistedit(ui, repo, state):
1139 """This function runs after either:
1139 """This function runs after either:
1140 - bootstrapcontinue (if the goal is 'continue')
1140 - bootstrapcontinue (if the goal is 'continue')
1141 - _newhistedit (if the goal is 'new')
1141 - _newhistedit (if the goal is 'new')
1142 """
1142 """
1143 # preprocess rules so that we can hide inner folds from the user
1143 # preprocess rules so that we can hide inner folds from the user
1144 # and only show one editor
1144 # and only show one editor
1145 actions = state.actions[:]
1145 actions = state.actions[:]
1146 for idx, (action, nextact) in enumerate(
1146 for idx, (action, nextact) in enumerate(
1147 zip(actions, actions[1:] + [None])):
1147 zip(actions, actions[1:] + [None])):
1148 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1148 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1149 state.actions[idx].__class__ = _multifold
1149 state.actions[idx].__class__ = _multifold
1150
1150
1151 # Force an initial state file write, so the user can run --abort/continue
1151 # Force an initial state file write, so the user can run --abort/continue
1152 # even if there's an exception before the first transaction serialize.
1152 # even if there's an exception before the first transaction serialize.
1153 state.write()
1153 state.write()
1154
1154
1155 total = len(state.actions)
1155 total = len(state.actions)
1156 pos = 0
1156 pos = 0
1157 tr = None
1157 tr = None
1158 # Don't use singletransaction by default since it rolls the entire
1158 # Don't use singletransaction by default since it rolls the entire
1159 # transaction back if an unexpected exception happens (like a
1159 # transaction back if an unexpected exception happens (like a
1160 # pretxncommit hook throws, or the user aborts the commit msg editor).
1160 # pretxncommit hook throws, or the user aborts the commit msg editor).
1161 if ui.configbool("histedit", "singletransaction"):
1161 if ui.configbool("histedit", "singletransaction"):
1162 # Don't use a 'with' for the transaction, since actions may close
1162 # Don't use a 'with' for the transaction, since actions may close
1163 # and reopen a transaction. For example, if the action executes an
1163 # and reopen a transaction. For example, if the action executes an
1164 # external process it may choose to commit the transaction first.
1164 # external process it may choose to commit the transaction first.
1165 tr = repo.transaction('histedit')
1165 tr = repo.transaction('histedit')
1166 with util.acceptintervention(tr):
1166 with util.acceptintervention(tr):
1167 while state.actions:
1167 while state.actions:
1168 state.write(tr=tr)
1168 state.write(tr=tr)
1169 actobj = state.actions[0]
1169 actobj = state.actions[0]
1170 pos += 1
1170 pos += 1
1171 ui.progress(_("editing"), pos, actobj.torule(),
1171 ui.progress(_("editing"), pos, actobj.torule(),
1172 _('changes'), total)
1172 _('changes'), total)
1173 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1173 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1174 actobj.torule()))
1174 actobj.torule()))
1175 parentctx, replacement_ = actobj.run()
1175 parentctx, replacement_ = actobj.run()
1176 state.parentctxnode = parentctx.node()
1176 state.parentctxnode = parentctx.node()
1177 state.replacements.extend(replacement_)
1177 state.replacements.extend(replacement_)
1178 state.actions.pop(0)
1178 state.actions.pop(0)
1179
1179
1180 state.write()
1180 state.write()
1181 ui.progress(_("editing"), None)
1181 ui.progress(_("editing"), None)
1182
1182
1183 def _finishhistedit(ui, repo, state, fm):
1183 def _finishhistedit(ui, repo, state, fm):
1184 """This action runs when histedit is finishing its session"""
1184 """This action runs when histedit is finishing its session"""
1185 repo.ui.pushbuffer()
1185 repo.ui.pushbuffer()
1186 hg.update(repo, state.parentctxnode, quietempty=True)
1186 hg.update(repo, state.parentctxnode, quietempty=True)
1187 repo.ui.popbuffer()
1187 repo.ui.popbuffer()
1188
1188
1189 mapping, tmpnodes, created, ntm = processreplacement(state)
1189 mapping, tmpnodes, created, ntm = processreplacement(state)
1190 if mapping:
1190 if mapping:
1191 for prec, succs in mapping.iteritems():
1191 for prec, succs in mapping.iteritems():
1192 if not succs:
1192 if not succs:
1193 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1193 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1194 else:
1194 else:
1195 ui.debug('histedit: %s is replaced by %s\n' % (
1195 ui.debug('histedit: %s is replaced by %s\n' % (
1196 node.short(prec), node.short(succs[0])))
1196 node.short(prec), node.short(succs[0])))
1197 if len(succs) > 1:
1197 if len(succs) > 1:
1198 m = 'histedit: %s'
1198 m = 'histedit: %s'
1199 for n in succs[1:]:
1199 for n in succs[1:]:
1200 ui.debug(m % node.short(n))
1200 ui.debug(m % node.short(n))
1201
1201
1202 if not state.keep:
1202 if not state.keep:
1203 if mapping:
1203 if mapping:
1204 movetopmostbookmarks(repo, state.topmost, ntm)
1204 movetopmostbookmarks(repo, state.topmost, ntm)
1205 # TODO update mq state
1205 # TODO update mq state
1206 else:
1206 else:
1207 mapping = {}
1207 mapping = {}
1208
1208
1209 for n in tmpnodes:
1209 for n in tmpnodes:
1210 mapping[n] = ()
1210 mapping[n] = ()
1211
1211
1212 # remove entries about unknown nodes
1212 # remove entries about unknown nodes
1213 nodemap = repo.unfiltered().changelog.nodemap
1213 nodemap = repo.unfiltered().changelog.nodemap
1214 mapping = {k: v for k, v in mapping.items()
1214 mapping = {k: v for k, v in mapping.items()
1215 if k in nodemap and all(n in nodemap for n in v)}
1215 if k in nodemap and all(n in nodemap for n in v)}
1216 scmutil.cleanupnodes(repo, mapping, 'histedit')
1216 scmutil.cleanupnodes(repo, mapping, 'histedit')
1217 hf = fm.hexfunc
1217 hf = fm.hexfunc
1218 fl = fm.formatlist
1218 fl = fm.formatlist
1219 fd = fm.formatdict
1219 fd = fm.formatdict
1220 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1220 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1221 for oldn, newn in mapping.iteritems()},
1221 for oldn, newn in mapping.iteritems()},
1222 key="oldnode", value="newnodes")
1222 key="oldnode", value="newnodes")
1223 fm.data(nodechanges=nodechanges)
1223 fm.data(nodechanges=nodechanges)
1224
1224
1225 state.clear()
1225 state.clear()
1226 if os.path.exists(repo.sjoin('undo')):
1226 if os.path.exists(repo.sjoin('undo')):
1227 os.unlink(repo.sjoin('undo'))
1227 os.unlink(repo.sjoin('undo'))
1228 if repo.vfs.exists('histedit-last-edit.txt'):
1228 if repo.vfs.exists('histedit-last-edit.txt'):
1229 repo.vfs.unlink('histedit-last-edit.txt')
1229 repo.vfs.unlink('histedit-last-edit.txt')
1230
1230
1231 def _aborthistedit(ui, repo, state):
1231 def _aborthistedit(ui, repo, state):
1232 try:
1232 try:
1233 state.read()
1233 state.read()
1234 __, leafs, tmpnodes, __ = processreplacement(state)
1234 __, leafs, tmpnodes, __ = processreplacement(state)
1235 ui.debug('restore wc to old parent %s\n'
1235 ui.debug('restore wc to old parent %s\n'
1236 % node.short(state.topmost))
1236 % node.short(state.topmost))
1237
1237
1238 # Recover our old commits if necessary
1238 # Recover our old commits if necessary
1239 if not state.topmost in repo and state.backupfile:
1239 if not state.topmost in repo and state.backupfile:
1240 backupfile = repo.vfs.join(state.backupfile)
1240 backupfile = repo.vfs.join(state.backupfile)
1241 f = hg.openpath(ui, backupfile)
1241 f = hg.openpath(ui, backupfile)
1242 gen = exchange.readbundle(ui, f, backupfile)
1242 gen = exchange.readbundle(ui, f, backupfile)
1243 with repo.transaction('histedit.abort') as tr:
1243 with repo.transaction('histedit.abort') as tr:
1244 bundle2.applybundle(repo, gen, tr, source='histedit',
1244 bundle2.applybundle(repo, gen, tr, source='histedit',
1245 url='bundle:' + backupfile)
1245 url='bundle:' + backupfile)
1246
1246
1247 os.remove(backupfile)
1247 os.remove(backupfile)
1248
1248
1249 # check whether we should update away
1249 # check whether we should update away
1250 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1250 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1251 state.parentctxnode, leafs | tmpnodes):
1251 state.parentctxnode, leafs | tmpnodes):
1252 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1252 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1253 cleanupnode(ui, repo, tmpnodes)
1253 cleanupnode(ui, repo, tmpnodes)
1254 cleanupnode(ui, repo, leafs)
1254 cleanupnode(ui, repo, leafs)
1255 except Exception:
1255 except Exception:
1256 if state.inprogress():
1256 if state.inprogress():
1257 ui.warn(_('warning: encountered an exception during histedit '
1257 ui.warn(_('warning: encountered an exception during histedit '
1258 '--abort; the repository may not have been completely '
1258 '--abort; the repository may not have been completely '
1259 'cleaned up\n'))
1259 'cleaned up\n'))
1260 raise
1260 raise
1261 finally:
1261 finally:
1262 state.clear()
1262 state.clear()
1263
1263
1264 def _edithisteditplan(ui, repo, state, rules):
1264 def _edithisteditplan(ui, repo, state, rules):
1265 state.read()
1265 state.read()
1266 if not rules:
1266 if not rules:
1267 comment = geteditcomment(ui,
1267 comment = geteditcomment(ui,
1268 node.short(state.parentctxnode),
1268 node.short(state.parentctxnode),
1269 node.short(state.topmost))
1269 node.short(state.topmost))
1270 rules = ruleeditor(repo, ui, state.actions, comment)
1270 rules = ruleeditor(repo, ui, state.actions, comment)
1271 else:
1271 else:
1272 rules = _readfile(ui, rules)
1272 rules = _readfile(ui, rules)
1273 actions = parserules(rules, state)
1273 actions = parserules(rules, state)
1274 ctxs = [repo[act.node] \
1274 ctxs = [repo[act.node] \
1275 for act in state.actions if act.node]
1275 for act in state.actions if act.node]
1276 warnverifyactions(ui, repo, actions, state, ctxs)
1276 warnverifyactions(ui, repo, actions, state, ctxs)
1277 state.actions = actions
1277 state.actions = actions
1278 state.write()
1278 state.write()
1279
1279
1280 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1280 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1281 outg = opts.get('outgoing')
1281 outg = opts.get('outgoing')
1282 rules = opts.get('commands', '')
1282 rules = opts.get('commands', '')
1283 force = opts.get('force')
1283 force = opts.get('force')
1284
1284
1285 cmdutil.checkunfinished(repo)
1285 cmdutil.checkunfinished(repo)
1286 cmdutil.bailifchanged(repo)
1286 cmdutil.bailifchanged(repo)
1287
1287
1288 topmost, empty = repo.dirstate.parents()
1288 topmost, empty = repo.dirstate.parents()
1289 if outg:
1289 if outg:
1290 if freeargs:
1290 if freeargs:
1291 remote = freeargs[0]
1291 remote = freeargs[0]
1292 else:
1292 else:
1293 remote = None
1293 remote = None
1294 root = findoutgoing(ui, repo, remote, force, opts)
1294 root = findoutgoing(ui, repo, remote, force, opts)
1295 else:
1295 else:
1296 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1296 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1297 if len(rr) != 1:
1297 if len(rr) != 1:
1298 raise error.Abort(_('The specified revisions must have '
1298 raise error.Abort(_('The specified revisions must have '
1299 'exactly one common root'))
1299 'exactly one common root'))
1300 root = rr[0].node()
1300 root = rr[0].node()
1301
1301
1302 revs = between(repo, root, topmost, state.keep)
1302 revs = between(repo, root, topmost, state.keep)
1303 if not revs:
1303 if not revs:
1304 raise error.Abort(_('%s is not an ancestor of working directory') %
1304 raise error.Abort(_('%s is not an ancestor of working directory') %
1305 node.short(root))
1305 node.short(root))
1306
1306
1307 ctxs = [repo[r] for r in revs]
1307 ctxs = [repo[r] for r in revs]
1308 if not rules:
1308 if not rules:
1309 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1309 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1310 actions = [pick(state, r) for r in revs]
1310 actions = [pick(state, r) for r in revs]
1311 rules = ruleeditor(repo, ui, actions, comment)
1311 rules = ruleeditor(repo, ui, actions, comment)
1312 else:
1312 else:
1313 rules = _readfile(ui, rules)
1313 rules = _readfile(ui, rules)
1314 actions = parserules(rules, state)
1314 actions = parserules(rules, state)
1315 warnverifyactions(ui, repo, actions, state, ctxs)
1315 warnverifyactions(ui, repo, actions, state, ctxs)
1316
1316
1317 parentctxnode = repo[root].parents()[0].node()
1317 parentctxnode = repo[root].parents()[0].node()
1318
1318
1319 state.parentctxnode = parentctxnode
1319 state.parentctxnode = parentctxnode
1320 state.actions = actions
1320 state.actions = actions
1321 state.topmost = topmost
1321 state.topmost = topmost
1322 state.replacements = []
1322 state.replacements = []
1323
1323
1324 ui.log("histedit", "%d actions to histedit", len(actions),
1324 ui.log("histedit", "%d actions to histedit", len(actions),
1325 histedit_num_actions=len(actions))
1325 histedit_num_actions=len(actions))
1326
1326
1327 # Create a backup so we can always abort completely.
1327 # Create a backup so we can always abort completely.
1328 backupfile = None
1328 backupfile = None
1329 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1329 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1330 backupfile = repair.backupbundle(repo, [parentctxnode],
1330 backupfile = repair.backupbundle(repo, [parentctxnode],
1331 [topmost], root, 'histedit')
1331 [topmost], root, 'histedit')
1332 state.backupfile = backupfile
1332 state.backupfile = backupfile
1333
1333
1334 def _getsummary(ctx):
1334 def _getsummary(ctx):
1335 # a common pattern is to extract the summary but default to the empty
1335 # a common pattern is to extract the summary but default to the empty
1336 # string
1336 # string
1337 summary = ctx.description() or ''
1337 summary = ctx.description() or ''
1338 if summary:
1338 if summary:
1339 summary = summary.splitlines()[0]
1339 summary = summary.splitlines()[0]
1340 return summary
1340 return summary
1341
1341
1342 def bootstrapcontinue(ui, state, opts):
1342 def bootstrapcontinue(ui, state, opts):
1343 repo = state.repo
1343 repo = state.repo
1344
1344
1345 ms = mergemod.mergestate.read(repo)
1345 ms = mergemod.mergestate.read(repo)
1346 mergeutil.checkunresolved(ms)
1346 mergeutil.checkunresolved(ms)
1347
1347
1348 if state.actions:
1348 if state.actions:
1349 actobj = state.actions.pop(0)
1349 actobj = state.actions.pop(0)
1350
1350
1351 if _isdirtywc(repo):
1351 if _isdirtywc(repo):
1352 actobj.continuedirty()
1352 actobj.continuedirty()
1353 if _isdirtywc(repo):
1353 if _isdirtywc(repo):
1354 abortdirty()
1354 abortdirty()
1355
1355
1356 parentctx, replacements = actobj.continueclean()
1356 parentctx, replacements = actobj.continueclean()
1357
1357
1358 state.parentctxnode = parentctx.node()
1358 state.parentctxnode = parentctx.node()
1359 state.replacements.extend(replacements)
1359 state.replacements.extend(replacements)
1360
1360
1361 return state
1361 return state
1362
1362
1363 def between(repo, old, new, keep):
1363 def between(repo, old, new, keep):
1364 """select and validate the set of revision to edit
1364 """select and validate the set of revision to edit
1365
1365
1366 When keep is false, the specified set can't have children."""
1366 When keep is false, the specified set can't have children."""
1367 revs = repo.revs('%n::%n', old, new)
1367 revs = repo.revs('%n::%n', old, new)
1368 if revs and not keep:
1368 if revs and not keep:
1369 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1369 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1370 repo.revs('(%ld::) - (%ld)', revs, revs)):
1370 repo.revs('(%ld::) - (%ld)', revs, revs)):
1371 raise error.Abort(_('can only histedit a changeset together '
1371 raise error.Abort(_('can only histedit a changeset together '
1372 'with all its descendants'))
1372 'with all its descendants'))
1373 if repo.revs('(%ld) and merge()', revs):
1373 if repo.revs('(%ld) and merge()', revs):
1374 raise error.Abort(_('cannot edit history that contains merges'))
1374 raise error.Abort(_('cannot edit history that contains merges'))
1375 root = repo[revs.first()] # list is already sorted by repo.revs()
1375 root = repo[revs.first()] # list is already sorted by repo.revs()
1376 if not root.mutable():
1376 if not root.mutable():
1377 raise error.Abort(_('cannot edit public changeset: %s') % root,
1377 raise error.Abort(_('cannot edit public changeset: %s') % root,
1378 hint=_("see 'hg help phases' for details"))
1378 hint=_("see 'hg help phases' for details"))
1379 return pycompat.maplist(repo.changelog.node, revs)
1379 return pycompat.maplist(repo.changelog.node, revs)
1380
1380
1381 def ruleeditor(repo, ui, actions, editcomment=""):
1381 def ruleeditor(repo, ui, actions, editcomment=""):
1382 """open an editor to edit rules
1382 """open an editor to edit rules
1383
1383
1384 rules are in the format [ [act, ctx], ...] like in state.rules
1384 rules are in the format [ [act, ctx], ...] like in state.rules
1385 """
1385 """
1386 if repo.ui.configbool("experimental", "histedit.autoverb"):
1386 if repo.ui.configbool("experimental", "histedit.autoverb"):
1387 newact = util.sortdict()
1387 newact = util.sortdict()
1388 for act in actions:
1388 for act in actions:
1389 ctx = repo[act.node]
1389 ctx = repo[act.node]
1390 summary = _getsummary(ctx)
1390 summary = _getsummary(ctx)
1391 fword = summary.split(' ', 1)[0].lower()
1391 fword = summary.split(' ', 1)[0].lower()
1392 added = False
1392 added = False
1393
1393
1394 # if it doesn't end with the special character '!' just skip this
1394 # if it doesn't end with the special character '!' just skip this
1395 if fword.endswith('!'):
1395 if fword.endswith('!'):
1396 fword = fword[:-1]
1396 fword = fword[:-1]
1397 if fword in primaryactions | secondaryactions | tertiaryactions:
1397 if fword in primaryactions | secondaryactions | tertiaryactions:
1398 act.verb = fword
1398 act.verb = fword
1399 # get the target summary
1399 # get the target summary
1400 tsum = summary[len(fword) + 1:].lstrip()
1400 tsum = summary[len(fword) + 1:].lstrip()
1401 # safe but slow: reverse iterate over the actions so we
1401 # safe but slow: reverse iterate over the actions so we
1402 # don't clash on two commits having the same summary
1402 # don't clash on two commits having the same summary
1403 for na, l in reversed(list(newact.iteritems())):
1403 for na, l in reversed(list(newact.iteritems())):
1404 actx = repo[na.node]
1404 actx = repo[na.node]
1405 asum = _getsummary(actx)
1405 asum = _getsummary(actx)
1406 if asum == tsum:
1406 if asum == tsum:
1407 added = True
1407 added = True
1408 l.append(act)
1408 l.append(act)
1409 break
1409 break
1410
1410
1411 if not added:
1411 if not added:
1412 newact[act] = []
1412 newact[act] = []
1413
1413
1414 # copy over and flatten the new list
1414 # copy over and flatten the new list
1415 actions = []
1415 actions = []
1416 for na, l in newact.iteritems():
1416 for na, l in newact.iteritems():
1417 actions.append(na)
1417 actions.append(na)
1418 actions += l
1418 actions += l
1419
1419
1420 rules = '\n'.join([act.torule() for act in actions])
1420 rules = '\n'.join([act.torule() for act in actions])
1421 rules += '\n\n'
1421 rules += '\n\n'
1422 rules += editcomment
1422 rules += editcomment
1423 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1423 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1424 repopath=repo.path, action='histedit')
1424 repopath=repo.path, action='histedit')
1425
1425
1426 # Save edit rules in .hg/histedit-last-edit.txt in case
1426 # Save edit rules in .hg/histedit-last-edit.txt in case
1427 # the user needs to ask for help after something
1427 # the user needs to ask for help after something
1428 # surprising happens.
1428 # surprising happens.
1429 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1429 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1430 f.write(rules)
1430 f.write(rules)
1431
1431
1432 return rules
1432 return rules
1433
1433
1434 def parserules(rules, state):
1434 def parserules(rules, state):
1435 """Read the histedit rules string and return list of action objects """
1435 """Read the histedit rules string and return list of action objects """
1436 rules = [l for l in (r.strip() for r in rules.splitlines())
1436 rules = [l for l in (r.strip() for r in rules.splitlines())
1437 if l and not l.startswith('#')]
1437 if l and not l.startswith('#')]
1438 actions = []
1438 actions = []
1439 for r in rules:
1439 for r in rules:
1440 if ' ' not in r:
1440 if ' ' not in r:
1441 raise error.ParseError(_('malformed line "%s"') % r)
1441 raise error.ParseError(_('malformed line "%s"') % r)
1442 verb, rest = r.split(' ', 1)
1442 verb, rest = r.split(' ', 1)
1443
1443
1444 if verb not in actiontable:
1444 if verb not in actiontable:
1445 raise error.ParseError(_('unknown action "%s"') % verb)
1445 raise error.ParseError(_('unknown action "%s"') % verb)
1446
1446
1447 action = actiontable[verb].fromrule(state, rest)
1447 action = actiontable[verb].fromrule(state, rest)
1448 actions.append(action)
1448 actions.append(action)
1449 return actions
1449 return actions
1450
1450
1451 def warnverifyactions(ui, repo, actions, state, ctxs):
1451 def warnverifyactions(ui, repo, actions, state, ctxs):
1452 try:
1452 try:
1453 verifyactions(actions, state, ctxs)
1453 verifyactions(actions, state, ctxs)
1454 except error.ParseError:
1454 except error.ParseError:
1455 if repo.vfs.exists('histedit-last-edit.txt'):
1455 if repo.vfs.exists('histedit-last-edit.txt'):
1456 ui.warn(_('warning: histedit rules saved '
1456 ui.warn(_('warning: histedit rules saved '
1457 'to: .hg/histedit-last-edit.txt\n'))
1457 'to: .hg/histedit-last-edit.txt\n'))
1458 raise
1458 raise
1459
1459
1460 def verifyactions(actions, state, ctxs):
1460 def verifyactions(actions, state, ctxs):
1461 """Verify that there exists exactly one action per given changeset and
1461 """Verify that there exists exactly one action per given changeset and
1462 other constraints.
1462 other constraints.
1463
1463
1464 Will abort if there are to many or too few rules, a malformed rule,
1464 Will abort if there are to many or too few rules, a malformed rule,
1465 or a rule on a changeset outside of the user-given range.
1465 or a rule on a changeset outside of the user-given range.
1466 """
1466 """
1467 expected = set(c.node() for c in ctxs)
1467 expected = set(c.node() for c in ctxs)
1468 seen = set()
1468 seen = set()
1469 prev = None
1469 prev = None
1470
1470
1471 if actions and actions[0].verb in ['roll', 'fold']:
1471 if actions and actions[0].verb in ['roll', 'fold']:
1472 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1472 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1473 actions[0].verb)
1473 actions[0].verb)
1474
1474
1475 for action in actions:
1475 for action in actions:
1476 action.verify(prev, expected, seen)
1476 action.verify(prev, expected, seen)
1477 prev = action
1477 prev = action
1478 if action.node is not None:
1478 if action.node is not None:
1479 seen.add(action.node)
1479 seen.add(action.node)
1480 missing = sorted(expected - seen) # sort to stabilize output
1480 missing = sorted(expected - seen) # sort to stabilize output
1481
1481
1482 if state.repo.ui.configbool('histedit', 'dropmissing'):
1482 if state.repo.ui.configbool('histedit', 'dropmissing'):
1483 if len(actions) == 0:
1483 if len(actions) == 0:
1484 raise error.ParseError(_('no rules provided'),
1484 raise error.ParseError(_('no rules provided'),
1485 hint=_('use strip extension to remove commits'))
1485 hint=_('use strip extension to remove commits'))
1486
1486
1487 drops = [drop(state, n) for n in missing]
1487 drops = [drop(state, n) for n in missing]
1488 # put the in the beginning so they execute immediately and
1488 # put the in the beginning so they execute immediately and
1489 # don't show in the edit-plan in the future
1489 # don't show in the edit-plan in the future
1490 actions[:0] = drops
1490 actions[:0] = drops
1491 elif missing:
1491 elif missing:
1492 raise error.ParseError(_('missing rules for changeset %s') %
1492 raise error.ParseError(_('missing rules for changeset %s') %
1493 node.short(missing[0]),
1493 node.short(missing[0]),
1494 hint=_('use "drop %s" to discard, see also: '
1494 hint=_('use "drop %s" to discard, see also: '
1495 "'hg help -e histedit.config'")
1495 "'hg help -e histedit.config'")
1496 % node.short(missing[0]))
1496 % node.short(missing[0]))
1497
1497
1498 def adjustreplacementsfrommarkers(repo, oldreplacements):
1498 def adjustreplacementsfrommarkers(repo, oldreplacements):
1499 """Adjust replacements from obsolescence markers
1499 """Adjust replacements from obsolescence markers
1500
1500
1501 Replacements structure is originally generated based on
1501 Replacements structure is originally generated based on
1502 histedit's state and does not account for changes that are
1502 histedit's state and does not account for changes that are
1503 not recorded there. This function fixes that by adding
1503 not recorded there. This function fixes that by adding
1504 data read from obsolescence markers"""
1504 data read from obsolescence markers"""
1505 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1505 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1506 return oldreplacements
1506 return oldreplacements
1507
1507
1508 unfi = repo.unfiltered()
1508 unfi = repo.unfiltered()
1509 nm = unfi.changelog.nodemap
1509 nm = unfi.changelog.nodemap
1510 obsstore = repo.obsstore
1510 obsstore = repo.obsstore
1511 newreplacements = list(oldreplacements)
1511 newreplacements = list(oldreplacements)
1512 oldsuccs = [r[1] for r in oldreplacements]
1512 oldsuccs = [r[1] for r in oldreplacements]
1513 # successors that have already been added to succstocheck once
1513 # successors that have already been added to succstocheck once
1514 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1514 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1515 succstocheck = list(seensuccs)
1515 succstocheck = list(seensuccs)
1516 while succstocheck:
1516 while succstocheck:
1517 n = succstocheck.pop()
1517 n = succstocheck.pop()
1518 missing = nm.get(n) is None
1518 missing = nm.get(n) is None
1519 markers = obsstore.successors.get(n, ())
1519 markers = obsstore.successors.get(n, ())
1520 if missing and not markers:
1520 if missing and not markers:
1521 # dead end, mark it as such
1521 # dead end, mark it as such
1522 newreplacements.append((n, ()))
1522 newreplacements.append((n, ()))
1523 for marker in markers:
1523 for marker in markers:
1524 nsuccs = marker[1]
1524 nsuccs = marker[1]
1525 newreplacements.append((n, nsuccs))
1525 newreplacements.append((n, nsuccs))
1526 for nsucc in nsuccs:
1526 for nsucc in nsuccs:
1527 if nsucc not in seensuccs:
1527 if nsucc not in seensuccs:
1528 seensuccs.add(nsucc)
1528 seensuccs.add(nsucc)
1529 succstocheck.append(nsucc)
1529 succstocheck.append(nsucc)
1530
1530
1531 return newreplacements
1531 return newreplacements
1532
1532
1533 def processreplacement(state):
1533 def processreplacement(state):
1534 """process the list of replacements to return
1534 """process the list of replacements to return
1535
1535
1536 1) the final mapping between original and created nodes
1536 1) the final mapping between original and created nodes
1537 2) the list of temporary node created by histedit
1537 2) the list of temporary node created by histedit
1538 3) the list of new commit created by histedit"""
1538 3) the list of new commit created by histedit"""
1539 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1539 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1540 allsuccs = set()
1540 allsuccs = set()
1541 replaced = set()
1541 replaced = set()
1542 fullmapping = {}
1542 fullmapping = {}
1543 # initialize basic set
1543 # initialize basic set
1544 # fullmapping records all operations recorded in replacement
1544 # fullmapping records all operations recorded in replacement
1545 for rep in replacements:
1545 for rep in replacements:
1546 allsuccs.update(rep[1])
1546 allsuccs.update(rep[1])
1547 replaced.add(rep[0])
1547 replaced.add(rep[0])
1548 fullmapping.setdefault(rep[0], set()).update(rep[1])
1548 fullmapping.setdefault(rep[0], set()).update(rep[1])
1549 new = allsuccs - replaced
1549 new = allsuccs - replaced
1550 tmpnodes = allsuccs & replaced
1550 tmpnodes = allsuccs & replaced
1551 # Reduce content fullmapping into direct relation between original nodes
1551 # Reduce content fullmapping into direct relation between original nodes
1552 # and final node created during history edition
1552 # and final node created during history edition
1553 # Dropped changeset are replaced by an empty list
1553 # Dropped changeset are replaced by an empty list
1554 toproceed = set(fullmapping)
1554 toproceed = set(fullmapping)
1555 final = {}
1555 final = {}
1556 while toproceed:
1556 while toproceed:
1557 for x in list(toproceed):
1557 for x in list(toproceed):
1558 succs = fullmapping[x]
1558 succs = fullmapping[x]
1559 for s in list(succs):
1559 for s in list(succs):
1560 if s in toproceed:
1560 if s in toproceed:
1561 # non final node with unknown closure
1561 # non final node with unknown closure
1562 # We can't process this now
1562 # We can't process this now
1563 break
1563 break
1564 elif s in final:
1564 elif s in final:
1565 # non final node, replace with closure
1565 # non final node, replace with closure
1566 succs.remove(s)
1566 succs.remove(s)
1567 succs.update(final[s])
1567 succs.update(final[s])
1568 else:
1568 else:
1569 final[x] = succs
1569 final[x] = succs
1570 toproceed.remove(x)
1570 toproceed.remove(x)
1571 # remove tmpnodes from final mapping
1571 # remove tmpnodes from final mapping
1572 for n in tmpnodes:
1572 for n in tmpnodes:
1573 del final[n]
1573 del final[n]
1574 # we expect all changes involved in final to exist in the repo
1574 # we expect all changes involved in final to exist in the repo
1575 # turn `final` into list (topologically sorted)
1575 # turn `final` into list (topologically sorted)
1576 nm = state.repo.changelog.nodemap
1576 nm = state.repo.changelog.nodemap
1577 for prec, succs in final.items():
1577 for prec, succs in final.items():
1578 final[prec] = sorted(succs, key=nm.get)
1578 final[prec] = sorted(succs, key=nm.get)
1579
1579
1580 # computed topmost element (necessary for bookmark)
1580 # computed topmost element (necessary for bookmark)
1581 if new:
1581 if new:
1582 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1582 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1583 elif not final:
1583 elif not final:
1584 # Nothing rewritten at all. we won't need `newtopmost`
1584 # Nothing rewritten at all. we won't need `newtopmost`
1585 # It is the same as `oldtopmost` and `processreplacement` know it
1585 # It is the same as `oldtopmost` and `processreplacement` know it
1586 newtopmost = None
1586 newtopmost = None
1587 else:
1587 else:
1588 # every body died. The newtopmost is the parent of the root.
1588 # every body died. The newtopmost is the parent of the root.
1589 r = state.repo.changelog.rev
1589 r = state.repo.changelog.rev
1590 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1590 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1591
1591
1592 return final, tmpnodes, new, newtopmost
1592 return final, tmpnodes, new, newtopmost
1593
1593
1594 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1594 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1595 """Move bookmark from oldtopmost to newly created topmost
1595 """Move bookmark from oldtopmost to newly created topmost
1596
1596
1597 This is arguably a feature and we may only want that for the active
1597 This is arguably a feature and we may only want that for the active
1598 bookmark. But the behavior is kept compatible with the old version for now.
1598 bookmark. But the behavior is kept compatible with the old version for now.
1599 """
1599 """
1600 if not oldtopmost or not newtopmost:
1600 if not oldtopmost or not newtopmost:
1601 return
1601 return
1602 oldbmarks = repo.nodebookmarks(oldtopmost)
1602 oldbmarks = repo.nodebookmarks(oldtopmost)
1603 if oldbmarks:
1603 if oldbmarks:
1604 with repo.lock(), repo.transaction('histedit') as tr:
1604 with repo.lock(), repo.transaction('histedit') as tr:
1605 marks = repo._bookmarks
1605 marks = repo._bookmarks
1606 changes = []
1606 changes = []
1607 for name in oldbmarks:
1607 for name in oldbmarks:
1608 changes.append((name, newtopmost))
1608 changes.append((name, newtopmost))
1609 marks.applychanges(repo, tr, changes)
1609 marks.applychanges(repo, tr, changes)
1610
1610
1611 def cleanupnode(ui, repo, nodes):
1611 def cleanupnode(ui, repo, nodes):
1612 """strip a group of nodes from the repository
1612 """strip a group of nodes from the repository
1613
1613
1614 The set of node to strip may contains unknown nodes."""
1614 The set of node to strip may contains unknown nodes."""
1615 with repo.lock():
1615 with repo.lock():
1616 # do not let filtering get in the way of the cleanse
1616 # do not let filtering get in the way of the cleanse
1617 # we should probably get rid of obsolescence marker created during the
1617 # we should probably get rid of obsolescence marker created during the
1618 # histedit, but we currently do not have such information.
1618 # histedit, but we currently do not have such information.
1619 repo = repo.unfiltered()
1619 repo = repo.unfiltered()
1620 # Find all nodes that need to be stripped
1620 # Find all nodes that need to be stripped
1621 # (we use %lr instead of %ln to silently ignore unknown items)
1621 # (we use %lr instead of %ln to silently ignore unknown items)
1622 nm = repo.changelog.nodemap
1622 nm = repo.changelog.nodemap
1623 nodes = sorted(n for n in nodes if n in nm)
1623 nodes = sorted(n for n in nodes if n in nm)
1624 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1624 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1625 if roots:
1625 if roots:
1626 repair.strip(ui, repo, roots)
1626 repair.strip(ui, repo, roots)
1627
1627
1628 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1628 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1629 if isinstance(nodelist, str):
1629 if isinstance(nodelist, str):
1630 nodelist = [nodelist]
1630 nodelist = [nodelist]
1631 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1631 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1632 state = histeditstate(repo)
1632 state = histeditstate(repo)
1633 state.read()
1633 state.read()
1634 histedit_nodes = {action.node for action
1634 histedit_nodes = {action.node for action
1635 in state.actions if action.node}
1635 in state.actions if action.node}
1636 common_nodes = histedit_nodes & set(nodelist)
1636 common_nodes = histedit_nodes & set(nodelist)
1637 if common_nodes:
1637 if common_nodes:
1638 raise error.Abort(_("histedit in progress, can't strip %s")
1638 raise error.Abort(_("histedit in progress, can't strip %s")
1639 % ', '.join(node.short(x) for x in common_nodes))
1639 % ', '.join(node.short(x) for x in common_nodes))
1640 return orig(ui, repo, nodelist, *args, **kwargs)
1640 return orig(ui, repo, nodelist, *args, **kwargs)
1641
1641
1642 extensions.wrapfunction(repair, 'strip', stripwrapper)
1642 extensions.wrapfunction(repair, 'strip', stripwrapper)
1643
1643
1644 def summaryhook(ui, repo):
1644 def summaryhook(ui, repo):
1645 if not os.path.exists(repo.vfs.join('histedit-state')):
1645 if not os.path.exists(repo.vfs.join('histedit-state')):
1646 return
1646 return
1647 state = histeditstate(repo)
1647 state = histeditstate(repo)
1648 state.read()
1648 state.read()
1649 if state.actions:
1649 if state.actions:
1650 # i18n: column positioning for "hg summary"
1650 # i18n: column positioning for "hg summary"
1651 ui.write(_('hist: %s (histedit --continue)\n') %
1651 ui.write(_('hist: %s (histedit --continue)\n') %
1652 (ui.label(_('%d remaining'), 'histedit.remaining') %
1652 (ui.label(_('%d remaining'), 'histedit.remaining') %
1653 len(state.actions)))
1653 len(state.actions)))
1654
1654
1655 def extsetup(ui):
1655 def extsetup(ui):
1656 cmdutil.summaryhooks.add('histedit', summaryhook)
1656 cmdutil.summaryhooks.add('histedit', summaryhook)
1657 cmdutil.unfinishedstates.append(
1657 cmdutil.unfinishedstates.append(
1658 ['histedit-state', False, True, _('histedit in progress'),
1658 ['histedit-state', False, True, _('histedit in progress'),
1659 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1659 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1660 cmdutil.afterresolvedstates.append(
1660 cmdutil.afterresolvedstates.append(
1661 ['histedit-state', _('hg histedit --continue')])
1661 ['histedit-state', _('hg histedit --continue')])
@@ -1,1142 +1,1142 b''
1 # hg.py - repository classes for mercurial
1 # hg.py - repository classes for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import shutil
14 import shutil
15 import stat
15 import stat
16
16
17 from .i18n import _
17 from .i18n import _
18 from .node import (
18 from .node import (
19 nullid,
19 nullid,
20 )
20 )
21
21
22 from . import (
22 from . import (
23 bookmarks,
23 bookmarks,
24 bundlerepo,
24 bundlerepo,
25 cacheutil,
25 cacheutil,
26 cmdutil,
26 cmdutil,
27 destutil,
27 destutil,
28 discovery,
28 discovery,
29 error,
29 error,
30 exchange,
30 exchange,
31 extensions,
31 extensions,
32 httppeer,
32 httppeer,
33 localrepo,
33 localrepo,
34 lock,
34 lock,
35 logcmdutil,
35 logcmdutil,
36 logexchange,
36 logexchange,
37 merge as mergemod,
37 merge as mergemod,
38 node,
38 node,
39 phases,
39 phases,
40 scmutil,
40 scmutil,
41 sshpeer,
41 sshpeer,
42 statichttprepo,
42 statichttprepo,
43 ui as uimod,
43 ui as uimod,
44 unionrepo,
44 unionrepo,
45 url,
45 url,
46 util,
46 util,
47 verify as verifymod,
47 verify as verifymod,
48 vfs as vfsmod,
48 vfs as vfsmod,
49 )
49 )
50
50
51 from .utils import (
51 from .utils import (
52 stringutil,
52 stringutil,
53 )
53 )
54
54
55 release = lock.release
55 release = lock.release
56
56
57 # shared features
57 # shared features
58 sharedbookmarks = 'bookmarks'
58 sharedbookmarks = 'bookmarks'
59
59
60 def _local(path):
60 def _local(path):
61 path = util.expandpath(util.urllocalpath(path))
61 path = util.expandpath(util.urllocalpath(path))
62 return (os.path.isfile(path) and bundlerepo or localrepo)
62 return (os.path.isfile(path) and bundlerepo or localrepo)
63
63
64 def addbranchrevs(lrepo, other, branches, revs):
64 def addbranchrevs(lrepo, other, branches, revs):
65 peer = other.peer() # a courtesy to callers using a localrepo for other
65 peer = other.peer() # a courtesy to callers using a localrepo for other
66 hashbranch, branches = branches
66 hashbranch, branches = branches
67 if not hashbranch and not branches:
67 if not hashbranch and not branches:
68 x = revs or None
68 x = revs or None
69 if util.safehasattr(revs, 'first'):
69 if util.safehasattr(revs, 'first'):
70 y = revs.first()
70 y = revs.first()
71 elif revs:
71 elif revs:
72 y = revs[0]
72 y = revs[0]
73 else:
73 else:
74 y = None
74 y = None
75 return x, y
75 return x, y
76 if revs:
76 if revs:
77 revs = list(revs)
77 revs = list(revs)
78 else:
78 else:
79 revs = []
79 revs = []
80
80
81 if not peer.capable('branchmap'):
81 if not peer.capable('branchmap'):
82 if branches:
82 if branches:
83 raise error.Abort(_("remote branch lookup not supported"))
83 raise error.Abort(_("remote branch lookup not supported"))
84 revs.append(hashbranch)
84 revs.append(hashbranch)
85 return revs, revs[0]
85 return revs, revs[0]
86 branchmap = peer.branchmap()
86 branchmap = peer.branchmap()
87
87
88 def primary(branch):
88 def primary(branch):
89 if branch == '.':
89 if branch == '.':
90 if not lrepo:
90 if not lrepo:
91 raise error.Abort(_("dirstate branch not accessible"))
91 raise error.Abort(_("dirstate branch not accessible"))
92 branch = lrepo.dirstate.branch()
92 branch = lrepo.dirstate.branch()
93 if branch in branchmap:
93 if branch in branchmap:
94 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
94 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
95 return True
95 return True
96 else:
96 else:
97 return False
97 return False
98
98
99 for branch in branches:
99 for branch in branches:
100 if not primary(branch):
100 if not primary(branch):
101 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
101 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
102 if hashbranch:
102 if hashbranch:
103 if not primary(hashbranch):
103 if not primary(hashbranch):
104 revs.append(hashbranch)
104 revs.append(hashbranch)
105 return revs, revs[0]
105 return revs, revs[0]
106
106
107 def parseurl(path, branches=None):
107 def parseurl(path, branches=None):
108 '''parse url#branch, returning (url, (branch, branches))'''
108 '''parse url#branch, returning (url, (branch, branches))'''
109
109
110 u = util.url(path)
110 u = util.url(path)
111 branch = None
111 branch = None
112 if u.fragment:
112 if u.fragment:
113 branch = u.fragment
113 branch = u.fragment
114 u.fragment = None
114 u.fragment = None
115 return bytes(u), (branch, branches or [])
115 return bytes(u), (branch, branches or [])
116
116
117 schemes = {
117 schemes = {
118 'bundle': bundlerepo,
118 'bundle': bundlerepo,
119 'union': unionrepo,
119 'union': unionrepo,
120 'file': _local,
120 'file': _local,
121 'http': httppeer,
121 'http': httppeer,
122 'https': httppeer,
122 'https': httppeer,
123 'ssh': sshpeer,
123 'ssh': sshpeer,
124 'static-http': statichttprepo,
124 'static-http': statichttprepo,
125 }
125 }
126
126
127 def _peerlookup(path):
127 def _peerlookup(path):
128 u = util.url(path)
128 u = util.url(path)
129 scheme = u.scheme or 'file'
129 scheme = u.scheme or 'file'
130 thing = schemes.get(scheme) or schemes['file']
130 thing = schemes.get(scheme) or schemes['file']
131 try:
131 try:
132 return thing(path)
132 return thing(path)
133 except TypeError:
133 except TypeError:
134 # we can't test callable(thing) because 'thing' can be an unloaded
134 # we can't test callable(thing) because 'thing' can be an unloaded
135 # module that implements __call__
135 # module that implements __call__
136 if not util.safehasattr(thing, 'instance'):
136 if not util.safehasattr(thing, 'instance'):
137 raise
137 raise
138 return thing
138 return thing
139
139
140 def islocal(repo):
140 def islocal(repo):
141 '''return true if repo (or path pointing to repo) is local'''
141 '''return true if repo (or path pointing to repo) is local'''
142 if isinstance(repo, bytes):
142 if isinstance(repo, bytes):
143 try:
143 try:
144 return _peerlookup(repo).islocal(repo)
144 return _peerlookup(repo).islocal(repo)
145 except AttributeError:
145 except AttributeError:
146 return False
146 return False
147 return repo.local()
147 return repo.local()
148
148
149 def openpath(ui, path):
149 def openpath(ui, path):
150 '''open path with open if local, url.open if remote'''
150 '''open path with open if local, url.open if remote'''
151 pathurl = util.url(path, parsequery=False, parsefragment=False)
151 pathurl = util.url(path, parsequery=False, parsefragment=False)
152 if pathurl.islocal():
152 if pathurl.islocal():
153 return util.posixfile(pathurl.localpath(), 'rb')
153 return util.posixfile(pathurl.localpath(), 'rb')
154 else:
154 else:
155 return url.open(ui, path)
155 return url.open(ui, path)
156
156
157 # a list of (ui, repo) functions called for wire peer initialization
157 # a list of (ui, repo) functions called for wire peer initialization
158 wirepeersetupfuncs = []
158 wirepeersetupfuncs = []
159
159
160 def _peerorrepo(ui, path, create=False, presetupfuncs=None):
160 def _peerorrepo(ui, path, create=False, presetupfuncs=None):
161 """return a repository object for the specified path"""
161 """return a repository object for the specified path"""
162 obj = _peerlookup(path).instance(ui, path, create)
162 obj = _peerlookup(path).instance(ui, path, create)
163 ui = getattr(obj, "ui", ui)
163 ui = getattr(obj, "ui", ui)
164 for f in presetupfuncs or []:
164 for f in presetupfuncs or []:
165 f(ui, obj)
165 f(ui, obj)
166 for name, module in extensions.extensions(ui):
166 for name, module in extensions.extensions(ui):
167 hook = getattr(module, 'reposetup', None)
167 hook = getattr(module, 'reposetup', None)
168 if hook:
168 if hook:
169 hook(ui, obj)
169 hook(ui, obj)
170 if not obj.local():
170 if not obj.local():
171 for f in wirepeersetupfuncs:
171 for f in wirepeersetupfuncs:
172 f(ui, obj)
172 f(ui, obj)
173 return obj
173 return obj
174
174
175 def repository(ui, path='', create=False, presetupfuncs=None):
175 def repository(ui, path='', create=False, presetupfuncs=None):
176 """return a repository object for the specified path"""
176 """return a repository object for the specified path"""
177 peer = _peerorrepo(ui, path, create, presetupfuncs=presetupfuncs)
177 peer = _peerorrepo(ui, path, create, presetupfuncs=presetupfuncs)
178 repo = peer.local()
178 repo = peer.local()
179 if not repo:
179 if not repo:
180 raise error.Abort(_("repository '%s' is not local") %
180 raise error.Abort(_("repository '%s' is not local") %
181 (path or peer.url()))
181 (path or peer.url()))
182 return repo.filtered('visible')
182 return repo.filtered('visible')
183
183
184 def peer(uiorrepo, opts, path, create=False):
184 def peer(uiorrepo, opts, path, create=False):
185 '''return a repository peer for the specified path'''
185 '''return a repository peer for the specified path'''
186 rui = remoteui(uiorrepo, opts)
186 rui = remoteui(uiorrepo, opts)
187 return _peerorrepo(rui, path, create).peer()
187 return _peerorrepo(rui, path, create).peer()
188
188
189 def defaultdest(source):
189 def defaultdest(source):
190 '''return default destination of clone if none is given
190 '''return default destination of clone if none is given
191
191
192 >>> defaultdest(b'foo')
192 >>> defaultdest(b'foo')
193 'foo'
193 'foo'
194 >>> defaultdest(b'/foo/bar')
194 >>> defaultdest(b'/foo/bar')
195 'bar'
195 'bar'
196 >>> defaultdest(b'/')
196 >>> defaultdest(b'/')
197 ''
197 ''
198 >>> defaultdest(b'')
198 >>> defaultdest(b'')
199 ''
199 ''
200 >>> defaultdest(b'http://example.org/')
200 >>> defaultdest(b'http://example.org/')
201 ''
201 ''
202 >>> defaultdest(b'http://example.org/foo/')
202 >>> defaultdest(b'http://example.org/foo/')
203 'foo'
203 'foo'
204 '''
204 '''
205 path = util.url(source).path
205 path = util.url(source).path
206 if not path:
206 if not path:
207 return ''
207 return ''
208 return os.path.basename(os.path.normpath(path))
208 return os.path.basename(os.path.normpath(path))
209
209
210 def sharedreposource(repo):
210 def sharedreposource(repo):
211 """Returns repository object for source repository of a shared repo.
211 """Returns repository object for source repository of a shared repo.
212
212
213 If repo is not a shared repository, returns None.
213 If repo is not a shared repository, returns None.
214 """
214 """
215 if repo.sharedpath == repo.path:
215 if repo.sharedpath == repo.path:
216 return None
216 return None
217
217
218 if util.safehasattr(repo, 'srcrepo') and repo.srcrepo:
218 if util.safehasattr(repo, 'srcrepo') and repo.srcrepo:
219 return repo.srcrepo
219 return repo.srcrepo
220
220
221 # the sharedpath always ends in the .hg; we want the path to the repo
221 # the sharedpath always ends in the .hg; we want the path to the repo
222 source = repo.vfs.split(repo.sharedpath)[0]
222 source = repo.vfs.split(repo.sharedpath)[0]
223 srcurl, branches = parseurl(source)
223 srcurl, branches = parseurl(source)
224 srcrepo = repository(repo.ui, srcurl)
224 srcrepo = repository(repo.ui, srcurl)
225 repo.srcrepo = srcrepo
225 repo.srcrepo = srcrepo
226 return srcrepo
226 return srcrepo
227
227
228 def share(ui, source, dest=None, update=True, bookmarks=True, defaultpath=None,
228 def share(ui, source, dest=None, update=True, bookmarks=True, defaultpath=None,
229 relative=False):
229 relative=False):
230 '''create a shared repository'''
230 '''create a shared repository'''
231
231
232 if not islocal(source):
232 if not islocal(source):
233 raise error.Abort(_('can only share local repositories'))
233 raise error.Abort(_('can only share local repositories'))
234
234
235 if not dest:
235 if not dest:
236 dest = defaultdest(source)
236 dest = defaultdest(source)
237 else:
237 else:
238 dest = ui.expandpath(dest)
238 dest = ui.expandpath(dest)
239
239
240 if isinstance(source, bytes):
240 if isinstance(source, bytes):
241 origsource = ui.expandpath(source)
241 origsource = ui.expandpath(source)
242 source, branches = parseurl(origsource)
242 source, branches = parseurl(origsource)
243 srcrepo = repository(ui, source)
243 srcrepo = repository(ui, source)
244 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
244 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
245 else:
245 else:
246 srcrepo = source.local()
246 srcrepo = source.local()
247 origsource = source = srcrepo.url()
247 origsource = source = srcrepo.url()
248 checkout = None
248 checkout = None
249
249
250 sharedpath = srcrepo.sharedpath # if our source is already sharing
250 sharedpath = srcrepo.sharedpath # if our source is already sharing
251
251
252 destwvfs = vfsmod.vfs(dest, realpath=True)
252 destwvfs = vfsmod.vfs(dest, realpath=True)
253 destvfs = vfsmod.vfs(os.path.join(destwvfs.base, '.hg'), realpath=True)
253 destvfs = vfsmod.vfs(os.path.join(destwvfs.base, '.hg'), realpath=True)
254
254
255 if destvfs.lexists():
255 if destvfs.lexists():
256 raise error.Abort(_('destination already exists'))
256 raise error.Abort(_('destination already exists'))
257
257
258 if not destwvfs.isdir():
258 if not destwvfs.isdir():
259 destwvfs.mkdir()
259 destwvfs.mkdir()
260 destvfs.makedir()
260 destvfs.makedir()
261
261
262 requirements = ''
262 requirements = ''
263 try:
263 try:
264 requirements = srcrepo.vfs.read('requires')
264 requirements = srcrepo.vfs.read('requires')
265 except IOError as inst:
265 except IOError as inst:
266 if inst.errno != errno.ENOENT:
266 if inst.errno != errno.ENOENT:
267 raise
267 raise
268
268
269 if relative:
269 if relative:
270 try:
270 try:
271 sharedpath = os.path.relpath(sharedpath, destvfs.base)
271 sharedpath = os.path.relpath(sharedpath, destvfs.base)
272 requirements += 'relshared\n'
272 requirements += 'relshared\n'
273 except (IOError, ValueError) as e:
273 except (IOError, ValueError) as e:
274 # ValueError is raised on Windows if the drive letters differ on
274 # ValueError is raised on Windows if the drive letters differ on
275 # each path
275 # each path
276 raise error.Abort(_('cannot calculate relative path'),
276 raise error.Abort(_('cannot calculate relative path'),
277 hint=stringutil.forcebytestr(e))
277 hint=stringutil.forcebytestr(e))
278 else:
278 else:
279 requirements += 'shared\n'
279 requirements += 'shared\n'
280
280
281 destvfs.write('requires', requirements)
281 destvfs.write('requires', requirements)
282 destvfs.write('sharedpath', sharedpath)
282 destvfs.write('sharedpath', sharedpath)
283
283
284 r = repository(ui, destwvfs.base)
284 r = repository(ui, destwvfs.base)
285 postshare(srcrepo, r, bookmarks=bookmarks, defaultpath=defaultpath)
285 postshare(srcrepo, r, bookmarks=bookmarks, defaultpath=defaultpath)
286 _postshareupdate(r, update, checkout=checkout)
286 _postshareupdate(r, update, checkout=checkout)
287 return r
287 return r
288
288
289 def unshare(ui, repo):
289 def unshare(ui, repo):
290 """convert a shared repository to a normal one
290 """convert a shared repository to a normal one
291
291
292 Copy the store data to the repo and remove the sharedpath data.
292 Copy the store data to the repo and remove the sharedpath data.
293 """
293 """
294
294
295 destlock = lock = None
295 destlock = lock = None
296 lock = repo.lock()
296 lock = repo.lock()
297 try:
297 try:
298 # we use locks here because if we race with commit, we
298 # we use locks here because if we race with commit, we
299 # can end up with extra data in the cloned revlogs that's
299 # can end up with extra data in the cloned revlogs that's
300 # not pointed to by changesets, thus causing verify to
300 # not pointed to by changesets, thus causing verify to
301 # fail
301 # fail
302
302
303 destlock = copystore(ui, repo, repo.path)
303 destlock = copystore(ui, repo, repo.path)
304
304
305 sharefile = repo.vfs.join('sharedpath')
305 sharefile = repo.vfs.join('sharedpath')
306 util.rename(sharefile, sharefile + '.old')
306 util.rename(sharefile, sharefile + '.old')
307
307
308 repo.requirements.discard('shared')
308 repo.requirements.discard('shared')
309 repo.requirements.discard('relshared')
309 repo.requirements.discard('relshared')
310 repo._writerequirements()
310 repo._writerequirements()
311 finally:
311 finally:
312 destlock and destlock.release()
312 destlock and destlock.release()
313 lock and lock.release()
313 lock and lock.release()
314
314
315 # update store, spath, svfs and sjoin of repo
315 # update store, spath, svfs and sjoin of repo
316 repo.unfiltered().__init__(repo.baseui, repo.root)
316 repo.unfiltered().__init__(repo.baseui, repo.root)
317
317
318 # TODO: figure out how to access subrepos that exist, but were previously
318 # TODO: figure out how to access subrepos that exist, but were previously
319 # removed from .hgsub
319 # removed from .hgsub
320 c = repo['.']
320 c = repo['.']
321 subs = c.substate
321 subs = c.substate
322 for s in sorted(subs):
322 for s in sorted(subs):
323 c.sub(s).unshare()
323 c.sub(s).unshare()
324
324
325 def postshare(sourcerepo, destrepo, bookmarks=True, defaultpath=None):
325 def postshare(sourcerepo, destrepo, bookmarks=True, defaultpath=None):
326 """Called after a new shared repo is created.
326 """Called after a new shared repo is created.
327
327
328 The new repo only has a requirements file and pointer to the source.
328 The new repo only has a requirements file and pointer to the source.
329 This function configures additional shared data.
329 This function configures additional shared data.
330
330
331 Extensions can wrap this function and write additional entries to
331 Extensions can wrap this function and write additional entries to
332 destrepo/.hg/shared to indicate additional pieces of data to be shared.
332 destrepo/.hg/shared to indicate additional pieces of data to be shared.
333 """
333 """
334 default = defaultpath or sourcerepo.ui.config('paths', 'default')
334 default = defaultpath or sourcerepo.ui.config('paths', 'default')
335 if default:
335 if default:
336 template = ('[paths]\n'
336 template = ('[paths]\n'
337 'default = %s\n')
337 'default = %s\n')
338 destrepo.vfs.write('hgrc', util.tonativeeol(template % default))
338 destrepo.vfs.write('hgrc', util.tonativeeol(template % default))
339
339
340 with destrepo.wlock():
340 with destrepo.wlock():
341 if bookmarks:
341 if bookmarks:
342 destrepo.vfs.write('shared', sharedbookmarks + '\n')
342 destrepo.vfs.write('shared', sharedbookmarks + '\n')
343
343
344 def _postshareupdate(repo, update, checkout=None):
344 def _postshareupdate(repo, update, checkout=None):
345 """Maybe perform a working directory update after a shared repo is created.
345 """Maybe perform a working directory update after a shared repo is created.
346
346
347 ``update`` can be a boolean or a revision to update to.
347 ``update`` can be a boolean or a revision to update to.
348 """
348 """
349 if not update:
349 if not update:
350 return
350 return
351
351
352 repo.ui.status(_("updating working directory\n"))
352 repo.ui.status(_("updating working directory\n"))
353 if update is not True:
353 if update is not True:
354 checkout = update
354 checkout = update
355 for test in (checkout, 'default', 'tip'):
355 for test in (checkout, 'default', 'tip'):
356 if test is None:
356 if test is None:
357 continue
357 continue
358 try:
358 try:
359 uprev = repo.lookup(test)
359 uprev = repo.lookup(test)
360 break
360 break
361 except error.RepoLookupError:
361 except error.RepoLookupError:
362 continue
362 continue
363 _update(repo, uprev)
363 _update(repo, uprev)
364
364
365 def copystore(ui, srcrepo, destpath):
365 def copystore(ui, srcrepo, destpath):
366 '''copy files from store of srcrepo in destpath
366 '''copy files from store of srcrepo in destpath
367
367
368 returns destlock
368 returns destlock
369 '''
369 '''
370 destlock = None
370 destlock = None
371 try:
371 try:
372 hardlink = None
372 hardlink = None
373 num = 0
373 num = 0
374 closetopic = [None]
374 closetopic = [None]
375 def prog(topic, pos):
375 def prog(topic, pos):
376 if pos is None:
376 if pos is None:
377 closetopic[0] = topic
377 closetopic[0] = topic
378 else:
378 else:
379 ui.progress(topic, pos + num)
379 ui.progress(topic, pos + num)
380 srcpublishing = srcrepo.publishing()
380 srcpublishing = srcrepo.publishing()
381 srcvfs = vfsmod.vfs(srcrepo.sharedpath)
381 srcvfs = vfsmod.vfs(srcrepo.sharedpath)
382 dstvfs = vfsmod.vfs(destpath)
382 dstvfs = vfsmod.vfs(destpath)
383 for f in srcrepo.store.copylist():
383 for f in srcrepo.store.copylist():
384 if srcpublishing and f.endswith('phaseroots'):
384 if srcpublishing and f.endswith('phaseroots'):
385 continue
385 continue
386 dstbase = os.path.dirname(f)
386 dstbase = os.path.dirname(f)
387 if dstbase and not dstvfs.exists(dstbase):
387 if dstbase and not dstvfs.exists(dstbase):
388 dstvfs.mkdir(dstbase)
388 dstvfs.mkdir(dstbase)
389 if srcvfs.exists(f):
389 if srcvfs.exists(f):
390 if f.endswith('data'):
390 if f.endswith('data'):
391 # 'dstbase' may be empty (e.g. revlog format 0)
391 # 'dstbase' may be empty (e.g. revlog format 0)
392 lockfile = os.path.join(dstbase, "lock")
392 lockfile = os.path.join(dstbase, "lock")
393 # lock to avoid premature writing to the target
393 # lock to avoid premature writing to the target
394 destlock = lock.lock(dstvfs, lockfile)
394 destlock = lock.lock(dstvfs, lockfile)
395 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
395 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
396 hardlink, progress=prog)
396 hardlink, progress=prog)
397 num += n
397 num += n
398 if hardlink:
398 if hardlink:
399 ui.debug("linked %d files\n" % num)
399 ui.debug("linked %d files\n" % num)
400 if closetopic[0]:
400 if closetopic[0]:
401 ui.progress(closetopic[0], None)
401 ui.progress(closetopic[0], None)
402 else:
402 else:
403 ui.debug("copied %d files\n" % num)
403 ui.debug("copied %d files\n" % num)
404 if closetopic[0]:
404 if closetopic[0]:
405 ui.progress(closetopic[0], None)
405 ui.progress(closetopic[0], None)
406 return destlock
406 return destlock
407 except: # re-raises
407 except: # re-raises
408 release(destlock)
408 release(destlock)
409 raise
409 raise
410
410
411 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
411 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
412 rev=None, update=True, stream=False):
412 rev=None, update=True, stream=False):
413 """Perform a clone using a shared repo.
413 """Perform a clone using a shared repo.
414
414
415 The store for the repository will be located at <sharepath>/.hg. The
415 The store for the repository will be located at <sharepath>/.hg. The
416 specified revisions will be cloned or pulled from "source". A shared repo
416 specified revisions will be cloned or pulled from "source". A shared repo
417 will be created at "dest" and a working copy will be created if "update" is
417 will be created at "dest" and a working copy will be created if "update" is
418 True.
418 True.
419 """
419 """
420 revs = None
420 revs = None
421 if rev:
421 if rev:
422 if not srcpeer.capable('lookup'):
422 if not srcpeer.capable('lookup'):
423 raise error.Abort(_("src repository does not support "
423 raise error.Abort(_("src repository does not support "
424 "revision lookup and so doesn't "
424 "revision lookup and so doesn't "
425 "support clone by revision"))
425 "support clone by revision"))
426 revs = [srcpeer.lookup(r) for r in rev]
426 revs = [srcpeer.lookup(r) for r in rev]
427
427
428 # Obtain a lock before checking for or cloning the pooled repo otherwise
428 # Obtain a lock before checking for or cloning the pooled repo otherwise
429 # 2 clients may race creating or populating it.
429 # 2 clients may race creating or populating it.
430 pooldir = os.path.dirname(sharepath)
430 pooldir = os.path.dirname(sharepath)
431 # lock class requires the directory to exist.
431 # lock class requires the directory to exist.
432 try:
432 try:
433 util.makedir(pooldir, False)
433 util.makedir(pooldir, False)
434 except OSError as e:
434 except OSError as e:
435 if e.errno != errno.EEXIST:
435 if e.errno != errno.EEXIST:
436 raise
436 raise
437
437
438 poolvfs = vfsmod.vfs(pooldir)
438 poolvfs = vfsmod.vfs(pooldir)
439 basename = os.path.basename(sharepath)
439 basename = os.path.basename(sharepath)
440
440
441 with lock.lock(poolvfs, '%s.lock' % basename):
441 with lock.lock(poolvfs, '%s.lock' % basename):
442 if os.path.exists(sharepath):
442 if os.path.exists(sharepath):
443 ui.status(_('(sharing from existing pooled repository %s)\n') %
443 ui.status(_('(sharing from existing pooled repository %s)\n') %
444 basename)
444 basename)
445 else:
445 else:
446 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
446 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
447 # Always use pull mode because hardlinks in share mode don't work
447 # Always use pull mode because hardlinks in share mode don't work
448 # well. Never update because working copies aren't necessary in
448 # well. Never update because working copies aren't necessary in
449 # share mode.
449 # share mode.
450 clone(ui, peeropts, source, dest=sharepath, pull=True,
450 clone(ui, peeropts, source, dest=sharepath, pull=True,
451 rev=rev, update=False, stream=stream)
451 rev=rev, update=False, stream=stream)
452
452
453 # Resolve the value to put in [paths] section for the source.
453 # Resolve the value to put in [paths] section for the source.
454 if islocal(source):
454 if islocal(source):
455 defaultpath = os.path.abspath(util.urllocalpath(source))
455 defaultpath = os.path.abspath(util.urllocalpath(source))
456 else:
456 else:
457 defaultpath = source
457 defaultpath = source
458
458
459 sharerepo = repository(ui, path=sharepath)
459 sharerepo = repository(ui, path=sharepath)
460 share(ui, sharerepo, dest=dest, update=False, bookmarks=False,
460 share(ui, sharerepo, dest=dest, update=False, bookmarks=False,
461 defaultpath=defaultpath)
461 defaultpath=defaultpath)
462
462
463 # We need to perform a pull against the dest repo to fetch bookmarks
463 # We need to perform a pull against the dest repo to fetch bookmarks
464 # and other non-store data that isn't shared by default. In the case of
464 # and other non-store data that isn't shared by default. In the case of
465 # non-existing shared repo, this means we pull from the remote twice. This
465 # non-existing shared repo, this means we pull from the remote twice. This
466 # is a bit weird. But at the time it was implemented, there wasn't an easy
466 # is a bit weird. But at the time it was implemented, there wasn't an easy
467 # way to pull just non-changegroup data.
467 # way to pull just non-changegroup data.
468 destrepo = repository(ui, path=dest)
468 destrepo = repository(ui, path=dest)
469 exchange.pull(destrepo, srcpeer, heads=revs)
469 exchange.pull(destrepo, srcpeer, heads=revs)
470
470
471 _postshareupdate(destrepo, update)
471 _postshareupdate(destrepo, update)
472
472
473 return srcpeer, peer(ui, peeropts, dest)
473 return srcpeer, peer(ui, peeropts, dest)
474
474
475 # Recomputing branch cache might be slow on big repos,
475 # Recomputing branch cache might be slow on big repos,
476 # so just copy it
476 # so just copy it
477 def _copycache(srcrepo, dstcachedir, fname):
477 def _copycache(srcrepo, dstcachedir, fname):
478 """copy a cache from srcrepo to destcachedir (if it exists)"""
478 """copy a cache from srcrepo to destcachedir (if it exists)"""
479 srcbranchcache = srcrepo.vfs.join('cache/%s' % fname)
479 srcbranchcache = srcrepo.vfs.join('cache/%s' % fname)
480 dstbranchcache = os.path.join(dstcachedir, fname)
480 dstbranchcache = os.path.join(dstcachedir, fname)
481 if os.path.exists(srcbranchcache):
481 if os.path.exists(srcbranchcache):
482 if not os.path.exists(dstcachedir):
482 if not os.path.exists(dstcachedir):
483 os.mkdir(dstcachedir)
483 os.mkdir(dstcachedir)
484 util.copyfile(srcbranchcache, dstbranchcache)
484 util.copyfile(srcbranchcache, dstbranchcache)
485
485
486 def clone(ui, peeropts, source, dest=None, pull=False, rev=None,
486 def clone(ui, peeropts, source, dest=None, pull=False, rev=None,
487 update=True, stream=False, branch=None, shareopts=None):
487 update=True, stream=False, branch=None, shareopts=None):
488 """Make a copy of an existing repository.
488 """Make a copy of an existing repository.
489
489
490 Create a copy of an existing repository in a new directory. The
490 Create a copy of an existing repository in a new directory. The
491 source and destination are URLs, as passed to the repository
491 source and destination are URLs, as passed to the repository
492 function. Returns a pair of repository peers, the source and
492 function. Returns a pair of repository peers, the source and
493 newly created destination.
493 newly created destination.
494
494
495 The location of the source is added to the new repository's
495 The location of the source is added to the new repository's
496 .hg/hgrc file, as the default to be used for future pulls and
496 .hg/hgrc file, as the default to be used for future pulls and
497 pushes.
497 pushes.
498
498
499 If an exception is raised, the partly cloned/updated destination
499 If an exception is raised, the partly cloned/updated destination
500 repository will be deleted.
500 repository will be deleted.
501
501
502 Arguments:
502 Arguments:
503
503
504 source: repository object or URL
504 source: repository object or URL
505
505
506 dest: URL of destination repository to create (defaults to base
506 dest: URL of destination repository to create (defaults to base
507 name of source repository)
507 name of source repository)
508
508
509 pull: always pull from source repository, even in local case or if the
509 pull: always pull from source repository, even in local case or if the
510 server prefers streaming
510 server prefers streaming
511
511
512 stream: stream raw data uncompressed from repository (fast over
512 stream: stream raw data uncompressed from repository (fast over
513 LAN, slow over WAN)
513 LAN, slow over WAN)
514
514
515 rev: revision to clone up to (implies pull=True)
515 rev: revision to clone up to (implies pull=True)
516
516
517 update: update working directory after clone completes, if
517 update: update working directory after clone completes, if
518 destination is local repository (True means update to default rev,
518 destination is local repository (True means update to default rev,
519 anything else is treated as a revision)
519 anything else is treated as a revision)
520
520
521 branch: branches to clone
521 branch: branches to clone
522
522
523 shareopts: dict of options to control auto sharing behavior. The "pool" key
523 shareopts: dict of options to control auto sharing behavior. The "pool" key
524 activates auto sharing mode and defines the directory for stores. The
524 activates auto sharing mode and defines the directory for stores. The
525 "mode" key determines how to construct the directory name of the shared
525 "mode" key determines how to construct the directory name of the shared
526 repository. "identity" means the name is derived from the node of the first
526 repository. "identity" means the name is derived from the node of the first
527 changeset in the repository. "remote" means the name is derived from the
527 changeset in the repository. "remote" means the name is derived from the
528 remote's path/URL. Defaults to "identity."
528 remote's path/URL. Defaults to "identity."
529 """
529 """
530
530
531 if isinstance(source, bytes):
531 if isinstance(source, bytes):
532 origsource = ui.expandpath(source)
532 origsource = ui.expandpath(source)
533 source, branch = parseurl(origsource, branch)
533 source, branches = parseurl(origsource, branch)
534 srcpeer = peer(ui, peeropts, source)
534 srcpeer = peer(ui, peeropts, source)
535 else:
535 else:
536 srcpeer = source.peer() # in case we were called with a localrepo
536 srcpeer = source.peer() # in case we were called with a localrepo
537 branch = (None, branch or [])
537 branches = (None, branch or [])
538 origsource = source = srcpeer.url()
538 origsource = source = srcpeer.url()
539 rev, checkout = addbranchrevs(srcpeer, srcpeer, branch, rev)
539 rev, checkout = addbranchrevs(srcpeer, srcpeer, branches, rev)
540
540
541 if dest is None:
541 if dest is None:
542 dest = defaultdest(source)
542 dest = defaultdest(source)
543 if dest:
543 if dest:
544 ui.status(_("destination directory: %s\n") % dest)
544 ui.status(_("destination directory: %s\n") % dest)
545 else:
545 else:
546 dest = ui.expandpath(dest)
546 dest = ui.expandpath(dest)
547
547
548 dest = util.urllocalpath(dest)
548 dest = util.urllocalpath(dest)
549 source = util.urllocalpath(source)
549 source = util.urllocalpath(source)
550
550
551 if not dest:
551 if not dest:
552 raise error.Abort(_("empty destination path is not valid"))
552 raise error.Abort(_("empty destination path is not valid"))
553
553
554 destvfs = vfsmod.vfs(dest, expandpath=True)
554 destvfs = vfsmod.vfs(dest, expandpath=True)
555 if destvfs.lexists():
555 if destvfs.lexists():
556 if not destvfs.isdir():
556 if not destvfs.isdir():
557 raise error.Abort(_("destination '%s' already exists") % dest)
557 raise error.Abort(_("destination '%s' already exists") % dest)
558 elif destvfs.listdir():
558 elif destvfs.listdir():
559 raise error.Abort(_("destination '%s' is not empty") % dest)
559 raise error.Abort(_("destination '%s' is not empty") % dest)
560
560
561 shareopts = shareopts or {}
561 shareopts = shareopts or {}
562 sharepool = shareopts.get('pool')
562 sharepool = shareopts.get('pool')
563 sharenamemode = shareopts.get('mode')
563 sharenamemode = shareopts.get('mode')
564 if sharepool and islocal(dest):
564 if sharepool and islocal(dest):
565 sharepath = None
565 sharepath = None
566 if sharenamemode == 'identity':
566 if sharenamemode == 'identity':
567 # Resolve the name from the initial changeset in the remote
567 # Resolve the name from the initial changeset in the remote
568 # repository. This returns nullid when the remote is empty. It
568 # repository. This returns nullid when the remote is empty. It
569 # raises RepoLookupError if revision 0 is filtered or otherwise
569 # raises RepoLookupError if revision 0 is filtered or otherwise
570 # not available. If we fail to resolve, sharing is not enabled.
570 # not available. If we fail to resolve, sharing is not enabled.
571 try:
571 try:
572 rootnode = srcpeer.lookup('0')
572 rootnode = srcpeer.lookup('0')
573 if rootnode != node.nullid:
573 if rootnode != node.nullid:
574 sharepath = os.path.join(sharepool, node.hex(rootnode))
574 sharepath = os.path.join(sharepool, node.hex(rootnode))
575 else:
575 else:
576 ui.status(_('(not using pooled storage: '
576 ui.status(_('(not using pooled storage: '
577 'remote appears to be empty)\n'))
577 'remote appears to be empty)\n'))
578 except error.RepoLookupError:
578 except error.RepoLookupError:
579 ui.status(_('(not using pooled storage: '
579 ui.status(_('(not using pooled storage: '
580 'unable to resolve identity of remote)\n'))
580 'unable to resolve identity of remote)\n'))
581 elif sharenamemode == 'remote':
581 elif sharenamemode == 'remote':
582 sharepath = os.path.join(
582 sharepath = os.path.join(
583 sharepool, node.hex(hashlib.sha1(source).digest()))
583 sharepool, node.hex(hashlib.sha1(source).digest()))
584 else:
584 else:
585 raise error.Abort(_('unknown share naming mode: %s') %
585 raise error.Abort(_('unknown share naming mode: %s') %
586 sharenamemode)
586 sharenamemode)
587
587
588 if sharepath:
588 if sharepath:
589 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
589 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
590 dest, pull=pull, rev=rev, update=update,
590 dest, pull=pull, rev=rev, update=update,
591 stream=stream)
591 stream=stream)
592
592
593 srclock = destlock = cleandir = None
593 srclock = destlock = cleandir = None
594 srcrepo = srcpeer.local()
594 srcrepo = srcpeer.local()
595 try:
595 try:
596 abspath = origsource
596 abspath = origsource
597 if islocal(origsource):
597 if islocal(origsource):
598 abspath = os.path.abspath(util.urllocalpath(origsource))
598 abspath = os.path.abspath(util.urllocalpath(origsource))
599
599
600 if islocal(dest):
600 if islocal(dest):
601 cleandir = dest
601 cleandir = dest
602
602
603 copy = False
603 copy = False
604 if (srcrepo and srcrepo.cancopy() and islocal(dest)
604 if (srcrepo and srcrepo.cancopy() and islocal(dest)
605 and not phases.hassecret(srcrepo)):
605 and not phases.hassecret(srcrepo)):
606 copy = not pull and not rev
606 copy = not pull and not rev
607
607
608 if copy:
608 if copy:
609 try:
609 try:
610 # we use a lock here because if we race with commit, we
610 # we use a lock here because if we race with commit, we
611 # can end up with extra data in the cloned revlogs that's
611 # can end up with extra data in the cloned revlogs that's
612 # not pointed to by changesets, thus causing verify to
612 # not pointed to by changesets, thus causing verify to
613 # fail
613 # fail
614 srclock = srcrepo.lock(wait=False)
614 srclock = srcrepo.lock(wait=False)
615 except error.LockError:
615 except error.LockError:
616 copy = False
616 copy = False
617
617
618 if copy:
618 if copy:
619 srcrepo.hook('preoutgoing', throw=True, source='clone')
619 srcrepo.hook('preoutgoing', throw=True, source='clone')
620 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
620 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
621 if not os.path.exists(dest):
621 if not os.path.exists(dest):
622 os.mkdir(dest)
622 os.mkdir(dest)
623 else:
623 else:
624 # only clean up directories we create ourselves
624 # only clean up directories we create ourselves
625 cleandir = hgdir
625 cleandir = hgdir
626 try:
626 try:
627 destpath = hgdir
627 destpath = hgdir
628 util.makedir(destpath, notindexed=True)
628 util.makedir(destpath, notindexed=True)
629 except OSError as inst:
629 except OSError as inst:
630 if inst.errno == errno.EEXIST:
630 if inst.errno == errno.EEXIST:
631 cleandir = None
631 cleandir = None
632 raise error.Abort(_("destination '%s' already exists")
632 raise error.Abort(_("destination '%s' already exists")
633 % dest)
633 % dest)
634 raise
634 raise
635
635
636 destlock = copystore(ui, srcrepo, destpath)
636 destlock = copystore(ui, srcrepo, destpath)
637 # copy bookmarks over
637 # copy bookmarks over
638 srcbookmarks = srcrepo.vfs.join('bookmarks')
638 srcbookmarks = srcrepo.vfs.join('bookmarks')
639 dstbookmarks = os.path.join(destpath, 'bookmarks')
639 dstbookmarks = os.path.join(destpath, 'bookmarks')
640 if os.path.exists(srcbookmarks):
640 if os.path.exists(srcbookmarks):
641 util.copyfile(srcbookmarks, dstbookmarks)
641 util.copyfile(srcbookmarks, dstbookmarks)
642
642
643 dstcachedir = os.path.join(destpath, 'cache')
643 dstcachedir = os.path.join(destpath, 'cache')
644 for cache in cacheutil.cachetocopy(srcrepo):
644 for cache in cacheutil.cachetocopy(srcrepo):
645 _copycache(srcrepo, dstcachedir, cache)
645 _copycache(srcrepo, dstcachedir, cache)
646
646
647 # we need to re-init the repo after manually copying the data
647 # we need to re-init the repo after manually copying the data
648 # into it
648 # into it
649 destpeer = peer(srcrepo, peeropts, dest)
649 destpeer = peer(srcrepo, peeropts, dest)
650 srcrepo.hook('outgoing', source='clone',
650 srcrepo.hook('outgoing', source='clone',
651 node=node.hex(node.nullid))
651 node=node.hex(node.nullid))
652 else:
652 else:
653 try:
653 try:
654 destpeer = peer(srcrepo or ui, peeropts, dest, create=True)
654 destpeer = peer(srcrepo or ui, peeropts, dest, create=True)
655 # only pass ui when no srcrepo
655 # only pass ui when no srcrepo
656 except OSError as inst:
656 except OSError as inst:
657 if inst.errno == errno.EEXIST:
657 if inst.errno == errno.EEXIST:
658 cleandir = None
658 cleandir = None
659 raise error.Abort(_("destination '%s' already exists")
659 raise error.Abort(_("destination '%s' already exists")
660 % dest)
660 % dest)
661 raise
661 raise
662
662
663 revs = None
663 revs = None
664 if rev:
664 if rev:
665 if not srcpeer.capable('lookup'):
665 if not srcpeer.capable('lookup'):
666 raise error.Abort(_("src repository does not support "
666 raise error.Abort(_("src repository does not support "
667 "revision lookup and so doesn't "
667 "revision lookup and so doesn't "
668 "support clone by revision"))
668 "support clone by revision"))
669 revs = [srcpeer.lookup(r) for r in rev]
669 revs = [srcpeer.lookup(r) for r in rev]
670 checkout = revs[0]
670 checkout = revs[0]
671 local = destpeer.local()
671 local = destpeer.local()
672 if local:
672 if local:
673 u = util.url(abspath)
673 u = util.url(abspath)
674 defaulturl = bytes(u)
674 defaulturl = bytes(u)
675 local.ui.setconfig('paths', 'default', defaulturl, 'clone')
675 local.ui.setconfig('paths', 'default', defaulturl, 'clone')
676 if not stream:
676 if not stream:
677 if pull:
677 if pull:
678 stream = False
678 stream = False
679 else:
679 else:
680 stream = None
680 stream = None
681 # internal config: ui.quietbookmarkmove
681 # internal config: ui.quietbookmarkmove
682 overrides = {('ui', 'quietbookmarkmove'): True}
682 overrides = {('ui', 'quietbookmarkmove'): True}
683 with local.ui.configoverride(overrides, 'clone'):
683 with local.ui.configoverride(overrides, 'clone'):
684 exchange.pull(local, srcpeer, revs,
684 exchange.pull(local, srcpeer, revs,
685 streamclonerequested=stream)
685 streamclonerequested=stream)
686 elif srcrepo:
686 elif srcrepo:
687 exchange.push(srcrepo, destpeer, revs=revs,
687 exchange.push(srcrepo, destpeer, revs=revs,
688 bookmarks=srcrepo._bookmarks.keys())
688 bookmarks=srcrepo._bookmarks.keys())
689 else:
689 else:
690 raise error.Abort(_("clone from remote to remote not supported")
690 raise error.Abort(_("clone from remote to remote not supported")
691 )
691 )
692
692
693 cleandir = None
693 cleandir = None
694
694
695 destrepo = destpeer.local()
695 destrepo = destpeer.local()
696 if destrepo:
696 if destrepo:
697 template = uimod.samplehgrcs['cloned']
697 template = uimod.samplehgrcs['cloned']
698 u = util.url(abspath)
698 u = util.url(abspath)
699 u.passwd = None
699 u.passwd = None
700 defaulturl = bytes(u)
700 defaulturl = bytes(u)
701 destrepo.vfs.write('hgrc', util.tonativeeol(template % defaulturl))
701 destrepo.vfs.write('hgrc', util.tonativeeol(template % defaulturl))
702 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
702 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
703
703
704 if ui.configbool('experimental', 'remotenames'):
704 if ui.configbool('experimental', 'remotenames'):
705 logexchange.pullremotenames(destrepo, srcpeer)
705 logexchange.pullremotenames(destrepo, srcpeer)
706
706
707 if update:
707 if update:
708 if update is not True:
708 if update is not True:
709 checkout = srcpeer.lookup(update)
709 checkout = srcpeer.lookup(update)
710 uprev = None
710 uprev = None
711 status = None
711 status = None
712 if checkout is not None:
712 if checkout is not None:
713 try:
713 try:
714 uprev = destrepo.lookup(checkout)
714 uprev = destrepo.lookup(checkout)
715 except error.RepoLookupError:
715 except error.RepoLookupError:
716 if update is not True:
716 if update is not True:
717 try:
717 try:
718 uprev = destrepo.lookup(update)
718 uprev = destrepo.lookup(update)
719 except error.RepoLookupError:
719 except error.RepoLookupError:
720 pass
720 pass
721 if uprev is None:
721 if uprev is None:
722 try:
722 try:
723 uprev = destrepo._bookmarks['@']
723 uprev = destrepo._bookmarks['@']
724 update = '@'
724 update = '@'
725 bn = destrepo[uprev].branch()
725 bn = destrepo[uprev].branch()
726 if bn == 'default':
726 if bn == 'default':
727 status = _("updating to bookmark @\n")
727 status = _("updating to bookmark @\n")
728 else:
728 else:
729 status = (_("updating to bookmark @ on branch %s\n")
729 status = (_("updating to bookmark @ on branch %s\n")
730 % bn)
730 % bn)
731 except KeyError:
731 except KeyError:
732 try:
732 try:
733 uprev = destrepo.branchtip('default')
733 uprev = destrepo.branchtip('default')
734 except error.RepoLookupError:
734 except error.RepoLookupError:
735 uprev = destrepo.lookup('tip')
735 uprev = destrepo.lookup('tip')
736 if not status:
736 if not status:
737 bn = destrepo[uprev].branch()
737 bn = destrepo[uprev].branch()
738 status = _("updating to branch %s\n") % bn
738 status = _("updating to branch %s\n") % bn
739 destrepo.ui.status(status)
739 destrepo.ui.status(status)
740 _update(destrepo, uprev)
740 _update(destrepo, uprev)
741 if update in destrepo._bookmarks:
741 if update in destrepo._bookmarks:
742 bookmarks.activate(destrepo, update)
742 bookmarks.activate(destrepo, update)
743 finally:
743 finally:
744 release(srclock, destlock)
744 release(srclock, destlock)
745 if cleandir is not None:
745 if cleandir is not None:
746 shutil.rmtree(cleandir, True)
746 shutil.rmtree(cleandir, True)
747 if srcpeer is not None:
747 if srcpeer is not None:
748 srcpeer.close()
748 srcpeer.close()
749 return srcpeer, destpeer
749 return srcpeer, destpeer
750
750
751 def _showstats(repo, stats, quietempty=False):
751 def _showstats(repo, stats, quietempty=False):
752 if quietempty and stats.isempty():
752 if quietempty and stats.isempty():
753 return
753 return
754 repo.ui.status(_("%d files updated, %d files merged, "
754 repo.ui.status(_("%d files updated, %d files merged, "
755 "%d files removed, %d files unresolved\n") % (
755 "%d files removed, %d files unresolved\n") % (
756 stats.updatedcount, stats.mergedcount,
756 stats.updatedcount, stats.mergedcount,
757 stats.removedcount, stats.unresolvedcount))
757 stats.removedcount, stats.unresolvedcount))
758
758
759 def updaterepo(repo, node, overwrite, updatecheck=None):
759 def updaterepo(repo, node, overwrite, updatecheck=None):
760 """Update the working directory to node.
760 """Update the working directory to node.
761
761
762 When overwrite is set, changes are clobbered, merged else
762 When overwrite is set, changes are clobbered, merged else
763
763
764 returns stats (see pydoc mercurial.merge.applyupdates)"""
764 returns stats (see pydoc mercurial.merge.applyupdates)"""
765 return mergemod.update(repo, node, False, overwrite,
765 return mergemod.update(repo, node, False, overwrite,
766 labels=['working copy', 'destination'],
766 labels=['working copy', 'destination'],
767 updatecheck=updatecheck)
767 updatecheck=updatecheck)
768
768
769 def update(repo, node, quietempty=False, updatecheck=None):
769 def update(repo, node, quietempty=False, updatecheck=None):
770 """update the working directory to node"""
770 """update the working directory to node"""
771 stats = updaterepo(repo, node, False, updatecheck=updatecheck)
771 stats = updaterepo(repo, node, False, updatecheck=updatecheck)
772 _showstats(repo, stats, quietempty)
772 _showstats(repo, stats, quietempty)
773 if stats.unresolvedcount:
773 if stats.unresolvedcount:
774 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
774 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
775 return stats.unresolvedcount > 0
775 return stats.unresolvedcount > 0
776
776
777 # naming conflict in clone()
777 # naming conflict in clone()
778 _update = update
778 _update = update
779
779
780 def clean(repo, node, show_stats=True, quietempty=False):
780 def clean(repo, node, show_stats=True, quietempty=False):
781 """forcibly switch the working directory to node, clobbering changes"""
781 """forcibly switch the working directory to node, clobbering changes"""
782 stats = updaterepo(repo, node, True)
782 stats = updaterepo(repo, node, True)
783 repo.vfs.unlinkpath('graftstate', ignoremissing=True)
783 repo.vfs.unlinkpath('graftstate', ignoremissing=True)
784 if show_stats:
784 if show_stats:
785 _showstats(repo, stats, quietempty)
785 _showstats(repo, stats, quietempty)
786 return stats.unresolvedcount > 0
786 return stats.unresolvedcount > 0
787
787
788 # naming conflict in updatetotally()
788 # naming conflict in updatetotally()
789 _clean = clean
789 _clean = clean
790
790
791 def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
791 def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
792 """Update the working directory with extra care for non-file components
792 """Update the working directory with extra care for non-file components
793
793
794 This takes care of non-file components below:
794 This takes care of non-file components below:
795
795
796 :bookmark: might be advanced or (in)activated
796 :bookmark: might be advanced or (in)activated
797
797
798 This takes arguments below:
798 This takes arguments below:
799
799
800 :checkout: to which revision the working directory is updated
800 :checkout: to which revision the working directory is updated
801 :brev: a name, which might be a bookmark to be activated after updating
801 :brev: a name, which might be a bookmark to be activated after updating
802 :clean: whether changes in the working directory can be discarded
802 :clean: whether changes in the working directory can be discarded
803 :updatecheck: how to deal with a dirty working directory
803 :updatecheck: how to deal with a dirty working directory
804
804
805 Valid values for updatecheck are (None => linear):
805 Valid values for updatecheck are (None => linear):
806
806
807 * abort: abort if the working directory is dirty
807 * abort: abort if the working directory is dirty
808 * none: don't check (merge working directory changes into destination)
808 * none: don't check (merge working directory changes into destination)
809 * linear: check that update is linear before merging working directory
809 * linear: check that update is linear before merging working directory
810 changes into destination
810 changes into destination
811 * noconflict: check that the update does not result in file merges
811 * noconflict: check that the update does not result in file merges
812
812
813 This returns whether conflict is detected at updating or not.
813 This returns whether conflict is detected at updating or not.
814 """
814 """
815 if updatecheck is None:
815 if updatecheck is None:
816 updatecheck = ui.config('commands', 'update.check')
816 updatecheck = ui.config('commands', 'update.check')
817 if updatecheck not in ('abort', 'none', 'linear', 'noconflict'):
817 if updatecheck not in ('abort', 'none', 'linear', 'noconflict'):
818 # If not configured, or invalid value configured
818 # If not configured, or invalid value configured
819 updatecheck = 'linear'
819 updatecheck = 'linear'
820 with repo.wlock():
820 with repo.wlock():
821 movemarkfrom = None
821 movemarkfrom = None
822 warndest = False
822 warndest = False
823 if checkout is None:
823 if checkout is None:
824 updata = destutil.destupdate(repo, clean=clean)
824 updata = destutil.destupdate(repo, clean=clean)
825 checkout, movemarkfrom, brev = updata
825 checkout, movemarkfrom, brev = updata
826 warndest = True
826 warndest = True
827
827
828 if clean:
828 if clean:
829 ret = _clean(repo, checkout)
829 ret = _clean(repo, checkout)
830 else:
830 else:
831 if updatecheck == 'abort':
831 if updatecheck == 'abort':
832 cmdutil.bailifchanged(repo, merge=False)
832 cmdutil.bailifchanged(repo, merge=False)
833 updatecheck = 'none'
833 updatecheck = 'none'
834 ret = _update(repo, checkout, updatecheck=updatecheck)
834 ret = _update(repo, checkout, updatecheck=updatecheck)
835
835
836 if not ret and movemarkfrom:
836 if not ret and movemarkfrom:
837 if movemarkfrom == repo['.'].node():
837 if movemarkfrom == repo['.'].node():
838 pass # no-op update
838 pass # no-op update
839 elif bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
839 elif bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
840 b = ui.label(repo._activebookmark, 'bookmarks.active')
840 b = ui.label(repo._activebookmark, 'bookmarks.active')
841 ui.status(_("updating bookmark %s\n") % b)
841 ui.status(_("updating bookmark %s\n") % b)
842 else:
842 else:
843 # this can happen with a non-linear update
843 # this can happen with a non-linear update
844 b = ui.label(repo._activebookmark, 'bookmarks')
844 b = ui.label(repo._activebookmark, 'bookmarks')
845 ui.status(_("(leaving bookmark %s)\n") % b)
845 ui.status(_("(leaving bookmark %s)\n") % b)
846 bookmarks.deactivate(repo)
846 bookmarks.deactivate(repo)
847 elif brev in repo._bookmarks:
847 elif brev in repo._bookmarks:
848 if brev != repo._activebookmark:
848 if brev != repo._activebookmark:
849 b = ui.label(brev, 'bookmarks.active')
849 b = ui.label(brev, 'bookmarks.active')
850 ui.status(_("(activating bookmark %s)\n") % b)
850 ui.status(_("(activating bookmark %s)\n") % b)
851 bookmarks.activate(repo, brev)
851 bookmarks.activate(repo, brev)
852 elif brev:
852 elif brev:
853 if repo._activebookmark:
853 if repo._activebookmark:
854 b = ui.label(repo._activebookmark, 'bookmarks')
854 b = ui.label(repo._activebookmark, 'bookmarks')
855 ui.status(_("(leaving bookmark %s)\n") % b)
855 ui.status(_("(leaving bookmark %s)\n") % b)
856 bookmarks.deactivate(repo)
856 bookmarks.deactivate(repo)
857
857
858 if warndest:
858 if warndest:
859 destutil.statusotherdests(ui, repo)
859 destutil.statusotherdests(ui, repo)
860
860
861 return ret
861 return ret
862
862
863 def merge(repo, node, force=None, remind=True, mergeforce=False, labels=None,
863 def merge(repo, node, force=None, remind=True, mergeforce=False, labels=None,
864 abort=False):
864 abort=False):
865 """Branch merge with node, resolving changes. Return true if any
865 """Branch merge with node, resolving changes. Return true if any
866 unresolved conflicts."""
866 unresolved conflicts."""
867 if not abort:
867 if not abort:
868 stats = mergemod.update(repo, node, True, force, mergeforce=mergeforce,
868 stats = mergemod.update(repo, node, True, force, mergeforce=mergeforce,
869 labels=labels)
869 labels=labels)
870 else:
870 else:
871 ms = mergemod.mergestate.read(repo)
871 ms = mergemod.mergestate.read(repo)
872 if ms.active():
872 if ms.active():
873 # there were conflicts
873 # there were conflicts
874 node = ms.localctx.hex()
874 node = ms.localctx.hex()
875 else:
875 else:
876 # there were no conficts, mergestate was not stored
876 # there were no conficts, mergestate was not stored
877 node = repo['.'].hex()
877 node = repo['.'].hex()
878
878
879 repo.ui.status(_("aborting the merge, updating back to"
879 repo.ui.status(_("aborting the merge, updating back to"
880 " %s\n") % node[:12])
880 " %s\n") % node[:12])
881 stats = mergemod.update(repo, node, branchmerge=False, force=True,
881 stats = mergemod.update(repo, node, branchmerge=False, force=True,
882 labels=labels)
882 labels=labels)
883
883
884 _showstats(repo, stats)
884 _showstats(repo, stats)
885 if stats.unresolvedcount:
885 if stats.unresolvedcount:
886 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
886 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
887 "or 'hg merge --abort' to abandon\n"))
887 "or 'hg merge --abort' to abandon\n"))
888 elif remind and not abort:
888 elif remind and not abort:
889 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
889 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
890 return stats.unresolvedcount > 0
890 return stats.unresolvedcount > 0
891
891
892 def _incoming(displaychlist, subreporecurse, ui, repo, source,
892 def _incoming(displaychlist, subreporecurse, ui, repo, source,
893 opts, buffered=False):
893 opts, buffered=False):
894 """
894 """
895 Helper for incoming / gincoming.
895 Helper for incoming / gincoming.
896 displaychlist gets called with
896 displaychlist gets called with
897 (remoterepo, incomingchangesetlist, displayer) parameters,
897 (remoterepo, incomingchangesetlist, displayer) parameters,
898 and is supposed to contain only code that can't be unified.
898 and is supposed to contain only code that can't be unified.
899 """
899 """
900 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
900 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
901 other = peer(repo, opts, source)
901 other = peer(repo, opts, source)
902 ui.status(_('comparing with %s\n') % util.hidepassword(source))
902 ui.status(_('comparing with %s\n') % util.hidepassword(source))
903 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
903 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
904
904
905 if revs:
905 if revs:
906 revs = [other.lookup(rev) for rev in revs]
906 revs = [other.lookup(rev) for rev in revs]
907 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
907 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
908 revs, opts["bundle"], opts["force"])
908 revs, opts["bundle"], opts["force"])
909 try:
909 try:
910 if not chlist:
910 if not chlist:
911 ui.status(_("no changes found\n"))
911 ui.status(_("no changes found\n"))
912 return subreporecurse()
912 return subreporecurse()
913 ui.pager('incoming')
913 ui.pager('incoming')
914 displayer = logcmdutil.changesetdisplayer(ui, other, opts,
914 displayer = logcmdutil.changesetdisplayer(ui, other, opts,
915 buffered=buffered)
915 buffered=buffered)
916 displaychlist(other, chlist, displayer)
916 displaychlist(other, chlist, displayer)
917 displayer.close()
917 displayer.close()
918 finally:
918 finally:
919 cleanupfn()
919 cleanupfn()
920 subreporecurse()
920 subreporecurse()
921 return 0 # exit code is zero since we found incoming changes
921 return 0 # exit code is zero since we found incoming changes
922
922
923 def incoming(ui, repo, source, opts):
923 def incoming(ui, repo, source, opts):
924 def subreporecurse():
924 def subreporecurse():
925 ret = 1
925 ret = 1
926 if opts.get('subrepos'):
926 if opts.get('subrepos'):
927 ctx = repo[None]
927 ctx = repo[None]
928 for subpath in sorted(ctx.substate):
928 for subpath in sorted(ctx.substate):
929 sub = ctx.sub(subpath)
929 sub = ctx.sub(subpath)
930 ret = min(ret, sub.incoming(ui, source, opts))
930 ret = min(ret, sub.incoming(ui, source, opts))
931 return ret
931 return ret
932
932
933 def display(other, chlist, displayer):
933 def display(other, chlist, displayer):
934 limit = logcmdutil.getlimit(opts)
934 limit = logcmdutil.getlimit(opts)
935 if opts.get('newest_first'):
935 if opts.get('newest_first'):
936 chlist.reverse()
936 chlist.reverse()
937 count = 0
937 count = 0
938 for n in chlist:
938 for n in chlist:
939 if limit is not None and count >= limit:
939 if limit is not None and count >= limit:
940 break
940 break
941 parents = [p for p in other.changelog.parents(n) if p != nullid]
941 parents = [p for p in other.changelog.parents(n) if p != nullid]
942 if opts.get('no_merges') and len(parents) == 2:
942 if opts.get('no_merges') and len(parents) == 2:
943 continue
943 continue
944 count += 1
944 count += 1
945 displayer.show(other[n])
945 displayer.show(other[n])
946 return _incoming(display, subreporecurse, ui, repo, source, opts)
946 return _incoming(display, subreporecurse, ui, repo, source, opts)
947
947
948 def _outgoing(ui, repo, dest, opts):
948 def _outgoing(ui, repo, dest, opts):
949 path = ui.paths.getpath(dest, default=('default-push', 'default'))
949 path = ui.paths.getpath(dest, default=('default-push', 'default'))
950 if not path:
950 if not path:
951 raise error.Abort(_('default repository not configured!'),
951 raise error.Abort(_('default repository not configured!'),
952 hint=_("see 'hg help config.paths'"))
952 hint=_("see 'hg help config.paths'"))
953 dest = path.pushloc or path.loc
953 dest = path.pushloc or path.loc
954 branches = path.branch, opts.get('branch') or []
954 branches = path.branch, opts.get('branch') or []
955
955
956 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
956 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
957 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
957 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
958 if revs:
958 if revs:
959 revs = [repo.lookup(rev) for rev in scmutil.revrange(repo, revs)]
959 revs = [repo.lookup(rev) for rev in scmutil.revrange(repo, revs)]
960
960
961 other = peer(repo, opts, dest)
961 other = peer(repo, opts, dest)
962 outgoing = discovery.findcommonoutgoing(repo, other, revs,
962 outgoing = discovery.findcommonoutgoing(repo, other, revs,
963 force=opts.get('force'))
963 force=opts.get('force'))
964 o = outgoing.missing
964 o = outgoing.missing
965 if not o:
965 if not o:
966 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
966 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
967 return o, other
967 return o, other
968
968
969 def outgoing(ui, repo, dest, opts):
969 def outgoing(ui, repo, dest, opts):
970 def recurse():
970 def recurse():
971 ret = 1
971 ret = 1
972 if opts.get('subrepos'):
972 if opts.get('subrepos'):
973 ctx = repo[None]
973 ctx = repo[None]
974 for subpath in sorted(ctx.substate):
974 for subpath in sorted(ctx.substate):
975 sub = ctx.sub(subpath)
975 sub = ctx.sub(subpath)
976 ret = min(ret, sub.outgoing(ui, dest, opts))
976 ret = min(ret, sub.outgoing(ui, dest, opts))
977 return ret
977 return ret
978
978
979 limit = logcmdutil.getlimit(opts)
979 limit = logcmdutil.getlimit(opts)
980 o, other = _outgoing(ui, repo, dest, opts)
980 o, other = _outgoing(ui, repo, dest, opts)
981 if not o:
981 if not o:
982 cmdutil.outgoinghooks(ui, repo, other, opts, o)
982 cmdutil.outgoinghooks(ui, repo, other, opts, o)
983 return recurse()
983 return recurse()
984
984
985 if opts.get('newest_first'):
985 if opts.get('newest_first'):
986 o.reverse()
986 o.reverse()
987 ui.pager('outgoing')
987 ui.pager('outgoing')
988 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
988 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
989 count = 0
989 count = 0
990 for n in o:
990 for n in o:
991 if limit is not None and count >= limit:
991 if limit is not None and count >= limit:
992 break
992 break
993 parents = [p for p in repo.changelog.parents(n) if p != nullid]
993 parents = [p for p in repo.changelog.parents(n) if p != nullid]
994 if opts.get('no_merges') and len(parents) == 2:
994 if opts.get('no_merges') and len(parents) == 2:
995 continue
995 continue
996 count += 1
996 count += 1
997 displayer.show(repo[n])
997 displayer.show(repo[n])
998 displayer.close()
998 displayer.close()
999 cmdutil.outgoinghooks(ui, repo, other, opts, o)
999 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1000 recurse()
1000 recurse()
1001 return 0 # exit code is zero since we found outgoing changes
1001 return 0 # exit code is zero since we found outgoing changes
1002
1002
1003 def verify(repo):
1003 def verify(repo):
1004 """verify the consistency of a repository"""
1004 """verify the consistency of a repository"""
1005 ret = verifymod.verify(repo)
1005 ret = verifymod.verify(repo)
1006
1006
1007 # Broken subrepo references in hidden csets don't seem worth worrying about,
1007 # Broken subrepo references in hidden csets don't seem worth worrying about,
1008 # since they can't be pushed/pulled, and --hidden can be used if they are a
1008 # since they can't be pushed/pulled, and --hidden can be used if they are a
1009 # concern.
1009 # concern.
1010
1010
1011 # pathto() is needed for -R case
1011 # pathto() is needed for -R case
1012 revs = repo.revs("filelog(%s)",
1012 revs = repo.revs("filelog(%s)",
1013 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
1013 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
1014
1014
1015 if revs:
1015 if revs:
1016 repo.ui.status(_('checking subrepo links\n'))
1016 repo.ui.status(_('checking subrepo links\n'))
1017 for rev in revs:
1017 for rev in revs:
1018 ctx = repo[rev]
1018 ctx = repo[rev]
1019 try:
1019 try:
1020 for subpath in ctx.substate:
1020 for subpath in ctx.substate:
1021 try:
1021 try:
1022 ret = (ctx.sub(subpath, allowcreate=False).verify()
1022 ret = (ctx.sub(subpath, allowcreate=False).verify()
1023 or ret)
1023 or ret)
1024 except error.RepoError as e:
1024 except error.RepoError as e:
1025 repo.ui.warn(('%s: %s\n') % (rev, e))
1025 repo.ui.warn(('%s: %s\n') % (rev, e))
1026 except Exception:
1026 except Exception:
1027 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
1027 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
1028 node.short(ctx.node()))
1028 node.short(ctx.node()))
1029
1029
1030 return ret
1030 return ret
1031
1031
1032 def remoteui(src, opts):
1032 def remoteui(src, opts):
1033 'build a remote ui from ui or repo and opts'
1033 'build a remote ui from ui or repo and opts'
1034 if util.safehasattr(src, 'baseui'): # looks like a repository
1034 if util.safehasattr(src, 'baseui'): # looks like a repository
1035 dst = src.baseui.copy() # drop repo-specific config
1035 dst = src.baseui.copy() # drop repo-specific config
1036 src = src.ui # copy target options from repo
1036 src = src.ui # copy target options from repo
1037 else: # assume it's a global ui object
1037 else: # assume it's a global ui object
1038 dst = src.copy() # keep all global options
1038 dst = src.copy() # keep all global options
1039
1039
1040 # copy ssh-specific options
1040 # copy ssh-specific options
1041 for o in 'ssh', 'remotecmd':
1041 for o in 'ssh', 'remotecmd':
1042 v = opts.get(o) or src.config('ui', o)
1042 v = opts.get(o) or src.config('ui', o)
1043 if v:
1043 if v:
1044 dst.setconfig("ui", o, v, 'copied')
1044 dst.setconfig("ui", o, v, 'copied')
1045
1045
1046 # copy bundle-specific options
1046 # copy bundle-specific options
1047 r = src.config('bundle', 'mainreporoot')
1047 r = src.config('bundle', 'mainreporoot')
1048 if r:
1048 if r:
1049 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
1049 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
1050
1050
1051 # copy selected local settings to the remote ui
1051 # copy selected local settings to the remote ui
1052 for sect in ('auth', 'hostfingerprints', 'hostsecurity', 'http_proxy'):
1052 for sect in ('auth', 'hostfingerprints', 'hostsecurity', 'http_proxy'):
1053 for key, val in src.configitems(sect):
1053 for key, val in src.configitems(sect):
1054 dst.setconfig(sect, key, val, 'copied')
1054 dst.setconfig(sect, key, val, 'copied')
1055 v = src.config('web', 'cacerts')
1055 v = src.config('web', 'cacerts')
1056 if v:
1056 if v:
1057 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
1057 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
1058
1058
1059 return dst
1059 return dst
1060
1060
1061 # Files of interest
1061 # Files of interest
1062 # Used to check if the repository has changed looking at mtime and size of
1062 # Used to check if the repository has changed looking at mtime and size of
1063 # these files.
1063 # these files.
1064 foi = [('spath', '00changelog.i'),
1064 foi = [('spath', '00changelog.i'),
1065 ('spath', 'phaseroots'), # ! phase can change content at the same size
1065 ('spath', 'phaseroots'), # ! phase can change content at the same size
1066 ('spath', 'obsstore'),
1066 ('spath', 'obsstore'),
1067 ('path', 'bookmarks'), # ! bookmark can change content at the same size
1067 ('path', 'bookmarks'), # ! bookmark can change content at the same size
1068 ]
1068 ]
1069
1069
1070 class cachedlocalrepo(object):
1070 class cachedlocalrepo(object):
1071 """Holds a localrepository that can be cached and reused."""
1071 """Holds a localrepository that can be cached and reused."""
1072
1072
1073 def __init__(self, repo):
1073 def __init__(self, repo):
1074 """Create a new cached repo from an existing repo.
1074 """Create a new cached repo from an existing repo.
1075
1075
1076 We assume the passed in repo was recently created. If the
1076 We assume the passed in repo was recently created. If the
1077 repo has changed between when it was created and when it was
1077 repo has changed between when it was created and when it was
1078 turned into a cache, it may not refresh properly.
1078 turned into a cache, it may not refresh properly.
1079 """
1079 """
1080 assert isinstance(repo, localrepo.localrepository)
1080 assert isinstance(repo, localrepo.localrepository)
1081 self._repo = repo
1081 self._repo = repo
1082 self._state, self.mtime = self._repostate()
1082 self._state, self.mtime = self._repostate()
1083 self._filtername = repo.filtername
1083 self._filtername = repo.filtername
1084
1084
1085 def fetch(self):
1085 def fetch(self):
1086 """Refresh (if necessary) and return a repository.
1086 """Refresh (if necessary) and return a repository.
1087
1087
1088 If the cached instance is out of date, it will be recreated
1088 If the cached instance is out of date, it will be recreated
1089 automatically and returned.
1089 automatically and returned.
1090
1090
1091 Returns a tuple of the repo and a boolean indicating whether a new
1091 Returns a tuple of the repo and a boolean indicating whether a new
1092 repo instance was created.
1092 repo instance was created.
1093 """
1093 """
1094 # We compare the mtimes and sizes of some well-known files to
1094 # We compare the mtimes and sizes of some well-known files to
1095 # determine if the repo changed. This is not precise, as mtimes
1095 # determine if the repo changed. This is not precise, as mtimes
1096 # are susceptible to clock skew and imprecise filesystems and
1096 # are susceptible to clock skew and imprecise filesystems and
1097 # file content can change while maintaining the same size.
1097 # file content can change while maintaining the same size.
1098
1098
1099 state, mtime = self._repostate()
1099 state, mtime = self._repostate()
1100 if state == self._state:
1100 if state == self._state:
1101 return self._repo, False
1101 return self._repo, False
1102
1102
1103 repo = repository(self._repo.baseui, self._repo.url())
1103 repo = repository(self._repo.baseui, self._repo.url())
1104 if self._filtername:
1104 if self._filtername:
1105 self._repo = repo.filtered(self._filtername)
1105 self._repo = repo.filtered(self._filtername)
1106 else:
1106 else:
1107 self._repo = repo.unfiltered()
1107 self._repo = repo.unfiltered()
1108 self._state = state
1108 self._state = state
1109 self.mtime = mtime
1109 self.mtime = mtime
1110
1110
1111 return self._repo, True
1111 return self._repo, True
1112
1112
1113 def _repostate(self):
1113 def _repostate(self):
1114 state = []
1114 state = []
1115 maxmtime = -1
1115 maxmtime = -1
1116 for attr, fname in foi:
1116 for attr, fname in foi:
1117 prefix = getattr(self._repo, attr)
1117 prefix = getattr(self._repo, attr)
1118 p = os.path.join(prefix, fname)
1118 p = os.path.join(prefix, fname)
1119 try:
1119 try:
1120 st = os.stat(p)
1120 st = os.stat(p)
1121 except OSError:
1121 except OSError:
1122 st = os.stat(prefix)
1122 st = os.stat(prefix)
1123 state.append((st[stat.ST_MTIME], st.st_size))
1123 state.append((st[stat.ST_MTIME], st.st_size))
1124 maxmtime = max(maxmtime, st[stat.ST_MTIME])
1124 maxmtime = max(maxmtime, st[stat.ST_MTIME])
1125
1125
1126 return tuple(state), maxmtime
1126 return tuple(state), maxmtime
1127
1127
1128 def copy(self):
1128 def copy(self):
1129 """Obtain a copy of this class instance.
1129 """Obtain a copy of this class instance.
1130
1130
1131 A new localrepository instance is obtained. The new instance should be
1131 A new localrepository instance is obtained. The new instance should be
1132 completely independent of the original.
1132 completely independent of the original.
1133 """
1133 """
1134 repo = repository(self._repo.baseui, self._repo.origroot)
1134 repo = repository(self._repo.baseui, self._repo.origroot)
1135 if self._filtername:
1135 if self._filtername:
1136 repo = repo.filtered(self._filtername)
1136 repo = repo.filtered(self._filtername)
1137 else:
1137 else:
1138 repo = repo.unfiltered()
1138 repo = repo.unfiltered()
1139 c = cachedlocalrepo(repo)
1139 c = cachedlocalrepo(repo)
1140 c._state = self._state
1140 c._state = self._state
1141 c.mtime = self.mtime
1141 c.mtime = self.mtime
1142 return c
1142 return c
General Comments 0
You need to be logged in to leave comments. Login now