##// END OF EJS Templates
amend: fix amend with copies in extras...
Martin von Zweigbergk -
r49833:877d7e1a stable
parent child Browse files
Show More
@@ -1,3982 +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 if changes:
2909 changeset_copies = (
2910 repo.ui.config(b'experimental', b'copies.read-from')
2911 != b'filelog-only'
2912 )
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
2915 # filectxs from the old commit.
2916 if changes or changeset_copies:
2910 # Recompute copies (avoid recording a -> b -> a)
2917 # Recompute copies (avoid recording a -> b -> a)
2911 copied = copies.pathcopies(base, wctx, matcher)
2918 copied = copies.pathcopies(base, wctx, matcher)
2912 if old.p2:
2919 if old.p2:
2913 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2920 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2914
2921
2915 # Prune files which were reverted by the updates: if old
2922 # Prune files which were reverted by the updates: if old
2916 # introduced file X and the file was renamed in the working
2923 # introduced file X and the file was renamed in the working
2917 # copy, then those two files are the same and
2924 # copy, then those two files are the same and
2918 # 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
2919 # 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
2920 # deleted), old X must be preserved.
2927 # deleted), old X must be preserved.
2921 files.update(filestoamend)
2928 files.update(filestoamend)
2922 files = [
2929 files = [
2923 f
2930 f
2924 for f in files
2931 for f in files
2925 if (f not in filestoamend or not samefile(f, wctx, base))
2932 if (f not in filestoamend or not samefile(f, wctx, base))
2926 ]
2933 ]
2927
2934
2928 def filectxfn(repo, ctx_, path):
2935 def filectxfn(repo, ctx_, path):
2929 try:
2936 try:
2937 # Return None for removed files.
2938 if path in wctx.removed():
2939 return None
2940
2930 # If the file being considered is not amongst the files
2941 # If the file being considered is not amongst the files
2931 # to be amended, we should return the file context from the
2942 # to be amended, we should use the file context from the
2932 # old changeset. This avoids issues when only some files in
2943 # old changeset. This avoids issues when only some files in
2933 # the working copy are being amended but there are also
2944 # the working copy are being amended but there are also
2934 # changes to other files from the old changeset.
2945 # changes to other files from the old changeset.
2935 if path not in filestoamend:
2946 if path in filestoamend:
2936 return old.filectx(path)
2947 fctx = wctx[path]
2937
2948 else:
2938 # Return None for removed files.
2949 fctx = old.filectx(path)
2939 if path in wctx.removed():
2940 return None
2941
2942 fctx = wctx[path]
2943 flags = fctx.flags()
2950 flags = fctx.flags()
2944 mctx = context.memfilectx(
2951 mctx = context.memfilectx(
2945 repo,
2952 repo,
2946 ctx_,
2953 ctx_,
2947 fctx.path(),
2954 fctx.path(),
2948 fctx.data(),
2955 fctx.data(),
2949 islink=b'l' in flags,
2956 islink=b'l' in flags,
2950 isexec=b'x' in flags,
2957 isexec=b'x' in flags,
2951 copysource=copied.get(path),
2958 copysource=copied.get(path),
2952 )
2959 )
2953 return mctx
2960 return mctx
2954 except KeyError:
2961 except KeyError:
2955 return None
2962 return None
2956
2963
2957 else:
2964 else:
2958 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2965 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2959
2966
2960 # Use version of files as in the old cset
2967 # Use version of files as in the old cset
2961 def filectxfn(repo, ctx_, path):
2968 def filectxfn(repo, ctx_, path):
2962 try:
2969 try:
2963 return old.filectx(path)
2970 return old.filectx(path)
2964 except KeyError:
2971 except KeyError:
2965 return None
2972 return None
2966
2973
2967 # 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
2968 # the message of the changeset to amend.
2975 # the message of the changeset to amend.
2969 message = logmessage(ui, opts)
2976 message = logmessage(ui, opts)
2970
2977
2971 editform = mergeeditform(old, b'commit.amend')
2978 editform = mergeeditform(old, b'commit.amend')
2972
2979
2973 if not message:
2980 if not message:
2974 message = old.description()
2981 message = old.description()
2975 # 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
2976 # 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
2977 # description, let's always start the editor.
2984 # description, let's always start the editor.
2978 doedit = not message or opts.get(b'edit') in [True, None]
2985 doedit = not message or opts.get(b'edit') in [True, None]
2979 else:
2986 else:
2980 # 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
2981 # --edit.
2988 # --edit.
2982 doedit = opts.get(b'edit') is True
2989 doedit = opts.get(b'edit') is True
2983 editor = getcommiteditor(edit=doedit, editform=editform)
2990 editor = getcommiteditor(edit=doedit, editform=editform)
2984
2991
2985 pureextra = extra.copy()
2992 pureextra = extra.copy()
2986 extra[b'amend_source'] = old.hex()
2993 extra[b'amend_source'] = old.hex()
2987
2994
2988 new = context.memctx(
2995 new = context.memctx(
2989 repo,
2996 repo,
2990 parents=[base.node(), old.p2().node()],
2997 parents=[base.node(), old.p2().node()],
2991 text=message,
2998 text=message,
2992 files=files,
2999 files=files,
2993 filectxfn=filectxfn,
3000 filectxfn=filectxfn,
2994 user=user,
3001 user=user,
2995 date=date,
3002 date=date,
2996 extra=extra,
3003 extra=extra,
2997 editor=editor,
3004 editor=editor,
2998 )
3005 )
2999
3006
3000 newdesc = changelog.stripdesc(new.description())
3007 newdesc = changelog.stripdesc(new.description())
3001 if (
3008 if (
3002 (not changes)
3009 (not changes)
3003 and newdesc == old.description()
3010 and newdesc == old.description()
3004 and user == old.user()
3011 and user == old.user()
3005 and (date == old.date() or datemaydiffer)
3012 and (date == old.date() or datemaydiffer)
3006 and pureextra == old.extra()
3013 and pureextra == old.extra()
3007 ):
3014 ):
3008 # nothing changed. continuing here would create a new node
3015 # nothing changed. continuing here would create a new node
3009 # anyway because of the amend_source noise.
3016 # anyway because of the amend_source noise.
3010 #
3017 #
3011 # This not what we expect from amend.
3018 # This not what we expect from amend.
3012 return old.node()
3019 return old.node()
3013
3020
3014 commitphase = None
3021 commitphase = None
3015 if opts.get(b'secret'):
3022 if opts.get(b'secret'):
3016 commitphase = phases.secret
3023 commitphase = phases.secret
3017 newid = repo.commitctx(new)
3024 newid = repo.commitctx(new)
3018 ms.reset()
3025 ms.reset()
3019
3026
3020 with repo.dirstate.parentchange():
3027 with repo.dirstate.parentchange():
3021 # Reroute the working copy parent to the new changeset
3028 # Reroute the working copy parent to the new changeset
3022 repo.setparents(newid, repo.nullid)
3029 repo.setparents(newid, repo.nullid)
3023
3030
3024 # Fixing the dirstate because localrepo.commitctx does not update
3031 # Fixing the dirstate because localrepo.commitctx does not update
3025 # 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
3026 # 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
3027 # could have done if it updated the dirstate. Now, we can
3034 # could have done if it updated the dirstate. Now, we can
3028 # selectively update the dirstate only for the amended files.
3035 # selectively update the dirstate only for the amended files.
3029 dirstate = repo.dirstate
3036 dirstate = repo.dirstate
3030
3037
3031 # 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
3032 # 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
3033 # the files may have changed since the command started; using "normal"
3040 # the files may have changed since the command started; using "normal"
3034 # would mark them as clean but with uncommitted contents.
3041 # would mark them as clean but with uncommitted contents.
3035 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3042 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3036 for f in normalfiles:
3043 for f in normalfiles:
3037 dirstate.update_file(
3044 dirstate.update_file(
3038 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
3045 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
3039 )
3046 )
3040
3047
3041 # Update the state of files which were removed in the amend
3048 # Update the state of files which were removed in the amend
3042 # to "removed" in the dirstate.
3049 # to "removed" in the dirstate.
3043 removedfiles = set(wctx.removed()) & filestoamend
3050 removedfiles = set(wctx.removed()) & filestoamend
3044 for f in removedfiles:
3051 for f in removedfiles:
3045 dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
3052 dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
3046
3053
3047 mapping = {old.node(): (newid,)}
3054 mapping = {old.node(): (newid,)}
3048 obsmetadata = None
3055 obsmetadata = None
3049 if opts.get(b'note'):
3056 if opts.get(b'note'):
3050 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3057 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3051 backup = ui.configbool(b'rewrite', b'backup-bundle')
3058 backup = ui.configbool(b'rewrite', b'backup-bundle')
3052 scmutil.cleanupnodes(
3059 scmutil.cleanupnodes(
3053 repo,
3060 repo,
3054 mapping,
3061 mapping,
3055 b'amend',
3062 b'amend',
3056 metadata=obsmetadata,
3063 metadata=obsmetadata,
3057 fixphase=True,
3064 fixphase=True,
3058 targetphase=commitphase,
3065 targetphase=commitphase,
3059 backup=backup,
3066 backup=backup,
3060 )
3067 )
3061
3068
3062 return newid
3069 return newid
3063
3070
3064
3071
3065 def commiteditor(repo, ctx, subs, editform=b''):
3072 def commiteditor(repo, ctx, subs, editform=b''):
3066 if ctx.description():
3073 if ctx.description():
3067 return ctx.description()
3074 return ctx.description()
3068 return commitforceeditor(
3075 return commitforceeditor(
3069 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3076 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3070 )
3077 )
3071
3078
3072
3079
3073 def commitforceeditor(
3080 def commitforceeditor(
3074 repo,
3081 repo,
3075 ctx,
3082 ctx,
3076 subs,
3083 subs,
3077 finishdesc=None,
3084 finishdesc=None,
3078 extramsg=None,
3085 extramsg=None,
3079 editform=b'',
3086 editform=b'',
3080 unchangedmessagedetection=False,
3087 unchangedmessagedetection=False,
3081 ):
3088 ):
3082 if not extramsg:
3089 if not extramsg:
3083 extramsg = _(b"Leave message empty to abort commit.")
3090 extramsg = _(b"Leave message empty to abort commit.")
3084
3091
3085 forms = [e for e in editform.split(b'.') if e]
3092 forms = [e for e in editform.split(b'.') if e]
3086 forms.insert(0, b'changeset')
3093 forms.insert(0, b'changeset')
3087 templatetext = None
3094 templatetext = None
3088 while forms:
3095 while forms:
3089 ref = b'.'.join(forms)
3096 ref = b'.'.join(forms)
3090 if repo.ui.config(b'committemplate', ref):
3097 if repo.ui.config(b'committemplate', ref):
3091 templatetext = committext = buildcommittemplate(
3098 templatetext = committext = buildcommittemplate(
3092 repo, ctx, subs, extramsg, ref
3099 repo, ctx, subs, extramsg, ref
3093 )
3100 )
3094 break
3101 break
3095 forms.pop()
3102 forms.pop()
3096 else:
3103 else:
3097 committext = buildcommittext(repo, ctx, subs, extramsg)
3104 committext = buildcommittext(repo, ctx, subs, extramsg)
3098
3105
3099 # run editor in the repository root
3106 # run editor in the repository root
3100 olddir = encoding.getcwd()
3107 olddir = encoding.getcwd()
3101 os.chdir(repo.root)
3108 os.chdir(repo.root)
3102
3109
3103 # make in-memory changes visible to external process
3110 # make in-memory changes visible to external process
3104 tr = repo.currenttransaction()
3111 tr = repo.currenttransaction()
3105 repo.dirstate.write(tr)
3112 repo.dirstate.write(tr)
3106 pending = tr and tr.writepending() and repo.root
3113 pending = tr and tr.writepending() and repo.root
3107
3114
3108 editortext = repo.ui.edit(
3115 editortext = repo.ui.edit(
3109 committext,
3116 committext,
3110 ctx.user(),
3117 ctx.user(),
3111 ctx.extra(),
3118 ctx.extra(),
3112 editform=editform,
3119 editform=editform,
3113 pending=pending,
3120 pending=pending,
3114 repopath=repo.path,
3121 repopath=repo.path,
3115 action=b'commit',
3122 action=b'commit',
3116 )
3123 )
3117 text = editortext
3124 text = editortext
3118
3125
3119 # 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
3120 # to display the diff)
3127 # to display the diff)
3121 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3128 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3122 if stripbelow:
3129 if stripbelow:
3123 text = text[: stripbelow.start()]
3130 text = text[: stripbelow.start()]
3124
3131
3125 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3132 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3126 os.chdir(olddir)
3133 os.chdir(olddir)
3127
3134
3128 if finishdesc:
3135 if finishdesc:
3129 text = finishdesc(text)
3136 text = finishdesc(text)
3130 if not text.strip():
3137 if not text.strip():
3131 raise error.InputError(_(b"empty commit message"))
3138 raise error.InputError(_(b"empty commit message"))
3132 if unchangedmessagedetection and editortext == templatetext:
3139 if unchangedmessagedetection and editortext == templatetext:
3133 raise error.InputError(_(b"commit message unchanged"))
3140 raise error.InputError(_(b"commit message unchanged"))
3134
3141
3135 return text
3142 return text
3136
3143
3137
3144
3138 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3145 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3139 ui = repo.ui
3146 ui = repo.ui
3140 spec = formatter.reference_templatespec(ref)
3147 spec = formatter.reference_templatespec(ref)
3141 t = logcmdutil.changesettemplater(ui, repo, spec)
3148 t = logcmdutil.changesettemplater(ui, repo, spec)
3142 t.t.cache.update(
3149 t.t.cache.update(
3143 (k, templater.unquotestring(v))
3150 (k, templater.unquotestring(v))
3144 for k, v in repo.ui.configitems(b'committemplate')
3151 for k, v in repo.ui.configitems(b'committemplate')
3145 )
3152 )
3146
3153
3147 if not extramsg:
3154 if not extramsg:
3148 extramsg = b'' # ensure that extramsg is string
3155 extramsg = b'' # ensure that extramsg is string
3149
3156
3150 ui.pushbuffer()
3157 ui.pushbuffer()
3151 t.show(ctx, extramsg=extramsg)
3158 t.show(ctx, extramsg=extramsg)
3152 return ui.popbuffer()
3159 return ui.popbuffer()
3153
3160
3154
3161
3155 def hgprefix(msg):
3162 def hgprefix(msg):
3156 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])
3157
3164
3158
3165
3159 def buildcommittext(repo, ctx, subs, extramsg):
3166 def buildcommittext(repo, ctx, subs, extramsg):
3160 edittext = []
3167 edittext = []
3161 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3168 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3162 if ctx.description():
3169 if ctx.description():
3163 edittext.append(ctx.description())
3170 edittext.append(ctx.description())
3164 edittext.append(b"")
3171 edittext.append(b"")
3165 edittext.append(b"") # Empty line between message and comments.
3172 edittext.append(b"") # Empty line between message and comments.
3166 edittext.append(
3173 edittext.append(
3167 hgprefix(
3174 hgprefix(
3168 _(
3175 _(
3169 b"Enter commit message."
3176 b"Enter commit message."
3170 b" Lines beginning with 'HG:' are removed."
3177 b" Lines beginning with 'HG:' are removed."
3171 )
3178 )
3172 )
3179 )
3173 )
3180 )
3174 edittext.append(hgprefix(extramsg))
3181 edittext.append(hgprefix(extramsg))
3175 edittext.append(b"HG: --")
3182 edittext.append(b"HG: --")
3176 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3183 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3177 if ctx.p2():
3184 if ctx.p2():
3178 edittext.append(hgprefix(_(b"branch merge")))
3185 edittext.append(hgprefix(_(b"branch merge")))
3179 if ctx.branch():
3186 if ctx.branch():
3180 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3187 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3181 if bookmarks.isactivewdirparent(repo):
3188 if bookmarks.isactivewdirparent(repo):
3182 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3189 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3183 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3190 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3184 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3191 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3185 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3192 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3186 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3193 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3187 if not added and not modified and not removed:
3194 if not added and not modified and not removed:
3188 edittext.append(hgprefix(_(b"no files changed")))
3195 edittext.append(hgprefix(_(b"no files changed")))
3189 edittext.append(b"")
3196 edittext.append(b"")
3190
3197
3191 return b"\n".join(edittext)
3198 return b"\n".join(edittext)
3192
3199
3193
3200
3194 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3201 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3195 if opts is None:
3202 if opts is None:
3196 opts = {}
3203 opts = {}
3197 ctx = repo[node]
3204 ctx = repo[node]
3198 parents = ctx.parents()
3205 parents = ctx.parents()
3199
3206
3200 if tip is not None and repo.changelog.tip() == tip:
3207 if tip is not None and repo.changelog.tip() == tip:
3201 # avoid reporting something like "committed new head" when
3208 # avoid reporting something like "committed new head" when
3202 # recommitting old changesets, and issue a helpful warning
3209 # recommitting old changesets, and issue a helpful warning
3203 # for most instances
3210 # for most instances
3204 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"))
3205 elif (
3212 elif (
3206 not opts.get(b'amend')
3213 not opts.get(b'amend')
3207 and bheads
3214 and bheads
3208 and node not in bheads
3215 and node not in bheads
3209 and not any(
3216 and not any(
3210 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
3211 )
3218 )
3212 ):
3219 ):
3213 repo.ui.status(_(b'created new head\n'))
3220 repo.ui.status(_(b'created new head\n'))
3214 # The message is not printed for initial roots. For the other
3221 # The message is not printed for initial roots. For the other
3215 # changesets, it is printed in the following situations:
3222 # changesets, it is printed in the following situations:
3216 #
3223 #
3217 # Par column: for the 2 parents with ...
3224 # Par column: for the 2 parents with ...
3218 # N: null or no parent
3225 # N: null or no parent
3219 # B: parent is on another named branch
3226 # B: parent is on another named branch
3220 # C: parent is a regular non head changeset
3227 # C: parent is a regular non head changeset
3221 # H: parent was a branch head of the current branch
3228 # H: parent was a branch head of the current branch
3222 # Msg column: whether we print "created new head" message
3229 # Msg column: whether we print "created new head" message
3223 # In the following, it is assumed that there already exists some
3230 # In the following, it is assumed that there already exists some
3224 # initial branch heads of the current branch, otherwise nothing is
3231 # initial branch heads of the current branch, otherwise nothing is
3225 # printed anyway.
3232 # printed anyway.
3226 #
3233 #
3227 # Par Msg Comment
3234 # Par Msg Comment
3228 # N N y additional topo root
3235 # N N y additional topo root
3229 #
3236 #
3230 # B N y additional branch root
3237 # B N y additional branch root
3231 # C N y additional topo head
3238 # C N y additional topo head
3232 # H N n usual case
3239 # H N n usual case
3233 #
3240 #
3234 # B B y weird additional branch root
3241 # B B y weird additional branch root
3235 # C B y branch merge
3242 # C B y branch merge
3236 # H B n merge with named branch
3243 # H B n merge with named branch
3237 #
3244 #
3238 # C C y additional head from merge
3245 # C C y additional head from merge
3239 # C H n merge with a head
3246 # C H n merge with a head
3240 #
3247 #
3241 # H H n head merge: head count decreases
3248 # H H n head merge: head count decreases
3242
3249
3243 if not opts.get(b'close_branch'):
3250 if not opts.get(b'close_branch'):
3244 for r in parents:
3251 for r in parents:
3245 if r.closesbranch() and r.branch() == branch:
3252 if r.closesbranch() and r.branch() == branch:
3246 repo.ui.status(
3253 repo.ui.status(
3247 _(b'reopening closed branch head %d\n') % r.rev()
3254 _(b'reopening closed branch head %d\n') % r.rev()
3248 )
3255 )
3249
3256
3250 if repo.ui.debugflag:
3257 if repo.ui.debugflag:
3251 repo.ui.write(
3258 repo.ui.write(
3252 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3259 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3253 )
3260 )
3254 elif repo.ui.verbose:
3261 elif repo.ui.verbose:
3255 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))
3256
3263
3257
3264
3258 def postcommitstatus(repo, pats, opts):
3265 def postcommitstatus(repo, pats, opts):
3259 return repo.status(match=scmutil.match(repo[None], pats, opts))
3266 return repo.status(match=scmutil.match(repo[None], pats, opts))
3260
3267
3261
3268
3262 def revert(ui, repo, ctx, *pats, **opts):
3269 def revert(ui, repo, ctx, *pats, **opts):
3263 opts = pycompat.byteskwargs(opts)
3270 opts = pycompat.byteskwargs(opts)
3264 parent, p2 = repo.dirstate.parents()
3271 parent, p2 = repo.dirstate.parents()
3265 node = ctx.node()
3272 node = ctx.node()
3266
3273
3267 mf = ctx.manifest()
3274 mf = ctx.manifest()
3268 if node == p2:
3275 if node == p2:
3269 parent = p2
3276 parent = p2
3270
3277
3271 # need all matching names in dirstate and manifest of target rev,
3278 # need all matching names in dirstate and manifest of target rev,
3272 # 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
3273 # but not other. in both cases, filesets should be evaluated against
3280 # but not other. in both cases, filesets should be evaluated against
3274 # workingctx to get consistent result (issue4497). this means 'set:**'
3281 # workingctx to get consistent result (issue4497). this means 'set:**'
3275 # cannot be used to select missing files from target rev.
3282 # cannot be used to select missing files from target rev.
3276
3283
3277 # `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
3278 # The mapping is in the form:
3285 # The mapping is in the form:
3279 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3286 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3280 names = {}
3287 names = {}
3281 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3288 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3282
3289
3283 with repo.wlock():
3290 with repo.wlock():
3284 ## filling of the `names` mapping
3291 ## filling of the `names` mapping
3285 # walk dirstate to fill `names`
3292 # walk dirstate to fill `names`
3286
3293
3287 interactive = opts.get(b'interactive', False)
3294 interactive = opts.get(b'interactive', False)
3288 wctx = repo[None]
3295 wctx = repo[None]
3289 m = scmutil.match(wctx, pats, opts)
3296 m = scmutil.match(wctx, pats, opts)
3290
3297
3291 # we'll need this later
3298 # we'll need this later
3292 targetsubs = sorted(s for s in wctx.substate if m(s))
3299 targetsubs = sorted(s for s in wctx.substate if m(s))
3293
3300
3294 if not m.always():
3301 if not m.always():
3295 matcher = matchmod.badmatch(m, lambda x, y: False)
3302 matcher = matchmod.badmatch(m, lambda x, y: False)
3296 for abs in wctx.walk(matcher):
3303 for abs in wctx.walk(matcher):
3297 names[abs] = m.exact(abs)
3304 names[abs] = m.exact(abs)
3298
3305
3299 # walk target manifest to fill `names`
3306 # walk target manifest to fill `names`
3300
3307
3301 def badfn(path, msg):
3308 def badfn(path, msg):
3302 if path in names:
3309 if path in names:
3303 return
3310 return
3304 if path in ctx.substate:
3311 if path in ctx.substate:
3305 return
3312 return
3306 path_ = path + b'/'
3313 path_ = path + b'/'
3307 for f in names:
3314 for f in names:
3308 if f.startswith(path_):
3315 if f.startswith(path_):
3309 return
3316 return
3310 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3317 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3311
3318
3312 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3319 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3313 if abs not in names:
3320 if abs not in names:
3314 names[abs] = m.exact(abs)
3321 names[abs] = m.exact(abs)
3315
3322
3316 # Find status of all file in `names`.
3323 # Find status of all file in `names`.
3317 m = scmutil.matchfiles(repo, names)
3324 m = scmutil.matchfiles(repo, names)
3318
3325
3319 changes = repo.status(
3326 changes = repo.status(
3320 node1=node, match=m, unknown=True, ignored=True, clean=True
3327 node1=node, match=m, unknown=True, ignored=True, clean=True
3321 )
3328 )
3322 else:
3329 else:
3323 changes = repo.status(node1=node, match=m)
3330 changes = repo.status(node1=node, match=m)
3324 for kind in changes:
3331 for kind in changes:
3325 for abs in kind:
3332 for abs in kind:
3326 names[abs] = m.exact(abs)
3333 names[abs] = m.exact(abs)
3327
3334
3328 m = scmutil.matchfiles(repo, names)
3335 m = scmutil.matchfiles(repo, names)
3329
3336
3330 modified = set(changes.modified)
3337 modified = set(changes.modified)
3331 added = set(changes.added)
3338 added = set(changes.added)
3332 removed = set(changes.removed)
3339 removed = set(changes.removed)
3333 _deleted = set(changes.deleted)
3340 _deleted = set(changes.deleted)
3334 unknown = set(changes.unknown)
3341 unknown = set(changes.unknown)
3335 unknown.update(changes.ignored)
3342 unknown.update(changes.ignored)
3336 clean = set(changes.clean)
3343 clean = set(changes.clean)
3337 modadded = set()
3344 modadded = set()
3338
3345
3339 # 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,
3340 # even when we revert against something else than parent. This will
3347 # even when we revert against something else than parent. This will
3341 # 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
3342 # or just forget etc).
3349 # or just forget etc).
3343 if parent == node:
3350 if parent == node:
3344 dsmodified = modified
3351 dsmodified = modified
3345 dsadded = added
3352 dsadded = added
3346 dsremoved = removed
3353 dsremoved = removed
3347 # store all local modifications, useful later for rename detection
3354 # store all local modifications, useful later for rename detection
3348 localchanges = dsmodified | dsadded
3355 localchanges = dsmodified | dsadded
3349 modified, added, removed = set(), set(), set()
3356 modified, added, removed = set(), set(), set()
3350 else:
3357 else:
3351 changes = repo.status(node1=parent, match=m)
3358 changes = repo.status(node1=parent, match=m)
3352 dsmodified = set(changes.modified)
3359 dsmodified = set(changes.modified)
3353 dsadded = set(changes.added)
3360 dsadded = set(changes.added)
3354 dsremoved = set(changes.removed)
3361 dsremoved = set(changes.removed)
3355 # store all local modifications, useful later for rename detection
3362 # store all local modifications, useful later for rename detection
3356 localchanges = dsmodified | dsadded
3363 localchanges = dsmodified | dsadded
3357
3364
3358 # only take into account for removes between wc and target
3365 # only take into account for removes between wc and target
3359 clean |= dsremoved - removed
3366 clean |= dsremoved - removed
3360 dsremoved &= removed
3367 dsremoved &= removed
3361 # distinct between dirstate remove and other
3368 # distinct between dirstate remove and other
3362 removed -= dsremoved
3369 removed -= dsremoved
3363
3370
3364 modadded = added & dsmodified
3371 modadded = added & dsmodified
3365 added -= modadded
3372 added -= modadded
3366
3373
3367 # tell newly modified apart.
3374 # tell newly modified apart.
3368 dsmodified &= modified
3375 dsmodified &= modified
3369 dsmodified |= modified & dsadded # dirstate added may need backup
3376 dsmodified |= modified & dsadded # dirstate added may need backup
3370 modified -= dsmodified
3377 modified -= dsmodified
3371
3378
3372 # 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
3373 # before making the distinction. The dirstate will be used for
3380 # before making the distinction. The dirstate will be used for
3374 # that purpose.
3381 # that purpose.
3375 dsadded = added
3382 dsadded = added
3376
3383
3377 # 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
3378 # modified, we need to post process the result
3385 # modified, we need to post process the result
3379 if p2 != repo.nullid:
3386 if p2 != repo.nullid:
3380 mergeadd = set(dsmodified)
3387 mergeadd = set(dsmodified)
3381 for path in dsmodified:
3388 for path in dsmodified:
3382 if path in mf:
3389 if path in mf:
3383 mergeadd.remove(path)
3390 mergeadd.remove(path)
3384 dsadded |= mergeadd
3391 dsadded |= mergeadd
3385 dsmodified -= mergeadd
3392 dsmodified -= mergeadd
3386
3393
3387 # 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
3388 for f in localchanges:
3395 for f in localchanges:
3389 src = repo.dirstate.copied(f)
3396 src = repo.dirstate.copied(f)
3390 # XXX should we check for rename down to target node?
3397 # XXX should we check for rename down to target node?
3391 if (
3398 if (
3392 src
3399 src
3393 and src not in names
3400 and src not in names
3394 and repo.dirstate.get_entry(src).removed
3401 and repo.dirstate.get_entry(src).removed
3395 ):
3402 ):
3396 dsremoved.add(src)
3403 dsremoved.add(src)
3397 names[src] = True
3404 names[src] = True
3398
3405
3399 # determine the exact nature of the deleted changesets
3406 # determine the exact nature of the deleted changesets
3400 deladded = set(_deleted)
3407 deladded = set(_deleted)
3401 for path in _deleted:
3408 for path in _deleted:
3402 if path in mf:
3409 if path in mf:
3403 deladded.remove(path)
3410 deladded.remove(path)
3404 deleted = _deleted - deladded
3411 deleted = _deleted - deladded
3405
3412
3406 # distinguish between file to forget and the other
3413 # distinguish between file to forget and the other
3407 added = set()
3414 added = set()
3408 for abs in dsadded:
3415 for abs in dsadded:
3409 if not repo.dirstate.get_entry(abs).added:
3416 if not repo.dirstate.get_entry(abs).added:
3410 added.add(abs)
3417 added.add(abs)
3411 dsadded -= added
3418 dsadded -= added
3412
3419
3413 for abs in deladded:
3420 for abs in deladded:
3414 if repo.dirstate.get_entry(abs).added:
3421 if repo.dirstate.get_entry(abs).added:
3415 dsadded.add(abs)
3422 dsadded.add(abs)
3416 deladded -= dsadded
3423 deladded -= dsadded
3417
3424
3418 # 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
3419 # 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.
3420 # Making the distinction at this stage helps have simpler backup
3427 # Making the distinction at this stage helps have simpler backup
3421 # logic.
3428 # logic.
3422 removunk = set()
3429 removunk = set()
3423 for abs in removed:
3430 for abs in removed:
3424 target = repo.wjoin(abs)
3431 target = repo.wjoin(abs)
3425 if os.path.lexists(target):
3432 if os.path.lexists(target):
3426 removunk.add(abs)
3433 removunk.add(abs)
3427 removed -= removunk
3434 removed -= removunk
3428
3435
3429 dsremovunk = set()
3436 dsremovunk = set()
3430 for abs in dsremoved:
3437 for abs in dsremoved:
3431 target = repo.wjoin(abs)
3438 target = repo.wjoin(abs)
3432 if os.path.lexists(target):
3439 if os.path.lexists(target):
3433 dsremovunk.add(abs)
3440 dsremovunk.add(abs)
3434 dsremoved -= dsremovunk
3441 dsremoved -= dsremovunk
3435
3442
3436 # action to be actually performed by revert
3443 # action to be actually performed by revert
3437 # (<list of file>, message>) tuple
3444 # (<list of file>, message>) tuple
3438 actions = {
3445 actions = {
3439 b'revert': ([], _(b'reverting %s\n')),
3446 b'revert': ([], _(b'reverting %s\n')),
3440 b'add': ([], _(b'adding %s\n')),
3447 b'add': ([], _(b'adding %s\n')),
3441 b'remove': ([], _(b'removing %s\n')),
3448 b'remove': ([], _(b'removing %s\n')),
3442 b'drop': ([], _(b'removing %s\n')),
3449 b'drop': ([], _(b'removing %s\n')),
3443 b'forget': ([], _(b'forgetting %s\n')),
3450 b'forget': ([], _(b'forgetting %s\n')),
3444 b'undelete': ([], _(b'undeleting %s\n')),
3451 b'undelete': ([], _(b'undeleting %s\n')),
3445 b'noop': (None, _(b'no changes needed to %s\n')),
3452 b'noop': (None, _(b'no changes needed to %s\n')),
3446 b'unknown': (None, _(b'file not managed: %s\n')),
3453 b'unknown': (None, _(b'file not managed: %s\n')),
3447 }
3454 }
3448
3455
3449 # "constant" that convey the backup strategy.
3456 # "constant" that convey the backup strategy.
3450 # 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
3451 # no_backup lower in the code.
3458 # no_backup lower in the code.
3452 # These values are ordered for comparison purposes
3459 # These values are ordered for comparison purposes
3453 backupinteractive = 3 # do backup if interactively modified
3460 backupinteractive = 3 # do backup if interactively modified
3454 backup = 2 # unconditionally do backup
3461 backup = 2 # unconditionally do backup
3455 check = 1 # check if the existing file differs from target
3462 check = 1 # check if the existing file differs from target
3456 discard = 0 # never do backup
3463 discard = 0 # never do backup
3457 if opts.get(b'no_backup'):
3464 if opts.get(b'no_backup'):
3458 backupinteractive = backup = check = discard
3465 backupinteractive = backup = check = discard
3459 if interactive:
3466 if interactive:
3460 dsmodifiedbackup = backupinteractive
3467 dsmodifiedbackup = backupinteractive
3461 else:
3468 else:
3462 dsmodifiedbackup = backup
3469 dsmodifiedbackup = backup
3463 tobackup = set()
3470 tobackup = set()
3464
3471
3465 backupanddel = actions[b'remove']
3472 backupanddel = actions[b'remove']
3466 if not opts.get(b'no_backup'):
3473 if not opts.get(b'no_backup'):
3467 backupanddel = actions[b'drop']
3474 backupanddel = actions[b'drop']
3468
3475
3469 disptable = (
3476 disptable = (
3470 # dispatch table:
3477 # dispatch table:
3471 # file state
3478 # file state
3472 # action
3479 # action
3473 # make backup
3480 # make backup
3474 ## Sets that results that will change file on disk
3481 ## Sets that results that will change file on disk
3475 # Modified compared to target, no local change
3482 # Modified compared to target, no local change
3476 (modified, actions[b'revert'], discard),
3483 (modified, actions[b'revert'], discard),
3477 # Modified compared to target, but local file is deleted
3484 # Modified compared to target, but local file is deleted
3478 (deleted, actions[b'revert'], discard),
3485 (deleted, actions[b'revert'], discard),
3479 # Modified compared to target, local change
3486 # Modified compared to target, local change
3480 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3487 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3481 # Added since target
3488 # Added since target
3482 (added, actions[b'remove'], discard),
3489 (added, actions[b'remove'], discard),
3483 # Added in working directory
3490 # Added in working directory
3484 (dsadded, actions[b'forget'], discard),
3491 (dsadded, actions[b'forget'], discard),
3485 # Added since target, have local modification
3492 # Added since target, have local modification
3486 (modadded, backupanddel, backup),
3493 (modadded, backupanddel, backup),
3487 # Added since target but file is missing in working directory
3494 # Added since target but file is missing in working directory
3488 (deladded, actions[b'drop'], discard),
3495 (deladded, actions[b'drop'], discard),
3489 # Removed since target, before working copy parent
3496 # Removed since target, before working copy parent
3490 (removed, actions[b'add'], discard),
3497 (removed, actions[b'add'], discard),
3491 # 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
3492 (removunk, actions[b'add'], check),
3499 (removunk, actions[b'add'], check),
3493 # Removed since targe, marked as such in working copy parent
3500 # Removed since targe, marked as such in working copy parent
3494 (dsremoved, actions[b'undelete'], discard),
3501 (dsremoved, actions[b'undelete'], discard),
3495 # 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
3496 (dsremovunk, actions[b'undelete'], check),
3503 (dsremovunk, actions[b'undelete'], check),
3497 ## the following sets does not result in any file changes
3504 ## the following sets does not result in any file changes
3498 # File with no modification
3505 # File with no modification
3499 (clean, actions[b'noop'], discard),
3506 (clean, actions[b'noop'], discard),
3500 # Existing file, not tracked anywhere
3507 # Existing file, not tracked anywhere
3501 (unknown, actions[b'unknown'], discard),
3508 (unknown, actions[b'unknown'], discard),
3502 )
3509 )
3503
3510
3504 for abs, exact in sorted(names.items()):
3511 for abs, exact in sorted(names.items()):
3505 # target file to be touch on disk (relative to cwd)
3512 # target file to be touch on disk (relative to cwd)
3506 target = repo.wjoin(abs)
3513 target = repo.wjoin(abs)
3507 # search the entry in the dispatch table.
3514 # search the entry in the dispatch table.
3508 # 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
3509 # directory parent and we are sure it needs to be reverted.
3516 # directory parent and we are sure it needs to be reverted.
3510 for table, (xlist, msg), dobackup in disptable:
3517 for table, (xlist, msg), dobackup in disptable:
3511 if abs not in table:
3518 if abs not in table:
3512 continue
3519 continue
3513 if xlist is not None:
3520 if xlist is not None:
3514 xlist.append(abs)
3521 xlist.append(abs)
3515 if dobackup:
3522 if dobackup:
3516 # If in interactive mode, don't automatically create
3523 # If in interactive mode, don't automatically create
3517 # .orig files (issue4793)
3524 # .orig files (issue4793)
3518 if dobackup == backupinteractive:
3525 if dobackup == backupinteractive:
3519 tobackup.add(abs)
3526 tobackup.add(abs)
3520 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3527 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3521 absbakname = scmutil.backuppath(ui, repo, abs)
3528 absbakname = scmutil.backuppath(ui, repo, abs)
3522 bakname = os.path.relpath(
3529 bakname = os.path.relpath(
3523 absbakname, start=repo.root
3530 absbakname, start=repo.root
3524 )
3531 )
3525 ui.note(
3532 ui.note(
3526 _(b'saving current version of %s as %s\n')
3533 _(b'saving current version of %s as %s\n')
3527 % (uipathfn(abs), uipathfn(bakname))
3534 % (uipathfn(abs), uipathfn(bakname))
3528 )
3535 )
3529 if not opts.get(b'dry_run'):
3536 if not opts.get(b'dry_run'):
3530 if interactive:
3537 if interactive:
3531 util.copyfile(target, absbakname)
3538 util.copyfile(target, absbakname)
3532 else:
3539 else:
3533 util.rename(target, absbakname)
3540 util.rename(target, absbakname)
3534 if opts.get(b'dry_run'):
3541 if opts.get(b'dry_run'):
3535 if ui.verbose or not exact:
3542 if ui.verbose or not exact:
3536 ui.status(msg % uipathfn(abs))
3543 ui.status(msg % uipathfn(abs))
3537 elif exact:
3544 elif exact:
3538 ui.warn(msg % uipathfn(abs))
3545 ui.warn(msg % uipathfn(abs))
3539 break
3546 break
3540
3547
3541 if not opts.get(b'dry_run'):
3548 if not opts.get(b'dry_run'):
3542 needdata = (b'revert', b'add', b'undelete')
3549 needdata = (b'revert', b'add', b'undelete')
3543 oplist = [actions[name][0] for name in needdata]
3550 oplist = [actions[name][0] for name in needdata]
3544 prefetch = scmutil.prefetchfiles
3551 prefetch = scmutil.prefetchfiles
3545 matchfiles = scmutil.matchfiles(
3552 matchfiles = scmutil.matchfiles(
3546 repo, [f for sublist in oplist for f in sublist]
3553 repo, [f for sublist in oplist for f in sublist]
3547 )
3554 )
3548 prefetch(
3555 prefetch(
3549 repo,
3556 repo,
3550 [(ctx.rev(), matchfiles)],
3557 [(ctx.rev(), matchfiles)],
3551 )
3558 )
3552 match = scmutil.match(repo[None], pats)
3559 match = scmutil.match(repo[None], pats)
3553 _performrevert(
3560 _performrevert(
3554 repo,
3561 repo,
3555 ctx,
3562 ctx,
3556 names,
3563 names,
3557 uipathfn,
3564 uipathfn,
3558 actions,
3565 actions,
3559 match,
3566 match,
3560 interactive,
3567 interactive,
3561 tobackup,
3568 tobackup,
3562 )
3569 )
3563
3570
3564 if targetsubs:
3571 if targetsubs:
3565 # Revert the subrepos on the revert list
3572 # Revert the subrepos on the revert list
3566 for sub in targetsubs:
3573 for sub in targetsubs:
3567 try:
3574 try:
3568 wctx.sub(sub).revert(
3575 wctx.sub(sub).revert(
3569 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3576 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3570 )
3577 )
3571 except KeyError:
3578 except KeyError:
3572 raise error.Abort(
3579 raise error.Abort(
3573 b"subrepository '%s' does not exist in %s!"
3580 b"subrepository '%s' does not exist in %s!"
3574 % (sub, short(ctx.node()))
3581 % (sub, short(ctx.node()))
3575 )
3582 )
3576
3583
3577
3584
3578 def _performrevert(
3585 def _performrevert(
3579 repo,
3586 repo,
3580 ctx,
3587 ctx,
3581 names,
3588 names,
3582 uipathfn,
3589 uipathfn,
3583 actions,
3590 actions,
3584 match,
3591 match,
3585 interactive=False,
3592 interactive=False,
3586 tobackup=None,
3593 tobackup=None,
3587 ):
3594 ):
3588 """function that actually perform all the actions computed for revert
3595 """function that actually perform all the actions computed for revert
3589
3596
3590 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
3591 the imminent revert.
3598 the imminent revert.
3592
3599
3593 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.
3594 """
3601 """
3595 parent, p2 = repo.dirstate.parents()
3602 parent, p2 = repo.dirstate.parents()
3596 node = ctx.node()
3603 node = ctx.node()
3597 excluded_files = []
3604 excluded_files = []
3598
3605
3599 def checkout(f):
3606 def checkout(f):
3600 fc = ctx[f]
3607 fc = ctx[f]
3601 repo.wwrite(f, fc.data(), fc.flags())
3608 repo.wwrite(f, fc.data(), fc.flags())
3602
3609
3603 def doremove(f):
3610 def doremove(f):
3604 try:
3611 try:
3605 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3612 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3606 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3613 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3607 except OSError:
3614 except OSError:
3608 pass
3615 pass
3609 repo.dirstate.set_untracked(f)
3616 repo.dirstate.set_untracked(f)
3610
3617
3611 def prntstatusmsg(action, f):
3618 def prntstatusmsg(action, f):
3612 exact = names[f]
3619 exact = names[f]
3613 if repo.ui.verbose or not exact:
3620 if repo.ui.verbose or not exact:
3614 repo.ui.status(actions[action][1] % uipathfn(f))
3621 repo.ui.status(actions[action][1] % uipathfn(f))
3615
3622
3616 audit_path = pathutil.pathauditor(repo.root, cached=True)
3623 audit_path = pathutil.pathauditor(repo.root, cached=True)
3617 for f in actions[b'forget'][0]:
3624 for f in actions[b'forget'][0]:
3618 if interactive:
3625 if interactive:
3619 choice = repo.ui.promptchoice(
3626 choice = repo.ui.promptchoice(
3620 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3627 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3621 )
3628 )
3622 if choice == 0:
3629 if choice == 0:
3623 prntstatusmsg(b'forget', f)
3630 prntstatusmsg(b'forget', f)
3624 repo.dirstate.set_untracked(f)
3631 repo.dirstate.set_untracked(f)
3625 else:
3632 else:
3626 excluded_files.append(f)
3633 excluded_files.append(f)
3627 else:
3634 else:
3628 prntstatusmsg(b'forget', f)
3635 prntstatusmsg(b'forget', f)
3629 repo.dirstate.set_untracked(f)
3636 repo.dirstate.set_untracked(f)
3630 for f in actions[b'remove'][0]:
3637 for f in actions[b'remove'][0]:
3631 audit_path(f)
3638 audit_path(f)
3632 if interactive:
3639 if interactive:
3633 choice = repo.ui.promptchoice(
3640 choice = repo.ui.promptchoice(
3634 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3641 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3635 )
3642 )
3636 if choice == 0:
3643 if choice == 0:
3637 prntstatusmsg(b'remove', f)
3644 prntstatusmsg(b'remove', f)
3638 doremove(f)
3645 doremove(f)
3639 else:
3646 else:
3640 excluded_files.append(f)
3647 excluded_files.append(f)
3641 else:
3648 else:
3642 prntstatusmsg(b'remove', f)
3649 prntstatusmsg(b'remove', f)
3643 doremove(f)
3650 doremove(f)
3644 for f in actions[b'drop'][0]:
3651 for f in actions[b'drop'][0]:
3645 audit_path(f)
3652 audit_path(f)
3646 prntstatusmsg(b'drop', f)
3653 prntstatusmsg(b'drop', f)
3647 repo.dirstate.set_untracked(f)
3654 repo.dirstate.set_untracked(f)
3648
3655
3649 # 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`
3650 # 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
3651 # merges to avoid losing information about copy introduced by the merge.
3658 # merges to avoid losing information about copy introduced by the merge.
3652 # This might comes with bugs ?
3659 # This might comes with bugs ?
3653 reset_copy = p2 == repo.nullid
3660 reset_copy = p2 == repo.nullid
3654
3661
3655 def normal(filename):
3662 def normal(filename):
3656 return repo.dirstate.set_tracked(filename, reset_copy=reset_copy)
3663 return repo.dirstate.set_tracked(filename, reset_copy=reset_copy)
3657
3664
3658 newlyaddedandmodifiedfiles = set()
3665 newlyaddedandmodifiedfiles = set()
3659 if interactive:
3666 if interactive:
3660 # Prompt the user for changes to revert
3667 # Prompt the user for changes to revert
3661 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]
3662 m = scmutil.matchfiles(repo, torevert)
3669 m = scmutil.matchfiles(repo, torevert)
3663 diffopts = patch.difffeatureopts(
3670 diffopts = patch.difffeatureopts(
3664 repo.ui,
3671 repo.ui,
3665 whitespace=True,
3672 whitespace=True,
3666 section=b'commands',
3673 section=b'commands',
3667 configprefix=b'revert.interactive.',
3674 configprefix=b'revert.interactive.',
3668 )
3675 )
3669 diffopts.nodates = True
3676 diffopts.nodates = True
3670 diffopts.git = True
3677 diffopts.git = True
3671 operation = b'apply'
3678 operation = b'apply'
3672 if node == parent:
3679 if node == parent:
3673 if repo.ui.configbool(
3680 if repo.ui.configbool(
3674 b'experimental', b'revert.interactive.select-to-keep'
3681 b'experimental', b'revert.interactive.select-to-keep'
3675 ):
3682 ):
3676 operation = b'keep'
3683 operation = b'keep'
3677 else:
3684 else:
3678 operation = b'discard'
3685 operation = b'discard'
3679
3686
3680 if operation == b'apply':
3687 if operation == b'apply':
3681 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3688 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3682 else:
3689 else:
3683 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3690 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3684 original_headers = patch.parsepatch(diff)
3691 original_headers = patch.parsepatch(diff)
3685
3692
3686 try:
3693 try:
3687
3694
3688 chunks, opts = recordfilter(
3695 chunks, opts = recordfilter(
3689 repo.ui, original_headers, match, operation=operation
3696 repo.ui, original_headers, match, operation=operation
3690 )
3697 )
3691 if operation == b'discard':
3698 if operation == b'discard':
3692 chunks = patch.reversehunks(chunks)
3699 chunks = patch.reversehunks(chunks)
3693
3700
3694 except error.PatchParseError as err:
3701 except error.PatchParseError as err:
3695 raise error.InputError(_(b'error parsing patch: %s') % err)
3702 raise error.InputError(_(b'error parsing patch: %s') % err)
3696 except error.PatchApplicationError as err:
3703 except error.PatchApplicationError as err:
3697 raise error.StateError(_(b'error applying patch: %s') % err)
3704 raise error.StateError(_(b'error applying patch: %s') % err)
3698
3705
3699 # 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
3700 # 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
3701 # "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
3702 # alsorestore value. Ideally we'd be able to partially revert
3709 # alsorestore value. Ideally we'd be able to partially revert
3703 # copied/renamed files.
3710 # copied/renamed files.
3704 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(chunks)
3711 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(chunks)
3705 if tobackup is None:
3712 if tobackup is None:
3706 tobackup = set()
3713 tobackup = set()
3707 # Apply changes
3714 # Apply changes
3708 fp = stringio()
3715 fp = stringio()
3709 # chunks are serialized per file, but files aren't sorted
3716 # chunks are serialized per file, but files aren't sorted
3710 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)}):
3711 prntstatusmsg(b'revert', f)
3718 prntstatusmsg(b'revert', f)
3712 files = set()
3719 files = set()
3713 for c in chunks:
3720 for c in chunks:
3714 if ishunk(c):
3721 if ishunk(c):
3715 abs = c.header.filename()
3722 abs = c.header.filename()
3716 # 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
3717 if c.header.filename() in tobackup:
3724 if c.header.filename() in tobackup:
3718 target = repo.wjoin(abs)
3725 target = repo.wjoin(abs)
3719 bakname = scmutil.backuppath(repo.ui, repo, abs)
3726 bakname = scmutil.backuppath(repo.ui, repo, abs)
3720 util.copyfile(target, bakname)
3727 util.copyfile(target, bakname)
3721 tobackup.remove(abs)
3728 tobackup.remove(abs)
3722 if abs not in files:
3729 if abs not in files:
3723 files.add(abs)
3730 files.add(abs)
3724 if operation == b'keep':
3731 if operation == b'keep':
3725 checkout(abs)
3732 checkout(abs)
3726 c.write(fp)
3733 c.write(fp)
3727 dopatch = fp.tell()
3734 dopatch = fp.tell()
3728 fp.seek(0)
3735 fp.seek(0)
3729 if dopatch:
3736 if dopatch:
3730 try:
3737 try:
3731 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3738 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3732 except error.PatchParseError as err:
3739 except error.PatchParseError as err:
3733 raise error.InputError(pycompat.bytestr(err))
3740 raise error.InputError(pycompat.bytestr(err))
3734 except error.PatchApplicationError as err:
3741 except error.PatchApplicationError as err:
3735 raise error.StateError(pycompat.bytestr(err))
3742 raise error.StateError(pycompat.bytestr(err))
3736 del fp
3743 del fp
3737 else:
3744 else:
3738 for f in actions[b'revert'][0]:
3745 for f in actions[b'revert'][0]:
3739 prntstatusmsg(b'revert', f)
3746 prntstatusmsg(b'revert', f)
3740 checkout(f)
3747 checkout(f)
3741 if normal:
3748 if normal:
3742 normal(f)
3749 normal(f)
3743
3750
3744 for f in actions[b'add'][0]:
3751 for f in actions[b'add'][0]:
3745 # 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
3746 if f not in newlyaddedandmodifiedfiles:
3753 if f not in newlyaddedandmodifiedfiles:
3747 prntstatusmsg(b'add', f)
3754 prntstatusmsg(b'add', f)
3748 checkout(f)
3755 checkout(f)
3749 repo.dirstate.set_tracked(f)
3756 repo.dirstate.set_tracked(f)
3750
3757
3751 for f in actions[b'undelete'][0]:
3758 for f in actions[b'undelete'][0]:
3752 if interactive:
3759 if interactive:
3753 choice = repo.ui.promptchoice(
3760 choice = repo.ui.promptchoice(
3754 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3761 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3755 )
3762 )
3756 if choice == 0:
3763 if choice == 0:
3757 prntstatusmsg(b'undelete', f)
3764 prntstatusmsg(b'undelete', f)
3758 checkout(f)
3765 checkout(f)
3759 normal(f)
3766 normal(f)
3760 else:
3767 else:
3761 excluded_files.append(f)
3768 excluded_files.append(f)
3762 else:
3769 else:
3763 prntstatusmsg(b'undelete', f)
3770 prntstatusmsg(b'undelete', f)
3764 checkout(f)
3771 checkout(f)
3765 normal(f)
3772 normal(f)
3766
3773
3767 copied = copies.pathcopies(repo[parent], ctx)
3774 copied = copies.pathcopies(repo[parent], ctx)
3768
3775
3769 for f in (
3776 for f in (
3770 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]
3771 ):
3778 ):
3772 if f in copied:
3779 if f in copied:
3773 repo.dirstate.copy(copied[f], f)
3780 repo.dirstate.copy(copied[f], f)
3774
3781
3775
3782
3776 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3783 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3777 # commands.outgoing. "missing" is "missing" of the result of
3784 # commands.outgoing. "missing" is "missing" of the result of
3778 # "findcommonoutgoing()"
3785 # "findcommonoutgoing()"
3779 outgoinghooks = util.hooks()
3786 outgoinghooks = util.hooks()
3780
3787
3781 # a list of (ui, repo) functions called by commands.summary
3788 # a list of (ui, repo) functions called by commands.summary
3782 summaryhooks = util.hooks()
3789 summaryhooks = util.hooks()
3783
3790
3784 # 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.
3785 #
3792 #
3786 # functions should return tuple of booleans below, if 'changes' is None:
3793 # functions should return tuple of booleans below, if 'changes' is None:
3787 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3794 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3788 #
3795 #
3789 # otherwise, 'changes' is a tuple of tuples below:
3796 # otherwise, 'changes' is a tuple of tuples below:
3790 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3797 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3791 # - (desturl, destbranch, destpeer, outgoing)
3798 # - (desturl, destbranch, destpeer, outgoing)
3792 summaryremotehooks = util.hooks()
3799 summaryremotehooks = util.hooks()
3793
3800
3794
3801
3795 def checkunfinished(repo, commit=False, skipmerge=False):
3802 def checkunfinished(repo, commit=False, skipmerge=False):
3796 """Look for an unfinished multistep operation, like graft, and abort
3803 """Look for an unfinished multistep operation, like graft, and abort
3797 if found. It's probably good to check this right before
3804 if found. It's probably good to check this right before
3798 bailifchanged().
3805 bailifchanged().
3799 """
3806 """
3800 # 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
3801 # precedence over update.
3808 # precedence over update.
3802 for state in statemod._unfinishedstates:
3809 for state in statemod._unfinishedstates:
3803 if (
3810 if (
3804 state._clearable
3811 state._clearable
3805 or (commit and state._allowcommit)
3812 or (commit and state._allowcommit)
3806 or state._reportonly
3813 or state._reportonly
3807 ):
3814 ):
3808 continue
3815 continue
3809 if state.isunfinished(repo):
3816 if state.isunfinished(repo):
3810 raise error.StateError(state.msg(), hint=state.hint())
3817 raise error.StateError(state.msg(), hint=state.hint())
3811
3818
3812 for s in statemod._unfinishedstates:
3819 for s in statemod._unfinishedstates:
3813 if (
3820 if (
3814 not s._clearable
3821 not s._clearable
3815 or (commit and s._allowcommit)
3822 or (commit and s._allowcommit)
3816 or (s._opname == b'merge' and skipmerge)
3823 or (s._opname == b'merge' and skipmerge)
3817 or s._reportonly
3824 or s._reportonly
3818 ):
3825 ):
3819 continue
3826 continue
3820 if s.isunfinished(repo):
3827 if s.isunfinished(repo):
3821 raise error.StateError(s.msg(), hint=s.hint())
3828 raise error.StateError(s.msg(), hint=s.hint())
3822
3829
3823
3830
3824 def clearunfinished(repo):
3831 def clearunfinished(repo):
3825 """Check for unfinished operations (as above), and clear the ones
3832 """Check for unfinished operations (as above), and clear the ones
3826 that are clearable.
3833 that are clearable.
3827 """
3834 """
3828 for state in statemod._unfinishedstates:
3835 for state in statemod._unfinishedstates:
3829 if state._reportonly:
3836 if state._reportonly:
3830 continue
3837 continue
3831 if not state._clearable and state.isunfinished(repo):
3838 if not state._clearable and state.isunfinished(repo):
3832 raise error.StateError(state.msg(), hint=state.hint())
3839 raise error.StateError(state.msg(), hint=state.hint())
3833
3840
3834 for s in statemod._unfinishedstates:
3841 for s in statemod._unfinishedstates:
3835 if s._opname == b'merge' or s._reportonly:
3842 if s._opname == b'merge' or s._reportonly:
3836 continue
3843 continue
3837 if s._clearable and s.isunfinished(repo):
3844 if s._clearable and s.isunfinished(repo):
3838 util.unlink(repo.vfs.join(s._fname))
3845 util.unlink(repo.vfs.join(s._fname))
3839
3846
3840
3847
3841 def getunfinishedstate(repo):
3848 def getunfinishedstate(repo):
3842 """Checks for unfinished operations and returns statecheck object
3849 """Checks for unfinished operations and returns statecheck object
3843 for it"""
3850 for it"""
3844 for state in statemod._unfinishedstates:
3851 for state in statemod._unfinishedstates:
3845 if state.isunfinished(repo):
3852 if state.isunfinished(repo):
3846 return state
3853 return state
3847 return None
3854 return None
3848
3855
3849
3856
3850 def howtocontinue(repo):
3857 def howtocontinue(repo):
3851 """Check for an unfinished operation and return the command to finish
3858 """Check for an unfinished operation and return the command to finish
3852 it.
3859 it.
3853
3860
3854 statemod._unfinishedstates list is checked for an unfinished operation
3861 statemod._unfinishedstates list is checked for an unfinished operation
3855 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
3856 continue is supported by the operation.
3863 continue is supported by the operation.
3857
3864
3858 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
3859 a boolean.
3866 a boolean.
3860 """
3867 """
3861 contmsg = _(b"continue: %s")
3868 contmsg = _(b"continue: %s")
3862 for state in statemod._unfinishedstates:
3869 for state in statemod._unfinishedstates:
3863 if not state._continueflag:
3870 if not state._continueflag:
3864 continue
3871 continue
3865 if state.isunfinished(repo):
3872 if state.isunfinished(repo):
3866 return contmsg % state.continuemsg(), True
3873 return contmsg % state.continuemsg(), True
3867 if repo[None].dirty(missing=True, merge=False, branch=False):
3874 if repo[None].dirty(missing=True, merge=False, branch=False):
3868 return contmsg % _(b"hg commit"), False
3875 return contmsg % _(b"hg commit"), False
3869 return None, None
3876 return None, None
3870
3877
3871
3878
3872 def checkafterresolved(repo):
3879 def checkafterresolved(repo):
3873 """Inform the user about the next action after completing hg resolve
3880 """Inform the user about the next action after completing hg resolve
3874
3881
3875 If there's a an unfinished operation that supports continue flag,
3882 If there's a an unfinished operation that supports continue flag,
3876 howtocontinue will yield repo.ui.warn as the reporter.
3883 howtocontinue will yield repo.ui.warn as the reporter.
3877
3884
3878 Otherwise, it will yield repo.ui.note.
3885 Otherwise, it will yield repo.ui.note.
3879 """
3886 """
3880 msg, warning = howtocontinue(repo)
3887 msg, warning = howtocontinue(repo)
3881 if msg is not None:
3888 if msg is not None:
3882 if warning:
3889 if warning:
3883 repo.ui.warn(b"%s\n" % msg)
3890 repo.ui.warn(b"%s\n" % msg)
3884 else:
3891 else:
3885 repo.ui.note(b"%s\n" % msg)
3892 repo.ui.note(b"%s\n" % msg)
3886
3893
3887
3894
3888 def wrongtooltocontinue(repo, task):
3895 def wrongtooltocontinue(repo, task):
3889 """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
3890 active task.
3897 active task.
3891
3898
3892 Uses howtocontinue() to find the active task.
3899 Uses howtocontinue() to find the active task.
3893
3900
3894 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
3895 a hint.
3902 a hint.
3896 """
3903 """
3897 after = howtocontinue(repo)
3904 after = howtocontinue(repo)
3898 hint = None
3905 hint = None
3899 if after[1]:
3906 if after[1]:
3900 hint = after[0]
3907 hint = after[0]
3901 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3908 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3902
3909
3903
3910
3904 def abortgraft(ui, repo, graftstate):
3911 def abortgraft(ui, repo, graftstate):
3905 """abort the interrupted graft and rollbacks to the state before interrupted
3912 """abort the interrupted graft and rollbacks to the state before interrupted
3906 graft"""
3913 graft"""
3907 if not graftstate.exists():
3914 if not graftstate.exists():
3908 raise error.StateError(_(b"no interrupted graft to abort"))
3915 raise error.StateError(_(b"no interrupted graft to abort"))
3909 statedata = readgraftstate(repo, graftstate)
3916 statedata = readgraftstate(repo, graftstate)
3910 newnodes = statedata.get(b'newnodes')
3917 newnodes = statedata.get(b'newnodes')
3911 if newnodes is None:
3918 if newnodes is None:
3912 # 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
3913 # the graft
3920 # the graft
3914 raise error.Abort(_(b"cannot abort using an old graftstate"))
3921 raise error.Abort(_(b"cannot abort using an old graftstate"))
3915
3922
3916 # changeset from which graft operation was started
3923 # changeset from which graft operation was started
3917 if len(newnodes) > 0:
3924 if len(newnodes) > 0:
3918 startctx = repo[newnodes[0]].p1()
3925 startctx = repo[newnodes[0]].p1()
3919 else:
3926 else:
3920 startctx = repo[b'.']
3927 startctx = repo[b'.']
3921 # whether to strip or not
3928 # whether to strip or not
3922 cleanup = False
3929 cleanup = False
3923
3930
3924 if newnodes:
3931 if newnodes:
3925 newnodes = [repo[r].rev() for r in newnodes]
3932 newnodes = [repo[r].rev() for r in newnodes]
3926 cleanup = True
3933 cleanup = True
3927 # checking that none of the newnodes turned public or is public
3934 # checking that none of the newnodes turned public or is public
3928 immutable = [c for c in newnodes if not repo[c].mutable()]
3935 immutable = [c for c in newnodes if not repo[c].mutable()]
3929 if immutable:
3936 if immutable:
3930 repo.ui.warn(
3937 repo.ui.warn(
3931 _(b"cannot clean up public changesets %s\n")
3938 _(b"cannot clean up public changesets %s\n")
3932 % b', '.join(bytes(repo[r]) for r in immutable),
3939 % b', '.join(bytes(repo[r]) for r in immutable),
3933 hint=_(b"see 'hg help phases' for details"),
3940 hint=_(b"see 'hg help phases' for details"),
3934 )
3941 )
3935 cleanup = False
3942 cleanup = False
3936
3943
3937 # 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
3938 desc = set(repo.changelog.descendants(newnodes))
3945 desc = set(repo.changelog.descendants(newnodes))
3939 if desc - set(newnodes):
3946 if desc - set(newnodes):
3940 repo.ui.warn(
3947 repo.ui.warn(
3941 _(
3948 _(
3942 b"new changesets detected on destination "
3949 b"new changesets detected on destination "
3943 b"branch, can't strip\n"
3950 b"branch, can't strip\n"
3944 )
3951 )
3945 )
3952 )
3946 cleanup = False
3953 cleanup = False
3947
3954
3948 if cleanup:
3955 if cleanup:
3949 with repo.wlock(), repo.lock():
3956 with repo.wlock(), repo.lock():
3950 mergemod.clean_update(startctx)
3957 mergemod.clean_update(startctx)
3951 # stripping the new nodes created
3958 # stripping the new nodes created
3952 strippoints = [
3959 strippoints = [
3953 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3960 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3954 ]
3961 ]
3955 repair.strip(repo.ui, repo, strippoints, backup=False)
3962 repair.strip(repo.ui, repo, strippoints, backup=False)
3956
3963
3957 if not cleanup:
3964 if not cleanup:
3958 # 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
3959 startctx = repo[b'.']
3966 startctx = repo[b'.']
3960 mergemod.clean_update(startctx)
3967 mergemod.clean_update(startctx)
3961
3968
3962 ui.status(_(b"graft aborted\n"))
3969 ui.status(_(b"graft aborted\n"))
3963 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])
3964 graftstate.delete()
3971 graftstate.delete()
3965 return 0
3972 return 0
3966
3973
3967
3974
3968 def readgraftstate(repo, graftstate):
3975 def readgraftstate(repo, graftstate):
3969 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3976 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3970 """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"""
3971 try:
3978 try:
3972 return graftstate.read()
3979 return graftstate.read()
3973 except error.CorruptedState:
3980 except error.CorruptedState:
3974 nodes = repo.vfs.read(b'graftstate').splitlines()
3981 nodes = repo.vfs.read(b'graftstate').splitlines()
3975 return {b'nodes': nodes}
3982 return {b'nodes': nodes}
3976
3983
3977
3984
3978 def hgabortgraft(ui, repo):
3985 def hgabortgraft(ui, repo):
3979 """abort logic for aborting graft using 'hg abort'"""
3986 """abort logic for aborting graft using 'hg abort'"""
3980 with repo.wlock():
3987 with repo.wlock():
3981 graftstate = statemod.cmdstate(repo, b'graftstate')
3988 graftstate = statemod.cmdstate(repo, b'graftstate')
3982 return abortgraft(ui, repo, graftstate)
3989 return abortgraft(ui, repo, graftstate)
@@ -1,463 +1,463 b''
1 #testcases extra sidedata
1 #testcases extra sidedata
2
2
3 #if extra
3 #if extra
4 $ cat >> $HGRCPATH << EOF
4 $ cat >> $HGRCPATH << EOF
5 > [experimental]
5 > [experimental]
6 > copies.write-to=changeset-only
6 > copies.write-to=changeset-only
7 > copies.read-from=changeset-only
7 > copies.read-from=changeset-only
8 > [alias]
8 > [alias]
9 > changesetcopies = log -r . -T 'files: {files}
9 > changesetcopies = log -r . -T 'files: {files}
10 > {extras % "{ifcontains("files", key, "{key}: {value}\n")}"}
10 > {extras % "{ifcontains("files", key, "{key}: {value}\n")}"}
11 > {extras % "{ifcontains("copies", key, "{key}: {value}\n")}"}'
11 > {extras % "{ifcontains("copies", key, "{key}: {value}\n")}"}'
12 > EOF
12 > EOF
13 #endif
13 #endif
14
14
15 #if sidedata
15 #if sidedata
16 $ cat >> $HGRCPATH << EOF
16 $ cat >> $HGRCPATH << EOF
17 > [format]
17 > [format]
18 > exp-use-copies-side-data-changeset = yes
18 > exp-use-copies-side-data-changeset = yes
19 > EOF
19 > EOF
20 #endif
20 #endif
21
21
22 $ cat >> $HGRCPATH << EOF
22 $ cat >> $HGRCPATH << EOF
23 > [alias]
23 > [alias]
24 > showcopies = log -r . -T '{file_copies % "{source} -> {name}\n"}'
24 > showcopies = log -r . -T '{file_copies % "{source} -> {name}\n"}'
25 > [extensions]
25 > [extensions]
26 > rebase =
26 > rebase =
27 > split =
27 > split =
28 > EOF
28 > EOF
29
29
30 Check that copies are recorded correctly
30 Check that copies are recorded correctly
31
31
32 $ hg init repo
32 $ hg init repo
33 $ cd repo
33 $ cd repo
34 #if sidedata
34 #if sidedata
35 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
35 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
36 format-variant repo config default
36 format-variant repo config default
37 copies-sdc: yes yes no
37 copies-sdc: yes yes no
38 revlog-v2: no no no
38 revlog-v2: no no no
39 changelog-v2: yes yes no
39 changelog-v2: yes yes no
40 #else
40 #else
41 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
41 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
42 format-variant repo config default
42 format-variant repo config default
43 copies-sdc: no no no
43 copies-sdc: no no no
44 revlog-v2: no no no
44 revlog-v2: no no no
45 changelog-v2: no no no
45 changelog-v2: no no no
46 #endif
46 #endif
47 $ echo a > a
47 $ echo a > a
48 $ hg add a
48 $ hg add a
49 $ hg ci -m initial
49 $ hg ci -m initial
50 $ hg cp a b
50 $ hg cp a b
51 $ hg cp a c
51 $ hg cp a c
52 $ hg cp a d
52 $ hg cp a d
53 $ hg ci -m 'copy a to b, c, and d'
53 $ hg ci -m 'copy a to b, c, and d'
54
54
55 #if extra
55 #if extra
56
56
57 $ hg changesetcopies
57 $ hg changesetcopies
58 files: b c d
58 files: b c d
59 filesadded: 0
59 filesadded: 0
60 1
60 1
61 2
61 2
62
62
63 p1copies: 0\x00a (esc)
63 p1copies: 0\x00a (esc)
64 1\x00a (esc)
64 1\x00a (esc)
65 2\x00a (esc)
65 2\x00a (esc)
66 #else
66 #else
67 $ hg debugsidedata -c -v -- -1
67 $ hg debugsidedata -c -v -- -1
68 1 sidedata entries
68 1 sidedata entries
69 entry-0014 size 44
69 entry-0014 size 44
70 '\x00\x00\x00\x04\x00\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x00\x06\x00\x00\x00\x03\x00\x00\x00\x00\x06\x00\x00\x00\x04\x00\x00\x00\x00abcd'
70 '\x00\x00\x00\x04\x00\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x00\x06\x00\x00\x00\x03\x00\x00\x00\x00\x06\x00\x00\x00\x04\x00\x00\x00\x00abcd'
71 #endif
71 #endif
72
72
73 $ hg showcopies
73 $ hg showcopies
74 a -> b
74 a -> b
75 a -> c
75 a -> c
76 a -> d
76 a -> d
77
77
78 #if extra
78 #if extra
79
79
80 $ hg showcopies --config experimental.copies.read-from=compatibility
80 $ hg showcopies --config experimental.copies.read-from=compatibility
81 a -> b
81 a -> b
82 a -> c
82 a -> c
83 a -> d
83 a -> d
84 $ hg showcopies --config experimental.copies.read-from=filelog-only
84 $ hg showcopies --config experimental.copies.read-from=filelog-only
85
85
86 #endif
86 #endif
87
87
88 Check that renames are recorded correctly
88 Check that renames are recorded correctly
89
89
90 $ hg mv b b2
90 $ hg mv b b2
91 $ hg ci -m 'rename b to b2'
91 $ hg ci -m 'rename b to b2'
92
92
93 #if extra
93 #if extra
94
94
95 $ hg changesetcopies
95 $ hg changesetcopies
96 files: b b2
96 files: b b2
97 filesadded: 1
97 filesadded: 1
98 filesremoved: 0
98 filesremoved: 0
99
99
100 p1copies: 1\x00b (esc)
100 p1copies: 1\x00b (esc)
101
101
102 #else
102 #else
103 $ hg debugsidedata -c -v -- -1
103 $ hg debugsidedata -c -v -- -1
104 1 sidedata entries
104 1 sidedata entries
105 entry-0014 size 25
105 entry-0014 size 25
106 '\x00\x00\x00\x02\x0c\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x03\x00\x00\x00\x00bb2'
106 '\x00\x00\x00\x02\x0c\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x03\x00\x00\x00\x00bb2'
107 #endif
107 #endif
108
108
109 $ hg showcopies
109 $ hg showcopies
110 b -> b2
110 b -> b2
111
111
112
112
113 Rename onto existing file. This should get recorded in the changeset files list and in the extras,
113 Rename onto existing file. This should get recorded in the changeset files list and in the extras,
114 even though there is no filelog entry.
114 even though there is no filelog entry.
115
115
116 $ hg cp b2 c --force
116 $ hg cp b2 c --force
117 $ hg st --copies
117 $ hg st --copies
118 M c
118 M c
119 b2
119 b2
120
120
121 #if extra
121 #if extra
122
122
123 $ hg debugindex c
123 $ hg debugindex c
124 rev linkrev nodeid p1 p2
124 rev linkrev nodeid p1 p2
125 0 1 b789fdd96dc2 000000000000 000000000000
125 0 1 b789fdd96dc2 000000000000 000000000000
126
126
127 #else
127 #else
128
128
129 $ hg debugindex c
129 $ hg debugindex c
130 rev linkrev nodeid p1 p2
130 rev linkrev nodeid p1 p2
131 0 1 37d9b5d994ea 000000000000 000000000000
131 0 1 37d9b5d994ea 000000000000 000000000000
132
132
133 #endif
133 #endif
134
134
135
135
136 $ hg ci -m 'move b onto d'
136 $ hg ci -m 'move b onto d'
137
137
138 #if extra
138 #if extra
139
139
140 $ hg changesetcopies
140 $ hg changesetcopies
141 files: c
141 files: c
142
142
143 p1copies: 0\x00b2 (esc)
143 p1copies: 0\x00b2 (esc)
144
144
145 #else
145 #else
146 $ hg debugsidedata -c -v -- -1
146 $ hg debugsidedata -c -v -- -1
147 1 sidedata entries
147 1 sidedata entries
148 entry-0014 size 25
148 entry-0014 size 25
149 '\x00\x00\x00\x02\x00\x00\x00\x00\x02\x00\x00\x00\x00\x16\x00\x00\x00\x03\x00\x00\x00\x00b2c'
149 '\x00\x00\x00\x02\x00\x00\x00\x00\x02\x00\x00\x00\x00\x16\x00\x00\x00\x03\x00\x00\x00\x00b2c'
150 #endif
150 #endif
151
151
152 $ hg showcopies
152 $ hg showcopies
153 b2 -> c
153 b2 -> c
154
154
155 #if extra
155 #if extra
156
156
157 $ hg debugindex c
157 $ hg debugindex c
158 rev linkrev nodeid p1 p2
158 rev linkrev nodeid p1 p2
159 0 1 b789fdd96dc2 000000000000 000000000000
159 0 1 b789fdd96dc2 000000000000 000000000000
160
160
161 #else
161 #else
162
162
163 $ hg debugindex c
163 $ hg debugindex c
164 rev linkrev nodeid p1 p2
164 rev linkrev nodeid p1 p2
165 0 1 37d9b5d994ea 000000000000 000000000000
165 0 1 37d9b5d994ea 000000000000 000000000000
166 1 3 029625640347 000000000000 000000000000
166 1 3 029625640347 000000000000 000000000000
167
167
168 #endif
168 #endif
169
169
170 Create a merge commit with copying done during merge.
170 Create a merge commit with copying done during merge.
171
171
172 $ hg co 0
172 $ hg co 0
173 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
173 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
174 $ hg cp a e
174 $ hg cp a e
175 $ hg cp a f
175 $ hg cp a f
176 $ hg ci -m 'copy a to e and f'
176 $ hg ci -m 'copy a to e and f'
177 created new head
177 created new head
178 $ hg merge 3
178 $ hg merge 3
179 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
179 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
180 (branch merge, don't forget to commit)
180 (branch merge, don't forget to commit)
181 File 'a' exists on both sides, so 'g' could be recorded as being from p1 or p2, but we currently
181 File 'a' exists on both sides, so 'g' could be recorded as being from p1 or p2, but we currently
182 always record it as being from p1
182 always record it as being from p1
183 $ hg cp a g
183 $ hg cp a g
184 File 'd' exists only in p2, so 'h' should be from p2
184 File 'd' exists only in p2, so 'h' should be from p2
185 $ hg cp d h
185 $ hg cp d h
186 File 'f' exists only in p1, so 'i' should be from p1
186 File 'f' exists only in p1, so 'i' should be from p1
187 $ hg cp f i
187 $ hg cp f i
188 $ hg ci -m 'merge'
188 $ hg ci -m 'merge'
189
189
190 #if extra
190 #if extra
191
191
192 $ hg changesetcopies
192 $ hg changesetcopies
193 files: g h i
193 files: g h i
194 filesadded: 0
194 filesadded: 0
195 1
195 1
196 2
196 2
197
197
198 p1copies: 0\x00a (esc)
198 p1copies: 0\x00a (esc)
199 2\x00f (esc)
199 2\x00f (esc)
200 p2copies: 1\x00d (esc)
200 p2copies: 1\x00d (esc)
201
201
202 #else
202 #else
203 $ hg debugsidedata -c -v -- -1
203 $ hg debugsidedata -c -v -- -1
204 1 sidedata entries
204 1 sidedata entries
205 entry-0014 size 64
205 entry-0014 size 64
206 '\x00\x00\x00\x06\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x06\x00\x00\x00\x04\x00\x00\x00\x00\x07\x00\x00\x00\x05\x00\x00\x00\x01\x06\x00\x00\x00\x06\x00\x00\x00\x02adfghi'
206 '\x00\x00\x00\x06\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x06\x00\x00\x00\x04\x00\x00\x00\x00\x07\x00\x00\x00\x05\x00\x00\x00\x01\x06\x00\x00\x00\x06\x00\x00\x00\x02adfghi'
207 #endif
207 #endif
208
208
209 $ hg showcopies
209 $ hg showcopies
210 a -> g
210 a -> g
211 d -> h
211 d -> h
212 f -> i
212 f -> i
213
213
214 Test writing to both changeset and filelog
214 Test writing to both changeset and filelog
215
215
216 $ hg cp a j
216 $ hg cp a j
217 #if extra
217 #if extra
218 $ hg ci -m 'copy a to j' --config experimental.copies.write-to=compatibility
218 $ hg ci -m 'copy a to j' --config experimental.copies.write-to=compatibility
219 $ hg changesetcopies
219 $ hg changesetcopies
220 files: j
220 files: j
221 filesadded: 0
221 filesadded: 0
222 filesremoved:
222 filesremoved:
223
223
224 p1copies: 0\x00a (esc)
224 p1copies: 0\x00a (esc)
225 p2copies:
225 p2copies:
226 #else
226 #else
227 $ hg ci -m 'copy a to j'
227 $ hg ci -m 'copy a to j'
228 $ hg debugsidedata -c -v -- -1
228 $ hg debugsidedata -c -v -- -1
229 1 sidedata entries
229 1 sidedata entries
230 entry-0014 size 24
230 entry-0014 size 24
231 '\x00\x00\x00\x02\x00\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x00aj'
231 '\x00\x00\x00\x02\x00\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x00aj'
232 #endif
232 #endif
233 $ hg debugdata j 0
233 $ hg debugdata j 0
234 \x01 (esc)
234 \x01 (esc)
235 copy: a
235 copy: a
236 copyrev: b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3
236 copyrev: b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3
237 \x01 (esc)
237 \x01 (esc)
238 a
238 a
239 $ hg showcopies
239 $ hg showcopies
240 a -> j
240 a -> j
241 $ hg showcopies --config experimental.copies.read-from=compatibility
241 $ hg showcopies --config experimental.copies.read-from=compatibility
242 a -> j
242 a -> j
243 $ hg showcopies --config experimental.copies.read-from=filelog-only
243 $ hg showcopies --config experimental.copies.read-from=filelog-only
244 a -> j
244 a -> j
245 Existing copy information in the changeset gets removed on amend and writing
245 Existing copy information in the changeset gets removed on amend and writing
246 copy information on to the filelog
246 copy information on to the filelog
247 #if extra
247 #if extra
248 $ hg ci --amend -m 'copy a to j, v2' \
248 $ hg ci --amend -m 'copy a to j, v2' \
249 > --config experimental.copies.write-to=filelog-only
249 > --config experimental.copies.write-to=filelog-only
250 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-*-amend.hg (glob)
250 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-*-amend.hg (glob)
251 $ hg changesetcopies
251 $ hg changesetcopies
252 files: j
252 files: j
253
253
254 #else
254 #else
255 $ hg ci --amend -m 'copy a to j, v2'
255 $ hg ci --amend -m 'copy a to j, v2'
256 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-*-amend.hg (glob)
256 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-*-amend.hg (glob)
257 $ hg debugsidedata -c -v -- -1
257 $ hg debugsidedata -c -v -- -1
258 1 sidedata entries
258 1 sidedata entries
259 entry-0014 size 24
259 entry-0014 size 24
260 '\x00\x00\x00\x02\x00\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x00aj'
260 '\x00\x00\x00\x02\x00\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x00aj'
261 #endif
261 #endif
262 $ hg showcopies --config experimental.copies.read-from=filelog-only
262 $ hg showcopies --config experimental.copies.read-from=filelog-only
263 a -> j
263 a -> j
264 The entries should be written to extras even if they're empty (so the client
264 The entries should be written to extras even if they're empty (so the client
265 won't have to fall back to reading from filelogs)
265 won't have to fall back to reading from filelogs)
266 $ echo x >> j
266 $ echo x >> j
267 #if extra
267 #if extra
268 $ hg ci -m 'modify j' --config experimental.copies.write-to=compatibility
268 $ hg ci -m 'modify j' --config experimental.copies.write-to=compatibility
269 $ hg changesetcopies
269 $ hg changesetcopies
270 files: j
270 files: j
271 filesadded:
271 filesadded:
272 filesremoved:
272 filesremoved:
273
273
274 p1copies:
274 p1copies:
275 p2copies:
275 p2copies:
276 #else
276 #else
277 $ hg ci -m 'modify j'
277 $ hg ci -m 'modify j'
278 $ hg debugsidedata -c -v -- -1
278 $ hg debugsidedata -c -v -- -1
279 1 sidedata entries
279 1 sidedata entries
280 entry-0014 size 14
280 entry-0014 size 14
281 '\x00\x00\x00\x01\x14\x00\x00\x00\x01\x00\x00\x00\x00j'
281 '\x00\x00\x00\x01\x14\x00\x00\x00\x01\x00\x00\x00\x00j'
282 #endif
282 #endif
283
283
284 Test writing only to filelog
284 Test writing only to filelog
285
285
286 $ hg cp a k
286 $ hg cp a k
287 #if extra
287 #if extra
288 $ hg ci -m 'copy a to k' --config experimental.copies.write-to=filelog-only
288 $ hg ci -m 'copy a to k' --config experimental.copies.write-to=filelog-only
289
289
290 $ hg changesetcopies
290 $ hg changesetcopies
291 files: k
291 files: k
292
292
293 #else
293 #else
294 $ hg ci -m 'copy a to k'
294 $ hg ci -m 'copy a to k'
295 $ hg debugsidedata -c -v -- -1
295 $ hg debugsidedata -c -v -- -1
296 1 sidedata entries
296 1 sidedata entries
297 entry-0014 size 24
297 entry-0014 size 24
298 '\x00\x00\x00\x02\x00\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x00ak'
298 '\x00\x00\x00\x02\x00\x00\x00\x00\x01\x00\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x00ak'
299 #endif
299 #endif
300
300
301 $ hg debugdata k 0
301 $ hg debugdata k 0
302 \x01 (esc)
302 \x01 (esc)
303 copy: a
303 copy: a
304 copyrev: b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3
304 copyrev: b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3
305 \x01 (esc)
305 \x01 (esc)
306 a
306 a
307 #if extra
307 #if extra
308 $ hg showcopies
308 $ hg showcopies
309
309
310 $ hg showcopies --config experimental.copies.read-from=compatibility
310 $ hg showcopies --config experimental.copies.read-from=compatibility
311 a -> k
311 a -> k
312 $ hg showcopies --config experimental.copies.read-from=filelog-only
312 $ hg showcopies --config experimental.copies.read-from=filelog-only
313 a -> k
313 a -> k
314 #else
314 #else
315 $ hg showcopies
315 $ hg showcopies
316 a -> k
316 a -> k
317 #endif
317 #endif
318
318
319 Existing copy information is preserved by amend
319 Existing copy information is preserved by amend
320 $ hg cp a l
320 $ hg cp a l
321 $ hg ci -m 'copy a to l'
321 $ hg ci -m 'copy a to l'
322 $ hg showcopies
322 $ hg showcopies
323 a -> l
323 a -> l
324 $ hg ci --amend -m 'new description'
324 $ hg ci --amend -m 'new description'
325 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-*-amend.hg (glob)
325 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-*-amend.hg (glob)
326 $ hg showcopies
326 $ hg showcopies
327 a -> l (no-extra !)
327 a -> l
328 $ cd ..
328 $ cd ..
329
329
330 Test rebasing a commit with copy information
330 Test rebasing a commit with copy information
331
331
332 $ hg init rebase-rename
332 $ hg init rebase-rename
333 $ cd rebase-rename
333 $ cd rebase-rename
334 $ echo a > a
334 $ echo a > a
335 $ hg ci -Aqm 'add a'
335 $ hg ci -Aqm 'add a'
336 $ echo a2 > a
336 $ echo a2 > a
337 $ hg ci -m 'modify a'
337 $ hg ci -m 'modify a'
338 $ hg co -q 0
338 $ hg co -q 0
339 $ hg mv a b
339 $ hg mv a b
340 $ hg ci -qm 'rename a to b'
340 $ hg ci -qm 'rename a to b'
341 Not only do we want this to run in-memory, it shouldn't fall back to
341 Not only do we want this to run in-memory, it shouldn't fall back to
342 on-disk merge (no conflicts), so we force it to be in-memory
342 on-disk merge (no conflicts), so we force it to be in-memory
343 with no fallback.
343 with no fallback.
344 $ hg rebase -d 1 --config rebase.experimental.inmemory=yes --config devel.rebase.force-in-memory-merge=yes
344 $ hg rebase -d 1 --config rebase.experimental.inmemory=yes --config devel.rebase.force-in-memory-merge=yes
345 rebasing 2:* tip "rename a to b" (glob)
345 rebasing 2:* tip "rename a to b" (glob)
346 merging a and b to b
346 merging a and b to b
347 saved backup bundle to $TESTTMP/rebase-rename/.hg/strip-backup/*-*-rebase.hg (glob)
347 saved backup bundle to $TESTTMP/rebase-rename/.hg/strip-backup/*-*-rebase.hg (glob)
348 $ hg st --change . --copies
348 $ hg st --change . --copies
349 A b
349 A b
350 a
350 a
351 R a
351 R a
352 $ cd ..
352 $ cd ..
353
353
354 Test splitting a commit
354 Test splitting a commit
355
355
356 $ hg init split
356 $ hg init split
357 $ cd split
357 $ cd split
358 $ echo a > a
358 $ echo a > a
359 $ echo b > b
359 $ echo b > b
360 $ hg ci -Aqm 'add a and b'
360 $ hg ci -Aqm 'add a and b'
361 $ echo a2 > a
361 $ echo a2 > a
362 $ hg mv b c
362 $ hg mv b c
363 $ hg ci -m 'modify a, move b to c'
363 $ hg ci -m 'modify a, move b to c'
364 $ hg --config ui.interactive=yes split <<EOF
364 $ hg --config ui.interactive=yes split <<EOF
365 > y
365 > y
366 > y
366 > y
367 > n
367 > n
368 > y
368 > y
369 > EOF
369 > EOF
370 diff --git a/a b/a
370 diff --git a/a b/a
371 1 hunks, 1 lines changed
371 1 hunks, 1 lines changed
372 examine changes to 'a'?
372 examine changes to 'a'?
373 (enter ? for help) [Ynesfdaq?] y
373 (enter ? for help) [Ynesfdaq?] y
374
374
375 @@ -1,1 +1,1 @@
375 @@ -1,1 +1,1 @@
376 -a
376 -a
377 +a2
377 +a2
378 record this change to 'a'?
378 record this change to 'a'?
379 (enter ? for help) [Ynesfdaq?] y
379 (enter ? for help) [Ynesfdaq?] y
380
380
381 diff --git a/b b/c
381 diff --git a/b b/c
382 rename from b
382 rename from b
383 rename to c
383 rename to c
384 examine changes to 'b' and 'c'?
384 examine changes to 'b' and 'c'?
385 (enter ? for help) [Ynesfdaq?] n
385 (enter ? for help) [Ynesfdaq?] n
386
386
387 created new head
387 created new head
388 diff --git a/b b/c
388 diff --git a/b b/c
389 rename from b
389 rename from b
390 rename to c
390 rename to c
391 examine changes to 'b' and 'c'?
391 examine changes to 'b' and 'c'?
392 (enter ? for help) [Ynesfdaq?] y
392 (enter ? for help) [Ynesfdaq?] y
393
393
394 saved backup bundle to $TESTTMP/split/.hg/strip-backup/*-*-split.hg (glob)
394 saved backup bundle to $TESTTMP/split/.hg/strip-backup/*-*-split.hg (glob)
395 $ cd ..
395 $ cd ..
396
396
397 Test committing half a rename
397 Test committing half a rename
398
398
399 $ hg init partial
399 $ hg init partial
400 $ cd partial
400 $ cd partial
401 $ echo a > a
401 $ echo a > a
402 $ hg ci -Aqm 'add a'
402 $ hg ci -Aqm 'add a'
403 $ hg mv a b
403 $ hg mv a b
404 $ hg ci -m 'remove a' a
404 $ hg ci -m 'remove a' a
405
405
406 #if sidedata
406 #if sidedata
407
407
408 Test upgrading/downgrading to sidedata storage
408 Test upgrading/downgrading to sidedata storage
409 ==============================================
409 ==============================================
410
410
411 downgrading
411 downgrading
412
412
413 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
413 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
414 format-variant repo config default
414 format-variant repo config default
415 copies-sdc: yes yes no
415 copies-sdc: yes yes no
416 revlog-v2: no no no
416 revlog-v2: no no no
417 changelog-v2: yes yes no
417 changelog-v2: yes yes no
418 $ hg debugsidedata -c -- 0
418 $ hg debugsidedata -c -- 0
419 1 sidedata entries
419 1 sidedata entries
420 entry-0014 size 14
420 entry-0014 size 14
421 $ hg debugsidedata -c -- 1
421 $ hg debugsidedata -c -- 1
422 1 sidedata entries
422 1 sidedata entries
423 entry-0014 size 14
423 entry-0014 size 14
424 $ hg debugsidedata -m -- 0
424 $ hg debugsidedata -m -- 0
425 $ cat << EOF > .hg/hgrc
425 $ cat << EOF > .hg/hgrc
426 > [format]
426 > [format]
427 > exp-use-copies-side-data-changeset = no
427 > exp-use-copies-side-data-changeset = no
428 > [experimental]
428 > [experimental]
429 > revlogv2 = enable-unstable-format-and-corrupt-my-data
429 > revlogv2 = enable-unstable-format-and-corrupt-my-data
430 > EOF
430 > EOF
431 $ hg debugupgraderepo --run --quiet --no-backup > /dev/null
431 $ hg debugupgraderepo --run --quiet --no-backup > /dev/null
432 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
432 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
433 format-variant repo config default
433 format-variant repo config default
434 copies-sdc: no no no
434 copies-sdc: no no no
435 revlog-v2: yes yes no
435 revlog-v2: yes yes no
436 changelog-v2: no no no
436 changelog-v2: no no no
437 $ hg debugsidedata -c -- 0
437 $ hg debugsidedata -c -- 0
438 $ hg debugsidedata -c -- 1
438 $ hg debugsidedata -c -- 1
439 $ hg debugsidedata -m -- 0
439 $ hg debugsidedata -m -- 0
440
440
441 upgrading
441 upgrading
442
442
443 $ cat << EOF > .hg/hgrc
443 $ cat << EOF > .hg/hgrc
444 > [format]
444 > [format]
445 > exp-use-copies-side-data-changeset = yes
445 > exp-use-copies-side-data-changeset = yes
446 > EOF
446 > EOF
447 $ hg debugupgraderepo --run --quiet --no-backup > /dev/null
447 $ hg debugupgraderepo --run --quiet --no-backup > /dev/null
448 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
448 $ hg debugformat -v | egrep 'format-variant|revlog-v2|copies-sdc|changelog-v2'
449 format-variant repo config default
449 format-variant repo config default
450 copies-sdc: yes yes no
450 copies-sdc: yes yes no
451 revlog-v2: no no no
451 revlog-v2: no no no
452 changelog-v2: yes yes no
452 changelog-v2: yes yes no
453 $ hg debugsidedata -c -- 0
453 $ hg debugsidedata -c -- 0
454 1 sidedata entries
454 1 sidedata entries
455 entry-0014 size 14
455 entry-0014 size 14
456 $ hg debugsidedata -c -- 1
456 $ hg debugsidedata -c -- 1
457 1 sidedata entries
457 1 sidedata entries
458 entry-0014 size 14
458 entry-0014 size 14
459 $ hg debugsidedata -m -- 0
459 $ hg debugsidedata -m -- 0
460
460
461 #endif
461 #endif
462
462
463 $ cd ..
463 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now