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