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