##// END OF EJS Templates
copy: recommend `--at-rev .` if target was added in parent commit...
Martin von Zweigbergk -
r49061:c62e4397 default
parent child Browse files
Show More
@@ -1,3946 +1,3964 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 = logcmdutil.revrange(repo, revs)
990 revs = logcmdutil.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 = logcmdutil.revsingle(repo, rev)
1483 ctx = logcmdutil.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 entry = repo.dirstate.get_entry(abstarget)
1645 entry = repo.dirstate.get_entry(abstarget)
1646
1646
1647 already_commited = entry.tracked and not entry.added
1647 already_commited = entry.tracked and not entry.added
1648
1648
1649 scmutil.checkportable(ui, abstarget)
1649 scmutil.checkportable(ui, abstarget)
1650
1650
1651 # check for collisions
1651 # check for collisions
1652 prevsrc = targets.get(abstarget)
1652 prevsrc = targets.get(abstarget)
1653 if prevsrc is not None:
1653 if prevsrc is not None:
1654 ui.warn(
1654 ui.warn(
1655 _(b'%s: not overwriting - %s collides with %s\n')
1655 _(b'%s: not overwriting - %s collides with %s\n')
1656 % (
1656 % (
1657 reltarget,
1657 reltarget,
1658 repo.pathto(abssrc, cwd),
1658 repo.pathto(abssrc, cwd),
1659 repo.pathto(prevsrc, cwd),
1659 repo.pathto(prevsrc, cwd),
1660 )
1660 )
1661 )
1661 )
1662 return True # report a failure
1662 return True # report a failure
1663
1663
1664 # check for overwrites
1664 # check for overwrites
1665 exists = os.path.lexists(target)
1665 exists = os.path.lexists(target)
1666 samefile = False
1666 samefile = False
1667 if exists and abssrc != abstarget:
1667 if exists and abssrc != abstarget:
1668 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1668 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1669 abstarget
1669 abstarget
1670 ):
1670 ):
1671 if not rename:
1671 if not rename:
1672 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1672 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1673 return True # report a failure
1673 return True # report a failure
1674 exists = False
1674 exists = False
1675 samefile = True
1675 samefile = True
1676
1676
1677 if not after and exists or after and already_commited:
1677 if not after and exists or after and already_commited:
1678 if not opts[b'force']:
1678 if not opts[b'force']:
1679 if already_commited:
1679 if already_commited:
1680 msg = _(b'%s: not overwriting - file already committed\n')
1680 msg = _(b'%s: not overwriting - file already committed\n')
1681 if after:
1681 # Check if if the target was added in the parent and the
1682 flags = b'--after --force'
1682 # source already existed in the grandparent.
1683 else:
1683 looks_like_copy_in_pctx = abstarget in pctx and any(
1684 flags = b'--force'
1684 abssrc in gpctx and abstarget not in gpctx
1685 if rename:
1685 for gpctx in pctx.parents()
1686 hint = (
1686 )
1687 _(
1687 if looks_like_copy_in_pctx:
1688 b"('hg rename %s' to replace the file by "
1688 if rename:
1689 b'recording a rename)\n'
1689 hint = _(
1690 b"('hg rename --at-rev .' to record the rename "
1691 b"in the parent of the working copy)\n"
1692 )
1693 else:
1694 hint = _(
1695 b"('hg copy --at-rev .' to record the copy in "
1696 b"the parent of the working copy)\n"
1690 )
1697 )
1691 % flags
1692 )
1693 else:
1698 else:
1694 hint = (
1699 if after:
1695 _(
1700 flags = b'--after --force'
1696 b"('hg copy %s' to replace the file by "
1701 else:
1697 b'recording a copy)\n'
1702 flags = b'--force'
1703 if rename:
1704 hint = (
1705 _(
1706 b"('hg rename %s' to replace the file by "
1707 b'recording a rename)\n'
1708 )
1709 % flags
1698 )
1710 )
1699 % flags
1711 else:
1700 )
1712 hint = (
1713 _(
1714 b"('hg copy %s' to replace the file by "
1715 b'recording a copy)\n'
1716 )
1717 % flags
1718 )
1701 else:
1719 else:
1702 msg = _(b'%s: not overwriting - file exists\n')
1720 msg = _(b'%s: not overwriting - file exists\n')
1703 if rename:
1721 if rename:
1704 hint = _(
1722 hint = _(
1705 b"('hg rename --after' to record the rename)\n"
1723 b"('hg rename --after' to record the rename)\n"
1706 )
1724 )
1707 else:
1725 else:
1708 hint = _(b"('hg copy --after' to record the copy)\n")
1726 hint = _(b"('hg copy --after' to record the copy)\n")
1709 ui.warn(msg % reltarget)
1727 ui.warn(msg % reltarget)
1710 ui.warn(hint)
1728 ui.warn(hint)
1711 return True # report a failure
1729 return True # report a failure
1712
1730
1713 if after:
1731 if after:
1714 if not exists:
1732 if not exists:
1715 if rename:
1733 if rename:
1716 ui.warn(
1734 ui.warn(
1717 _(b'%s: not recording move - %s does not exist\n')
1735 _(b'%s: not recording move - %s does not exist\n')
1718 % (relsrc, reltarget)
1736 % (relsrc, reltarget)
1719 )
1737 )
1720 else:
1738 else:
1721 ui.warn(
1739 ui.warn(
1722 _(b'%s: not recording copy - %s does not exist\n')
1740 _(b'%s: not recording copy - %s does not exist\n')
1723 % (relsrc, reltarget)
1741 % (relsrc, reltarget)
1724 )
1742 )
1725 return True # report a failure
1743 return True # report a failure
1726 elif not dryrun:
1744 elif not dryrun:
1727 try:
1745 try:
1728 if exists:
1746 if exists:
1729 os.unlink(target)
1747 os.unlink(target)
1730 targetdir = os.path.dirname(target) or b'.'
1748 targetdir = os.path.dirname(target) or b'.'
1731 if not os.path.isdir(targetdir):
1749 if not os.path.isdir(targetdir):
1732 os.makedirs(targetdir)
1750 os.makedirs(targetdir)
1733 if samefile:
1751 if samefile:
1734 tmp = target + b"~hgrename"
1752 tmp = target + b"~hgrename"
1735 os.rename(src, tmp)
1753 os.rename(src, tmp)
1736 os.rename(tmp, target)
1754 os.rename(tmp, target)
1737 else:
1755 else:
1738 # Preserve stat info on renames, not on copies; this matches
1756 # Preserve stat info on renames, not on copies; this matches
1739 # Linux CLI behavior.
1757 # Linux CLI behavior.
1740 util.copyfile(src, target, copystat=rename)
1758 util.copyfile(src, target, copystat=rename)
1741 srcexists = True
1759 srcexists = True
1742 except IOError as inst:
1760 except IOError as inst:
1743 if inst.errno == errno.ENOENT:
1761 if inst.errno == errno.ENOENT:
1744 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1762 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1745 srcexists = False
1763 srcexists = False
1746 else:
1764 else:
1747 ui.warn(
1765 ui.warn(
1748 _(b'%s: cannot copy - %s\n')
1766 _(b'%s: cannot copy - %s\n')
1749 % (relsrc, encoding.strtolocal(inst.strerror))
1767 % (relsrc, encoding.strtolocal(inst.strerror))
1750 )
1768 )
1751 return True # report a failure
1769 return True # report a failure
1752
1770
1753 if ui.verbose or not exact:
1771 if ui.verbose or not exact:
1754 if rename:
1772 if rename:
1755 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1773 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1756 else:
1774 else:
1757 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1775 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1758
1776
1759 targets[abstarget] = abssrc
1777 targets[abstarget] = abssrc
1760
1778
1761 # fix up dirstate
1779 # fix up dirstate
1762 scmutil.dirstatecopy(
1780 scmutil.dirstatecopy(
1763 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1781 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1764 )
1782 )
1765 if rename and not dryrun:
1783 if rename and not dryrun:
1766 if not after and srcexists and not samefile:
1784 if not after and srcexists and not samefile:
1767 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1785 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1768 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1786 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1769 ctx.forget([abssrc])
1787 ctx.forget([abssrc])
1770
1788
1771 # pat: ossep
1789 # pat: ossep
1772 # dest ossep
1790 # dest ossep
1773 # srcs: list of (hgsep, hgsep, ossep, bool)
1791 # srcs: list of (hgsep, hgsep, ossep, bool)
1774 # return: function that takes hgsep and returns ossep
1792 # return: function that takes hgsep and returns ossep
1775 def targetpathfn(pat, dest, srcs):
1793 def targetpathfn(pat, dest, srcs):
1776 if os.path.isdir(pat):
1794 if os.path.isdir(pat):
1777 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1795 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1778 abspfx = util.localpath(abspfx)
1796 abspfx = util.localpath(abspfx)
1779 if destdirexists:
1797 if destdirexists:
1780 striplen = len(os.path.split(abspfx)[0])
1798 striplen = len(os.path.split(abspfx)[0])
1781 else:
1799 else:
1782 striplen = len(abspfx)
1800 striplen = len(abspfx)
1783 if striplen:
1801 if striplen:
1784 striplen += len(pycompat.ossep)
1802 striplen += len(pycompat.ossep)
1785 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1803 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1786 elif destdirexists:
1804 elif destdirexists:
1787 res = lambda p: os.path.join(
1805 res = lambda p: os.path.join(
1788 dest, os.path.basename(util.localpath(p))
1806 dest, os.path.basename(util.localpath(p))
1789 )
1807 )
1790 else:
1808 else:
1791 res = lambda p: dest
1809 res = lambda p: dest
1792 return res
1810 return res
1793
1811
1794 # pat: ossep
1812 # pat: ossep
1795 # dest ossep
1813 # dest ossep
1796 # srcs: list of (hgsep, hgsep, ossep, bool)
1814 # srcs: list of (hgsep, hgsep, ossep, bool)
1797 # return: function that takes hgsep and returns ossep
1815 # return: function that takes hgsep and returns ossep
1798 def targetpathafterfn(pat, dest, srcs):
1816 def targetpathafterfn(pat, dest, srcs):
1799 if matchmod.patkind(pat):
1817 if matchmod.patkind(pat):
1800 # a mercurial pattern
1818 # a mercurial pattern
1801 res = lambda p: os.path.join(
1819 res = lambda p: os.path.join(
1802 dest, os.path.basename(util.localpath(p))
1820 dest, os.path.basename(util.localpath(p))
1803 )
1821 )
1804 else:
1822 else:
1805 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1823 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1806 if len(abspfx) < len(srcs[0][0]):
1824 if len(abspfx) < len(srcs[0][0]):
1807 # A directory. Either the target path contains the last
1825 # A directory. Either the target path contains the last
1808 # component of the source path or it does not.
1826 # component of the source path or it does not.
1809 def evalpath(striplen):
1827 def evalpath(striplen):
1810 score = 0
1828 score = 0
1811 for s in srcs:
1829 for s in srcs:
1812 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1830 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1813 if os.path.lexists(t):
1831 if os.path.lexists(t):
1814 score += 1
1832 score += 1
1815 return score
1833 return score
1816
1834
1817 abspfx = util.localpath(abspfx)
1835 abspfx = util.localpath(abspfx)
1818 striplen = len(abspfx)
1836 striplen = len(abspfx)
1819 if striplen:
1837 if striplen:
1820 striplen += len(pycompat.ossep)
1838 striplen += len(pycompat.ossep)
1821 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1839 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1822 score = evalpath(striplen)
1840 score = evalpath(striplen)
1823 striplen1 = len(os.path.split(abspfx)[0])
1841 striplen1 = len(os.path.split(abspfx)[0])
1824 if striplen1:
1842 if striplen1:
1825 striplen1 += len(pycompat.ossep)
1843 striplen1 += len(pycompat.ossep)
1826 if evalpath(striplen1) > score:
1844 if evalpath(striplen1) > score:
1827 striplen = striplen1
1845 striplen = striplen1
1828 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1846 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1829 else:
1847 else:
1830 # a file
1848 # a file
1831 if destdirexists:
1849 if destdirexists:
1832 res = lambda p: os.path.join(
1850 res = lambda p: os.path.join(
1833 dest, os.path.basename(util.localpath(p))
1851 dest, os.path.basename(util.localpath(p))
1834 )
1852 )
1835 else:
1853 else:
1836 res = lambda p: dest
1854 res = lambda p: dest
1837 return res
1855 return res
1838
1856
1839 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1857 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1840 if not destdirexists:
1858 if not destdirexists:
1841 if len(pats) > 1 or matchmod.patkind(pats[0]):
1859 if len(pats) > 1 or matchmod.patkind(pats[0]):
1842 raise error.InputError(
1860 raise error.InputError(
1843 _(
1861 _(
1844 b'with multiple sources, destination must be an '
1862 b'with multiple sources, destination must be an '
1845 b'existing directory'
1863 b'existing directory'
1846 )
1864 )
1847 )
1865 )
1848 if util.endswithsep(dest):
1866 if util.endswithsep(dest):
1849 raise error.InputError(
1867 raise error.InputError(
1850 _(b'destination %s is not a directory') % dest
1868 _(b'destination %s is not a directory') % dest
1851 )
1869 )
1852
1870
1853 tfn = targetpathfn
1871 tfn = targetpathfn
1854 if after:
1872 if after:
1855 tfn = targetpathafterfn
1873 tfn = targetpathafterfn
1856 copylist = []
1874 copylist = []
1857 for pat in pats:
1875 for pat in pats:
1858 srcs = walkpat(pat)
1876 srcs = walkpat(pat)
1859 if not srcs:
1877 if not srcs:
1860 continue
1878 continue
1861 copylist.append((tfn(pat, dest, srcs), srcs))
1879 copylist.append((tfn(pat, dest, srcs), srcs))
1862 if not copylist:
1880 if not copylist:
1863 hint = None
1881 hint = None
1864 if rename:
1882 if rename:
1865 hint = _(b'maybe you meant to use --after --at-rev=.')
1883 hint = _(b'maybe you meant to use --after --at-rev=.')
1866 raise error.InputError(_(b'no files to copy'), hint=hint)
1884 raise error.InputError(_(b'no files to copy'), hint=hint)
1867
1885
1868 errors = 0
1886 errors = 0
1869 for targetpath, srcs in copylist:
1887 for targetpath, srcs in copylist:
1870 for abssrc, relsrc, exact in srcs:
1888 for abssrc, relsrc, exact in srcs:
1871 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1889 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1872 errors += 1
1890 errors += 1
1873
1891
1874 return errors != 0
1892 return errors != 0
1875
1893
1876
1894
1877 ## facility to let extension process additional data into an import patch
1895 ## facility to let extension process additional data into an import patch
1878 # list of identifier to be executed in order
1896 # list of identifier to be executed in order
1879 extrapreimport = [] # run before commit
1897 extrapreimport = [] # run before commit
1880 extrapostimport = [] # run after commit
1898 extrapostimport = [] # run after commit
1881 # mapping from identifier to actual import function
1899 # mapping from identifier to actual import function
1882 #
1900 #
1883 # 'preimport' are run before the commit is made and are provided the following
1901 # 'preimport' are run before the commit is made and are provided the following
1884 # arguments:
1902 # arguments:
1885 # - repo: the localrepository instance,
1903 # - repo: the localrepository instance,
1886 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1904 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1887 # - extra: the future extra dictionary of the changeset, please mutate it,
1905 # - extra: the future extra dictionary of the changeset, please mutate it,
1888 # - opts: the import options.
1906 # - opts: the import options.
1889 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1907 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1890 # mutation of in memory commit and more. Feel free to rework the code to get
1908 # mutation of in memory commit and more. Feel free to rework the code to get
1891 # there.
1909 # there.
1892 extrapreimportmap = {}
1910 extrapreimportmap = {}
1893 # 'postimport' are run after the commit is made and are provided the following
1911 # 'postimport' are run after the commit is made and are provided the following
1894 # argument:
1912 # argument:
1895 # - ctx: the changectx created by import.
1913 # - ctx: the changectx created by import.
1896 extrapostimportmap = {}
1914 extrapostimportmap = {}
1897
1915
1898
1916
1899 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1917 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1900 """Utility function used by commands.import to import a single patch
1918 """Utility function used by commands.import to import a single patch
1901
1919
1902 This function is explicitly defined here to help the evolve extension to
1920 This function is explicitly defined here to help the evolve extension to
1903 wrap this part of the import logic.
1921 wrap this part of the import logic.
1904
1922
1905 The API is currently a bit ugly because it a simple code translation from
1923 The API is currently a bit ugly because it a simple code translation from
1906 the import command. Feel free to make it better.
1924 the import command. Feel free to make it better.
1907
1925
1908 :patchdata: a dictionary containing parsed patch data (such as from
1926 :patchdata: a dictionary containing parsed patch data (such as from
1909 ``patch.extract()``)
1927 ``patch.extract()``)
1910 :parents: nodes that will be parent of the created commit
1928 :parents: nodes that will be parent of the created commit
1911 :opts: the full dict of option passed to the import command
1929 :opts: the full dict of option passed to the import command
1912 :msgs: list to save commit message to.
1930 :msgs: list to save commit message to.
1913 (used in case we need to save it when failing)
1931 (used in case we need to save it when failing)
1914 :updatefunc: a function that update a repo to a given node
1932 :updatefunc: a function that update a repo to a given node
1915 updatefunc(<repo>, <node>)
1933 updatefunc(<repo>, <node>)
1916 """
1934 """
1917 # avoid cycle context -> subrepo -> cmdutil
1935 # avoid cycle context -> subrepo -> cmdutil
1918 from . import context
1936 from . import context
1919
1937
1920 tmpname = patchdata.get(b'filename')
1938 tmpname = patchdata.get(b'filename')
1921 message = patchdata.get(b'message')
1939 message = patchdata.get(b'message')
1922 user = opts.get(b'user') or patchdata.get(b'user')
1940 user = opts.get(b'user') or patchdata.get(b'user')
1923 date = opts.get(b'date') or patchdata.get(b'date')
1941 date = opts.get(b'date') or patchdata.get(b'date')
1924 branch = patchdata.get(b'branch')
1942 branch = patchdata.get(b'branch')
1925 nodeid = patchdata.get(b'nodeid')
1943 nodeid = patchdata.get(b'nodeid')
1926 p1 = patchdata.get(b'p1')
1944 p1 = patchdata.get(b'p1')
1927 p2 = patchdata.get(b'p2')
1945 p2 = patchdata.get(b'p2')
1928
1946
1929 nocommit = opts.get(b'no_commit')
1947 nocommit = opts.get(b'no_commit')
1930 importbranch = opts.get(b'import_branch')
1948 importbranch = opts.get(b'import_branch')
1931 update = not opts.get(b'bypass')
1949 update = not opts.get(b'bypass')
1932 strip = opts[b"strip"]
1950 strip = opts[b"strip"]
1933 prefix = opts[b"prefix"]
1951 prefix = opts[b"prefix"]
1934 sim = float(opts.get(b'similarity') or 0)
1952 sim = float(opts.get(b'similarity') or 0)
1935
1953
1936 if not tmpname:
1954 if not tmpname:
1937 return None, None, False
1955 return None, None, False
1938
1956
1939 rejects = False
1957 rejects = False
1940
1958
1941 cmdline_message = logmessage(ui, opts)
1959 cmdline_message = logmessage(ui, opts)
1942 if cmdline_message:
1960 if cmdline_message:
1943 # pickup the cmdline msg
1961 # pickup the cmdline msg
1944 message = cmdline_message
1962 message = cmdline_message
1945 elif message:
1963 elif message:
1946 # pickup the patch msg
1964 # pickup the patch msg
1947 message = message.strip()
1965 message = message.strip()
1948 else:
1966 else:
1949 # launch the editor
1967 # launch the editor
1950 message = None
1968 message = None
1951 ui.debug(b'message:\n%s\n' % (message or b''))
1969 ui.debug(b'message:\n%s\n' % (message or b''))
1952
1970
1953 if len(parents) == 1:
1971 if len(parents) == 1:
1954 parents.append(repo[nullrev])
1972 parents.append(repo[nullrev])
1955 if opts.get(b'exact'):
1973 if opts.get(b'exact'):
1956 if not nodeid or not p1:
1974 if not nodeid or not p1:
1957 raise error.InputError(_(b'not a Mercurial patch'))
1975 raise error.InputError(_(b'not a Mercurial patch'))
1958 p1 = repo[p1]
1976 p1 = repo[p1]
1959 p2 = repo[p2 or nullrev]
1977 p2 = repo[p2 or nullrev]
1960 elif p2:
1978 elif p2:
1961 try:
1979 try:
1962 p1 = repo[p1]
1980 p1 = repo[p1]
1963 p2 = repo[p2]
1981 p2 = repo[p2]
1964 # Without any options, consider p2 only if the
1982 # Without any options, consider p2 only if the
1965 # patch is being applied on top of the recorded
1983 # patch is being applied on top of the recorded
1966 # first parent.
1984 # first parent.
1967 if p1 != parents[0]:
1985 if p1 != parents[0]:
1968 p1 = parents[0]
1986 p1 = parents[0]
1969 p2 = repo[nullrev]
1987 p2 = repo[nullrev]
1970 except error.RepoError:
1988 except error.RepoError:
1971 p1, p2 = parents
1989 p1, p2 = parents
1972 if p2.rev() == nullrev:
1990 if p2.rev() == nullrev:
1973 ui.warn(
1991 ui.warn(
1974 _(
1992 _(
1975 b"warning: import the patch as a normal revision\n"
1993 b"warning: import the patch as a normal revision\n"
1976 b"(use --exact to import the patch as a merge)\n"
1994 b"(use --exact to import the patch as a merge)\n"
1977 )
1995 )
1978 )
1996 )
1979 else:
1997 else:
1980 p1, p2 = parents
1998 p1, p2 = parents
1981
1999
1982 n = None
2000 n = None
1983 if update:
2001 if update:
1984 if p1 != parents[0]:
2002 if p1 != parents[0]:
1985 updatefunc(repo, p1.node())
2003 updatefunc(repo, p1.node())
1986 if p2 != parents[1]:
2004 if p2 != parents[1]:
1987 repo.setparents(p1.node(), p2.node())
2005 repo.setparents(p1.node(), p2.node())
1988
2006
1989 if opts.get(b'exact') or importbranch:
2007 if opts.get(b'exact') or importbranch:
1990 repo.dirstate.setbranch(branch or b'default')
2008 repo.dirstate.setbranch(branch or b'default')
1991
2009
1992 partial = opts.get(b'partial', False)
2010 partial = opts.get(b'partial', False)
1993 files = set()
2011 files = set()
1994 try:
2012 try:
1995 patch.patch(
2013 patch.patch(
1996 ui,
2014 ui,
1997 repo,
2015 repo,
1998 tmpname,
2016 tmpname,
1999 strip=strip,
2017 strip=strip,
2000 prefix=prefix,
2018 prefix=prefix,
2001 files=files,
2019 files=files,
2002 eolmode=None,
2020 eolmode=None,
2003 similarity=sim / 100.0,
2021 similarity=sim / 100.0,
2004 )
2022 )
2005 except error.PatchError as e:
2023 except error.PatchError as e:
2006 if not partial:
2024 if not partial:
2007 raise error.Abort(pycompat.bytestr(e))
2025 raise error.Abort(pycompat.bytestr(e))
2008 if partial:
2026 if partial:
2009 rejects = True
2027 rejects = True
2010
2028
2011 files = list(files)
2029 files = list(files)
2012 if nocommit:
2030 if nocommit:
2013 if message:
2031 if message:
2014 msgs.append(message)
2032 msgs.append(message)
2015 else:
2033 else:
2016 if opts.get(b'exact') or p2:
2034 if opts.get(b'exact') or p2:
2017 # If you got here, you either use --force and know what
2035 # If you got here, you either use --force and know what
2018 # you are doing or used --exact or a merge patch while
2036 # you are doing or used --exact or a merge patch while
2019 # being updated to its first parent.
2037 # being updated to its first parent.
2020 m = None
2038 m = None
2021 else:
2039 else:
2022 m = scmutil.matchfiles(repo, files or [])
2040 m = scmutil.matchfiles(repo, files or [])
2023 editform = mergeeditform(repo[None], b'import.normal')
2041 editform = mergeeditform(repo[None], b'import.normal')
2024 if opts.get(b'exact'):
2042 if opts.get(b'exact'):
2025 editor = None
2043 editor = None
2026 else:
2044 else:
2027 editor = getcommiteditor(
2045 editor = getcommiteditor(
2028 editform=editform, **pycompat.strkwargs(opts)
2046 editform=editform, **pycompat.strkwargs(opts)
2029 )
2047 )
2030 extra = {}
2048 extra = {}
2031 for idfunc in extrapreimport:
2049 for idfunc in extrapreimport:
2032 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2050 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2033 overrides = {}
2051 overrides = {}
2034 if partial:
2052 if partial:
2035 overrides[(b'ui', b'allowemptycommit')] = True
2053 overrides[(b'ui', b'allowemptycommit')] = True
2036 if opts.get(b'secret'):
2054 if opts.get(b'secret'):
2037 overrides[(b'phases', b'new-commit')] = b'secret'
2055 overrides[(b'phases', b'new-commit')] = b'secret'
2038 with repo.ui.configoverride(overrides, b'import'):
2056 with repo.ui.configoverride(overrides, b'import'):
2039 n = repo.commit(
2057 n = repo.commit(
2040 message, user, date, match=m, editor=editor, extra=extra
2058 message, user, date, match=m, editor=editor, extra=extra
2041 )
2059 )
2042 for idfunc in extrapostimport:
2060 for idfunc in extrapostimport:
2043 extrapostimportmap[idfunc](repo[n])
2061 extrapostimportmap[idfunc](repo[n])
2044 else:
2062 else:
2045 if opts.get(b'exact') or importbranch:
2063 if opts.get(b'exact') or importbranch:
2046 branch = branch or b'default'
2064 branch = branch or b'default'
2047 else:
2065 else:
2048 branch = p1.branch()
2066 branch = p1.branch()
2049 store = patch.filestore()
2067 store = patch.filestore()
2050 try:
2068 try:
2051 files = set()
2069 files = set()
2052 try:
2070 try:
2053 patch.patchrepo(
2071 patch.patchrepo(
2054 ui,
2072 ui,
2055 repo,
2073 repo,
2056 p1,
2074 p1,
2057 store,
2075 store,
2058 tmpname,
2076 tmpname,
2059 strip,
2077 strip,
2060 prefix,
2078 prefix,
2061 files,
2079 files,
2062 eolmode=None,
2080 eolmode=None,
2063 )
2081 )
2064 except error.PatchError as e:
2082 except error.PatchError as e:
2065 raise error.Abort(stringutil.forcebytestr(e))
2083 raise error.Abort(stringutil.forcebytestr(e))
2066 if opts.get(b'exact'):
2084 if opts.get(b'exact'):
2067 editor = None
2085 editor = None
2068 else:
2086 else:
2069 editor = getcommiteditor(editform=b'import.bypass')
2087 editor = getcommiteditor(editform=b'import.bypass')
2070 memctx = context.memctx(
2088 memctx = context.memctx(
2071 repo,
2089 repo,
2072 (p1.node(), p2.node()),
2090 (p1.node(), p2.node()),
2073 message,
2091 message,
2074 files=files,
2092 files=files,
2075 filectxfn=store,
2093 filectxfn=store,
2076 user=user,
2094 user=user,
2077 date=date,
2095 date=date,
2078 branch=branch,
2096 branch=branch,
2079 editor=editor,
2097 editor=editor,
2080 )
2098 )
2081
2099
2082 overrides = {}
2100 overrides = {}
2083 if opts.get(b'secret'):
2101 if opts.get(b'secret'):
2084 overrides[(b'phases', b'new-commit')] = b'secret'
2102 overrides[(b'phases', b'new-commit')] = b'secret'
2085 with repo.ui.configoverride(overrides, b'import'):
2103 with repo.ui.configoverride(overrides, b'import'):
2086 n = memctx.commit()
2104 n = memctx.commit()
2087 finally:
2105 finally:
2088 store.close()
2106 store.close()
2089 if opts.get(b'exact') and nocommit:
2107 if opts.get(b'exact') and nocommit:
2090 # --exact with --no-commit is still useful in that it does merge
2108 # --exact with --no-commit is still useful in that it does merge
2091 # and branch bits
2109 # and branch bits
2092 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
2110 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
2093 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
2111 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
2094 raise error.Abort(_(b'patch is damaged or loses information'))
2112 raise error.Abort(_(b'patch is damaged or loses information'))
2095 msg = _(b'applied to working directory')
2113 msg = _(b'applied to working directory')
2096 if n:
2114 if n:
2097 # i18n: refers to a short changeset id
2115 # i18n: refers to a short changeset id
2098 msg = _(b'created %s') % short(n)
2116 msg = _(b'created %s') % short(n)
2099 return msg, n, rejects
2117 return msg, n, rejects
2100
2118
2101
2119
2102 # facility to let extensions include additional data in an exported patch
2120 # facility to let extensions include additional data in an exported patch
2103 # list of identifiers to be executed in order
2121 # list of identifiers to be executed in order
2104 extraexport = []
2122 extraexport = []
2105 # mapping from identifier to actual export function
2123 # mapping from identifier to actual export function
2106 # function as to return a string to be added to the header or None
2124 # function as to return a string to be added to the header or None
2107 # it is given two arguments (sequencenumber, changectx)
2125 # it is given two arguments (sequencenumber, changectx)
2108 extraexportmap = {}
2126 extraexportmap = {}
2109
2127
2110
2128
2111 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2129 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2112 node = scmutil.binnode(ctx)
2130 node = scmutil.binnode(ctx)
2113 parents = [p.node() for p in ctx.parents() if p]
2131 parents = [p.node() for p in ctx.parents() if p]
2114 branch = ctx.branch()
2132 branch = ctx.branch()
2115 if switch_parent:
2133 if switch_parent:
2116 parents.reverse()
2134 parents.reverse()
2117
2135
2118 if parents:
2136 if parents:
2119 prev = parents[0]
2137 prev = parents[0]
2120 else:
2138 else:
2121 prev = repo.nullid
2139 prev = repo.nullid
2122
2140
2123 fm.context(ctx=ctx)
2141 fm.context(ctx=ctx)
2124 fm.plain(b'# HG changeset patch\n')
2142 fm.plain(b'# HG changeset patch\n')
2125 fm.write(b'user', b'# User %s\n', ctx.user())
2143 fm.write(b'user', b'# User %s\n', ctx.user())
2126 fm.plain(b'# Date %d %d\n' % ctx.date())
2144 fm.plain(b'# Date %d %d\n' % ctx.date())
2127 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2145 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2128 fm.condwrite(
2146 fm.condwrite(
2129 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2147 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2130 )
2148 )
2131 fm.write(b'node', b'# Node ID %s\n', hex(node))
2149 fm.write(b'node', b'# Node ID %s\n', hex(node))
2132 fm.plain(b'# Parent %s\n' % hex(prev))
2150 fm.plain(b'# Parent %s\n' % hex(prev))
2133 if len(parents) > 1:
2151 if len(parents) > 1:
2134 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2152 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2135 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2153 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2136
2154
2137 # TODO: redesign extraexportmap function to support formatter
2155 # TODO: redesign extraexportmap function to support formatter
2138 for headerid in extraexport:
2156 for headerid in extraexport:
2139 header = extraexportmap[headerid](seqno, ctx)
2157 header = extraexportmap[headerid](seqno, ctx)
2140 if header is not None:
2158 if header is not None:
2141 fm.plain(b'# %s\n' % header)
2159 fm.plain(b'# %s\n' % header)
2142
2160
2143 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2161 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2144 fm.plain(b'\n')
2162 fm.plain(b'\n')
2145
2163
2146 if fm.isplain():
2164 if fm.isplain():
2147 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2165 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2148 for chunk, label in chunkiter:
2166 for chunk, label in chunkiter:
2149 fm.plain(chunk, label=label)
2167 fm.plain(chunk, label=label)
2150 else:
2168 else:
2151 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2169 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2152 # TODO: make it structured?
2170 # TODO: make it structured?
2153 fm.data(diff=b''.join(chunkiter))
2171 fm.data(diff=b''.join(chunkiter))
2154
2172
2155
2173
2156 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2174 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2157 """Export changesets to stdout or a single file"""
2175 """Export changesets to stdout or a single file"""
2158 for seqno, rev in enumerate(revs, 1):
2176 for seqno, rev in enumerate(revs, 1):
2159 ctx = repo[rev]
2177 ctx = repo[rev]
2160 if not dest.startswith(b'<'):
2178 if not dest.startswith(b'<'):
2161 repo.ui.note(b"%s\n" % dest)
2179 repo.ui.note(b"%s\n" % dest)
2162 fm.startitem()
2180 fm.startitem()
2163 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2181 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2164
2182
2165
2183
2166 def _exportfntemplate(
2184 def _exportfntemplate(
2167 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2185 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2168 ):
2186 ):
2169 """Export changesets to possibly multiple files"""
2187 """Export changesets to possibly multiple files"""
2170 total = len(revs)
2188 total = len(revs)
2171 revwidth = max(len(str(rev)) for rev in revs)
2189 revwidth = max(len(str(rev)) for rev in revs)
2172 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2190 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2173
2191
2174 for seqno, rev in enumerate(revs, 1):
2192 for seqno, rev in enumerate(revs, 1):
2175 ctx = repo[rev]
2193 ctx = repo[rev]
2176 dest = makefilename(
2194 dest = makefilename(
2177 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2195 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2178 )
2196 )
2179 filemap.setdefault(dest, []).append((seqno, rev))
2197 filemap.setdefault(dest, []).append((seqno, rev))
2180
2198
2181 for dest in filemap:
2199 for dest in filemap:
2182 with formatter.maybereopen(basefm, dest) as fm:
2200 with formatter.maybereopen(basefm, dest) as fm:
2183 repo.ui.note(b"%s\n" % dest)
2201 repo.ui.note(b"%s\n" % dest)
2184 for seqno, rev in filemap[dest]:
2202 for seqno, rev in filemap[dest]:
2185 fm.startitem()
2203 fm.startitem()
2186 ctx = repo[rev]
2204 ctx = repo[rev]
2187 _exportsingle(
2205 _exportsingle(
2188 repo, ctx, fm, match, switch_parent, seqno, diffopts
2206 repo, ctx, fm, match, switch_parent, seqno, diffopts
2189 )
2207 )
2190
2208
2191
2209
2192 def _prefetchchangedfiles(repo, revs, match):
2210 def _prefetchchangedfiles(repo, revs, match):
2193 allfiles = set()
2211 allfiles = set()
2194 for rev in revs:
2212 for rev in revs:
2195 for file in repo[rev].files():
2213 for file in repo[rev].files():
2196 if not match or match(file):
2214 if not match or match(file):
2197 allfiles.add(file)
2215 allfiles.add(file)
2198 match = scmutil.matchfiles(repo, allfiles)
2216 match = scmutil.matchfiles(repo, allfiles)
2199 revmatches = [(rev, match) for rev in revs]
2217 revmatches = [(rev, match) for rev in revs]
2200 scmutil.prefetchfiles(repo, revmatches)
2218 scmutil.prefetchfiles(repo, revmatches)
2201
2219
2202
2220
2203 def export(
2221 def export(
2204 repo,
2222 repo,
2205 revs,
2223 revs,
2206 basefm,
2224 basefm,
2207 fntemplate=b'hg-%h.patch',
2225 fntemplate=b'hg-%h.patch',
2208 switch_parent=False,
2226 switch_parent=False,
2209 opts=None,
2227 opts=None,
2210 match=None,
2228 match=None,
2211 ):
2229 ):
2212 """export changesets as hg patches
2230 """export changesets as hg patches
2213
2231
2214 Args:
2232 Args:
2215 repo: The repository from which we're exporting revisions.
2233 repo: The repository from which we're exporting revisions.
2216 revs: A list of revisions to export as revision numbers.
2234 revs: A list of revisions to export as revision numbers.
2217 basefm: A formatter to which patches should be written.
2235 basefm: A formatter to which patches should be written.
2218 fntemplate: An optional string to use for generating patch file names.
2236 fntemplate: An optional string to use for generating patch file names.
2219 switch_parent: If True, show diffs against second parent when not nullid.
2237 switch_parent: If True, show diffs against second parent when not nullid.
2220 Default is false, which always shows diff against p1.
2238 Default is false, which always shows diff against p1.
2221 opts: diff options to use for generating the patch.
2239 opts: diff options to use for generating the patch.
2222 match: If specified, only export changes to files matching this matcher.
2240 match: If specified, only export changes to files matching this matcher.
2223
2241
2224 Returns:
2242 Returns:
2225 Nothing.
2243 Nothing.
2226
2244
2227 Side Effect:
2245 Side Effect:
2228 "HG Changeset Patch" data is emitted to one of the following
2246 "HG Changeset Patch" data is emitted to one of the following
2229 destinations:
2247 destinations:
2230 fntemplate specified: Each rev is written to a unique file named using
2248 fntemplate specified: Each rev is written to a unique file named using
2231 the given template.
2249 the given template.
2232 Otherwise: All revs will be written to basefm.
2250 Otherwise: All revs will be written to basefm.
2233 """
2251 """
2234 _prefetchchangedfiles(repo, revs, match)
2252 _prefetchchangedfiles(repo, revs, match)
2235
2253
2236 if not fntemplate:
2254 if not fntemplate:
2237 _exportfile(
2255 _exportfile(
2238 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2256 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2239 )
2257 )
2240 else:
2258 else:
2241 _exportfntemplate(
2259 _exportfntemplate(
2242 repo, revs, basefm, fntemplate, switch_parent, opts, match
2260 repo, revs, basefm, fntemplate, switch_parent, opts, match
2243 )
2261 )
2244
2262
2245
2263
2246 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2264 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2247 """Export changesets to the given file stream"""
2265 """Export changesets to the given file stream"""
2248 _prefetchchangedfiles(repo, revs, match)
2266 _prefetchchangedfiles(repo, revs, match)
2249
2267
2250 dest = getattr(fp, 'name', b'<unnamed>')
2268 dest = getattr(fp, 'name', b'<unnamed>')
2251 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2269 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2252 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2270 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2253
2271
2254
2272
2255 def showmarker(fm, marker, index=None):
2273 def showmarker(fm, marker, index=None):
2256 """utility function to display obsolescence marker in a readable way
2274 """utility function to display obsolescence marker in a readable way
2257
2275
2258 To be used by debug function."""
2276 To be used by debug function."""
2259 if index is not None:
2277 if index is not None:
2260 fm.write(b'index', b'%i ', index)
2278 fm.write(b'index', b'%i ', index)
2261 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2279 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2262 succs = marker.succnodes()
2280 succs = marker.succnodes()
2263 fm.condwrite(
2281 fm.condwrite(
2264 succs,
2282 succs,
2265 b'succnodes',
2283 b'succnodes',
2266 b'%s ',
2284 b'%s ',
2267 fm.formatlist(map(hex, succs), name=b'node'),
2285 fm.formatlist(map(hex, succs), name=b'node'),
2268 )
2286 )
2269 fm.write(b'flag', b'%X ', marker.flags())
2287 fm.write(b'flag', b'%X ', marker.flags())
2270 parents = marker.parentnodes()
2288 parents = marker.parentnodes()
2271 if parents is not None:
2289 if parents is not None:
2272 fm.write(
2290 fm.write(
2273 b'parentnodes',
2291 b'parentnodes',
2274 b'{%s} ',
2292 b'{%s} ',
2275 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2293 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2276 )
2294 )
2277 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2295 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2278 meta = marker.metadata().copy()
2296 meta = marker.metadata().copy()
2279 meta.pop(b'date', None)
2297 meta.pop(b'date', None)
2280 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2298 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2281 fm.write(
2299 fm.write(
2282 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2300 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2283 )
2301 )
2284 fm.plain(b'\n')
2302 fm.plain(b'\n')
2285
2303
2286
2304
2287 def finddate(ui, repo, date):
2305 def finddate(ui, repo, date):
2288 """Find the tipmost changeset that matches the given date spec"""
2306 """Find the tipmost changeset that matches the given date spec"""
2289 mrevs = repo.revs(b'date(%s)', date)
2307 mrevs = repo.revs(b'date(%s)', date)
2290 try:
2308 try:
2291 rev = mrevs.max()
2309 rev = mrevs.max()
2292 except ValueError:
2310 except ValueError:
2293 raise error.InputError(_(b"revision matching date not found"))
2311 raise error.InputError(_(b"revision matching date not found"))
2294
2312
2295 ui.status(
2313 ui.status(
2296 _(b"found revision %d from %s\n")
2314 _(b"found revision %d from %s\n")
2297 % (rev, dateutil.datestr(repo[rev].date()))
2315 % (rev, dateutil.datestr(repo[rev].date()))
2298 )
2316 )
2299 return b'%d' % rev
2317 return b'%d' % rev
2300
2318
2301
2319
2302 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2320 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2303 bad = []
2321 bad = []
2304
2322
2305 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2323 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2306 names = []
2324 names = []
2307 wctx = repo[None]
2325 wctx = repo[None]
2308 cca = None
2326 cca = None
2309 abort, warn = scmutil.checkportabilityalert(ui)
2327 abort, warn = scmutil.checkportabilityalert(ui)
2310 if abort or warn:
2328 if abort or warn:
2311 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2329 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2312
2330
2313 match = repo.narrowmatch(match, includeexact=True)
2331 match = repo.narrowmatch(match, includeexact=True)
2314 badmatch = matchmod.badmatch(match, badfn)
2332 badmatch = matchmod.badmatch(match, badfn)
2315 dirstate = repo.dirstate
2333 dirstate = repo.dirstate
2316 # We don't want to just call wctx.walk here, since it would return a lot of
2334 # We don't want to just call wctx.walk here, since it would return a lot of
2317 # clean files, which we aren't interested in and takes time.
2335 # clean files, which we aren't interested in and takes time.
2318 for f in sorted(
2336 for f in sorted(
2319 dirstate.walk(
2337 dirstate.walk(
2320 badmatch,
2338 badmatch,
2321 subrepos=sorted(wctx.substate),
2339 subrepos=sorted(wctx.substate),
2322 unknown=True,
2340 unknown=True,
2323 ignored=False,
2341 ignored=False,
2324 full=False,
2342 full=False,
2325 )
2343 )
2326 ):
2344 ):
2327 exact = match.exact(f)
2345 exact = match.exact(f)
2328 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2346 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2329 if cca:
2347 if cca:
2330 cca(f)
2348 cca(f)
2331 names.append(f)
2349 names.append(f)
2332 if ui.verbose or not exact:
2350 if ui.verbose or not exact:
2333 ui.status(
2351 ui.status(
2334 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2352 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2335 )
2353 )
2336
2354
2337 for subpath in sorted(wctx.substate):
2355 for subpath in sorted(wctx.substate):
2338 sub = wctx.sub(subpath)
2356 sub = wctx.sub(subpath)
2339 try:
2357 try:
2340 submatch = matchmod.subdirmatcher(subpath, match)
2358 submatch = matchmod.subdirmatcher(subpath, match)
2341 subprefix = repo.wvfs.reljoin(prefix, subpath)
2359 subprefix = repo.wvfs.reljoin(prefix, subpath)
2342 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2360 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2343 if opts.get('subrepos'):
2361 if opts.get('subrepos'):
2344 bad.extend(
2362 bad.extend(
2345 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2363 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2346 )
2364 )
2347 else:
2365 else:
2348 bad.extend(
2366 bad.extend(
2349 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2367 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2350 )
2368 )
2351 except error.LookupError:
2369 except error.LookupError:
2352 ui.status(
2370 ui.status(
2353 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2371 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2354 )
2372 )
2355
2373
2356 if not opts.get('dry_run'):
2374 if not opts.get('dry_run'):
2357 rejected = wctx.add(names, prefix)
2375 rejected = wctx.add(names, prefix)
2358 bad.extend(f for f in rejected if f in match.files())
2376 bad.extend(f for f in rejected if f in match.files())
2359 return bad
2377 return bad
2360
2378
2361
2379
2362 def addwebdirpath(repo, serverpath, webconf):
2380 def addwebdirpath(repo, serverpath, webconf):
2363 webconf[serverpath] = repo.root
2381 webconf[serverpath] = repo.root
2364 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2382 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2365
2383
2366 for r in repo.revs(b'filelog("path:.hgsub")'):
2384 for r in repo.revs(b'filelog("path:.hgsub")'):
2367 ctx = repo[r]
2385 ctx = repo[r]
2368 for subpath in ctx.substate:
2386 for subpath in ctx.substate:
2369 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2387 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2370
2388
2371
2389
2372 def forget(
2390 def forget(
2373 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2391 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2374 ):
2392 ):
2375 if dryrun and interactive:
2393 if dryrun and interactive:
2376 raise error.InputError(
2394 raise error.InputError(
2377 _(b"cannot specify both --dry-run and --interactive")
2395 _(b"cannot specify both --dry-run and --interactive")
2378 )
2396 )
2379 bad = []
2397 bad = []
2380 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2398 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2381 wctx = repo[None]
2399 wctx = repo[None]
2382 forgot = []
2400 forgot = []
2383
2401
2384 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2402 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2385 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2403 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2386 if explicitonly:
2404 if explicitonly:
2387 forget = [f for f in forget if match.exact(f)]
2405 forget = [f for f in forget if match.exact(f)]
2388
2406
2389 for subpath in sorted(wctx.substate):
2407 for subpath in sorted(wctx.substate):
2390 sub = wctx.sub(subpath)
2408 sub = wctx.sub(subpath)
2391 submatch = matchmod.subdirmatcher(subpath, match)
2409 submatch = matchmod.subdirmatcher(subpath, match)
2392 subprefix = repo.wvfs.reljoin(prefix, subpath)
2410 subprefix = repo.wvfs.reljoin(prefix, subpath)
2393 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2411 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2394 try:
2412 try:
2395 subbad, subforgot = sub.forget(
2413 subbad, subforgot = sub.forget(
2396 submatch,
2414 submatch,
2397 subprefix,
2415 subprefix,
2398 subuipathfn,
2416 subuipathfn,
2399 dryrun=dryrun,
2417 dryrun=dryrun,
2400 interactive=interactive,
2418 interactive=interactive,
2401 )
2419 )
2402 bad.extend([subpath + b'/' + f for f in subbad])
2420 bad.extend([subpath + b'/' + f for f in subbad])
2403 forgot.extend([subpath + b'/' + f for f in subforgot])
2421 forgot.extend([subpath + b'/' + f for f in subforgot])
2404 except error.LookupError:
2422 except error.LookupError:
2405 ui.status(
2423 ui.status(
2406 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2424 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2407 )
2425 )
2408
2426
2409 if not explicitonly:
2427 if not explicitonly:
2410 for f in match.files():
2428 for f in match.files():
2411 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2429 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2412 if f not in forgot:
2430 if f not in forgot:
2413 if repo.wvfs.exists(f):
2431 if repo.wvfs.exists(f):
2414 # Don't complain if the exact case match wasn't given.
2432 # Don't complain if the exact case match wasn't given.
2415 # But don't do this until after checking 'forgot', so
2433 # But don't do this until after checking 'forgot', so
2416 # that subrepo files aren't normalized, and this op is
2434 # that subrepo files aren't normalized, and this op is
2417 # purely from data cached by the status walk above.
2435 # purely from data cached by the status walk above.
2418 if repo.dirstate.normalize(f) in repo.dirstate:
2436 if repo.dirstate.normalize(f) in repo.dirstate:
2419 continue
2437 continue
2420 ui.warn(
2438 ui.warn(
2421 _(
2439 _(
2422 b'not removing %s: '
2440 b'not removing %s: '
2423 b'file is already untracked\n'
2441 b'file is already untracked\n'
2424 )
2442 )
2425 % uipathfn(f)
2443 % uipathfn(f)
2426 )
2444 )
2427 bad.append(f)
2445 bad.append(f)
2428
2446
2429 if interactive:
2447 if interactive:
2430 responses = _(
2448 responses = _(
2431 b'[Ynsa?]'
2449 b'[Ynsa?]'
2432 b'$$ &Yes, forget this file'
2450 b'$$ &Yes, forget this file'
2433 b'$$ &No, skip this file'
2451 b'$$ &No, skip this file'
2434 b'$$ &Skip remaining files'
2452 b'$$ &Skip remaining files'
2435 b'$$ Include &all remaining files'
2453 b'$$ Include &all remaining files'
2436 b'$$ &? (display help)'
2454 b'$$ &? (display help)'
2437 )
2455 )
2438 for filename in forget[:]:
2456 for filename in forget[:]:
2439 r = ui.promptchoice(
2457 r = ui.promptchoice(
2440 _(b'forget %s %s') % (uipathfn(filename), responses)
2458 _(b'forget %s %s') % (uipathfn(filename), responses)
2441 )
2459 )
2442 if r == 4: # ?
2460 if r == 4: # ?
2443 while r == 4:
2461 while r == 4:
2444 for c, t in ui.extractchoices(responses)[1]:
2462 for c, t in ui.extractchoices(responses)[1]:
2445 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2463 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2446 r = ui.promptchoice(
2464 r = ui.promptchoice(
2447 _(b'forget %s %s') % (uipathfn(filename), responses)
2465 _(b'forget %s %s') % (uipathfn(filename), responses)
2448 )
2466 )
2449 if r == 0: # yes
2467 if r == 0: # yes
2450 continue
2468 continue
2451 elif r == 1: # no
2469 elif r == 1: # no
2452 forget.remove(filename)
2470 forget.remove(filename)
2453 elif r == 2: # Skip
2471 elif r == 2: # Skip
2454 fnindex = forget.index(filename)
2472 fnindex = forget.index(filename)
2455 del forget[fnindex:]
2473 del forget[fnindex:]
2456 break
2474 break
2457 elif r == 3: # All
2475 elif r == 3: # All
2458 break
2476 break
2459
2477
2460 for f in forget:
2478 for f in forget:
2461 if ui.verbose or not match.exact(f) or interactive:
2479 if ui.verbose or not match.exact(f) or interactive:
2462 ui.status(
2480 ui.status(
2463 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2481 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2464 )
2482 )
2465
2483
2466 if not dryrun:
2484 if not dryrun:
2467 rejected = wctx.forget(forget, prefix)
2485 rejected = wctx.forget(forget, prefix)
2468 bad.extend(f for f in rejected if f in match.files())
2486 bad.extend(f for f in rejected if f in match.files())
2469 forgot.extend(f for f in forget if f not in rejected)
2487 forgot.extend(f for f in forget if f not in rejected)
2470 return bad, forgot
2488 return bad, forgot
2471
2489
2472
2490
2473 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2491 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2474 ret = 1
2492 ret = 1
2475
2493
2476 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2494 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2477 if fm.isplain() and not needsfctx:
2495 if fm.isplain() and not needsfctx:
2478 # Fast path. The speed-up comes from skipping the formatter, and batching
2496 # Fast path. The speed-up comes from skipping the formatter, and batching
2479 # calls to ui.write.
2497 # calls to ui.write.
2480 buf = []
2498 buf = []
2481 for f in ctx.matches(m):
2499 for f in ctx.matches(m):
2482 buf.append(fmt % uipathfn(f))
2500 buf.append(fmt % uipathfn(f))
2483 if len(buf) > 100:
2501 if len(buf) > 100:
2484 ui.write(b''.join(buf))
2502 ui.write(b''.join(buf))
2485 del buf[:]
2503 del buf[:]
2486 ret = 0
2504 ret = 0
2487 if buf:
2505 if buf:
2488 ui.write(b''.join(buf))
2506 ui.write(b''.join(buf))
2489 else:
2507 else:
2490 for f in ctx.matches(m):
2508 for f in ctx.matches(m):
2491 fm.startitem()
2509 fm.startitem()
2492 fm.context(ctx=ctx)
2510 fm.context(ctx=ctx)
2493 if needsfctx:
2511 if needsfctx:
2494 fc = ctx[f]
2512 fc = ctx[f]
2495 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2513 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2496 fm.data(path=f)
2514 fm.data(path=f)
2497 fm.plain(fmt % uipathfn(f))
2515 fm.plain(fmt % uipathfn(f))
2498 ret = 0
2516 ret = 0
2499
2517
2500 for subpath in sorted(ctx.substate):
2518 for subpath in sorted(ctx.substate):
2501 submatch = matchmod.subdirmatcher(subpath, m)
2519 submatch = matchmod.subdirmatcher(subpath, m)
2502 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2520 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2503 if subrepos or m.exact(subpath) or any(submatch.files()):
2521 if subrepos or m.exact(subpath) or any(submatch.files()):
2504 sub = ctx.sub(subpath)
2522 sub = ctx.sub(subpath)
2505 try:
2523 try:
2506 recurse = m.exact(subpath) or subrepos
2524 recurse = m.exact(subpath) or subrepos
2507 if (
2525 if (
2508 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2526 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2509 == 0
2527 == 0
2510 ):
2528 ):
2511 ret = 0
2529 ret = 0
2512 except error.LookupError:
2530 except error.LookupError:
2513 ui.status(
2531 ui.status(
2514 _(b"skipping missing subrepository: %s\n")
2532 _(b"skipping missing subrepository: %s\n")
2515 % uipathfn(subpath)
2533 % uipathfn(subpath)
2516 )
2534 )
2517
2535
2518 return ret
2536 return ret
2519
2537
2520
2538
2521 def remove(
2539 def remove(
2522 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2540 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2523 ):
2541 ):
2524 ret = 0
2542 ret = 0
2525 s = repo.status(match=m, clean=True)
2543 s = repo.status(match=m, clean=True)
2526 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2544 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2527
2545
2528 wctx = repo[None]
2546 wctx = repo[None]
2529
2547
2530 if warnings is None:
2548 if warnings is None:
2531 warnings = []
2549 warnings = []
2532 warn = True
2550 warn = True
2533 else:
2551 else:
2534 warn = False
2552 warn = False
2535
2553
2536 subs = sorted(wctx.substate)
2554 subs = sorted(wctx.substate)
2537 progress = ui.makeprogress(
2555 progress = ui.makeprogress(
2538 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2556 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2539 )
2557 )
2540 for subpath in subs:
2558 for subpath in subs:
2541 submatch = matchmod.subdirmatcher(subpath, m)
2559 submatch = matchmod.subdirmatcher(subpath, m)
2542 subprefix = repo.wvfs.reljoin(prefix, subpath)
2560 subprefix = repo.wvfs.reljoin(prefix, subpath)
2543 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2561 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2544 if subrepos or m.exact(subpath) or any(submatch.files()):
2562 if subrepos or m.exact(subpath) or any(submatch.files()):
2545 progress.increment()
2563 progress.increment()
2546 sub = wctx.sub(subpath)
2564 sub = wctx.sub(subpath)
2547 try:
2565 try:
2548 if sub.removefiles(
2566 if sub.removefiles(
2549 submatch,
2567 submatch,
2550 subprefix,
2568 subprefix,
2551 subuipathfn,
2569 subuipathfn,
2552 after,
2570 after,
2553 force,
2571 force,
2554 subrepos,
2572 subrepos,
2555 dryrun,
2573 dryrun,
2556 warnings,
2574 warnings,
2557 ):
2575 ):
2558 ret = 1
2576 ret = 1
2559 except error.LookupError:
2577 except error.LookupError:
2560 warnings.append(
2578 warnings.append(
2561 _(b"skipping missing subrepository: %s\n")
2579 _(b"skipping missing subrepository: %s\n")
2562 % uipathfn(subpath)
2580 % uipathfn(subpath)
2563 )
2581 )
2564 progress.complete()
2582 progress.complete()
2565
2583
2566 # warn about failure to delete explicit files/dirs
2584 # warn about failure to delete explicit files/dirs
2567 deleteddirs = pathutil.dirs(deleted)
2585 deleteddirs = pathutil.dirs(deleted)
2568 files = m.files()
2586 files = m.files()
2569 progress = ui.makeprogress(
2587 progress = ui.makeprogress(
2570 _(b'deleting'), total=len(files), unit=_(b'files')
2588 _(b'deleting'), total=len(files), unit=_(b'files')
2571 )
2589 )
2572 for f in files:
2590 for f in files:
2573
2591
2574 def insubrepo():
2592 def insubrepo():
2575 for subpath in wctx.substate:
2593 for subpath in wctx.substate:
2576 if f.startswith(subpath + b'/'):
2594 if f.startswith(subpath + b'/'):
2577 return True
2595 return True
2578 return False
2596 return False
2579
2597
2580 progress.increment()
2598 progress.increment()
2581 isdir = f in deleteddirs or wctx.hasdir(f)
2599 isdir = f in deleteddirs or wctx.hasdir(f)
2582 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2600 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2583 continue
2601 continue
2584
2602
2585 if repo.wvfs.exists(f):
2603 if repo.wvfs.exists(f):
2586 if repo.wvfs.isdir(f):
2604 if repo.wvfs.isdir(f):
2587 warnings.append(
2605 warnings.append(
2588 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2606 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2589 )
2607 )
2590 else:
2608 else:
2591 warnings.append(
2609 warnings.append(
2592 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2610 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2593 )
2611 )
2594 # missing files will generate a warning elsewhere
2612 # missing files will generate a warning elsewhere
2595 ret = 1
2613 ret = 1
2596 progress.complete()
2614 progress.complete()
2597
2615
2598 if force:
2616 if force:
2599 list = modified + deleted + clean + added
2617 list = modified + deleted + clean + added
2600 elif after:
2618 elif after:
2601 list = deleted
2619 list = deleted
2602 remaining = modified + added + clean
2620 remaining = modified + added + clean
2603 progress = ui.makeprogress(
2621 progress = ui.makeprogress(
2604 _(b'skipping'), total=len(remaining), unit=_(b'files')
2622 _(b'skipping'), total=len(remaining), unit=_(b'files')
2605 )
2623 )
2606 for f in remaining:
2624 for f in remaining:
2607 progress.increment()
2625 progress.increment()
2608 if ui.verbose or (f in files):
2626 if ui.verbose or (f in files):
2609 warnings.append(
2627 warnings.append(
2610 _(b'not removing %s: file still exists\n') % uipathfn(f)
2628 _(b'not removing %s: file still exists\n') % uipathfn(f)
2611 )
2629 )
2612 ret = 1
2630 ret = 1
2613 progress.complete()
2631 progress.complete()
2614 else:
2632 else:
2615 list = deleted + clean
2633 list = deleted + clean
2616 progress = ui.makeprogress(
2634 progress = ui.makeprogress(
2617 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2635 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2618 )
2636 )
2619 for f in modified:
2637 for f in modified:
2620 progress.increment()
2638 progress.increment()
2621 warnings.append(
2639 warnings.append(
2622 _(
2640 _(
2623 b'not removing %s: file is modified (use -f'
2641 b'not removing %s: file is modified (use -f'
2624 b' to force removal)\n'
2642 b' to force removal)\n'
2625 )
2643 )
2626 % uipathfn(f)
2644 % uipathfn(f)
2627 )
2645 )
2628 ret = 1
2646 ret = 1
2629 for f in added:
2647 for f in added:
2630 progress.increment()
2648 progress.increment()
2631 warnings.append(
2649 warnings.append(
2632 _(
2650 _(
2633 b"not removing %s: file has been marked for add"
2651 b"not removing %s: file has been marked for add"
2634 b" (use 'hg forget' to undo add)\n"
2652 b" (use 'hg forget' to undo add)\n"
2635 )
2653 )
2636 % uipathfn(f)
2654 % uipathfn(f)
2637 )
2655 )
2638 ret = 1
2656 ret = 1
2639 progress.complete()
2657 progress.complete()
2640
2658
2641 list = sorted(list)
2659 list = sorted(list)
2642 progress = ui.makeprogress(
2660 progress = ui.makeprogress(
2643 _(b'deleting'), total=len(list), unit=_(b'files')
2661 _(b'deleting'), total=len(list), unit=_(b'files')
2644 )
2662 )
2645 for f in list:
2663 for f in list:
2646 if ui.verbose or not m.exact(f):
2664 if ui.verbose or not m.exact(f):
2647 progress.increment()
2665 progress.increment()
2648 ui.status(
2666 ui.status(
2649 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2667 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2650 )
2668 )
2651 progress.complete()
2669 progress.complete()
2652
2670
2653 if not dryrun:
2671 if not dryrun:
2654 with repo.wlock():
2672 with repo.wlock():
2655 if not after:
2673 if not after:
2656 for f in list:
2674 for f in list:
2657 if f in added:
2675 if f in added:
2658 continue # we never unlink added files on remove
2676 continue # we never unlink added files on remove
2659 rmdir = repo.ui.configbool(
2677 rmdir = repo.ui.configbool(
2660 b'experimental', b'removeemptydirs'
2678 b'experimental', b'removeemptydirs'
2661 )
2679 )
2662 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2680 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2663 repo[None].forget(list)
2681 repo[None].forget(list)
2664
2682
2665 if warn:
2683 if warn:
2666 for warning in warnings:
2684 for warning in warnings:
2667 ui.warn(warning)
2685 ui.warn(warning)
2668
2686
2669 return ret
2687 return ret
2670
2688
2671
2689
2672 def _catfmtneedsdata(fm):
2690 def _catfmtneedsdata(fm):
2673 return not fm.datahint() or b'data' in fm.datahint()
2691 return not fm.datahint() or b'data' in fm.datahint()
2674
2692
2675
2693
2676 def _updatecatformatter(fm, ctx, matcher, path, decode):
2694 def _updatecatformatter(fm, ctx, matcher, path, decode):
2677 """Hook for adding data to the formatter used by ``hg cat``.
2695 """Hook for adding data to the formatter used by ``hg cat``.
2678
2696
2679 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2697 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2680 this method first."""
2698 this method first."""
2681
2699
2682 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2700 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2683 # wasn't requested.
2701 # wasn't requested.
2684 data = b''
2702 data = b''
2685 if _catfmtneedsdata(fm):
2703 if _catfmtneedsdata(fm):
2686 data = ctx[path].data()
2704 data = ctx[path].data()
2687 if decode:
2705 if decode:
2688 data = ctx.repo().wwritedata(path, data)
2706 data = ctx.repo().wwritedata(path, data)
2689 fm.startitem()
2707 fm.startitem()
2690 fm.context(ctx=ctx)
2708 fm.context(ctx=ctx)
2691 fm.write(b'data', b'%s', data)
2709 fm.write(b'data', b'%s', data)
2692 fm.data(path=path)
2710 fm.data(path=path)
2693
2711
2694
2712
2695 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2713 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2696 err = 1
2714 err = 1
2697 opts = pycompat.byteskwargs(opts)
2715 opts = pycompat.byteskwargs(opts)
2698
2716
2699 def write(path):
2717 def write(path):
2700 filename = None
2718 filename = None
2701 if fntemplate:
2719 if fntemplate:
2702 filename = makefilename(
2720 filename = makefilename(
2703 ctx, fntemplate, pathname=os.path.join(prefix, path)
2721 ctx, fntemplate, pathname=os.path.join(prefix, path)
2704 )
2722 )
2705 # attempt to create the directory if it does not already exist
2723 # attempt to create the directory if it does not already exist
2706 try:
2724 try:
2707 os.makedirs(os.path.dirname(filename))
2725 os.makedirs(os.path.dirname(filename))
2708 except OSError:
2726 except OSError:
2709 pass
2727 pass
2710 with formatter.maybereopen(basefm, filename) as fm:
2728 with formatter.maybereopen(basefm, filename) as fm:
2711 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2729 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2712
2730
2713 # Automation often uses hg cat on single files, so special case it
2731 # Automation often uses hg cat on single files, so special case it
2714 # for performance to avoid the cost of parsing the manifest.
2732 # for performance to avoid the cost of parsing the manifest.
2715 if len(matcher.files()) == 1 and not matcher.anypats():
2733 if len(matcher.files()) == 1 and not matcher.anypats():
2716 file = matcher.files()[0]
2734 file = matcher.files()[0]
2717 mfl = repo.manifestlog
2735 mfl = repo.manifestlog
2718 mfnode = ctx.manifestnode()
2736 mfnode = ctx.manifestnode()
2719 try:
2737 try:
2720 if mfnode and mfl[mfnode].find(file)[0]:
2738 if mfnode and mfl[mfnode].find(file)[0]:
2721 if _catfmtneedsdata(basefm):
2739 if _catfmtneedsdata(basefm):
2722 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2740 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2723 write(file)
2741 write(file)
2724 return 0
2742 return 0
2725 except KeyError:
2743 except KeyError:
2726 pass
2744 pass
2727
2745
2728 if _catfmtneedsdata(basefm):
2746 if _catfmtneedsdata(basefm):
2729 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2747 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2730
2748
2731 for abs in ctx.walk(matcher):
2749 for abs in ctx.walk(matcher):
2732 write(abs)
2750 write(abs)
2733 err = 0
2751 err = 0
2734
2752
2735 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2753 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2736 for subpath in sorted(ctx.substate):
2754 for subpath in sorted(ctx.substate):
2737 sub = ctx.sub(subpath)
2755 sub = ctx.sub(subpath)
2738 try:
2756 try:
2739 submatch = matchmod.subdirmatcher(subpath, matcher)
2757 submatch = matchmod.subdirmatcher(subpath, matcher)
2740 subprefix = os.path.join(prefix, subpath)
2758 subprefix = os.path.join(prefix, subpath)
2741 if not sub.cat(
2759 if not sub.cat(
2742 submatch,
2760 submatch,
2743 basefm,
2761 basefm,
2744 fntemplate,
2762 fntemplate,
2745 subprefix,
2763 subprefix,
2746 **pycompat.strkwargs(opts)
2764 **pycompat.strkwargs(opts)
2747 ):
2765 ):
2748 err = 0
2766 err = 0
2749 except error.RepoLookupError:
2767 except error.RepoLookupError:
2750 ui.status(
2768 ui.status(
2751 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2769 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2752 )
2770 )
2753
2771
2754 return err
2772 return err
2755
2773
2756
2774
2757 def commit(ui, repo, commitfunc, pats, opts):
2775 def commit(ui, repo, commitfunc, pats, opts):
2758 '''commit the specified files or all outstanding changes'''
2776 '''commit the specified files or all outstanding changes'''
2759 date = opts.get(b'date')
2777 date = opts.get(b'date')
2760 if date:
2778 if date:
2761 opts[b'date'] = dateutil.parsedate(date)
2779 opts[b'date'] = dateutil.parsedate(date)
2762 message = logmessage(ui, opts)
2780 message = logmessage(ui, opts)
2763 matcher = scmutil.match(repo[None], pats, opts)
2781 matcher = scmutil.match(repo[None], pats, opts)
2764
2782
2765 dsguard = None
2783 dsguard = None
2766 # extract addremove carefully -- this function can be called from a command
2784 # extract addremove carefully -- this function can be called from a command
2767 # that doesn't support addremove
2785 # that doesn't support addremove
2768 if opts.get(b'addremove'):
2786 if opts.get(b'addremove'):
2769 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2787 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2770 with dsguard or util.nullcontextmanager():
2788 with dsguard or util.nullcontextmanager():
2771 if dsguard:
2789 if dsguard:
2772 relative = scmutil.anypats(pats, opts)
2790 relative = scmutil.anypats(pats, opts)
2773 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2791 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2774 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2792 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2775 raise error.Abort(
2793 raise error.Abort(
2776 _(b"failed to mark all new/missing files as added/removed")
2794 _(b"failed to mark all new/missing files as added/removed")
2777 )
2795 )
2778
2796
2779 return commitfunc(ui, repo, message, matcher, opts)
2797 return commitfunc(ui, repo, message, matcher, opts)
2780
2798
2781
2799
2782 def samefile(f, ctx1, ctx2):
2800 def samefile(f, ctx1, ctx2):
2783 if f in ctx1.manifest():
2801 if f in ctx1.manifest():
2784 a = ctx1.filectx(f)
2802 a = ctx1.filectx(f)
2785 if f in ctx2.manifest():
2803 if f in ctx2.manifest():
2786 b = ctx2.filectx(f)
2804 b = ctx2.filectx(f)
2787 return not a.cmp(b) and a.flags() == b.flags()
2805 return not a.cmp(b) and a.flags() == b.flags()
2788 else:
2806 else:
2789 return False
2807 return False
2790 else:
2808 else:
2791 return f not in ctx2.manifest()
2809 return f not in ctx2.manifest()
2792
2810
2793
2811
2794 def amend(ui, repo, old, extra, pats, opts):
2812 def amend(ui, repo, old, extra, pats, opts):
2795 # avoid cycle context -> subrepo -> cmdutil
2813 # avoid cycle context -> subrepo -> cmdutil
2796 from . import context
2814 from . import context
2797
2815
2798 # amend will reuse the existing user if not specified, but the obsolete
2816 # amend will reuse the existing user if not specified, but the obsolete
2799 # marker creation requires that the current user's name is specified.
2817 # marker creation requires that the current user's name is specified.
2800 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2818 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2801 ui.username() # raise exception if username not set
2819 ui.username() # raise exception if username not set
2802
2820
2803 ui.note(_(b'amending changeset %s\n') % old)
2821 ui.note(_(b'amending changeset %s\n') % old)
2804 base = old.p1()
2822 base = old.p1()
2805
2823
2806 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2824 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2807 # Participating changesets:
2825 # Participating changesets:
2808 #
2826 #
2809 # wctx o - workingctx that contains changes from working copy
2827 # wctx o - workingctx that contains changes from working copy
2810 # | to go into amending commit
2828 # | to go into amending commit
2811 # |
2829 # |
2812 # old o - changeset to amend
2830 # old o - changeset to amend
2813 # |
2831 # |
2814 # base o - first parent of the changeset to amend
2832 # base o - first parent of the changeset to amend
2815 wctx = repo[None]
2833 wctx = repo[None]
2816
2834
2817 # Copy to avoid mutating input
2835 # Copy to avoid mutating input
2818 extra = extra.copy()
2836 extra = extra.copy()
2819 # Update extra dict from amended commit (e.g. to preserve graft
2837 # Update extra dict from amended commit (e.g. to preserve graft
2820 # source)
2838 # source)
2821 extra.update(old.extra())
2839 extra.update(old.extra())
2822
2840
2823 # Also update it from the from the wctx
2841 # Also update it from the from the wctx
2824 extra.update(wctx.extra())
2842 extra.update(wctx.extra())
2825
2843
2826 # date-only change should be ignored?
2844 # date-only change should be ignored?
2827 datemaydiffer = resolve_commit_options(ui, opts)
2845 datemaydiffer = resolve_commit_options(ui, opts)
2828 opts = pycompat.byteskwargs(opts)
2846 opts = pycompat.byteskwargs(opts)
2829
2847
2830 date = old.date()
2848 date = old.date()
2831 if opts.get(b'date'):
2849 if opts.get(b'date'):
2832 date = dateutil.parsedate(opts.get(b'date'))
2850 date = dateutil.parsedate(opts.get(b'date'))
2833 user = opts.get(b'user') or old.user()
2851 user = opts.get(b'user') or old.user()
2834
2852
2835 if len(old.parents()) > 1:
2853 if len(old.parents()) > 1:
2836 # ctx.files() isn't reliable for merges, so fall back to the
2854 # ctx.files() isn't reliable for merges, so fall back to the
2837 # slower repo.status() method
2855 # slower repo.status() method
2838 st = base.status(old)
2856 st = base.status(old)
2839 files = set(st.modified) | set(st.added) | set(st.removed)
2857 files = set(st.modified) | set(st.added) | set(st.removed)
2840 else:
2858 else:
2841 files = set(old.files())
2859 files = set(old.files())
2842
2860
2843 # add/remove the files to the working copy if the "addremove" option
2861 # add/remove the files to the working copy if the "addremove" option
2844 # was specified.
2862 # was specified.
2845 matcher = scmutil.match(wctx, pats, opts)
2863 matcher = scmutil.match(wctx, pats, opts)
2846 relative = scmutil.anypats(pats, opts)
2864 relative = scmutil.anypats(pats, opts)
2847 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2865 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2848 if opts.get(b'addremove') and scmutil.addremove(
2866 if opts.get(b'addremove') and scmutil.addremove(
2849 repo, matcher, b"", uipathfn, opts
2867 repo, matcher, b"", uipathfn, opts
2850 ):
2868 ):
2851 raise error.Abort(
2869 raise error.Abort(
2852 _(b"failed to mark all new/missing files as added/removed")
2870 _(b"failed to mark all new/missing files as added/removed")
2853 )
2871 )
2854
2872
2855 # Check subrepos. This depends on in-place wctx._status update in
2873 # Check subrepos. This depends on in-place wctx._status update in
2856 # subrepo.precommit(). To minimize the risk of this hack, we do
2874 # subrepo.precommit(). To minimize the risk of this hack, we do
2857 # nothing if .hgsub does not exist.
2875 # nothing if .hgsub does not exist.
2858 if b'.hgsub' in wctx or b'.hgsub' in old:
2876 if b'.hgsub' in wctx or b'.hgsub' in old:
2859 subs, commitsubs, newsubstate = subrepoutil.precommit(
2877 subs, commitsubs, newsubstate = subrepoutil.precommit(
2860 ui, wctx, wctx._status, matcher
2878 ui, wctx, wctx._status, matcher
2861 )
2879 )
2862 # amend should abort if commitsubrepos is enabled
2880 # amend should abort if commitsubrepos is enabled
2863 assert not commitsubs
2881 assert not commitsubs
2864 if subs:
2882 if subs:
2865 subrepoutil.writestate(repo, newsubstate)
2883 subrepoutil.writestate(repo, newsubstate)
2866
2884
2867 ms = mergestatemod.mergestate.read(repo)
2885 ms = mergestatemod.mergestate.read(repo)
2868 mergeutil.checkunresolved(ms)
2886 mergeutil.checkunresolved(ms)
2869
2887
2870 filestoamend = {f for f in wctx.files() if matcher(f)}
2888 filestoamend = {f for f in wctx.files() if matcher(f)}
2871
2889
2872 changes = len(filestoamend) > 0
2890 changes = len(filestoamend) > 0
2873 if changes:
2891 if changes:
2874 # Recompute copies (avoid recording a -> b -> a)
2892 # Recompute copies (avoid recording a -> b -> a)
2875 copied = copies.pathcopies(base, wctx, matcher)
2893 copied = copies.pathcopies(base, wctx, matcher)
2876 if old.p2:
2894 if old.p2:
2877 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2895 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2878
2896
2879 # Prune files which were reverted by the updates: if old
2897 # Prune files which were reverted by the updates: if old
2880 # introduced file X and the file was renamed in the working
2898 # introduced file X and the file was renamed in the working
2881 # copy, then those two files are the same and
2899 # copy, then those two files are the same and
2882 # we can discard X from our list of files. Likewise if X
2900 # we can discard X from our list of files. Likewise if X
2883 # was removed, it's no longer relevant. If X is missing (aka
2901 # was removed, it's no longer relevant. If X is missing (aka
2884 # deleted), old X must be preserved.
2902 # deleted), old X must be preserved.
2885 files.update(filestoamend)
2903 files.update(filestoamend)
2886 files = [
2904 files = [
2887 f
2905 f
2888 for f in files
2906 for f in files
2889 if (f not in filestoamend or not samefile(f, wctx, base))
2907 if (f not in filestoamend or not samefile(f, wctx, base))
2890 ]
2908 ]
2891
2909
2892 def filectxfn(repo, ctx_, path):
2910 def filectxfn(repo, ctx_, path):
2893 try:
2911 try:
2894 # If the file being considered is not amongst the files
2912 # If the file being considered is not amongst the files
2895 # to be amended, we should return the file context from the
2913 # to be amended, we should return the file context from the
2896 # old changeset. This avoids issues when only some files in
2914 # old changeset. This avoids issues when only some files in
2897 # the working copy are being amended but there are also
2915 # the working copy are being amended but there are also
2898 # changes to other files from the old changeset.
2916 # changes to other files from the old changeset.
2899 if path not in filestoamend:
2917 if path not in filestoamend:
2900 return old.filectx(path)
2918 return old.filectx(path)
2901
2919
2902 # Return None for removed files.
2920 # Return None for removed files.
2903 if path in wctx.removed():
2921 if path in wctx.removed():
2904 return None
2922 return None
2905
2923
2906 fctx = wctx[path]
2924 fctx = wctx[path]
2907 flags = fctx.flags()
2925 flags = fctx.flags()
2908 mctx = context.memfilectx(
2926 mctx = context.memfilectx(
2909 repo,
2927 repo,
2910 ctx_,
2928 ctx_,
2911 fctx.path(),
2929 fctx.path(),
2912 fctx.data(),
2930 fctx.data(),
2913 islink=b'l' in flags,
2931 islink=b'l' in flags,
2914 isexec=b'x' in flags,
2932 isexec=b'x' in flags,
2915 copysource=copied.get(path),
2933 copysource=copied.get(path),
2916 )
2934 )
2917 return mctx
2935 return mctx
2918 except KeyError:
2936 except KeyError:
2919 return None
2937 return None
2920
2938
2921 else:
2939 else:
2922 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2940 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2923
2941
2924 # Use version of files as in the old cset
2942 # Use version of files as in the old cset
2925 def filectxfn(repo, ctx_, path):
2943 def filectxfn(repo, ctx_, path):
2926 try:
2944 try:
2927 return old.filectx(path)
2945 return old.filectx(path)
2928 except KeyError:
2946 except KeyError:
2929 return None
2947 return None
2930
2948
2931 # See if we got a message from -m or -l, if not, open the editor with
2949 # See if we got a message from -m or -l, if not, open the editor with
2932 # the message of the changeset to amend.
2950 # the message of the changeset to amend.
2933 message = logmessage(ui, opts)
2951 message = logmessage(ui, opts)
2934
2952
2935 editform = mergeeditform(old, b'commit.amend')
2953 editform = mergeeditform(old, b'commit.amend')
2936
2954
2937 if not message:
2955 if not message:
2938 message = old.description()
2956 message = old.description()
2939 # Default if message isn't provided and --edit is not passed is to
2957 # Default if message isn't provided and --edit is not passed is to
2940 # invoke editor, but allow --no-edit. If somehow we don't have any
2958 # invoke editor, but allow --no-edit. If somehow we don't have any
2941 # description, let's always start the editor.
2959 # description, let's always start the editor.
2942 doedit = not message or opts.get(b'edit') in [True, None]
2960 doedit = not message or opts.get(b'edit') in [True, None]
2943 else:
2961 else:
2944 # Default if message is provided is to not invoke editor, but allow
2962 # Default if message is provided is to not invoke editor, but allow
2945 # --edit.
2963 # --edit.
2946 doedit = opts.get(b'edit') is True
2964 doedit = opts.get(b'edit') is True
2947 editor = getcommiteditor(edit=doedit, editform=editform)
2965 editor = getcommiteditor(edit=doedit, editform=editform)
2948
2966
2949 pureextra = extra.copy()
2967 pureextra = extra.copy()
2950 extra[b'amend_source'] = old.hex()
2968 extra[b'amend_source'] = old.hex()
2951
2969
2952 new = context.memctx(
2970 new = context.memctx(
2953 repo,
2971 repo,
2954 parents=[base.node(), old.p2().node()],
2972 parents=[base.node(), old.p2().node()],
2955 text=message,
2973 text=message,
2956 files=files,
2974 files=files,
2957 filectxfn=filectxfn,
2975 filectxfn=filectxfn,
2958 user=user,
2976 user=user,
2959 date=date,
2977 date=date,
2960 extra=extra,
2978 extra=extra,
2961 editor=editor,
2979 editor=editor,
2962 )
2980 )
2963
2981
2964 newdesc = changelog.stripdesc(new.description())
2982 newdesc = changelog.stripdesc(new.description())
2965 if (
2983 if (
2966 (not changes)
2984 (not changes)
2967 and newdesc == old.description()
2985 and newdesc == old.description()
2968 and user == old.user()
2986 and user == old.user()
2969 and (date == old.date() or datemaydiffer)
2987 and (date == old.date() or datemaydiffer)
2970 and pureextra == old.extra()
2988 and pureextra == old.extra()
2971 ):
2989 ):
2972 # nothing changed. continuing here would create a new node
2990 # nothing changed. continuing here would create a new node
2973 # anyway because of the amend_source noise.
2991 # anyway because of the amend_source noise.
2974 #
2992 #
2975 # This not what we expect from amend.
2993 # This not what we expect from amend.
2976 return old.node()
2994 return old.node()
2977
2995
2978 commitphase = None
2996 commitphase = None
2979 if opts.get(b'secret'):
2997 if opts.get(b'secret'):
2980 commitphase = phases.secret
2998 commitphase = phases.secret
2981 newid = repo.commitctx(new)
2999 newid = repo.commitctx(new)
2982 ms.reset()
3000 ms.reset()
2983
3001
2984 with repo.dirstate.parentchange():
3002 with repo.dirstate.parentchange():
2985 # Reroute the working copy parent to the new changeset
3003 # Reroute the working copy parent to the new changeset
2986 repo.setparents(newid, repo.nullid)
3004 repo.setparents(newid, repo.nullid)
2987
3005
2988 # Fixing the dirstate because localrepo.commitctx does not update
3006 # Fixing the dirstate because localrepo.commitctx does not update
2989 # it. This is rather convenient because we did not need to update
3007 # it. This is rather convenient because we did not need to update
2990 # the dirstate for all the files in the new commit which commitctx
3008 # the dirstate for all the files in the new commit which commitctx
2991 # could have done if it updated the dirstate. Now, we can
3009 # could have done if it updated the dirstate. Now, we can
2992 # selectively update the dirstate only for the amended files.
3010 # selectively update the dirstate only for the amended files.
2993 dirstate = repo.dirstate
3011 dirstate = repo.dirstate
2994
3012
2995 # Update the state of the files which were added and modified in the
3013 # Update the state of the files which were added and modified in the
2996 # amend to "normal" in the dirstate. We need to use "normallookup" since
3014 # amend to "normal" in the dirstate. We need to use "normallookup" since
2997 # the files may have changed since the command started; using "normal"
3015 # the files may have changed since the command started; using "normal"
2998 # would mark them as clean but with uncommitted contents.
3016 # would mark them as clean but with uncommitted contents.
2999 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3017 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3000 for f in normalfiles:
3018 for f in normalfiles:
3001 dirstate.update_file(
3019 dirstate.update_file(
3002 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
3020 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
3003 )
3021 )
3004
3022
3005 # Update the state of files which were removed in the amend
3023 # Update the state of files which were removed in the amend
3006 # to "removed" in the dirstate.
3024 # to "removed" in the dirstate.
3007 removedfiles = set(wctx.removed()) & filestoamend
3025 removedfiles = set(wctx.removed()) & filestoamend
3008 for f in removedfiles:
3026 for f in removedfiles:
3009 dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
3027 dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
3010
3028
3011 mapping = {old.node(): (newid,)}
3029 mapping = {old.node(): (newid,)}
3012 obsmetadata = None
3030 obsmetadata = None
3013 if opts.get(b'note'):
3031 if opts.get(b'note'):
3014 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3032 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3015 backup = ui.configbool(b'rewrite', b'backup-bundle')
3033 backup = ui.configbool(b'rewrite', b'backup-bundle')
3016 scmutil.cleanupnodes(
3034 scmutil.cleanupnodes(
3017 repo,
3035 repo,
3018 mapping,
3036 mapping,
3019 b'amend',
3037 b'amend',
3020 metadata=obsmetadata,
3038 metadata=obsmetadata,
3021 fixphase=True,
3039 fixphase=True,
3022 targetphase=commitphase,
3040 targetphase=commitphase,
3023 backup=backup,
3041 backup=backup,
3024 )
3042 )
3025
3043
3026 return newid
3044 return newid
3027
3045
3028
3046
3029 def commiteditor(repo, ctx, subs, editform=b''):
3047 def commiteditor(repo, ctx, subs, editform=b''):
3030 if ctx.description():
3048 if ctx.description():
3031 return ctx.description()
3049 return ctx.description()
3032 return commitforceeditor(
3050 return commitforceeditor(
3033 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3051 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3034 )
3052 )
3035
3053
3036
3054
3037 def commitforceeditor(
3055 def commitforceeditor(
3038 repo,
3056 repo,
3039 ctx,
3057 ctx,
3040 subs,
3058 subs,
3041 finishdesc=None,
3059 finishdesc=None,
3042 extramsg=None,
3060 extramsg=None,
3043 editform=b'',
3061 editform=b'',
3044 unchangedmessagedetection=False,
3062 unchangedmessagedetection=False,
3045 ):
3063 ):
3046 if not extramsg:
3064 if not extramsg:
3047 extramsg = _(b"Leave message empty to abort commit.")
3065 extramsg = _(b"Leave message empty to abort commit.")
3048
3066
3049 forms = [e for e in editform.split(b'.') if e]
3067 forms = [e for e in editform.split(b'.') if e]
3050 forms.insert(0, b'changeset')
3068 forms.insert(0, b'changeset')
3051 templatetext = None
3069 templatetext = None
3052 while forms:
3070 while forms:
3053 ref = b'.'.join(forms)
3071 ref = b'.'.join(forms)
3054 if repo.ui.config(b'committemplate', ref):
3072 if repo.ui.config(b'committemplate', ref):
3055 templatetext = committext = buildcommittemplate(
3073 templatetext = committext = buildcommittemplate(
3056 repo, ctx, subs, extramsg, ref
3074 repo, ctx, subs, extramsg, ref
3057 )
3075 )
3058 break
3076 break
3059 forms.pop()
3077 forms.pop()
3060 else:
3078 else:
3061 committext = buildcommittext(repo, ctx, subs, extramsg)
3079 committext = buildcommittext(repo, ctx, subs, extramsg)
3062
3080
3063 # run editor in the repository root
3081 # run editor in the repository root
3064 olddir = encoding.getcwd()
3082 olddir = encoding.getcwd()
3065 os.chdir(repo.root)
3083 os.chdir(repo.root)
3066
3084
3067 # make in-memory changes visible to external process
3085 # make in-memory changes visible to external process
3068 tr = repo.currenttransaction()
3086 tr = repo.currenttransaction()
3069 repo.dirstate.write(tr)
3087 repo.dirstate.write(tr)
3070 pending = tr and tr.writepending() and repo.root
3088 pending = tr and tr.writepending() and repo.root
3071
3089
3072 editortext = repo.ui.edit(
3090 editortext = repo.ui.edit(
3073 committext,
3091 committext,
3074 ctx.user(),
3092 ctx.user(),
3075 ctx.extra(),
3093 ctx.extra(),
3076 editform=editform,
3094 editform=editform,
3077 pending=pending,
3095 pending=pending,
3078 repopath=repo.path,
3096 repopath=repo.path,
3079 action=b'commit',
3097 action=b'commit',
3080 )
3098 )
3081 text = editortext
3099 text = editortext
3082
3100
3083 # strip away anything below this special string (used for editors that want
3101 # strip away anything below this special string (used for editors that want
3084 # to display the diff)
3102 # to display the diff)
3085 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3103 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3086 if stripbelow:
3104 if stripbelow:
3087 text = text[: stripbelow.start()]
3105 text = text[: stripbelow.start()]
3088
3106
3089 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3107 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3090 os.chdir(olddir)
3108 os.chdir(olddir)
3091
3109
3092 if finishdesc:
3110 if finishdesc:
3093 text = finishdesc(text)
3111 text = finishdesc(text)
3094 if not text.strip():
3112 if not text.strip():
3095 raise error.InputError(_(b"empty commit message"))
3113 raise error.InputError(_(b"empty commit message"))
3096 if unchangedmessagedetection and editortext == templatetext:
3114 if unchangedmessagedetection and editortext == templatetext:
3097 raise error.InputError(_(b"commit message unchanged"))
3115 raise error.InputError(_(b"commit message unchanged"))
3098
3116
3099 return text
3117 return text
3100
3118
3101
3119
3102 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3120 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3103 ui = repo.ui
3121 ui = repo.ui
3104 spec = formatter.reference_templatespec(ref)
3122 spec = formatter.reference_templatespec(ref)
3105 t = logcmdutil.changesettemplater(ui, repo, spec)
3123 t = logcmdutil.changesettemplater(ui, repo, spec)
3106 t.t.cache.update(
3124 t.t.cache.update(
3107 (k, templater.unquotestring(v))
3125 (k, templater.unquotestring(v))
3108 for k, v in repo.ui.configitems(b'committemplate')
3126 for k, v in repo.ui.configitems(b'committemplate')
3109 )
3127 )
3110
3128
3111 if not extramsg:
3129 if not extramsg:
3112 extramsg = b'' # ensure that extramsg is string
3130 extramsg = b'' # ensure that extramsg is string
3113
3131
3114 ui.pushbuffer()
3132 ui.pushbuffer()
3115 t.show(ctx, extramsg=extramsg)
3133 t.show(ctx, extramsg=extramsg)
3116 return ui.popbuffer()
3134 return ui.popbuffer()
3117
3135
3118
3136
3119 def hgprefix(msg):
3137 def hgprefix(msg):
3120 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3138 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3121
3139
3122
3140
3123 def buildcommittext(repo, ctx, subs, extramsg):
3141 def buildcommittext(repo, ctx, subs, extramsg):
3124 edittext = []
3142 edittext = []
3125 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3143 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3126 if ctx.description():
3144 if ctx.description():
3127 edittext.append(ctx.description())
3145 edittext.append(ctx.description())
3128 edittext.append(b"")
3146 edittext.append(b"")
3129 edittext.append(b"") # Empty line between message and comments.
3147 edittext.append(b"") # Empty line between message and comments.
3130 edittext.append(
3148 edittext.append(
3131 hgprefix(
3149 hgprefix(
3132 _(
3150 _(
3133 b"Enter commit message."
3151 b"Enter commit message."
3134 b" Lines beginning with 'HG:' are removed."
3152 b" Lines beginning with 'HG:' are removed."
3135 )
3153 )
3136 )
3154 )
3137 )
3155 )
3138 edittext.append(hgprefix(extramsg))
3156 edittext.append(hgprefix(extramsg))
3139 edittext.append(b"HG: --")
3157 edittext.append(b"HG: --")
3140 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3158 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3141 if ctx.p2():
3159 if ctx.p2():
3142 edittext.append(hgprefix(_(b"branch merge")))
3160 edittext.append(hgprefix(_(b"branch merge")))
3143 if ctx.branch():
3161 if ctx.branch():
3144 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3162 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3145 if bookmarks.isactivewdirparent(repo):
3163 if bookmarks.isactivewdirparent(repo):
3146 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3164 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3147 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3165 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3148 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3166 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3149 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3167 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3150 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3168 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3151 if not added and not modified and not removed:
3169 if not added and not modified and not removed:
3152 edittext.append(hgprefix(_(b"no files changed")))
3170 edittext.append(hgprefix(_(b"no files changed")))
3153 edittext.append(b"")
3171 edittext.append(b"")
3154
3172
3155 return b"\n".join(edittext)
3173 return b"\n".join(edittext)
3156
3174
3157
3175
3158 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3176 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3159 if opts is None:
3177 if opts is None:
3160 opts = {}
3178 opts = {}
3161 ctx = repo[node]
3179 ctx = repo[node]
3162 parents = ctx.parents()
3180 parents = ctx.parents()
3163
3181
3164 if tip is not None and repo.changelog.tip() == tip:
3182 if tip is not None and repo.changelog.tip() == tip:
3165 # avoid reporting something like "committed new head" when
3183 # avoid reporting something like "committed new head" when
3166 # recommitting old changesets, and issue a helpful warning
3184 # recommitting old changesets, and issue a helpful warning
3167 # for most instances
3185 # for most instances
3168 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3186 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3169 elif (
3187 elif (
3170 not opts.get(b'amend')
3188 not opts.get(b'amend')
3171 and bheads
3189 and bheads
3172 and node not in bheads
3190 and node not in bheads
3173 and not any(
3191 and not any(
3174 p.node() in bheads and p.branch() == branch for p in parents
3192 p.node() in bheads and p.branch() == branch for p in parents
3175 )
3193 )
3176 ):
3194 ):
3177 repo.ui.status(_(b'created new head\n'))
3195 repo.ui.status(_(b'created new head\n'))
3178 # The message is not printed for initial roots. For the other
3196 # The message is not printed for initial roots. For the other
3179 # changesets, it is printed in the following situations:
3197 # changesets, it is printed in the following situations:
3180 #
3198 #
3181 # Par column: for the 2 parents with ...
3199 # Par column: for the 2 parents with ...
3182 # N: null or no parent
3200 # N: null or no parent
3183 # B: parent is on another named branch
3201 # B: parent is on another named branch
3184 # C: parent is a regular non head changeset
3202 # C: parent is a regular non head changeset
3185 # H: parent was a branch head of the current branch
3203 # H: parent was a branch head of the current branch
3186 # Msg column: whether we print "created new head" message
3204 # Msg column: whether we print "created new head" message
3187 # In the following, it is assumed that there already exists some
3205 # In the following, it is assumed that there already exists some
3188 # initial branch heads of the current branch, otherwise nothing is
3206 # initial branch heads of the current branch, otherwise nothing is
3189 # printed anyway.
3207 # printed anyway.
3190 #
3208 #
3191 # Par Msg Comment
3209 # Par Msg Comment
3192 # N N y additional topo root
3210 # N N y additional topo root
3193 #
3211 #
3194 # B N y additional branch root
3212 # B N y additional branch root
3195 # C N y additional topo head
3213 # C N y additional topo head
3196 # H N n usual case
3214 # H N n usual case
3197 #
3215 #
3198 # B B y weird additional branch root
3216 # B B y weird additional branch root
3199 # C B y branch merge
3217 # C B y branch merge
3200 # H B n merge with named branch
3218 # H B n merge with named branch
3201 #
3219 #
3202 # C C y additional head from merge
3220 # C C y additional head from merge
3203 # C H n merge with a head
3221 # C H n merge with a head
3204 #
3222 #
3205 # H H n head merge: head count decreases
3223 # H H n head merge: head count decreases
3206
3224
3207 if not opts.get(b'close_branch'):
3225 if not opts.get(b'close_branch'):
3208 for r in parents:
3226 for r in parents:
3209 if r.closesbranch() and r.branch() == branch:
3227 if r.closesbranch() and r.branch() == branch:
3210 repo.ui.status(
3228 repo.ui.status(
3211 _(b'reopening closed branch head %d\n') % r.rev()
3229 _(b'reopening closed branch head %d\n') % r.rev()
3212 )
3230 )
3213
3231
3214 if repo.ui.debugflag:
3232 if repo.ui.debugflag:
3215 repo.ui.write(
3233 repo.ui.write(
3216 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3234 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3217 )
3235 )
3218 elif repo.ui.verbose:
3236 elif repo.ui.verbose:
3219 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3237 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3220
3238
3221
3239
3222 def postcommitstatus(repo, pats, opts):
3240 def postcommitstatus(repo, pats, opts):
3223 return repo.status(match=scmutil.match(repo[None], pats, opts))
3241 return repo.status(match=scmutil.match(repo[None], pats, opts))
3224
3242
3225
3243
3226 def revert(ui, repo, ctx, *pats, **opts):
3244 def revert(ui, repo, ctx, *pats, **opts):
3227 opts = pycompat.byteskwargs(opts)
3245 opts = pycompat.byteskwargs(opts)
3228 parent, p2 = repo.dirstate.parents()
3246 parent, p2 = repo.dirstate.parents()
3229 node = ctx.node()
3247 node = ctx.node()
3230
3248
3231 mf = ctx.manifest()
3249 mf = ctx.manifest()
3232 if node == p2:
3250 if node == p2:
3233 parent = p2
3251 parent = p2
3234
3252
3235 # need all matching names in dirstate and manifest of target rev,
3253 # need all matching names in dirstate and manifest of target rev,
3236 # so have to walk both. do not print errors if files exist in one
3254 # so have to walk both. do not print errors if files exist in one
3237 # but not other. in both cases, filesets should be evaluated against
3255 # but not other. in both cases, filesets should be evaluated against
3238 # workingctx to get consistent result (issue4497). this means 'set:**'
3256 # workingctx to get consistent result (issue4497). this means 'set:**'
3239 # cannot be used to select missing files from target rev.
3257 # cannot be used to select missing files from target rev.
3240
3258
3241 # `names` is a mapping for all elements in working copy and target revision
3259 # `names` is a mapping for all elements in working copy and target revision
3242 # The mapping is in the form:
3260 # The mapping is in the form:
3243 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3261 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3244 names = {}
3262 names = {}
3245 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3263 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3246
3264
3247 with repo.wlock():
3265 with repo.wlock():
3248 ## filling of the `names` mapping
3266 ## filling of the `names` mapping
3249 # walk dirstate to fill `names`
3267 # walk dirstate to fill `names`
3250
3268
3251 interactive = opts.get(b'interactive', False)
3269 interactive = opts.get(b'interactive', False)
3252 wctx = repo[None]
3270 wctx = repo[None]
3253 m = scmutil.match(wctx, pats, opts)
3271 m = scmutil.match(wctx, pats, opts)
3254
3272
3255 # we'll need this later
3273 # we'll need this later
3256 targetsubs = sorted(s for s in wctx.substate if m(s))
3274 targetsubs = sorted(s for s in wctx.substate if m(s))
3257
3275
3258 if not m.always():
3276 if not m.always():
3259 matcher = matchmod.badmatch(m, lambda x, y: False)
3277 matcher = matchmod.badmatch(m, lambda x, y: False)
3260 for abs in wctx.walk(matcher):
3278 for abs in wctx.walk(matcher):
3261 names[abs] = m.exact(abs)
3279 names[abs] = m.exact(abs)
3262
3280
3263 # walk target manifest to fill `names`
3281 # walk target manifest to fill `names`
3264
3282
3265 def badfn(path, msg):
3283 def badfn(path, msg):
3266 if path in names:
3284 if path in names:
3267 return
3285 return
3268 if path in ctx.substate:
3286 if path in ctx.substate:
3269 return
3287 return
3270 path_ = path + b'/'
3288 path_ = path + b'/'
3271 for f in names:
3289 for f in names:
3272 if f.startswith(path_):
3290 if f.startswith(path_):
3273 return
3291 return
3274 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3292 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3275
3293
3276 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3294 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3277 if abs not in names:
3295 if abs not in names:
3278 names[abs] = m.exact(abs)
3296 names[abs] = m.exact(abs)
3279
3297
3280 # Find status of all file in `names`.
3298 # Find status of all file in `names`.
3281 m = scmutil.matchfiles(repo, names)
3299 m = scmutil.matchfiles(repo, names)
3282
3300
3283 changes = repo.status(
3301 changes = repo.status(
3284 node1=node, match=m, unknown=True, ignored=True, clean=True
3302 node1=node, match=m, unknown=True, ignored=True, clean=True
3285 )
3303 )
3286 else:
3304 else:
3287 changes = repo.status(node1=node, match=m)
3305 changes = repo.status(node1=node, match=m)
3288 for kind in changes:
3306 for kind in changes:
3289 for abs in kind:
3307 for abs in kind:
3290 names[abs] = m.exact(abs)
3308 names[abs] = m.exact(abs)
3291
3309
3292 m = scmutil.matchfiles(repo, names)
3310 m = scmutil.matchfiles(repo, names)
3293
3311
3294 modified = set(changes.modified)
3312 modified = set(changes.modified)
3295 added = set(changes.added)
3313 added = set(changes.added)
3296 removed = set(changes.removed)
3314 removed = set(changes.removed)
3297 _deleted = set(changes.deleted)
3315 _deleted = set(changes.deleted)
3298 unknown = set(changes.unknown)
3316 unknown = set(changes.unknown)
3299 unknown.update(changes.ignored)
3317 unknown.update(changes.ignored)
3300 clean = set(changes.clean)
3318 clean = set(changes.clean)
3301 modadded = set()
3319 modadded = set()
3302
3320
3303 # We need to account for the state of the file in the dirstate,
3321 # We need to account for the state of the file in the dirstate,
3304 # even when we revert against something else than parent. This will
3322 # even when we revert against something else than parent. This will
3305 # slightly alter the behavior of revert (doing back up or not, delete
3323 # slightly alter the behavior of revert (doing back up or not, delete
3306 # or just forget etc).
3324 # or just forget etc).
3307 if parent == node:
3325 if parent == node:
3308 dsmodified = modified
3326 dsmodified = modified
3309 dsadded = added
3327 dsadded = added
3310 dsremoved = removed
3328 dsremoved = removed
3311 # store all local modifications, useful later for rename detection
3329 # store all local modifications, useful later for rename detection
3312 localchanges = dsmodified | dsadded
3330 localchanges = dsmodified | dsadded
3313 modified, added, removed = set(), set(), set()
3331 modified, added, removed = set(), set(), set()
3314 else:
3332 else:
3315 changes = repo.status(node1=parent, match=m)
3333 changes = repo.status(node1=parent, match=m)
3316 dsmodified = set(changes.modified)
3334 dsmodified = set(changes.modified)
3317 dsadded = set(changes.added)
3335 dsadded = set(changes.added)
3318 dsremoved = set(changes.removed)
3336 dsremoved = set(changes.removed)
3319 # store all local modifications, useful later for rename detection
3337 # store all local modifications, useful later for rename detection
3320 localchanges = dsmodified | dsadded
3338 localchanges = dsmodified | dsadded
3321
3339
3322 # only take into account for removes between wc and target
3340 # only take into account for removes between wc and target
3323 clean |= dsremoved - removed
3341 clean |= dsremoved - removed
3324 dsremoved &= removed
3342 dsremoved &= removed
3325 # distinct between dirstate remove and other
3343 # distinct between dirstate remove and other
3326 removed -= dsremoved
3344 removed -= dsremoved
3327
3345
3328 modadded = added & dsmodified
3346 modadded = added & dsmodified
3329 added -= modadded
3347 added -= modadded
3330
3348
3331 # tell newly modified apart.
3349 # tell newly modified apart.
3332 dsmodified &= modified
3350 dsmodified &= modified
3333 dsmodified |= modified & dsadded # dirstate added may need backup
3351 dsmodified |= modified & dsadded # dirstate added may need backup
3334 modified -= dsmodified
3352 modified -= dsmodified
3335
3353
3336 # We need to wait for some post-processing to update this set
3354 # We need to wait for some post-processing to update this set
3337 # before making the distinction. The dirstate will be used for
3355 # before making the distinction. The dirstate will be used for
3338 # that purpose.
3356 # that purpose.
3339 dsadded = added
3357 dsadded = added
3340
3358
3341 # in case of merge, files that are actually added can be reported as
3359 # in case of merge, files that are actually added can be reported as
3342 # modified, we need to post process the result
3360 # modified, we need to post process the result
3343 if p2 != repo.nullid:
3361 if p2 != repo.nullid:
3344 mergeadd = set(dsmodified)
3362 mergeadd = set(dsmodified)
3345 for path in dsmodified:
3363 for path in dsmodified:
3346 if path in mf:
3364 if path in mf:
3347 mergeadd.remove(path)
3365 mergeadd.remove(path)
3348 dsadded |= mergeadd
3366 dsadded |= mergeadd
3349 dsmodified -= mergeadd
3367 dsmodified -= mergeadd
3350
3368
3351 # if f is a rename, update `names` to also revert the source
3369 # if f is a rename, update `names` to also revert the source
3352 for f in localchanges:
3370 for f in localchanges:
3353 src = repo.dirstate.copied(f)
3371 src = repo.dirstate.copied(f)
3354 # XXX should we check for rename down to target node?
3372 # XXX should we check for rename down to target node?
3355 if (
3373 if (
3356 src
3374 src
3357 and src not in names
3375 and src not in names
3358 and repo.dirstate.get_entry(src).removed
3376 and repo.dirstate.get_entry(src).removed
3359 ):
3377 ):
3360 dsremoved.add(src)
3378 dsremoved.add(src)
3361 names[src] = True
3379 names[src] = True
3362
3380
3363 # determine the exact nature of the deleted changesets
3381 # determine the exact nature of the deleted changesets
3364 deladded = set(_deleted)
3382 deladded = set(_deleted)
3365 for path in _deleted:
3383 for path in _deleted:
3366 if path in mf:
3384 if path in mf:
3367 deladded.remove(path)
3385 deladded.remove(path)
3368 deleted = _deleted - deladded
3386 deleted = _deleted - deladded
3369
3387
3370 # distinguish between file to forget and the other
3388 # distinguish between file to forget and the other
3371 added = set()
3389 added = set()
3372 for abs in dsadded:
3390 for abs in dsadded:
3373 if not repo.dirstate.get_entry(abs).added:
3391 if not repo.dirstate.get_entry(abs).added:
3374 added.add(abs)
3392 added.add(abs)
3375 dsadded -= added
3393 dsadded -= added
3376
3394
3377 for abs in deladded:
3395 for abs in deladded:
3378 if repo.dirstate.get_entry(abs).added:
3396 if repo.dirstate.get_entry(abs).added:
3379 dsadded.add(abs)
3397 dsadded.add(abs)
3380 deladded -= dsadded
3398 deladded -= dsadded
3381
3399
3382 # For files marked as removed, we check if an unknown file is present at
3400 # For files marked as removed, we check if an unknown file is present at
3383 # the same path. If a such file exists it may need to be backed up.
3401 # the same path. If a such file exists it may need to be backed up.
3384 # Making the distinction at this stage helps have simpler backup
3402 # Making the distinction at this stage helps have simpler backup
3385 # logic.
3403 # logic.
3386 removunk = set()
3404 removunk = set()
3387 for abs in removed:
3405 for abs in removed:
3388 target = repo.wjoin(abs)
3406 target = repo.wjoin(abs)
3389 if os.path.lexists(target):
3407 if os.path.lexists(target):
3390 removunk.add(abs)
3408 removunk.add(abs)
3391 removed -= removunk
3409 removed -= removunk
3392
3410
3393 dsremovunk = set()
3411 dsremovunk = set()
3394 for abs in dsremoved:
3412 for abs in dsremoved:
3395 target = repo.wjoin(abs)
3413 target = repo.wjoin(abs)
3396 if os.path.lexists(target):
3414 if os.path.lexists(target):
3397 dsremovunk.add(abs)
3415 dsremovunk.add(abs)
3398 dsremoved -= dsremovunk
3416 dsremoved -= dsremovunk
3399
3417
3400 # action to be actually performed by revert
3418 # action to be actually performed by revert
3401 # (<list of file>, message>) tuple
3419 # (<list of file>, message>) tuple
3402 actions = {
3420 actions = {
3403 b'revert': ([], _(b'reverting %s\n')),
3421 b'revert': ([], _(b'reverting %s\n')),
3404 b'add': ([], _(b'adding %s\n')),
3422 b'add': ([], _(b'adding %s\n')),
3405 b'remove': ([], _(b'removing %s\n')),
3423 b'remove': ([], _(b'removing %s\n')),
3406 b'drop': ([], _(b'removing %s\n')),
3424 b'drop': ([], _(b'removing %s\n')),
3407 b'forget': ([], _(b'forgetting %s\n')),
3425 b'forget': ([], _(b'forgetting %s\n')),
3408 b'undelete': ([], _(b'undeleting %s\n')),
3426 b'undelete': ([], _(b'undeleting %s\n')),
3409 b'noop': (None, _(b'no changes needed to %s\n')),
3427 b'noop': (None, _(b'no changes needed to %s\n')),
3410 b'unknown': (None, _(b'file not managed: %s\n')),
3428 b'unknown': (None, _(b'file not managed: %s\n')),
3411 }
3429 }
3412
3430
3413 # "constant" that convey the backup strategy.
3431 # "constant" that convey the backup strategy.
3414 # All set to `discard` if `no-backup` is set do avoid checking
3432 # All set to `discard` if `no-backup` is set do avoid checking
3415 # no_backup lower in the code.
3433 # no_backup lower in the code.
3416 # These values are ordered for comparison purposes
3434 # These values are ordered for comparison purposes
3417 backupinteractive = 3 # do backup if interactively modified
3435 backupinteractive = 3 # do backup if interactively modified
3418 backup = 2 # unconditionally do backup
3436 backup = 2 # unconditionally do backup
3419 check = 1 # check if the existing file differs from target
3437 check = 1 # check if the existing file differs from target
3420 discard = 0 # never do backup
3438 discard = 0 # never do backup
3421 if opts.get(b'no_backup'):
3439 if opts.get(b'no_backup'):
3422 backupinteractive = backup = check = discard
3440 backupinteractive = backup = check = discard
3423 if interactive:
3441 if interactive:
3424 dsmodifiedbackup = backupinteractive
3442 dsmodifiedbackup = backupinteractive
3425 else:
3443 else:
3426 dsmodifiedbackup = backup
3444 dsmodifiedbackup = backup
3427 tobackup = set()
3445 tobackup = set()
3428
3446
3429 backupanddel = actions[b'remove']
3447 backupanddel = actions[b'remove']
3430 if not opts.get(b'no_backup'):
3448 if not opts.get(b'no_backup'):
3431 backupanddel = actions[b'drop']
3449 backupanddel = actions[b'drop']
3432
3450
3433 disptable = (
3451 disptable = (
3434 # dispatch table:
3452 # dispatch table:
3435 # file state
3453 # file state
3436 # action
3454 # action
3437 # make backup
3455 # make backup
3438 ## Sets that results that will change file on disk
3456 ## Sets that results that will change file on disk
3439 # Modified compared to target, no local change
3457 # Modified compared to target, no local change
3440 (modified, actions[b'revert'], discard),
3458 (modified, actions[b'revert'], discard),
3441 # Modified compared to target, but local file is deleted
3459 # Modified compared to target, but local file is deleted
3442 (deleted, actions[b'revert'], discard),
3460 (deleted, actions[b'revert'], discard),
3443 # Modified compared to target, local change
3461 # Modified compared to target, local change
3444 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3462 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3445 # Added since target
3463 # Added since target
3446 (added, actions[b'remove'], discard),
3464 (added, actions[b'remove'], discard),
3447 # Added in working directory
3465 # Added in working directory
3448 (dsadded, actions[b'forget'], discard),
3466 (dsadded, actions[b'forget'], discard),
3449 # Added since target, have local modification
3467 # Added since target, have local modification
3450 (modadded, backupanddel, backup),
3468 (modadded, backupanddel, backup),
3451 # Added since target but file is missing in working directory
3469 # Added since target but file is missing in working directory
3452 (deladded, actions[b'drop'], discard),
3470 (deladded, actions[b'drop'], discard),
3453 # Removed since target, before working copy parent
3471 # Removed since target, before working copy parent
3454 (removed, actions[b'add'], discard),
3472 (removed, actions[b'add'], discard),
3455 # Same as `removed` but an unknown file exists at the same path
3473 # Same as `removed` but an unknown file exists at the same path
3456 (removunk, actions[b'add'], check),
3474 (removunk, actions[b'add'], check),
3457 # Removed since targe, marked as such in working copy parent
3475 # Removed since targe, marked as such in working copy parent
3458 (dsremoved, actions[b'undelete'], discard),
3476 (dsremoved, actions[b'undelete'], discard),
3459 # Same as `dsremoved` but an unknown file exists at the same path
3477 # Same as `dsremoved` but an unknown file exists at the same path
3460 (dsremovunk, actions[b'undelete'], check),
3478 (dsremovunk, actions[b'undelete'], check),
3461 ## the following sets does not result in any file changes
3479 ## the following sets does not result in any file changes
3462 # File with no modification
3480 # File with no modification
3463 (clean, actions[b'noop'], discard),
3481 (clean, actions[b'noop'], discard),
3464 # Existing file, not tracked anywhere
3482 # Existing file, not tracked anywhere
3465 (unknown, actions[b'unknown'], discard),
3483 (unknown, actions[b'unknown'], discard),
3466 )
3484 )
3467
3485
3468 for abs, exact in sorted(names.items()):
3486 for abs, exact in sorted(names.items()):
3469 # target file to be touch on disk (relative to cwd)
3487 # target file to be touch on disk (relative to cwd)
3470 target = repo.wjoin(abs)
3488 target = repo.wjoin(abs)
3471 # search the entry in the dispatch table.
3489 # search the entry in the dispatch table.
3472 # if the file is in any of these sets, it was touched in the working
3490 # if the file is in any of these sets, it was touched in the working
3473 # directory parent and we are sure it needs to be reverted.
3491 # directory parent and we are sure it needs to be reverted.
3474 for table, (xlist, msg), dobackup in disptable:
3492 for table, (xlist, msg), dobackup in disptable:
3475 if abs not in table:
3493 if abs not in table:
3476 continue
3494 continue
3477 if xlist is not None:
3495 if xlist is not None:
3478 xlist.append(abs)
3496 xlist.append(abs)
3479 if dobackup:
3497 if dobackup:
3480 # If in interactive mode, don't automatically create
3498 # If in interactive mode, don't automatically create
3481 # .orig files (issue4793)
3499 # .orig files (issue4793)
3482 if dobackup == backupinteractive:
3500 if dobackup == backupinteractive:
3483 tobackup.add(abs)
3501 tobackup.add(abs)
3484 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3502 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3485 absbakname = scmutil.backuppath(ui, repo, abs)
3503 absbakname = scmutil.backuppath(ui, repo, abs)
3486 bakname = os.path.relpath(
3504 bakname = os.path.relpath(
3487 absbakname, start=repo.root
3505 absbakname, start=repo.root
3488 )
3506 )
3489 ui.note(
3507 ui.note(
3490 _(b'saving current version of %s as %s\n')
3508 _(b'saving current version of %s as %s\n')
3491 % (uipathfn(abs), uipathfn(bakname))
3509 % (uipathfn(abs), uipathfn(bakname))
3492 )
3510 )
3493 if not opts.get(b'dry_run'):
3511 if not opts.get(b'dry_run'):
3494 if interactive:
3512 if interactive:
3495 util.copyfile(target, absbakname)
3513 util.copyfile(target, absbakname)
3496 else:
3514 else:
3497 util.rename(target, absbakname)
3515 util.rename(target, absbakname)
3498 if opts.get(b'dry_run'):
3516 if opts.get(b'dry_run'):
3499 if ui.verbose or not exact:
3517 if ui.verbose or not exact:
3500 ui.status(msg % uipathfn(abs))
3518 ui.status(msg % uipathfn(abs))
3501 elif exact:
3519 elif exact:
3502 ui.warn(msg % uipathfn(abs))
3520 ui.warn(msg % uipathfn(abs))
3503 break
3521 break
3504
3522
3505 if not opts.get(b'dry_run'):
3523 if not opts.get(b'dry_run'):
3506 needdata = (b'revert', b'add', b'undelete')
3524 needdata = (b'revert', b'add', b'undelete')
3507 oplist = [actions[name][0] for name in needdata]
3525 oplist = [actions[name][0] for name in needdata]
3508 prefetch = scmutil.prefetchfiles
3526 prefetch = scmutil.prefetchfiles
3509 matchfiles = scmutil.matchfiles(
3527 matchfiles = scmutil.matchfiles(
3510 repo, [f for sublist in oplist for f in sublist]
3528 repo, [f for sublist in oplist for f in sublist]
3511 )
3529 )
3512 prefetch(
3530 prefetch(
3513 repo,
3531 repo,
3514 [(ctx.rev(), matchfiles)],
3532 [(ctx.rev(), matchfiles)],
3515 )
3533 )
3516 match = scmutil.match(repo[None], pats)
3534 match = scmutil.match(repo[None], pats)
3517 _performrevert(
3535 _performrevert(
3518 repo,
3536 repo,
3519 ctx,
3537 ctx,
3520 names,
3538 names,
3521 uipathfn,
3539 uipathfn,
3522 actions,
3540 actions,
3523 match,
3541 match,
3524 interactive,
3542 interactive,
3525 tobackup,
3543 tobackup,
3526 )
3544 )
3527
3545
3528 if targetsubs:
3546 if targetsubs:
3529 # Revert the subrepos on the revert list
3547 # Revert the subrepos on the revert list
3530 for sub in targetsubs:
3548 for sub in targetsubs:
3531 try:
3549 try:
3532 wctx.sub(sub).revert(
3550 wctx.sub(sub).revert(
3533 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3551 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3534 )
3552 )
3535 except KeyError:
3553 except KeyError:
3536 raise error.Abort(
3554 raise error.Abort(
3537 b"subrepository '%s' does not exist in %s!"
3555 b"subrepository '%s' does not exist in %s!"
3538 % (sub, short(ctx.node()))
3556 % (sub, short(ctx.node()))
3539 )
3557 )
3540
3558
3541
3559
3542 def _performrevert(
3560 def _performrevert(
3543 repo,
3561 repo,
3544 ctx,
3562 ctx,
3545 names,
3563 names,
3546 uipathfn,
3564 uipathfn,
3547 actions,
3565 actions,
3548 match,
3566 match,
3549 interactive=False,
3567 interactive=False,
3550 tobackup=None,
3568 tobackup=None,
3551 ):
3569 ):
3552 """function that actually perform all the actions computed for revert
3570 """function that actually perform all the actions computed for revert
3553
3571
3554 This is an independent function to let extension to plug in and react to
3572 This is an independent function to let extension to plug in and react to
3555 the imminent revert.
3573 the imminent revert.
3556
3574
3557 Make sure you have the working directory locked when calling this function.
3575 Make sure you have the working directory locked when calling this function.
3558 """
3576 """
3559 parent, p2 = repo.dirstate.parents()
3577 parent, p2 = repo.dirstate.parents()
3560 node = ctx.node()
3578 node = ctx.node()
3561 excluded_files = []
3579 excluded_files = []
3562
3580
3563 def checkout(f):
3581 def checkout(f):
3564 fc = ctx[f]
3582 fc = ctx[f]
3565 repo.wwrite(f, fc.data(), fc.flags())
3583 repo.wwrite(f, fc.data(), fc.flags())
3566
3584
3567 def doremove(f):
3585 def doremove(f):
3568 try:
3586 try:
3569 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3587 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3570 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3588 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3571 except OSError:
3589 except OSError:
3572 pass
3590 pass
3573 repo.dirstate.set_untracked(f)
3591 repo.dirstate.set_untracked(f)
3574
3592
3575 def prntstatusmsg(action, f):
3593 def prntstatusmsg(action, f):
3576 exact = names[f]
3594 exact = names[f]
3577 if repo.ui.verbose or not exact:
3595 if repo.ui.verbose or not exact:
3578 repo.ui.status(actions[action][1] % uipathfn(f))
3596 repo.ui.status(actions[action][1] % uipathfn(f))
3579
3597
3580 audit_path = pathutil.pathauditor(repo.root, cached=True)
3598 audit_path = pathutil.pathauditor(repo.root, cached=True)
3581 for f in actions[b'forget'][0]:
3599 for f in actions[b'forget'][0]:
3582 if interactive:
3600 if interactive:
3583 choice = repo.ui.promptchoice(
3601 choice = repo.ui.promptchoice(
3584 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3602 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3585 )
3603 )
3586 if choice == 0:
3604 if choice == 0:
3587 prntstatusmsg(b'forget', f)
3605 prntstatusmsg(b'forget', f)
3588 repo.dirstate.set_untracked(f)
3606 repo.dirstate.set_untracked(f)
3589 else:
3607 else:
3590 excluded_files.append(f)
3608 excluded_files.append(f)
3591 else:
3609 else:
3592 prntstatusmsg(b'forget', f)
3610 prntstatusmsg(b'forget', f)
3593 repo.dirstate.set_untracked(f)
3611 repo.dirstate.set_untracked(f)
3594 for f in actions[b'remove'][0]:
3612 for f in actions[b'remove'][0]:
3595 audit_path(f)
3613 audit_path(f)
3596 if interactive:
3614 if interactive:
3597 choice = repo.ui.promptchoice(
3615 choice = repo.ui.promptchoice(
3598 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3616 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3599 )
3617 )
3600 if choice == 0:
3618 if choice == 0:
3601 prntstatusmsg(b'remove', f)
3619 prntstatusmsg(b'remove', f)
3602 doremove(f)
3620 doremove(f)
3603 else:
3621 else:
3604 excluded_files.append(f)
3622 excluded_files.append(f)
3605 else:
3623 else:
3606 prntstatusmsg(b'remove', f)
3624 prntstatusmsg(b'remove', f)
3607 doremove(f)
3625 doremove(f)
3608 for f in actions[b'drop'][0]:
3626 for f in actions[b'drop'][0]:
3609 audit_path(f)
3627 audit_path(f)
3610 prntstatusmsg(b'drop', f)
3628 prntstatusmsg(b'drop', f)
3611 repo.dirstate.set_untracked(f)
3629 repo.dirstate.set_untracked(f)
3612
3630
3613 normal = None
3631 normal = None
3614 if node == parent:
3632 if node == parent:
3615 # We're reverting to our parent. If possible, we'd like status
3633 # We're reverting to our parent. If possible, we'd like status
3616 # to report the file as clean. We have to use normallookup for
3634 # to report the file as clean. We have to use normallookup for
3617 # merges to avoid losing information about merged/dirty files.
3635 # merges to avoid losing information about merged/dirty files.
3618 if p2 != repo.nullid:
3636 if p2 != repo.nullid:
3619 normal = repo.dirstate.set_tracked
3637 normal = repo.dirstate.set_tracked
3620 else:
3638 else:
3621 normal = repo.dirstate.set_clean
3639 normal = repo.dirstate.set_clean
3622
3640
3623 newlyaddedandmodifiedfiles = set()
3641 newlyaddedandmodifiedfiles = set()
3624 if interactive:
3642 if interactive:
3625 # Prompt the user for changes to revert
3643 # Prompt the user for changes to revert
3626 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3644 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3627 m = scmutil.matchfiles(repo, torevert)
3645 m = scmutil.matchfiles(repo, torevert)
3628 diffopts = patch.difffeatureopts(
3646 diffopts = patch.difffeatureopts(
3629 repo.ui,
3647 repo.ui,
3630 whitespace=True,
3648 whitespace=True,
3631 section=b'commands',
3649 section=b'commands',
3632 configprefix=b'revert.interactive.',
3650 configprefix=b'revert.interactive.',
3633 )
3651 )
3634 diffopts.nodates = True
3652 diffopts.nodates = True
3635 diffopts.git = True
3653 diffopts.git = True
3636 operation = b'apply'
3654 operation = b'apply'
3637 if node == parent:
3655 if node == parent:
3638 if repo.ui.configbool(
3656 if repo.ui.configbool(
3639 b'experimental', b'revert.interactive.select-to-keep'
3657 b'experimental', b'revert.interactive.select-to-keep'
3640 ):
3658 ):
3641 operation = b'keep'
3659 operation = b'keep'
3642 else:
3660 else:
3643 operation = b'discard'
3661 operation = b'discard'
3644
3662
3645 if operation == b'apply':
3663 if operation == b'apply':
3646 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3664 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3647 else:
3665 else:
3648 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3666 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3649 original_headers = patch.parsepatch(diff)
3667 original_headers = patch.parsepatch(diff)
3650
3668
3651 try:
3669 try:
3652
3670
3653 chunks, opts = recordfilter(
3671 chunks, opts = recordfilter(
3654 repo.ui, original_headers, match, operation=operation
3672 repo.ui, original_headers, match, operation=operation
3655 )
3673 )
3656 if operation == b'discard':
3674 if operation == b'discard':
3657 chunks = patch.reversehunks(chunks)
3675 chunks = patch.reversehunks(chunks)
3658
3676
3659 except error.PatchError as err:
3677 except error.PatchError as err:
3660 raise error.Abort(_(b'error parsing patch: %s') % err)
3678 raise error.Abort(_(b'error parsing patch: %s') % err)
3661
3679
3662 # FIXME: when doing an interactive revert of a copy, there's no way of
3680 # FIXME: when doing an interactive revert of a copy, there's no way of
3663 # performing a partial revert of the added file, the only option is
3681 # performing a partial revert of the added file, the only option is
3664 # "remove added file <name> (Yn)?", so we don't need to worry about the
3682 # "remove added file <name> (Yn)?", so we don't need to worry about the
3665 # alsorestore value. Ideally we'd be able to partially revert
3683 # alsorestore value. Ideally we'd be able to partially revert
3666 # copied/renamed files.
3684 # copied/renamed files.
3667 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(chunks)
3685 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(chunks)
3668 if tobackup is None:
3686 if tobackup is None:
3669 tobackup = set()
3687 tobackup = set()
3670 # Apply changes
3688 # Apply changes
3671 fp = stringio()
3689 fp = stringio()
3672 # chunks are serialized per file, but files aren't sorted
3690 # chunks are serialized per file, but files aren't sorted
3673 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3691 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3674 prntstatusmsg(b'revert', f)
3692 prntstatusmsg(b'revert', f)
3675 files = set()
3693 files = set()
3676 for c in chunks:
3694 for c in chunks:
3677 if ishunk(c):
3695 if ishunk(c):
3678 abs = c.header.filename()
3696 abs = c.header.filename()
3679 # Create a backup file only if this hunk should be backed up
3697 # Create a backup file only if this hunk should be backed up
3680 if c.header.filename() in tobackup:
3698 if c.header.filename() in tobackup:
3681 target = repo.wjoin(abs)
3699 target = repo.wjoin(abs)
3682 bakname = scmutil.backuppath(repo.ui, repo, abs)
3700 bakname = scmutil.backuppath(repo.ui, repo, abs)
3683 util.copyfile(target, bakname)
3701 util.copyfile(target, bakname)
3684 tobackup.remove(abs)
3702 tobackup.remove(abs)
3685 if abs not in files:
3703 if abs not in files:
3686 files.add(abs)
3704 files.add(abs)
3687 if operation == b'keep':
3705 if operation == b'keep':
3688 checkout(abs)
3706 checkout(abs)
3689 c.write(fp)
3707 c.write(fp)
3690 dopatch = fp.tell()
3708 dopatch = fp.tell()
3691 fp.seek(0)
3709 fp.seek(0)
3692 if dopatch:
3710 if dopatch:
3693 try:
3711 try:
3694 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3712 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3695 except error.PatchError as err:
3713 except error.PatchError as err:
3696 raise error.Abort(pycompat.bytestr(err))
3714 raise error.Abort(pycompat.bytestr(err))
3697 del fp
3715 del fp
3698 else:
3716 else:
3699 for f in actions[b'revert'][0]:
3717 for f in actions[b'revert'][0]:
3700 prntstatusmsg(b'revert', f)
3718 prntstatusmsg(b'revert', f)
3701 checkout(f)
3719 checkout(f)
3702 if normal:
3720 if normal:
3703 normal(f)
3721 normal(f)
3704
3722
3705 for f in actions[b'add'][0]:
3723 for f in actions[b'add'][0]:
3706 # Don't checkout modified files, they are already created by the diff
3724 # Don't checkout modified files, they are already created by the diff
3707 if f not in newlyaddedandmodifiedfiles:
3725 if f not in newlyaddedandmodifiedfiles:
3708 prntstatusmsg(b'add', f)
3726 prntstatusmsg(b'add', f)
3709 checkout(f)
3727 checkout(f)
3710 repo.dirstate.set_tracked(f)
3728 repo.dirstate.set_tracked(f)
3711
3729
3712 normal = repo.dirstate.set_tracked
3730 normal = repo.dirstate.set_tracked
3713 if node == parent and p2 == repo.nullid:
3731 if node == parent and p2 == repo.nullid:
3714 normal = repo.dirstate.set_clean
3732 normal = repo.dirstate.set_clean
3715 for f in actions[b'undelete'][0]:
3733 for f in actions[b'undelete'][0]:
3716 if interactive:
3734 if interactive:
3717 choice = repo.ui.promptchoice(
3735 choice = repo.ui.promptchoice(
3718 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3736 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3719 )
3737 )
3720 if choice == 0:
3738 if choice == 0:
3721 prntstatusmsg(b'undelete', f)
3739 prntstatusmsg(b'undelete', f)
3722 checkout(f)
3740 checkout(f)
3723 normal(f)
3741 normal(f)
3724 else:
3742 else:
3725 excluded_files.append(f)
3743 excluded_files.append(f)
3726 else:
3744 else:
3727 prntstatusmsg(b'undelete', f)
3745 prntstatusmsg(b'undelete', f)
3728 checkout(f)
3746 checkout(f)
3729 normal(f)
3747 normal(f)
3730
3748
3731 copied = copies.pathcopies(repo[parent], ctx)
3749 copied = copies.pathcopies(repo[parent], ctx)
3732
3750
3733 for f in (
3751 for f in (
3734 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3752 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3735 ):
3753 ):
3736 if f in copied:
3754 if f in copied:
3737 repo.dirstate.copy(copied[f], f)
3755 repo.dirstate.copy(copied[f], f)
3738
3756
3739
3757
3740 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3758 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3741 # commands.outgoing. "missing" is "missing" of the result of
3759 # commands.outgoing. "missing" is "missing" of the result of
3742 # "findcommonoutgoing()"
3760 # "findcommonoutgoing()"
3743 outgoinghooks = util.hooks()
3761 outgoinghooks = util.hooks()
3744
3762
3745 # a list of (ui, repo) functions called by commands.summary
3763 # a list of (ui, repo) functions called by commands.summary
3746 summaryhooks = util.hooks()
3764 summaryhooks = util.hooks()
3747
3765
3748 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3766 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3749 #
3767 #
3750 # functions should return tuple of booleans below, if 'changes' is None:
3768 # functions should return tuple of booleans below, if 'changes' is None:
3751 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3769 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3752 #
3770 #
3753 # otherwise, 'changes' is a tuple of tuples below:
3771 # otherwise, 'changes' is a tuple of tuples below:
3754 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3772 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3755 # - (desturl, destbranch, destpeer, outgoing)
3773 # - (desturl, destbranch, destpeer, outgoing)
3756 summaryremotehooks = util.hooks()
3774 summaryremotehooks = util.hooks()
3757
3775
3758
3776
3759 def checkunfinished(repo, commit=False, skipmerge=False):
3777 def checkunfinished(repo, commit=False, skipmerge=False):
3760 """Look for an unfinished multistep operation, like graft, and abort
3778 """Look for an unfinished multistep operation, like graft, and abort
3761 if found. It's probably good to check this right before
3779 if found. It's probably good to check this right before
3762 bailifchanged().
3780 bailifchanged().
3763 """
3781 """
3764 # Check for non-clearable states first, so things like rebase will take
3782 # Check for non-clearable states first, so things like rebase will take
3765 # precedence over update.
3783 # precedence over update.
3766 for state in statemod._unfinishedstates:
3784 for state in statemod._unfinishedstates:
3767 if (
3785 if (
3768 state._clearable
3786 state._clearable
3769 or (commit and state._allowcommit)
3787 or (commit and state._allowcommit)
3770 or state._reportonly
3788 or state._reportonly
3771 ):
3789 ):
3772 continue
3790 continue
3773 if state.isunfinished(repo):
3791 if state.isunfinished(repo):
3774 raise error.StateError(state.msg(), hint=state.hint())
3792 raise error.StateError(state.msg(), hint=state.hint())
3775
3793
3776 for s in statemod._unfinishedstates:
3794 for s in statemod._unfinishedstates:
3777 if (
3795 if (
3778 not s._clearable
3796 not s._clearable
3779 or (commit and s._allowcommit)
3797 or (commit and s._allowcommit)
3780 or (s._opname == b'merge' and skipmerge)
3798 or (s._opname == b'merge' and skipmerge)
3781 or s._reportonly
3799 or s._reportonly
3782 ):
3800 ):
3783 continue
3801 continue
3784 if s.isunfinished(repo):
3802 if s.isunfinished(repo):
3785 raise error.StateError(s.msg(), hint=s.hint())
3803 raise error.StateError(s.msg(), hint=s.hint())
3786
3804
3787
3805
3788 def clearunfinished(repo):
3806 def clearunfinished(repo):
3789 """Check for unfinished operations (as above), and clear the ones
3807 """Check for unfinished operations (as above), and clear the ones
3790 that are clearable.
3808 that are clearable.
3791 """
3809 """
3792 for state in statemod._unfinishedstates:
3810 for state in statemod._unfinishedstates:
3793 if state._reportonly:
3811 if state._reportonly:
3794 continue
3812 continue
3795 if not state._clearable and state.isunfinished(repo):
3813 if not state._clearable and state.isunfinished(repo):
3796 raise error.StateError(state.msg(), hint=state.hint())
3814 raise error.StateError(state.msg(), hint=state.hint())
3797
3815
3798 for s in statemod._unfinishedstates:
3816 for s in statemod._unfinishedstates:
3799 if s._opname == b'merge' or s._reportonly:
3817 if s._opname == b'merge' or s._reportonly:
3800 continue
3818 continue
3801 if s._clearable and s.isunfinished(repo):
3819 if s._clearable and s.isunfinished(repo):
3802 util.unlink(repo.vfs.join(s._fname))
3820 util.unlink(repo.vfs.join(s._fname))
3803
3821
3804
3822
3805 def getunfinishedstate(repo):
3823 def getunfinishedstate(repo):
3806 """Checks for unfinished operations and returns statecheck object
3824 """Checks for unfinished operations and returns statecheck object
3807 for it"""
3825 for it"""
3808 for state in statemod._unfinishedstates:
3826 for state in statemod._unfinishedstates:
3809 if state.isunfinished(repo):
3827 if state.isunfinished(repo):
3810 return state
3828 return state
3811 return None
3829 return None
3812
3830
3813
3831
3814 def howtocontinue(repo):
3832 def howtocontinue(repo):
3815 """Check for an unfinished operation and return the command to finish
3833 """Check for an unfinished operation and return the command to finish
3816 it.
3834 it.
3817
3835
3818 statemod._unfinishedstates list is checked for an unfinished operation
3836 statemod._unfinishedstates list is checked for an unfinished operation
3819 and the corresponding message to finish it is generated if a method to
3837 and the corresponding message to finish it is generated if a method to
3820 continue is supported by the operation.
3838 continue is supported by the operation.
3821
3839
3822 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3840 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3823 a boolean.
3841 a boolean.
3824 """
3842 """
3825 contmsg = _(b"continue: %s")
3843 contmsg = _(b"continue: %s")
3826 for state in statemod._unfinishedstates:
3844 for state in statemod._unfinishedstates:
3827 if not state._continueflag:
3845 if not state._continueflag:
3828 continue
3846 continue
3829 if state.isunfinished(repo):
3847 if state.isunfinished(repo):
3830 return contmsg % state.continuemsg(), True
3848 return contmsg % state.continuemsg(), True
3831 if repo[None].dirty(missing=True, merge=False, branch=False):
3849 if repo[None].dirty(missing=True, merge=False, branch=False):
3832 return contmsg % _(b"hg commit"), False
3850 return contmsg % _(b"hg commit"), False
3833 return None, None
3851 return None, None
3834
3852
3835
3853
3836 def checkafterresolved(repo):
3854 def checkafterresolved(repo):
3837 """Inform the user about the next action after completing hg resolve
3855 """Inform the user about the next action after completing hg resolve
3838
3856
3839 If there's a an unfinished operation that supports continue flag,
3857 If there's a an unfinished operation that supports continue flag,
3840 howtocontinue will yield repo.ui.warn as the reporter.
3858 howtocontinue will yield repo.ui.warn as the reporter.
3841
3859
3842 Otherwise, it will yield repo.ui.note.
3860 Otherwise, it will yield repo.ui.note.
3843 """
3861 """
3844 msg, warning = howtocontinue(repo)
3862 msg, warning = howtocontinue(repo)
3845 if msg is not None:
3863 if msg is not None:
3846 if warning:
3864 if warning:
3847 repo.ui.warn(b"%s\n" % msg)
3865 repo.ui.warn(b"%s\n" % msg)
3848 else:
3866 else:
3849 repo.ui.note(b"%s\n" % msg)
3867 repo.ui.note(b"%s\n" % msg)
3850
3868
3851
3869
3852 def wrongtooltocontinue(repo, task):
3870 def wrongtooltocontinue(repo, task):
3853 """Raise an abort suggesting how to properly continue if there is an
3871 """Raise an abort suggesting how to properly continue if there is an
3854 active task.
3872 active task.
3855
3873
3856 Uses howtocontinue() to find the active task.
3874 Uses howtocontinue() to find the active task.
3857
3875
3858 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3876 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3859 a hint.
3877 a hint.
3860 """
3878 """
3861 after = howtocontinue(repo)
3879 after = howtocontinue(repo)
3862 hint = None
3880 hint = None
3863 if after[1]:
3881 if after[1]:
3864 hint = after[0]
3882 hint = after[0]
3865 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3883 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3866
3884
3867
3885
3868 def abortgraft(ui, repo, graftstate):
3886 def abortgraft(ui, repo, graftstate):
3869 """abort the interrupted graft and rollbacks to the state before interrupted
3887 """abort the interrupted graft and rollbacks to the state before interrupted
3870 graft"""
3888 graft"""
3871 if not graftstate.exists():
3889 if not graftstate.exists():
3872 raise error.StateError(_(b"no interrupted graft to abort"))
3890 raise error.StateError(_(b"no interrupted graft to abort"))
3873 statedata = readgraftstate(repo, graftstate)
3891 statedata = readgraftstate(repo, graftstate)
3874 newnodes = statedata.get(b'newnodes')
3892 newnodes = statedata.get(b'newnodes')
3875 if newnodes is None:
3893 if newnodes is None:
3876 # and old graft state which does not have all the data required to abort
3894 # and old graft state which does not have all the data required to abort
3877 # the graft
3895 # the graft
3878 raise error.Abort(_(b"cannot abort using an old graftstate"))
3896 raise error.Abort(_(b"cannot abort using an old graftstate"))
3879
3897
3880 # changeset from which graft operation was started
3898 # changeset from which graft operation was started
3881 if len(newnodes) > 0:
3899 if len(newnodes) > 0:
3882 startctx = repo[newnodes[0]].p1()
3900 startctx = repo[newnodes[0]].p1()
3883 else:
3901 else:
3884 startctx = repo[b'.']
3902 startctx = repo[b'.']
3885 # whether to strip or not
3903 # whether to strip or not
3886 cleanup = False
3904 cleanup = False
3887
3905
3888 if newnodes:
3906 if newnodes:
3889 newnodes = [repo[r].rev() for r in newnodes]
3907 newnodes = [repo[r].rev() for r in newnodes]
3890 cleanup = True
3908 cleanup = True
3891 # checking that none of the newnodes turned public or is public
3909 # checking that none of the newnodes turned public or is public
3892 immutable = [c for c in newnodes if not repo[c].mutable()]
3910 immutable = [c for c in newnodes if not repo[c].mutable()]
3893 if immutable:
3911 if immutable:
3894 repo.ui.warn(
3912 repo.ui.warn(
3895 _(b"cannot clean up public changesets %s\n")
3913 _(b"cannot clean up public changesets %s\n")
3896 % b', '.join(bytes(repo[r]) for r in immutable),
3914 % b', '.join(bytes(repo[r]) for r in immutable),
3897 hint=_(b"see 'hg help phases' for details"),
3915 hint=_(b"see 'hg help phases' for details"),
3898 )
3916 )
3899 cleanup = False
3917 cleanup = False
3900
3918
3901 # checking that no new nodes are created on top of grafted revs
3919 # checking that no new nodes are created on top of grafted revs
3902 desc = set(repo.changelog.descendants(newnodes))
3920 desc = set(repo.changelog.descendants(newnodes))
3903 if desc - set(newnodes):
3921 if desc - set(newnodes):
3904 repo.ui.warn(
3922 repo.ui.warn(
3905 _(
3923 _(
3906 b"new changesets detected on destination "
3924 b"new changesets detected on destination "
3907 b"branch, can't strip\n"
3925 b"branch, can't strip\n"
3908 )
3926 )
3909 )
3927 )
3910 cleanup = False
3928 cleanup = False
3911
3929
3912 if cleanup:
3930 if cleanup:
3913 with repo.wlock(), repo.lock():
3931 with repo.wlock(), repo.lock():
3914 mergemod.clean_update(startctx)
3932 mergemod.clean_update(startctx)
3915 # stripping the new nodes created
3933 # stripping the new nodes created
3916 strippoints = [
3934 strippoints = [
3917 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3935 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3918 ]
3936 ]
3919 repair.strip(repo.ui, repo, strippoints, backup=False)
3937 repair.strip(repo.ui, repo, strippoints, backup=False)
3920
3938
3921 if not cleanup:
3939 if not cleanup:
3922 # we don't update to the startnode if we can't strip
3940 # we don't update to the startnode if we can't strip
3923 startctx = repo[b'.']
3941 startctx = repo[b'.']
3924 mergemod.clean_update(startctx)
3942 mergemod.clean_update(startctx)
3925
3943
3926 ui.status(_(b"graft aborted\n"))
3944 ui.status(_(b"graft aborted\n"))
3927 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
3945 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
3928 graftstate.delete()
3946 graftstate.delete()
3929 return 0
3947 return 0
3930
3948
3931
3949
3932 def readgraftstate(repo, graftstate):
3950 def readgraftstate(repo, graftstate):
3933 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3951 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3934 """read the graft state file and return a dict of the data stored in it"""
3952 """read the graft state file and return a dict of the data stored in it"""
3935 try:
3953 try:
3936 return graftstate.read()
3954 return graftstate.read()
3937 except error.CorruptedState:
3955 except error.CorruptedState:
3938 nodes = repo.vfs.read(b'graftstate').splitlines()
3956 nodes = repo.vfs.read(b'graftstate').splitlines()
3939 return {b'nodes': nodes}
3957 return {b'nodes': nodes}
3940
3958
3941
3959
3942 def hgabortgraft(ui, repo):
3960 def hgabortgraft(ui, repo):
3943 """abort logic for aborting graft using 'hg abort'"""
3961 """abort logic for aborting graft using 'hg abort'"""
3944 with repo.wlock():
3962 with repo.wlock():
3945 graftstate = statemod.cmdstate(repo, b'graftstate')
3963 graftstate = statemod.cmdstate(repo, b'graftstate')
3946 return abortgraft(ui, repo, graftstate)
3964 return abortgraft(ui, repo, graftstate)
@@ -1,107 +1,118 b''
1 $ hg init
1 $ hg init
2 $ mkdir d1 d1/d11 d2
2 $ mkdir d1 d1/d11 d2
3 $ echo d1/a > d1/a
3 $ echo d1/a > d1/a
4 $ echo d1/ba > d1/ba
4 $ echo d1/ba > d1/ba
5 $ echo d1/a1 > d1/d11/a1
5 $ echo d1/a1 > d1/d11/a1
6 $ echo d1/b > d1/b
6 $ echo d1/b > d1/b
7 $ echo d2/b > d2/b
7 $ echo d2/b > d2/b
8 $ hg add d1/a d1/b d1/ba d1/d11/a1 d2/b
8 $ hg add d1/a d1/b d1/ba d1/d11/a1 d2/b
9 $ hg commit -m "intial"
9 $ hg commit -m "intial"
10
10
11
11
12 Test single file
12 Test single file
13
13
14 # One recorded copy, one copy to record after commit
14 # One recorded copy, one copy to record after commit
15 $ hg cp d1/b d1/c
15 $ hg cp d1/b d1/c
16 $ cp d1/b d1/d
16 $ cp d1/b d1/d
17 $ hg add d1/d
17 $ hg add d1/d
18 $ hg ci -m 'copy d1/b to d1/c and d1/d'
18 $ hg ci -m 'copy d1/b to d1/c and d1/d'
19 $ hg st -C --change .
19 $ hg st -C --change .
20 A d1/c
20 A d1/c
21 d1/b
21 d1/b
22 A d1/d
22 A d1/d
23 # Errors out without --after for now
23 # Errors out without --after for now
24 $ hg cp --at-rev . d1/b d1/d
24 $ hg cp --at-rev . d1/b d1/d
25 abort: --at-rev requires --after
25 abort: --at-rev requires --after
26 [10]
26 [10]
27 # Errors out with non-existent source
27 # Errors out with non-existent source
28 $ hg cp -A --at-rev . d1/non-existent d1/d
28 $ hg cp -A --at-rev . d1/non-existent d1/d
29 d1/non-existent: no such file in rev 55d1fd85ef0a
29 d1/non-existent: no such file in rev 55d1fd85ef0a
30 abort: no files to copy
30 abort: no files to copy
31 [10]
31 [10]
32 # Errors out with non-existent destination
32 # Errors out with non-existent destination
33 $ hg cp -A --at-rev . d1/b d1/non-existent
33 $ hg cp -A --at-rev . d1/b d1/non-existent
34 abort: d1/non-existent: copy destination does not exist in 8a9d70fa20c9
34 abort: d1/non-existent: copy destination does not exist in 8a9d70fa20c9
35 [10]
35 [10]
36 # Successful invocation
36 # Successful invocation
37 $ hg cp -A --at-rev . d1/b d1/d
37 $ hg cp -A --at-rev . d1/b d1/d
38 saved backup bundle to $TESTTMP/.hg/strip-backup/8a9d70fa20c9-973ae357-copy.hg
38 saved backup bundle to $TESTTMP/.hg/strip-backup/8a9d70fa20c9-973ae357-copy.hg
39 # New copy is recorded, and previously recorded copy is also still there
39 # New copy is recorded, and previously recorded copy is also still there
40 $ hg st -C --change .
40 $ hg st -C --change .
41 A d1/c
41 A d1/c
42 d1/b
42 d1/b
43 A d1/d
43 A d1/d
44 d1/b
44 d1/b
45 # Should get helpful message if we try to copy or rename after commit
46 $ hg cp --forget --at-rev . d1/d
47 saved backup bundle to $TESTTMP/.hg/strip-backup/3f7c325d3f9e-46f377bb-uncopy.hg
48 $ hg cp d1/b d1/d
49 d1/d: not overwriting - file already committed
50 ('hg copy --at-rev .' to record the copy in the parent of the working copy)
51 [1]
52 $ hg mv d1/b d1/d
53 d1/d: not overwriting - file already committed
54 ('hg rename --at-rev .' to record the rename in the parent of the working copy)
55 [1]
45
56
46 Test moved file (not copied) using 'hg cp' command
57 Test moved file (not copied) using 'hg cp' command
47
58
48 $ hg co 0
59 $ hg co 0
49 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
60 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
50 $ mv d1/b d1/d
61 $ mv d1/b d1/d
51 $ hg rm -A d1/b
62 $ hg rm -A d1/b
52 $ hg add d1/d
63 $ hg add d1/d
53 $ hg ci -m 'move d1/b to d1/d'
64 $ hg ci -m 'move d1/b to d1/d'
54 created new head
65 created new head
55 $ hg cp -A --at-rev . d1/b d1/d
66 $ hg cp -A --at-rev . d1/b d1/d
56 saved backup bundle to $TESTTMP/.hg/strip-backup/519850c3ea27-153c8fbb-copy.hg
67 saved backup bundle to $TESTTMP/.hg/strip-backup/519850c3ea27-153c8fbb-copy.hg
57 $ hg st -C --change .
68 $ hg st -C --change .
58 A d1/d
69 A d1/d
59 d1/b
70 d1/b
60 R d1/b
71 R d1/b
61
72
62 Test moved file (not copied) using 'hg mv' command
73 Test moved file (not copied) using 'hg mv' command
63
74
64 $ hg co 0
75 $ hg co 0
65 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
76 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
66 $ mv d1/b d1/d
77 $ mv d1/b d1/d
67 $ hg rm -A d1/b
78 $ hg rm -A d1/b
68 $ hg add d1/d
79 $ hg add d1/d
69 $ hg ci -m 'move d1/b to d1/d'
80 $ hg ci -m 'move d1/b to d1/d'
70 created new head
81 created new head
71 $ hg mv -A --at-rev . d1/b d1/d
82 $ hg mv -A --at-rev . d1/b d1/d
72 saved backup bundle to $TESTTMP/.hg/strip-backup/519850c3ea27-153c8fbb-copy.hg
83 saved backup bundle to $TESTTMP/.hg/strip-backup/519850c3ea27-153c8fbb-copy.hg
73 $ hg st -C --change .
84 $ hg st -C --change .
74 A d1/d
85 A d1/d
75 d1/b
86 d1/b
76 R d1/b
87 R d1/b
77
88
78 Test moved file (not copied) for which source still exists
89 Test moved file (not copied) for which source still exists
79
90
80 $ hg co 0
91 $ hg co 0
81 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
92 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
82 $ cp d1/b d1/d
93 $ cp d1/b d1/d
83 $ hg add d1/d
94 $ hg add d1/d
84 $ hg ci -m 'copy d1/b to d1/d'
95 $ hg ci -m 'copy d1/b to d1/d'
85 created new head
96 created new head
86 $ hg mv -A --at-rev . d1/b d1/d
97 $ hg mv -A --at-rev . d1/b d1/d
87 saved backup bundle to $TESTTMP/.hg/strip-backup/c8d0f6bcf7ca-1c9bb53e-copy.hg
98 saved backup bundle to $TESTTMP/.hg/strip-backup/c8d0f6bcf7ca-1c9bb53e-copy.hg
88 $ hg st -C --change .
99 $ hg st -C --change .
89 A d1/d
100 A d1/d
90 d1/b
101 d1/b
91
102
92 Test using directory as destination
103 Test using directory as destination
93
104
94 $ hg co 0
105 $ hg co 0
95 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
106 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
96 $ cp -R d1 d3
107 $ cp -R d1 d3
97 $ hg add d3
108 $ hg add d3
98 adding d3/a
109 adding d3/a
99 adding d3/b
110 adding d3/b
100 adding d3/ba
111 adding d3/ba
101 adding d3/d11/a1
112 adding d3/d11/a1
102 $ hg ci -m 'copy d1/ to d3/'
113 $ hg ci -m 'copy d1/ to d3/'
103 created new head
114 created new head
104 $ hg cp -A --at-rev . d1 d3
115 $ hg cp -A --at-rev . d1 d3
105 abort: d3: --at-rev does not support a directory as destination
116 abort: d3: --at-rev does not support a directory as destination
106 [10]
117 [10]
107
118
General Comments 0
You need to be logged in to leave comments. Login now