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