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