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