##// END OF EJS Templates
branch: pass current transaction when writing branch in import
marmoute -
r51158:798e4314 default
parent child Browse files
Show More
@@ -1,4127 +1,4129 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, b'_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(branch or b'default')
2034 repo.dirstate.setbranch(
2035 branch or b'default', repo.currenttransaction()
2036 )
2035
2037
2036 partial = opts.get(b'partial', False)
2038 partial = opts.get(b'partial', False)
2037 files = set()
2039 files = set()
2038 try:
2040 try:
2039 patch.patch(
2041 patch.patch(
2040 ui,
2042 ui,
2041 repo,
2043 repo,
2042 tmpname,
2044 tmpname,
2043 strip=strip,
2045 strip=strip,
2044 prefix=prefix,
2046 prefix=prefix,
2045 files=files,
2047 files=files,
2046 eolmode=None,
2048 eolmode=None,
2047 similarity=sim / 100.0,
2049 similarity=sim / 100.0,
2048 )
2050 )
2049 except error.PatchParseError as e:
2051 except error.PatchParseError as e:
2050 raise error.InputError(
2052 raise error.InputError(
2051 pycompat.bytestr(e),
2053 pycompat.bytestr(e),
2052 hint=_(
2054 hint=_(
2053 b'check that whitespace in the patch has not been mangled'
2055 b'check that whitespace in the patch has not been mangled'
2054 ),
2056 ),
2055 )
2057 )
2056 except error.PatchApplicationError as e:
2058 except error.PatchApplicationError as e:
2057 if not partial:
2059 if not partial:
2058 raise error.StateError(pycompat.bytestr(e))
2060 raise error.StateError(pycompat.bytestr(e))
2059 if partial:
2061 if partial:
2060 rejects = True
2062 rejects = True
2061
2063
2062 files = list(files)
2064 files = list(files)
2063 if nocommit:
2065 if nocommit:
2064 if message:
2066 if message:
2065 msgs.append(message)
2067 msgs.append(message)
2066 else:
2068 else:
2067 if opts.get(b'exact') or p2:
2069 if opts.get(b'exact') or p2:
2068 # If you got here, you either use --force and know what
2070 # If you got here, you either use --force and know what
2069 # you are doing or used --exact or a merge patch while
2071 # you are doing or used --exact or a merge patch while
2070 # being updated to its first parent.
2072 # being updated to its first parent.
2071 m = None
2073 m = None
2072 else:
2074 else:
2073 m = scmutil.matchfiles(repo, files or [])
2075 m = scmutil.matchfiles(repo, files or [])
2074 editform = mergeeditform(repo[None], b'import.normal')
2076 editform = mergeeditform(repo[None], b'import.normal')
2075 if opts.get(b'exact'):
2077 if opts.get(b'exact'):
2076 editor = None
2078 editor = None
2077 else:
2079 else:
2078 editor = getcommiteditor(
2080 editor = getcommiteditor(
2079 editform=editform, **pycompat.strkwargs(opts)
2081 editform=editform, **pycompat.strkwargs(opts)
2080 )
2082 )
2081 extra = {}
2083 extra = {}
2082 for idfunc in extrapreimport:
2084 for idfunc in extrapreimport:
2083 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2085 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2084 overrides = {}
2086 overrides = {}
2085 if partial:
2087 if partial:
2086 overrides[(b'ui', b'allowemptycommit')] = True
2088 overrides[(b'ui', b'allowemptycommit')] = True
2087 if opts.get(b'secret'):
2089 if opts.get(b'secret'):
2088 overrides[(b'phases', b'new-commit')] = b'secret'
2090 overrides[(b'phases', b'new-commit')] = b'secret'
2089 with repo.ui.configoverride(overrides, b'import'):
2091 with repo.ui.configoverride(overrides, b'import'):
2090 n = repo.commit(
2092 n = repo.commit(
2091 message, user, date, match=m, editor=editor, extra=extra
2093 message, user, date, match=m, editor=editor, extra=extra
2092 )
2094 )
2093 for idfunc in extrapostimport:
2095 for idfunc in extrapostimport:
2094 extrapostimportmap[idfunc](repo[n])
2096 extrapostimportmap[idfunc](repo[n])
2095 else:
2097 else:
2096 if opts.get(b'exact') or importbranch:
2098 if opts.get(b'exact') or importbranch:
2097 branch = branch or b'default'
2099 branch = branch or b'default'
2098 else:
2100 else:
2099 branch = p1.branch()
2101 branch = p1.branch()
2100 store = patch.filestore()
2102 store = patch.filestore()
2101 try:
2103 try:
2102 files = set()
2104 files = set()
2103 try:
2105 try:
2104 patch.patchrepo(
2106 patch.patchrepo(
2105 ui,
2107 ui,
2106 repo,
2108 repo,
2107 p1,
2109 p1,
2108 store,
2110 store,
2109 tmpname,
2111 tmpname,
2110 strip,
2112 strip,
2111 prefix,
2113 prefix,
2112 files,
2114 files,
2113 eolmode=None,
2115 eolmode=None,
2114 )
2116 )
2115 except error.PatchParseError as e:
2117 except error.PatchParseError as e:
2116 raise error.InputError(
2118 raise error.InputError(
2117 stringutil.forcebytestr(e),
2119 stringutil.forcebytestr(e),
2118 hint=_(
2120 hint=_(
2119 b'check that whitespace in the patch has not been mangled'
2121 b'check that whitespace in the patch has not been mangled'
2120 ),
2122 ),
2121 )
2123 )
2122 except error.PatchApplicationError as e:
2124 except error.PatchApplicationError as e:
2123 raise error.StateError(stringutil.forcebytestr(e))
2125 raise error.StateError(stringutil.forcebytestr(e))
2124 if opts.get(b'exact'):
2126 if opts.get(b'exact'):
2125 editor = None
2127 editor = None
2126 else:
2128 else:
2127 editor = getcommiteditor(editform=b'import.bypass')
2129 editor = getcommiteditor(editform=b'import.bypass')
2128 memctx = context.memctx(
2130 memctx = context.memctx(
2129 repo,
2131 repo,
2130 (p1.node(), p2.node()),
2132 (p1.node(), p2.node()),
2131 message,
2133 message,
2132 files=files,
2134 files=files,
2133 filectxfn=store,
2135 filectxfn=store,
2134 user=user,
2136 user=user,
2135 date=date,
2137 date=date,
2136 branch=branch,
2138 branch=branch,
2137 editor=editor,
2139 editor=editor,
2138 )
2140 )
2139
2141
2140 overrides = {}
2142 overrides = {}
2141 if opts.get(b'secret'):
2143 if opts.get(b'secret'):
2142 overrides[(b'phases', b'new-commit')] = b'secret'
2144 overrides[(b'phases', b'new-commit')] = b'secret'
2143 with repo.ui.configoverride(overrides, b'import'):
2145 with repo.ui.configoverride(overrides, b'import'):
2144 n = memctx.commit()
2146 n = memctx.commit()
2145 finally:
2147 finally:
2146 store.close()
2148 store.close()
2147 if opts.get(b'exact') and nocommit:
2149 if opts.get(b'exact') and nocommit:
2148 # --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
2149 # and branch bits
2151 # and branch bits
2150 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"))
2151 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):
2152 raise error.Abort(_(b'patch is damaged or loses information'))
2154 raise error.Abort(_(b'patch is damaged or loses information'))
2153 msg = _(b'applied to working directory')
2155 msg = _(b'applied to working directory')
2154 if n:
2156 if n:
2155 # i18n: refers to a short changeset id
2157 # i18n: refers to a short changeset id
2156 msg = _(b'created %s') % short(n)
2158 msg = _(b'created %s') % short(n)
2157 return msg, n, rejects
2159 return msg, n, rejects
2158
2160
2159
2161
2160 # facility to let extensions include additional data in an exported patch
2162 # facility to let extensions include additional data in an exported patch
2161 # list of identifiers to be executed in order
2163 # list of identifiers to be executed in order
2162 extraexport = []
2164 extraexport = []
2163 # mapping from identifier to actual export function
2165 # mapping from identifier to actual export function
2164 # 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
2165 # it is given two arguments (sequencenumber, changectx)
2167 # it is given two arguments (sequencenumber, changectx)
2166 extraexportmap = {}
2168 extraexportmap = {}
2167
2169
2168
2170
2169 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2171 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2170 node = scmutil.binnode(ctx)
2172 node = scmutil.binnode(ctx)
2171 parents = [p.node() for p in ctx.parents() if p]
2173 parents = [p.node() for p in ctx.parents() if p]
2172 branch = ctx.branch()
2174 branch = ctx.branch()
2173 if switch_parent:
2175 if switch_parent:
2174 parents.reverse()
2176 parents.reverse()
2175
2177
2176 if parents:
2178 if parents:
2177 prev = parents[0]
2179 prev = parents[0]
2178 else:
2180 else:
2179 prev = repo.nullid
2181 prev = repo.nullid
2180
2182
2181 fm.context(ctx=ctx)
2183 fm.context(ctx=ctx)
2182 fm.plain(b'# HG changeset patch\n')
2184 fm.plain(b'# HG changeset patch\n')
2183 fm.write(b'user', b'# User %s\n', ctx.user())
2185 fm.write(b'user', b'# User %s\n', ctx.user())
2184 fm.plain(b'# Date %d %d\n' % ctx.date())
2186 fm.plain(b'# Date %d %d\n' % ctx.date())
2185 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2187 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2186 fm.condwrite(
2188 fm.condwrite(
2187 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
2188 )
2190 )
2189 fm.write(b'node', b'# Node ID %s\n', hex(node))
2191 fm.write(b'node', b'# Node ID %s\n', hex(node))
2190 fm.plain(b'# Parent %s\n' % hex(prev))
2192 fm.plain(b'# Parent %s\n' % hex(prev))
2191 if len(parents) > 1:
2193 if len(parents) > 1:
2192 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2194 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2193 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'))
2194
2196
2195 # TODO: redesign extraexportmap function to support formatter
2197 # TODO: redesign extraexportmap function to support formatter
2196 for headerid in extraexport:
2198 for headerid in extraexport:
2197 header = extraexportmap[headerid](seqno, ctx)
2199 header = extraexportmap[headerid](seqno, ctx)
2198 if header is not None:
2200 if header is not None:
2199 fm.plain(b'# %s\n' % header)
2201 fm.plain(b'# %s\n' % header)
2200
2202
2201 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2203 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2202 fm.plain(b'\n')
2204 fm.plain(b'\n')
2203
2205
2204 if fm.isplain():
2206 if fm.isplain():
2205 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2207 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2206 for chunk, label in chunkiter:
2208 for chunk, label in chunkiter:
2207 fm.plain(chunk, label=label)
2209 fm.plain(chunk, label=label)
2208 else:
2210 else:
2209 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2211 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2210 # TODO: make it structured?
2212 # TODO: make it structured?
2211 fm.data(diff=b''.join(chunkiter))
2213 fm.data(diff=b''.join(chunkiter))
2212
2214
2213
2215
2214 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2216 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2215 """Export changesets to stdout or a single file"""
2217 """Export changesets to stdout or a single file"""
2216 for seqno, rev in enumerate(revs, 1):
2218 for seqno, rev in enumerate(revs, 1):
2217 ctx = repo[rev]
2219 ctx = repo[rev]
2218 if not dest.startswith(b'<'):
2220 if not dest.startswith(b'<'):
2219 repo.ui.note(b"%s\n" % dest)
2221 repo.ui.note(b"%s\n" % dest)
2220 fm.startitem()
2222 fm.startitem()
2221 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2223 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2222
2224
2223
2225
2224 def _exportfntemplate(
2226 def _exportfntemplate(
2225 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2227 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2226 ):
2228 ):
2227 """Export changesets to possibly multiple files"""
2229 """Export changesets to possibly multiple files"""
2228 total = len(revs)
2230 total = len(revs)
2229 revwidth = max(len(str(rev)) for rev in revs)
2231 revwidth = max(len(str(rev)) for rev in revs)
2230 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2232 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2231
2233
2232 for seqno, rev in enumerate(revs, 1):
2234 for seqno, rev in enumerate(revs, 1):
2233 ctx = repo[rev]
2235 ctx = repo[rev]
2234 dest = makefilename(
2236 dest = makefilename(
2235 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2237 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2236 )
2238 )
2237 filemap.setdefault(dest, []).append((seqno, rev))
2239 filemap.setdefault(dest, []).append((seqno, rev))
2238
2240
2239 for dest in filemap:
2241 for dest in filemap:
2240 with formatter.maybereopen(basefm, dest) as fm:
2242 with formatter.maybereopen(basefm, dest) as fm:
2241 repo.ui.note(b"%s\n" % dest)
2243 repo.ui.note(b"%s\n" % dest)
2242 for seqno, rev in filemap[dest]:
2244 for seqno, rev in filemap[dest]:
2243 fm.startitem()
2245 fm.startitem()
2244 ctx = repo[rev]
2246 ctx = repo[rev]
2245 _exportsingle(
2247 _exportsingle(
2246 repo, ctx, fm, match, switch_parent, seqno, diffopts
2248 repo, ctx, fm, match, switch_parent, seqno, diffopts
2247 )
2249 )
2248
2250
2249
2251
2250 def _prefetchchangedfiles(repo, revs, match):
2252 def _prefetchchangedfiles(repo, revs, match):
2251 allfiles = set()
2253 allfiles = set()
2252 for rev in revs:
2254 for rev in revs:
2253 for file in repo[rev].files():
2255 for file in repo[rev].files():
2254 if not match or match(file):
2256 if not match or match(file):
2255 allfiles.add(file)
2257 allfiles.add(file)
2256 match = scmutil.matchfiles(repo, allfiles)
2258 match = scmutil.matchfiles(repo, allfiles)
2257 revmatches = [(rev, match) for rev in revs]
2259 revmatches = [(rev, match) for rev in revs]
2258 scmutil.prefetchfiles(repo, revmatches)
2260 scmutil.prefetchfiles(repo, revmatches)
2259
2261
2260
2262
2261 def export(
2263 def export(
2262 repo,
2264 repo,
2263 revs,
2265 revs,
2264 basefm,
2266 basefm,
2265 fntemplate=b'hg-%h.patch',
2267 fntemplate=b'hg-%h.patch',
2266 switch_parent=False,
2268 switch_parent=False,
2267 opts=None,
2269 opts=None,
2268 match=None,
2270 match=None,
2269 ):
2271 ):
2270 """export changesets as hg patches
2272 """export changesets as hg patches
2271
2273
2272 Args:
2274 Args:
2273 repo: The repository from which we're exporting revisions.
2275 repo: The repository from which we're exporting revisions.
2274 revs: A list of revisions to export as revision numbers.
2276 revs: A list of revisions to export as revision numbers.
2275 basefm: A formatter to which patches should be written.
2277 basefm: A formatter to which patches should be written.
2276 fntemplate: An optional string to use for generating patch file names.
2278 fntemplate: An optional string to use for generating patch file names.
2277 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.
2278 Default is false, which always shows diff against p1.
2280 Default is false, which always shows diff against p1.
2279 opts: diff options to use for generating the patch.
2281 opts: diff options to use for generating the patch.
2280 match: If specified, only export changes to files matching this matcher.
2282 match: If specified, only export changes to files matching this matcher.
2281
2283
2282 Returns:
2284 Returns:
2283 Nothing.
2285 Nothing.
2284
2286
2285 Side Effect:
2287 Side Effect:
2286 "HG Changeset Patch" data is emitted to one of the following
2288 "HG Changeset Patch" data is emitted to one of the following
2287 destinations:
2289 destinations:
2288 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
2289 the given template.
2291 the given template.
2290 Otherwise: All revs will be written to basefm.
2292 Otherwise: All revs will be written to basefm.
2291 """
2293 """
2292 _prefetchchangedfiles(repo, revs, match)
2294 _prefetchchangedfiles(repo, revs, match)
2293
2295
2294 if not fntemplate:
2296 if not fntemplate:
2295 _exportfile(
2297 _exportfile(
2296 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2298 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2297 )
2299 )
2298 else:
2300 else:
2299 _exportfntemplate(
2301 _exportfntemplate(
2300 repo, revs, basefm, fntemplate, switch_parent, opts, match
2302 repo, revs, basefm, fntemplate, switch_parent, opts, match
2301 )
2303 )
2302
2304
2303
2305
2304 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):
2305 """Export changesets to the given file stream"""
2307 """Export changesets to the given file stream"""
2306 _prefetchchangedfiles(repo, revs, match)
2308 _prefetchchangedfiles(repo, revs, match)
2307
2309
2308 dest = getattr(fp, 'name', b'<unnamed>')
2310 dest = getattr(fp, 'name', b'<unnamed>')
2309 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2311 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2310 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2312 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2311
2313
2312
2314
2313 def showmarker(fm, marker, index=None):
2315 def showmarker(fm, marker, index=None):
2314 """utility function to display obsolescence marker in a readable way
2316 """utility function to display obsolescence marker in a readable way
2315
2317
2316 To be used by debug function."""
2318 To be used by debug function."""
2317 if index is not None:
2319 if index is not None:
2318 fm.write(b'index', b'%i ', index)
2320 fm.write(b'index', b'%i ', index)
2319 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2321 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2320 succs = marker.succnodes()
2322 succs = marker.succnodes()
2321 fm.condwrite(
2323 fm.condwrite(
2322 succs,
2324 succs,
2323 b'succnodes',
2325 b'succnodes',
2324 b'%s ',
2326 b'%s ',
2325 fm.formatlist(map(hex, succs), name=b'node'),
2327 fm.formatlist(map(hex, succs), name=b'node'),
2326 )
2328 )
2327 fm.write(b'flag', b'%X ', marker.flags())
2329 fm.write(b'flag', b'%X ', marker.flags())
2328 parents = marker.parentnodes()
2330 parents = marker.parentnodes()
2329 if parents is not None:
2331 if parents is not None:
2330 fm.write(
2332 fm.write(
2331 b'parentnodes',
2333 b'parentnodes',
2332 b'{%s} ',
2334 b'{%s} ',
2333 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2335 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2334 )
2336 )
2335 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2337 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2336 meta = marker.metadata().copy()
2338 meta = marker.metadata().copy()
2337 meta.pop(b'date', None)
2339 meta.pop(b'date', None)
2338 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2340 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2339 fm.write(
2341 fm.write(
2340 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', ')
2341 )
2343 )
2342 fm.plain(b'\n')
2344 fm.plain(b'\n')
2343
2345
2344
2346
2345 def finddate(ui, repo, date):
2347 def finddate(ui, repo, date):
2346 """Find the tipmost changeset that matches the given date spec"""
2348 """Find the tipmost changeset that matches the given date spec"""
2347 mrevs = repo.revs(b'date(%s)', date)
2349 mrevs = repo.revs(b'date(%s)', date)
2348 try:
2350 try:
2349 rev = mrevs.max()
2351 rev = mrevs.max()
2350 except ValueError:
2352 except ValueError:
2351 raise error.InputError(_(b"revision matching date not found"))
2353 raise error.InputError(_(b"revision matching date not found"))
2352
2354
2353 ui.status(
2355 ui.status(
2354 _(b"found revision %d from %s\n")
2356 _(b"found revision %d from %s\n")
2355 % (rev, dateutil.datestr(repo[rev].date()))
2357 % (rev, dateutil.datestr(repo[rev].date()))
2356 )
2358 )
2357 return b'%d' % rev
2359 return b'%d' % rev
2358
2360
2359
2361
2360 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2362 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2361 bad = []
2363 bad = []
2362
2364
2363 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)
2364 names = []
2366 names = []
2365 wctx = repo[None]
2367 wctx = repo[None]
2366 cca = None
2368 cca = None
2367 abort, warn = scmutil.checkportabilityalert(ui)
2369 abort, warn = scmutil.checkportabilityalert(ui)
2368 if abort or warn:
2370 if abort or warn:
2369 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2371 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2370
2372
2371 match = repo.narrowmatch(match, includeexact=True)
2373 match = repo.narrowmatch(match, includeexact=True)
2372 badmatch = matchmod.badmatch(match, badfn)
2374 badmatch = matchmod.badmatch(match, badfn)
2373 dirstate = repo.dirstate
2375 dirstate = repo.dirstate
2374 # 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
2375 # clean files, which we aren't interested in and takes time.
2377 # clean files, which we aren't interested in and takes time.
2376 for f in sorted(
2378 for f in sorted(
2377 dirstate.walk(
2379 dirstate.walk(
2378 badmatch,
2380 badmatch,
2379 subrepos=sorted(wctx.substate),
2381 subrepos=sorted(wctx.substate),
2380 unknown=True,
2382 unknown=True,
2381 ignored=False,
2383 ignored=False,
2382 full=False,
2384 full=False,
2383 )
2385 )
2384 ):
2386 ):
2385 exact = match.exact(f)
2387 exact = match.exact(f)
2386 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):
2387 if cca:
2389 if cca:
2388 cca(f)
2390 cca(f)
2389 names.append(f)
2391 names.append(f)
2390 if ui.verbose or not exact:
2392 if ui.verbose or not exact:
2391 ui.status(
2393 ui.status(
2392 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2394 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2393 )
2395 )
2394
2396
2395 for subpath in sorted(wctx.substate):
2397 for subpath in sorted(wctx.substate):
2396 sub = wctx.sub(subpath)
2398 sub = wctx.sub(subpath)
2397 try:
2399 try:
2398 submatch = matchmod.subdirmatcher(subpath, match)
2400 submatch = matchmod.subdirmatcher(subpath, match)
2399 subprefix = repo.wvfs.reljoin(prefix, subpath)
2401 subprefix = repo.wvfs.reljoin(prefix, subpath)
2400 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2402 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2401 if opts.get('subrepos'):
2403 if opts.get('subrepos'):
2402 bad.extend(
2404 bad.extend(
2403 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2405 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2404 )
2406 )
2405 else:
2407 else:
2406 bad.extend(
2408 bad.extend(
2407 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2409 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2408 )
2410 )
2409 except error.LookupError:
2411 except error.LookupError:
2410 ui.status(
2412 ui.status(
2411 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2413 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2412 )
2414 )
2413
2415
2414 if not opts.get('dry_run'):
2416 if not opts.get('dry_run'):
2415 rejected = wctx.add(names, prefix)
2417 rejected = wctx.add(names, prefix)
2416 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())
2417 return bad
2419 return bad
2418
2420
2419
2421
2420 def addwebdirpath(repo, serverpath, webconf):
2422 def addwebdirpath(repo, serverpath, webconf):
2421 webconf[serverpath] = repo.root
2423 webconf[serverpath] = repo.root
2422 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2424 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2423
2425
2424 for r in repo.revs(b'filelog("path:.hgsub")'):
2426 for r in repo.revs(b'filelog("path:.hgsub")'):
2425 ctx = repo[r]
2427 ctx = repo[r]
2426 for subpath in ctx.substate:
2428 for subpath in ctx.substate:
2427 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2429 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2428
2430
2429
2431
2430 def forget(
2432 def forget(
2431 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2433 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2432 ):
2434 ):
2433 if dryrun and interactive:
2435 if dryrun and interactive:
2434 raise error.InputError(
2436 raise error.InputError(
2435 _(b"cannot specify both --dry-run and --interactive")
2437 _(b"cannot specify both --dry-run and --interactive")
2436 )
2438 )
2437 bad = []
2439 bad = []
2438 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)
2439 wctx = repo[None]
2441 wctx = repo[None]
2440 forgot = []
2442 forgot = []
2441
2443
2442 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2444 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2443 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2445 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2444 if explicitonly:
2446 if explicitonly:
2445 forget = [f for f in forget if match.exact(f)]
2447 forget = [f for f in forget if match.exact(f)]
2446
2448
2447 for subpath in sorted(wctx.substate):
2449 for subpath in sorted(wctx.substate):
2448 sub = wctx.sub(subpath)
2450 sub = wctx.sub(subpath)
2449 submatch = matchmod.subdirmatcher(subpath, match)
2451 submatch = matchmod.subdirmatcher(subpath, match)
2450 subprefix = repo.wvfs.reljoin(prefix, subpath)
2452 subprefix = repo.wvfs.reljoin(prefix, subpath)
2451 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2453 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2452 try:
2454 try:
2453 subbad, subforgot = sub.forget(
2455 subbad, subforgot = sub.forget(
2454 submatch,
2456 submatch,
2455 subprefix,
2457 subprefix,
2456 subuipathfn,
2458 subuipathfn,
2457 dryrun=dryrun,
2459 dryrun=dryrun,
2458 interactive=interactive,
2460 interactive=interactive,
2459 )
2461 )
2460 bad.extend([subpath + b'/' + f for f in subbad])
2462 bad.extend([subpath + b'/' + f for f in subbad])
2461 forgot.extend([subpath + b'/' + f for f in subforgot])
2463 forgot.extend([subpath + b'/' + f for f in subforgot])
2462 except error.LookupError:
2464 except error.LookupError:
2463 ui.status(
2465 ui.status(
2464 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2466 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2465 )
2467 )
2466
2468
2467 if not explicitonly:
2469 if not explicitonly:
2468 for f in match.files():
2470 for f in match.files():
2469 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):
2470 if f not in forgot:
2472 if f not in forgot:
2471 if repo.wvfs.exists(f):
2473 if repo.wvfs.exists(f):
2472 # Don't complain if the exact case match wasn't given.
2474 # Don't complain if the exact case match wasn't given.
2473 # But don't do this until after checking 'forgot', so
2475 # But don't do this until after checking 'forgot', so
2474 # that subrepo files aren't normalized, and this op is
2476 # that subrepo files aren't normalized, and this op is
2475 # purely from data cached by the status walk above.
2477 # purely from data cached by the status walk above.
2476 if repo.dirstate.normalize(f) in repo.dirstate:
2478 if repo.dirstate.normalize(f) in repo.dirstate:
2477 continue
2479 continue
2478 ui.warn(
2480 ui.warn(
2479 _(
2481 _(
2480 b'not removing %s: '
2482 b'not removing %s: '
2481 b'file is already untracked\n'
2483 b'file is already untracked\n'
2482 )
2484 )
2483 % uipathfn(f)
2485 % uipathfn(f)
2484 )
2486 )
2485 bad.append(f)
2487 bad.append(f)
2486
2488
2487 if interactive:
2489 if interactive:
2488 responses = _(
2490 responses = _(
2489 b'[Ynsa?]'
2491 b'[Ynsa?]'
2490 b'$$ &Yes, forget this file'
2492 b'$$ &Yes, forget this file'
2491 b'$$ &No, skip this file'
2493 b'$$ &No, skip this file'
2492 b'$$ &Skip remaining files'
2494 b'$$ &Skip remaining files'
2493 b'$$ Include &all remaining files'
2495 b'$$ Include &all remaining files'
2494 b'$$ &? (display help)'
2496 b'$$ &? (display help)'
2495 )
2497 )
2496 for filename in forget[:]:
2498 for filename in forget[:]:
2497 r = ui.promptchoice(
2499 r = ui.promptchoice(
2498 _(b'forget %s %s') % (uipathfn(filename), responses)
2500 _(b'forget %s %s') % (uipathfn(filename), responses)
2499 )
2501 )
2500 if r == 4: # ?
2502 if r == 4: # ?
2501 while r == 4:
2503 while r == 4:
2502 for c, t in ui.extractchoices(responses)[1]:
2504 for c, t in ui.extractchoices(responses)[1]:
2503 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2505 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2504 r = ui.promptchoice(
2506 r = ui.promptchoice(
2505 _(b'forget %s %s') % (uipathfn(filename), responses)
2507 _(b'forget %s %s') % (uipathfn(filename), responses)
2506 )
2508 )
2507 if r == 0: # yes
2509 if r == 0: # yes
2508 continue
2510 continue
2509 elif r == 1: # no
2511 elif r == 1: # no
2510 forget.remove(filename)
2512 forget.remove(filename)
2511 elif r == 2: # Skip
2513 elif r == 2: # Skip
2512 fnindex = forget.index(filename)
2514 fnindex = forget.index(filename)
2513 del forget[fnindex:]
2515 del forget[fnindex:]
2514 break
2516 break
2515 elif r == 3: # All
2517 elif r == 3: # All
2516 break
2518 break
2517
2519
2518 for f in forget:
2520 for f in forget:
2519 if ui.verbose or not match.exact(f) or interactive:
2521 if ui.verbose or not match.exact(f) or interactive:
2520 ui.status(
2522 ui.status(
2521 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2523 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2522 )
2524 )
2523
2525
2524 if not dryrun:
2526 if not dryrun:
2525 rejected = wctx.forget(forget, prefix)
2527 rejected = wctx.forget(forget, prefix)
2526 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())
2527 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)
2528 return bad, forgot
2530 return bad, forgot
2529
2531
2530
2532
2531 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2533 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2532 ret = 1
2534 ret = 1
2533
2535
2534 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2536 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2535 if fm.isplain() and not needsfctx:
2537 if fm.isplain() and not needsfctx:
2536 # 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
2537 # calls to ui.write.
2539 # calls to ui.write.
2538 buf = []
2540 buf = []
2539 for f in ctx.matches(m):
2541 for f in ctx.matches(m):
2540 buf.append(fmt % uipathfn(f))
2542 buf.append(fmt % uipathfn(f))
2541 if len(buf) > 100:
2543 if len(buf) > 100:
2542 ui.write(b''.join(buf))
2544 ui.write(b''.join(buf))
2543 del buf[:]
2545 del buf[:]
2544 ret = 0
2546 ret = 0
2545 if buf:
2547 if buf:
2546 ui.write(b''.join(buf))
2548 ui.write(b''.join(buf))
2547 else:
2549 else:
2548 for f in ctx.matches(m):
2550 for f in ctx.matches(m):
2549 fm.startitem()
2551 fm.startitem()
2550 fm.context(ctx=ctx)
2552 fm.context(ctx=ctx)
2551 if needsfctx:
2553 if needsfctx:
2552 fc = ctx[f]
2554 fc = ctx[f]
2553 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())
2554 fm.data(path=f)
2556 fm.data(path=f)
2555 fm.plain(fmt % uipathfn(f))
2557 fm.plain(fmt % uipathfn(f))
2556 ret = 0
2558 ret = 0
2557
2559
2558 for subpath in sorted(ctx.substate):
2560 for subpath in sorted(ctx.substate):
2559 submatch = matchmod.subdirmatcher(subpath, m)
2561 submatch = matchmod.subdirmatcher(subpath, m)
2560 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2562 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2561 if subrepos or m.exact(subpath) or any(submatch.files()):
2563 if subrepos or m.exact(subpath) or any(submatch.files()):
2562 sub = ctx.sub(subpath)
2564 sub = ctx.sub(subpath)
2563 try:
2565 try:
2564 recurse = m.exact(subpath) or subrepos
2566 recurse = m.exact(subpath) or subrepos
2565 if (
2567 if (
2566 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2568 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2567 == 0
2569 == 0
2568 ):
2570 ):
2569 ret = 0
2571 ret = 0
2570 except error.LookupError:
2572 except error.LookupError:
2571 ui.status(
2573 ui.status(
2572 _(b"skipping missing subrepository: %s\n")
2574 _(b"skipping missing subrepository: %s\n")
2573 % uipathfn(subpath)
2575 % uipathfn(subpath)
2574 )
2576 )
2575
2577
2576 return ret
2578 return ret
2577
2579
2578
2580
2579 def remove(
2581 def remove(
2580 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2582 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2581 ):
2583 ):
2582 ret = 0
2584 ret = 0
2583 s = repo.status(match=m, clean=True)
2585 s = repo.status(match=m, clean=True)
2584 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
2585
2587
2586 wctx = repo[None]
2588 wctx = repo[None]
2587
2589
2588 if warnings is None:
2590 if warnings is None:
2589 warnings = []
2591 warnings = []
2590 warn = True
2592 warn = True
2591 else:
2593 else:
2592 warn = False
2594 warn = False
2593
2595
2594 subs = sorted(wctx.substate)
2596 subs = sorted(wctx.substate)
2595 progress = ui.makeprogress(
2597 progress = ui.makeprogress(
2596 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2598 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2597 )
2599 )
2598 for subpath in subs:
2600 for subpath in subs:
2599 submatch = matchmod.subdirmatcher(subpath, m)
2601 submatch = matchmod.subdirmatcher(subpath, m)
2600 subprefix = repo.wvfs.reljoin(prefix, subpath)
2602 subprefix = repo.wvfs.reljoin(prefix, subpath)
2601 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2603 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2602 if subrepos or m.exact(subpath) or any(submatch.files()):
2604 if subrepos or m.exact(subpath) or any(submatch.files()):
2603 progress.increment()
2605 progress.increment()
2604 sub = wctx.sub(subpath)
2606 sub = wctx.sub(subpath)
2605 try:
2607 try:
2606 if sub.removefiles(
2608 if sub.removefiles(
2607 submatch,
2609 submatch,
2608 subprefix,
2610 subprefix,
2609 subuipathfn,
2611 subuipathfn,
2610 after,
2612 after,
2611 force,
2613 force,
2612 subrepos,
2614 subrepos,
2613 dryrun,
2615 dryrun,
2614 warnings,
2616 warnings,
2615 ):
2617 ):
2616 ret = 1
2618 ret = 1
2617 except error.LookupError:
2619 except error.LookupError:
2618 warnings.append(
2620 warnings.append(
2619 _(b"skipping missing subrepository: %s\n")
2621 _(b"skipping missing subrepository: %s\n")
2620 % uipathfn(subpath)
2622 % uipathfn(subpath)
2621 )
2623 )
2622 progress.complete()
2624 progress.complete()
2623
2625
2624 # warn about failure to delete explicit files/dirs
2626 # warn about failure to delete explicit files/dirs
2625 deleteddirs = pathutil.dirs(deleted)
2627 deleteddirs = pathutil.dirs(deleted)
2626 files = m.files()
2628 files = m.files()
2627 progress = ui.makeprogress(
2629 progress = ui.makeprogress(
2628 _(b'deleting'), total=len(files), unit=_(b'files')
2630 _(b'deleting'), total=len(files), unit=_(b'files')
2629 )
2631 )
2630 for f in files:
2632 for f in files:
2631
2633
2632 def insubrepo():
2634 def insubrepo():
2633 for subpath in wctx.substate:
2635 for subpath in wctx.substate:
2634 if f.startswith(subpath + b'/'):
2636 if f.startswith(subpath + b'/'):
2635 return True
2637 return True
2636 return False
2638 return False
2637
2639
2638 progress.increment()
2640 progress.increment()
2639 isdir = f in deleteddirs or wctx.hasdir(f)
2641 isdir = f in deleteddirs or wctx.hasdir(f)
2640 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:
2641 continue
2643 continue
2642
2644
2643 if repo.wvfs.exists(f):
2645 if repo.wvfs.exists(f):
2644 if repo.wvfs.isdir(f):
2646 if repo.wvfs.isdir(f):
2645 warnings.append(
2647 warnings.append(
2646 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2648 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2647 )
2649 )
2648 else:
2650 else:
2649 warnings.append(
2651 warnings.append(
2650 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2652 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2651 )
2653 )
2652 # missing files will generate a warning elsewhere
2654 # missing files will generate a warning elsewhere
2653 ret = 1
2655 ret = 1
2654 progress.complete()
2656 progress.complete()
2655
2657
2656 if force:
2658 if force:
2657 list = modified + deleted + clean + added
2659 list = modified + deleted + clean + added
2658 elif after:
2660 elif after:
2659 list = deleted
2661 list = deleted
2660 remaining = modified + added + clean
2662 remaining = modified + added + clean
2661 progress = ui.makeprogress(
2663 progress = ui.makeprogress(
2662 _(b'skipping'), total=len(remaining), unit=_(b'files')
2664 _(b'skipping'), total=len(remaining), unit=_(b'files')
2663 )
2665 )
2664 for f in remaining:
2666 for f in remaining:
2665 progress.increment()
2667 progress.increment()
2666 if ui.verbose or (f in files):
2668 if ui.verbose or (f in files):
2667 warnings.append(
2669 warnings.append(
2668 _(b'not removing %s: file still exists\n') % uipathfn(f)
2670 _(b'not removing %s: file still exists\n') % uipathfn(f)
2669 )
2671 )
2670 ret = 1
2672 ret = 1
2671 progress.complete()
2673 progress.complete()
2672 else:
2674 else:
2673 list = deleted + clean
2675 list = deleted + clean
2674 progress = ui.makeprogress(
2676 progress = ui.makeprogress(
2675 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2677 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2676 )
2678 )
2677 for f in modified:
2679 for f in modified:
2678 progress.increment()
2680 progress.increment()
2679 warnings.append(
2681 warnings.append(
2680 _(
2682 _(
2681 b'not removing %s: file is modified (use -f'
2683 b'not removing %s: file is modified (use -f'
2682 b' to force removal)\n'
2684 b' to force removal)\n'
2683 )
2685 )
2684 % uipathfn(f)
2686 % uipathfn(f)
2685 )
2687 )
2686 ret = 1
2688 ret = 1
2687 for f in added:
2689 for f in added:
2688 progress.increment()
2690 progress.increment()
2689 warnings.append(
2691 warnings.append(
2690 _(
2692 _(
2691 b"not removing %s: file has been marked for add"
2693 b"not removing %s: file has been marked for add"
2692 b" (use 'hg forget' to undo add)\n"
2694 b" (use 'hg forget' to undo add)\n"
2693 )
2695 )
2694 % uipathfn(f)
2696 % uipathfn(f)
2695 )
2697 )
2696 ret = 1
2698 ret = 1
2697 progress.complete()
2699 progress.complete()
2698
2700
2699 list = sorted(list)
2701 list = sorted(list)
2700 progress = ui.makeprogress(
2702 progress = ui.makeprogress(
2701 _(b'deleting'), total=len(list), unit=_(b'files')
2703 _(b'deleting'), total=len(list), unit=_(b'files')
2702 )
2704 )
2703 for f in list:
2705 for f in list:
2704 if ui.verbose or not m.exact(f):
2706 if ui.verbose or not m.exact(f):
2705 progress.increment()
2707 progress.increment()
2706 ui.status(
2708 ui.status(
2707 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2709 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2708 )
2710 )
2709 progress.complete()
2711 progress.complete()
2710
2712
2711 if not dryrun:
2713 if not dryrun:
2712 with repo.wlock():
2714 with repo.wlock():
2713 if not after:
2715 if not after:
2714 for f in list:
2716 for f in list:
2715 if f in added:
2717 if f in added:
2716 continue # we never unlink added files on remove
2718 continue # we never unlink added files on remove
2717 rmdir = repo.ui.configbool(
2719 rmdir = repo.ui.configbool(
2718 b'experimental', b'removeemptydirs'
2720 b'experimental', b'removeemptydirs'
2719 )
2721 )
2720 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2722 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2721 repo[None].forget(list)
2723 repo[None].forget(list)
2722
2724
2723 if warn:
2725 if warn:
2724 for warning in warnings:
2726 for warning in warnings:
2725 ui.warn(warning)
2727 ui.warn(warning)
2726
2728
2727 return ret
2729 return ret
2728
2730
2729
2731
2730 def _catfmtneedsdata(fm):
2732 def _catfmtneedsdata(fm):
2731 return not fm.datahint() or b'data' in fm.datahint()
2733 return not fm.datahint() or b'data' in fm.datahint()
2732
2734
2733
2735
2734 def _updatecatformatter(fm, ctx, matcher, path, decode):
2736 def _updatecatformatter(fm, ctx, matcher, path, decode):
2735 """Hook for adding data to the formatter used by ``hg cat``.
2737 """Hook for adding data to the formatter used by ``hg cat``.
2736
2738
2737 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
2738 this method first."""
2740 this method first."""
2739
2741
2740 # 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
2741 # wasn't requested.
2743 # wasn't requested.
2742 data = b''
2744 data = b''
2743 if _catfmtneedsdata(fm):
2745 if _catfmtneedsdata(fm):
2744 data = ctx[path].data()
2746 data = ctx[path].data()
2745 if decode:
2747 if decode:
2746 data = ctx.repo().wwritedata(path, data)
2748 data = ctx.repo().wwritedata(path, data)
2747 fm.startitem()
2749 fm.startitem()
2748 fm.context(ctx=ctx)
2750 fm.context(ctx=ctx)
2749 fm.write(b'data', b'%s', data)
2751 fm.write(b'data', b'%s', data)
2750 fm.data(path=path)
2752 fm.data(path=path)
2751
2753
2752
2754
2753 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2755 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2754 err = 1
2756 err = 1
2755 opts = pycompat.byteskwargs(opts)
2757 opts = pycompat.byteskwargs(opts)
2756
2758
2757 def write(path):
2759 def write(path):
2758 filename = None
2760 filename = None
2759 if fntemplate:
2761 if fntemplate:
2760 filename = makefilename(
2762 filename = makefilename(
2761 ctx, fntemplate, pathname=os.path.join(prefix, path)
2763 ctx, fntemplate, pathname=os.path.join(prefix, path)
2762 )
2764 )
2763 # attempt to create the directory if it does not already exist
2765 # attempt to create the directory if it does not already exist
2764 try:
2766 try:
2765 os.makedirs(os.path.dirname(filename))
2767 os.makedirs(os.path.dirname(filename))
2766 except OSError:
2768 except OSError:
2767 pass
2769 pass
2768 with formatter.maybereopen(basefm, filename) as fm:
2770 with formatter.maybereopen(basefm, filename) as fm:
2769 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2771 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2770
2772
2771 # Automation often uses hg cat on single files, so special case it
2773 # Automation often uses hg cat on single files, so special case it
2772 # for performance to avoid the cost of parsing the manifest.
2774 # for performance to avoid the cost of parsing the manifest.
2773 if len(matcher.files()) == 1 and not matcher.anypats():
2775 if len(matcher.files()) == 1 and not matcher.anypats():
2774 file = matcher.files()[0]
2776 file = matcher.files()[0]
2775 mfl = repo.manifestlog
2777 mfl = repo.manifestlog
2776 mfnode = ctx.manifestnode()
2778 mfnode = ctx.manifestnode()
2777 try:
2779 try:
2778 if mfnode and mfl[mfnode].find(file)[0]:
2780 if mfnode and mfl[mfnode].find(file)[0]:
2779 if _catfmtneedsdata(basefm):
2781 if _catfmtneedsdata(basefm):
2780 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2782 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2781 write(file)
2783 write(file)
2782 return 0
2784 return 0
2783 except KeyError:
2785 except KeyError:
2784 pass
2786 pass
2785
2787
2786 if _catfmtneedsdata(basefm):
2788 if _catfmtneedsdata(basefm):
2787 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2789 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2788
2790
2789 for abs in ctx.walk(matcher):
2791 for abs in ctx.walk(matcher):
2790 write(abs)
2792 write(abs)
2791 err = 0
2793 err = 0
2792
2794
2793 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2795 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2794 for subpath in sorted(ctx.substate):
2796 for subpath in sorted(ctx.substate):
2795 sub = ctx.sub(subpath)
2797 sub = ctx.sub(subpath)
2796 try:
2798 try:
2797 submatch = matchmod.subdirmatcher(subpath, matcher)
2799 submatch = matchmod.subdirmatcher(subpath, matcher)
2798 subprefix = os.path.join(prefix, subpath)
2800 subprefix = os.path.join(prefix, subpath)
2799 if not sub.cat(
2801 if not sub.cat(
2800 submatch,
2802 submatch,
2801 basefm,
2803 basefm,
2802 fntemplate,
2804 fntemplate,
2803 subprefix,
2805 subprefix,
2804 **pycompat.strkwargs(opts),
2806 **pycompat.strkwargs(opts),
2805 ):
2807 ):
2806 err = 0
2808 err = 0
2807 except error.RepoLookupError:
2809 except error.RepoLookupError:
2808 ui.status(
2810 ui.status(
2809 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2811 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2810 )
2812 )
2811
2813
2812 return err
2814 return err
2813
2815
2814
2816
2815 class _AddRemoveContext:
2817 class _AddRemoveContext:
2816 """a small (hacky) context to deal with lazy opening of context
2818 """a small (hacky) context to deal with lazy opening of context
2817
2819
2818 This is to be used in the `commit` function right below. This deals with
2820 This is to be used in the `commit` function right below. This deals with
2819 lazily open a `changing_files` context inside a `transaction` that span the
2821 lazily open a `changing_files` context inside a `transaction` that span the
2820 full commit operation.
2822 full commit operation.
2821
2823
2822 We need :
2824 We need :
2823 - a `changing_files` context to wrap the dirstate change within the
2825 - a `changing_files` context to wrap the dirstate change within the
2824 "addremove" operation,
2826 "addremove" operation,
2825 - a transaction to make sure these change are not written right after the
2827 - a transaction to make sure these change are not written right after the
2826 addremove, but when the commit operation succeed.
2828 addremove, but when the commit operation succeed.
2827
2829
2828 However it get complicated because:
2830 However it get complicated because:
2829 - opening a transaction "this early" shuffle hooks order, especially the
2831 - opening a transaction "this early" shuffle hooks order, especially the
2830 `precommit` one happening after the `pretxtopen` one which I am not too
2832 `precommit` one happening after the `pretxtopen` one which I am not too
2831 enthusiastic about.
2833 enthusiastic about.
2832 - the `mq` extensions + the `record` extension stacks many layers of call
2834 - the `mq` extensions + the `record` extension stacks many layers of call
2833 to implement `qrefresh --interactive` and this result with `mq` calling a
2835 to implement `qrefresh --interactive` and this result with `mq` calling a
2834 `strip` in the middle of this function. Which prevent the existence of
2836 `strip` in the middle of this function. Which prevent the existence of
2835 transaction wrapping all of its function code. (however, `qrefresh` never
2837 transaction wrapping all of its function code. (however, `qrefresh` never
2836 call the `addremove` bits.
2838 call the `addremove` bits.
2837 - the largefile extensions (and maybe other extensions?) wraps `addremove`
2839 - the largefile extensions (and maybe other extensions?) wraps `addremove`
2838 so slicing `addremove` in smaller bits is a complex endeavour.
2840 so slicing `addremove` in smaller bits is a complex endeavour.
2839
2841
2840 So I eventually took a this shortcut that open the transaction if we
2842 So I eventually took a this shortcut that open the transaction if we
2841 actually needs it, not disturbing much of the rest of the code.
2843 actually needs it, not disturbing much of the rest of the code.
2842
2844
2843 It will result in some hooks order change for `hg commit --addremove`,
2845 It will result in some hooks order change for `hg commit --addremove`,
2844 however it seems a corner case enough to ignore that for now (hopefully).
2846 however it seems a corner case enough to ignore that for now (hopefully).
2845
2847
2846 Notes that None of the above problems seems insurmountable, however I have
2848 Notes that None of the above problems seems insurmountable, however I have
2847 been fighting with this specific piece of code for a couple of day already
2849 been fighting with this specific piece of code for a couple of day already
2848 and I need a solution to keep moving forward on the bigger work around
2850 and I need a solution to keep moving forward on the bigger work around
2849 `changing_files` context that is being introduced at the same time as this
2851 `changing_files` context that is being introduced at the same time as this
2850 hack.
2852 hack.
2851
2853
2852 Each problem seems to have a solution:
2854 Each problem seems to have a solution:
2853 - the hook order issue could be solved by refactoring the many-layer stack
2855 - the hook order issue could be solved by refactoring the many-layer stack
2854 that currently composes a commit and calling them earlier,
2856 that currently composes a commit and calling them earlier,
2855 - the mq issue could be solved by refactoring `mq` so that the final strip
2857 - the mq issue could be solved by refactoring `mq` so that the final strip
2856 is done after transaction closure. Be warned that the mq code is quite
2858 is done after transaction closure. Be warned that the mq code is quite
2857 antic however.
2859 antic however.
2858 - large-file could be reworked in parallel of the `addremove` to be
2860 - large-file could be reworked in parallel of the `addremove` to be
2859 friendlier to this.
2861 friendlier to this.
2860
2862
2861 However each of these tasks are too much a diversion right now. In addition
2863 However each of these tasks are too much a diversion right now. In addition
2862 they will be much easier to undertake when the `changing_files` dust has
2864 they will be much easier to undertake when the `changing_files` dust has
2863 settled."""
2865 settled."""
2864
2866
2865 def __init__(self, repo):
2867 def __init__(self, repo):
2866 self._repo = repo
2868 self._repo = repo
2867 self._transaction = None
2869 self._transaction = None
2868 self._dirstate_context = None
2870 self._dirstate_context = None
2869 self._state = None
2871 self._state = None
2870
2872
2871 def __enter__(self):
2873 def __enter__(self):
2872 assert self._state is None
2874 assert self._state is None
2873 self._state = True
2875 self._state = True
2874 return self
2876 return self
2875
2877
2876 def open_transaction(self):
2878 def open_transaction(self):
2877 """open a `transaction` and `changing_files` context
2879 """open a `transaction` and `changing_files` context
2878
2880
2879 Call this when you know that change to the dirstate will be needed and
2881 Call this when you know that change to the dirstate will be needed and
2880 we need to open the transaction early
2882 we need to open the transaction early
2881
2883
2882 This will also open the dirstate `changing_files` context, so you should
2884 This will also open the dirstate `changing_files` context, so you should
2883 call `close_dirstate_context` when the distate changes are done.
2885 call `close_dirstate_context` when the distate changes are done.
2884 """
2886 """
2885 assert self._state is not None
2887 assert self._state is not None
2886 if self._transaction is None:
2888 if self._transaction is None:
2887 self._transaction = self._repo.transaction(b'commit')
2889 self._transaction = self._repo.transaction(b'commit')
2888 self._transaction.__enter__()
2890 self._transaction.__enter__()
2889 if self._dirstate_context is None:
2891 if self._dirstate_context is None:
2890 self._dirstate_context = self._repo.dirstate.changing_files(
2892 self._dirstate_context = self._repo.dirstate.changing_files(
2891 self._repo
2893 self._repo
2892 )
2894 )
2893 self._dirstate_context.__enter__()
2895 self._dirstate_context.__enter__()
2894
2896
2895 def close_dirstate_context(self):
2897 def close_dirstate_context(self):
2896 """close the change_files if any
2898 """close the change_files if any
2897
2899
2898 Call this after the (potential) `open_transaction` call to close the
2900 Call this after the (potential) `open_transaction` call to close the
2899 (potential) changing_files context.
2901 (potential) changing_files context.
2900 """
2902 """
2901 if self._dirstate_context is not None:
2903 if self._dirstate_context is not None:
2902 self._dirstate_context.__exit__(None, None, None)
2904 self._dirstate_context.__exit__(None, None, None)
2903 self._dirstate_context = None
2905 self._dirstate_context = None
2904
2906
2905 def __exit__(self, *args):
2907 def __exit__(self, *args):
2906 if self._dirstate_context is not None:
2908 if self._dirstate_context is not None:
2907 self._dirstate_context.__exit__(*args)
2909 self._dirstate_context.__exit__(*args)
2908 if self._transaction is not None:
2910 if self._transaction is not None:
2909 self._transaction.__exit__(*args)
2911 self._transaction.__exit__(*args)
2910
2912
2911
2913
2912 def commit(ui, repo, commitfunc, pats, opts):
2914 def commit(ui, repo, commitfunc, pats, opts):
2913 '''commit the specified files or all outstanding changes'''
2915 '''commit the specified files or all outstanding changes'''
2914 date = opts.get(b'date')
2916 date = opts.get(b'date')
2915 if date:
2917 if date:
2916 opts[b'date'] = dateutil.parsedate(date)
2918 opts[b'date'] = dateutil.parsedate(date)
2917
2919
2918 with repo.wlock(), repo.lock():
2920 with repo.wlock(), repo.lock():
2919 message = logmessage(ui, opts)
2921 message = logmessage(ui, opts)
2920 matcher = scmutil.match(repo[None], pats, opts)
2922 matcher = scmutil.match(repo[None], pats, opts)
2921
2923
2922 with _AddRemoveContext(repo) as c:
2924 with _AddRemoveContext(repo) as c:
2923 # extract addremove carefully -- this function can be called from a
2925 # extract addremove carefully -- this function can be called from a
2924 # command that doesn't support addremove
2926 # command that doesn't support addremove
2925 if opts.get(b'addremove'):
2927 if opts.get(b'addremove'):
2926 relative = scmutil.anypats(pats, opts)
2928 relative = scmutil.anypats(pats, opts)
2927 uipathfn = scmutil.getuipathfn(
2929 uipathfn = scmutil.getuipathfn(
2928 repo,
2930 repo,
2929 legacyrelativevalue=relative,
2931 legacyrelativevalue=relative,
2930 )
2932 )
2931 r = scmutil.addremove(
2933 r = scmutil.addremove(
2932 repo,
2934 repo,
2933 matcher,
2935 matcher,
2934 b"",
2936 b"",
2935 uipathfn,
2937 uipathfn,
2936 opts,
2938 opts,
2937 open_tr=c.open_transaction,
2939 open_tr=c.open_transaction,
2938 )
2940 )
2939 m = _(b"failed to mark all new/missing files as added/removed")
2941 m = _(b"failed to mark all new/missing files as added/removed")
2940 if r != 0:
2942 if r != 0:
2941 raise error.Abort(m)
2943 raise error.Abort(m)
2942 c.close_dirstate_context()
2944 c.close_dirstate_context()
2943 return commitfunc(ui, repo, message, matcher, opts)
2945 return commitfunc(ui, repo, message, matcher, opts)
2944
2946
2945
2947
2946 def samefile(f, ctx1, ctx2):
2948 def samefile(f, ctx1, ctx2):
2947 if f in ctx1.manifest():
2949 if f in ctx1.manifest():
2948 a = ctx1.filectx(f)
2950 a = ctx1.filectx(f)
2949 if f in ctx2.manifest():
2951 if f in ctx2.manifest():
2950 b = ctx2.filectx(f)
2952 b = ctx2.filectx(f)
2951 return not a.cmp(b) and a.flags() == b.flags()
2953 return not a.cmp(b) and a.flags() == b.flags()
2952 else:
2954 else:
2953 return False
2955 return False
2954 else:
2956 else:
2955 return f not in ctx2.manifest()
2957 return f not in ctx2.manifest()
2956
2958
2957
2959
2958 def amend(ui, repo, old, extra, pats, opts: Dict[str, Any]):
2960 def amend(ui, repo, old, extra, pats, opts: Dict[str, Any]):
2959 # avoid cycle context -> subrepo -> cmdutil
2961 # avoid cycle context -> subrepo -> cmdutil
2960 from . import context
2962 from . import context
2961
2963
2962 # amend will reuse the existing user if not specified, but the obsolete
2964 # amend will reuse the existing user if not specified, but the obsolete
2963 # marker creation requires that the current user's name is specified.
2965 # marker creation requires that the current user's name is specified.
2964 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2966 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2965 ui.username() # raise exception if username not set
2967 ui.username() # raise exception if username not set
2966
2968
2967 ui.note(_(b'amending changeset %s\n') % old)
2969 ui.note(_(b'amending changeset %s\n') % old)
2968 base = old.p1()
2970 base = old.p1()
2969
2971
2970 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2972 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2971 # Participating changesets:
2973 # Participating changesets:
2972 #
2974 #
2973 # wctx o - workingctx that contains changes from working copy
2975 # wctx o - workingctx that contains changes from working copy
2974 # | to go into amending commit
2976 # | to go into amending commit
2975 # |
2977 # |
2976 # old o - changeset to amend
2978 # old o - changeset to amend
2977 # |
2979 # |
2978 # base o - first parent of the changeset to amend
2980 # base o - first parent of the changeset to amend
2979 wctx = repo[None]
2981 wctx = repo[None]
2980
2982
2981 # Copy to avoid mutating input
2983 # Copy to avoid mutating input
2982 extra = extra.copy()
2984 extra = extra.copy()
2983 # Update extra dict from amended commit (e.g. to preserve graft
2985 # Update extra dict from amended commit (e.g. to preserve graft
2984 # source)
2986 # source)
2985 extra.update(old.extra())
2987 extra.update(old.extra())
2986
2988
2987 # Also update it from the from the wctx
2989 # Also update it from the from the wctx
2988 extra.update(wctx.extra())
2990 extra.update(wctx.extra())
2989
2991
2990 # date-only change should be ignored?
2992 # date-only change should be ignored?
2991 datemaydiffer = resolve_commit_options(ui, opts)
2993 datemaydiffer = resolve_commit_options(ui, opts)
2992 opts = pycompat.byteskwargs(opts)
2994 opts = pycompat.byteskwargs(opts)
2993
2995
2994 date = old.date()
2996 date = old.date()
2995 if opts.get(b'date'):
2997 if opts.get(b'date'):
2996 date = dateutil.parsedate(opts.get(b'date'))
2998 date = dateutil.parsedate(opts.get(b'date'))
2997 user = opts.get(b'user') or old.user()
2999 user = opts.get(b'user') or old.user()
2998
3000
2999 if len(old.parents()) > 1:
3001 if len(old.parents()) > 1:
3000 # ctx.files() isn't reliable for merges, so fall back to the
3002 # ctx.files() isn't reliable for merges, so fall back to the
3001 # slower repo.status() method
3003 # slower repo.status() method
3002 st = base.status(old)
3004 st = base.status(old)
3003 files = set(st.modified) | set(st.added) | set(st.removed)
3005 files = set(st.modified) | set(st.added) | set(st.removed)
3004 else:
3006 else:
3005 files = set(old.files())
3007 files = set(old.files())
3006
3008
3007 # add/remove the files to the working copy if the "addremove" option
3009 # add/remove the files to the working copy if the "addremove" option
3008 # was specified.
3010 # was specified.
3009 matcher = scmutil.match(wctx, pats, opts)
3011 matcher = scmutil.match(wctx, pats, opts)
3010 relative = scmutil.anypats(pats, opts)
3012 relative = scmutil.anypats(pats, opts)
3011 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
3013 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
3012 if opts.get(b'addremove'):
3014 if opts.get(b'addremove'):
3013 with repo.dirstate.changing_files(repo):
3015 with repo.dirstate.changing_files(repo):
3014 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
3016 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
3015 m = _(
3017 m = _(
3016 b"failed to mark all new/missing files as added/removed"
3018 b"failed to mark all new/missing files as added/removed"
3017 )
3019 )
3018 raise error.Abort(m)
3020 raise error.Abort(m)
3019
3021
3020 # Check subrepos. This depends on in-place wctx._status update in
3022 # Check subrepos. This depends on in-place wctx._status update in
3021 # subrepo.precommit(). To minimize the risk of this hack, we do
3023 # subrepo.precommit(). To minimize the risk of this hack, we do
3022 # nothing if .hgsub does not exist.
3024 # nothing if .hgsub does not exist.
3023 if b'.hgsub' in wctx or b'.hgsub' in old:
3025 if b'.hgsub' in wctx or b'.hgsub' in old:
3024 subs, commitsubs, newsubstate = subrepoutil.precommit(
3026 subs, commitsubs, newsubstate = subrepoutil.precommit(
3025 ui, wctx, wctx._status, matcher
3027 ui, wctx, wctx._status, matcher
3026 )
3028 )
3027 # amend should abort if commitsubrepos is enabled
3029 # amend should abort if commitsubrepos is enabled
3028 assert not commitsubs
3030 assert not commitsubs
3029 if subs:
3031 if subs:
3030 subrepoutil.writestate(repo, newsubstate)
3032 subrepoutil.writestate(repo, newsubstate)
3031
3033
3032 ms = mergestatemod.mergestate.read(repo)
3034 ms = mergestatemod.mergestate.read(repo)
3033 mergeutil.checkunresolved(ms)
3035 mergeutil.checkunresolved(ms)
3034
3036
3035 filestoamend = {f for f in wctx.files() if matcher(f)}
3037 filestoamend = {f for f in wctx.files() if matcher(f)}
3036
3038
3037 changes = len(filestoamend) > 0
3039 changes = len(filestoamend) > 0
3038 changeset_copies = (
3040 changeset_copies = (
3039 repo.ui.config(b'experimental', b'copies.read-from')
3041 repo.ui.config(b'experimental', b'copies.read-from')
3040 != b'filelog-only'
3042 != b'filelog-only'
3041 )
3043 )
3042 # If there are changes to amend or if copy information needs to be read
3044 # If there are changes to amend or if copy information needs to be read
3043 # from the changeset extras, we cannot take the fast path of using
3045 # from the changeset extras, we cannot take the fast path of using
3044 # filectxs from the old commit.
3046 # filectxs from the old commit.
3045 if changes or changeset_copies:
3047 if changes or changeset_copies:
3046 # Recompute copies (avoid recording a -> b -> a)
3048 # Recompute copies (avoid recording a -> b -> a)
3047 copied = copies.pathcopies(base, wctx)
3049 copied = copies.pathcopies(base, wctx)
3048 if old.p2():
3050 if old.p2():
3049 copied.update(copies.pathcopies(old.p2(), wctx))
3051 copied.update(copies.pathcopies(old.p2(), wctx))
3050
3052
3051 # Prune files which were reverted by the updates: if old
3053 # Prune files which were reverted by the updates: if old
3052 # introduced file X and the file was renamed in the working
3054 # introduced file X and the file was renamed in the working
3053 # copy, then those two files are the same and
3055 # copy, then those two files are the same and
3054 # we can discard X from our list of files. Likewise if X
3056 # we can discard X from our list of files. Likewise if X
3055 # was removed, it's no longer relevant. If X is missing (aka
3057 # was removed, it's no longer relevant. If X is missing (aka
3056 # deleted), old X must be preserved.
3058 # deleted), old X must be preserved.
3057 files.update(filestoamend)
3059 files.update(filestoamend)
3058 files = [
3060 files = [
3059 f
3061 f
3060 for f in files
3062 for f in files
3061 if (f not in filestoamend or not samefile(f, wctx, base))
3063 if (f not in filestoamend or not samefile(f, wctx, base))
3062 ]
3064 ]
3063
3065
3064 def filectxfn(repo, ctx_, path):
3066 def filectxfn(repo, ctx_, path):
3065 try:
3067 try:
3066 # If the file being considered is not amongst the files
3068 # If the file being considered is not amongst the files
3067 # to be amended, we should use the file context from the
3069 # to be amended, we should use the file context from the
3068 # old changeset. This avoids issues when only some files in
3070 # old changeset. This avoids issues when only some files in
3069 # the working copy are being amended but there are also
3071 # the working copy are being amended but there are also
3070 # changes to other files from the old changeset.
3072 # changes to other files from the old changeset.
3071 if path in filestoamend:
3073 if path in filestoamend:
3072 # Return None for removed files.
3074 # Return None for removed files.
3073 if path in wctx.removed():
3075 if path in wctx.removed():
3074 return None
3076 return None
3075 fctx = wctx[path]
3077 fctx = wctx[path]
3076 else:
3078 else:
3077 fctx = old.filectx(path)
3079 fctx = old.filectx(path)
3078 flags = fctx.flags()
3080 flags = fctx.flags()
3079 mctx = context.memfilectx(
3081 mctx = context.memfilectx(
3080 repo,
3082 repo,
3081 ctx_,
3083 ctx_,
3082 fctx.path(),
3084 fctx.path(),
3083 fctx.data(),
3085 fctx.data(),
3084 islink=b'l' in flags,
3086 islink=b'l' in flags,
3085 isexec=b'x' in flags,
3087 isexec=b'x' in flags,
3086 copysource=copied.get(path),
3088 copysource=copied.get(path),
3087 )
3089 )
3088 return mctx
3090 return mctx
3089 except KeyError:
3091 except KeyError:
3090 return None
3092 return None
3091
3093
3092 else:
3094 else:
3093 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
3095 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
3094
3096
3095 # Use version of files as in the old cset
3097 # Use version of files as in the old cset
3096 def filectxfn(repo, ctx_, path):
3098 def filectxfn(repo, ctx_, path):
3097 try:
3099 try:
3098 return old.filectx(path)
3100 return old.filectx(path)
3099 except KeyError:
3101 except KeyError:
3100 return None
3102 return None
3101
3103
3102 # See if we got a message from -m or -l, if not, open the editor with
3104 # See if we got a message from -m or -l, if not, open the editor with
3103 # the message of the changeset to amend.
3105 # the message of the changeset to amend.
3104 message = logmessage(ui, opts)
3106 message = logmessage(ui, opts)
3105
3107
3106 editform = mergeeditform(old, b'commit.amend')
3108 editform = mergeeditform(old, b'commit.amend')
3107
3109
3108 if not message:
3110 if not message:
3109 message = old.description()
3111 message = old.description()
3110 # Default if message isn't provided and --edit is not passed is to
3112 # Default if message isn't provided and --edit is not passed is to
3111 # invoke editor, but allow --no-edit. If somehow we don't have any
3113 # invoke editor, but allow --no-edit. If somehow we don't have any
3112 # description, let's always start the editor.
3114 # description, let's always start the editor.
3113 doedit = not message or opts.get(b'edit') in [True, None]
3115 doedit = not message or opts.get(b'edit') in [True, None]
3114 else:
3116 else:
3115 # Default if message is provided is to not invoke editor, but allow
3117 # Default if message is provided is to not invoke editor, but allow
3116 # --edit.
3118 # --edit.
3117 doedit = opts.get(b'edit') is True
3119 doedit = opts.get(b'edit') is True
3118 editor = getcommiteditor(edit=doedit, editform=editform)
3120 editor = getcommiteditor(edit=doedit, editform=editform)
3119
3121
3120 pureextra = extra.copy()
3122 pureextra = extra.copy()
3121 extra[b'amend_source'] = old.hex()
3123 extra[b'amend_source'] = old.hex()
3122
3124
3123 new = context.memctx(
3125 new = context.memctx(
3124 repo,
3126 repo,
3125 parents=[base.node(), old.p2().node()],
3127 parents=[base.node(), old.p2().node()],
3126 text=message,
3128 text=message,
3127 files=files,
3129 files=files,
3128 filectxfn=filectxfn,
3130 filectxfn=filectxfn,
3129 user=user,
3131 user=user,
3130 date=date,
3132 date=date,
3131 extra=extra,
3133 extra=extra,
3132 editor=editor,
3134 editor=editor,
3133 )
3135 )
3134
3136
3135 newdesc = changelog.stripdesc(new.description())
3137 newdesc = changelog.stripdesc(new.description())
3136 if (
3138 if (
3137 (not changes)
3139 (not changes)
3138 and newdesc == old.description()
3140 and newdesc == old.description()
3139 and user == old.user()
3141 and user == old.user()
3140 and (date == old.date() or datemaydiffer)
3142 and (date == old.date() or datemaydiffer)
3141 and pureextra == old.extra()
3143 and pureextra == old.extra()
3142 ):
3144 ):
3143 # nothing changed. continuing here would create a new node
3145 # nothing changed. continuing here would create a new node
3144 # anyway because of the amend_source noise.
3146 # anyway because of the amend_source noise.
3145 #
3147 #
3146 # This not what we expect from amend.
3148 # This not what we expect from amend.
3147 return old.node()
3149 return old.node()
3148
3150
3149 commitphase = None
3151 commitphase = None
3150 if opts.get(b'secret'):
3152 if opts.get(b'secret'):
3151 commitphase = phases.secret
3153 commitphase = phases.secret
3152 elif opts.get(b'draft'):
3154 elif opts.get(b'draft'):
3153 commitphase = phases.draft
3155 commitphase = phases.draft
3154 newid = repo.commitctx(new)
3156 newid = repo.commitctx(new)
3155 ms.reset()
3157 ms.reset()
3156
3158
3157 with repo.dirstate.changing_parents(repo):
3159 with repo.dirstate.changing_parents(repo):
3158 # Reroute the working copy parent to the new changeset
3160 # Reroute the working copy parent to the new changeset
3159 repo.setparents(newid, repo.nullid)
3161 repo.setparents(newid, repo.nullid)
3160
3162
3161 # Fixing the dirstate because localrepo.commitctx does not update
3163 # Fixing the dirstate because localrepo.commitctx does not update
3162 # it. This is rather convenient because we did not need to update
3164 # it. This is rather convenient because we did not need to update
3163 # the dirstate for all the files in the new commit which commitctx
3165 # the dirstate for all the files in the new commit which commitctx
3164 # could have done if it updated the dirstate. Now, we can
3166 # could have done if it updated the dirstate. Now, we can
3165 # selectively update the dirstate only for the amended files.
3167 # selectively update the dirstate only for the amended files.
3166 dirstate = repo.dirstate
3168 dirstate = repo.dirstate
3167
3169
3168 # Update the state of the files which were added and modified in the
3170 # Update the state of the files which were added and modified in the
3169 # amend to "normal" in the dirstate. We need to use "normallookup" since
3171 # amend to "normal" in the dirstate. We need to use "normallookup" since
3170 # the files may have changed since the command started; using "normal"
3172 # the files may have changed since the command started; using "normal"
3171 # would mark them as clean but with uncommitted contents.
3173 # would mark them as clean but with uncommitted contents.
3172 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3174 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3173 for f in normalfiles:
3175 for f in normalfiles:
3174 dirstate.update_file(
3176 dirstate.update_file(
3175 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
3177 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
3176 )
3178 )
3177
3179
3178 # Update the state of files which were removed in the amend
3180 # Update the state of files which were removed in the amend
3179 # to "removed" in the dirstate.
3181 # to "removed" in the dirstate.
3180 removedfiles = set(wctx.removed()) & filestoamend
3182 removedfiles = set(wctx.removed()) & filestoamend
3181 for f in removedfiles:
3183 for f in removedfiles:
3182 dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
3184 dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
3183
3185
3184 mapping = {old.node(): (newid,)}
3186 mapping = {old.node(): (newid,)}
3185 obsmetadata = None
3187 obsmetadata = None
3186 if opts.get(b'note'):
3188 if opts.get(b'note'):
3187 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3189 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3188 backup = ui.configbool(b'rewrite', b'backup-bundle')
3190 backup = ui.configbool(b'rewrite', b'backup-bundle')
3189 scmutil.cleanupnodes(
3191 scmutil.cleanupnodes(
3190 repo,
3192 repo,
3191 mapping,
3193 mapping,
3192 b'amend',
3194 b'amend',
3193 metadata=obsmetadata,
3195 metadata=obsmetadata,
3194 fixphase=True,
3196 fixphase=True,
3195 targetphase=commitphase,
3197 targetphase=commitphase,
3196 backup=backup,
3198 backup=backup,
3197 )
3199 )
3198
3200
3199 return newid
3201 return newid
3200
3202
3201
3203
3202 def commiteditor(repo, ctx, subs, editform=b''):
3204 def commiteditor(repo, ctx, subs, editform=b''):
3203 if ctx.description():
3205 if ctx.description():
3204 return ctx.description()
3206 return ctx.description()
3205 return commitforceeditor(
3207 return commitforceeditor(
3206 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3208 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3207 )
3209 )
3208
3210
3209
3211
3210 def commitforceeditor(
3212 def commitforceeditor(
3211 repo,
3213 repo,
3212 ctx,
3214 ctx,
3213 subs,
3215 subs,
3214 finishdesc=None,
3216 finishdesc=None,
3215 extramsg=None,
3217 extramsg=None,
3216 editform=b'',
3218 editform=b'',
3217 unchangedmessagedetection=False,
3219 unchangedmessagedetection=False,
3218 ):
3220 ):
3219 if not extramsg:
3221 if not extramsg:
3220 extramsg = _(b"Leave message empty to abort commit.")
3222 extramsg = _(b"Leave message empty to abort commit.")
3221
3223
3222 forms = [e for e in editform.split(b'.') if e]
3224 forms = [e for e in editform.split(b'.') if e]
3223 forms.insert(0, b'changeset')
3225 forms.insert(0, b'changeset')
3224 templatetext = None
3226 templatetext = None
3225 while forms:
3227 while forms:
3226 ref = b'.'.join(forms)
3228 ref = b'.'.join(forms)
3227 if repo.ui.config(b'committemplate', ref):
3229 if repo.ui.config(b'committemplate', ref):
3228 templatetext = committext = buildcommittemplate(
3230 templatetext = committext = buildcommittemplate(
3229 repo, ctx, subs, extramsg, ref
3231 repo, ctx, subs, extramsg, ref
3230 )
3232 )
3231 break
3233 break
3232 forms.pop()
3234 forms.pop()
3233 else:
3235 else:
3234 committext = buildcommittext(repo, ctx, subs, extramsg)
3236 committext = buildcommittext(repo, ctx, subs, extramsg)
3235
3237
3236 # run editor in the repository root
3238 # run editor in the repository root
3237 olddir = encoding.getcwd()
3239 olddir = encoding.getcwd()
3238 os.chdir(repo.root)
3240 os.chdir(repo.root)
3239
3241
3240 # make in-memory changes visible to external process
3242 # make in-memory changes visible to external process
3241 tr = repo.currenttransaction()
3243 tr = repo.currenttransaction()
3242 repo.dirstate.write(tr)
3244 repo.dirstate.write(tr)
3243 pending = tr and tr.writepending() and repo.root
3245 pending = tr and tr.writepending() and repo.root
3244
3246
3245 editortext = repo.ui.edit(
3247 editortext = repo.ui.edit(
3246 committext,
3248 committext,
3247 ctx.user(),
3249 ctx.user(),
3248 ctx.extra(),
3250 ctx.extra(),
3249 editform=editform,
3251 editform=editform,
3250 pending=pending,
3252 pending=pending,
3251 repopath=repo.path,
3253 repopath=repo.path,
3252 action=b'commit',
3254 action=b'commit',
3253 )
3255 )
3254 text = editortext
3256 text = editortext
3255
3257
3256 # strip away anything below this special string (used for editors that want
3258 # strip away anything below this special string (used for editors that want
3257 # to display the diff)
3259 # to display the diff)
3258 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3260 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3259 if stripbelow:
3261 if stripbelow:
3260 text = text[: stripbelow.start()]
3262 text = text[: stripbelow.start()]
3261
3263
3262 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3264 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3263 os.chdir(olddir)
3265 os.chdir(olddir)
3264
3266
3265 if finishdesc:
3267 if finishdesc:
3266 text = finishdesc(text)
3268 text = finishdesc(text)
3267 if not text.strip():
3269 if not text.strip():
3268 raise error.InputError(_(b"empty commit message"))
3270 raise error.InputError(_(b"empty commit message"))
3269 if unchangedmessagedetection and editortext == templatetext:
3271 if unchangedmessagedetection and editortext == templatetext:
3270 raise error.InputError(_(b"commit message unchanged"))
3272 raise error.InputError(_(b"commit message unchanged"))
3271
3273
3272 return text
3274 return text
3273
3275
3274
3276
3275 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3277 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3276 ui = repo.ui
3278 ui = repo.ui
3277 spec = formatter.reference_templatespec(ref)
3279 spec = formatter.reference_templatespec(ref)
3278 t = logcmdutil.changesettemplater(ui, repo, spec)
3280 t = logcmdutil.changesettemplater(ui, repo, spec)
3279 t.t.cache.update(
3281 t.t.cache.update(
3280 (k, templater.unquotestring(v))
3282 (k, templater.unquotestring(v))
3281 for k, v in repo.ui.configitems(b'committemplate')
3283 for k, v in repo.ui.configitems(b'committemplate')
3282 )
3284 )
3283
3285
3284 if not extramsg:
3286 if not extramsg:
3285 extramsg = b'' # ensure that extramsg is string
3287 extramsg = b'' # ensure that extramsg is string
3286
3288
3287 ui.pushbuffer()
3289 ui.pushbuffer()
3288 t.show(ctx, extramsg=extramsg)
3290 t.show(ctx, extramsg=extramsg)
3289 return ui.popbuffer()
3291 return ui.popbuffer()
3290
3292
3291
3293
3292 def hgprefix(msg):
3294 def hgprefix(msg):
3293 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3295 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3294
3296
3295
3297
3296 def buildcommittext(repo, ctx, subs, extramsg):
3298 def buildcommittext(repo, ctx, subs, extramsg):
3297 edittext = []
3299 edittext = []
3298 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3300 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3299 if ctx.description():
3301 if ctx.description():
3300 edittext.append(ctx.description())
3302 edittext.append(ctx.description())
3301 edittext.append(b"")
3303 edittext.append(b"")
3302 edittext.append(b"") # Empty line between message and comments.
3304 edittext.append(b"") # Empty line between message and comments.
3303 edittext.append(
3305 edittext.append(
3304 hgprefix(
3306 hgprefix(
3305 _(
3307 _(
3306 b"Enter commit message."
3308 b"Enter commit message."
3307 b" Lines beginning with 'HG:' are removed."
3309 b" Lines beginning with 'HG:' are removed."
3308 )
3310 )
3309 )
3311 )
3310 )
3312 )
3311 edittext.append(hgprefix(extramsg))
3313 edittext.append(hgprefix(extramsg))
3312 edittext.append(b"HG: --")
3314 edittext.append(b"HG: --")
3313 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3315 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3314 if ctx.p2():
3316 if ctx.p2():
3315 edittext.append(hgprefix(_(b"branch merge")))
3317 edittext.append(hgprefix(_(b"branch merge")))
3316 if ctx.branch():
3318 if ctx.branch():
3317 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3319 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3318 if bookmarks.isactivewdirparent(repo):
3320 if bookmarks.isactivewdirparent(repo):
3319 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3321 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3320 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3322 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3321 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3323 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3322 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3324 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3323 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3325 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3324 if not added and not modified and not removed:
3326 if not added and not modified and not removed:
3325 edittext.append(hgprefix(_(b"no files changed")))
3327 edittext.append(hgprefix(_(b"no files changed")))
3326 edittext.append(b"")
3328 edittext.append(b"")
3327
3329
3328 return b"\n".join(edittext)
3330 return b"\n".join(edittext)
3329
3331
3330
3332
3331 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3333 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3332 if opts is None:
3334 if opts is None:
3333 opts = {}
3335 opts = {}
3334 ctx = repo[node]
3336 ctx = repo[node]
3335 parents = ctx.parents()
3337 parents = ctx.parents()
3336
3338
3337 if tip is not None and repo.changelog.tip() == tip:
3339 if tip is not None and repo.changelog.tip() == tip:
3338 # avoid reporting something like "committed new head" when
3340 # avoid reporting something like "committed new head" when
3339 # recommitting old changesets, and issue a helpful warning
3341 # recommitting old changesets, and issue a helpful warning
3340 # for most instances
3342 # for most instances
3341 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3343 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3342 elif (
3344 elif (
3343 not opts.get(b'amend')
3345 not opts.get(b'amend')
3344 and bheads
3346 and bheads
3345 and node not in bheads
3347 and node not in bheads
3346 and not any(
3348 and not any(
3347 p.node() in bheads and p.branch() == branch for p in parents
3349 p.node() in bheads and p.branch() == branch for p in parents
3348 )
3350 )
3349 ):
3351 ):
3350 repo.ui.status(_(b'created new head\n'))
3352 repo.ui.status(_(b'created new head\n'))
3351 # The message is not printed for initial roots. For the other
3353 # The message is not printed for initial roots. For the other
3352 # changesets, it is printed in the following situations:
3354 # changesets, it is printed in the following situations:
3353 #
3355 #
3354 # Par column: for the 2 parents with ...
3356 # Par column: for the 2 parents with ...
3355 # N: null or no parent
3357 # N: null or no parent
3356 # B: parent is on another named branch
3358 # B: parent is on another named branch
3357 # C: parent is a regular non head changeset
3359 # C: parent is a regular non head changeset
3358 # H: parent was a branch head of the current branch
3360 # H: parent was a branch head of the current branch
3359 # Msg column: whether we print "created new head" message
3361 # Msg column: whether we print "created new head" message
3360 # In the following, it is assumed that there already exists some
3362 # In the following, it is assumed that there already exists some
3361 # initial branch heads of the current branch, otherwise nothing is
3363 # initial branch heads of the current branch, otherwise nothing is
3362 # printed anyway.
3364 # printed anyway.
3363 #
3365 #
3364 # Par Msg Comment
3366 # Par Msg Comment
3365 # N N y additional topo root
3367 # N N y additional topo root
3366 #
3368 #
3367 # B N y additional branch root
3369 # B N y additional branch root
3368 # C N y additional topo head
3370 # C N y additional topo head
3369 # H N n usual case
3371 # H N n usual case
3370 #
3372 #
3371 # B B y weird additional branch root
3373 # B B y weird additional branch root
3372 # C B y branch merge
3374 # C B y branch merge
3373 # H B n merge with named branch
3375 # H B n merge with named branch
3374 #
3376 #
3375 # C C y additional head from merge
3377 # C C y additional head from merge
3376 # C H n merge with a head
3378 # C H n merge with a head
3377 #
3379 #
3378 # H H n head merge: head count decreases
3380 # H H n head merge: head count decreases
3379
3381
3380 if not opts.get(b'close_branch'):
3382 if not opts.get(b'close_branch'):
3381 for r in parents:
3383 for r in parents:
3382 if r.closesbranch() and r.branch() == branch:
3384 if r.closesbranch() and r.branch() == branch:
3383 repo.ui.status(
3385 repo.ui.status(
3384 _(b'reopening closed branch head %d\n') % r.rev()
3386 _(b'reopening closed branch head %d\n') % r.rev()
3385 )
3387 )
3386
3388
3387 if repo.ui.debugflag:
3389 if repo.ui.debugflag:
3388 repo.ui.write(
3390 repo.ui.write(
3389 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3391 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3390 )
3392 )
3391 elif repo.ui.verbose:
3393 elif repo.ui.verbose:
3392 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3394 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3393
3395
3394
3396
3395 def postcommitstatus(repo, pats, opts):
3397 def postcommitstatus(repo, pats, opts):
3396 return repo.status(match=scmutil.match(repo[None], pats, opts))
3398 return repo.status(match=scmutil.match(repo[None], pats, opts))
3397
3399
3398
3400
3399 def revert(ui, repo, ctx, *pats, **opts):
3401 def revert(ui, repo, ctx, *pats, **opts):
3400 opts = pycompat.byteskwargs(opts)
3402 opts = pycompat.byteskwargs(opts)
3401 parent, p2 = repo.dirstate.parents()
3403 parent, p2 = repo.dirstate.parents()
3402 node = ctx.node()
3404 node = ctx.node()
3403
3405
3404 mf = ctx.manifest()
3406 mf = ctx.manifest()
3405 if node == p2:
3407 if node == p2:
3406 parent = p2
3408 parent = p2
3407
3409
3408 # need all matching names in dirstate and manifest of target rev,
3410 # need all matching names in dirstate and manifest of target rev,
3409 # so have to walk both. do not print errors if files exist in one
3411 # so have to walk both. do not print errors if files exist in one
3410 # but not other. in both cases, filesets should be evaluated against
3412 # but not other. in both cases, filesets should be evaluated against
3411 # workingctx to get consistent result (issue4497). this means 'set:**'
3413 # workingctx to get consistent result (issue4497). this means 'set:**'
3412 # cannot be used to select missing files from target rev.
3414 # cannot be used to select missing files from target rev.
3413
3415
3414 # `names` is a mapping for all elements in working copy and target revision
3416 # `names` is a mapping for all elements in working copy and target revision
3415 # The mapping is in the form:
3417 # The mapping is in the form:
3416 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3418 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3417 names = {}
3419 names = {}
3418 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3420 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3419
3421
3420 with repo.wlock(), repo.dirstate.changing_files(repo):
3422 with repo.wlock(), repo.dirstate.changing_files(repo):
3421 ## filling of the `names` mapping
3423 ## filling of the `names` mapping
3422 # walk dirstate to fill `names`
3424 # walk dirstate to fill `names`
3423
3425
3424 interactive = opts.get(b'interactive', False)
3426 interactive = opts.get(b'interactive', False)
3425 wctx = repo[None]
3427 wctx = repo[None]
3426 m = scmutil.match(wctx, pats, opts)
3428 m = scmutil.match(wctx, pats, opts)
3427
3429
3428 # we'll need this later
3430 # we'll need this later
3429 targetsubs = sorted(s for s in wctx.substate if m(s))
3431 targetsubs = sorted(s for s in wctx.substate if m(s))
3430
3432
3431 if not m.always():
3433 if not m.always():
3432 matcher = matchmod.badmatch(m, lambda x, y: False)
3434 matcher = matchmod.badmatch(m, lambda x, y: False)
3433 for abs in wctx.walk(matcher):
3435 for abs in wctx.walk(matcher):
3434 names[abs] = m.exact(abs)
3436 names[abs] = m.exact(abs)
3435
3437
3436 # walk target manifest to fill `names`
3438 # walk target manifest to fill `names`
3437
3439
3438 def badfn(path, msg):
3440 def badfn(path, msg):
3439 if path in names:
3441 if path in names:
3440 return
3442 return
3441 if path in ctx.substate:
3443 if path in ctx.substate:
3442 return
3444 return
3443 path_ = path + b'/'
3445 path_ = path + b'/'
3444 for f in names:
3446 for f in names:
3445 if f.startswith(path_):
3447 if f.startswith(path_):
3446 return
3448 return
3447 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3449 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3448
3450
3449 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3451 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3450 if abs not in names:
3452 if abs not in names:
3451 names[abs] = m.exact(abs)
3453 names[abs] = m.exact(abs)
3452
3454
3453 # Find status of all file in `names`.
3455 # Find status of all file in `names`.
3454 m = scmutil.matchfiles(repo, names)
3456 m = scmutil.matchfiles(repo, names)
3455
3457
3456 changes = repo.status(
3458 changes = repo.status(
3457 node1=node, match=m, unknown=True, ignored=True, clean=True
3459 node1=node, match=m, unknown=True, ignored=True, clean=True
3458 )
3460 )
3459 else:
3461 else:
3460 changes = repo.status(node1=node, match=m)
3462 changes = repo.status(node1=node, match=m)
3461 for kind in changes:
3463 for kind in changes:
3462 for abs in kind:
3464 for abs in kind:
3463 names[abs] = m.exact(abs)
3465 names[abs] = m.exact(abs)
3464
3466
3465 m = scmutil.matchfiles(repo, names)
3467 m = scmutil.matchfiles(repo, names)
3466
3468
3467 modified = set(changes.modified)
3469 modified = set(changes.modified)
3468 added = set(changes.added)
3470 added = set(changes.added)
3469 removed = set(changes.removed)
3471 removed = set(changes.removed)
3470 _deleted = set(changes.deleted)
3472 _deleted = set(changes.deleted)
3471 unknown = set(changes.unknown)
3473 unknown = set(changes.unknown)
3472 unknown.update(changes.ignored)
3474 unknown.update(changes.ignored)
3473 clean = set(changes.clean)
3475 clean = set(changes.clean)
3474 modadded = set()
3476 modadded = set()
3475
3477
3476 # We need to account for the state of the file in the dirstate,
3478 # We need to account for the state of the file in the dirstate,
3477 # even when we revert against something else than parent. This will
3479 # even when we revert against something else than parent. This will
3478 # slightly alter the behavior of revert (doing back up or not, delete
3480 # slightly alter the behavior of revert (doing back up or not, delete
3479 # or just forget etc).
3481 # or just forget etc).
3480 if parent == node:
3482 if parent == node:
3481 dsmodified = modified
3483 dsmodified = modified
3482 dsadded = added
3484 dsadded = added
3483 dsremoved = removed
3485 dsremoved = removed
3484 # store all local modifications, useful later for rename detection
3486 # store all local modifications, useful later for rename detection
3485 localchanges = dsmodified | dsadded
3487 localchanges = dsmodified | dsadded
3486 modified, added, removed = set(), set(), set()
3488 modified, added, removed = set(), set(), set()
3487 else:
3489 else:
3488 changes = repo.status(node1=parent, match=m)
3490 changes = repo.status(node1=parent, match=m)
3489 dsmodified = set(changes.modified)
3491 dsmodified = set(changes.modified)
3490 dsadded = set(changes.added)
3492 dsadded = set(changes.added)
3491 dsremoved = set(changes.removed)
3493 dsremoved = set(changes.removed)
3492 # store all local modifications, useful later for rename detection
3494 # store all local modifications, useful later for rename detection
3493 localchanges = dsmodified | dsadded
3495 localchanges = dsmodified | dsadded
3494
3496
3495 # only take into account for removes between wc and target
3497 # only take into account for removes between wc and target
3496 clean |= dsremoved - removed
3498 clean |= dsremoved - removed
3497 dsremoved &= removed
3499 dsremoved &= removed
3498 # distinct between dirstate remove and other
3500 # distinct between dirstate remove and other
3499 removed -= dsremoved
3501 removed -= dsremoved
3500
3502
3501 modadded = added & dsmodified
3503 modadded = added & dsmodified
3502 added -= modadded
3504 added -= modadded
3503
3505
3504 # tell newly modified apart.
3506 # tell newly modified apart.
3505 dsmodified &= modified
3507 dsmodified &= modified
3506 dsmodified |= modified & dsadded # dirstate added may need backup
3508 dsmodified |= modified & dsadded # dirstate added may need backup
3507 modified -= dsmodified
3509 modified -= dsmodified
3508
3510
3509 # We need to wait for some post-processing to update this set
3511 # We need to wait for some post-processing to update this set
3510 # before making the distinction. The dirstate will be used for
3512 # before making the distinction. The dirstate will be used for
3511 # that purpose.
3513 # that purpose.
3512 dsadded = added
3514 dsadded = added
3513
3515
3514 # in case of merge, files that are actually added can be reported as
3516 # in case of merge, files that are actually added can be reported as
3515 # modified, we need to post process the result
3517 # modified, we need to post process the result
3516 if p2 != repo.nullid:
3518 if p2 != repo.nullid:
3517 mergeadd = set(dsmodified)
3519 mergeadd = set(dsmodified)
3518 for path in dsmodified:
3520 for path in dsmodified:
3519 if path in mf:
3521 if path in mf:
3520 mergeadd.remove(path)
3522 mergeadd.remove(path)
3521 dsadded |= mergeadd
3523 dsadded |= mergeadd
3522 dsmodified -= mergeadd
3524 dsmodified -= mergeadd
3523
3525
3524 # if f is a rename, update `names` to also revert the source
3526 # if f is a rename, update `names` to also revert the source
3525 for f in localchanges:
3527 for f in localchanges:
3526 src = repo.dirstate.copied(f)
3528 src = repo.dirstate.copied(f)
3527 # XXX should we check for rename down to target node?
3529 # XXX should we check for rename down to target node?
3528 if (
3530 if (
3529 src
3531 src
3530 and src not in names
3532 and src not in names
3531 and repo.dirstate.get_entry(src).removed
3533 and repo.dirstate.get_entry(src).removed
3532 ):
3534 ):
3533 dsremoved.add(src)
3535 dsremoved.add(src)
3534 names[src] = True
3536 names[src] = True
3535
3537
3536 # determine the exact nature of the deleted changesets
3538 # determine the exact nature of the deleted changesets
3537 deladded = set(_deleted)
3539 deladded = set(_deleted)
3538 for path in _deleted:
3540 for path in _deleted:
3539 if path in mf:
3541 if path in mf:
3540 deladded.remove(path)
3542 deladded.remove(path)
3541 deleted = _deleted - deladded
3543 deleted = _deleted - deladded
3542
3544
3543 # distinguish between file to forget and the other
3545 # distinguish between file to forget and the other
3544 added = set()
3546 added = set()
3545 for abs in dsadded:
3547 for abs in dsadded:
3546 if not repo.dirstate.get_entry(abs).added:
3548 if not repo.dirstate.get_entry(abs).added:
3547 added.add(abs)
3549 added.add(abs)
3548 dsadded -= added
3550 dsadded -= added
3549
3551
3550 for abs in deladded:
3552 for abs in deladded:
3551 if repo.dirstate.get_entry(abs).added:
3553 if repo.dirstate.get_entry(abs).added:
3552 dsadded.add(abs)
3554 dsadded.add(abs)
3553 deladded -= dsadded
3555 deladded -= dsadded
3554
3556
3555 # For files marked as removed, we check if an unknown file is present at
3557 # For files marked as removed, we check if an unknown file is present at
3556 # the same path. If a such file exists it may need to be backed up.
3558 # the same path. If a such file exists it may need to be backed up.
3557 # Making the distinction at this stage helps have simpler backup
3559 # Making the distinction at this stage helps have simpler backup
3558 # logic.
3560 # logic.
3559 removunk = set()
3561 removunk = set()
3560 for abs in removed:
3562 for abs in removed:
3561 target = repo.wjoin(abs)
3563 target = repo.wjoin(abs)
3562 if os.path.lexists(target):
3564 if os.path.lexists(target):
3563 removunk.add(abs)
3565 removunk.add(abs)
3564 removed -= removunk
3566 removed -= removunk
3565
3567
3566 dsremovunk = set()
3568 dsremovunk = set()
3567 for abs in dsremoved:
3569 for abs in dsremoved:
3568 target = repo.wjoin(abs)
3570 target = repo.wjoin(abs)
3569 if os.path.lexists(target):
3571 if os.path.lexists(target):
3570 dsremovunk.add(abs)
3572 dsremovunk.add(abs)
3571 dsremoved -= dsremovunk
3573 dsremoved -= dsremovunk
3572
3574
3573 # action to be actually performed by revert
3575 # action to be actually performed by revert
3574 # (<list of file>, message>) tuple
3576 # (<list of file>, message>) tuple
3575 actions = {
3577 actions = {
3576 b'revert': ([], _(b'reverting %s\n')),
3578 b'revert': ([], _(b'reverting %s\n')),
3577 b'add': ([], _(b'adding %s\n')),
3579 b'add': ([], _(b'adding %s\n')),
3578 b'remove': ([], _(b'removing %s\n')),
3580 b'remove': ([], _(b'removing %s\n')),
3579 b'drop': ([], _(b'removing %s\n')),
3581 b'drop': ([], _(b'removing %s\n')),
3580 b'forget': ([], _(b'forgetting %s\n')),
3582 b'forget': ([], _(b'forgetting %s\n')),
3581 b'undelete': ([], _(b'undeleting %s\n')),
3583 b'undelete': ([], _(b'undeleting %s\n')),
3582 b'noop': (None, _(b'no changes needed to %s\n')),
3584 b'noop': (None, _(b'no changes needed to %s\n')),
3583 b'unknown': (None, _(b'file not managed: %s\n')),
3585 b'unknown': (None, _(b'file not managed: %s\n')),
3584 }
3586 }
3585
3587
3586 # "constant" that convey the backup strategy.
3588 # "constant" that convey the backup strategy.
3587 # All set to `discard` if `no-backup` is set do avoid checking
3589 # All set to `discard` if `no-backup` is set do avoid checking
3588 # no_backup lower in the code.
3590 # no_backup lower in the code.
3589 # These values are ordered for comparison purposes
3591 # These values are ordered for comparison purposes
3590 backupinteractive = 3 # do backup if interactively modified
3592 backupinteractive = 3 # do backup if interactively modified
3591 backup = 2 # unconditionally do backup
3593 backup = 2 # unconditionally do backup
3592 check = 1 # check if the existing file differs from target
3594 check = 1 # check if the existing file differs from target
3593 discard = 0 # never do backup
3595 discard = 0 # never do backup
3594 if opts.get(b'no_backup'):
3596 if opts.get(b'no_backup'):
3595 backupinteractive = backup = check = discard
3597 backupinteractive = backup = check = discard
3596 if interactive:
3598 if interactive:
3597 dsmodifiedbackup = backupinteractive
3599 dsmodifiedbackup = backupinteractive
3598 else:
3600 else:
3599 dsmodifiedbackup = backup
3601 dsmodifiedbackup = backup
3600 tobackup = set()
3602 tobackup = set()
3601
3603
3602 backupanddel = actions[b'remove']
3604 backupanddel = actions[b'remove']
3603 if not opts.get(b'no_backup'):
3605 if not opts.get(b'no_backup'):
3604 backupanddel = actions[b'drop']
3606 backupanddel = actions[b'drop']
3605
3607
3606 disptable = (
3608 disptable = (
3607 # dispatch table:
3609 # dispatch table:
3608 # file state
3610 # file state
3609 # action
3611 # action
3610 # make backup
3612 # make backup
3611 ## Sets that results that will change file on disk
3613 ## Sets that results that will change file on disk
3612 # Modified compared to target, no local change
3614 # Modified compared to target, no local change
3613 (modified, actions[b'revert'], discard),
3615 (modified, actions[b'revert'], discard),
3614 # Modified compared to target, but local file is deleted
3616 # Modified compared to target, but local file is deleted
3615 (deleted, actions[b'revert'], discard),
3617 (deleted, actions[b'revert'], discard),
3616 # Modified compared to target, local change
3618 # Modified compared to target, local change
3617 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3619 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3618 # Added since target
3620 # Added since target
3619 (added, actions[b'remove'], discard),
3621 (added, actions[b'remove'], discard),
3620 # Added in working directory
3622 # Added in working directory
3621 (dsadded, actions[b'forget'], discard),
3623 (dsadded, actions[b'forget'], discard),
3622 # Added since target, have local modification
3624 # Added since target, have local modification
3623 (modadded, backupanddel, backup),
3625 (modadded, backupanddel, backup),
3624 # Added since target but file is missing in working directory
3626 # Added since target but file is missing in working directory
3625 (deladded, actions[b'drop'], discard),
3627 (deladded, actions[b'drop'], discard),
3626 # Removed since target, before working copy parent
3628 # Removed since target, before working copy parent
3627 (removed, actions[b'add'], discard),
3629 (removed, actions[b'add'], discard),
3628 # Same as `removed` but an unknown file exists at the same path
3630 # Same as `removed` but an unknown file exists at the same path
3629 (removunk, actions[b'add'], check),
3631 (removunk, actions[b'add'], check),
3630 # Removed since targe, marked as such in working copy parent
3632 # Removed since targe, marked as such in working copy parent
3631 (dsremoved, actions[b'undelete'], discard),
3633 (dsremoved, actions[b'undelete'], discard),
3632 # Same as `dsremoved` but an unknown file exists at the same path
3634 # Same as `dsremoved` but an unknown file exists at the same path
3633 (dsremovunk, actions[b'undelete'], check),
3635 (dsremovunk, actions[b'undelete'], check),
3634 ## the following sets does not result in any file changes
3636 ## the following sets does not result in any file changes
3635 # File with no modification
3637 # File with no modification
3636 (clean, actions[b'noop'], discard),
3638 (clean, actions[b'noop'], discard),
3637 # Existing file, not tracked anywhere
3639 # Existing file, not tracked anywhere
3638 (unknown, actions[b'unknown'], discard),
3640 (unknown, actions[b'unknown'], discard),
3639 )
3641 )
3640
3642
3641 for abs, exact in sorted(names.items()):
3643 for abs, exact in sorted(names.items()):
3642 # target file to be touch on disk (relative to cwd)
3644 # target file to be touch on disk (relative to cwd)
3643 target = repo.wjoin(abs)
3645 target = repo.wjoin(abs)
3644 # search the entry in the dispatch table.
3646 # search the entry in the dispatch table.
3645 # if the file is in any of these sets, it was touched in the working
3647 # if the file is in any of these sets, it was touched in the working
3646 # directory parent and we are sure it needs to be reverted.
3648 # directory parent and we are sure it needs to be reverted.
3647 for table, (xlist, msg), dobackup in disptable:
3649 for table, (xlist, msg), dobackup in disptable:
3648 if abs not in table:
3650 if abs not in table:
3649 continue
3651 continue
3650 if xlist is not None:
3652 if xlist is not None:
3651 xlist.append(abs)
3653 xlist.append(abs)
3652 if dobackup:
3654 if dobackup:
3653 # If in interactive mode, don't automatically create
3655 # If in interactive mode, don't automatically create
3654 # .orig files (issue4793)
3656 # .orig files (issue4793)
3655 if dobackup == backupinteractive:
3657 if dobackup == backupinteractive:
3656 tobackup.add(abs)
3658 tobackup.add(abs)
3657 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3659 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3658 absbakname = scmutil.backuppath(ui, repo, abs)
3660 absbakname = scmutil.backuppath(ui, repo, abs)
3659 bakname = os.path.relpath(
3661 bakname = os.path.relpath(
3660 absbakname, start=repo.root
3662 absbakname, start=repo.root
3661 )
3663 )
3662 ui.note(
3664 ui.note(
3663 _(b'saving current version of %s as %s\n')
3665 _(b'saving current version of %s as %s\n')
3664 % (uipathfn(abs), uipathfn(bakname))
3666 % (uipathfn(abs), uipathfn(bakname))
3665 )
3667 )
3666 if not opts.get(b'dry_run'):
3668 if not opts.get(b'dry_run'):
3667 if interactive:
3669 if interactive:
3668 util.copyfile(target, absbakname)
3670 util.copyfile(target, absbakname)
3669 else:
3671 else:
3670 util.rename(target, absbakname)
3672 util.rename(target, absbakname)
3671 if opts.get(b'dry_run'):
3673 if opts.get(b'dry_run'):
3672 if ui.verbose or not exact:
3674 if ui.verbose or not exact:
3673 ui.status(msg % uipathfn(abs))
3675 ui.status(msg % uipathfn(abs))
3674 elif exact:
3676 elif exact:
3675 ui.warn(msg % uipathfn(abs))
3677 ui.warn(msg % uipathfn(abs))
3676 break
3678 break
3677
3679
3678 if not opts.get(b'dry_run'):
3680 if not opts.get(b'dry_run'):
3679 needdata = (b'revert', b'add', b'undelete')
3681 needdata = (b'revert', b'add', b'undelete')
3680 oplist = [actions[name][0] for name in needdata]
3682 oplist = [actions[name][0] for name in needdata]
3681 prefetch = scmutil.prefetchfiles
3683 prefetch = scmutil.prefetchfiles
3682 matchfiles = scmutil.matchfiles(
3684 matchfiles = scmutil.matchfiles(
3683 repo, [f for sublist in oplist for f in sublist]
3685 repo, [f for sublist in oplist for f in sublist]
3684 )
3686 )
3685 prefetch(
3687 prefetch(
3686 repo,
3688 repo,
3687 [(ctx.rev(), matchfiles)],
3689 [(ctx.rev(), matchfiles)],
3688 )
3690 )
3689 match = scmutil.match(repo[None], pats)
3691 match = scmutil.match(repo[None], pats)
3690 _performrevert(
3692 _performrevert(
3691 repo,
3693 repo,
3692 ctx,
3694 ctx,
3693 names,
3695 names,
3694 uipathfn,
3696 uipathfn,
3695 actions,
3697 actions,
3696 match,
3698 match,
3697 interactive,
3699 interactive,
3698 tobackup,
3700 tobackup,
3699 )
3701 )
3700
3702
3701 if targetsubs:
3703 if targetsubs:
3702 # Revert the subrepos on the revert list
3704 # Revert the subrepos on the revert list
3703 for sub in targetsubs:
3705 for sub in targetsubs:
3704 try:
3706 try:
3705 wctx.sub(sub).revert(
3707 wctx.sub(sub).revert(
3706 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3708 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3707 )
3709 )
3708 except KeyError:
3710 except KeyError:
3709 raise error.Abort(
3711 raise error.Abort(
3710 b"subrepository '%s' does not exist in %s!"
3712 b"subrepository '%s' does not exist in %s!"
3711 % (sub, short(ctx.node()))
3713 % (sub, short(ctx.node()))
3712 )
3714 )
3713
3715
3714
3716
3715 def _performrevert(
3717 def _performrevert(
3716 repo,
3718 repo,
3717 ctx,
3719 ctx,
3718 names,
3720 names,
3719 uipathfn,
3721 uipathfn,
3720 actions,
3722 actions,
3721 match,
3723 match,
3722 interactive=False,
3724 interactive=False,
3723 tobackup=None,
3725 tobackup=None,
3724 ):
3726 ):
3725 """function that actually perform all the actions computed for revert
3727 """function that actually perform all the actions computed for revert
3726
3728
3727 This is an independent function to let extension to plug in and react to
3729 This is an independent function to let extension to plug in and react to
3728 the imminent revert.
3730 the imminent revert.
3729
3731
3730 Make sure you have the working directory locked when calling this function.
3732 Make sure you have the working directory locked when calling this function.
3731 """
3733 """
3732 parent, p2 = repo.dirstate.parents()
3734 parent, p2 = repo.dirstate.parents()
3733 node = ctx.node()
3735 node = ctx.node()
3734 excluded_files = []
3736 excluded_files = []
3735
3737
3736 def checkout(f):
3738 def checkout(f):
3737 fc = ctx[f]
3739 fc = ctx[f]
3738 repo.wwrite(f, fc.data(), fc.flags())
3740 repo.wwrite(f, fc.data(), fc.flags())
3739
3741
3740 def doremove(f):
3742 def doremove(f):
3741 try:
3743 try:
3742 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3744 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3743 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3745 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3744 except OSError:
3746 except OSError:
3745 pass
3747 pass
3746 repo.dirstate.set_untracked(f)
3748 repo.dirstate.set_untracked(f)
3747
3749
3748 def prntstatusmsg(action, f):
3750 def prntstatusmsg(action, f):
3749 exact = names[f]
3751 exact = names[f]
3750 if repo.ui.verbose or not exact:
3752 if repo.ui.verbose or not exact:
3751 repo.ui.status(actions[action][1] % uipathfn(f))
3753 repo.ui.status(actions[action][1] % uipathfn(f))
3752
3754
3753 audit_path = pathutil.pathauditor(repo.root, cached=True)
3755 audit_path = pathutil.pathauditor(repo.root, cached=True)
3754 for f in actions[b'forget'][0]:
3756 for f in actions[b'forget'][0]:
3755 if interactive:
3757 if interactive:
3756 choice = repo.ui.promptchoice(
3758 choice = repo.ui.promptchoice(
3757 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3759 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3758 )
3760 )
3759 if choice == 0:
3761 if choice == 0:
3760 prntstatusmsg(b'forget', f)
3762 prntstatusmsg(b'forget', f)
3761 repo.dirstate.set_untracked(f)
3763 repo.dirstate.set_untracked(f)
3762 else:
3764 else:
3763 excluded_files.append(f)
3765 excluded_files.append(f)
3764 else:
3766 else:
3765 prntstatusmsg(b'forget', f)
3767 prntstatusmsg(b'forget', f)
3766 repo.dirstate.set_untracked(f)
3768 repo.dirstate.set_untracked(f)
3767 for f in actions[b'remove'][0]:
3769 for f in actions[b'remove'][0]:
3768 audit_path(f)
3770 audit_path(f)
3769 if interactive:
3771 if interactive:
3770 choice = repo.ui.promptchoice(
3772 choice = repo.ui.promptchoice(
3771 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3773 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3772 )
3774 )
3773 if choice == 0:
3775 if choice == 0:
3774 prntstatusmsg(b'remove', f)
3776 prntstatusmsg(b'remove', f)
3775 doremove(f)
3777 doremove(f)
3776 else:
3778 else:
3777 excluded_files.append(f)
3779 excluded_files.append(f)
3778 else:
3780 else:
3779 prntstatusmsg(b'remove', f)
3781 prntstatusmsg(b'remove', f)
3780 doremove(f)
3782 doremove(f)
3781 for f in actions[b'drop'][0]:
3783 for f in actions[b'drop'][0]:
3782 audit_path(f)
3784 audit_path(f)
3783 prntstatusmsg(b'drop', f)
3785 prntstatusmsg(b'drop', f)
3784 repo.dirstate.set_untracked(f)
3786 repo.dirstate.set_untracked(f)
3785
3787
3786 # We are reverting to our parent. If possible, we had like `hg status`
3788 # We are reverting to our parent. If possible, we had like `hg status`
3787 # to report the file as clean. We have to be less agressive for
3789 # to report the file as clean. We have to be less agressive for
3788 # merges to avoid losing information about copy introduced by the merge.
3790 # merges to avoid losing information about copy introduced by the merge.
3789 # This might comes with bugs ?
3791 # This might comes with bugs ?
3790 reset_copy = p2 == repo.nullid
3792 reset_copy = p2 == repo.nullid
3791
3793
3792 def normal(filename):
3794 def normal(filename):
3793 return repo.dirstate.set_tracked(filename, reset_copy=reset_copy)
3795 return repo.dirstate.set_tracked(filename, reset_copy=reset_copy)
3794
3796
3795 newlyaddedandmodifiedfiles = set()
3797 newlyaddedandmodifiedfiles = set()
3796 if interactive:
3798 if interactive:
3797 # Prompt the user for changes to revert
3799 # Prompt the user for changes to revert
3798 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3800 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3799 m = scmutil.matchfiles(repo, torevert)
3801 m = scmutil.matchfiles(repo, torevert)
3800 diffopts = patch.difffeatureopts(
3802 diffopts = patch.difffeatureopts(
3801 repo.ui,
3803 repo.ui,
3802 whitespace=True,
3804 whitespace=True,
3803 section=b'commands',
3805 section=b'commands',
3804 configprefix=b'revert.interactive.',
3806 configprefix=b'revert.interactive.',
3805 )
3807 )
3806 diffopts.nodates = True
3808 diffopts.nodates = True
3807 diffopts.git = True
3809 diffopts.git = True
3808 operation = b'apply'
3810 operation = b'apply'
3809 if node == parent:
3811 if node == parent:
3810 if repo.ui.configbool(
3812 if repo.ui.configbool(
3811 b'experimental', b'revert.interactive.select-to-keep'
3813 b'experimental', b'revert.interactive.select-to-keep'
3812 ):
3814 ):
3813 operation = b'keep'
3815 operation = b'keep'
3814 else:
3816 else:
3815 operation = b'discard'
3817 operation = b'discard'
3816
3818
3817 if operation == b'apply':
3819 if operation == b'apply':
3818 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3820 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3819 else:
3821 else:
3820 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3822 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3821 original_headers = patch.parsepatch(diff)
3823 original_headers = patch.parsepatch(diff)
3822
3824
3823 try:
3825 try:
3824
3826
3825 chunks, opts = recordfilter(
3827 chunks, opts = recordfilter(
3826 repo.ui, original_headers, match, operation=operation
3828 repo.ui, original_headers, match, operation=operation
3827 )
3829 )
3828 if operation == b'discard':
3830 if operation == b'discard':
3829 chunks = patch.reversehunks(chunks)
3831 chunks = patch.reversehunks(chunks)
3830
3832
3831 except error.PatchParseError as err:
3833 except error.PatchParseError as err:
3832 raise error.InputError(_(b'error parsing patch: %s') % err)
3834 raise error.InputError(_(b'error parsing patch: %s') % err)
3833 except error.PatchApplicationError as err:
3835 except error.PatchApplicationError as err:
3834 raise error.StateError(_(b'error applying patch: %s') % err)
3836 raise error.StateError(_(b'error applying patch: %s') % err)
3835
3837
3836 # FIXME: when doing an interactive revert of a copy, there's no way of
3838 # FIXME: when doing an interactive revert of a copy, there's no way of
3837 # performing a partial revert of the added file, the only option is
3839 # performing a partial revert of the added file, the only option is
3838 # "remove added file <name> (Yn)?", so we don't need to worry about the
3840 # "remove added file <name> (Yn)?", so we don't need to worry about the
3839 # alsorestore value. Ideally we'd be able to partially revert
3841 # alsorestore value. Ideally we'd be able to partially revert
3840 # copied/renamed files.
3842 # copied/renamed files.
3841 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(chunks)
3843 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(chunks)
3842 if tobackup is None:
3844 if tobackup is None:
3843 tobackup = set()
3845 tobackup = set()
3844 # Apply changes
3846 # Apply changes
3845 fp = stringio()
3847 fp = stringio()
3846 # chunks are serialized per file, but files aren't sorted
3848 # chunks are serialized per file, but files aren't sorted
3847 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3849 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3848 prntstatusmsg(b'revert', f)
3850 prntstatusmsg(b'revert', f)
3849 files = set()
3851 files = set()
3850 for c in chunks:
3852 for c in chunks:
3851 if ishunk(c):
3853 if ishunk(c):
3852 abs = c.header.filename()
3854 abs = c.header.filename()
3853 # Create a backup file only if this hunk should be backed up
3855 # Create a backup file only if this hunk should be backed up
3854 if c.header.filename() in tobackup:
3856 if c.header.filename() in tobackup:
3855 target = repo.wjoin(abs)
3857 target = repo.wjoin(abs)
3856 bakname = scmutil.backuppath(repo.ui, repo, abs)
3858 bakname = scmutil.backuppath(repo.ui, repo, abs)
3857 util.copyfile(target, bakname)
3859 util.copyfile(target, bakname)
3858 tobackup.remove(abs)
3860 tobackup.remove(abs)
3859 if abs not in files:
3861 if abs not in files:
3860 files.add(abs)
3862 files.add(abs)
3861 if operation == b'keep':
3863 if operation == b'keep':
3862 checkout(abs)
3864 checkout(abs)
3863 c.write(fp)
3865 c.write(fp)
3864 dopatch = fp.tell()
3866 dopatch = fp.tell()
3865 fp.seek(0)
3867 fp.seek(0)
3866 if dopatch:
3868 if dopatch:
3867 try:
3869 try:
3868 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3870 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3869 except error.PatchParseError as err:
3871 except error.PatchParseError as err:
3870 raise error.InputError(pycompat.bytestr(err))
3872 raise error.InputError(pycompat.bytestr(err))
3871 except error.PatchApplicationError as err:
3873 except error.PatchApplicationError as err:
3872 raise error.StateError(pycompat.bytestr(err))
3874 raise error.StateError(pycompat.bytestr(err))
3873 del fp
3875 del fp
3874 else:
3876 else:
3875 for f in actions[b'revert'][0]:
3877 for f in actions[b'revert'][0]:
3876 prntstatusmsg(b'revert', f)
3878 prntstatusmsg(b'revert', f)
3877 checkout(f)
3879 checkout(f)
3878 if normal:
3880 if normal:
3879 normal(f)
3881 normal(f)
3880
3882
3881 for f in actions[b'add'][0]:
3883 for f in actions[b'add'][0]:
3882 # Don't checkout modified files, they are already created by the diff
3884 # Don't checkout modified files, they are already created by the diff
3883 if f in newlyaddedandmodifiedfiles:
3885 if f in newlyaddedandmodifiedfiles:
3884 continue
3886 continue
3885
3887
3886 if interactive:
3888 if interactive:
3887 choice = repo.ui.promptchoice(
3889 choice = repo.ui.promptchoice(
3888 _(b"add new file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3890 _(b"add new file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3889 )
3891 )
3890 if choice != 0:
3892 if choice != 0:
3891 continue
3893 continue
3892 prntstatusmsg(b'add', f)
3894 prntstatusmsg(b'add', f)
3893 checkout(f)
3895 checkout(f)
3894 repo.dirstate.set_tracked(f)
3896 repo.dirstate.set_tracked(f)
3895
3897
3896 for f in actions[b'undelete'][0]:
3898 for f in actions[b'undelete'][0]:
3897 if interactive:
3899 if interactive:
3898 choice = repo.ui.promptchoice(
3900 choice = repo.ui.promptchoice(
3899 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3901 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3900 )
3902 )
3901 if choice == 0:
3903 if choice == 0:
3902 prntstatusmsg(b'undelete', f)
3904 prntstatusmsg(b'undelete', f)
3903 checkout(f)
3905 checkout(f)
3904 normal(f)
3906 normal(f)
3905 else:
3907 else:
3906 excluded_files.append(f)
3908 excluded_files.append(f)
3907 else:
3909 else:
3908 prntstatusmsg(b'undelete', f)
3910 prntstatusmsg(b'undelete', f)
3909 checkout(f)
3911 checkout(f)
3910 normal(f)
3912 normal(f)
3911
3913
3912 copied = copies.pathcopies(repo[parent], ctx)
3914 copied = copies.pathcopies(repo[parent], ctx)
3913
3915
3914 for f in (
3916 for f in (
3915 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3917 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3916 ):
3918 ):
3917 if f in copied:
3919 if f in copied:
3918 repo.dirstate.copy(copied[f], f)
3920 repo.dirstate.copy(copied[f], f)
3919
3921
3920
3922
3921 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3923 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3922 # commands.outgoing. "missing" is "missing" of the result of
3924 # commands.outgoing. "missing" is "missing" of the result of
3923 # "findcommonoutgoing()"
3925 # "findcommonoutgoing()"
3924 outgoinghooks = util.hooks()
3926 outgoinghooks = util.hooks()
3925
3927
3926 # a list of (ui, repo) functions called by commands.summary
3928 # a list of (ui, repo) functions called by commands.summary
3927 summaryhooks = util.hooks()
3929 summaryhooks = util.hooks()
3928
3930
3929 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3931 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3930 #
3932 #
3931 # functions should return tuple of booleans below, if 'changes' is None:
3933 # functions should return tuple of booleans below, if 'changes' is None:
3932 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3934 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3933 #
3935 #
3934 # otherwise, 'changes' is a tuple of tuples below:
3936 # otherwise, 'changes' is a tuple of tuples below:
3935 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3937 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3936 # - (desturl, destbranch, destpeer, outgoing)
3938 # - (desturl, destbranch, destpeer, outgoing)
3937 summaryremotehooks = util.hooks()
3939 summaryremotehooks = util.hooks()
3938
3940
3939
3941
3940 def checkunfinished(repo, commit=False, skipmerge=False):
3942 def checkunfinished(repo, commit=False, skipmerge=False):
3941 """Look for an unfinished multistep operation, like graft, and abort
3943 """Look for an unfinished multistep operation, like graft, and abort
3942 if found. It's probably good to check this right before
3944 if found. It's probably good to check this right before
3943 bailifchanged().
3945 bailifchanged().
3944 """
3946 """
3945 # Check for non-clearable states first, so things like rebase will take
3947 # Check for non-clearable states first, so things like rebase will take
3946 # precedence over update.
3948 # precedence over update.
3947 for state in statemod._unfinishedstates:
3949 for state in statemod._unfinishedstates:
3948 if (
3950 if (
3949 state._clearable
3951 state._clearable
3950 or (commit and state._allowcommit)
3952 or (commit and state._allowcommit)
3951 or state._reportonly
3953 or state._reportonly
3952 ):
3954 ):
3953 continue
3955 continue
3954 if state.isunfinished(repo):
3956 if state.isunfinished(repo):
3955 raise error.StateError(state.msg(), hint=state.hint())
3957 raise error.StateError(state.msg(), hint=state.hint())
3956
3958
3957 for s in statemod._unfinishedstates:
3959 for s in statemod._unfinishedstates:
3958 if (
3960 if (
3959 not s._clearable
3961 not s._clearable
3960 or (commit and s._allowcommit)
3962 or (commit and s._allowcommit)
3961 or (s._opname == b'merge' and skipmerge)
3963 or (s._opname == b'merge' and skipmerge)
3962 or s._reportonly
3964 or s._reportonly
3963 ):
3965 ):
3964 continue
3966 continue
3965 if s.isunfinished(repo):
3967 if s.isunfinished(repo):
3966 raise error.StateError(s.msg(), hint=s.hint())
3968 raise error.StateError(s.msg(), hint=s.hint())
3967
3969
3968
3970
3969 def clearunfinished(repo):
3971 def clearunfinished(repo):
3970 """Check for unfinished operations (as above), and clear the ones
3972 """Check for unfinished operations (as above), and clear the ones
3971 that are clearable.
3973 that are clearable.
3972 """
3974 """
3973 for state in statemod._unfinishedstates:
3975 for state in statemod._unfinishedstates:
3974 if state._reportonly:
3976 if state._reportonly:
3975 continue
3977 continue
3976 if not state._clearable and state.isunfinished(repo):
3978 if not state._clearable and state.isunfinished(repo):
3977 raise error.StateError(state.msg(), hint=state.hint())
3979 raise error.StateError(state.msg(), hint=state.hint())
3978
3980
3979 for s in statemod._unfinishedstates:
3981 for s in statemod._unfinishedstates:
3980 if s._opname == b'merge' or s._reportonly:
3982 if s._opname == b'merge' or s._reportonly:
3981 continue
3983 continue
3982 if s._clearable and s.isunfinished(repo):
3984 if s._clearable and s.isunfinished(repo):
3983 util.unlink(repo.vfs.join(s._fname))
3985 util.unlink(repo.vfs.join(s._fname))
3984
3986
3985
3987
3986 def getunfinishedstate(repo):
3988 def getunfinishedstate(repo):
3987 """Checks for unfinished operations and returns statecheck object
3989 """Checks for unfinished operations and returns statecheck object
3988 for it"""
3990 for it"""
3989 for state in statemod._unfinishedstates:
3991 for state in statemod._unfinishedstates:
3990 if state.isunfinished(repo):
3992 if state.isunfinished(repo):
3991 return state
3993 return state
3992 return None
3994 return None
3993
3995
3994
3996
3995 def howtocontinue(repo):
3997 def howtocontinue(repo):
3996 """Check for an unfinished operation and return the command to finish
3998 """Check for an unfinished operation and return the command to finish
3997 it.
3999 it.
3998
4000
3999 statemod._unfinishedstates list is checked for an unfinished operation
4001 statemod._unfinishedstates list is checked for an unfinished operation
4000 and the corresponding message to finish it is generated if a method to
4002 and the corresponding message to finish it is generated if a method to
4001 continue is supported by the operation.
4003 continue is supported by the operation.
4002
4004
4003 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
4005 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
4004 a boolean.
4006 a boolean.
4005 """
4007 """
4006 contmsg = _(b"continue: %s")
4008 contmsg = _(b"continue: %s")
4007 for state in statemod._unfinishedstates:
4009 for state in statemod._unfinishedstates:
4008 if not state._continueflag:
4010 if not state._continueflag:
4009 continue
4011 continue
4010 if state.isunfinished(repo):
4012 if state.isunfinished(repo):
4011 return contmsg % state.continuemsg(), True
4013 return contmsg % state.continuemsg(), True
4012 if repo[None].dirty(missing=True, merge=False, branch=False):
4014 if repo[None].dirty(missing=True, merge=False, branch=False):
4013 return contmsg % _(b"hg commit"), False
4015 return contmsg % _(b"hg commit"), False
4014 return None, None
4016 return None, None
4015
4017
4016
4018
4017 def checkafterresolved(repo):
4019 def checkafterresolved(repo):
4018 """Inform the user about the next action after completing hg resolve
4020 """Inform the user about the next action after completing hg resolve
4019
4021
4020 If there's a an unfinished operation that supports continue flag,
4022 If there's a an unfinished operation that supports continue flag,
4021 howtocontinue will yield repo.ui.warn as the reporter.
4023 howtocontinue will yield repo.ui.warn as the reporter.
4022
4024
4023 Otherwise, it will yield repo.ui.note.
4025 Otherwise, it will yield repo.ui.note.
4024 """
4026 """
4025 msg, warning = howtocontinue(repo)
4027 msg, warning = howtocontinue(repo)
4026 if msg is not None:
4028 if msg is not None:
4027 if warning:
4029 if warning:
4028 repo.ui.warn(b"%s\n" % msg)
4030 repo.ui.warn(b"%s\n" % msg)
4029 else:
4031 else:
4030 repo.ui.note(b"%s\n" % msg)
4032 repo.ui.note(b"%s\n" % msg)
4031
4033
4032
4034
4033 def wrongtooltocontinue(repo, task):
4035 def wrongtooltocontinue(repo, task):
4034 """Raise an abort suggesting how to properly continue if there is an
4036 """Raise an abort suggesting how to properly continue if there is an
4035 active task.
4037 active task.
4036
4038
4037 Uses howtocontinue() to find the active task.
4039 Uses howtocontinue() to find the active task.
4038
4040
4039 If there's no task (repo.ui.note for 'hg commit'), it does not offer
4041 If there's no task (repo.ui.note for 'hg commit'), it does not offer
4040 a hint.
4042 a hint.
4041 """
4043 """
4042 after = howtocontinue(repo)
4044 after = howtocontinue(repo)
4043 hint = None
4045 hint = None
4044 if after[1]:
4046 if after[1]:
4045 hint = after[0]
4047 hint = after[0]
4046 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
4048 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
4047
4049
4048
4050
4049 def abortgraft(ui, repo, graftstate):
4051 def abortgraft(ui, repo, graftstate):
4050 """abort the interrupted graft and rollbacks to the state before interrupted
4052 """abort the interrupted graft and rollbacks to the state before interrupted
4051 graft"""
4053 graft"""
4052 if not graftstate.exists():
4054 if not graftstate.exists():
4053 raise error.StateError(_(b"no interrupted graft to abort"))
4055 raise error.StateError(_(b"no interrupted graft to abort"))
4054 statedata = readgraftstate(repo, graftstate)
4056 statedata = readgraftstate(repo, graftstate)
4055 newnodes = statedata.get(b'newnodes')
4057 newnodes = statedata.get(b'newnodes')
4056 if newnodes is None:
4058 if newnodes is None:
4057 # and old graft state which does not have all the data required to abort
4059 # and old graft state which does not have all the data required to abort
4058 # the graft
4060 # the graft
4059 raise error.Abort(_(b"cannot abort using an old graftstate"))
4061 raise error.Abort(_(b"cannot abort using an old graftstate"))
4060
4062
4061 # changeset from which graft operation was started
4063 # changeset from which graft operation was started
4062 if len(newnodes) > 0:
4064 if len(newnodes) > 0:
4063 startctx = repo[newnodes[0]].p1()
4065 startctx = repo[newnodes[0]].p1()
4064 else:
4066 else:
4065 startctx = repo[b'.']
4067 startctx = repo[b'.']
4066 # whether to strip or not
4068 # whether to strip or not
4067 cleanup = False
4069 cleanup = False
4068
4070
4069 if newnodes:
4071 if newnodes:
4070 newnodes = [repo[r].rev() for r in newnodes]
4072 newnodes = [repo[r].rev() for r in newnodes]
4071 cleanup = True
4073 cleanup = True
4072 # checking that none of the newnodes turned public or is public
4074 # checking that none of the newnodes turned public or is public
4073 immutable = [c for c in newnodes if not repo[c].mutable()]
4075 immutable = [c for c in newnodes if not repo[c].mutable()]
4074 if immutable:
4076 if immutable:
4075 repo.ui.warn(
4077 repo.ui.warn(
4076 _(b"cannot clean up public changesets %s\n")
4078 _(b"cannot clean up public changesets %s\n")
4077 % b', '.join(bytes(repo[r]) for r in immutable),
4079 % b', '.join(bytes(repo[r]) for r in immutable),
4078 hint=_(b"see 'hg help phases' for details"),
4080 hint=_(b"see 'hg help phases' for details"),
4079 )
4081 )
4080 cleanup = False
4082 cleanup = False
4081
4083
4082 # checking that no new nodes are created on top of grafted revs
4084 # checking that no new nodes are created on top of grafted revs
4083 desc = set(repo.changelog.descendants(newnodes))
4085 desc = set(repo.changelog.descendants(newnodes))
4084 if desc - set(newnodes):
4086 if desc - set(newnodes):
4085 repo.ui.warn(
4087 repo.ui.warn(
4086 _(
4088 _(
4087 b"new changesets detected on destination "
4089 b"new changesets detected on destination "
4088 b"branch, can't strip\n"
4090 b"branch, can't strip\n"
4089 )
4091 )
4090 )
4092 )
4091 cleanup = False
4093 cleanup = False
4092
4094
4093 if cleanup:
4095 if cleanup:
4094 with repo.wlock(), repo.lock():
4096 with repo.wlock(), repo.lock():
4095 mergemod.clean_update(startctx)
4097 mergemod.clean_update(startctx)
4096 # stripping the new nodes created
4098 # stripping the new nodes created
4097 strippoints = [
4099 strippoints = [
4098 c.node() for c in repo.set(b"roots(%ld)", newnodes)
4100 c.node() for c in repo.set(b"roots(%ld)", newnodes)
4099 ]
4101 ]
4100 repair.strip(repo.ui, repo, strippoints, backup=False)
4102 repair.strip(repo.ui, repo, strippoints, backup=False)
4101
4103
4102 if not cleanup:
4104 if not cleanup:
4103 # we don't update to the startnode if we can't strip
4105 # we don't update to the startnode if we can't strip
4104 startctx = repo[b'.']
4106 startctx = repo[b'.']
4105 mergemod.clean_update(startctx)
4107 mergemod.clean_update(startctx)
4106
4108
4107 ui.status(_(b"graft aborted\n"))
4109 ui.status(_(b"graft aborted\n"))
4108 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
4110 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
4109 graftstate.delete()
4111 graftstate.delete()
4110 return 0
4112 return 0
4111
4113
4112
4114
4113 def readgraftstate(repo, graftstate):
4115 def readgraftstate(repo, graftstate):
4114 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
4116 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
4115 """read the graft state file and return a dict of the data stored in it"""
4117 """read the graft state file and return a dict of the data stored in it"""
4116 try:
4118 try:
4117 return graftstate.read()
4119 return graftstate.read()
4118 except error.CorruptedState:
4120 except error.CorruptedState:
4119 nodes = repo.vfs.read(b'graftstate').splitlines()
4121 nodes = repo.vfs.read(b'graftstate').splitlines()
4120 return {b'nodes': nodes}
4122 return {b'nodes': nodes}
4121
4123
4122
4124
4123 def hgabortgraft(ui, repo):
4125 def hgabortgraft(ui, repo):
4124 """abort logic for aborting graft using 'hg abort'"""
4126 """abort logic for aborting graft using 'hg abort'"""
4125 with repo.wlock():
4127 with repo.wlock():
4126 graftstate = statemod.cmdstate(repo, b'graftstate')
4128 graftstate = statemod.cmdstate(repo, b'graftstate')
4127 return abortgraft(ui, repo, graftstate)
4129 return abortgraft(ui, repo, graftstate)
General Comments 0
You need to be logged in to leave comments. Login now