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