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