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