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