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