##// END OF EJS Templates
split: use literal syntax to build a set of one element
Yuya Nishihara -
r43162:a65c4715 default
parent child Browse files
Show More
@@ -1,3439 +1,3439 b''
1 # cmdutil.py - help for command processing in mercurial
1 # cmdutil.py - help for command processing in mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy as copymod
10 import copy as copymod
11 import errno
11 import errno
12 import os
12 import os
13 import re
13 import re
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 hex,
17 hex,
18 nullid,
18 nullid,
19 nullrev,
19 nullrev,
20 short,
20 short,
21 )
21 )
22
22
23 from . import (
23 from . import (
24 bookmarks,
24 bookmarks,
25 changelog,
25 changelog,
26 copies,
26 copies,
27 crecord as crecordmod,
27 crecord as crecordmod,
28 dirstateguard,
28 dirstateguard,
29 encoding,
29 encoding,
30 error,
30 error,
31 formatter,
31 formatter,
32 logcmdutil,
32 logcmdutil,
33 match as matchmod,
33 match as matchmod,
34 merge as mergemod,
34 merge as mergemod,
35 mergeutil,
35 mergeutil,
36 obsolete,
36 obsolete,
37 patch,
37 patch,
38 pathutil,
38 pathutil,
39 phases,
39 phases,
40 pycompat,
40 pycompat,
41 repair,
41 repair,
42 revlog,
42 revlog,
43 rewriteutil,
43 rewriteutil,
44 scmutil,
44 scmutil,
45 smartset,
45 smartset,
46 state as statemod,
46 state as statemod,
47 subrepoutil,
47 subrepoutil,
48 templatekw,
48 templatekw,
49 templater,
49 templater,
50 util,
50 util,
51 vfs as vfsmod,
51 vfs as vfsmod,
52 )
52 )
53
53
54 from .utils import (
54 from .utils import (
55 dateutil,
55 dateutil,
56 stringutil,
56 stringutil,
57 )
57 )
58
58
59 stringio = util.stringio
59 stringio = util.stringio
60
60
61 # templates of common command options
61 # templates of common command options
62
62
63 dryrunopts = [
63 dryrunopts = [
64 ('n', 'dry-run', None,
64 ('n', 'dry-run', None,
65 _('do not perform actions, just print output')),
65 _('do not perform actions, just print output')),
66 ]
66 ]
67
67
68 confirmopts = [
68 confirmopts = [
69 ('', 'confirm', None,
69 ('', 'confirm', None,
70 _('ask before applying actions')),
70 _('ask before applying actions')),
71 ]
71 ]
72
72
73 remoteopts = [
73 remoteopts = [
74 ('e', 'ssh', '',
74 ('e', 'ssh', '',
75 _('specify ssh command to use'), _('CMD')),
75 _('specify ssh command to use'), _('CMD')),
76 ('', 'remotecmd', '',
76 ('', 'remotecmd', '',
77 _('specify hg command to run on the remote side'), _('CMD')),
77 _('specify hg command to run on the remote side'), _('CMD')),
78 ('', 'insecure', None,
78 ('', 'insecure', None,
79 _('do not verify server certificate (ignoring web.cacerts config)')),
79 _('do not verify server certificate (ignoring web.cacerts config)')),
80 ]
80 ]
81
81
82 walkopts = [
82 walkopts = [
83 ('I', 'include', [],
83 ('I', 'include', [],
84 _('include names matching the given patterns'), _('PATTERN')),
84 _('include names matching the given patterns'), _('PATTERN')),
85 ('X', 'exclude', [],
85 ('X', 'exclude', [],
86 _('exclude names matching the given patterns'), _('PATTERN')),
86 _('exclude names matching the given patterns'), _('PATTERN')),
87 ]
87 ]
88
88
89 commitopts = [
89 commitopts = [
90 ('m', 'message', '',
90 ('m', 'message', '',
91 _('use text as commit message'), _('TEXT')),
91 _('use text as commit message'), _('TEXT')),
92 ('l', 'logfile', '',
92 ('l', 'logfile', '',
93 _('read commit message from file'), _('FILE')),
93 _('read commit message from file'), _('FILE')),
94 ]
94 ]
95
95
96 commitopts2 = [
96 commitopts2 = [
97 ('d', 'date', '',
97 ('d', 'date', '',
98 _('record the specified date as commit date'), _('DATE')),
98 _('record the specified date as commit date'), _('DATE')),
99 ('u', 'user', '',
99 ('u', 'user', '',
100 _('record the specified user as committer'), _('USER')),
100 _('record the specified user as committer'), _('USER')),
101 ]
101 ]
102
102
103 formatteropts = [
103 formatteropts = [
104 ('T', 'template', '',
104 ('T', 'template', '',
105 _('display with template'), _('TEMPLATE')),
105 _('display with template'), _('TEMPLATE')),
106 ]
106 ]
107
107
108 templateopts = [
108 templateopts = [
109 ('', 'style', '',
109 ('', 'style', '',
110 _('display using template map file (DEPRECATED)'), _('STYLE')),
110 _('display using template map file (DEPRECATED)'), _('STYLE')),
111 ('T', 'template', '',
111 ('T', 'template', '',
112 _('display with template'), _('TEMPLATE')),
112 _('display with template'), _('TEMPLATE')),
113 ]
113 ]
114
114
115 logopts = [
115 logopts = [
116 ('p', 'patch', None, _('show patch')),
116 ('p', 'patch', None, _('show patch')),
117 ('g', 'git', None, _('use git extended diff format')),
117 ('g', 'git', None, _('use git extended diff format')),
118 ('l', 'limit', '',
118 ('l', 'limit', '',
119 _('limit number of changes displayed'), _('NUM')),
119 _('limit number of changes displayed'), _('NUM')),
120 ('M', 'no-merges', None, _('do not show merges')),
120 ('M', 'no-merges', None, _('do not show merges')),
121 ('', 'stat', None, _('output diffstat-style summary of changes')),
121 ('', 'stat', None, _('output diffstat-style summary of changes')),
122 ('G', 'graph', None, _("show the revision DAG")),
122 ('G', 'graph', None, _("show the revision DAG")),
123 ] + templateopts
123 ] + templateopts
124
124
125 diffopts = [
125 diffopts = [
126 ('a', 'text', None, _('treat all files as text')),
126 ('a', 'text', None, _('treat all files as text')),
127 ('g', 'git', None, _('use git extended diff format')),
127 ('g', 'git', None, _('use git extended diff format')),
128 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
128 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
129 ('', 'nodates', None, _('omit dates from diff headers'))
129 ('', 'nodates', None, _('omit dates from diff headers'))
130 ]
130 ]
131
131
132 diffwsopts = [
132 diffwsopts = [
133 ('w', 'ignore-all-space', None,
133 ('w', 'ignore-all-space', None,
134 _('ignore white space when comparing lines')),
134 _('ignore white space when comparing lines')),
135 ('b', 'ignore-space-change', None,
135 ('b', 'ignore-space-change', None,
136 _('ignore changes in the amount of white space')),
136 _('ignore changes in the amount of white space')),
137 ('B', 'ignore-blank-lines', None,
137 ('B', 'ignore-blank-lines', None,
138 _('ignore changes whose lines are all blank')),
138 _('ignore changes whose lines are all blank')),
139 ('Z', 'ignore-space-at-eol', None,
139 ('Z', 'ignore-space-at-eol', None,
140 _('ignore changes in whitespace at EOL')),
140 _('ignore changes in whitespace at EOL')),
141 ]
141 ]
142
142
143 diffopts2 = [
143 diffopts2 = [
144 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
144 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
145 ('p', 'show-function', None, _('show which function each change is in')),
145 ('p', 'show-function', None, _('show which function each change is in')),
146 ('', 'reverse', None, _('produce a diff that undoes the changes')),
146 ('', 'reverse', None, _('produce a diff that undoes the changes')),
147 ] + diffwsopts + [
147 ] + diffwsopts + [
148 ('U', 'unified', '',
148 ('U', 'unified', '',
149 _('number of lines of context to show'), _('NUM')),
149 _('number of lines of context to show'), _('NUM')),
150 ('', 'stat', None, _('output diffstat-style summary of changes')),
150 ('', 'stat', None, _('output diffstat-style summary of changes')),
151 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
151 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
152 ]
152 ]
153
153
154 mergetoolopts = [
154 mergetoolopts = [
155 ('t', 'tool', '', _('specify merge tool'), _('TOOL')),
155 ('t', 'tool', '', _('specify merge tool'), _('TOOL')),
156 ]
156 ]
157
157
158 similarityopts = [
158 similarityopts = [
159 ('s', 'similarity', '',
159 ('s', 'similarity', '',
160 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
160 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
161 ]
161 ]
162
162
163 subrepoopts = [
163 subrepoopts = [
164 ('S', 'subrepos', None,
164 ('S', 'subrepos', None,
165 _('recurse into subrepositories'))
165 _('recurse into subrepositories'))
166 ]
166 ]
167
167
168 debugrevlogopts = [
168 debugrevlogopts = [
169 ('c', 'changelog', False, _('open changelog')),
169 ('c', 'changelog', False, _('open changelog')),
170 ('m', 'manifest', False, _('open manifest')),
170 ('m', 'manifest', False, _('open manifest')),
171 ('', 'dir', '', _('open directory manifest')),
171 ('', 'dir', '', _('open directory manifest')),
172 ]
172 ]
173
173
174 # special string such that everything below this line will be ingored in the
174 # special string such that everything below this line will be ingored in the
175 # editor text
175 # editor text
176 _linebelow = "^HG: ------------------------ >8 ------------------------$"
176 _linebelow = "^HG: ------------------------ >8 ------------------------$"
177
177
178 def ishunk(x):
178 def ishunk(x):
179 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
179 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
180 return isinstance(x, hunkclasses)
180 return isinstance(x, hunkclasses)
181
181
182 def newandmodified(chunks, originalchunks):
182 def newandmodified(chunks, originalchunks):
183 newlyaddedandmodifiedfiles = set()
183 newlyaddedandmodifiedfiles = set()
184 alsorestore = set()
184 alsorestore = set()
185 for chunk in chunks:
185 for chunk in chunks:
186 if (ishunk(chunk) and chunk.header.isnewfile() and chunk not in
186 if (ishunk(chunk) and chunk.header.isnewfile() and chunk not in
187 originalchunks):
187 originalchunks):
188 newlyaddedandmodifiedfiles.add(chunk.header.filename())
188 newlyaddedandmodifiedfiles.add(chunk.header.filename())
189 alsorestore.update(set(chunk.header.files()) -
189 alsorestore.update(set(chunk.header.files()) -
190 set([chunk.header.filename()]))
190 {chunk.header.filename()})
191 return newlyaddedandmodifiedfiles, alsorestore
191 return newlyaddedandmodifiedfiles, alsorestore
192
192
193 def parsealiases(cmd):
193 def parsealiases(cmd):
194 return cmd.split("|")
194 return cmd.split("|")
195
195
196 def setupwrapcolorwrite(ui):
196 def setupwrapcolorwrite(ui):
197 # wrap ui.write so diff output can be labeled/colorized
197 # wrap ui.write so diff output can be labeled/colorized
198 def wrapwrite(orig, *args, **kw):
198 def wrapwrite(orig, *args, **kw):
199 label = kw.pop(r'label', '')
199 label = kw.pop(r'label', '')
200 for chunk, l in patch.difflabel(lambda: args):
200 for chunk, l in patch.difflabel(lambda: args):
201 orig(chunk, label=label + l)
201 orig(chunk, label=label + l)
202
202
203 oldwrite = ui.write
203 oldwrite = ui.write
204 def wrap(*args, **kwargs):
204 def wrap(*args, **kwargs):
205 return wrapwrite(oldwrite, *args, **kwargs)
205 return wrapwrite(oldwrite, *args, **kwargs)
206 setattr(ui, 'write', wrap)
206 setattr(ui, 'write', wrap)
207 return oldwrite
207 return oldwrite
208
208
209 def filterchunks(ui, originalhunks, usecurses, testfile, match,
209 def filterchunks(ui, originalhunks, usecurses, testfile, match,
210 operation=None):
210 operation=None):
211 try:
211 try:
212 if usecurses:
212 if usecurses:
213 if testfile:
213 if testfile:
214 recordfn = crecordmod.testdecorator(
214 recordfn = crecordmod.testdecorator(
215 testfile, crecordmod.testchunkselector)
215 testfile, crecordmod.testchunkselector)
216 else:
216 else:
217 recordfn = crecordmod.chunkselector
217 recordfn = crecordmod.chunkselector
218
218
219 return crecordmod.filterpatch(ui, originalhunks, recordfn,
219 return crecordmod.filterpatch(ui, originalhunks, recordfn,
220 operation)
220 operation)
221 except crecordmod.fallbackerror as e:
221 except crecordmod.fallbackerror as e:
222 ui.warn('%s\n' % e.message)
222 ui.warn('%s\n' % e.message)
223 ui.warn(_('falling back to text mode\n'))
223 ui.warn(_('falling back to text mode\n'))
224
224
225 return patch.filterpatch(ui, originalhunks, match, operation)
225 return patch.filterpatch(ui, originalhunks, match, operation)
226
226
227 def recordfilter(ui, originalhunks, match, operation=None):
227 def recordfilter(ui, originalhunks, match, operation=None):
228 """ Prompts the user to filter the originalhunks and return a list of
228 """ Prompts the user to filter the originalhunks and return a list of
229 selected hunks.
229 selected hunks.
230 *operation* is used for to build ui messages to indicate the user what
230 *operation* is used for to build ui messages to indicate the user what
231 kind of filtering they are doing: reverting, committing, shelving, etc.
231 kind of filtering they are doing: reverting, committing, shelving, etc.
232 (see patch.filterpatch).
232 (see patch.filterpatch).
233 """
233 """
234 usecurses = crecordmod.checkcurses(ui)
234 usecurses = crecordmod.checkcurses(ui)
235 testfile = ui.config('experimental', 'crecordtest')
235 testfile = ui.config('experimental', 'crecordtest')
236 oldwrite = setupwrapcolorwrite(ui)
236 oldwrite = setupwrapcolorwrite(ui)
237 try:
237 try:
238 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
238 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
239 testfile, match, operation)
239 testfile, match, operation)
240 finally:
240 finally:
241 ui.write = oldwrite
241 ui.write = oldwrite
242 return newchunks, newopts
242 return newchunks, newopts
243
243
244 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
244 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
245 filterfn, *pats, **opts):
245 filterfn, *pats, **opts):
246 opts = pycompat.byteskwargs(opts)
246 opts = pycompat.byteskwargs(opts)
247 if not ui.interactive():
247 if not ui.interactive():
248 if cmdsuggest:
248 if cmdsuggest:
249 msg = _('running non-interactively, use %s instead') % cmdsuggest
249 msg = _('running non-interactively, use %s instead') % cmdsuggest
250 else:
250 else:
251 msg = _('running non-interactively')
251 msg = _('running non-interactively')
252 raise error.Abort(msg)
252 raise error.Abort(msg)
253
253
254 # make sure username is set before going interactive
254 # make sure username is set before going interactive
255 if not opts.get('user'):
255 if not opts.get('user'):
256 ui.username() # raise exception, username not provided
256 ui.username() # raise exception, username not provided
257
257
258 def recordfunc(ui, repo, message, match, opts):
258 def recordfunc(ui, repo, message, match, opts):
259 """This is generic record driver.
259 """This is generic record driver.
260
260
261 Its job is to interactively filter local changes, and
261 Its job is to interactively filter local changes, and
262 accordingly prepare working directory into a state in which the
262 accordingly prepare working directory into a state in which the
263 job can be delegated to a non-interactive commit command such as
263 job can be delegated to a non-interactive commit command such as
264 'commit' or 'qrefresh'.
264 'commit' or 'qrefresh'.
265
265
266 After the actual job is done by non-interactive command, the
266 After the actual job is done by non-interactive command, the
267 working directory is restored to its original state.
267 working directory is restored to its original state.
268
268
269 In the end we'll record interesting changes, and everything else
269 In the end we'll record interesting changes, and everything else
270 will be left in place, so the user can continue working.
270 will be left in place, so the user can continue working.
271 """
271 """
272 if not opts.get('interactive-unshelve'):
272 if not opts.get('interactive-unshelve'):
273 checkunfinished(repo, commit=True)
273 checkunfinished(repo, commit=True)
274 wctx = repo[None]
274 wctx = repo[None]
275 merge = len(wctx.parents()) > 1
275 merge = len(wctx.parents()) > 1
276 if merge:
276 if merge:
277 raise error.Abort(_('cannot partially commit a merge '
277 raise error.Abort(_('cannot partially commit a merge '
278 '(use "hg commit" instead)'))
278 '(use "hg commit" instead)'))
279
279
280 def fail(f, msg):
280 def fail(f, msg):
281 raise error.Abort('%s: %s' % (f, msg))
281 raise error.Abort('%s: %s' % (f, msg))
282
282
283 force = opts.get('force')
283 force = opts.get('force')
284 if not force:
284 if not force:
285 vdirs = []
285 vdirs = []
286 match = matchmod.badmatch(match, fail)
286 match = matchmod.badmatch(match, fail)
287 match.explicitdir = vdirs.append
287 match.explicitdir = vdirs.append
288
288
289 status = repo.status(match=match)
289 status = repo.status(match=match)
290
290
291 overrides = {(b'ui', b'commitsubrepos'): True}
291 overrides = {(b'ui', b'commitsubrepos'): True}
292
292
293 with repo.ui.configoverride(overrides, b'record'):
293 with repo.ui.configoverride(overrides, b'record'):
294 # subrepoutil.precommit() modifies the status
294 # subrepoutil.precommit() modifies the status
295 tmpstatus = scmutil.status(copymod.copy(status[0]),
295 tmpstatus = scmutil.status(copymod.copy(status[0]),
296 copymod.copy(status[1]),
296 copymod.copy(status[1]),
297 copymod.copy(status[2]),
297 copymod.copy(status[2]),
298 copymod.copy(status[3]),
298 copymod.copy(status[3]),
299 copymod.copy(status[4]),
299 copymod.copy(status[4]),
300 copymod.copy(status[5]),
300 copymod.copy(status[5]),
301 copymod.copy(status[6]))
301 copymod.copy(status[6]))
302
302
303 # Force allows -X subrepo to skip the subrepo.
303 # Force allows -X subrepo to skip the subrepo.
304 subs, commitsubs, newstate = subrepoutil.precommit(
304 subs, commitsubs, newstate = subrepoutil.precommit(
305 repo.ui, wctx, tmpstatus, match, force=True)
305 repo.ui, wctx, tmpstatus, match, force=True)
306 for s in subs:
306 for s in subs:
307 if s in commitsubs:
307 if s in commitsubs:
308 dirtyreason = wctx.sub(s).dirtyreason(True)
308 dirtyreason = wctx.sub(s).dirtyreason(True)
309 raise error.Abort(dirtyreason)
309 raise error.Abort(dirtyreason)
310
310
311 if not force:
311 if not force:
312 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
312 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
313 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True,
313 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True,
314 section='commands',
314 section='commands',
315 configprefix='commit.interactive.')
315 configprefix='commit.interactive.')
316 diffopts.nodates = True
316 diffopts.nodates = True
317 diffopts.git = True
317 diffopts.git = True
318 diffopts.showfunc = True
318 diffopts.showfunc = True
319 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
319 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
320 originalchunks = patch.parsepatch(originaldiff)
320 originalchunks = patch.parsepatch(originaldiff)
321 match = scmutil.match(repo[None], pats)
321 match = scmutil.match(repo[None], pats)
322
322
323 # 1. filter patch, since we are intending to apply subset of it
323 # 1. filter patch, since we are intending to apply subset of it
324 try:
324 try:
325 chunks, newopts = filterfn(ui, originalchunks, match)
325 chunks, newopts = filterfn(ui, originalchunks, match)
326 except error.PatchError as err:
326 except error.PatchError as err:
327 raise error.Abort(_('error parsing patch: %s') % err)
327 raise error.Abort(_('error parsing patch: %s') % err)
328 opts.update(newopts)
328 opts.update(newopts)
329
329
330 # We need to keep a backup of files that have been newly added and
330 # We need to keep a backup of files that have been newly added and
331 # modified during the recording process because there is a previous
331 # modified during the recording process because there is a previous
332 # version without the edit in the workdir. We also will need to restore
332 # version without the edit in the workdir. We also will need to restore
333 # files that were the sources of renames so that the patch application
333 # files that were the sources of renames so that the patch application
334 # works.
334 # works.
335 newlyaddedandmodifiedfiles, alsorestore = newandmodified(chunks,
335 newlyaddedandmodifiedfiles, alsorestore = newandmodified(chunks,
336 originalchunks)
336 originalchunks)
337 contenders = set()
337 contenders = set()
338 for h in chunks:
338 for h in chunks:
339 try:
339 try:
340 contenders.update(set(h.files()))
340 contenders.update(set(h.files()))
341 except AttributeError:
341 except AttributeError:
342 pass
342 pass
343
343
344 changed = status.modified + status.added + status.removed
344 changed = status.modified + status.added + status.removed
345 newfiles = [f for f in changed if f in contenders]
345 newfiles = [f for f in changed if f in contenders]
346 if not newfiles:
346 if not newfiles:
347 ui.status(_('no changes to record\n'))
347 ui.status(_('no changes to record\n'))
348 return 0
348 return 0
349
349
350 modified = set(status.modified)
350 modified = set(status.modified)
351
351
352 # 2. backup changed files, so we can restore them in the end
352 # 2. backup changed files, so we can restore them in the end
353
353
354 if backupall:
354 if backupall:
355 tobackup = changed
355 tobackup = changed
356 else:
356 else:
357 tobackup = [f for f in newfiles if f in modified or f in
357 tobackup = [f for f in newfiles if f in modified or f in
358 newlyaddedandmodifiedfiles]
358 newlyaddedandmodifiedfiles]
359 backups = {}
359 backups = {}
360 if tobackup:
360 if tobackup:
361 backupdir = repo.vfs.join('record-backups')
361 backupdir = repo.vfs.join('record-backups')
362 try:
362 try:
363 os.mkdir(backupdir)
363 os.mkdir(backupdir)
364 except OSError as err:
364 except OSError as err:
365 if err.errno != errno.EEXIST:
365 if err.errno != errno.EEXIST:
366 raise
366 raise
367 try:
367 try:
368 # backup continues
368 # backup continues
369 for f in tobackup:
369 for f in tobackup:
370 fd, tmpname = pycompat.mkstemp(prefix=f.replace('/', '_') + '.',
370 fd, tmpname = pycompat.mkstemp(prefix=f.replace('/', '_') + '.',
371 dir=backupdir)
371 dir=backupdir)
372 os.close(fd)
372 os.close(fd)
373 ui.debug('backup %r as %r\n' % (f, tmpname))
373 ui.debug('backup %r as %r\n' % (f, tmpname))
374 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
374 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
375 backups[f] = tmpname
375 backups[f] = tmpname
376
376
377 fp = stringio()
377 fp = stringio()
378 for c in chunks:
378 for c in chunks:
379 fname = c.filename()
379 fname = c.filename()
380 if fname in backups:
380 if fname in backups:
381 c.write(fp)
381 c.write(fp)
382 dopatch = fp.tell()
382 dopatch = fp.tell()
383 fp.seek(0)
383 fp.seek(0)
384
384
385 # 2.5 optionally review / modify patch in text editor
385 # 2.5 optionally review / modify patch in text editor
386 if opts.get('review', False):
386 if opts.get('review', False):
387 patchtext = (crecordmod.diffhelptext
387 patchtext = (crecordmod.diffhelptext
388 + crecordmod.patchhelptext
388 + crecordmod.patchhelptext
389 + fp.read())
389 + fp.read())
390 reviewedpatch = ui.edit(patchtext, "",
390 reviewedpatch = ui.edit(patchtext, "",
391 action="diff",
391 action="diff",
392 repopath=repo.path)
392 repopath=repo.path)
393 fp.truncate(0)
393 fp.truncate(0)
394 fp.write(reviewedpatch)
394 fp.write(reviewedpatch)
395 fp.seek(0)
395 fp.seek(0)
396
396
397 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
397 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
398 # 3a. apply filtered patch to clean repo (clean)
398 # 3a. apply filtered patch to clean repo (clean)
399 if backups:
399 if backups:
400 # Equivalent to hg.revert
400 # Equivalent to hg.revert
401 m = scmutil.matchfiles(repo, set(backups.keys()) | alsorestore)
401 m = scmutil.matchfiles(repo, set(backups.keys()) | alsorestore)
402 mergemod.update(repo, repo.dirstate.p1(), branchmerge=False,
402 mergemod.update(repo, repo.dirstate.p1(), branchmerge=False,
403 force=True, matcher=m)
403 force=True, matcher=m)
404
404
405 # 3b. (apply)
405 # 3b. (apply)
406 if dopatch:
406 if dopatch:
407 try:
407 try:
408 ui.debug('applying patch\n')
408 ui.debug('applying patch\n')
409 ui.debug(fp.getvalue())
409 ui.debug(fp.getvalue())
410 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
410 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
411 except error.PatchError as err:
411 except error.PatchError as err:
412 raise error.Abort(pycompat.bytestr(err))
412 raise error.Abort(pycompat.bytestr(err))
413 del fp
413 del fp
414
414
415 # 4. We prepared working directory according to filtered
415 # 4. We prepared working directory according to filtered
416 # patch. Now is the time to delegate the job to
416 # patch. Now is the time to delegate the job to
417 # commit/qrefresh or the like!
417 # commit/qrefresh or the like!
418
418
419 # Make all of the pathnames absolute.
419 # Make all of the pathnames absolute.
420 newfiles = [repo.wjoin(nf) for nf in newfiles]
420 newfiles = [repo.wjoin(nf) for nf in newfiles]
421 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
421 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
422 finally:
422 finally:
423 # 5. finally restore backed-up files
423 # 5. finally restore backed-up files
424 try:
424 try:
425 dirstate = repo.dirstate
425 dirstate = repo.dirstate
426 for realname, tmpname in backups.iteritems():
426 for realname, tmpname in backups.iteritems():
427 ui.debug('restoring %r to %r\n' % (tmpname, realname))
427 ui.debug('restoring %r to %r\n' % (tmpname, realname))
428
428
429 if dirstate[realname] == 'n':
429 if dirstate[realname] == 'n':
430 # without normallookup, restoring timestamp
430 # without normallookup, restoring timestamp
431 # may cause partially committed files
431 # may cause partially committed files
432 # to be treated as unmodified
432 # to be treated as unmodified
433 dirstate.normallookup(realname)
433 dirstate.normallookup(realname)
434
434
435 # copystat=True here and above are a hack to trick any
435 # copystat=True here and above are a hack to trick any
436 # editors that have f open that we haven't modified them.
436 # editors that have f open that we haven't modified them.
437 #
437 #
438 # Also note that this racy as an editor could notice the
438 # Also note that this racy as an editor could notice the
439 # file's mtime before we've finished writing it.
439 # file's mtime before we've finished writing it.
440 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
440 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
441 os.unlink(tmpname)
441 os.unlink(tmpname)
442 if tobackup:
442 if tobackup:
443 os.rmdir(backupdir)
443 os.rmdir(backupdir)
444 except OSError:
444 except OSError:
445 pass
445 pass
446
446
447 def recordinwlock(ui, repo, message, match, opts):
447 def recordinwlock(ui, repo, message, match, opts):
448 with repo.wlock():
448 with repo.wlock():
449 return recordfunc(ui, repo, message, match, opts)
449 return recordfunc(ui, repo, message, match, opts)
450
450
451 return commit(ui, repo, recordinwlock, pats, opts)
451 return commit(ui, repo, recordinwlock, pats, opts)
452
452
453 class dirnode(object):
453 class dirnode(object):
454 """
454 """
455 Represent a directory in user working copy with information required for
455 Represent a directory in user working copy with information required for
456 the purpose of tersing its status.
456 the purpose of tersing its status.
457
457
458 path is the path to the directory, without a trailing '/'
458 path is the path to the directory, without a trailing '/'
459
459
460 statuses is a set of statuses of all files in this directory (this includes
460 statuses is a set of statuses of all files in this directory (this includes
461 all the files in all the subdirectories too)
461 all the files in all the subdirectories too)
462
462
463 files is a list of files which are direct child of this directory
463 files is a list of files which are direct child of this directory
464
464
465 subdirs is a dictionary of sub-directory name as the key and it's own
465 subdirs is a dictionary of sub-directory name as the key and it's own
466 dirnode object as the value
466 dirnode object as the value
467 """
467 """
468
468
469 def __init__(self, dirpath):
469 def __init__(self, dirpath):
470 self.path = dirpath
470 self.path = dirpath
471 self.statuses = set()
471 self.statuses = set()
472 self.files = []
472 self.files = []
473 self.subdirs = {}
473 self.subdirs = {}
474
474
475 def _addfileindir(self, filename, status):
475 def _addfileindir(self, filename, status):
476 """Add a file in this directory as a direct child."""
476 """Add a file in this directory as a direct child."""
477 self.files.append((filename, status))
477 self.files.append((filename, status))
478
478
479 def addfile(self, filename, status):
479 def addfile(self, filename, status):
480 """
480 """
481 Add a file to this directory or to its direct parent directory.
481 Add a file to this directory or to its direct parent directory.
482
482
483 If the file is not direct child of this directory, we traverse to the
483 If the file is not direct child of this directory, we traverse to the
484 directory of which this file is a direct child of and add the file
484 directory of which this file is a direct child of and add the file
485 there.
485 there.
486 """
486 """
487
487
488 # the filename contains a path separator, it means it's not the direct
488 # the filename contains a path separator, it means it's not the direct
489 # child of this directory
489 # child of this directory
490 if '/' in filename:
490 if '/' in filename:
491 subdir, filep = filename.split('/', 1)
491 subdir, filep = filename.split('/', 1)
492
492
493 # does the dirnode object for subdir exists
493 # does the dirnode object for subdir exists
494 if subdir not in self.subdirs:
494 if subdir not in self.subdirs:
495 subdirpath = pathutil.join(self.path, subdir)
495 subdirpath = pathutil.join(self.path, subdir)
496 self.subdirs[subdir] = dirnode(subdirpath)
496 self.subdirs[subdir] = dirnode(subdirpath)
497
497
498 # try adding the file in subdir
498 # try adding the file in subdir
499 self.subdirs[subdir].addfile(filep, status)
499 self.subdirs[subdir].addfile(filep, status)
500
500
501 else:
501 else:
502 self._addfileindir(filename, status)
502 self._addfileindir(filename, status)
503
503
504 if status not in self.statuses:
504 if status not in self.statuses:
505 self.statuses.add(status)
505 self.statuses.add(status)
506
506
507 def iterfilepaths(self):
507 def iterfilepaths(self):
508 """Yield (status, path) for files directly under this directory."""
508 """Yield (status, path) for files directly under this directory."""
509 for f, st in self.files:
509 for f, st in self.files:
510 yield st, pathutil.join(self.path, f)
510 yield st, pathutil.join(self.path, f)
511
511
512 def tersewalk(self, terseargs):
512 def tersewalk(self, terseargs):
513 """
513 """
514 Yield (status, path) obtained by processing the status of this
514 Yield (status, path) obtained by processing the status of this
515 dirnode.
515 dirnode.
516
516
517 terseargs is the string of arguments passed by the user with `--terse`
517 terseargs is the string of arguments passed by the user with `--terse`
518 flag.
518 flag.
519
519
520 Following are the cases which can happen:
520 Following are the cases which can happen:
521
521
522 1) All the files in the directory (including all the files in its
522 1) All the files in the directory (including all the files in its
523 subdirectories) share the same status and the user has asked us to terse
523 subdirectories) share the same status and the user has asked us to terse
524 that status. -> yield (status, dirpath). dirpath will end in '/'.
524 that status. -> yield (status, dirpath). dirpath will end in '/'.
525
525
526 2) Otherwise, we do following:
526 2) Otherwise, we do following:
527
527
528 a) Yield (status, filepath) for all the files which are in this
528 a) Yield (status, filepath) for all the files which are in this
529 directory (only the ones in this directory, not the subdirs)
529 directory (only the ones in this directory, not the subdirs)
530
530
531 b) Recurse the function on all the subdirectories of this
531 b) Recurse the function on all the subdirectories of this
532 directory
532 directory
533 """
533 """
534
534
535 if len(self.statuses) == 1:
535 if len(self.statuses) == 1:
536 onlyst = self.statuses.pop()
536 onlyst = self.statuses.pop()
537
537
538 # Making sure we terse only when the status abbreviation is
538 # Making sure we terse only when the status abbreviation is
539 # passed as terse argument
539 # passed as terse argument
540 if onlyst in terseargs:
540 if onlyst in terseargs:
541 yield onlyst, self.path + '/'
541 yield onlyst, self.path + '/'
542 return
542 return
543
543
544 # add the files to status list
544 # add the files to status list
545 for st, fpath in self.iterfilepaths():
545 for st, fpath in self.iterfilepaths():
546 yield st, fpath
546 yield st, fpath
547
547
548 #recurse on the subdirs
548 #recurse on the subdirs
549 for dirobj in self.subdirs.values():
549 for dirobj in self.subdirs.values():
550 for st, fpath in dirobj.tersewalk(terseargs):
550 for st, fpath in dirobj.tersewalk(terseargs):
551 yield st, fpath
551 yield st, fpath
552
552
553 def tersedir(statuslist, terseargs):
553 def tersedir(statuslist, terseargs):
554 """
554 """
555 Terse the status if all the files in a directory shares the same status.
555 Terse the status if all the files in a directory shares the same status.
556
556
557 statuslist is scmutil.status() object which contains a list of files for
557 statuslist is scmutil.status() object which contains a list of files for
558 each status.
558 each status.
559 terseargs is string which is passed by the user as the argument to `--terse`
559 terseargs is string which is passed by the user as the argument to `--terse`
560 flag.
560 flag.
561
561
562 The function makes a tree of objects of dirnode class, and at each node it
562 The function makes a tree of objects of dirnode class, and at each node it
563 stores the information required to know whether we can terse a certain
563 stores the information required to know whether we can terse a certain
564 directory or not.
564 directory or not.
565 """
565 """
566 # the order matters here as that is used to produce final list
566 # the order matters here as that is used to produce final list
567 allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c')
567 allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c')
568
568
569 # checking the argument validity
569 # checking the argument validity
570 for s in pycompat.bytestr(terseargs):
570 for s in pycompat.bytestr(terseargs):
571 if s not in allst:
571 if s not in allst:
572 raise error.Abort(_("'%s' not recognized") % s)
572 raise error.Abort(_("'%s' not recognized") % s)
573
573
574 # creating a dirnode object for the root of the repo
574 # creating a dirnode object for the root of the repo
575 rootobj = dirnode('')
575 rootobj = dirnode('')
576 pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown',
576 pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown',
577 'ignored', 'removed')
577 'ignored', 'removed')
578
578
579 tersedict = {}
579 tersedict = {}
580 for attrname in pstatus:
580 for attrname in pstatus:
581 statuschar = attrname[0:1]
581 statuschar = attrname[0:1]
582 for f in getattr(statuslist, attrname):
582 for f in getattr(statuslist, attrname):
583 rootobj.addfile(f, statuschar)
583 rootobj.addfile(f, statuschar)
584 tersedict[statuschar] = []
584 tersedict[statuschar] = []
585
585
586 # we won't be tersing the root dir, so add files in it
586 # we won't be tersing the root dir, so add files in it
587 for st, fpath in rootobj.iterfilepaths():
587 for st, fpath in rootobj.iterfilepaths():
588 tersedict[st].append(fpath)
588 tersedict[st].append(fpath)
589
589
590 # process each sub-directory and build tersedict
590 # process each sub-directory and build tersedict
591 for subdir in rootobj.subdirs.values():
591 for subdir in rootobj.subdirs.values():
592 for st, f in subdir.tersewalk(terseargs):
592 for st, f in subdir.tersewalk(terseargs):
593 tersedict[st].append(f)
593 tersedict[st].append(f)
594
594
595 tersedlist = []
595 tersedlist = []
596 for st in allst:
596 for st in allst:
597 tersedict[st].sort()
597 tersedict[st].sort()
598 tersedlist.append(tersedict[st])
598 tersedlist.append(tersedict[st])
599
599
600 return tersedlist
600 return tersedlist
601
601
602 def _commentlines(raw):
602 def _commentlines(raw):
603 '''Surround lineswith a comment char and a new line'''
603 '''Surround lineswith a comment char and a new line'''
604 lines = raw.splitlines()
604 lines = raw.splitlines()
605 commentedlines = ['# %s' % line for line in lines]
605 commentedlines = ['# %s' % line for line in lines]
606 return '\n'.join(commentedlines) + '\n'
606 return '\n'.join(commentedlines) + '\n'
607
607
608 def _conflictsmsg(repo):
608 def _conflictsmsg(repo):
609 mergestate = mergemod.mergestate.read(repo)
609 mergestate = mergemod.mergestate.read(repo)
610 if not mergestate.active():
610 if not mergestate.active():
611 return
611 return
612
612
613 m = scmutil.match(repo[None])
613 m = scmutil.match(repo[None])
614 unresolvedlist = [f for f in mergestate.unresolved() if m(f)]
614 unresolvedlist = [f for f in mergestate.unresolved() if m(f)]
615 if unresolvedlist:
615 if unresolvedlist:
616 mergeliststr = '\n'.join(
616 mergeliststr = '\n'.join(
617 [' %s' % util.pathto(repo.root, encoding.getcwd(), path)
617 [' %s' % util.pathto(repo.root, encoding.getcwd(), path)
618 for path in sorted(unresolvedlist)])
618 for path in sorted(unresolvedlist)])
619 msg = _('''Unresolved merge conflicts:
619 msg = _('''Unresolved merge conflicts:
620
620
621 %s
621 %s
622
622
623 To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
623 To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
624 else:
624 else:
625 msg = _('No unresolved merge conflicts.')
625 msg = _('No unresolved merge conflicts.')
626
626
627 return _commentlines(msg)
627 return _commentlines(msg)
628
628
629 def morestatus(repo, fm):
629 def morestatus(repo, fm):
630 statetuple = statemod.getrepostate(repo)
630 statetuple = statemod.getrepostate(repo)
631 label = 'status.morestatus'
631 label = 'status.morestatus'
632 if statetuple:
632 if statetuple:
633 state, helpfulmsg = statetuple
633 state, helpfulmsg = statetuple
634 statemsg = _('The repository is in an unfinished *%s* state.') % state
634 statemsg = _('The repository is in an unfinished *%s* state.') % state
635 fm.plain('%s\n' % _commentlines(statemsg), label=label)
635 fm.plain('%s\n' % _commentlines(statemsg), label=label)
636 conmsg = _conflictsmsg(repo)
636 conmsg = _conflictsmsg(repo)
637 if conmsg:
637 if conmsg:
638 fm.plain('%s\n' % conmsg, label=label)
638 fm.plain('%s\n' % conmsg, label=label)
639 if helpfulmsg:
639 if helpfulmsg:
640 fm.plain('%s\n' % _commentlines(helpfulmsg), label=label)
640 fm.plain('%s\n' % _commentlines(helpfulmsg), label=label)
641
641
642 def findpossible(cmd, table, strict=False):
642 def findpossible(cmd, table, strict=False):
643 """
643 """
644 Return cmd -> (aliases, command table entry)
644 Return cmd -> (aliases, command table entry)
645 for each matching command.
645 for each matching command.
646 Return debug commands (or their aliases) only if no normal command matches.
646 Return debug commands (or their aliases) only if no normal command matches.
647 """
647 """
648 choice = {}
648 choice = {}
649 debugchoice = {}
649 debugchoice = {}
650
650
651 if cmd in table:
651 if cmd in table:
652 # short-circuit exact matches, "log" alias beats "log|history"
652 # short-circuit exact matches, "log" alias beats "log|history"
653 keys = [cmd]
653 keys = [cmd]
654 else:
654 else:
655 keys = table.keys()
655 keys = table.keys()
656
656
657 allcmds = []
657 allcmds = []
658 for e in keys:
658 for e in keys:
659 aliases = parsealiases(e)
659 aliases = parsealiases(e)
660 allcmds.extend(aliases)
660 allcmds.extend(aliases)
661 found = None
661 found = None
662 if cmd in aliases:
662 if cmd in aliases:
663 found = cmd
663 found = cmd
664 elif not strict:
664 elif not strict:
665 for a in aliases:
665 for a in aliases:
666 if a.startswith(cmd):
666 if a.startswith(cmd):
667 found = a
667 found = a
668 break
668 break
669 if found is not None:
669 if found is not None:
670 if aliases[0].startswith("debug") or found.startswith("debug"):
670 if aliases[0].startswith("debug") or found.startswith("debug"):
671 debugchoice[found] = (aliases, table[e])
671 debugchoice[found] = (aliases, table[e])
672 else:
672 else:
673 choice[found] = (aliases, table[e])
673 choice[found] = (aliases, table[e])
674
674
675 if not choice and debugchoice:
675 if not choice and debugchoice:
676 choice = debugchoice
676 choice = debugchoice
677
677
678 return choice, allcmds
678 return choice, allcmds
679
679
680 def findcmd(cmd, table, strict=True):
680 def findcmd(cmd, table, strict=True):
681 """Return (aliases, command table entry) for command string."""
681 """Return (aliases, command table entry) for command string."""
682 choice, allcmds = findpossible(cmd, table, strict)
682 choice, allcmds = findpossible(cmd, table, strict)
683
683
684 if cmd in choice:
684 if cmd in choice:
685 return choice[cmd]
685 return choice[cmd]
686
686
687 if len(choice) > 1:
687 if len(choice) > 1:
688 clist = sorted(choice)
688 clist = sorted(choice)
689 raise error.AmbiguousCommand(cmd, clist)
689 raise error.AmbiguousCommand(cmd, clist)
690
690
691 if choice:
691 if choice:
692 return list(choice.values())[0]
692 return list(choice.values())[0]
693
693
694 raise error.UnknownCommand(cmd, allcmds)
694 raise error.UnknownCommand(cmd, allcmds)
695
695
696 def changebranch(ui, repo, revs, label):
696 def changebranch(ui, repo, revs, label):
697 """ Change the branch name of given revs to label """
697 """ Change the branch name of given revs to label """
698
698
699 with repo.wlock(), repo.lock(), repo.transaction('branches'):
699 with repo.wlock(), repo.lock(), repo.transaction('branches'):
700 # abort in case of uncommitted merge or dirty wdir
700 # abort in case of uncommitted merge or dirty wdir
701 bailifchanged(repo)
701 bailifchanged(repo)
702 revs = scmutil.revrange(repo, revs)
702 revs = scmutil.revrange(repo, revs)
703 if not revs:
703 if not revs:
704 raise error.Abort("empty revision set")
704 raise error.Abort("empty revision set")
705 roots = repo.revs('roots(%ld)', revs)
705 roots = repo.revs('roots(%ld)', revs)
706 if len(roots) > 1:
706 if len(roots) > 1:
707 raise error.Abort(_("cannot change branch of non-linear revisions"))
707 raise error.Abort(_("cannot change branch of non-linear revisions"))
708 rewriteutil.precheck(repo, revs, 'change branch of')
708 rewriteutil.precheck(repo, revs, 'change branch of')
709
709
710 root = repo[roots.first()]
710 root = repo[roots.first()]
711 rpb = {parent.branch() for parent in root.parents()}
711 rpb = {parent.branch() for parent in root.parents()}
712 if label not in rpb and label in repo.branchmap():
712 if label not in rpb and label in repo.branchmap():
713 raise error.Abort(_("a branch of the same name already exists"))
713 raise error.Abort(_("a branch of the same name already exists"))
714
714
715 if repo.revs('obsolete() and %ld', revs):
715 if repo.revs('obsolete() and %ld', revs):
716 raise error.Abort(_("cannot change branch of a obsolete changeset"))
716 raise error.Abort(_("cannot change branch of a obsolete changeset"))
717
717
718 # make sure only topological heads
718 # make sure only topological heads
719 if repo.revs('heads(%ld) - head()', revs):
719 if repo.revs('heads(%ld) - head()', revs):
720 raise error.Abort(_("cannot change branch in middle of a stack"))
720 raise error.Abort(_("cannot change branch in middle of a stack"))
721
721
722 replacements = {}
722 replacements = {}
723 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
723 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
724 # mercurial.subrepo -> mercurial.cmdutil
724 # mercurial.subrepo -> mercurial.cmdutil
725 from . import context
725 from . import context
726 for rev in revs:
726 for rev in revs:
727 ctx = repo[rev]
727 ctx = repo[rev]
728 oldbranch = ctx.branch()
728 oldbranch = ctx.branch()
729 # check if ctx has same branch
729 # check if ctx has same branch
730 if oldbranch == label:
730 if oldbranch == label:
731 continue
731 continue
732
732
733 def filectxfn(repo, newctx, path):
733 def filectxfn(repo, newctx, path):
734 try:
734 try:
735 return ctx[path]
735 return ctx[path]
736 except error.ManifestLookupError:
736 except error.ManifestLookupError:
737 return None
737 return None
738
738
739 ui.debug("changing branch of '%s' from '%s' to '%s'\n"
739 ui.debug("changing branch of '%s' from '%s' to '%s'\n"
740 % (hex(ctx.node()), oldbranch, label))
740 % (hex(ctx.node()), oldbranch, label))
741 extra = ctx.extra()
741 extra = ctx.extra()
742 extra['branch_change'] = hex(ctx.node())
742 extra['branch_change'] = hex(ctx.node())
743 # While changing branch of set of linear commits, make sure that
743 # While changing branch of set of linear commits, make sure that
744 # we base our commits on new parent rather than old parent which
744 # we base our commits on new parent rather than old parent which
745 # was obsoleted while changing the branch
745 # was obsoleted while changing the branch
746 p1 = ctx.p1().node()
746 p1 = ctx.p1().node()
747 p2 = ctx.p2().node()
747 p2 = ctx.p2().node()
748 if p1 in replacements:
748 if p1 in replacements:
749 p1 = replacements[p1][0]
749 p1 = replacements[p1][0]
750 if p2 in replacements:
750 if p2 in replacements:
751 p2 = replacements[p2][0]
751 p2 = replacements[p2][0]
752
752
753 mc = context.memctx(repo, (p1, p2),
753 mc = context.memctx(repo, (p1, p2),
754 ctx.description(),
754 ctx.description(),
755 ctx.files(),
755 ctx.files(),
756 filectxfn,
756 filectxfn,
757 user=ctx.user(),
757 user=ctx.user(),
758 date=ctx.date(),
758 date=ctx.date(),
759 extra=extra,
759 extra=extra,
760 branch=label)
760 branch=label)
761
761
762 newnode = repo.commitctx(mc)
762 newnode = repo.commitctx(mc)
763 replacements[ctx.node()] = (newnode,)
763 replacements[ctx.node()] = (newnode,)
764 ui.debug('new node id is %s\n' % hex(newnode))
764 ui.debug('new node id is %s\n' % hex(newnode))
765
765
766 # create obsmarkers and move bookmarks
766 # create obsmarkers and move bookmarks
767 scmutil.cleanupnodes(repo, replacements, 'branch-change', fixphase=True)
767 scmutil.cleanupnodes(repo, replacements, 'branch-change', fixphase=True)
768
768
769 # move the working copy too
769 # move the working copy too
770 wctx = repo[None]
770 wctx = repo[None]
771 # in-progress merge is a bit too complex for now.
771 # in-progress merge is a bit too complex for now.
772 if len(wctx.parents()) == 1:
772 if len(wctx.parents()) == 1:
773 newid = replacements.get(wctx.p1().node())
773 newid = replacements.get(wctx.p1().node())
774 if newid is not None:
774 if newid is not None:
775 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
775 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
776 # mercurial.cmdutil
776 # mercurial.cmdutil
777 from . import hg
777 from . import hg
778 hg.update(repo, newid[0], quietempty=True)
778 hg.update(repo, newid[0], quietempty=True)
779
779
780 ui.status(_("changed branch on %d changesets\n") % len(replacements))
780 ui.status(_("changed branch on %d changesets\n") % len(replacements))
781
781
782 def findrepo(p):
782 def findrepo(p):
783 while not os.path.isdir(os.path.join(p, ".hg")):
783 while not os.path.isdir(os.path.join(p, ".hg")):
784 oldp, p = p, os.path.dirname(p)
784 oldp, p = p, os.path.dirname(p)
785 if p == oldp:
785 if p == oldp:
786 return None
786 return None
787
787
788 return p
788 return p
789
789
790 def bailifchanged(repo, merge=True, hint=None):
790 def bailifchanged(repo, merge=True, hint=None):
791 """ enforce the precondition that working directory must be clean.
791 """ enforce the precondition that working directory must be clean.
792
792
793 'merge' can be set to false if a pending uncommitted merge should be
793 'merge' can be set to false if a pending uncommitted merge should be
794 ignored (such as when 'update --check' runs).
794 ignored (such as when 'update --check' runs).
795
795
796 'hint' is the usual hint given to Abort exception.
796 'hint' is the usual hint given to Abort exception.
797 """
797 """
798
798
799 if merge and repo.dirstate.p2() != nullid:
799 if merge and repo.dirstate.p2() != nullid:
800 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
800 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
801 modified, added, removed, deleted = repo.status()[:4]
801 modified, added, removed, deleted = repo.status()[:4]
802 if modified or added or removed or deleted:
802 if modified or added or removed or deleted:
803 raise error.Abort(_('uncommitted changes'), hint=hint)
803 raise error.Abort(_('uncommitted changes'), hint=hint)
804 ctx = repo[None]
804 ctx = repo[None]
805 for s in sorted(ctx.substate):
805 for s in sorted(ctx.substate):
806 ctx.sub(s).bailifchanged(hint=hint)
806 ctx.sub(s).bailifchanged(hint=hint)
807
807
808 def logmessage(ui, opts):
808 def logmessage(ui, opts):
809 """ get the log message according to -m and -l option """
809 """ get the log message according to -m and -l option """
810 message = opts.get('message')
810 message = opts.get('message')
811 logfile = opts.get('logfile')
811 logfile = opts.get('logfile')
812
812
813 if message and logfile:
813 if message and logfile:
814 raise error.Abort(_('options --message and --logfile are mutually '
814 raise error.Abort(_('options --message and --logfile are mutually '
815 'exclusive'))
815 'exclusive'))
816 if not message and logfile:
816 if not message and logfile:
817 try:
817 try:
818 if isstdiofilename(logfile):
818 if isstdiofilename(logfile):
819 message = ui.fin.read()
819 message = ui.fin.read()
820 else:
820 else:
821 message = '\n'.join(util.readfile(logfile).splitlines())
821 message = '\n'.join(util.readfile(logfile).splitlines())
822 except IOError as inst:
822 except IOError as inst:
823 raise error.Abort(_("can't read commit message '%s': %s") %
823 raise error.Abort(_("can't read commit message '%s': %s") %
824 (logfile, encoding.strtolocal(inst.strerror)))
824 (logfile, encoding.strtolocal(inst.strerror)))
825 return message
825 return message
826
826
827 def mergeeditform(ctxorbool, baseformname):
827 def mergeeditform(ctxorbool, baseformname):
828 """return appropriate editform name (referencing a committemplate)
828 """return appropriate editform name (referencing a committemplate)
829
829
830 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
830 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
831 merging is committed.
831 merging is committed.
832
832
833 This returns baseformname with '.merge' appended if it is a merge,
833 This returns baseformname with '.merge' appended if it is a merge,
834 otherwise '.normal' is appended.
834 otherwise '.normal' is appended.
835 """
835 """
836 if isinstance(ctxorbool, bool):
836 if isinstance(ctxorbool, bool):
837 if ctxorbool:
837 if ctxorbool:
838 return baseformname + ".merge"
838 return baseformname + ".merge"
839 elif len(ctxorbool.parents()) > 1:
839 elif len(ctxorbool.parents()) > 1:
840 return baseformname + ".merge"
840 return baseformname + ".merge"
841
841
842 return baseformname + ".normal"
842 return baseformname + ".normal"
843
843
844 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
844 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
845 editform='', **opts):
845 editform='', **opts):
846 """get appropriate commit message editor according to '--edit' option
846 """get appropriate commit message editor according to '--edit' option
847
847
848 'finishdesc' is a function to be called with edited commit message
848 'finishdesc' is a function to be called with edited commit message
849 (= 'description' of the new changeset) just after editing, but
849 (= 'description' of the new changeset) just after editing, but
850 before checking empty-ness. It should return actual text to be
850 before checking empty-ness. It should return actual text to be
851 stored into history. This allows to change description before
851 stored into history. This allows to change description before
852 storing.
852 storing.
853
853
854 'extramsg' is a extra message to be shown in the editor instead of
854 'extramsg' is a extra message to be shown in the editor instead of
855 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
855 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
856 is automatically added.
856 is automatically added.
857
857
858 'editform' is a dot-separated list of names, to distinguish
858 'editform' is a dot-separated list of names, to distinguish
859 the purpose of commit text editing.
859 the purpose of commit text editing.
860
860
861 'getcommiteditor' returns 'commitforceeditor' regardless of
861 'getcommiteditor' returns 'commitforceeditor' regardless of
862 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
862 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
863 they are specific for usage in MQ.
863 they are specific for usage in MQ.
864 """
864 """
865 if edit or finishdesc or extramsg:
865 if edit or finishdesc or extramsg:
866 return lambda r, c, s: commitforceeditor(r, c, s,
866 return lambda r, c, s: commitforceeditor(r, c, s,
867 finishdesc=finishdesc,
867 finishdesc=finishdesc,
868 extramsg=extramsg,
868 extramsg=extramsg,
869 editform=editform)
869 editform=editform)
870 elif editform:
870 elif editform:
871 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
871 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
872 else:
872 else:
873 return commiteditor
873 return commiteditor
874
874
875 def _escapecommandtemplate(tmpl):
875 def _escapecommandtemplate(tmpl):
876 parts = []
876 parts = []
877 for typ, start, end in templater.scantemplate(tmpl, raw=True):
877 for typ, start, end in templater.scantemplate(tmpl, raw=True):
878 if typ == b'string':
878 if typ == b'string':
879 parts.append(stringutil.escapestr(tmpl[start:end]))
879 parts.append(stringutil.escapestr(tmpl[start:end]))
880 else:
880 else:
881 parts.append(tmpl[start:end])
881 parts.append(tmpl[start:end])
882 return b''.join(parts)
882 return b''.join(parts)
883
883
884 def rendercommandtemplate(ui, tmpl, props):
884 def rendercommandtemplate(ui, tmpl, props):
885 r"""Expand a literal template 'tmpl' in a way suitable for command line
885 r"""Expand a literal template 'tmpl' in a way suitable for command line
886
886
887 '\' in outermost string is not taken as an escape character because it
887 '\' in outermost string is not taken as an escape character because it
888 is a directory separator on Windows.
888 is a directory separator on Windows.
889
889
890 >>> from . import ui as uimod
890 >>> from . import ui as uimod
891 >>> ui = uimod.ui()
891 >>> ui = uimod.ui()
892 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
892 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
893 'c:\\foo'
893 'c:\\foo'
894 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
894 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
895 'c:{path}'
895 'c:{path}'
896 """
896 """
897 if not tmpl:
897 if not tmpl:
898 return tmpl
898 return tmpl
899 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
899 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
900 return t.renderdefault(props)
900 return t.renderdefault(props)
901
901
902 def rendertemplate(ctx, tmpl, props=None):
902 def rendertemplate(ctx, tmpl, props=None):
903 """Expand a literal template 'tmpl' byte-string against one changeset
903 """Expand a literal template 'tmpl' byte-string against one changeset
904
904
905 Each props item must be a stringify-able value or a callable returning
905 Each props item must be a stringify-able value or a callable returning
906 such value, i.e. no bare list nor dict should be passed.
906 such value, i.e. no bare list nor dict should be passed.
907 """
907 """
908 repo = ctx.repo()
908 repo = ctx.repo()
909 tres = formatter.templateresources(repo.ui, repo)
909 tres = formatter.templateresources(repo.ui, repo)
910 t = formatter.maketemplater(repo.ui, tmpl, defaults=templatekw.keywords,
910 t = formatter.maketemplater(repo.ui, tmpl, defaults=templatekw.keywords,
911 resources=tres)
911 resources=tres)
912 mapping = {'ctx': ctx}
912 mapping = {'ctx': ctx}
913 if props:
913 if props:
914 mapping.update(props)
914 mapping.update(props)
915 return t.renderdefault(mapping)
915 return t.renderdefault(mapping)
916
916
917 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
917 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
918 r"""Convert old-style filename format string to template string
918 r"""Convert old-style filename format string to template string
919
919
920 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
920 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
921 'foo-{reporoot|basename}-{seqno}.patch'
921 'foo-{reporoot|basename}-{seqno}.patch'
922 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
922 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
923 '{rev}{tags % "{tag}"}{node}'
923 '{rev}{tags % "{tag}"}{node}'
924
924
925 '\' in outermost strings has to be escaped because it is a directory
925 '\' in outermost strings has to be escaped because it is a directory
926 separator on Windows:
926 separator on Windows:
927
927
928 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
928 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
929 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
929 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
930 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
930 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
931 '\\\\\\\\foo\\\\bar.patch'
931 '\\\\\\\\foo\\\\bar.patch'
932 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
932 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
933 '\\\\{tags % "{tag}"}'
933 '\\\\{tags % "{tag}"}'
934
934
935 but inner strings follow the template rules (i.e. '\' is taken as an
935 but inner strings follow the template rules (i.e. '\' is taken as an
936 escape character):
936 escape character):
937
937
938 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
938 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
939 '{"c:\\tmp"}'
939 '{"c:\\tmp"}'
940 """
940 """
941 expander = {
941 expander = {
942 b'H': b'{node}',
942 b'H': b'{node}',
943 b'R': b'{rev}',
943 b'R': b'{rev}',
944 b'h': b'{node|short}',
944 b'h': b'{node|short}',
945 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
945 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
946 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
946 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
947 b'%': b'%',
947 b'%': b'%',
948 b'b': b'{reporoot|basename}',
948 b'b': b'{reporoot|basename}',
949 }
949 }
950 if total is not None:
950 if total is not None:
951 expander[b'N'] = b'{total}'
951 expander[b'N'] = b'{total}'
952 if seqno is not None:
952 if seqno is not None:
953 expander[b'n'] = b'{seqno}'
953 expander[b'n'] = b'{seqno}'
954 if total is not None and seqno is not None:
954 if total is not None and seqno is not None:
955 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
955 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
956 if pathname is not None:
956 if pathname is not None:
957 expander[b's'] = b'{pathname|basename}'
957 expander[b's'] = b'{pathname|basename}'
958 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
958 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
959 expander[b'p'] = b'{pathname}'
959 expander[b'p'] = b'{pathname}'
960
960
961 newname = []
961 newname = []
962 for typ, start, end in templater.scantemplate(pat, raw=True):
962 for typ, start, end in templater.scantemplate(pat, raw=True):
963 if typ != b'string':
963 if typ != b'string':
964 newname.append(pat[start:end])
964 newname.append(pat[start:end])
965 continue
965 continue
966 i = start
966 i = start
967 while i < end:
967 while i < end:
968 n = pat.find(b'%', i, end)
968 n = pat.find(b'%', i, end)
969 if n < 0:
969 if n < 0:
970 newname.append(stringutil.escapestr(pat[i:end]))
970 newname.append(stringutil.escapestr(pat[i:end]))
971 break
971 break
972 newname.append(stringutil.escapestr(pat[i:n]))
972 newname.append(stringutil.escapestr(pat[i:n]))
973 if n + 2 > end:
973 if n + 2 > end:
974 raise error.Abort(_("incomplete format spec in output "
974 raise error.Abort(_("incomplete format spec in output "
975 "filename"))
975 "filename"))
976 c = pat[n + 1:n + 2]
976 c = pat[n + 1:n + 2]
977 i = n + 2
977 i = n + 2
978 try:
978 try:
979 newname.append(expander[c])
979 newname.append(expander[c])
980 except KeyError:
980 except KeyError:
981 raise error.Abort(_("invalid format spec '%%%s' in output "
981 raise error.Abort(_("invalid format spec '%%%s' in output "
982 "filename") % c)
982 "filename") % c)
983 return ''.join(newname)
983 return ''.join(newname)
984
984
985 def makefilename(ctx, pat, **props):
985 def makefilename(ctx, pat, **props):
986 if not pat:
986 if not pat:
987 return pat
987 return pat
988 tmpl = _buildfntemplate(pat, **props)
988 tmpl = _buildfntemplate(pat, **props)
989 # BUG: alias expansion shouldn't be made against template fragments
989 # BUG: alias expansion shouldn't be made against template fragments
990 # rewritten from %-format strings, but we have no easy way to partially
990 # rewritten from %-format strings, but we have no easy way to partially
991 # disable the expansion.
991 # disable the expansion.
992 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
992 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
993
993
994 def isstdiofilename(pat):
994 def isstdiofilename(pat):
995 """True if the given pat looks like a filename denoting stdin/stdout"""
995 """True if the given pat looks like a filename denoting stdin/stdout"""
996 return not pat or pat == '-'
996 return not pat or pat == '-'
997
997
998 class _unclosablefile(object):
998 class _unclosablefile(object):
999 def __init__(self, fp):
999 def __init__(self, fp):
1000 self._fp = fp
1000 self._fp = fp
1001
1001
1002 def close(self):
1002 def close(self):
1003 pass
1003 pass
1004
1004
1005 def __iter__(self):
1005 def __iter__(self):
1006 return iter(self._fp)
1006 return iter(self._fp)
1007
1007
1008 def __getattr__(self, attr):
1008 def __getattr__(self, attr):
1009 return getattr(self._fp, attr)
1009 return getattr(self._fp, attr)
1010
1010
1011 def __enter__(self):
1011 def __enter__(self):
1012 return self
1012 return self
1013
1013
1014 def __exit__(self, exc_type, exc_value, exc_tb):
1014 def __exit__(self, exc_type, exc_value, exc_tb):
1015 pass
1015 pass
1016
1016
1017 def makefileobj(ctx, pat, mode='wb', **props):
1017 def makefileobj(ctx, pat, mode='wb', **props):
1018 writable = mode not in ('r', 'rb')
1018 writable = mode not in ('r', 'rb')
1019
1019
1020 if isstdiofilename(pat):
1020 if isstdiofilename(pat):
1021 repo = ctx.repo()
1021 repo = ctx.repo()
1022 if writable:
1022 if writable:
1023 fp = repo.ui.fout
1023 fp = repo.ui.fout
1024 else:
1024 else:
1025 fp = repo.ui.fin
1025 fp = repo.ui.fin
1026 return _unclosablefile(fp)
1026 return _unclosablefile(fp)
1027 fn = makefilename(ctx, pat, **props)
1027 fn = makefilename(ctx, pat, **props)
1028 return open(fn, mode)
1028 return open(fn, mode)
1029
1029
1030 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1030 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1031 """opens the changelog, manifest, a filelog or a given revlog"""
1031 """opens the changelog, manifest, a filelog or a given revlog"""
1032 cl = opts['changelog']
1032 cl = opts['changelog']
1033 mf = opts['manifest']
1033 mf = opts['manifest']
1034 dir = opts['dir']
1034 dir = opts['dir']
1035 msg = None
1035 msg = None
1036 if cl and mf:
1036 if cl and mf:
1037 msg = _('cannot specify --changelog and --manifest at the same time')
1037 msg = _('cannot specify --changelog and --manifest at the same time')
1038 elif cl and dir:
1038 elif cl and dir:
1039 msg = _('cannot specify --changelog and --dir at the same time')
1039 msg = _('cannot specify --changelog and --dir at the same time')
1040 elif cl or mf or dir:
1040 elif cl or mf or dir:
1041 if file_:
1041 if file_:
1042 msg = _('cannot specify filename with --changelog or --manifest')
1042 msg = _('cannot specify filename with --changelog or --manifest')
1043 elif not repo:
1043 elif not repo:
1044 msg = _('cannot specify --changelog or --manifest or --dir '
1044 msg = _('cannot specify --changelog or --manifest or --dir '
1045 'without a repository')
1045 'without a repository')
1046 if msg:
1046 if msg:
1047 raise error.Abort(msg)
1047 raise error.Abort(msg)
1048
1048
1049 r = None
1049 r = None
1050 if repo:
1050 if repo:
1051 if cl:
1051 if cl:
1052 r = repo.unfiltered().changelog
1052 r = repo.unfiltered().changelog
1053 elif dir:
1053 elif dir:
1054 if 'treemanifest' not in repo.requirements:
1054 if 'treemanifest' not in repo.requirements:
1055 raise error.Abort(_("--dir can only be used on repos with "
1055 raise error.Abort(_("--dir can only be used on repos with "
1056 "treemanifest enabled"))
1056 "treemanifest enabled"))
1057 if not dir.endswith('/'):
1057 if not dir.endswith('/'):
1058 dir = dir + '/'
1058 dir = dir + '/'
1059 dirlog = repo.manifestlog.getstorage(dir)
1059 dirlog = repo.manifestlog.getstorage(dir)
1060 if len(dirlog):
1060 if len(dirlog):
1061 r = dirlog
1061 r = dirlog
1062 elif mf:
1062 elif mf:
1063 r = repo.manifestlog.getstorage(b'')
1063 r = repo.manifestlog.getstorage(b'')
1064 elif file_:
1064 elif file_:
1065 filelog = repo.file(file_)
1065 filelog = repo.file(file_)
1066 if len(filelog):
1066 if len(filelog):
1067 r = filelog
1067 r = filelog
1068
1068
1069 # Not all storage may be revlogs. If requested, try to return an actual
1069 # Not all storage may be revlogs. If requested, try to return an actual
1070 # revlog instance.
1070 # revlog instance.
1071 if returnrevlog:
1071 if returnrevlog:
1072 if isinstance(r, revlog.revlog):
1072 if isinstance(r, revlog.revlog):
1073 pass
1073 pass
1074 elif util.safehasattr(r, '_revlog'):
1074 elif util.safehasattr(r, '_revlog'):
1075 r = r._revlog
1075 r = r._revlog
1076 elif r is not None:
1076 elif r is not None:
1077 raise error.Abort(_('%r does not appear to be a revlog') % r)
1077 raise error.Abort(_('%r does not appear to be a revlog') % r)
1078
1078
1079 if not r:
1079 if not r:
1080 if not returnrevlog:
1080 if not returnrevlog:
1081 raise error.Abort(_('cannot give path to non-revlog'))
1081 raise error.Abort(_('cannot give path to non-revlog'))
1082
1082
1083 if not file_:
1083 if not file_:
1084 raise error.CommandError(cmd, _('invalid arguments'))
1084 raise error.CommandError(cmd, _('invalid arguments'))
1085 if not os.path.isfile(file_):
1085 if not os.path.isfile(file_):
1086 raise error.Abort(_("revlog '%s' not found") % file_)
1086 raise error.Abort(_("revlog '%s' not found") % file_)
1087 r = revlog.revlog(vfsmod.vfs(encoding.getcwd(), audit=False),
1087 r = revlog.revlog(vfsmod.vfs(encoding.getcwd(), audit=False),
1088 file_[:-2] + ".i")
1088 file_[:-2] + ".i")
1089 return r
1089 return r
1090
1090
1091 def openrevlog(repo, cmd, file_, opts):
1091 def openrevlog(repo, cmd, file_, opts):
1092 """Obtain a revlog backing storage of an item.
1092 """Obtain a revlog backing storage of an item.
1093
1093
1094 This is similar to ``openstorage()`` except it always returns a revlog.
1094 This is similar to ``openstorage()`` except it always returns a revlog.
1095
1095
1096 In most cases, a caller cares about the main storage object - not the
1096 In most cases, a caller cares about the main storage object - not the
1097 revlog backing it. Therefore, this function should only be used by code
1097 revlog backing it. Therefore, this function should only be used by code
1098 that needs to examine low-level revlog implementation details. e.g. debug
1098 that needs to examine low-level revlog implementation details. e.g. debug
1099 commands.
1099 commands.
1100 """
1100 """
1101 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1101 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1102
1102
1103 def copy(ui, repo, pats, opts, rename=False):
1103 def copy(ui, repo, pats, opts, rename=False):
1104 # called with the repo lock held
1104 # called with the repo lock held
1105 #
1105 #
1106 # hgsep => pathname that uses "/" to separate directories
1106 # hgsep => pathname that uses "/" to separate directories
1107 # ossep => pathname that uses os.sep to separate directories
1107 # ossep => pathname that uses os.sep to separate directories
1108 cwd = repo.getcwd()
1108 cwd = repo.getcwd()
1109 targets = {}
1109 targets = {}
1110 after = opts.get("after")
1110 after = opts.get("after")
1111 dryrun = opts.get("dry_run")
1111 dryrun = opts.get("dry_run")
1112 wctx = repo[None]
1112 wctx = repo[None]
1113
1113
1114 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1114 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1115 def walkpat(pat):
1115 def walkpat(pat):
1116 srcs = []
1116 srcs = []
1117 if after:
1117 if after:
1118 badstates = '?'
1118 badstates = '?'
1119 else:
1119 else:
1120 badstates = '?r'
1120 badstates = '?r'
1121 m = scmutil.match(wctx, [pat], opts, globbed=True)
1121 m = scmutil.match(wctx, [pat], opts, globbed=True)
1122 for abs in wctx.walk(m):
1122 for abs in wctx.walk(m):
1123 state = repo.dirstate[abs]
1123 state = repo.dirstate[abs]
1124 rel = uipathfn(abs)
1124 rel = uipathfn(abs)
1125 exact = m.exact(abs)
1125 exact = m.exact(abs)
1126 if state in badstates:
1126 if state in badstates:
1127 if exact and state == '?':
1127 if exact and state == '?':
1128 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1128 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1129 if exact and state == 'r':
1129 if exact and state == 'r':
1130 ui.warn(_('%s: not copying - file has been marked for'
1130 ui.warn(_('%s: not copying - file has been marked for'
1131 ' remove\n') % rel)
1131 ' remove\n') % rel)
1132 continue
1132 continue
1133 # abs: hgsep
1133 # abs: hgsep
1134 # rel: ossep
1134 # rel: ossep
1135 srcs.append((abs, rel, exact))
1135 srcs.append((abs, rel, exact))
1136 return srcs
1136 return srcs
1137
1137
1138 # abssrc: hgsep
1138 # abssrc: hgsep
1139 # relsrc: ossep
1139 # relsrc: ossep
1140 # otarget: ossep
1140 # otarget: ossep
1141 def copyfile(abssrc, relsrc, otarget, exact):
1141 def copyfile(abssrc, relsrc, otarget, exact):
1142 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1142 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1143 if '/' in abstarget:
1143 if '/' in abstarget:
1144 # We cannot normalize abstarget itself, this would prevent
1144 # We cannot normalize abstarget itself, this would prevent
1145 # case only renames, like a => A.
1145 # case only renames, like a => A.
1146 abspath, absname = abstarget.rsplit('/', 1)
1146 abspath, absname = abstarget.rsplit('/', 1)
1147 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1147 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1148 reltarget = repo.pathto(abstarget, cwd)
1148 reltarget = repo.pathto(abstarget, cwd)
1149 target = repo.wjoin(abstarget)
1149 target = repo.wjoin(abstarget)
1150 src = repo.wjoin(abssrc)
1150 src = repo.wjoin(abssrc)
1151 state = repo.dirstate[abstarget]
1151 state = repo.dirstate[abstarget]
1152
1152
1153 scmutil.checkportable(ui, abstarget)
1153 scmutil.checkportable(ui, abstarget)
1154
1154
1155 # check for collisions
1155 # check for collisions
1156 prevsrc = targets.get(abstarget)
1156 prevsrc = targets.get(abstarget)
1157 if prevsrc is not None:
1157 if prevsrc is not None:
1158 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1158 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1159 (reltarget, repo.pathto(abssrc, cwd),
1159 (reltarget, repo.pathto(abssrc, cwd),
1160 repo.pathto(prevsrc, cwd)))
1160 repo.pathto(prevsrc, cwd)))
1161 return True # report a failure
1161 return True # report a failure
1162
1162
1163 # check for overwrites
1163 # check for overwrites
1164 exists = os.path.lexists(target)
1164 exists = os.path.lexists(target)
1165 samefile = False
1165 samefile = False
1166 if exists and abssrc != abstarget:
1166 if exists and abssrc != abstarget:
1167 if (repo.dirstate.normalize(abssrc) ==
1167 if (repo.dirstate.normalize(abssrc) ==
1168 repo.dirstate.normalize(abstarget)):
1168 repo.dirstate.normalize(abstarget)):
1169 if not rename:
1169 if not rename:
1170 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1170 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1171 return True # report a failure
1171 return True # report a failure
1172 exists = False
1172 exists = False
1173 samefile = True
1173 samefile = True
1174
1174
1175 if not after and exists or after and state in 'mn':
1175 if not after and exists or after and state in 'mn':
1176 if not opts['force']:
1176 if not opts['force']:
1177 if state in 'mn':
1177 if state in 'mn':
1178 msg = _('%s: not overwriting - file already committed\n')
1178 msg = _('%s: not overwriting - file already committed\n')
1179 if after:
1179 if after:
1180 flags = '--after --force'
1180 flags = '--after --force'
1181 else:
1181 else:
1182 flags = '--force'
1182 flags = '--force'
1183 if rename:
1183 if rename:
1184 hint = _("('hg rename %s' to replace the file by "
1184 hint = _("('hg rename %s' to replace the file by "
1185 'recording a rename)\n') % flags
1185 'recording a rename)\n') % flags
1186 else:
1186 else:
1187 hint = _("('hg copy %s' to replace the file by "
1187 hint = _("('hg copy %s' to replace the file by "
1188 'recording a copy)\n') % flags
1188 'recording a copy)\n') % flags
1189 else:
1189 else:
1190 msg = _('%s: not overwriting - file exists\n')
1190 msg = _('%s: not overwriting - file exists\n')
1191 if rename:
1191 if rename:
1192 hint = _("('hg rename --after' to record the rename)\n")
1192 hint = _("('hg rename --after' to record the rename)\n")
1193 else:
1193 else:
1194 hint = _("('hg copy --after' to record the copy)\n")
1194 hint = _("('hg copy --after' to record the copy)\n")
1195 ui.warn(msg % reltarget)
1195 ui.warn(msg % reltarget)
1196 ui.warn(hint)
1196 ui.warn(hint)
1197 return True # report a failure
1197 return True # report a failure
1198
1198
1199 if after:
1199 if after:
1200 if not exists:
1200 if not exists:
1201 if rename:
1201 if rename:
1202 ui.warn(_('%s: not recording move - %s does not exist\n') %
1202 ui.warn(_('%s: not recording move - %s does not exist\n') %
1203 (relsrc, reltarget))
1203 (relsrc, reltarget))
1204 else:
1204 else:
1205 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1205 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1206 (relsrc, reltarget))
1206 (relsrc, reltarget))
1207 return True # report a failure
1207 return True # report a failure
1208 elif not dryrun:
1208 elif not dryrun:
1209 try:
1209 try:
1210 if exists:
1210 if exists:
1211 os.unlink(target)
1211 os.unlink(target)
1212 targetdir = os.path.dirname(target) or '.'
1212 targetdir = os.path.dirname(target) or '.'
1213 if not os.path.isdir(targetdir):
1213 if not os.path.isdir(targetdir):
1214 os.makedirs(targetdir)
1214 os.makedirs(targetdir)
1215 if samefile:
1215 if samefile:
1216 tmp = target + "~hgrename"
1216 tmp = target + "~hgrename"
1217 os.rename(src, tmp)
1217 os.rename(src, tmp)
1218 os.rename(tmp, target)
1218 os.rename(tmp, target)
1219 else:
1219 else:
1220 # Preserve stat info on renames, not on copies; this matches
1220 # Preserve stat info on renames, not on copies; this matches
1221 # Linux CLI behavior.
1221 # Linux CLI behavior.
1222 util.copyfile(src, target, copystat=rename)
1222 util.copyfile(src, target, copystat=rename)
1223 srcexists = True
1223 srcexists = True
1224 except IOError as inst:
1224 except IOError as inst:
1225 if inst.errno == errno.ENOENT:
1225 if inst.errno == errno.ENOENT:
1226 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1226 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1227 srcexists = False
1227 srcexists = False
1228 else:
1228 else:
1229 ui.warn(_('%s: cannot copy - %s\n') %
1229 ui.warn(_('%s: cannot copy - %s\n') %
1230 (relsrc, encoding.strtolocal(inst.strerror)))
1230 (relsrc, encoding.strtolocal(inst.strerror)))
1231 return True # report a failure
1231 return True # report a failure
1232
1232
1233 if ui.verbose or not exact:
1233 if ui.verbose or not exact:
1234 if rename:
1234 if rename:
1235 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1235 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1236 else:
1236 else:
1237 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1237 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1238
1238
1239 targets[abstarget] = abssrc
1239 targets[abstarget] = abssrc
1240
1240
1241 # fix up dirstate
1241 # fix up dirstate
1242 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1242 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1243 dryrun=dryrun, cwd=cwd)
1243 dryrun=dryrun, cwd=cwd)
1244 if rename and not dryrun:
1244 if rename and not dryrun:
1245 if not after and srcexists and not samefile:
1245 if not after and srcexists and not samefile:
1246 rmdir = repo.ui.configbool('experimental', 'removeemptydirs')
1246 rmdir = repo.ui.configbool('experimental', 'removeemptydirs')
1247 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1247 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1248 wctx.forget([abssrc])
1248 wctx.forget([abssrc])
1249
1249
1250 # pat: ossep
1250 # pat: ossep
1251 # dest ossep
1251 # dest ossep
1252 # srcs: list of (hgsep, hgsep, ossep, bool)
1252 # srcs: list of (hgsep, hgsep, ossep, bool)
1253 # return: function that takes hgsep and returns ossep
1253 # return: function that takes hgsep and returns ossep
1254 def targetpathfn(pat, dest, srcs):
1254 def targetpathfn(pat, dest, srcs):
1255 if os.path.isdir(pat):
1255 if os.path.isdir(pat):
1256 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1256 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1257 abspfx = util.localpath(abspfx)
1257 abspfx = util.localpath(abspfx)
1258 if destdirexists:
1258 if destdirexists:
1259 striplen = len(os.path.split(abspfx)[0])
1259 striplen = len(os.path.split(abspfx)[0])
1260 else:
1260 else:
1261 striplen = len(abspfx)
1261 striplen = len(abspfx)
1262 if striplen:
1262 if striplen:
1263 striplen += len(pycompat.ossep)
1263 striplen += len(pycompat.ossep)
1264 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1264 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1265 elif destdirexists:
1265 elif destdirexists:
1266 res = lambda p: os.path.join(dest,
1266 res = lambda p: os.path.join(dest,
1267 os.path.basename(util.localpath(p)))
1267 os.path.basename(util.localpath(p)))
1268 else:
1268 else:
1269 res = lambda p: dest
1269 res = lambda p: dest
1270 return res
1270 return res
1271
1271
1272 # pat: ossep
1272 # pat: ossep
1273 # dest ossep
1273 # dest ossep
1274 # srcs: list of (hgsep, hgsep, ossep, bool)
1274 # srcs: list of (hgsep, hgsep, ossep, bool)
1275 # return: function that takes hgsep and returns ossep
1275 # return: function that takes hgsep and returns ossep
1276 def targetpathafterfn(pat, dest, srcs):
1276 def targetpathafterfn(pat, dest, srcs):
1277 if matchmod.patkind(pat):
1277 if matchmod.patkind(pat):
1278 # a mercurial pattern
1278 # a mercurial pattern
1279 res = lambda p: os.path.join(dest,
1279 res = lambda p: os.path.join(dest,
1280 os.path.basename(util.localpath(p)))
1280 os.path.basename(util.localpath(p)))
1281 else:
1281 else:
1282 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1282 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1283 if len(abspfx) < len(srcs[0][0]):
1283 if len(abspfx) < len(srcs[0][0]):
1284 # A directory. Either the target path contains the last
1284 # A directory. Either the target path contains the last
1285 # component of the source path or it does not.
1285 # component of the source path or it does not.
1286 def evalpath(striplen):
1286 def evalpath(striplen):
1287 score = 0
1287 score = 0
1288 for s in srcs:
1288 for s in srcs:
1289 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1289 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1290 if os.path.lexists(t):
1290 if os.path.lexists(t):
1291 score += 1
1291 score += 1
1292 return score
1292 return score
1293
1293
1294 abspfx = util.localpath(abspfx)
1294 abspfx = util.localpath(abspfx)
1295 striplen = len(abspfx)
1295 striplen = len(abspfx)
1296 if striplen:
1296 if striplen:
1297 striplen += len(pycompat.ossep)
1297 striplen += len(pycompat.ossep)
1298 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1298 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1299 score = evalpath(striplen)
1299 score = evalpath(striplen)
1300 striplen1 = len(os.path.split(abspfx)[0])
1300 striplen1 = len(os.path.split(abspfx)[0])
1301 if striplen1:
1301 if striplen1:
1302 striplen1 += len(pycompat.ossep)
1302 striplen1 += len(pycompat.ossep)
1303 if evalpath(striplen1) > score:
1303 if evalpath(striplen1) > score:
1304 striplen = striplen1
1304 striplen = striplen1
1305 res = lambda p: os.path.join(dest,
1305 res = lambda p: os.path.join(dest,
1306 util.localpath(p)[striplen:])
1306 util.localpath(p)[striplen:])
1307 else:
1307 else:
1308 # a file
1308 # a file
1309 if destdirexists:
1309 if destdirexists:
1310 res = lambda p: os.path.join(dest,
1310 res = lambda p: os.path.join(dest,
1311 os.path.basename(util.localpath(p)))
1311 os.path.basename(util.localpath(p)))
1312 else:
1312 else:
1313 res = lambda p: dest
1313 res = lambda p: dest
1314 return res
1314 return res
1315
1315
1316 pats = scmutil.expandpats(pats)
1316 pats = scmutil.expandpats(pats)
1317 if not pats:
1317 if not pats:
1318 raise error.Abort(_('no source or destination specified'))
1318 raise error.Abort(_('no source or destination specified'))
1319 if len(pats) == 1:
1319 if len(pats) == 1:
1320 raise error.Abort(_('no destination specified'))
1320 raise error.Abort(_('no destination specified'))
1321 dest = pats.pop()
1321 dest = pats.pop()
1322 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1322 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1323 if not destdirexists:
1323 if not destdirexists:
1324 if len(pats) > 1 or matchmod.patkind(pats[0]):
1324 if len(pats) > 1 or matchmod.patkind(pats[0]):
1325 raise error.Abort(_('with multiple sources, destination must be an '
1325 raise error.Abort(_('with multiple sources, destination must be an '
1326 'existing directory'))
1326 'existing directory'))
1327 if util.endswithsep(dest):
1327 if util.endswithsep(dest):
1328 raise error.Abort(_('destination %s is not a directory') % dest)
1328 raise error.Abort(_('destination %s is not a directory') % dest)
1329
1329
1330 tfn = targetpathfn
1330 tfn = targetpathfn
1331 if after:
1331 if after:
1332 tfn = targetpathafterfn
1332 tfn = targetpathafterfn
1333 copylist = []
1333 copylist = []
1334 for pat in pats:
1334 for pat in pats:
1335 srcs = walkpat(pat)
1335 srcs = walkpat(pat)
1336 if not srcs:
1336 if not srcs:
1337 continue
1337 continue
1338 copylist.append((tfn(pat, dest, srcs), srcs))
1338 copylist.append((tfn(pat, dest, srcs), srcs))
1339 if not copylist:
1339 if not copylist:
1340 raise error.Abort(_('no files to copy'))
1340 raise error.Abort(_('no files to copy'))
1341
1341
1342 errors = 0
1342 errors = 0
1343 for targetpath, srcs in copylist:
1343 for targetpath, srcs in copylist:
1344 for abssrc, relsrc, exact in srcs:
1344 for abssrc, relsrc, exact in srcs:
1345 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1345 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1346 errors += 1
1346 errors += 1
1347
1347
1348 return errors != 0
1348 return errors != 0
1349
1349
1350 ## facility to let extension process additional data into an import patch
1350 ## facility to let extension process additional data into an import patch
1351 # list of identifier to be executed in order
1351 # list of identifier to be executed in order
1352 extrapreimport = [] # run before commit
1352 extrapreimport = [] # run before commit
1353 extrapostimport = [] # run after commit
1353 extrapostimport = [] # run after commit
1354 # mapping from identifier to actual import function
1354 # mapping from identifier to actual import function
1355 #
1355 #
1356 # 'preimport' are run before the commit is made and are provided the following
1356 # 'preimport' are run before the commit is made and are provided the following
1357 # arguments:
1357 # arguments:
1358 # - repo: the localrepository instance,
1358 # - repo: the localrepository instance,
1359 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1359 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1360 # - extra: the future extra dictionary of the changeset, please mutate it,
1360 # - extra: the future extra dictionary of the changeset, please mutate it,
1361 # - opts: the import options.
1361 # - opts: the import options.
1362 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1362 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1363 # mutation of in memory commit and more. Feel free to rework the code to get
1363 # mutation of in memory commit and more. Feel free to rework the code to get
1364 # there.
1364 # there.
1365 extrapreimportmap = {}
1365 extrapreimportmap = {}
1366 # 'postimport' are run after the commit is made and are provided the following
1366 # 'postimport' are run after the commit is made and are provided the following
1367 # argument:
1367 # argument:
1368 # - ctx: the changectx created by import.
1368 # - ctx: the changectx created by import.
1369 extrapostimportmap = {}
1369 extrapostimportmap = {}
1370
1370
1371 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1371 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1372 """Utility function used by commands.import to import a single patch
1372 """Utility function used by commands.import to import a single patch
1373
1373
1374 This function is explicitly defined here to help the evolve extension to
1374 This function is explicitly defined here to help the evolve extension to
1375 wrap this part of the import logic.
1375 wrap this part of the import logic.
1376
1376
1377 The API is currently a bit ugly because it a simple code translation from
1377 The API is currently a bit ugly because it a simple code translation from
1378 the import command. Feel free to make it better.
1378 the import command. Feel free to make it better.
1379
1379
1380 :patchdata: a dictionary containing parsed patch data (such as from
1380 :patchdata: a dictionary containing parsed patch data (such as from
1381 ``patch.extract()``)
1381 ``patch.extract()``)
1382 :parents: nodes that will be parent of the created commit
1382 :parents: nodes that will be parent of the created commit
1383 :opts: the full dict of option passed to the import command
1383 :opts: the full dict of option passed to the import command
1384 :msgs: list to save commit message to.
1384 :msgs: list to save commit message to.
1385 (used in case we need to save it when failing)
1385 (used in case we need to save it when failing)
1386 :updatefunc: a function that update a repo to a given node
1386 :updatefunc: a function that update a repo to a given node
1387 updatefunc(<repo>, <node>)
1387 updatefunc(<repo>, <node>)
1388 """
1388 """
1389 # avoid cycle context -> subrepo -> cmdutil
1389 # avoid cycle context -> subrepo -> cmdutil
1390 from . import context
1390 from . import context
1391
1391
1392 tmpname = patchdata.get('filename')
1392 tmpname = patchdata.get('filename')
1393 message = patchdata.get('message')
1393 message = patchdata.get('message')
1394 user = opts.get('user') or patchdata.get('user')
1394 user = opts.get('user') or patchdata.get('user')
1395 date = opts.get('date') or patchdata.get('date')
1395 date = opts.get('date') or patchdata.get('date')
1396 branch = patchdata.get('branch')
1396 branch = patchdata.get('branch')
1397 nodeid = patchdata.get('nodeid')
1397 nodeid = patchdata.get('nodeid')
1398 p1 = patchdata.get('p1')
1398 p1 = patchdata.get('p1')
1399 p2 = patchdata.get('p2')
1399 p2 = patchdata.get('p2')
1400
1400
1401 nocommit = opts.get('no_commit')
1401 nocommit = opts.get('no_commit')
1402 importbranch = opts.get('import_branch')
1402 importbranch = opts.get('import_branch')
1403 update = not opts.get('bypass')
1403 update = not opts.get('bypass')
1404 strip = opts["strip"]
1404 strip = opts["strip"]
1405 prefix = opts["prefix"]
1405 prefix = opts["prefix"]
1406 sim = float(opts.get('similarity') or 0)
1406 sim = float(opts.get('similarity') or 0)
1407
1407
1408 if not tmpname:
1408 if not tmpname:
1409 return None, None, False
1409 return None, None, False
1410
1410
1411 rejects = False
1411 rejects = False
1412
1412
1413 cmdline_message = logmessage(ui, opts)
1413 cmdline_message = logmessage(ui, opts)
1414 if cmdline_message:
1414 if cmdline_message:
1415 # pickup the cmdline msg
1415 # pickup the cmdline msg
1416 message = cmdline_message
1416 message = cmdline_message
1417 elif message:
1417 elif message:
1418 # pickup the patch msg
1418 # pickup the patch msg
1419 message = message.strip()
1419 message = message.strip()
1420 else:
1420 else:
1421 # launch the editor
1421 # launch the editor
1422 message = None
1422 message = None
1423 ui.debug('message:\n%s\n' % (message or ''))
1423 ui.debug('message:\n%s\n' % (message or ''))
1424
1424
1425 if len(parents) == 1:
1425 if len(parents) == 1:
1426 parents.append(repo[nullid])
1426 parents.append(repo[nullid])
1427 if opts.get('exact'):
1427 if opts.get('exact'):
1428 if not nodeid or not p1:
1428 if not nodeid or not p1:
1429 raise error.Abort(_('not a Mercurial patch'))
1429 raise error.Abort(_('not a Mercurial patch'))
1430 p1 = repo[p1]
1430 p1 = repo[p1]
1431 p2 = repo[p2 or nullid]
1431 p2 = repo[p2 or nullid]
1432 elif p2:
1432 elif p2:
1433 try:
1433 try:
1434 p1 = repo[p1]
1434 p1 = repo[p1]
1435 p2 = repo[p2]
1435 p2 = repo[p2]
1436 # Without any options, consider p2 only if the
1436 # Without any options, consider p2 only if the
1437 # patch is being applied on top of the recorded
1437 # patch is being applied on top of the recorded
1438 # first parent.
1438 # first parent.
1439 if p1 != parents[0]:
1439 if p1 != parents[0]:
1440 p1 = parents[0]
1440 p1 = parents[0]
1441 p2 = repo[nullid]
1441 p2 = repo[nullid]
1442 except error.RepoError:
1442 except error.RepoError:
1443 p1, p2 = parents
1443 p1, p2 = parents
1444 if p2.node() == nullid:
1444 if p2.node() == nullid:
1445 ui.warn(_("warning: import the patch as a normal revision\n"
1445 ui.warn(_("warning: import the patch as a normal revision\n"
1446 "(use --exact to import the patch as a merge)\n"))
1446 "(use --exact to import the patch as a merge)\n"))
1447 else:
1447 else:
1448 p1, p2 = parents
1448 p1, p2 = parents
1449
1449
1450 n = None
1450 n = None
1451 if update:
1451 if update:
1452 if p1 != parents[0]:
1452 if p1 != parents[0]:
1453 updatefunc(repo, p1.node())
1453 updatefunc(repo, p1.node())
1454 if p2 != parents[1]:
1454 if p2 != parents[1]:
1455 repo.setparents(p1.node(), p2.node())
1455 repo.setparents(p1.node(), p2.node())
1456
1456
1457 if opts.get('exact') or importbranch:
1457 if opts.get('exact') or importbranch:
1458 repo.dirstate.setbranch(branch or 'default')
1458 repo.dirstate.setbranch(branch or 'default')
1459
1459
1460 partial = opts.get('partial', False)
1460 partial = opts.get('partial', False)
1461 files = set()
1461 files = set()
1462 try:
1462 try:
1463 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1463 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1464 files=files, eolmode=None, similarity=sim / 100.0)
1464 files=files, eolmode=None, similarity=sim / 100.0)
1465 except error.PatchError as e:
1465 except error.PatchError as e:
1466 if not partial:
1466 if not partial:
1467 raise error.Abort(pycompat.bytestr(e))
1467 raise error.Abort(pycompat.bytestr(e))
1468 if partial:
1468 if partial:
1469 rejects = True
1469 rejects = True
1470
1470
1471 files = list(files)
1471 files = list(files)
1472 if nocommit:
1472 if nocommit:
1473 if message:
1473 if message:
1474 msgs.append(message)
1474 msgs.append(message)
1475 else:
1475 else:
1476 if opts.get('exact') or p2:
1476 if opts.get('exact') or p2:
1477 # If you got here, you either use --force and know what
1477 # If you got here, you either use --force and know what
1478 # you are doing or used --exact or a merge patch while
1478 # you are doing or used --exact or a merge patch while
1479 # being updated to its first parent.
1479 # being updated to its first parent.
1480 m = None
1480 m = None
1481 else:
1481 else:
1482 m = scmutil.matchfiles(repo, files or [])
1482 m = scmutil.matchfiles(repo, files or [])
1483 editform = mergeeditform(repo[None], 'import.normal')
1483 editform = mergeeditform(repo[None], 'import.normal')
1484 if opts.get('exact'):
1484 if opts.get('exact'):
1485 editor = None
1485 editor = None
1486 else:
1486 else:
1487 editor = getcommiteditor(editform=editform,
1487 editor = getcommiteditor(editform=editform,
1488 **pycompat.strkwargs(opts))
1488 **pycompat.strkwargs(opts))
1489 extra = {}
1489 extra = {}
1490 for idfunc in extrapreimport:
1490 for idfunc in extrapreimport:
1491 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1491 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1492 overrides = {}
1492 overrides = {}
1493 if partial:
1493 if partial:
1494 overrides[('ui', 'allowemptycommit')] = True
1494 overrides[('ui', 'allowemptycommit')] = True
1495 with repo.ui.configoverride(overrides, 'import'):
1495 with repo.ui.configoverride(overrides, 'import'):
1496 n = repo.commit(message, user,
1496 n = repo.commit(message, user,
1497 date, match=m,
1497 date, match=m,
1498 editor=editor, extra=extra)
1498 editor=editor, extra=extra)
1499 for idfunc in extrapostimport:
1499 for idfunc in extrapostimport:
1500 extrapostimportmap[idfunc](repo[n])
1500 extrapostimportmap[idfunc](repo[n])
1501 else:
1501 else:
1502 if opts.get('exact') or importbranch:
1502 if opts.get('exact') or importbranch:
1503 branch = branch or 'default'
1503 branch = branch or 'default'
1504 else:
1504 else:
1505 branch = p1.branch()
1505 branch = p1.branch()
1506 store = patch.filestore()
1506 store = patch.filestore()
1507 try:
1507 try:
1508 files = set()
1508 files = set()
1509 try:
1509 try:
1510 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1510 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1511 files, eolmode=None)
1511 files, eolmode=None)
1512 except error.PatchError as e:
1512 except error.PatchError as e:
1513 raise error.Abort(stringutil.forcebytestr(e))
1513 raise error.Abort(stringutil.forcebytestr(e))
1514 if opts.get('exact'):
1514 if opts.get('exact'):
1515 editor = None
1515 editor = None
1516 else:
1516 else:
1517 editor = getcommiteditor(editform='import.bypass')
1517 editor = getcommiteditor(editform='import.bypass')
1518 memctx = context.memctx(repo, (p1.node(), p2.node()),
1518 memctx = context.memctx(repo, (p1.node(), p2.node()),
1519 message,
1519 message,
1520 files=files,
1520 files=files,
1521 filectxfn=store,
1521 filectxfn=store,
1522 user=user,
1522 user=user,
1523 date=date,
1523 date=date,
1524 branch=branch,
1524 branch=branch,
1525 editor=editor)
1525 editor=editor)
1526 n = memctx.commit()
1526 n = memctx.commit()
1527 finally:
1527 finally:
1528 store.close()
1528 store.close()
1529 if opts.get('exact') and nocommit:
1529 if opts.get('exact') and nocommit:
1530 # --exact with --no-commit is still useful in that it does merge
1530 # --exact with --no-commit is still useful in that it does merge
1531 # and branch bits
1531 # and branch bits
1532 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1532 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1533 elif opts.get('exact') and (not n or hex(n) != nodeid):
1533 elif opts.get('exact') and (not n or hex(n) != nodeid):
1534 raise error.Abort(_('patch is damaged or loses information'))
1534 raise error.Abort(_('patch is damaged or loses information'))
1535 msg = _('applied to working directory')
1535 msg = _('applied to working directory')
1536 if n:
1536 if n:
1537 # i18n: refers to a short changeset id
1537 # i18n: refers to a short changeset id
1538 msg = _('created %s') % short(n)
1538 msg = _('created %s') % short(n)
1539 return msg, n, rejects
1539 return msg, n, rejects
1540
1540
1541 # facility to let extensions include additional data in an exported patch
1541 # facility to let extensions include additional data in an exported patch
1542 # list of identifiers to be executed in order
1542 # list of identifiers to be executed in order
1543 extraexport = []
1543 extraexport = []
1544 # mapping from identifier to actual export function
1544 # mapping from identifier to actual export function
1545 # function as to return a string to be added to the header or None
1545 # function as to return a string to be added to the header or None
1546 # it is given two arguments (sequencenumber, changectx)
1546 # it is given two arguments (sequencenumber, changectx)
1547 extraexportmap = {}
1547 extraexportmap = {}
1548
1548
1549 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
1549 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
1550 node = scmutil.binnode(ctx)
1550 node = scmutil.binnode(ctx)
1551 parents = [p.node() for p in ctx.parents() if p]
1551 parents = [p.node() for p in ctx.parents() if p]
1552 branch = ctx.branch()
1552 branch = ctx.branch()
1553 if switch_parent:
1553 if switch_parent:
1554 parents.reverse()
1554 parents.reverse()
1555
1555
1556 if parents:
1556 if parents:
1557 prev = parents[0]
1557 prev = parents[0]
1558 else:
1558 else:
1559 prev = nullid
1559 prev = nullid
1560
1560
1561 fm.context(ctx=ctx)
1561 fm.context(ctx=ctx)
1562 fm.plain('# HG changeset patch\n')
1562 fm.plain('# HG changeset patch\n')
1563 fm.write('user', '# User %s\n', ctx.user())
1563 fm.write('user', '# User %s\n', ctx.user())
1564 fm.plain('# Date %d %d\n' % ctx.date())
1564 fm.plain('# Date %d %d\n' % ctx.date())
1565 fm.write('date', '# %s\n', fm.formatdate(ctx.date()))
1565 fm.write('date', '# %s\n', fm.formatdate(ctx.date()))
1566 fm.condwrite(branch and branch != 'default',
1566 fm.condwrite(branch and branch != 'default',
1567 'branch', '# Branch %s\n', branch)
1567 'branch', '# Branch %s\n', branch)
1568 fm.write('node', '# Node ID %s\n', hex(node))
1568 fm.write('node', '# Node ID %s\n', hex(node))
1569 fm.plain('# Parent %s\n' % hex(prev))
1569 fm.plain('# Parent %s\n' % hex(prev))
1570 if len(parents) > 1:
1570 if len(parents) > 1:
1571 fm.plain('# Parent %s\n' % hex(parents[1]))
1571 fm.plain('# Parent %s\n' % hex(parents[1]))
1572 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name='node'))
1572 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name='node'))
1573
1573
1574 # TODO: redesign extraexportmap function to support formatter
1574 # TODO: redesign extraexportmap function to support formatter
1575 for headerid in extraexport:
1575 for headerid in extraexport:
1576 header = extraexportmap[headerid](seqno, ctx)
1576 header = extraexportmap[headerid](seqno, ctx)
1577 if header is not None:
1577 if header is not None:
1578 fm.plain('# %s\n' % header)
1578 fm.plain('# %s\n' % header)
1579
1579
1580 fm.write('desc', '%s\n', ctx.description().rstrip())
1580 fm.write('desc', '%s\n', ctx.description().rstrip())
1581 fm.plain('\n')
1581 fm.plain('\n')
1582
1582
1583 if fm.isplain():
1583 if fm.isplain():
1584 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
1584 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
1585 for chunk, label in chunkiter:
1585 for chunk, label in chunkiter:
1586 fm.plain(chunk, label=label)
1586 fm.plain(chunk, label=label)
1587 else:
1587 else:
1588 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
1588 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
1589 # TODO: make it structured?
1589 # TODO: make it structured?
1590 fm.data(diff=b''.join(chunkiter))
1590 fm.data(diff=b''.join(chunkiter))
1591
1591
1592 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
1592 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
1593 """Export changesets to stdout or a single file"""
1593 """Export changesets to stdout or a single file"""
1594 for seqno, rev in enumerate(revs, 1):
1594 for seqno, rev in enumerate(revs, 1):
1595 ctx = repo[rev]
1595 ctx = repo[rev]
1596 if not dest.startswith('<'):
1596 if not dest.startswith('<'):
1597 repo.ui.note("%s\n" % dest)
1597 repo.ui.note("%s\n" % dest)
1598 fm.startitem()
1598 fm.startitem()
1599 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
1599 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
1600
1600
1601 def _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, diffopts,
1601 def _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, diffopts,
1602 match):
1602 match):
1603 """Export changesets to possibly multiple files"""
1603 """Export changesets to possibly multiple files"""
1604 total = len(revs)
1604 total = len(revs)
1605 revwidth = max(len(str(rev)) for rev in revs)
1605 revwidth = max(len(str(rev)) for rev in revs)
1606 filemap = util.sortdict() # filename: [(seqno, rev), ...]
1606 filemap = util.sortdict() # filename: [(seqno, rev), ...]
1607
1607
1608 for seqno, rev in enumerate(revs, 1):
1608 for seqno, rev in enumerate(revs, 1):
1609 ctx = repo[rev]
1609 ctx = repo[rev]
1610 dest = makefilename(ctx, fntemplate,
1610 dest = makefilename(ctx, fntemplate,
1611 total=total, seqno=seqno, revwidth=revwidth)
1611 total=total, seqno=seqno, revwidth=revwidth)
1612 filemap.setdefault(dest, []).append((seqno, rev))
1612 filemap.setdefault(dest, []).append((seqno, rev))
1613
1613
1614 for dest in filemap:
1614 for dest in filemap:
1615 with formatter.maybereopen(basefm, dest) as fm:
1615 with formatter.maybereopen(basefm, dest) as fm:
1616 repo.ui.note("%s\n" % dest)
1616 repo.ui.note("%s\n" % dest)
1617 for seqno, rev in filemap[dest]:
1617 for seqno, rev in filemap[dest]:
1618 fm.startitem()
1618 fm.startitem()
1619 ctx = repo[rev]
1619 ctx = repo[rev]
1620 _exportsingle(repo, ctx, fm, match, switch_parent, seqno,
1620 _exportsingle(repo, ctx, fm, match, switch_parent, seqno,
1621 diffopts)
1621 diffopts)
1622
1622
1623 def _prefetchchangedfiles(repo, revs, match):
1623 def _prefetchchangedfiles(repo, revs, match):
1624 allfiles = set()
1624 allfiles = set()
1625 for rev in revs:
1625 for rev in revs:
1626 for file in repo[rev].files():
1626 for file in repo[rev].files():
1627 if not match or match(file):
1627 if not match or match(file):
1628 allfiles.add(file)
1628 allfiles.add(file)
1629 scmutil.prefetchfiles(repo, revs, scmutil.matchfiles(repo, allfiles))
1629 scmutil.prefetchfiles(repo, revs, scmutil.matchfiles(repo, allfiles))
1630
1630
1631 def export(repo, revs, basefm, fntemplate='hg-%h.patch', switch_parent=False,
1631 def export(repo, revs, basefm, fntemplate='hg-%h.patch', switch_parent=False,
1632 opts=None, match=None):
1632 opts=None, match=None):
1633 '''export changesets as hg patches
1633 '''export changesets as hg patches
1634
1634
1635 Args:
1635 Args:
1636 repo: The repository from which we're exporting revisions.
1636 repo: The repository from which we're exporting revisions.
1637 revs: A list of revisions to export as revision numbers.
1637 revs: A list of revisions to export as revision numbers.
1638 basefm: A formatter to which patches should be written.
1638 basefm: A formatter to which patches should be written.
1639 fntemplate: An optional string to use for generating patch file names.
1639 fntemplate: An optional string to use for generating patch file names.
1640 switch_parent: If True, show diffs against second parent when not nullid.
1640 switch_parent: If True, show diffs against second parent when not nullid.
1641 Default is false, which always shows diff against p1.
1641 Default is false, which always shows diff against p1.
1642 opts: diff options to use for generating the patch.
1642 opts: diff options to use for generating the patch.
1643 match: If specified, only export changes to files matching this matcher.
1643 match: If specified, only export changes to files matching this matcher.
1644
1644
1645 Returns:
1645 Returns:
1646 Nothing.
1646 Nothing.
1647
1647
1648 Side Effect:
1648 Side Effect:
1649 "HG Changeset Patch" data is emitted to one of the following
1649 "HG Changeset Patch" data is emitted to one of the following
1650 destinations:
1650 destinations:
1651 fntemplate specified: Each rev is written to a unique file named using
1651 fntemplate specified: Each rev is written to a unique file named using
1652 the given template.
1652 the given template.
1653 Otherwise: All revs will be written to basefm.
1653 Otherwise: All revs will be written to basefm.
1654 '''
1654 '''
1655 _prefetchchangedfiles(repo, revs, match)
1655 _prefetchchangedfiles(repo, revs, match)
1656
1656
1657 if not fntemplate:
1657 if not fntemplate:
1658 _exportfile(repo, revs, basefm, '<unnamed>', switch_parent, opts, match)
1658 _exportfile(repo, revs, basefm, '<unnamed>', switch_parent, opts, match)
1659 else:
1659 else:
1660 _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, opts,
1660 _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, opts,
1661 match)
1661 match)
1662
1662
1663 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
1663 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
1664 """Export changesets to the given file stream"""
1664 """Export changesets to the given file stream"""
1665 _prefetchchangedfiles(repo, revs, match)
1665 _prefetchchangedfiles(repo, revs, match)
1666
1666
1667 dest = getattr(fp, 'name', '<unnamed>')
1667 dest = getattr(fp, 'name', '<unnamed>')
1668 with formatter.formatter(repo.ui, fp, 'export', {}) as fm:
1668 with formatter.formatter(repo.ui, fp, 'export', {}) as fm:
1669 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
1669 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
1670
1670
1671 def showmarker(fm, marker, index=None):
1671 def showmarker(fm, marker, index=None):
1672 """utility function to display obsolescence marker in a readable way
1672 """utility function to display obsolescence marker in a readable way
1673
1673
1674 To be used by debug function."""
1674 To be used by debug function."""
1675 if index is not None:
1675 if index is not None:
1676 fm.write('index', '%i ', index)
1676 fm.write('index', '%i ', index)
1677 fm.write('prednode', '%s ', hex(marker.prednode()))
1677 fm.write('prednode', '%s ', hex(marker.prednode()))
1678 succs = marker.succnodes()
1678 succs = marker.succnodes()
1679 fm.condwrite(succs, 'succnodes', '%s ',
1679 fm.condwrite(succs, 'succnodes', '%s ',
1680 fm.formatlist(map(hex, succs), name='node'))
1680 fm.formatlist(map(hex, succs), name='node'))
1681 fm.write('flag', '%X ', marker.flags())
1681 fm.write('flag', '%X ', marker.flags())
1682 parents = marker.parentnodes()
1682 parents = marker.parentnodes()
1683 if parents is not None:
1683 if parents is not None:
1684 fm.write('parentnodes', '{%s} ',
1684 fm.write('parentnodes', '{%s} ',
1685 fm.formatlist(map(hex, parents), name='node', sep=', '))
1685 fm.formatlist(map(hex, parents), name='node', sep=', '))
1686 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1686 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1687 meta = marker.metadata().copy()
1687 meta = marker.metadata().copy()
1688 meta.pop('date', None)
1688 meta.pop('date', None)
1689 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
1689 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
1690 fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
1690 fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
1691 fm.plain('\n')
1691 fm.plain('\n')
1692
1692
1693 def finddate(ui, repo, date):
1693 def finddate(ui, repo, date):
1694 """Find the tipmost changeset that matches the given date spec"""
1694 """Find the tipmost changeset that matches the given date spec"""
1695
1695
1696 df = dateutil.matchdate(date)
1696 df = dateutil.matchdate(date)
1697 m = scmutil.matchall(repo)
1697 m = scmutil.matchall(repo)
1698 results = {}
1698 results = {}
1699
1699
1700 def prep(ctx, fns):
1700 def prep(ctx, fns):
1701 d = ctx.date()
1701 d = ctx.date()
1702 if df(d[0]):
1702 if df(d[0]):
1703 results[ctx.rev()] = d
1703 results[ctx.rev()] = d
1704
1704
1705 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1705 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1706 rev = ctx.rev()
1706 rev = ctx.rev()
1707 if rev in results:
1707 if rev in results:
1708 ui.status(_("found revision %s from %s\n") %
1708 ui.status(_("found revision %s from %s\n") %
1709 (rev, dateutil.datestr(results[rev])))
1709 (rev, dateutil.datestr(results[rev])))
1710 return '%d' % rev
1710 return '%d' % rev
1711
1711
1712 raise error.Abort(_("revision matching date not found"))
1712 raise error.Abort(_("revision matching date not found"))
1713
1713
1714 def increasingwindows(windowsize=8, sizelimit=512):
1714 def increasingwindows(windowsize=8, sizelimit=512):
1715 while True:
1715 while True:
1716 yield windowsize
1716 yield windowsize
1717 if windowsize < sizelimit:
1717 if windowsize < sizelimit:
1718 windowsize *= 2
1718 windowsize *= 2
1719
1719
1720 def _walkrevs(repo, opts):
1720 def _walkrevs(repo, opts):
1721 # Default --rev value depends on --follow but --follow behavior
1721 # Default --rev value depends on --follow but --follow behavior
1722 # depends on revisions resolved from --rev...
1722 # depends on revisions resolved from --rev...
1723 follow = opts.get('follow') or opts.get('follow_first')
1723 follow = opts.get('follow') or opts.get('follow_first')
1724 if opts.get('rev'):
1724 if opts.get('rev'):
1725 revs = scmutil.revrange(repo, opts['rev'])
1725 revs = scmutil.revrange(repo, opts['rev'])
1726 elif follow and repo.dirstate.p1() == nullid:
1726 elif follow and repo.dirstate.p1() == nullid:
1727 revs = smartset.baseset()
1727 revs = smartset.baseset()
1728 elif follow:
1728 elif follow:
1729 revs = repo.revs('reverse(:.)')
1729 revs = repo.revs('reverse(:.)')
1730 else:
1730 else:
1731 revs = smartset.spanset(repo)
1731 revs = smartset.spanset(repo)
1732 revs.reverse()
1732 revs.reverse()
1733 return revs
1733 return revs
1734
1734
1735 class FileWalkError(Exception):
1735 class FileWalkError(Exception):
1736 pass
1736 pass
1737
1737
1738 def walkfilerevs(repo, match, follow, revs, fncache):
1738 def walkfilerevs(repo, match, follow, revs, fncache):
1739 '''Walks the file history for the matched files.
1739 '''Walks the file history for the matched files.
1740
1740
1741 Returns the changeset revs that are involved in the file history.
1741 Returns the changeset revs that are involved in the file history.
1742
1742
1743 Throws FileWalkError if the file history can't be walked using
1743 Throws FileWalkError if the file history can't be walked using
1744 filelogs alone.
1744 filelogs alone.
1745 '''
1745 '''
1746 wanted = set()
1746 wanted = set()
1747 copies = []
1747 copies = []
1748 minrev, maxrev = min(revs), max(revs)
1748 minrev, maxrev = min(revs), max(revs)
1749 def filerevs(filelog, last):
1749 def filerevs(filelog, last):
1750 """
1750 """
1751 Only files, no patterns. Check the history of each file.
1751 Only files, no patterns. Check the history of each file.
1752
1752
1753 Examines filelog entries within minrev, maxrev linkrev range
1753 Examines filelog entries within minrev, maxrev linkrev range
1754 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1754 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1755 tuples in backwards order
1755 tuples in backwards order
1756 """
1756 """
1757 cl_count = len(repo)
1757 cl_count = len(repo)
1758 revs = []
1758 revs = []
1759 for j in pycompat.xrange(0, last + 1):
1759 for j in pycompat.xrange(0, last + 1):
1760 linkrev = filelog.linkrev(j)
1760 linkrev = filelog.linkrev(j)
1761 if linkrev < minrev:
1761 if linkrev < minrev:
1762 continue
1762 continue
1763 # only yield rev for which we have the changelog, it can
1763 # only yield rev for which we have the changelog, it can
1764 # happen while doing "hg log" during a pull or commit
1764 # happen while doing "hg log" during a pull or commit
1765 if linkrev >= cl_count:
1765 if linkrev >= cl_count:
1766 break
1766 break
1767
1767
1768 parentlinkrevs = []
1768 parentlinkrevs = []
1769 for p in filelog.parentrevs(j):
1769 for p in filelog.parentrevs(j):
1770 if p != nullrev:
1770 if p != nullrev:
1771 parentlinkrevs.append(filelog.linkrev(p))
1771 parentlinkrevs.append(filelog.linkrev(p))
1772 n = filelog.node(j)
1772 n = filelog.node(j)
1773 revs.append((linkrev, parentlinkrevs,
1773 revs.append((linkrev, parentlinkrevs,
1774 follow and filelog.renamed(n)))
1774 follow and filelog.renamed(n)))
1775
1775
1776 return reversed(revs)
1776 return reversed(revs)
1777 def iterfiles():
1777 def iterfiles():
1778 pctx = repo['.']
1778 pctx = repo['.']
1779 for filename in match.files():
1779 for filename in match.files():
1780 if follow:
1780 if follow:
1781 if filename not in pctx:
1781 if filename not in pctx:
1782 raise error.Abort(_('cannot follow file not in parent '
1782 raise error.Abort(_('cannot follow file not in parent '
1783 'revision: "%s"') % filename)
1783 'revision: "%s"') % filename)
1784 yield filename, pctx[filename].filenode()
1784 yield filename, pctx[filename].filenode()
1785 else:
1785 else:
1786 yield filename, None
1786 yield filename, None
1787 for filename_node in copies:
1787 for filename_node in copies:
1788 yield filename_node
1788 yield filename_node
1789
1789
1790 for file_, node in iterfiles():
1790 for file_, node in iterfiles():
1791 filelog = repo.file(file_)
1791 filelog = repo.file(file_)
1792 if not len(filelog):
1792 if not len(filelog):
1793 if node is None:
1793 if node is None:
1794 # A zero count may be a directory or deleted file, so
1794 # A zero count may be a directory or deleted file, so
1795 # try to find matching entries on the slow path.
1795 # try to find matching entries on the slow path.
1796 if follow:
1796 if follow:
1797 raise error.Abort(
1797 raise error.Abort(
1798 _('cannot follow nonexistent file: "%s"') % file_)
1798 _('cannot follow nonexistent file: "%s"') % file_)
1799 raise FileWalkError("Cannot walk via filelog")
1799 raise FileWalkError("Cannot walk via filelog")
1800 else:
1800 else:
1801 continue
1801 continue
1802
1802
1803 if node is None:
1803 if node is None:
1804 last = len(filelog) - 1
1804 last = len(filelog) - 1
1805 else:
1805 else:
1806 last = filelog.rev(node)
1806 last = filelog.rev(node)
1807
1807
1808 # keep track of all ancestors of the file
1808 # keep track of all ancestors of the file
1809 ancestors = {filelog.linkrev(last)}
1809 ancestors = {filelog.linkrev(last)}
1810
1810
1811 # iterate from latest to oldest revision
1811 # iterate from latest to oldest revision
1812 for rev, flparentlinkrevs, copied in filerevs(filelog, last):
1812 for rev, flparentlinkrevs, copied in filerevs(filelog, last):
1813 if not follow:
1813 if not follow:
1814 if rev > maxrev:
1814 if rev > maxrev:
1815 continue
1815 continue
1816 else:
1816 else:
1817 # Note that last might not be the first interesting
1817 # Note that last might not be the first interesting
1818 # rev to us:
1818 # rev to us:
1819 # if the file has been changed after maxrev, we'll
1819 # if the file has been changed after maxrev, we'll
1820 # have linkrev(last) > maxrev, and we still need
1820 # have linkrev(last) > maxrev, and we still need
1821 # to explore the file graph
1821 # to explore the file graph
1822 if rev not in ancestors:
1822 if rev not in ancestors:
1823 continue
1823 continue
1824 # XXX insert 1327 fix here
1824 # XXX insert 1327 fix here
1825 if flparentlinkrevs:
1825 if flparentlinkrevs:
1826 ancestors.update(flparentlinkrevs)
1826 ancestors.update(flparentlinkrevs)
1827
1827
1828 fncache.setdefault(rev, []).append(file_)
1828 fncache.setdefault(rev, []).append(file_)
1829 wanted.add(rev)
1829 wanted.add(rev)
1830 if copied:
1830 if copied:
1831 copies.append(copied)
1831 copies.append(copied)
1832
1832
1833 return wanted
1833 return wanted
1834
1834
1835 class _followfilter(object):
1835 class _followfilter(object):
1836 def __init__(self, repo, onlyfirst=False):
1836 def __init__(self, repo, onlyfirst=False):
1837 self.repo = repo
1837 self.repo = repo
1838 self.startrev = nullrev
1838 self.startrev = nullrev
1839 self.roots = set()
1839 self.roots = set()
1840 self.onlyfirst = onlyfirst
1840 self.onlyfirst = onlyfirst
1841
1841
1842 def match(self, rev):
1842 def match(self, rev):
1843 def realparents(rev):
1843 def realparents(rev):
1844 if self.onlyfirst:
1844 if self.onlyfirst:
1845 return self.repo.changelog.parentrevs(rev)[0:1]
1845 return self.repo.changelog.parentrevs(rev)[0:1]
1846 else:
1846 else:
1847 return filter(lambda x: x != nullrev,
1847 return filter(lambda x: x != nullrev,
1848 self.repo.changelog.parentrevs(rev))
1848 self.repo.changelog.parentrevs(rev))
1849
1849
1850 if self.startrev == nullrev:
1850 if self.startrev == nullrev:
1851 self.startrev = rev
1851 self.startrev = rev
1852 return True
1852 return True
1853
1853
1854 if rev > self.startrev:
1854 if rev > self.startrev:
1855 # forward: all descendants
1855 # forward: all descendants
1856 if not self.roots:
1856 if not self.roots:
1857 self.roots.add(self.startrev)
1857 self.roots.add(self.startrev)
1858 for parent in realparents(rev):
1858 for parent in realparents(rev):
1859 if parent in self.roots:
1859 if parent in self.roots:
1860 self.roots.add(rev)
1860 self.roots.add(rev)
1861 return True
1861 return True
1862 else:
1862 else:
1863 # backwards: all parents
1863 # backwards: all parents
1864 if not self.roots:
1864 if not self.roots:
1865 self.roots.update(realparents(self.startrev))
1865 self.roots.update(realparents(self.startrev))
1866 if rev in self.roots:
1866 if rev in self.roots:
1867 self.roots.remove(rev)
1867 self.roots.remove(rev)
1868 self.roots.update(realparents(rev))
1868 self.roots.update(realparents(rev))
1869 return True
1869 return True
1870
1870
1871 return False
1871 return False
1872
1872
1873 def walkchangerevs(repo, match, opts, prepare):
1873 def walkchangerevs(repo, match, opts, prepare):
1874 '''Iterate over files and the revs in which they changed.
1874 '''Iterate over files and the revs in which they changed.
1875
1875
1876 Callers most commonly need to iterate backwards over the history
1876 Callers most commonly need to iterate backwards over the history
1877 in which they are interested. Doing so has awful (quadratic-looking)
1877 in which they are interested. Doing so has awful (quadratic-looking)
1878 performance, so we use iterators in a "windowed" way.
1878 performance, so we use iterators in a "windowed" way.
1879
1879
1880 We walk a window of revisions in the desired order. Within the
1880 We walk a window of revisions in the desired order. Within the
1881 window, we first walk forwards to gather data, then in the desired
1881 window, we first walk forwards to gather data, then in the desired
1882 order (usually backwards) to display it.
1882 order (usually backwards) to display it.
1883
1883
1884 This function returns an iterator yielding contexts. Before
1884 This function returns an iterator yielding contexts. Before
1885 yielding each context, the iterator will first call the prepare
1885 yielding each context, the iterator will first call the prepare
1886 function on each context in the window in forward order.'''
1886 function on each context in the window in forward order.'''
1887
1887
1888 allfiles = opts.get('all_files')
1888 allfiles = opts.get('all_files')
1889 follow = opts.get('follow') or opts.get('follow_first')
1889 follow = opts.get('follow') or opts.get('follow_first')
1890 revs = _walkrevs(repo, opts)
1890 revs = _walkrevs(repo, opts)
1891 if not revs:
1891 if not revs:
1892 return []
1892 return []
1893 wanted = set()
1893 wanted = set()
1894 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
1894 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
1895 fncache = {}
1895 fncache = {}
1896 change = repo.__getitem__
1896 change = repo.__getitem__
1897
1897
1898 # First step is to fill wanted, the set of revisions that we want to yield.
1898 # First step is to fill wanted, the set of revisions that we want to yield.
1899 # When it does not induce extra cost, we also fill fncache for revisions in
1899 # When it does not induce extra cost, we also fill fncache for revisions in
1900 # wanted: a cache of filenames that were changed (ctx.files()) and that
1900 # wanted: a cache of filenames that were changed (ctx.files()) and that
1901 # match the file filtering conditions.
1901 # match the file filtering conditions.
1902
1902
1903 if match.always() or allfiles:
1903 if match.always() or allfiles:
1904 # No files, no patterns. Display all revs.
1904 # No files, no patterns. Display all revs.
1905 wanted = revs
1905 wanted = revs
1906 elif not slowpath:
1906 elif not slowpath:
1907 # We only have to read through the filelog to find wanted revisions
1907 # We only have to read through the filelog to find wanted revisions
1908
1908
1909 try:
1909 try:
1910 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1910 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1911 except FileWalkError:
1911 except FileWalkError:
1912 slowpath = True
1912 slowpath = True
1913
1913
1914 # We decided to fall back to the slowpath because at least one
1914 # We decided to fall back to the slowpath because at least one
1915 # of the paths was not a file. Check to see if at least one of them
1915 # of the paths was not a file. Check to see if at least one of them
1916 # existed in history, otherwise simply return
1916 # existed in history, otherwise simply return
1917 for path in match.files():
1917 for path in match.files():
1918 if path == '.' or path in repo.store:
1918 if path == '.' or path in repo.store:
1919 break
1919 break
1920 else:
1920 else:
1921 return []
1921 return []
1922
1922
1923 if slowpath:
1923 if slowpath:
1924 # We have to read the changelog to match filenames against
1924 # We have to read the changelog to match filenames against
1925 # changed files
1925 # changed files
1926
1926
1927 if follow:
1927 if follow:
1928 raise error.Abort(_('can only follow copies/renames for explicit '
1928 raise error.Abort(_('can only follow copies/renames for explicit '
1929 'filenames'))
1929 'filenames'))
1930
1930
1931 # The slow path checks files modified in every changeset.
1931 # The slow path checks files modified in every changeset.
1932 # This is really slow on large repos, so compute the set lazily.
1932 # This is really slow on large repos, so compute the set lazily.
1933 class lazywantedset(object):
1933 class lazywantedset(object):
1934 def __init__(self):
1934 def __init__(self):
1935 self.set = set()
1935 self.set = set()
1936 self.revs = set(revs)
1936 self.revs = set(revs)
1937
1937
1938 # No need to worry about locality here because it will be accessed
1938 # No need to worry about locality here because it will be accessed
1939 # in the same order as the increasing window below.
1939 # in the same order as the increasing window below.
1940 def __contains__(self, value):
1940 def __contains__(self, value):
1941 if value in self.set:
1941 if value in self.set:
1942 return True
1942 return True
1943 elif not value in self.revs:
1943 elif not value in self.revs:
1944 return False
1944 return False
1945 else:
1945 else:
1946 self.revs.discard(value)
1946 self.revs.discard(value)
1947 ctx = change(value)
1947 ctx = change(value)
1948 if allfiles:
1948 if allfiles:
1949 matches = list(ctx.manifest().walk(match))
1949 matches = list(ctx.manifest().walk(match))
1950 else:
1950 else:
1951 matches = [f for f in ctx.files() if match(f)]
1951 matches = [f for f in ctx.files() if match(f)]
1952 if matches:
1952 if matches:
1953 fncache[value] = matches
1953 fncache[value] = matches
1954 self.set.add(value)
1954 self.set.add(value)
1955 return True
1955 return True
1956 return False
1956 return False
1957
1957
1958 def discard(self, value):
1958 def discard(self, value):
1959 self.revs.discard(value)
1959 self.revs.discard(value)
1960 self.set.discard(value)
1960 self.set.discard(value)
1961
1961
1962 wanted = lazywantedset()
1962 wanted = lazywantedset()
1963
1963
1964 # it might be worthwhile to do this in the iterator if the rev range
1964 # it might be worthwhile to do this in the iterator if the rev range
1965 # is descending and the prune args are all within that range
1965 # is descending and the prune args are all within that range
1966 for rev in opts.get('prune', ()):
1966 for rev in opts.get('prune', ()):
1967 rev = repo[rev].rev()
1967 rev = repo[rev].rev()
1968 ff = _followfilter(repo)
1968 ff = _followfilter(repo)
1969 stop = min(revs[0], revs[-1])
1969 stop = min(revs[0], revs[-1])
1970 for x in pycompat.xrange(rev, stop - 1, -1):
1970 for x in pycompat.xrange(rev, stop - 1, -1):
1971 if ff.match(x):
1971 if ff.match(x):
1972 wanted = wanted - [x]
1972 wanted = wanted - [x]
1973
1973
1974 # Now that wanted is correctly initialized, we can iterate over the
1974 # Now that wanted is correctly initialized, we can iterate over the
1975 # revision range, yielding only revisions in wanted.
1975 # revision range, yielding only revisions in wanted.
1976 def iterate():
1976 def iterate():
1977 if follow and match.always():
1977 if follow and match.always():
1978 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1978 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1979 def want(rev):
1979 def want(rev):
1980 return ff.match(rev) and rev in wanted
1980 return ff.match(rev) and rev in wanted
1981 else:
1981 else:
1982 def want(rev):
1982 def want(rev):
1983 return rev in wanted
1983 return rev in wanted
1984
1984
1985 it = iter(revs)
1985 it = iter(revs)
1986 stopiteration = False
1986 stopiteration = False
1987 for windowsize in increasingwindows():
1987 for windowsize in increasingwindows():
1988 nrevs = []
1988 nrevs = []
1989 for i in pycompat.xrange(windowsize):
1989 for i in pycompat.xrange(windowsize):
1990 rev = next(it, None)
1990 rev = next(it, None)
1991 if rev is None:
1991 if rev is None:
1992 stopiteration = True
1992 stopiteration = True
1993 break
1993 break
1994 elif want(rev):
1994 elif want(rev):
1995 nrevs.append(rev)
1995 nrevs.append(rev)
1996 for rev in sorted(nrevs):
1996 for rev in sorted(nrevs):
1997 fns = fncache.get(rev)
1997 fns = fncache.get(rev)
1998 ctx = change(rev)
1998 ctx = change(rev)
1999 if not fns:
1999 if not fns:
2000 def fns_generator():
2000 def fns_generator():
2001 if allfiles:
2001 if allfiles:
2002 fiter = iter(ctx)
2002 fiter = iter(ctx)
2003 else:
2003 else:
2004 fiter = ctx.files()
2004 fiter = ctx.files()
2005 for f in fiter:
2005 for f in fiter:
2006 if match(f):
2006 if match(f):
2007 yield f
2007 yield f
2008 fns = fns_generator()
2008 fns = fns_generator()
2009 prepare(ctx, fns)
2009 prepare(ctx, fns)
2010 for rev in nrevs:
2010 for rev in nrevs:
2011 yield change(rev)
2011 yield change(rev)
2012
2012
2013 if stopiteration:
2013 if stopiteration:
2014 break
2014 break
2015
2015
2016 return iterate()
2016 return iterate()
2017
2017
2018 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2018 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2019 bad = []
2019 bad = []
2020
2020
2021 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2021 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2022 names = []
2022 names = []
2023 wctx = repo[None]
2023 wctx = repo[None]
2024 cca = None
2024 cca = None
2025 abort, warn = scmutil.checkportabilityalert(ui)
2025 abort, warn = scmutil.checkportabilityalert(ui)
2026 if abort or warn:
2026 if abort or warn:
2027 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2027 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2028
2028
2029 match = repo.narrowmatch(match, includeexact=True)
2029 match = repo.narrowmatch(match, includeexact=True)
2030 badmatch = matchmod.badmatch(match, badfn)
2030 badmatch = matchmod.badmatch(match, badfn)
2031 dirstate = repo.dirstate
2031 dirstate = repo.dirstate
2032 # We don't want to just call wctx.walk here, since it would return a lot of
2032 # We don't want to just call wctx.walk here, since it would return a lot of
2033 # clean files, which we aren't interested in and takes time.
2033 # clean files, which we aren't interested in and takes time.
2034 for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
2034 for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
2035 unknown=True, ignored=False, full=False)):
2035 unknown=True, ignored=False, full=False)):
2036 exact = match.exact(f)
2036 exact = match.exact(f)
2037 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2037 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2038 if cca:
2038 if cca:
2039 cca(f)
2039 cca(f)
2040 names.append(f)
2040 names.append(f)
2041 if ui.verbose or not exact:
2041 if ui.verbose or not exact:
2042 ui.status(_('adding %s\n') % uipathfn(f),
2042 ui.status(_('adding %s\n') % uipathfn(f),
2043 label='ui.addremove.added')
2043 label='ui.addremove.added')
2044
2044
2045 for subpath in sorted(wctx.substate):
2045 for subpath in sorted(wctx.substate):
2046 sub = wctx.sub(subpath)
2046 sub = wctx.sub(subpath)
2047 try:
2047 try:
2048 submatch = matchmod.subdirmatcher(subpath, match)
2048 submatch = matchmod.subdirmatcher(subpath, match)
2049 subprefix = repo.wvfs.reljoin(prefix, subpath)
2049 subprefix = repo.wvfs.reljoin(prefix, subpath)
2050 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2050 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2051 if opts.get(r'subrepos'):
2051 if opts.get(r'subrepos'):
2052 bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, False,
2052 bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, False,
2053 **opts))
2053 **opts))
2054 else:
2054 else:
2055 bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, True,
2055 bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, True,
2056 **opts))
2056 **opts))
2057 except error.LookupError:
2057 except error.LookupError:
2058 ui.status(_("skipping missing subrepository: %s\n")
2058 ui.status(_("skipping missing subrepository: %s\n")
2059 % uipathfn(subpath))
2059 % uipathfn(subpath))
2060
2060
2061 if not opts.get(r'dry_run'):
2061 if not opts.get(r'dry_run'):
2062 rejected = wctx.add(names, prefix)
2062 rejected = wctx.add(names, prefix)
2063 bad.extend(f for f in rejected if f in match.files())
2063 bad.extend(f for f in rejected if f in match.files())
2064 return bad
2064 return bad
2065
2065
2066 def addwebdirpath(repo, serverpath, webconf):
2066 def addwebdirpath(repo, serverpath, webconf):
2067 webconf[serverpath] = repo.root
2067 webconf[serverpath] = repo.root
2068 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2068 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2069
2069
2070 for r in repo.revs('filelog("path:.hgsub")'):
2070 for r in repo.revs('filelog("path:.hgsub")'):
2071 ctx = repo[r]
2071 ctx = repo[r]
2072 for subpath in ctx.substate:
2072 for subpath in ctx.substate:
2073 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2073 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2074
2074
2075 def forget(ui, repo, match, prefix, uipathfn, explicitonly, dryrun,
2075 def forget(ui, repo, match, prefix, uipathfn, explicitonly, dryrun,
2076 interactive):
2076 interactive):
2077 if dryrun and interactive:
2077 if dryrun and interactive:
2078 raise error.Abort(_("cannot specify both --dry-run and --interactive"))
2078 raise error.Abort(_("cannot specify both --dry-run and --interactive"))
2079 bad = []
2079 bad = []
2080 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2080 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2081 wctx = repo[None]
2081 wctx = repo[None]
2082 forgot = []
2082 forgot = []
2083
2083
2084 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2084 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2085 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2085 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2086 if explicitonly:
2086 if explicitonly:
2087 forget = [f for f in forget if match.exact(f)]
2087 forget = [f for f in forget if match.exact(f)]
2088
2088
2089 for subpath in sorted(wctx.substate):
2089 for subpath in sorted(wctx.substate):
2090 sub = wctx.sub(subpath)
2090 sub = wctx.sub(subpath)
2091 submatch = matchmod.subdirmatcher(subpath, match)
2091 submatch = matchmod.subdirmatcher(subpath, match)
2092 subprefix = repo.wvfs.reljoin(prefix, subpath)
2092 subprefix = repo.wvfs.reljoin(prefix, subpath)
2093 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2093 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2094 try:
2094 try:
2095 subbad, subforgot = sub.forget(submatch, subprefix, subuipathfn,
2095 subbad, subforgot = sub.forget(submatch, subprefix, subuipathfn,
2096 dryrun=dryrun,
2096 dryrun=dryrun,
2097 interactive=interactive)
2097 interactive=interactive)
2098 bad.extend([subpath + '/' + f for f in subbad])
2098 bad.extend([subpath + '/' + f for f in subbad])
2099 forgot.extend([subpath + '/' + f for f in subforgot])
2099 forgot.extend([subpath + '/' + f for f in subforgot])
2100 except error.LookupError:
2100 except error.LookupError:
2101 ui.status(_("skipping missing subrepository: %s\n")
2101 ui.status(_("skipping missing subrepository: %s\n")
2102 % uipathfn(subpath))
2102 % uipathfn(subpath))
2103
2103
2104 if not explicitonly:
2104 if not explicitonly:
2105 for f in match.files():
2105 for f in match.files():
2106 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2106 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2107 if f not in forgot:
2107 if f not in forgot:
2108 if repo.wvfs.exists(f):
2108 if repo.wvfs.exists(f):
2109 # Don't complain if the exact case match wasn't given.
2109 # Don't complain if the exact case match wasn't given.
2110 # But don't do this until after checking 'forgot', so
2110 # But don't do this until after checking 'forgot', so
2111 # that subrepo files aren't normalized, and this op is
2111 # that subrepo files aren't normalized, and this op is
2112 # purely from data cached by the status walk above.
2112 # purely from data cached by the status walk above.
2113 if repo.dirstate.normalize(f) in repo.dirstate:
2113 if repo.dirstate.normalize(f) in repo.dirstate:
2114 continue
2114 continue
2115 ui.warn(_('not removing %s: '
2115 ui.warn(_('not removing %s: '
2116 'file is already untracked\n')
2116 'file is already untracked\n')
2117 % uipathfn(f))
2117 % uipathfn(f))
2118 bad.append(f)
2118 bad.append(f)
2119
2119
2120 if interactive:
2120 if interactive:
2121 responses = _('[Ynsa?]'
2121 responses = _('[Ynsa?]'
2122 '$$ &Yes, forget this file'
2122 '$$ &Yes, forget this file'
2123 '$$ &No, skip this file'
2123 '$$ &No, skip this file'
2124 '$$ &Skip remaining files'
2124 '$$ &Skip remaining files'
2125 '$$ Include &all remaining files'
2125 '$$ Include &all remaining files'
2126 '$$ &? (display help)')
2126 '$$ &? (display help)')
2127 for filename in forget[:]:
2127 for filename in forget[:]:
2128 r = ui.promptchoice(_('forget %s %s') %
2128 r = ui.promptchoice(_('forget %s %s') %
2129 (uipathfn(filename), responses))
2129 (uipathfn(filename), responses))
2130 if r == 4: # ?
2130 if r == 4: # ?
2131 while r == 4:
2131 while r == 4:
2132 for c, t in ui.extractchoices(responses)[1]:
2132 for c, t in ui.extractchoices(responses)[1]:
2133 ui.write('%s - %s\n' % (c, encoding.lower(t)))
2133 ui.write('%s - %s\n' % (c, encoding.lower(t)))
2134 r = ui.promptchoice(_('forget %s %s') %
2134 r = ui.promptchoice(_('forget %s %s') %
2135 (uipathfn(filename), responses))
2135 (uipathfn(filename), responses))
2136 if r == 0: # yes
2136 if r == 0: # yes
2137 continue
2137 continue
2138 elif r == 1: # no
2138 elif r == 1: # no
2139 forget.remove(filename)
2139 forget.remove(filename)
2140 elif r == 2: # Skip
2140 elif r == 2: # Skip
2141 fnindex = forget.index(filename)
2141 fnindex = forget.index(filename)
2142 del forget[fnindex:]
2142 del forget[fnindex:]
2143 break
2143 break
2144 elif r == 3: # All
2144 elif r == 3: # All
2145 break
2145 break
2146
2146
2147 for f in forget:
2147 for f in forget:
2148 if ui.verbose or not match.exact(f) or interactive:
2148 if ui.verbose or not match.exact(f) or interactive:
2149 ui.status(_('removing %s\n') % uipathfn(f),
2149 ui.status(_('removing %s\n') % uipathfn(f),
2150 label='ui.addremove.removed')
2150 label='ui.addremove.removed')
2151
2151
2152 if not dryrun:
2152 if not dryrun:
2153 rejected = wctx.forget(forget, prefix)
2153 rejected = wctx.forget(forget, prefix)
2154 bad.extend(f for f in rejected if f in match.files())
2154 bad.extend(f for f in rejected if f in match.files())
2155 forgot.extend(f for f in forget if f not in rejected)
2155 forgot.extend(f for f in forget if f not in rejected)
2156 return bad, forgot
2156 return bad, forgot
2157
2157
2158 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2158 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2159 ret = 1
2159 ret = 1
2160
2160
2161 needsfctx = ui.verbose or {'size', 'flags'} & fm.datahint()
2161 needsfctx = ui.verbose or {'size', 'flags'} & fm.datahint()
2162 for f in ctx.matches(m):
2162 for f in ctx.matches(m):
2163 fm.startitem()
2163 fm.startitem()
2164 fm.context(ctx=ctx)
2164 fm.context(ctx=ctx)
2165 if needsfctx:
2165 if needsfctx:
2166 fc = ctx[f]
2166 fc = ctx[f]
2167 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2167 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2168 fm.data(path=f)
2168 fm.data(path=f)
2169 fm.plain(fmt % uipathfn(f))
2169 fm.plain(fmt % uipathfn(f))
2170 ret = 0
2170 ret = 0
2171
2171
2172 for subpath in sorted(ctx.substate):
2172 for subpath in sorted(ctx.substate):
2173 submatch = matchmod.subdirmatcher(subpath, m)
2173 submatch = matchmod.subdirmatcher(subpath, m)
2174 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2174 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2175 if (subrepos or m.exact(subpath) or any(submatch.files())):
2175 if (subrepos or m.exact(subpath) or any(submatch.files())):
2176 sub = ctx.sub(subpath)
2176 sub = ctx.sub(subpath)
2177 try:
2177 try:
2178 recurse = m.exact(subpath) or subrepos
2178 recurse = m.exact(subpath) or subrepos
2179 if sub.printfiles(ui, submatch, subuipathfn, fm, fmt,
2179 if sub.printfiles(ui, submatch, subuipathfn, fm, fmt,
2180 recurse) == 0:
2180 recurse) == 0:
2181 ret = 0
2181 ret = 0
2182 except error.LookupError:
2182 except error.LookupError:
2183 ui.status(_("skipping missing subrepository: %s\n")
2183 ui.status(_("skipping missing subrepository: %s\n")
2184 % uipathfn(subpath))
2184 % uipathfn(subpath))
2185
2185
2186 return ret
2186 return ret
2187
2187
2188 def remove(ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun,
2188 def remove(ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun,
2189 warnings=None):
2189 warnings=None):
2190 ret = 0
2190 ret = 0
2191 s = repo.status(match=m, clean=True)
2191 s = repo.status(match=m, clean=True)
2192 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2192 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2193
2193
2194 wctx = repo[None]
2194 wctx = repo[None]
2195
2195
2196 if warnings is None:
2196 if warnings is None:
2197 warnings = []
2197 warnings = []
2198 warn = True
2198 warn = True
2199 else:
2199 else:
2200 warn = False
2200 warn = False
2201
2201
2202 subs = sorted(wctx.substate)
2202 subs = sorted(wctx.substate)
2203 progress = ui.makeprogress(_('searching'), total=len(subs),
2203 progress = ui.makeprogress(_('searching'), total=len(subs),
2204 unit=_('subrepos'))
2204 unit=_('subrepos'))
2205 for subpath in subs:
2205 for subpath in subs:
2206 submatch = matchmod.subdirmatcher(subpath, m)
2206 submatch = matchmod.subdirmatcher(subpath, m)
2207 subprefix = repo.wvfs.reljoin(prefix, subpath)
2207 subprefix = repo.wvfs.reljoin(prefix, subpath)
2208 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2208 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2209 if subrepos or m.exact(subpath) or any(submatch.files()):
2209 if subrepos or m.exact(subpath) or any(submatch.files()):
2210 progress.increment()
2210 progress.increment()
2211 sub = wctx.sub(subpath)
2211 sub = wctx.sub(subpath)
2212 try:
2212 try:
2213 if sub.removefiles(submatch, subprefix, subuipathfn, after,
2213 if sub.removefiles(submatch, subprefix, subuipathfn, after,
2214 force, subrepos, dryrun, warnings):
2214 force, subrepos, dryrun, warnings):
2215 ret = 1
2215 ret = 1
2216 except error.LookupError:
2216 except error.LookupError:
2217 warnings.append(_("skipping missing subrepository: %s\n")
2217 warnings.append(_("skipping missing subrepository: %s\n")
2218 % uipathfn(subpath))
2218 % uipathfn(subpath))
2219 progress.complete()
2219 progress.complete()
2220
2220
2221 # warn about failure to delete explicit files/dirs
2221 # warn about failure to delete explicit files/dirs
2222 deleteddirs = util.dirs(deleted)
2222 deleteddirs = util.dirs(deleted)
2223 files = m.files()
2223 files = m.files()
2224 progress = ui.makeprogress(_('deleting'), total=len(files),
2224 progress = ui.makeprogress(_('deleting'), total=len(files),
2225 unit=_('files'))
2225 unit=_('files'))
2226 for f in files:
2226 for f in files:
2227 def insubrepo():
2227 def insubrepo():
2228 for subpath in wctx.substate:
2228 for subpath in wctx.substate:
2229 if f.startswith(subpath + '/'):
2229 if f.startswith(subpath + '/'):
2230 return True
2230 return True
2231 return False
2231 return False
2232
2232
2233 progress.increment()
2233 progress.increment()
2234 isdir = f in deleteddirs or wctx.hasdir(f)
2234 isdir = f in deleteddirs or wctx.hasdir(f)
2235 if (f in repo.dirstate or isdir or f == '.'
2235 if (f in repo.dirstate or isdir or f == '.'
2236 or insubrepo() or f in subs):
2236 or insubrepo() or f in subs):
2237 continue
2237 continue
2238
2238
2239 if repo.wvfs.exists(f):
2239 if repo.wvfs.exists(f):
2240 if repo.wvfs.isdir(f):
2240 if repo.wvfs.isdir(f):
2241 warnings.append(_('not removing %s: no tracked files\n')
2241 warnings.append(_('not removing %s: no tracked files\n')
2242 % uipathfn(f))
2242 % uipathfn(f))
2243 else:
2243 else:
2244 warnings.append(_('not removing %s: file is untracked\n')
2244 warnings.append(_('not removing %s: file is untracked\n')
2245 % uipathfn(f))
2245 % uipathfn(f))
2246 # missing files will generate a warning elsewhere
2246 # missing files will generate a warning elsewhere
2247 ret = 1
2247 ret = 1
2248 progress.complete()
2248 progress.complete()
2249
2249
2250 if force:
2250 if force:
2251 list = modified + deleted + clean + added
2251 list = modified + deleted + clean + added
2252 elif after:
2252 elif after:
2253 list = deleted
2253 list = deleted
2254 remaining = modified + added + clean
2254 remaining = modified + added + clean
2255 progress = ui.makeprogress(_('skipping'), total=len(remaining),
2255 progress = ui.makeprogress(_('skipping'), total=len(remaining),
2256 unit=_('files'))
2256 unit=_('files'))
2257 for f in remaining:
2257 for f in remaining:
2258 progress.increment()
2258 progress.increment()
2259 if ui.verbose or (f in files):
2259 if ui.verbose or (f in files):
2260 warnings.append(_('not removing %s: file still exists\n')
2260 warnings.append(_('not removing %s: file still exists\n')
2261 % uipathfn(f))
2261 % uipathfn(f))
2262 ret = 1
2262 ret = 1
2263 progress.complete()
2263 progress.complete()
2264 else:
2264 else:
2265 list = deleted + clean
2265 list = deleted + clean
2266 progress = ui.makeprogress(_('skipping'),
2266 progress = ui.makeprogress(_('skipping'),
2267 total=(len(modified) + len(added)),
2267 total=(len(modified) + len(added)),
2268 unit=_('files'))
2268 unit=_('files'))
2269 for f in modified:
2269 for f in modified:
2270 progress.increment()
2270 progress.increment()
2271 warnings.append(_('not removing %s: file is modified (use -f'
2271 warnings.append(_('not removing %s: file is modified (use -f'
2272 ' to force removal)\n') % uipathfn(f))
2272 ' to force removal)\n') % uipathfn(f))
2273 ret = 1
2273 ret = 1
2274 for f in added:
2274 for f in added:
2275 progress.increment()
2275 progress.increment()
2276 warnings.append(_("not removing %s: file has been marked for add"
2276 warnings.append(_("not removing %s: file has been marked for add"
2277 " (use 'hg forget' to undo add)\n") % uipathfn(f))
2277 " (use 'hg forget' to undo add)\n") % uipathfn(f))
2278 ret = 1
2278 ret = 1
2279 progress.complete()
2279 progress.complete()
2280
2280
2281 list = sorted(list)
2281 list = sorted(list)
2282 progress = ui.makeprogress(_('deleting'), total=len(list),
2282 progress = ui.makeprogress(_('deleting'), total=len(list),
2283 unit=_('files'))
2283 unit=_('files'))
2284 for f in list:
2284 for f in list:
2285 if ui.verbose or not m.exact(f):
2285 if ui.verbose or not m.exact(f):
2286 progress.increment()
2286 progress.increment()
2287 ui.status(_('removing %s\n') % uipathfn(f),
2287 ui.status(_('removing %s\n') % uipathfn(f),
2288 label='ui.addremove.removed')
2288 label='ui.addremove.removed')
2289 progress.complete()
2289 progress.complete()
2290
2290
2291 if not dryrun:
2291 if not dryrun:
2292 with repo.wlock():
2292 with repo.wlock():
2293 if not after:
2293 if not after:
2294 for f in list:
2294 for f in list:
2295 if f in added:
2295 if f in added:
2296 continue # we never unlink added files on remove
2296 continue # we never unlink added files on remove
2297 rmdir = repo.ui.configbool('experimental',
2297 rmdir = repo.ui.configbool('experimental',
2298 'removeemptydirs')
2298 'removeemptydirs')
2299 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2299 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2300 repo[None].forget(list)
2300 repo[None].forget(list)
2301
2301
2302 if warn:
2302 if warn:
2303 for warning in warnings:
2303 for warning in warnings:
2304 ui.warn(warning)
2304 ui.warn(warning)
2305
2305
2306 return ret
2306 return ret
2307
2307
2308 def _catfmtneedsdata(fm):
2308 def _catfmtneedsdata(fm):
2309 return not fm.datahint() or 'data' in fm.datahint()
2309 return not fm.datahint() or 'data' in fm.datahint()
2310
2310
2311 def _updatecatformatter(fm, ctx, matcher, path, decode):
2311 def _updatecatformatter(fm, ctx, matcher, path, decode):
2312 """Hook for adding data to the formatter used by ``hg cat``.
2312 """Hook for adding data to the formatter used by ``hg cat``.
2313
2313
2314 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2314 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2315 this method first."""
2315 this method first."""
2316
2316
2317 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2317 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2318 # wasn't requested.
2318 # wasn't requested.
2319 data = b''
2319 data = b''
2320 if _catfmtneedsdata(fm):
2320 if _catfmtneedsdata(fm):
2321 data = ctx[path].data()
2321 data = ctx[path].data()
2322 if decode:
2322 if decode:
2323 data = ctx.repo().wwritedata(path, data)
2323 data = ctx.repo().wwritedata(path, data)
2324 fm.startitem()
2324 fm.startitem()
2325 fm.context(ctx=ctx)
2325 fm.context(ctx=ctx)
2326 fm.write('data', '%s', data)
2326 fm.write('data', '%s', data)
2327 fm.data(path=path)
2327 fm.data(path=path)
2328
2328
2329 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2329 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2330 err = 1
2330 err = 1
2331 opts = pycompat.byteskwargs(opts)
2331 opts = pycompat.byteskwargs(opts)
2332
2332
2333 def write(path):
2333 def write(path):
2334 filename = None
2334 filename = None
2335 if fntemplate:
2335 if fntemplate:
2336 filename = makefilename(ctx, fntemplate,
2336 filename = makefilename(ctx, fntemplate,
2337 pathname=os.path.join(prefix, path))
2337 pathname=os.path.join(prefix, path))
2338 # attempt to create the directory if it does not already exist
2338 # attempt to create the directory if it does not already exist
2339 try:
2339 try:
2340 os.makedirs(os.path.dirname(filename))
2340 os.makedirs(os.path.dirname(filename))
2341 except OSError:
2341 except OSError:
2342 pass
2342 pass
2343 with formatter.maybereopen(basefm, filename) as fm:
2343 with formatter.maybereopen(basefm, filename) as fm:
2344 _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
2344 _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
2345
2345
2346 # Automation often uses hg cat on single files, so special case it
2346 # Automation often uses hg cat on single files, so special case it
2347 # for performance to avoid the cost of parsing the manifest.
2347 # for performance to avoid the cost of parsing the manifest.
2348 if len(matcher.files()) == 1 and not matcher.anypats():
2348 if len(matcher.files()) == 1 and not matcher.anypats():
2349 file = matcher.files()[0]
2349 file = matcher.files()[0]
2350 mfl = repo.manifestlog
2350 mfl = repo.manifestlog
2351 mfnode = ctx.manifestnode()
2351 mfnode = ctx.manifestnode()
2352 try:
2352 try:
2353 if mfnode and mfl[mfnode].find(file)[0]:
2353 if mfnode and mfl[mfnode].find(file)[0]:
2354 if _catfmtneedsdata(basefm):
2354 if _catfmtneedsdata(basefm):
2355 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2355 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2356 write(file)
2356 write(file)
2357 return 0
2357 return 0
2358 except KeyError:
2358 except KeyError:
2359 pass
2359 pass
2360
2360
2361 if _catfmtneedsdata(basefm):
2361 if _catfmtneedsdata(basefm):
2362 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2362 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2363
2363
2364 for abs in ctx.walk(matcher):
2364 for abs in ctx.walk(matcher):
2365 write(abs)
2365 write(abs)
2366 err = 0
2366 err = 0
2367
2367
2368 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2368 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2369 for subpath in sorted(ctx.substate):
2369 for subpath in sorted(ctx.substate):
2370 sub = ctx.sub(subpath)
2370 sub = ctx.sub(subpath)
2371 try:
2371 try:
2372 submatch = matchmod.subdirmatcher(subpath, matcher)
2372 submatch = matchmod.subdirmatcher(subpath, matcher)
2373 subprefix = os.path.join(prefix, subpath)
2373 subprefix = os.path.join(prefix, subpath)
2374 if not sub.cat(submatch, basefm, fntemplate, subprefix,
2374 if not sub.cat(submatch, basefm, fntemplate, subprefix,
2375 **pycompat.strkwargs(opts)):
2375 **pycompat.strkwargs(opts)):
2376 err = 0
2376 err = 0
2377 except error.RepoLookupError:
2377 except error.RepoLookupError:
2378 ui.status(_("skipping missing subrepository: %s\n") %
2378 ui.status(_("skipping missing subrepository: %s\n") %
2379 uipathfn(subpath))
2379 uipathfn(subpath))
2380
2380
2381 return err
2381 return err
2382
2382
2383 def commit(ui, repo, commitfunc, pats, opts):
2383 def commit(ui, repo, commitfunc, pats, opts):
2384 '''commit the specified files or all outstanding changes'''
2384 '''commit the specified files or all outstanding changes'''
2385 date = opts.get('date')
2385 date = opts.get('date')
2386 if date:
2386 if date:
2387 opts['date'] = dateutil.parsedate(date)
2387 opts['date'] = dateutil.parsedate(date)
2388 message = logmessage(ui, opts)
2388 message = logmessage(ui, opts)
2389 matcher = scmutil.match(repo[None], pats, opts)
2389 matcher = scmutil.match(repo[None], pats, opts)
2390
2390
2391 dsguard = None
2391 dsguard = None
2392 # extract addremove carefully -- this function can be called from a command
2392 # extract addremove carefully -- this function can be called from a command
2393 # that doesn't support addremove
2393 # that doesn't support addremove
2394 if opts.get('addremove'):
2394 if opts.get('addremove'):
2395 dsguard = dirstateguard.dirstateguard(repo, 'commit')
2395 dsguard = dirstateguard.dirstateguard(repo, 'commit')
2396 with dsguard or util.nullcontextmanager():
2396 with dsguard or util.nullcontextmanager():
2397 if dsguard:
2397 if dsguard:
2398 relative = scmutil.anypats(pats, opts)
2398 relative = scmutil.anypats(pats, opts)
2399 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2399 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2400 if scmutil.addremove(repo, matcher, "", uipathfn, opts) != 0:
2400 if scmutil.addremove(repo, matcher, "", uipathfn, opts) != 0:
2401 raise error.Abort(
2401 raise error.Abort(
2402 _("failed to mark all new/missing files as added/removed"))
2402 _("failed to mark all new/missing files as added/removed"))
2403
2403
2404 return commitfunc(ui, repo, message, matcher, opts)
2404 return commitfunc(ui, repo, message, matcher, opts)
2405
2405
2406 def samefile(f, ctx1, ctx2):
2406 def samefile(f, ctx1, ctx2):
2407 if f in ctx1.manifest():
2407 if f in ctx1.manifest():
2408 a = ctx1.filectx(f)
2408 a = ctx1.filectx(f)
2409 if f in ctx2.manifest():
2409 if f in ctx2.manifest():
2410 b = ctx2.filectx(f)
2410 b = ctx2.filectx(f)
2411 return (not a.cmp(b)
2411 return (not a.cmp(b)
2412 and a.flags() == b.flags())
2412 and a.flags() == b.flags())
2413 else:
2413 else:
2414 return False
2414 return False
2415 else:
2415 else:
2416 return f not in ctx2.manifest()
2416 return f not in ctx2.manifest()
2417
2417
2418 def amend(ui, repo, old, extra, pats, opts):
2418 def amend(ui, repo, old, extra, pats, opts):
2419 # avoid cycle context -> subrepo -> cmdutil
2419 # avoid cycle context -> subrepo -> cmdutil
2420 from . import context
2420 from . import context
2421
2421
2422 # amend will reuse the existing user if not specified, but the obsolete
2422 # amend will reuse the existing user if not specified, but the obsolete
2423 # marker creation requires that the current user's name is specified.
2423 # marker creation requires that the current user's name is specified.
2424 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2424 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2425 ui.username() # raise exception if username not set
2425 ui.username() # raise exception if username not set
2426
2426
2427 ui.note(_('amending changeset %s\n') % old)
2427 ui.note(_('amending changeset %s\n') % old)
2428 base = old.p1()
2428 base = old.p1()
2429
2429
2430 with repo.wlock(), repo.lock(), repo.transaction('amend'):
2430 with repo.wlock(), repo.lock(), repo.transaction('amend'):
2431 # Participating changesets:
2431 # Participating changesets:
2432 #
2432 #
2433 # wctx o - workingctx that contains changes from working copy
2433 # wctx o - workingctx that contains changes from working copy
2434 # | to go into amending commit
2434 # | to go into amending commit
2435 # |
2435 # |
2436 # old o - changeset to amend
2436 # old o - changeset to amend
2437 # |
2437 # |
2438 # base o - first parent of the changeset to amend
2438 # base o - first parent of the changeset to amend
2439 wctx = repo[None]
2439 wctx = repo[None]
2440
2440
2441 # Copy to avoid mutating input
2441 # Copy to avoid mutating input
2442 extra = extra.copy()
2442 extra = extra.copy()
2443 # Update extra dict from amended commit (e.g. to preserve graft
2443 # Update extra dict from amended commit (e.g. to preserve graft
2444 # source)
2444 # source)
2445 extra.update(old.extra())
2445 extra.update(old.extra())
2446
2446
2447 # Also update it from the from the wctx
2447 # Also update it from the from the wctx
2448 extra.update(wctx.extra())
2448 extra.update(wctx.extra())
2449
2449
2450 user = opts.get('user') or old.user()
2450 user = opts.get('user') or old.user()
2451
2451
2452 datemaydiffer = False # date-only change should be ignored?
2452 datemaydiffer = False # date-only change should be ignored?
2453 if opts.get('date') and opts.get('currentdate'):
2453 if opts.get('date') and opts.get('currentdate'):
2454 raise error.Abort(_('--date and --currentdate are mutually '
2454 raise error.Abort(_('--date and --currentdate are mutually '
2455 'exclusive'))
2455 'exclusive'))
2456 if opts.get('date'):
2456 if opts.get('date'):
2457 date = dateutil.parsedate(opts.get('date'))
2457 date = dateutil.parsedate(opts.get('date'))
2458 elif opts.get('currentdate'):
2458 elif opts.get('currentdate'):
2459 date = dateutil.makedate()
2459 date = dateutil.makedate()
2460 elif (ui.configbool('rewrite', 'update-timestamp')
2460 elif (ui.configbool('rewrite', 'update-timestamp')
2461 and opts.get('currentdate') is None):
2461 and opts.get('currentdate') is None):
2462 date = dateutil.makedate()
2462 date = dateutil.makedate()
2463 datemaydiffer = True
2463 datemaydiffer = True
2464 else:
2464 else:
2465 date = old.date()
2465 date = old.date()
2466
2466
2467 if len(old.parents()) > 1:
2467 if len(old.parents()) > 1:
2468 # ctx.files() isn't reliable for merges, so fall back to the
2468 # ctx.files() isn't reliable for merges, so fall back to the
2469 # slower repo.status() method
2469 # slower repo.status() method
2470 files = {fn for st in base.status(old)[:3] for fn in st}
2470 files = {fn for st in base.status(old)[:3] for fn in st}
2471 else:
2471 else:
2472 files = set(old.files())
2472 files = set(old.files())
2473
2473
2474 # add/remove the files to the working copy if the "addremove" option
2474 # add/remove the files to the working copy if the "addremove" option
2475 # was specified.
2475 # was specified.
2476 matcher = scmutil.match(wctx, pats, opts)
2476 matcher = scmutil.match(wctx, pats, opts)
2477 relative = scmutil.anypats(pats, opts)
2477 relative = scmutil.anypats(pats, opts)
2478 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2478 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2479 if (opts.get('addremove')
2479 if (opts.get('addremove')
2480 and scmutil.addremove(repo, matcher, "", uipathfn, opts)):
2480 and scmutil.addremove(repo, matcher, "", uipathfn, opts)):
2481 raise error.Abort(
2481 raise error.Abort(
2482 _("failed to mark all new/missing files as added/removed"))
2482 _("failed to mark all new/missing files as added/removed"))
2483
2483
2484 # Check subrepos. This depends on in-place wctx._status update in
2484 # Check subrepos. This depends on in-place wctx._status update in
2485 # subrepo.precommit(). To minimize the risk of this hack, we do
2485 # subrepo.precommit(). To minimize the risk of this hack, we do
2486 # nothing if .hgsub does not exist.
2486 # nothing if .hgsub does not exist.
2487 if '.hgsub' in wctx or '.hgsub' in old:
2487 if '.hgsub' in wctx or '.hgsub' in old:
2488 subs, commitsubs, newsubstate = subrepoutil.precommit(
2488 subs, commitsubs, newsubstate = subrepoutil.precommit(
2489 ui, wctx, wctx._status, matcher)
2489 ui, wctx, wctx._status, matcher)
2490 # amend should abort if commitsubrepos is enabled
2490 # amend should abort if commitsubrepos is enabled
2491 assert not commitsubs
2491 assert not commitsubs
2492 if subs:
2492 if subs:
2493 subrepoutil.writestate(repo, newsubstate)
2493 subrepoutil.writestate(repo, newsubstate)
2494
2494
2495 ms = mergemod.mergestate.read(repo)
2495 ms = mergemod.mergestate.read(repo)
2496 mergeutil.checkunresolved(ms)
2496 mergeutil.checkunresolved(ms)
2497
2497
2498 filestoamend = set(f for f in wctx.files() if matcher(f))
2498 filestoamend = set(f for f in wctx.files() if matcher(f))
2499
2499
2500 changes = (len(filestoamend) > 0)
2500 changes = (len(filestoamend) > 0)
2501 if changes:
2501 if changes:
2502 # Recompute copies (avoid recording a -> b -> a)
2502 # Recompute copies (avoid recording a -> b -> a)
2503 copied = copies.pathcopies(base, wctx, matcher)
2503 copied = copies.pathcopies(base, wctx, matcher)
2504 if old.p2:
2504 if old.p2:
2505 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2505 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2506
2506
2507 # Prune files which were reverted by the updates: if old
2507 # Prune files which were reverted by the updates: if old
2508 # introduced file X and the file was renamed in the working
2508 # introduced file X and the file was renamed in the working
2509 # copy, then those two files are the same and
2509 # copy, then those two files are the same and
2510 # we can discard X from our list of files. Likewise if X
2510 # we can discard X from our list of files. Likewise if X
2511 # was removed, it's no longer relevant. If X is missing (aka
2511 # was removed, it's no longer relevant. If X is missing (aka
2512 # deleted), old X must be preserved.
2512 # deleted), old X must be preserved.
2513 files.update(filestoamend)
2513 files.update(filestoamend)
2514 files = [f for f in files if (f not in filestoamend
2514 files = [f for f in files if (f not in filestoamend
2515 or not samefile(f, wctx, base))]
2515 or not samefile(f, wctx, base))]
2516
2516
2517 def filectxfn(repo, ctx_, path):
2517 def filectxfn(repo, ctx_, path):
2518 try:
2518 try:
2519 # If the file being considered is not amongst the files
2519 # If the file being considered is not amongst the files
2520 # to be amended, we should return the file context from the
2520 # to be amended, we should return the file context from the
2521 # old changeset. This avoids issues when only some files in
2521 # old changeset. This avoids issues when only some files in
2522 # the working copy are being amended but there are also
2522 # the working copy are being amended but there are also
2523 # changes to other files from the old changeset.
2523 # changes to other files from the old changeset.
2524 if path not in filestoamend:
2524 if path not in filestoamend:
2525 return old.filectx(path)
2525 return old.filectx(path)
2526
2526
2527 # Return None for removed files.
2527 # Return None for removed files.
2528 if path in wctx.removed():
2528 if path in wctx.removed():
2529 return None
2529 return None
2530
2530
2531 fctx = wctx[path]
2531 fctx = wctx[path]
2532 flags = fctx.flags()
2532 flags = fctx.flags()
2533 mctx = context.memfilectx(repo, ctx_,
2533 mctx = context.memfilectx(repo, ctx_,
2534 fctx.path(), fctx.data(),
2534 fctx.path(), fctx.data(),
2535 islink='l' in flags,
2535 islink='l' in flags,
2536 isexec='x' in flags,
2536 isexec='x' in flags,
2537 copysource=copied.get(path))
2537 copysource=copied.get(path))
2538 return mctx
2538 return mctx
2539 except KeyError:
2539 except KeyError:
2540 return None
2540 return None
2541 else:
2541 else:
2542 ui.note(_('copying changeset %s to %s\n') % (old, base))
2542 ui.note(_('copying changeset %s to %s\n') % (old, base))
2543
2543
2544 # Use version of files as in the old cset
2544 # Use version of files as in the old cset
2545 def filectxfn(repo, ctx_, path):
2545 def filectxfn(repo, ctx_, path):
2546 try:
2546 try:
2547 return old.filectx(path)
2547 return old.filectx(path)
2548 except KeyError:
2548 except KeyError:
2549 return None
2549 return None
2550
2550
2551 # See if we got a message from -m or -l, if not, open the editor with
2551 # See if we got a message from -m or -l, if not, open the editor with
2552 # the message of the changeset to amend.
2552 # the message of the changeset to amend.
2553 message = logmessage(ui, opts)
2553 message = logmessage(ui, opts)
2554
2554
2555 editform = mergeeditform(old, 'commit.amend')
2555 editform = mergeeditform(old, 'commit.amend')
2556
2556
2557 if not message:
2557 if not message:
2558 message = old.description()
2558 message = old.description()
2559 # Default if message isn't provided and --edit is not passed is to
2559 # Default if message isn't provided and --edit is not passed is to
2560 # invoke editor, but allow --no-edit. If somehow we don't have any
2560 # invoke editor, but allow --no-edit. If somehow we don't have any
2561 # description, let's always start the editor.
2561 # description, let's always start the editor.
2562 doedit = not message or opts.get('edit') in [True, None]
2562 doedit = not message or opts.get('edit') in [True, None]
2563 else:
2563 else:
2564 # Default if message is provided is to not invoke editor, but allow
2564 # Default if message is provided is to not invoke editor, but allow
2565 # --edit.
2565 # --edit.
2566 doedit = opts.get('edit') is True
2566 doedit = opts.get('edit') is True
2567 editor = getcommiteditor(edit=doedit, editform=editform)
2567 editor = getcommiteditor(edit=doedit, editform=editform)
2568
2568
2569 pureextra = extra.copy()
2569 pureextra = extra.copy()
2570 extra['amend_source'] = old.hex()
2570 extra['amend_source'] = old.hex()
2571
2571
2572 new = context.memctx(repo,
2572 new = context.memctx(repo,
2573 parents=[base.node(), old.p2().node()],
2573 parents=[base.node(), old.p2().node()],
2574 text=message,
2574 text=message,
2575 files=files,
2575 files=files,
2576 filectxfn=filectxfn,
2576 filectxfn=filectxfn,
2577 user=user,
2577 user=user,
2578 date=date,
2578 date=date,
2579 extra=extra,
2579 extra=extra,
2580 editor=editor)
2580 editor=editor)
2581
2581
2582 newdesc = changelog.stripdesc(new.description())
2582 newdesc = changelog.stripdesc(new.description())
2583 if ((not changes)
2583 if ((not changes)
2584 and newdesc == old.description()
2584 and newdesc == old.description()
2585 and user == old.user()
2585 and user == old.user()
2586 and (date == old.date() or datemaydiffer)
2586 and (date == old.date() or datemaydiffer)
2587 and pureextra == old.extra()):
2587 and pureextra == old.extra()):
2588 # nothing changed. continuing here would create a new node
2588 # nothing changed. continuing here would create a new node
2589 # anyway because of the amend_source noise.
2589 # anyway because of the amend_source noise.
2590 #
2590 #
2591 # This not what we expect from amend.
2591 # This not what we expect from amend.
2592 return old.node()
2592 return old.node()
2593
2593
2594 commitphase = None
2594 commitphase = None
2595 if opts.get('secret'):
2595 if opts.get('secret'):
2596 commitphase = phases.secret
2596 commitphase = phases.secret
2597 newid = repo.commitctx(new)
2597 newid = repo.commitctx(new)
2598
2598
2599 # Reroute the working copy parent to the new changeset
2599 # Reroute the working copy parent to the new changeset
2600 repo.setparents(newid, nullid)
2600 repo.setparents(newid, nullid)
2601 mapping = {old.node(): (newid,)}
2601 mapping = {old.node(): (newid,)}
2602 obsmetadata = None
2602 obsmetadata = None
2603 if opts.get('note'):
2603 if opts.get('note'):
2604 obsmetadata = {'note': encoding.fromlocal(opts['note'])}
2604 obsmetadata = {'note': encoding.fromlocal(opts['note'])}
2605 backup = ui.configbool('rewrite', 'backup-bundle')
2605 backup = ui.configbool('rewrite', 'backup-bundle')
2606 scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata,
2606 scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata,
2607 fixphase=True, targetphase=commitphase,
2607 fixphase=True, targetphase=commitphase,
2608 backup=backup)
2608 backup=backup)
2609
2609
2610 # Fixing the dirstate because localrepo.commitctx does not update
2610 # Fixing the dirstate because localrepo.commitctx does not update
2611 # it. This is rather convenient because we did not need to update
2611 # it. This is rather convenient because we did not need to update
2612 # the dirstate for all the files in the new commit which commitctx
2612 # the dirstate for all the files in the new commit which commitctx
2613 # could have done if it updated the dirstate. Now, we can
2613 # could have done if it updated the dirstate. Now, we can
2614 # selectively update the dirstate only for the amended files.
2614 # selectively update the dirstate only for the amended files.
2615 dirstate = repo.dirstate
2615 dirstate = repo.dirstate
2616
2616
2617 # Update the state of the files which were added and
2617 # Update the state of the files which were added and
2618 # and modified in the amend to "normal" in the dirstate.
2618 # and modified in the amend to "normal" in the dirstate.
2619 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2619 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2620 for f in normalfiles:
2620 for f in normalfiles:
2621 dirstate.normal(f)
2621 dirstate.normal(f)
2622
2622
2623 # Update the state of files which were removed in the amend
2623 # Update the state of files which were removed in the amend
2624 # to "removed" in the dirstate.
2624 # to "removed" in the dirstate.
2625 removedfiles = set(wctx.removed()) & filestoamend
2625 removedfiles = set(wctx.removed()) & filestoamend
2626 for f in removedfiles:
2626 for f in removedfiles:
2627 dirstate.drop(f)
2627 dirstate.drop(f)
2628
2628
2629 return newid
2629 return newid
2630
2630
2631 def commiteditor(repo, ctx, subs, editform=''):
2631 def commiteditor(repo, ctx, subs, editform=''):
2632 if ctx.description():
2632 if ctx.description():
2633 return ctx.description()
2633 return ctx.description()
2634 return commitforceeditor(repo, ctx, subs, editform=editform,
2634 return commitforceeditor(repo, ctx, subs, editform=editform,
2635 unchangedmessagedetection=True)
2635 unchangedmessagedetection=True)
2636
2636
2637 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2637 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2638 editform='', unchangedmessagedetection=False):
2638 editform='', unchangedmessagedetection=False):
2639 if not extramsg:
2639 if not extramsg:
2640 extramsg = _("Leave message empty to abort commit.")
2640 extramsg = _("Leave message empty to abort commit.")
2641
2641
2642 forms = [e for e in editform.split('.') if e]
2642 forms = [e for e in editform.split('.') if e]
2643 forms.insert(0, 'changeset')
2643 forms.insert(0, 'changeset')
2644 templatetext = None
2644 templatetext = None
2645 while forms:
2645 while forms:
2646 ref = '.'.join(forms)
2646 ref = '.'.join(forms)
2647 if repo.ui.config('committemplate', ref):
2647 if repo.ui.config('committemplate', ref):
2648 templatetext = committext = buildcommittemplate(
2648 templatetext = committext = buildcommittemplate(
2649 repo, ctx, subs, extramsg, ref)
2649 repo, ctx, subs, extramsg, ref)
2650 break
2650 break
2651 forms.pop()
2651 forms.pop()
2652 else:
2652 else:
2653 committext = buildcommittext(repo, ctx, subs, extramsg)
2653 committext = buildcommittext(repo, ctx, subs, extramsg)
2654
2654
2655 # run editor in the repository root
2655 # run editor in the repository root
2656 olddir = encoding.getcwd()
2656 olddir = encoding.getcwd()
2657 os.chdir(repo.root)
2657 os.chdir(repo.root)
2658
2658
2659 # make in-memory changes visible to external process
2659 # make in-memory changes visible to external process
2660 tr = repo.currenttransaction()
2660 tr = repo.currenttransaction()
2661 repo.dirstate.write(tr)
2661 repo.dirstate.write(tr)
2662 pending = tr and tr.writepending() and repo.root
2662 pending = tr and tr.writepending() and repo.root
2663
2663
2664 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2664 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2665 editform=editform, pending=pending,
2665 editform=editform, pending=pending,
2666 repopath=repo.path, action='commit')
2666 repopath=repo.path, action='commit')
2667 text = editortext
2667 text = editortext
2668
2668
2669 # strip away anything below this special string (used for editors that want
2669 # strip away anything below this special string (used for editors that want
2670 # to display the diff)
2670 # to display the diff)
2671 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2671 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2672 if stripbelow:
2672 if stripbelow:
2673 text = text[:stripbelow.start()]
2673 text = text[:stripbelow.start()]
2674
2674
2675 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2675 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2676 os.chdir(olddir)
2676 os.chdir(olddir)
2677
2677
2678 if finishdesc:
2678 if finishdesc:
2679 text = finishdesc(text)
2679 text = finishdesc(text)
2680 if not text.strip():
2680 if not text.strip():
2681 raise error.Abort(_("empty commit message"))
2681 raise error.Abort(_("empty commit message"))
2682 if unchangedmessagedetection and editortext == templatetext:
2682 if unchangedmessagedetection and editortext == templatetext:
2683 raise error.Abort(_("commit message unchanged"))
2683 raise error.Abort(_("commit message unchanged"))
2684
2684
2685 return text
2685 return text
2686
2686
2687 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
2687 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
2688 ui = repo.ui
2688 ui = repo.ui
2689 spec = formatter.templatespec(ref, None, None)
2689 spec = formatter.templatespec(ref, None, None)
2690 t = logcmdutil.changesettemplater(ui, repo, spec)
2690 t = logcmdutil.changesettemplater(ui, repo, spec)
2691 t.t.cache.update((k, templater.unquotestring(v))
2691 t.t.cache.update((k, templater.unquotestring(v))
2692 for k, v in repo.ui.configitems('committemplate'))
2692 for k, v in repo.ui.configitems('committemplate'))
2693
2693
2694 if not extramsg:
2694 if not extramsg:
2695 extramsg = '' # ensure that extramsg is string
2695 extramsg = '' # ensure that extramsg is string
2696
2696
2697 ui.pushbuffer()
2697 ui.pushbuffer()
2698 t.show(ctx, extramsg=extramsg)
2698 t.show(ctx, extramsg=extramsg)
2699 return ui.popbuffer()
2699 return ui.popbuffer()
2700
2700
2701 def hgprefix(msg):
2701 def hgprefix(msg):
2702 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2702 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2703
2703
2704 def buildcommittext(repo, ctx, subs, extramsg):
2704 def buildcommittext(repo, ctx, subs, extramsg):
2705 edittext = []
2705 edittext = []
2706 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2706 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2707 if ctx.description():
2707 if ctx.description():
2708 edittext.append(ctx.description())
2708 edittext.append(ctx.description())
2709 edittext.append("")
2709 edittext.append("")
2710 edittext.append("") # Empty line between message and comments.
2710 edittext.append("") # Empty line between message and comments.
2711 edittext.append(hgprefix(_("Enter commit message."
2711 edittext.append(hgprefix(_("Enter commit message."
2712 " Lines beginning with 'HG:' are removed.")))
2712 " Lines beginning with 'HG:' are removed.")))
2713 edittext.append(hgprefix(extramsg))
2713 edittext.append(hgprefix(extramsg))
2714 edittext.append("HG: --")
2714 edittext.append("HG: --")
2715 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2715 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2716 if ctx.p2():
2716 if ctx.p2():
2717 edittext.append(hgprefix(_("branch merge")))
2717 edittext.append(hgprefix(_("branch merge")))
2718 if ctx.branch():
2718 if ctx.branch():
2719 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2719 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2720 if bookmarks.isactivewdirparent(repo):
2720 if bookmarks.isactivewdirparent(repo):
2721 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2721 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2722 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2722 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2723 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2723 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2724 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2724 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2725 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2725 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2726 if not added and not modified and not removed:
2726 if not added and not modified and not removed:
2727 edittext.append(hgprefix(_("no files changed")))
2727 edittext.append(hgprefix(_("no files changed")))
2728 edittext.append("")
2728 edittext.append("")
2729
2729
2730 return "\n".join(edittext)
2730 return "\n".join(edittext)
2731
2731
2732 def commitstatus(repo, node, branch, bheads=None, opts=None):
2732 def commitstatus(repo, node, branch, bheads=None, opts=None):
2733 if opts is None:
2733 if opts is None:
2734 opts = {}
2734 opts = {}
2735 ctx = repo[node]
2735 ctx = repo[node]
2736 parents = ctx.parents()
2736 parents = ctx.parents()
2737
2737
2738 if (not opts.get('amend') and bheads and node not in bheads and not
2738 if (not opts.get('amend') and bheads and node not in bheads and not
2739 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2739 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2740 repo.ui.status(_('created new head\n'))
2740 repo.ui.status(_('created new head\n'))
2741 # The message is not printed for initial roots. For the other
2741 # The message is not printed for initial roots. For the other
2742 # changesets, it is printed in the following situations:
2742 # changesets, it is printed in the following situations:
2743 #
2743 #
2744 # Par column: for the 2 parents with ...
2744 # Par column: for the 2 parents with ...
2745 # N: null or no parent
2745 # N: null or no parent
2746 # B: parent is on another named branch
2746 # B: parent is on another named branch
2747 # C: parent is a regular non head changeset
2747 # C: parent is a regular non head changeset
2748 # H: parent was a branch head of the current branch
2748 # H: parent was a branch head of the current branch
2749 # Msg column: whether we print "created new head" message
2749 # Msg column: whether we print "created new head" message
2750 # In the following, it is assumed that there already exists some
2750 # In the following, it is assumed that there already exists some
2751 # initial branch heads of the current branch, otherwise nothing is
2751 # initial branch heads of the current branch, otherwise nothing is
2752 # printed anyway.
2752 # printed anyway.
2753 #
2753 #
2754 # Par Msg Comment
2754 # Par Msg Comment
2755 # N N y additional topo root
2755 # N N y additional topo root
2756 #
2756 #
2757 # B N y additional branch root
2757 # B N y additional branch root
2758 # C N y additional topo head
2758 # C N y additional topo head
2759 # H N n usual case
2759 # H N n usual case
2760 #
2760 #
2761 # B B y weird additional branch root
2761 # B B y weird additional branch root
2762 # C B y branch merge
2762 # C B y branch merge
2763 # H B n merge with named branch
2763 # H B n merge with named branch
2764 #
2764 #
2765 # C C y additional head from merge
2765 # C C y additional head from merge
2766 # C H n merge with a head
2766 # C H n merge with a head
2767 #
2767 #
2768 # H H n head merge: head count decreases
2768 # H H n head merge: head count decreases
2769
2769
2770 if not opts.get('close_branch'):
2770 if not opts.get('close_branch'):
2771 for r in parents:
2771 for r in parents:
2772 if r.closesbranch() and r.branch() == branch:
2772 if r.closesbranch() and r.branch() == branch:
2773 repo.ui.status(_('reopening closed branch head %d\n') % r.rev())
2773 repo.ui.status(_('reopening closed branch head %d\n') % r.rev())
2774
2774
2775 if repo.ui.debugflag:
2775 if repo.ui.debugflag:
2776 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx.hex()))
2776 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx.hex()))
2777 elif repo.ui.verbose:
2777 elif repo.ui.verbose:
2778 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx))
2778 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx))
2779
2779
2780 def postcommitstatus(repo, pats, opts):
2780 def postcommitstatus(repo, pats, opts):
2781 return repo.status(match=scmutil.match(repo[None], pats, opts))
2781 return repo.status(match=scmutil.match(repo[None], pats, opts))
2782
2782
2783 def revert(ui, repo, ctx, parents, *pats, **opts):
2783 def revert(ui, repo, ctx, parents, *pats, **opts):
2784 opts = pycompat.byteskwargs(opts)
2784 opts = pycompat.byteskwargs(opts)
2785 parent, p2 = parents
2785 parent, p2 = parents
2786 node = ctx.node()
2786 node = ctx.node()
2787
2787
2788 mf = ctx.manifest()
2788 mf = ctx.manifest()
2789 if node == p2:
2789 if node == p2:
2790 parent = p2
2790 parent = p2
2791
2791
2792 # need all matching names in dirstate and manifest of target rev,
2792 # need all matching names in dirstate and manifest of target rev,
2793 # so have to walk both. do not print errors if files exist in one
2793 # so have to walk both. do not print errors if files exist in one
2794 # but not other. in both cases, filesets should be evaluated against
2794 # but not other. in both cases, filesets should be evaluated against
2795 # workingctx to get consistent result (issue4497). this means 'set:**'
2795 # workingctx to get consistent result (issue4497). this means 'set:**'
2796 # cannot be used to select missing files from target rev.
2796 # cannot be used to select missing files from target rev.
2797
2797
2798 # `names` is a mapping for all elements in working copy and target revision
2798 # `names` is a mapping for all elements in working copy and target revision
2799 # The mapping is in the form:
2799 # The mapping is in the form:
2800 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2800 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2801 names = {}
2801 names = {}
2802 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2802 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2803
2803
2804 with repo.wlock():
2804 with repo.wlock():
2805 ## filling of the `names` mapping
2805 ## filling of the `names` mapping
2806 # walk dirstate to fill `names`
2806 # walk dirstate to fill `names`
2807
2807
2808 interactive = opts.get('interactive', False)
2808 interactive = opts.get('interactive', False)
2809 wctx = repo[None]
2809 wctx = repo[None]
2810 m = scmutil.match(wctx, pats, opts)
2810 m = scmutil.match(wctx, pats, opts)
2811
2811
2812 # we'll need this later
2812 # we'll need this later
2813 targetsubs = sorted(s for s in wctx.substate if m(s))
2813 targetsubs = sorted(s for s in wctx.substate if m(s))
2814
2814
2815 if not m.always():
2815 if not m.always():
2816 matcher = matchmod.badmatch(m, lambda x, y: False)
2816 matcher = matchmod.badmatch(m, lambda x, y: False)
2817 for abs in wctx.walk(matcher):
2817 for abs in wctx.walk(matcher):
2818 names[abs] = m.exact(abs)
2818 names[abs] = m.exact(abs)
2819
2819
2820 # walk target manifest to fill `names`
2820 # walk target manifest to fill `names`
2821
2821
2822 def badfn(path, msg):
2822 def badfn(path, msg):
2823 if path in names:
2823 if path in names:
2824 return
2824 return
2825 if path in ctx.substate:
2825 if path in ctx.substate:
2826 return
2826 return
2827 path_ = path + '/'
2827 path_ = path + '/'
2828 for f in names:
2828 for f in names:
2829 if f.startswith(path_):
2829 if f.startswith(path_):
2830 return
2830 return
2831 ui.warn("%s: %s\n" % (uipathfn(path), msg))
2831 ui.warn("%s: %s\n" % (uipathfn(path), msg))
2832
2832
2833 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2833 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2834 if abs not in names:
2834 if abs not in names:
2835 names[abs] = m.exact(abs)
2835 names[abs] = m.exact(abs)
2836
2836
2837 # Find status of all file in `names`.
2837 # Find status of all file in `names`.
2838 m = scmutil.matchfiles(repo, names)
2838 m = scmutil.matchfiles(repo, names)
2839
2839
2840 changes = repo.status(node1=node, match=m,
2840 changes = repo.status(node1=node, match=m,
2841 unknown=True, ignored=True, clean=True)
2841 unknown=True, ignored=True, clean=True)
2842 else:
2842 else:
2843 changes = repo.status(node1=node, match=m)
2843 changes = repo.status(node1=node, match=m)
2844 for kind in changes:
2844 for kind in changes:
2845 for abs in kind:
2845 for abs in kind:
2846 names[abs] = m.exact(abs)
2846 names[abs] = m.exact(abs)
2847
2847
2848 m = scmutil.matchfiles(repo, names)
2848 m = scmutil.matchfiles(repo, names)
2849
2849
2850 modified = set(changes.modified)
2850 modified = set(changes.modified)
2851 added = set(changes.added)
2851 added = set(changes.added)
2852 removed = set(changes.removed)
2852 removed = set(changes.removed)
2853 _deleted = set(changes.deleted)
2853 _deleted = set(changes.deleted)
2854 unknown = set(changes.unknown)
2854 unknown = set(changes.unknown)
2855 unknown.update(changes.ignored)
2855 unknown.update(changes.ignored)
2856 clean = set(changes.clean)
2856 clean = set(changes.clean)
2857 modadded = set()
2857 modadded = set()
2858
2858
2859 # We need to account for the state of the file in the dirstate,
2859 # We need to account for the state of the file in the dirstate,
2860 # even when we revert against something else than parent. This will
2860 # even when we revert against something else than parent. This will
2861 # slightly alter the behavior of revert (doing back up or not, delete
2861 # slightly alter the behavior of revert (doing back up or not, delete
2862 # or just forget etc).
2862 # or just forget etc).
2863 if parent == node:
2863 if parent == node:
2864 dsmodified = modified
2864 dsmodified = modified
2865 dsadded = added
2865 dsadded = added
2866 dsremoved = removed
2866 dsremoved = removed
2867 # store all local modifications, useful later for rename detection
2867 # store all local modifications, useful later for rename detection
2868 localchanges = dsmodified | dsadded
2868 localchanges = dsmodified | dsadded
2869 modified, added, removed = set(), set(), set()
2869 modified, added, removed = set(), set(), set()
2870 else:
2870 else:
2871 changes = repo.status(node1=parent, match=m)
2871 changes = repo.status(node1=parent, match=m)
2872 dsmodified = set(changes.modified)
2872 dsmodified = set(changes.modified)
2873 dsadded = set(changes.added)
2873 dsadded = set(changes.added)
2874 dsremoved = set(changes.removed)
2874 dsremoved = set(changes.removed)
2875 # store all local modifications, useful later for rename detection
2875 # store all local modifications, useful later for rename detection
2876 localchanges = dsmodified | dsadded
2876 localchanges = dsmodified | dsadded
2877
2877
2878 # only take into account for removes between wc and target
2878 # only take into account for removes between wc and target
2879 clean |= dsremoved - removed
2879 clean |= dsremoved - removed
2880 dsremoved &= removed
2880 dsremoved &= removed
2881 # distinct between dirstate remove and other
2881 # distinct between dirstate remove and other
2882 removed -= dsremoved
2882 removed -= dsremoved
2883
2883
2884 modadded = added & dsmodified
2884 modadded = added & dsmodified
2885 added -= modadded
2885 added -= modadded
2886
2886
2887 # tell newly modified apart.
2887 # tell newly modified apart.
2888 dsmodified &= modified
2888 dsmodified &= modified
2889 dsmodified |= modified & dsadded # dirstate added may need backup
2889 dsmodified |= modified & dsadded # dirstate added may need backup
2890 modified -= dsmodified
2890 modified -= dsmodified
2891
2891
2892 # We need to wait for some post-processing to update this set
2892 # We need to wait for some post-processing to update this set
2893 # before making the distinction. The dirstate will be used for
2893 # before making the distinction. The dirstate will be used for
2894 # that purpose.
2894 # that purpose.
2895 dsadded = added
2895 dsadded = added
2896
2896
2897 # in case of merge, files that are actually added can be reported as
2897 # in case of merge, files that are actually added can be reported as
2898 # modified, we need to post process the result
2898 # modified, we need to post process the result
2899 if p2 != nullid:
2899 if p2 != nullid:
2900 mergeadd = set(dsmodified)
2900 mergeadd = set(dsmodified)
2901 for path in dsmodified:
2901 for path in dsmodified:
2902 if path in mf:
2902 if path in mf:
2903 mergeadd.remove(path)
2903 mergeadd.remove(path)
2904 dsadded |= mergeadd
2904 dsadded |= mergeadd
2905 dsmodified -= mergeadd
2905 dsmodified -= mergeadd
2906
2906
2907 # if f is a rename, update `names` to also revert the source
2907 # if f is a rename, update `names` to also revert the source
2908 for f in localchanges:
2908 for f in localchanges:
2909 src = repo.dirstate.copied(f)
2909 src = repo.dirstate.copied(f)
2910 # XXX should we check for rename down to target node?
2910 # XXX should we check for rename down to target node?
2911 if src and src not in names and repo.dirstate[src] == 'r':
2911 if src and src not in names and repo.dirstate[src] == 'r':
2912 dsremoved.add(src)
2912 dsremoved.add(src)
2913 names[src] = True
2913 names[src] = True
2914
2914
2915 # determine the exact nature of the deleted changesets
2915 # determine the exact nature of the deleted changesets
2916 deladded = set(_deleted)
2916 deladded = set(_deleted)
2917 for path in _deleted:
2917 for path in _deleted:
2918 if path in mf:
2918 if path in mf:
2919 deladded.remove(path)
2919 deladded.remove(path)
2920 deleted = _deleted - deladded
2920 deleted = _deleted - deladded
2921
2921
2922 # distinguish between file to forget and the other
2922 # distinguish between file to forget and the other
2923 added = set()
2923 added = set()
2924 for abs in dsadded:
2924 for abs in dsadded:
2925 if repo.dirstate[abs] != 'a':
2925 if repo.dirstate[abs] != 'a':
2926 added.add(abs)
2926 added.add(abs)
2927 dsadded -= added
2927 dsadded -= added
2928
2928
2929 for abs in deladded:
2929 for abs in deladded:
2930 if repo.dirstate[abs] == 'a':
2930 if repo.dirstate[abs] == 'a':
2931 dsadded.add(abs)
2931 dsadded.add(abs)
2932 deladded -= dsadded
2932 deladded -= dsadded
2933
2933
2934 # For files marked as removed, we check if an unknown file is present at
2934 # For files marked as removed, we check if an unknown file is present at
2935 # the same path. If a such file exists it may need to be backed up.
2935 # the same path. If a such file exists it may need to be backed up.
2936 # Making the distinction at this stage helps have simpler backup
2936 # Making the distinction at this stage helps have simpler backup
2937 # logic.
2937 # logic.
2938 removunk = set()
2938 removunk = set()
2939 for abs in removed:
2939 for abs in removed:
2940 target = repo.wjoin(abs)
2940 target = repo.wjoin(abs)
2941 if os.path.lexists(target):
2941 if os.path.lexists(target):
2942 removunk.add(abs)
2942 removunk.add(abs)
2943 removed -= removunk
2943 removed -= removunk
2944
2944
2945 dsremovunk = set()
2945 dsremovunk = set()
2946 for abs in dsremoved:
2946 for abs in dsremoved:
2947 target = repo.wjoin(abs)
2947 target = repo.wjoin(abs)
2948 if os.path.lexists(target):
2948 if os.path.lexists(target):
2949 dsremovunk.add(abs)
2949 dsremovunk.add(abs)
2950 dsremoved -= dsremovunk
2950 dsremoved -= dsremovunk
2951
2951
2952 # action to be actually performed by revert
2952 # action to be actually performed by revert
2953 # (<list of file>, message>) tuple
2953 # (<list of file>, message>) tuple
2954 actions = {'revert': ([], _('reverting %s\n')),
2954 actions = {'revert': ([], _('reverting %s\n')),
2955 'add': ([], _('adding %s\n')),
2955 'add': ([], _('adding %s\n')),
2956 'remove': ([], _('removing %s\n')),
2956 'remove': ([], _('removing %s\n')),
2957 'drop': ([], _('removing %s\n')),
2957 'drop': ([], _('removing %s\n')),
2958 'forget': ([], _('forgetting %s\n')),
2958 'forget': ([], _('forgetting %s\n')),
2959 'undelete': ([], _('undeleting %s\n')),
2959 'undelete': ([], _('undeleting %s\n')),
2960 'noop': (None, _('no changes needed to %s\n')),
2960 'noop': (None, _('no changes needed to %s\n')),
2961 'unknown': (None, _('file not managed: %s\n')),
2961 'unknown': (None, _('file not managed: %s\n')),
2962 }
2962 }
2963
2963
2964 # "constant" that convey the backup strategy.
2964 # "constant" that convey the backup strategy.
2965 # All set to `discard` if `no-backup` is set do avoid checking
2965 # All set to `discard` if `no-backup` is set do avoid checking
2966 # no_backup lower in the code.
2966 # no_backup lower in the code.
2967 # These values are ordered for comparison purposes
2967 # These values are ordered for comparison purposes
2968 backupinteractive = 3 # do backup if interactively modified
2968 backupinteractive = 3 # do backup if interactively modified
2969 backup = 2 # unconditionally do backup
2969 backup = 2 # unconditionally do backup
2970 check = 1 # check if the existing file differs from target
2970 check = 1 # check if the existing file differs from target
2971 discard = 0 # never do backup
2971 discard = 0 # never do backup
2972 if opts.get('no_backup'):
2972 if opts.get('no_backup'):
2973 backupinteractive = backup = check = discard
2973 backupinteractive = backup = check = discard
2974 if interactive:
2974 if interactive:
2975 dsmodifiedbackup = backupinteractive
2975 dsmodifiedbackup = backupinteractive
2976 else:
2976 else:
2977 dsmodifiedbackup = backup
2977 dsmodifiedbackup = backup
2978 tobackup = set()
2978 tobackup = set()
2979
2979
2980 backupanddel = actions['remove']
2980 backupanddel = actions['remove']
2981 if not opts.get('no_backup'):
2981 if not opts.get('no_backup'):
2982 backupanddel = actions['drop']
2982 backupanddel = actions['drop']
2983
2983
2984 disptable = (
2984 disptable = (
2985 # dispatch table:
2985 # dispatch table:
2986 # file state
2986 # file state
2987 # action
2987 # action
2988 # make backup
2988 # make backup
2989
2989
2990 ## Sets that results that will change file on disk
2990 ## Sets that results that will change file on disk
2991 # Modified compared to target, no local change
2991 # Modified compared to target, no local change
2992 (modified, actions['revert'], discard),
2992 (modified, actions['revert'], discard),
2993 # Modified compared to target, but local file is deleted
2993 # Modified compared to target, but local file is deleted
2994 (deleted, actions['revert'], discard),
2994 (deleted, actions['revert'], discard),
2995 # Modified compared to target, local change
2995 # Modified compared to target, local change
2996 (dsmodified, actions['revert'], dsmodifiedbackup),
2996 (dsmodified, actions['revert'], dsmodifiedbackup),
2997 # Added since target
2997 # Added since target
2998 (added, actions['remove'], discard),
2998 (added, actions['remove'], discard),
2999 # Added in working directory
2999 # Added in working directory
3000 (dsadded, actions['forget'], discard),
3000 (dsadded, actions['forget'], discard),
3001 # Added since target, have local modification
3001 # Added since target, have local modification
3002 (modadded, backupanddel, backup),
3002 (modadded, backupanddel, backup),
3003 # Added since target but file is missing in working directory
3003 # Added since target but file is missing in working directory
3004 (deladded, actions['drop'], discard),
3004 (deladded, actions['drop'], discard),
3005 # Removed since target, before working copy parent
3005 # Removed since target, before working copy parent
3006 (removed, actions['add'], discard),
3006 (removed, actions['add'], discard),
3007 # Same as `removed` but an unknown file exists at the same path
3007 # Same as `removed` but an unknown file exists at the same path
3008 (removunk, actions['add'], check),
3008 (removunk, actions['add'], check),
3009 # Removed since targe, marked as such in working copy parent
3009 # Removed since targe, marked as such in working copy parent
3010 (dsremoved, actions['undelete'], discard),
3010 (dsremoved, actions['undelete'], discard),
3011 # Same as `dsremoved` but an unknown file exists at the same path
3011 # Same as `dsremoved` but an unknown file exists at the same path
3012 (dsremovunk, actions['undelete'], check),
3012 (dsremovunk, actions['undelete'], check),
3013 ## the following sets does not result in any file changes
3013 ## the following sets does not result in any file changes
3014 # File with no modification
3014 # File with no modification
3015 (clean, actions['noop'], discard),
3015 (clean, actions['noop'], discard),
3016 # Existing file, not tracked anywhere
3016 # Existing file, not tracked anywhere
3017 (unknown, actions['unknown'], discard),
3017 (unknown, actions['unknown'], discard),
3018 )
3018 )
3019
3019
3020 for abs, exact in sorted(names.items()):
3020 for abs, exact in sorted(names.items()):
3021 # target file to be touch on disk (relative to cwd)
3021 # target file to be touch on disk (relative to cwd)
3022 target = repo.wjoin(abs)
3022 target = repo.wjoin(abs)
3023 # search the entry in the dispatch table.
3023 # search the entry in the dispatch table.
3024 # if the file is in any of these sets, it was touched in the working
3024 # if the file is in any of these sets, it was touched in the working
3025 # directory parent and we are sure it needs to be reverted.
3025 # directory parent and we are sure it needs to be reverted.
3026 for table, (xlist, msg), dobackup in disptable:
3026 for table, (xlist, msg), dobackup in disptable:
3027 if abs not in table:
3027 if abs not in table:
3028 continue
3028 continue
3029 if xlist is not None:
3029 if xlist is not None:
3030 xlist.append(abs)
3030 xlist.append(abs)
3031 if dobackup:
3031 if dobackup:
3032 # If in interactive mode, don't automatically create
3032 # If in interactive mode, don't automatically create
3033 # .orig files (issue4793)
3033 # .orig files (issue4793)
3034 if dobackup == backupinteractive:
3034 if dobackup == backupinteractive:
3035 tobackup.add(abs)
3035 tobackup.add(abs)
3036 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3036 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3037 absbakname = scmutil.backuppath(ui, repo, abs)
3037 absbakname = scmutil.backuppath(ui, repo, abs)
3038 bakname = os.path.relpath(absbakname,
3038 bakname = os.path.relpath(absbakname,
3039 start=repo.root)
3039 start=repo.root)
3040 ui.note(_('saving current version of %s as %s\n') %
3040 ui.note(_('saving current version of %s as %s\n') %
3041 (uipathfn(abs), uipathfn(bakname)))
3041 (uipathfn(abs), uipathfn(bakname)))
3042 if not opts.get('dry_run'):
3042 if not opts.get('dry_run'):
3043 if interactive:
3043 if interactive:
3044 util.copyfile(target, absbakname)
3044 util.copyfile(target, absbakname)
3045 else:
3045 else:
3046 util.rename(target, absbakname)
3046 util.rename(target, absbakname)
3047 if opts.get('dry_run'):
3047 if opts.get('dry_run'):
3048 if ui.verbose or not exact:
3048 if ui.verbose or not exact:
3049 ui.status(msg % uipathfn(abs))
3049 ui.status(msg % uipathfn(abs))
3050 elif exact:
3050 elif exact:
3051 ui.warn(msg % uipathfn(abs))
3051 ui.warn(msg % uipathfn(abs))
3052 break
3052 break
3053
3053
3054 if not opts.get('dry_run'):
3054 if not opts.get('dry_run'):
3055 needdata = ('revert', 'add', 'undelete')
3055 needdata = ('revert', 'add', 'undelete')
3056 oplist = [actions[name][0] for name in needdata]
3056 oplist = [actions[name][0] for name in needdata]
3057 prefetch = scmutil.prefetchfiles
3057 prefetch = scmutil.prefetchfiles
3058 matchfiles = scmutil.matchfiles
3058 matchfiles = scmutil.matchfiles
3059 prefetch(repo, [ctx.rev()],
3059 prefetch(repo, [ctx.rev()],
3060 matchfiles(repo,
3060 matchfiles(repo,
3061 [f for sublist in oplist for f in sublist]))
3061 [f for sublist in oplist for f in sublist]))
3062 match = scmutil.match(repo[None], pats)
3062 match = scmutil.match(repo[None], pats)
3063 _performrevert(repo, parents, ctx, names, uipathfn, actions,
3063 _performrevert(repo, parents, ctx, names, uipathfn, actions,
3064 match, interactive, tobackup)
3064 match, interactive, tobackup)
3065
3065
3066 if targetsubs:
3066 if targetsubs:
3067 # Revert the subrepos on the revert list
3067 # Revert the subrepos on the revert list
3068 for sub in targetsubs:
3068 for sub in targetsubs:
3069 try:
3069 try:
3070 wctx.sub(sub).revert(ctx.substate[sub], *pats,
3070 wctx.sub(sub).revert(ctx.substate[sub], *pats,
3071 **pycompat.strkwargs(opts))
3071 **pycompat.strkwargs(opts))
3072 except KeyError:
3072 except KeyError:
3073 raise error.Abort("subrepository '%s' does not exist in %s!"
3073 raise error.Abort("subrepository '%s' does not exist in %s!"
3074 % (sub, short(ctx.node())))
3074 % (sub, short(ctx.node())))
3075
3075
3076 def _performrevert(repo, parents, ctx, names, uipathfn, actions,
3076 def _performrevert(repo, parents, ctx, names, uipathfn, actions,
3077 match, interactive=False, tobackup=None):
3077 match, interactive=False, tobackup=None):
3078 """function that actually perform all the actions computed for revert
3078 """function that actually perform all the actions computed for revert
3079
3079
3080 This is an independent function to let extension to plug in and react to
3080 This is an independent function to let extension to plug in and react to
3081 the imminent revert.
3081 the imminent revert.
3082
3082
3083 Make sure you have the working directory locked when calling this function.
3083 Make sure you have the working directory locked when calling this function.
3084 """
3084 """
3085 parent, p2 = parents
3085 parent, p2 = parents
3086 node = ctx.node()
3086 node = ctx.node()
3087 excluded_files = []
3087 excluded_files = []
3088
3088
3089 def checkout(f):
3089 def checkout(f):
3090 fc = ctx[f]
3090 fc = ctx[f]
3091 repo.wwrite(f, fc.data(), fc.flags())
3091 repo.wwrite(f, fc.data(), fc.flags())
3092
3092
3093 def doremove(f):
3093 def doremove(f):
3094 try:
3094 try:
3095 rmdir = repo.ui.configbool('experimental', 'removeemptydirs')
3095 rmdir = repo.ui.configbool('experimental', 'removeemptydirs')
3096 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3096 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3097 except OSError:
3097 except OSError:
3098 pass
3098 pass
3099 repo.dirstate.remove(f)
3099 repo.dirstate.remove(f)
3100
3100
3101 def prntstatusmsg(action, f):
3101 def prntstatusmsg(action, f):
3102 exact = names[f]
3102 exact = names[f]
3103 if repo.ui.verbose or not exact:
3103 if repo.ui.verbose or not exact:
3104 repo.ui.status(actions[action][1] % uipathfn(f))
3104 repo.ui.status(actions[action][1] % uipathfn(f))
3105
3105
3106 audit_path = pathutil.pathauditor(repo.root, cached=True)
3106 audit_path = pathutil.pathauditor(repo.root, cached=True)
3107 for f in actions['forget'][0]:
3107 for f in actions['forget'][0]:
3108 if interactive:
3108 if interactive:
3109 choice = repo.ui.promptchoice(
3109 choice = repo.ui.promptchoice(
3110 _("forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f))
3110 _("forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f))
3111 if choice == 0:
3111 if choice == 0:
3112 prntstatusmsg('forget', f)
3112 prntstatusmsg('forget', f)
3113 repo.dirstate.drop(f)
3113 repo.dirstate.drop(f)
3114 else:
3114 else:
3115 excluded_files.append(f)
3115 excluded_files.append(f)
3116 else:
3116 else:
3117 prntstatusmsg('forget', f)
3117 prntstatusmsg('forget', f)
3118 repo.dirstate.drop(f)
3118 repo.dirstate.drop(f)
3119 for f in actions['remove'][0]:
3119 for f in actions['remove'][0]:
3120 audit_path(f)
3120 audit_path(f)
3121 if interactive:
3121 if interactive:
3122 choice = repo.ui.promptchoice(
3122 choice = repo.ui.promptchoice(
3123 _("remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f))
3123 _("remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f))
3124 if choice == 0:
3124 if choice == 0:
3125 prntstatusmsg('remove', f)
3125 prntstatusmsg('remove', f)
3126 doremove(f)
3126 doremove(f)
3127 else:
3127 else:
3128 excluded_files.append(f)
3128 excluded_files.append(f)
3129 else:
3129 else:
3130 prntstatusmsg('remove', f)
3130 prntstatusmsg('remove', f)
3131 doremove(f)
3131 doremove(f)
3132 for f in actions['drop'][0]:
3132 for f in actions['drop'][0]:
3133 audit_path(f)
3133 audit_path(f)
3134 prntstatusmsg('drop', f)
3134 prntstatusmsg('drop', f)
3135 repo.dirstate.remove(f)
3135 repo.dirstate.remove(f)
3136
3136
3137 normal = None
3137 normal = None
3138 if node == parent:
3138 if node == parent:
3139 # We're reverting to our parent. If possible, we'd like status
3139 # We're reverting to our parent. If possible, we'd like status
3140 # to report the file as clean. We have to use normallookup for
3140 # to report the file as clean. We have to use normallookup for
3141 # merges to avoid losing information about merged/dirty files.
3141 # merges to avoid losing information about merged/dirty files.
3142 if p2 != nullid:
3142 if p2 != nullid:
3143 normal = repo.dirstate.normallookup
3143 normal = repo.dirstate.normallookup
3144 else:
3144 else:
3145 normal = repo.dirstate.normal
3145 normal = repo.dirstate.normal
3146
3146
3147 newlyaddedandmodifiedfiles = set()
3147 newlyaddedandmodifiedfiles = set()
3148 if interactive:
3148 if interactive:
3149 # Prompt the user for changes to revert
3149 # Prompt the user for changes to revert
3150 torevert = [f for f in actions['revert'][0] if f not in excluded_files]
3150 torevert = [f for f in actions['revert'][0] if f not in excluded_files]
3151 m = scmutil.matchfiles(repo, torevert)
3151 m = scmutil.matchfiles(repo, torevert)
3152 diffopts = patch.difffeatureopts(repo.ui, whitespace=True,
3152 diffopts = patch.difffeatureopts(repo.ui, whitespace=True,
3153 section='commands',
3153 section='commands',
3154 configprefix='revert.interactive.')
3154 configprefix='revert.interactive.')
3155 diffopts.nodates = True
3155 diffopts.nodates = True
3156 diffopts.git = True
3156 diffopts.git = True
3157 operation = 'apply'
3157 operation = 'apply'
3158 if node == parent:
3158 if node == parent:
3159 if repo.ui.configbool('experimental',
3159 if repo.ui.configbool('experimental',
3160 'revert.interactive.select-to-keep'):
3160 'revert.interactive.select-to-keep'):
3161 operation = 'keep'
3161 operation = 'keep'
3162 else:
3162 else:
3163 operation = 'discard'
3163 operation = 'discard'
3164
3164
3165 if operation == 'apply':
3165 if operation == 'apply':
3166 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3166 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3167 else:
3167 else:
3168 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3168 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3169 originalchunks = patch.parsepatch(diff)
3169 originalchunks = patch.parsepatch(diff)
3170
3170
3171 try:
3171 try:
3172
3172
3173 chunks, opts = recordfilter(repo.ui, originalchunks, match,
3173 chunks, opts = recordfilter(repo.ui, originalchunks, match,
3174 operation=operation)
3174 operation=operation)
3175 if operation == 'discard':
3175 if operation == 'discard':
3176 chunks = patch.reversehunks(chunks)
3176 chunks = patch.reversehunks(chunks)
3177
3177
3178 except error.PatchError as err:
3178 except error.PatchError as err:
3179 raise error.Abort(_('error parsing patch: %s') % err)
3179 raise error.Abort(_('error parsing patch: %s') % err)
3180
3180
3181 # FIXME: when doing an interactive revert of a copy, there's no way of
3181 # FIXME: when doing an interactive revert of a copy, there's no way of
3182 # performing a partial revert of the added file, the only option is
3182 # performing a partial revert of the added file, the only option is
3183 # "remove added file <name> (Yn)?", so we don't need to worry about the
3183 # "remove added file <name> (Yn)?", so we don't need to worry about the
3184 # alsorestore value. Ideally we'd be able to partially revert
3184 # alsorestore value. Ideally we'd be able to partially revert
3185 # copied/renamed files.
3185 # copied/renamed files.
3186 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3186 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3187 chunks, originalchunks)
3187 chunks, originalchunks)
3188 if tobackup is None:
3188 if tobackup is None:
3189 tobackup = set()
3189 tobackup = set()
3190 # Apply changes
3190 # Apply changes
3191 fp = stringio()
3191 fp = stringio()
3192 # chunks are serialized per file, but files aren't sorted
3192 # chunks are serialized per file, but files aren't sorted
3193 for f in sorted(set(c.header.filename() for c in chunks if ishunk(c))):
3193 for f in sorted(set(c.header.filename() for c in chunks if ishunk(c))):
3194 prntstatusmsg('revert', f)
3194 prntstatusmsg('revert', f)
3195 files = set()
3195 files = set()
3196 for c in chunks:
3196 for c in chunks:
3197 if ishunk(c):
3197 if ishunk(c):
3198 abs = c.header.filename()
3198 abs = c.header.filename()
3199 # Create a backup file only if this hunk should be backed up
3199 # Create a backup file only if this hunk should be backed up
3200 if c.header.filename() in tobackup:
3200 if c.header.filename() in tobackup:
3201 target = repo.wjoin(abs)
3201 target = repo.wjoin(abs)
3202 bakname = scmutil.backuppath(repo.ui, repo, abs)
3202 bakname = scmutil.backuppath(repo.ui, repo, abs)
3203 util.copyfile(target, bakname)
3203 util.copyfile(target, bakname)
3204 tobackup.remove(abs)
3204 tobackup.remove(abs)
3205 if abs not in files:
3205 if abs not in files:
3206 files.add(abs)
3206 files.add(abs)
3207 if operation == 'keep':
3207 if operation == 'keep':
3208 checkout(abs)
3208 checkout(abs)
3209 c.write(fp)
3209 c.write(fp)
3210 dopatch = fp.tell()
3210 dopatch = fp.tell()
3211 fp.seek(0)
3211 fp.seek(0)
3212 if dopatch:
3212 if dopatch:
3213 try:
3213 try:
3214 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3214 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3215 except error.PatchError as err:
3215 except error.PatchError as err:
3216 raise error.Abort(pycompat.bytestr(err))
3216 raise error.Abort(pycompat.bytestr(err))
3217 del fp
3217 del fp
3218 else:
3218 else:
3219 for f in actions['revert'][0]:
3219 for f in actions['revert'][0]:
3220 prntstatusmsg('revert', f)
3220 prntstatusmsg('revert', f)
3221 checkout(f)
3221 checkout(f)
3222 if normal:
3222 if normal:
3223 normal(f)
3223 normal(f)
3224
3224
3225 for f in actions['add'][0]:
3225 for f in actions['add'][0]:
3226 # Don't checkout modified files, they are already created by the diff
3226 # Don't checkout modified files, they are already created by the diff
3227 if f not in newlyaddedandmodifiedfiles:
3227 if f not in newlyaddedandmodifiedfiles:
3228 prntstatusmsg('add', f)
3228 prntstatusmsg('add', f)
3229 checkout(f)
3229 checkout(f)
3230 repo.dirstate.add(f)
3230 repo.dirstate.add(f)
3231
3231
3232 normal = repo.dirstate.normallookup
3232 normal = repo.dirstate.normallookup
3233 if node == parent and p2 == nullid:
3233 if node == parent and p2 == nullid:
3234 normal = repo.dirstate.normal
3234 normal = repo.dirstate.normal
3235 for f in actions['undelete'][0]:
3235 for f in actions['undelete'][0]:
3236 if interactive:
3236 if interactive:
3237 choice = repo.ui.promptchoice(
3237 choice = repo.ui.promptchoice(
3238 _("add back removed file %s (Yn)?$$ &Yes $$ &No") % f)
3238 _("add back removed file %s (Yn)?$$ &Yes $$ &No") % f)
3239 if choice == 0:
3239 if choice == 0:
3240 prntstatusmsg('undelete', f)
3240 prntstatusmsg('undelete', f)
3241 checkout(f)
3241 checkout(f)
3242 normal(f)
3242 normal(f)
3243 else:
3243 else:
3244 excluded_files.append(f)
3244 excluded_files.append(f)
3245 else:
3245 else:
3246 prntstatusmsg('undelete', f)
3246 prntstatusmsg('undelete', f)
3247 checkout(f)
3247 checkout(f)
3248 normal(f)
3248 normal(f)
3249
3249
3250 copied = copies.pathcopies(repo[parent], ctx)
3250 copied = copies.pathcopies(repo[parent], ctx)
3251
3251
3252 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3252 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3253 if f in copied:
3253 if f in copied:
3254 repo.dirstate.copy(copied[f], f)
3254 repo.dirstate.copy(copied[f], f)
3255
3255
3256 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3256 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3257 # commands.outgoing. "missing" is "missing" of the result of
3257 # commands.outgoing. "missing" is "missing" of the result of
3258 # "findcommonoutgoing()"
3258 # "findcommonoutgoing()"
3259 outgoinghooks = util.hooks()
3259 outgoinghooks = util.hooks()
3260
3260
3261 # a list of (ui, repo) functions called by commands.summary
3261 # a list of (ui, repo) functions called by commands.summary
3262 summaryhooks = util.hooks()
3262 summaryhooks = util.hooks()
3263
3263
3264 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3264 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3265 #
3265 #
3266 # functions should return tuple of booleans below, if 'changes' is None:
3266 # functions should return tuple of booleans below, if 'changes' is None:
3267 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3267 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3268 #
3268 #
3269 # otherwise, 'changes' is a tuple of tuples below:
3269 # otherwise, 'changes' is a tuple of tuples below:
3270 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3270 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3271 # - (desturl, destbranch, destpeer, outgoing)
3271 # - (desturl, destbranch, destpeer, outgoing)
3272 summaryremotehooks = util.hooks()
3272 summaryremotehooks = util.hooks()
3273
3273
3274
3274
3275 def checkunfinished(repo, commit=False, skipmerge=False):
3275 def checkunfinished(repo, commit=False, skipmerge=False):
3276 '''Look for an unfinished multistep operation, like graft, and abort
3276 '''Look for an unfinished multistep operation, like graft, and abort
3277 if found. It's probably good to check this right before
3277 if found. It's probably good to check this right before
3278 bailifchanged().
3278 bailifchanged().
3279 '''
3279 '''
3280 # Check for non-clearable states first, so things like rebase will take
3280 # Check for non-clearable states first, so things like rebase will take
3281 # precedence over update.
3281 # precedence over update.
3282 for state in statemod._unfinishedstates:
3282 for state in statemod._unfinishedstates:
3283 if (state._clearable or (commit and state._allowcommit) or
3283 if (state._clearable or (commit and state._allowcommit) or
3284 state._reportonly):
3284 state._reportonly):
3285 continue
3285 continue
3286 if state.isunfinished(repo):
3286 if state.isunfinished(repo):
3287 raise error.Abort(state.msg(), hint=state.hint())
3287 raise error.Abort(state.msg(), hint=state.hint())
3288
3288
3289 for s in statemod._unfinishedstates:
3289 for s in statemod._unfinishedstates:
3290 if (not s._clearable or (commit and s._allowcommit) or
3290 if (not s._clearable or (commit and s._allowcommit) or
3291 (s._opname == 'merge' and skipmerge) or s._reportonly):
3291 (s._opname == 'merge' and skipmerge) or s._reportonly):
3292 continue
3292 continue
3293 if s.isunfinished(repo):
3293 if s.isunfinished(repo):
3294 raise error.Abort(s.msg(), hint=s.hint())
3294 raise error.Abort(s.msg(), hint=s.hint())
3295
3295
3296 def clearunfinished(repo):
3296 def clearunfinished(repo):
3297 '''Check for unfinished operations (as above), and clear the ones
3297 '''Check for unfinished operations (as above), and clear the ones
3298 that are clearable.
3298 that are clearable.
3299 '''
3299 '''
3300 for state in statemod._unfinishedstates:
3300 for state in statemod._unfinishedstates:
3301 if state._reportonly:
3301 if state._reportonly:
3302 continue
3302 continue
3303 if not state._clearable and state.isunfinished(repo):
3303 if not state._clearable and state.isunfinished(repo):
3304 raise error.Abort(state.msg(), hint=state.hint())
3304 raise error.Abort(state.msg(), hint=state.hint())
3305
3305
3306 for s in statemod._unfinishedstates:
3306 for s in statemod._unfinishedstates:
3307 if s._opname == 'merge' or state._reportonly:
3307 if s._opname == 'merge' or state._reportonly:
3308 continue
3308 continue
3309 if s._clearable and s.isunfinished(repo):
3309 if s._clearable and s.isunfinished(repo):
3310 util.unlink(repo.vfs.join(s._fname))
3310 util.unlink(repo.vfs.join(s._fname))
3311
3311
3312 def getunfinishedstate(repo):
3312 def getunfinishedstate(repo):
3313 ''' Checks for unfinished operations and returns statecheck object
3313 ''' Checks for unfinished operations and returns statecheck object
3314 for it'''
3314 for it'''
3315 for state in statemod._unfinishedstates:
3315 for state in statemod._unfinishedstates:
3316 if state.isunfinished(repo):
3316 if state.isunfinished(repo):
3317 return state
3317 return state
3318 return None
3318 return None
3319
3319
3320 def howtocontinue(repo):
3320 def howtocontinue(repo):
3321 '''Check for an unfinished operation and return the command to finish
3321 '''Check for an unfinished operation and return the command to finish
3322 it.
3322 it.
3323
3323
3324 statemod._unfinishedstates list is checked for an unfinished operation
3324 statemod._unfinishedstates list is checked for an unfinished operation
3325 and the corresponding message to finish it is generated if a method to
3325 and the corresponding message to finish it is generated if a method to
3326 continue is supported by the operation.
3326 continue is supported by the operation.
3327
3327
3328 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3328 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3329 a boolean.
3329 a boolean.
3330 '''
3330 '''
3331 contmsg = _("continue: %s")
3331 contmsg = _("continue: %s")
3332 for state in statemod._unfinishedstates:
3332 for state in statemod._unfinishedstates:
3333 if not state._continueflag:
3333 if not state._continueflag:
3334 continue
3334 continue
3335 if state.isunfinished(repo):
3335 if state.isunfinished(repo):
3336 return contmsg % state.continuemsg(), True
3336 return contmsg % state.continuemsg(), True
3337 if repo[None].dirty(missing=True, merge=False, branch=False):
3337 if repo[None].dirty(missing=True, merge=False, branch=False):
3338 return contmsg % _("hg commit"), False
3338 return contmsg % _("hg commit"), False
3339 return None, None
3339 return None, None
3340
3340
3341 def checkafterresolved(repo):
3341 def checkafterresolved(repo):
3342 '''Inform the user about the next action after completing hg resolve
3342 '''Inform the user about the next action after completing hg resolve
3343
3343
3344 If there's a an unfinished operation that supports continue flag,
3344 If there's a an unfinished operation that supports continue flag,
3345 howtocontinue will yield repo.ui.warn as the reporter.
3345 howtocontinue will yield repo.ui.warn as the reporter.
3346
3346
3347 Otherwise, it will yield repo.ui.note.
3347 Otherwise, it will yield repo.ui.note.
3348 '''
3348 '''
3349 msg, warning = howtocontinue(repo)
3349 msg, warning = howtocontinue(repo)
3350 if msg is not None:
3350 if msg is not None:
3351 if warning:
3351 if warning:
3352 repo.ui.warn("%s\n" % msg)
3352 repo.ui.warn("%s\n" % msg)
3353 else:
3353 else:
3354 repo.ui.note("%s\n" % msg)
3354 repo.ui.note("%s\n" % msg)
3355
3355
3356 def wrongtooltocontinue(repo, task):
3356 def wrongtooltocontinue(repo, task):
3357 '''Raise an abort suggesting how to properly continue if there is an
3357 '''Raise an abort suggesting how to properly continue if there is an
3358 active task.
3358 active task.
3359
3359
3360 Uses howtocontinue() to find the active task.
3360 Uses howtocontinue() to find the active task.
3361
3361
3362 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3362 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3363 a hint.
3363 a hint.
3364 '''
3364 '''
3365 after = howtocontinue(repo)
3365 after = howtocontinue(repo)
3366 hint = None
3366 hint = None
3367 if after[1]:
3367 if after[1]:
3368 hint = after[0]
3368 hint = after[0]
3369 raise error.Abort(_('no %s in progress') % task, hint=hint)
3369 raise error.Abort(_('no %s in progress') % task, hint=hint)
3370
3370
3371 def abortgraft(ui, repo, graftstate):
3371 def abortgraft(ui, repo, graftstate):
3372 """abort the interrupted graft and rollbacks to the state before interrupted
3372 """abort the interrupted graft and rollbacks to the state before interrupted
3373 graft"""
3373 graft"""
3374 if not graftstate.exists():
3374 if not graftstate.exists():
3375 raise error.Abort(_("no interrupted graft to abort"))
3375 raise error.Abort(_("no interrupted graft to abort"))
3376 statedata = readgraftstate(repo, graftstate)
3376 statedata = readgraftstate(repo, graftstate)
3377 newnodes = statedata.get('newnodes')
3377 newnodes = statedata.get('newnodes')
3378 if newnodes is None:
3378 if newnodes is None:
3379 # and old graft state which does not have all the data required to abort
3379 # and old graft state which does not have all the data required to abort
3380 # the graft
3380 # the graft
3381 raise error.Abort(_("cannot abort using an old graftstate"))
3381 raise error.Abort(_("cannot abort using an old graftstate"))
3382
3382
3383 # changeset from which graft operation was started
3383 # changeset from which graft operation was started
3384 if len(newnodes) > 0:
3384 if len(newnodes) > 0:
3385 startctx = repo[newnodes[0]].p1()
3385 startctx = repo[newnodes[0]].p1()
3386 else:
3386 else:
3387 startctx = repo['.']
3387 startctx = repo['.']
3388 # whether to strip or not
3388 # whether to strip or not
3389 cleanup = False
3389 cleanup = False
3390 from . import hg
3390 from . import hg
3391 if newnodes:
3391 if newnodes:
3392 newnodes = [repo[r].rev() for r in newnodes]
3392 newnodes = [repo[r].rev() for r in newnodes]
3393 cleanup = True
3393 cleanup = True
3394 # checking that none of the newnodes turned public or is public
3394 # checking that none of the newnodes turned public or is public
3395 immutable = [c for c in newnodes if not repo[c].mutable()]
3395 immutable = [c for c in newnodes if not repo[c].mutable()]
3396 if immutable:
3396 if immutable:
3397 repo.ui.warn(_("cannot clean up public changesets %s\n")
3397 repo.ui.warn(_("cannot clean up public changesets %s\n")
3398 % ', '.join(bytes(repo[r]) for r in immutable),
3398 % ', '.join(bytes(repo[r]) for r in immutable),
3399 hint=_("see 'hg help phases' for details"))
3399 hint=_("see 'hg help phases' for details"))
3400 cleanup = False
3400 cleanup = False
3401
3401
3402 # checking that no new nodes are created on top of grafted revs
3402 # checking that no new nodes are created on top of grafted revs
3403 desc = set(repo.changelog.descendants(newnodes))
3403 desc = set(repo.changelog.descendants(newnodes))
3404 if desc - set(newnodes):
3404 if desc - set(newnodes):
3405 repo.ui.warn(_("new changesets detected on destination "
3405 repo.ui.warn(_("new changesets detected on destination "
3406 "branch, can't strip\n"))
3406 "branch, can't strip\n"))
3407 cleanup = False
3407 cleanup = False
3408
3408
3409 if cleanup:
3409 if cleanup:
3410 with repo.wlock(), repo.lock():
3410 with repo.wlock(), repo.lock():
3411 hg.updaterepo(repo, startctx.node(), overwrite=True)
3411 hg.updaterepo(repo, startctx.node(), overwrite=True)
3412 # stripping the new nodes created
3412 # stripping the new nodes created
3413 strippoints = [c.node() for c in repo.set("roots(%ld)",
3413 strippoints = [c.node() for c in repo.set("roots(%ld)",
3414 newnodes)]
3414 newnodes)]
3415 repair.strip(repo.ui, repo, strippoints, backup=False)
3415 repair.strip(repo.ui, repo, strippoints, backup=False)
3416
3416
3417 if not cleanup:
3417 if not cleanup:
3418 # we don't update to the startnode if we can't strip
3418 # we don't update to the startnode if we can't strip
3419 startctx = repo['.']
3419 startctx = repo['.']
3420 hg.updaterepo(repo, startctx.node(), overwrite=True)
3420 hg.updaterepo(repo, startctx.node(), overwrite=True)
3421
3421
3422 ui.status(_("graft aborted\n"))
3422 ui.status(_("graft aborted\n"))
3423 ui.status(_("working directory is now at %s\n") % startctx.hex()[:12])
3423 ui.status(_("working directory is now at %s\n") % startctx.hex()[:12])
3424 graftstate.delete()
3424 graftstate.delete()
3425 return 0
3425 return 0
3426
3426
3427 def readgraftstate(repo, graftstate):
3427 def readgraftstate(repo, graftstate):
3428 """read the graft state file and return a dict of the data stored in it"""
3428 """read the graft state file and return a dict of the data stored in it"""
3429 try:
3429 try:
3430 return graftstate.read()
3430 return graftstate.read()
3431 except error.CorruptedState:
3431 except error.CorruptedState:
3432 nodes = repo.vfs.read('graftstate').splitlines()
3432 nodes = repo.vfs.read('graftstate').splitlines()
3433 return {'nodes': nodes}
3433 return {'nodes': nodes}
3434
3434
3435 def hgabortgraft(ui, repo):
3435 def hgabortgraft(ui, repo):
3436 """ abort logic for aborting graft using 'hg abort'"""
3436 """ abort logic for aborting graft using 'hg abort'"""
3437 with repo.wlock():
3437 with repo.wlock():
3438 graftstate = statemod.cmdstate(repo, 'graftstate')
3438 graftstate = statemod.cmdstate(repo, 'graftstate')
3439 return abortgraft(ui, repo, graftstate)
3439 return abortgraft(ui, repo, graftstate)
General Comments 0
You need to be logged in to leave comments. Login now