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