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