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