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