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