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