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