##// END OF EJS Templates
branch: delete obsolete message about changing branch of obsolete commit...
Martin von Zweigbergk -
r47785:45dcb63f default
parent child Browse files
Show More
@@ -1,3925 +1,3920
1 # cmdutil.py - help for command processing in mercurial
1 # cmdutil.py - help for command processing in mercurial
2 #
2 #
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy as copymod
10 import copy as copymod
11 import errno
11 import errno
12 import os
12 import os
13 import re
13 import re
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 hex,
17 hex,
18 nullrev,
18 nullrev,
19 short,
19 short,
20 )
20 )
21 from .pycompat import (
21 from .pycompat import (
22 getattr,
22 getattr,
23 open,
23 open,
24 setattr,
24 setattr,
25 )
25 )
26 from .thirdparty import attr
26 from .thirdparty import attr
27
27
28 from . import (
28 from . import (
29 bookmarks,
29 bookmarks,
30 changelog,
30 changelog,
31 copies,
31 copies,
32 crecord as crecordmod,
32 crecord as crecordmod,
33 dirstateguard,
33 dirstateguard,
34 encoding,
34 encoding,
35 error,
35 error,
36 formatter,
36 formatter,
37 logcmdutil,
37 logcmdutil,
38 match as matchmod,
38 match as matchmod,
39 merge as mergemod,
39 merge as mergemod,
40 mergestate as mergestatemod,
40 mergestate as mergestatemod,
41 mergeutil,
41 mergeutil,
42 obsolete,
42 obsolete,
43 patch,
43 patch,
44 pathutil,
44 pathutil,
45 phases,
45 phases,
46 pycompat,
46 pycompat,
47 repair,
47 repair,
48 revlog,
48 revlog,
49 rewriteutil,
49 rewriteutil,
50 scmutil,
50 scmutil,
51 state as statemod,
51 state as statemod,
52 subrepoutil,
52 subrepoutil,
53 templatekw,
53 templatekw,
54 templater,
54 templater,
55 util,
55 util,
56 vfs as vfsmod,
56 vfs as vfsmod,
57 )
57 )
58
58
59 from .utils import (
59 from .utils import (
60 dateutil,
60 dateutil,
61 stringutil,
61 stringutil,
62 )
62 )
63
63
64 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 b'''Unresolved merge conflicts:
872 b'''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):
1001 raise error.InputError(
1002 _(b"cannot change branch of a obsolete changeset")
1003 )
1004
1005 # make sure only topological heads
1000 # make sure only topological heads
1006 if repo.revs(b'heads(%ld) - head()', revs):
1001 if repo.revs(b'heads(%ld) - head()', revs):
1007 raise error.InputError(
1002 raise error.InputError(
1008 _(b"cannot change branch in middle of a stack")
1003 _(b"cannot change branch in middle of a stack")
1009 )
1004 )
1010
1005
1011 replacements = {}
1006 replacements = {}
1012 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
1007 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
1013 # mercurial.subrepo -> mercurial.cmdutil
1008 # mercurial.subrepo -> mercurial.cmdutil
1014 from . import context
1009 from . import context
1015
1010
1016 for rev in revs:
1011 for rev in revs:
1017 ctx = repo[rev]
1012 ctx = repo[rev]
1018 oldbranch = ctx.branch()
1013 oldbranch = ctx.branch()
1019 # check if ctx has same branch
1014 # check if ctx has same branch
1020 if oldbranch == label:
1015 if oldbranch == label:
1021 continue
1016 continue
1022
1017
1023 def filectxfn(repo, newctx, path):
1018 def filectxfn(repo, newctx, path):
1024 try:
1019 try:
1025 return ctx[path]
1020 return ctx[path]
1026 except error.ManifestLookupError:
1021 except error.ManifestLookupError:
1027 return None
1022 return None
1028
1023
1029 ui.debug(
1024 ui.debug(
1030 b"changing branch of '%s' from '%s' to '%s'\n"
1025 b"changing branch of '%s' from '%s' to '%s'\n"
1031 % (hex(ctx.node()), oldbranch, label)
1026 % (hex(ctx.node()), oldbranch, label)
1032 )
1027 )
1033 extra = ctx.extra()
1028 extra = ctx.extra()
1034 extra[b'branch_change'] = hex(ctx.node())
1029 extra[b'branch_change'] = hex(ctx.node())
1035 # While changing branch of set of linear commits, make sure that
1030 # While changing branch of set of linear commits, make sure that
1036 # we base our commits on new parent rather than old parent which
1031 # we base our commits on new parent rather than old parent which
1037 # was obsoleted while changing the branch
1032 # was obsoleted while changing the branch
1038 p1 = ctx.p1().node()
1033 p1 = ctx.p1().node()
1039 p2 = ctx.p2().node()
1034 p2 = ctx.p2().node()
1040 if p1 in replacements:
1035 if p1 in replacements:
1041 p1 = replacements[p1][0]
1036 p1 = replacements[p1][0]
1042 if p2 in replacements:
1037 if p2 in replacements:
1043 p2 = replacements[p2][0]
1038 p2 = replacements[p2][0]
1044
1039
1045 mc = context.memctx(
1040 mc = context.memctx(
1046 repo,
1041 repo,
1047 (p1, p2),
1042 (p1, p2),
1048 ctx.description(),
1043 ctx.description(),
1049 ctx.files(),
1044 ctx.files(),
1050 filectxfn,
1045 filectxfn,
1051 user=ctx.user(),
1046 user=ctx.user(),
1052 date=ctx.date(),
1047 date=ctx.date(),
1053 extra=extra,
1048 extra=extra,
1054 branch=label,
1049 branch=label,
1055 )
1050 )
1056
1051
1057 newnode = repo.commitctx(mc)
1052 newnode = repo.commitctx(mc)
1058 replacements[ctx.node()] = (newnode,)
1053 replacements[ctx.node()] = (newnode,)
1059 ui.debug(b'new node id is %s\n' % hex(newnode))
1054 ui.debug(b'new node id is %s\n' % hex(newnode))
1060
1055
1061 # create obsmarkers and move bookmarks
1056 # create obsmarkers and move bookmarks
1062 scmutil.cleanupnodes(
1057 scmutil.cleanupnodes(
1063 repo, replacements, b'branch-change', fixphase=True
1058 repo, replacements, b'branch-change', fixphase=True
1064 )
1059 )
1065
1060
1066 # move the working copy too
1061 # move the working copy too
1067 wctx = repo[None]
1062 wctx = repo[None]
1068 # in-progress merge is a bit too complex for now.
1063 # in-progress merge is a bit too complex for now.
1069 if len(wctx.parents()) == 1:
1064 if len(wctx.parents()) == 1:
1070 newid = replacements.get(wctx.p1().node())
1065 newid = replacements.get(wctx.p1().node())
1071 if newid is not None:
1066 if newid is not None:
1072 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
1067 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
1073 # mercurial.cmdutil
1068 # mercurial.cmdutil
1074 from . import hg
1069 from . import hg
1075
1070
1076 hg.update(repo, newid[0], quietempty=True)
1071 hg.update(repo, newid[0], quietempty=True)
1077
1072
1078 ui.status(_(b"changed branch on %d changesets\n") % len(replacements))
1073 ui.status(_(b"changed branch on %d changesets\n") % len(replacements))
1079
1074
1080
1075
1081 def findrepo(p):
1076 def findrepo(p):
1082 while not os.path.isdir(os.path.join(p, b".hg")):
1077 while not os.path.isdir(os.path.join(p, b".hg")):
1083 oldp, p = p, os.path.dirname(p)
1078 oldp, p = p, os.path.dirname(p)
1084 if p == oldp:
1079 if p == oldp:
1085 return None
1080 return None
1086
1081
1087 return p
1082 return p
1088
1083
1089
1084
1090 def bailifchanged(repo, merge=True, hint=None):
1085 def bailifchanged(repo, merge=True, hint=None):
1091 """enforce the precondition that working directory must be clean.
1086 """enforce the precondition that working directory must be clean.
1092
1087
1093 'merge' can be set to false if a pending uncommitted merge should be
1088 'merge' can be set to false if a pending uncommitted merge should be
1094 ignored (such as when 'update --check' runs).
1089 ignored (such as when 'update --check' runs).
1095
1090
1096 'hint' is the usual hint given to Abort exception.
1091 'hint' is the usual hint given to Abort exception.
1097 """
1092 """
1098
1093
1099 if merge and repo.dirstate.p2() != repo.nullid:
1094 if merge and repo.dirstate.p2() != repo.nullid:
1100 raise error.StateError(_(b'outstanding uncommitted merge'), hint=hint)
1095 raise error.StateError(_(b'outstanding uncommitted merge'), hint=hint)
1101 st = repo.status()
1096 st = repo.status()
1102 if st.modified or st.added or st.removed or st.deleted:
1097 if st.modified or st.added or st.removed or st.deleted:
1103 raise error.StateError(_(b'uncommitted changes'), hint=hint)
1098 raise error.StateError(_(b'uncommitted changes'), hint=hint)
1104 ctx = repo[None]
1099 ctx = repo[None]
1105 for s in sorted(ctx.substate):
1100 for s in sorted(ctx.substate):
1106 ctx.sub(s).bailifchanged(hint=hint)
1101 ctx.sub(s).bailifchanged(hint=hint)
1107
1102
1108
1103
1109 def logmessage(ui, opts):
1104 def logmessage(ui, opts):
1110 """ get the log message according to -m and -l option """
1105 """ get the log message according to -m and -l option """
1111
1106
1112 check_at_most_one_arg(opts, b'message', b'logfile')
1107 check_at_most_one_arg(opts, b'message', b'logfile')
1113
1108
1114 message = opts.get(b'message')
1109 message = opts.get(b'message')
1115 logfile = opts.get(b'logfile')
1110 logfile = opts.get(b'logfile')
1116
1111
1117 if not message and logfile:
1112 if not message and logfile:
1118 try:
1113 try:
1119 if isstdiofilename(logfile):
1114 if isstdiofilename(logfile):
1120 message = ui.fin.read()
1115 message = ui.fin.read()
1121 else:
1116 else:
1122 message = b'\n'.join(util.readfile(logfile).splitlines())
1117 message = b'\n'.join(util.readfile(logfile).splitlines())
1123 except IOError as inst:
1118 except IOError as inst:
1124 raise error.Abort(
1119 raise error.Abort(
1125 _(b"can't read commit message '%s': %s")
1120 _(b"can't read commit message '%s': %s")
1126 % (logfile, encoding.strtolocal(inst.strerror))
1121 % (logfile, encoding.strtolocal(inst.strerror))
1127 )
1122 )
1128 return message
1123 return message
1129
1124
1130
1125
1131 def mergeeditform(ctxorbool, baseformname):
1126 def mergeeditform(ctxorbool, baseformname):
1132 """return appropriate editform name (referencing a committemplate)
1127 """return appropriate editform name (referencing a committemplate)
1133
1128
1134 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
1129 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
1135 merging is committed.
1130 merging is committed.
1136
1131
1137 This returns baseformname with '.merge' appended if it is a merge,
1132 This returns baseformname with '.merge' appended if it is a merge,
1138 otherwise '.normal' is appended.
1133 otherwise '.normal' is appended.
1139 """
1134 """
1140 if isinstance(ctxorbool, bool):
1135 if isinstance(ctxorbool, bool):
1141 if ctxorbool:
1136 if ctxorbool:
1142 return baseformname + b".merge"
1137 return baseformname + b".merge"
1143 elif len(ctxorbool.parents()) > 1:
1138 elif len(ctxorbool.parents()) > 1:
1144 return baseformname + b".merge"
1139 return baseformname + b".merge"
1145
1140
1146 return baseformname + b".normal"
1141 return baseformname + b".normal"
1147
1142
1148
1143
1149 def getcommiteditor(
1144 def getcommiteditor(
1150 edit=False, finishdesc=None, extramsg=None, editform=b'', **opts
1145 edit=False, finishdesc=None, extramsg=None, editform=b'', **opts
1151 ):
1146 ):
1152 """get appropriate commit message editor according to '--edit' option
1147 """get appropriate commit message editor according to '--edit' option
1153
1148
1154 'finishdesc' is a function to be called with edited commit message
1149 'finishdesc' is a function to be called with edited commit message
1155 (= 'description' of the new changeset) just after editing, but
1150 (= 'description' of the new changeset) just after editing, but
1156 before checking empty-ness. It should return actual text to be
1151 before checking empty-ness. It should return actual text to be
1157 stored into history. This allows to change description before
1152 stored into history. This allows to change description before
1158 storing.
1153 storing.
1159
1154
1160 'extramsg' is a extra message to be shown in the editor instead of
1155 '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
1156 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
1162 is automatically added.
1157 is automatically added.
1163
1158
1164 'editform' is a dot-separated list of names, to distinguish
1159 'editform' is a dot-separated list of names, to distinguish
1165 the purpose of commit text editing.
1160 the purpose of commit text editing.
1166
1161
1167 'getcommiteditor' returns 'commitforceeditor' regardless of
1162 'getcommiteditor' returns 'commitforceeditor' regardless of
1168 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
1163 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
1169 they are specific for usage in MQ.
1164 they are specific for usage in MQ.
1170 """
1165 """
1171 if edit or finishdesc or extramsg:
1166 if edit or finishdesc or extramsg:
1172 return lambda r, c, s: commitforceeditor(
1167 return lambda r, c, s: commitforceeditor(
1173 r, c, s, finishdesc=finishdesc, extramsg=extramsg, editform=editform
1168 r, c, s, finishdesc=finishdesc, extramsg=extramsg, editform=editform
1174 )
1169 )
1175 elif editform:
1170 elif editform:
1176 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
1171 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
1177 else:
1172 else:
1178 return commiteditor
1173 return commiteditor
1179
1174
1180
1175
1181 def _escapecommandtemplate(tmpl):
1176 def _escapecommandtemplate(tmpl):
1182 parts = []
1177 parts = []
1183 for typ, start, end in templater.scantemplate(tmpl, raw=True):
1178 for typ, start, end in templater.scantemplate(tmpl, raw=True):
1184 if typ == b'string':
1179 if typ == b'string':
1185 parts.append(stringutil.escapestr(tmpl[start:end]))
1180 parts.append(stringutil.escapestr(tmpl[start:end]))
1186 else:
1181 else:
1187 parts.append(tmpl[start:end])
1182 parts.append(tmpl[start:end])
1188 return b''.join(parts)
1183 return b''.join(parts)
1189
1184
1190
1185
1191 def rendercommandtemplate(ui, tmpl, props):
1186 def rendercommandtemplate(ui, tmpl, props):
1192 r"""Expand a literal template 'tmpl' in a way suitable for command line
1187 r"""Expand a literal template 'tmpl' in a way suitable for command line
1193
1188
1194 '\' in outermost string is not taken as an escape character because it
1189 '\' in outermost string is not taken as an escape character because it
1195 is a directory separator on Windows.
1190 is a directory separator on Windows.
1196
1191
1197 >>> from . import ui as uimod
1192 >>> from . import ui as uimod
1198 >>> ui = uimod.ui()
1193 >>> ui = uimod.ui()
1199 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
1194 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
1200 'c:\\foo'
1195 'c:\\foo'
1201 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
1196 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
1202 'c:{path}'
1197 'c:{path}'
1203 """
1198 """
1204 if not tmpl:
1199 if not tmpl:
1205 return tmpl
1200 return tmpl
1206 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
1201 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
1207 return t.renderdefault(props)
1202 return t.renderdefault(props)
1208
1203
1209
1204
1210 def rendertemplate(ctx, tmpl, props=None):
1205 def rendertemplate(ctx, tmpl, props=None):
1211 """Expand a literal template 'tmpl' byte-string against one changeset
1206 """Expand a literal template 'tmpl' byte-string against one changeset
1212
1207
1213 Each props item must be a stringify-able value or a callable returning
1208 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.
1209 such value, i.e. no bare list nor dict should be passed.
1215 """
1210 """
1216 repo = ctx.repo()
1211 repo = ctx.repo()
1217 tres = formatter.templateresources(repo.ui, repo)
1212 tres = formatter.templateresources(repo.ui, repo)
1218 t = formatter.maketemplater(
1213 t = formatter.maketemplater(
1219 repo.ui, tmpl, defaults=templatekw.keywords, resources=tres
1214 repo.ui, tmpl, defaults=templatekw.keywords, resources=tres
1220 )
1215 )
1221 mapping = {b'ctx': ctx}
1216 mapping = {b'ctx': ctx}
1222 if props:
1217 if props:
1223 mapping.update(props)
1218 mapping.update(props)
1224 return t.renderdefault(mapping)
1219 return t.renderdefault(mapping)
1225
1220
1226
1221
1227 def format_changeset_summary(ui, ctx, command=None, default_spec=None):
1222 def format_changeset_summary(ui, ctx, command=None, default_spec=None):
1228 """Format a changeset summary (one line)."""
1223 """Format a changeset summary (one line)."""
1229 spec = None
1224 spec = None
1230 if command:
1225 if command:
1231 spec = ui.config(
1226 spec = ui.config(
1232 b'command-templates', b'oneline-summary.%s' % command, None
1227 b'command-templates', b'oneline-summary.%s' % command, None
1233 )
1228 )
1234 if not spec:
1229 if not spec:
1235 spec = ui.config(b'command-templates', b'oneline-summary')
1230 spec = ui.config(b'command-templates', b'oneline-summary')
1236 if not spec:
1231 if not spec:
1237 spec = default_spec
1232 spec = default_spec
1238 if not spec:
1233 if not spec:
1239 spec = (
1234 spec = (
1240 b'{separate(" ", '
1235 b'{separate(" ", '
1241 b'label("oneline-summary.changeset", "{rev}:{node|short}")'
1236 b'label("oneline-summary.changeset", "{rev}:{node|short}")'
1242 b', '
1237 b', '
1243 b'join(filter(namespaces % "{ifeq(namespace, "branches", "", join(names % "{label("oneline-summary.{namespace}", name)}", " "))}"), " ")'
1238 b'join(filter(namespaces % "{ifeq(namespace, "branches", "", join(names % "{label("oneline-summary.{namespace}", name)}", " "))}"), " ")'
1244 b')} '
1239 b')} '
1245 b'"{label("oneline-summary.desc", desc|firstline)}"'
1240 b'"{label("oneline-summary.desc", desc|firstline)}"'
1246 )
1241 )
1247 text = rendertemplate(ctx, spec)
1242 text = rendertemplate(ctx, spec)
1248 return text.split(b'\n')[0]
1243 return text.split(b'\n')[0]
1249
1244
1250
1245
1251 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
1246 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
1252 r"""Convert old-style filename format string to template string
1247 r"""Convert old-style filename format string to template string
1253
1248
1254 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
1249 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
1255 'foo-{reporoot|basename}-{seqno}.patch'
1250 'foo-{reporoot|basename}-{seqno}.patch'
1256 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
1251 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
1257 '{rev}{tags % "{tag}"}{node}'
1252 '{rev}{tags % "{tag}"}{node}'
1258
1253
1259 '\' in outermost strings has to be escaped because it is a directory
1254 '\' in outermost strings has to be escaped because it is a directory
1260 separator on Windows:
1255 separator on Windows:
1261
1256
1262 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
1257 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
1263 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
1258 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
1264 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
1259 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
1265 '\\\\\\\\foo\\\\bar.patch'
1260 '\\\\\\\\foo\\\\bar.patch'
1266 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
1261 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
1267 '\\\\{tags % "{tag}"}'
1262 '\\\\{tags % "{tag}"}'
1268
1263
1269 but inner strings follow the template rules (i.e. '\' is taken as an
1264 but inner strings follow the template rules (i.e. '\' is taken as an
1270 escape character):
1265 escape character):
1271
1266
1272 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
1267 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
1273 '{"c:\\tmp"}'
1268 '{"c:\\tmp"}'
1274 """
1269 """
1275 expander = {
1270 expander = {
1276 b'H': b'{node}',
1271 b'H': b'{node}',
1277 b'R': b'{rev}',
1272 b'R': b'{rev}',
1278 b'h': b'{node|short}',
1273 b'h': b'{node|short}',
1279 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
1274 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
1280 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
1275 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
1281 b'%': b'%',
1276 b'%': b'%',
1282 b'b': b'{reporoot|basename}',
1277 b'b': b'{reporoot|basename}',
1283 }
1278 }
1284 if total is not None:
1279 if total is not None:
1285 expander[b'N'] = b'{total}'
1280 expander[b'N'] = b'{total}'
1286 if seqno is not None:
1281 if seqno is not None:
1287 expander[b'n'] = b'{seqno}'
1282 expander[b'n'] = b'{seqno}'
1288 if total is not None and seqno is not None:
1283 if total is not None and seqno is not None:
1289 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
1284 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
1290 if pathname is not None:
1285 if pathname is not None:
1291 expander[b's'] = b'{pathname|basename}'
1286 expander[b's'] = b'{pathname|basename}'
1292 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
1287 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
1293 expander[b'p'] = b'{pathname}'
1288 expander[b'p'] = b'{pathname}'
1294
1289
1295 newname = []
1290 newname = []
1296 for typ, start, end in templater.scantemplate(pat, raw=True):
1291 for typ, start, end in templater.scantemplate(pat, raw=True):
1297 if typ != b'string':
1292 if typ != b'string':
1298 newname.append(pat[start:end])
1293 newname.append(pat[start:end])
1299 continue
1294 continue
1300 i = start
1295 i = start
1301 while i < end:
1296 while i < end:
1302 n = pat.find(b'%', i, end)
1297 n = pat.find(b'%', i, end)
1303 if n < 0:
1298 if n < 0:
1304 newname.append(stringutil.escapestr(pat[i:end]))
1299 newname.append(stringutil.escapestr(pat[i:end]))
1305 break
1300 break
1306 newname.append(stringutil.escapestr(pat[i:n]))
1301 newname.append(stringutil.escapestr(pat[i:n]))
1307 if n + 2 > end:
1302 if n + 2 > end:
1308 raise error.Abort(
1303 raise error.Abort(
1309 _(b"incomplete format spec in output filename")
1304 _(b"incomplete format spec in output filename")
1310 )
1305 )
1311 c = pat[n + 1 : n + 2]
1306 c = pat[n + 1 : n + 2]
1312 i = n + 2
1307 i = n + 2
1313 try:
1308 try:
1314 newname.append(expander[c])
1309 newname.append(expander[c])
1315 except KeyError:
1310 except KeyError:
1316 raise error.Abort(
1311 raise error.Abort(
1317 _(b"invalid format spec '%%%s' in output filename") % c
1312 _(b"invalid format spec '%%%s' in output filename") % c
1318 )
1313 )
1319 return b''.join(newname)
1314 return b''.join(newname)
1320
1315
1321
1316
1322 def makefilename(ctx, pat, **props):
1317 def makefilename(ctx, pat, **props):
1323 if not pat:
1318 if not pat:
1324 return pat
1319 return pat
1325 tmpl = _buildfntemplate(pat, **props)
1320 tmpl = _buildfntemplate(pat, **props)
1326 # BUG: alias expansion shouldn't be made against template fragments
1321 # BUG: alias expansion shouldn't be made against template fragments
1327 # rewritten from %-format strings, but we have no easy way to partially
1322 # rewritten from %-format strings, but we have no easy way to partially
1328 # disable the expansion.
1323 # disable the expansion.
1329 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
1324 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
1330
1325
1331
1326
1332 def isstdiofilename(pat):
1327 def isstdiofilename(pat):
1333 """True if the given pat looks like a filename denoting stdin/stdout"""
1328 """True if the given pat looks like a filename denoting stdin/stdout"""
1334 return not pat or pat == b'-'
1329 return not pat or pat == b'-'
1335
1330
1336
1331
1337 class _unclosablefile(object):
1332 class _unclosablefile(object):
1338 def __init__(self, fp):
1333 def __init__(self, fp):
1339 self._fp = fp
1334 self._fp = fp
1340
1335
1341 def close(self):
1336 def close(self):
1342 pass
1337 pass
1343
1338
1344 def __iter__(self):
1339 def __iter__(self):
1345 return iter(self._fp)
1340 return iter(self._fp)
1346
1341
1347 def __getattr__(self, attr):
1342 def __getattr__(self, attr):
1348 return getattr(self._fp, attr)
1343 return getattr(self._fp, attr)
1349
1344
1350 def __enter__(self):
1345 def __enter__(self):
1351 return self
1346 return self
1352
1347
1353 def __exit__(self, exc_type, exc_value, exc_tb):
1348 def __exit__(self, exc_type, exc_value, exc_tb):
1354 pass
1349 pass
1355
1350
1356
1351
1357 def makefileobj(ctx, pat, mode=b'wb', **props):
1352 def makefileobj(ctx, pat, mode=b'wb', **props):
1358 writable = mode not in (b'r', b'rb')
1353 writable = mode not in (b'r', b'rb')
1359
1354
1360 if isstdiofilename(pat):
1355 if isstdiofilename(pat):
1361 repo = ctx.repo()
1356 repo = ctx.repo()
1362 if writable:
1357 if writable:
1363 fp = repo.ui.fout
1358 fp = repo.ui.fout
1364 else:
1359 else:
1365 fp = repo.ui.fin
1360 fp = repo.ui.fin
1366 return _unclosablefile(fp)
1361 return _unclosablefile(fp)
1367 fn = makefilename(ctx, pat, **props)
1362 fn = makefilename(ctx, pat, **props)
1368 return open(fn, mode)
1363 return open(fn, mode)
1369
1364
1370
1365
1371 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1366 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1372 """opens the changelog, manifest, a filelog or a given revlog"""
1367 """opens the changelog, manifest, a filelog or a given revlog"""
1373 cl = opts[b'changelog']
1368 cl = opts[b'changelog']
1374 mf = opts[b'manifest']
1369 mf = opts[b'manifest']
1375 dir = opts[b'dir']
1370 dir = opts[b'dir']
1376 msg = None
1371 msg = None
1377 if cl and mf:
1372 if cl and mf:
1378 msg = _(b'cannot specify --changelog and --manifest at the same time')
1373 msg = _(b'cannot specify --changelog and --manifest at the same time')
1379 elif cl and dir:
1374 elif cl and dir:
1380 msg = _(b'cannot specify --changelog and --dir at the same time')
1375 msg = _(b'cannot specify --changelog and --dir at the same time')
1381 elif cl or mf or dir:
1376 elif cl or mf or dir:
1382 if file_:
1377 if file_:
1383 msg = _(b'cannot specify filename with --changelog or --manifest')
1378 msg = _(b'cannot specify filename with --changelog or --manifest')
1384 elif not repo:
1379 elif not repo:
1385 msg = _(
1380 msg = _(
1386 b'cannot specify --changelog or --manifest or --dir '
1381 b'cannot specify --changelog or --manifest or --dir '
1387 b'without a repository'
1382 b'without a repository'
1388 )
1383 )
1389 if msg:
1384 if msg:
1390 raise error.InputError(msg)
1385 raise error.InputError(msg)
1391
1386
1392 r = None
1387 r = None
1393 if repo:
1388 if repo:
1394 if cl:
1389 if cl:
1395 r = repo.unfiltered().changelog
1390 r = repo.unfiltered().changelog
1396 elif dir:
1391 elif dir:
1397 if not scmutil.istreemanifest(repo):
1392 if not scmutil.istreemanifest(repo):
1398 raise error.InputError(
1393 raise error.InputError(
1399 _(
1394 _(
1400 b"--dir can only be used on repos with "
1395 b"--dir can only be used on repos with "
1401 b"treemanifest enabled"
1396 b"treemanifest enabled"
1402 )
1397 )
1403 )
1398 )
1404 if not dir.endswith(b'/'):
1399 if not dir.endswith(b'/'):
1405 dir = dir + b'/'
1400 dir = dir + b'/'
1406 dirlog = repo.manifestlog.getstorage(dir)
1401 dirlog = repo.manifestlog.getstorage(dir)
1407 if len(dirlog):
1402 if len(dirlog):
1408 r = dirlog
1403 r = dirlog
1409 elif mf:
1404 elif mf:
1410 r = repo.manifestlog.getstorage(b'')
1405 r = repo.manifestlog.getstorage(b'')
1411 elif file_:
1406 elif file_:
1412 filelog = repo.file(file_)
1407 filelog = repo.file(file_)
1413 if len(filelog):
1408 if len(filelog):
1414 r = filelog
1409 r = filelog
1415
1410
1416 # Not all storage may be revlogs. If requested, try to return an actual
1411 # Not all storage may be revlogs. If requested, try to return an actual
1417 # revlog instance.
1412 # revlog instance.
1418 if returnrevlog:
1413 if returnrevlog:
1419 if isinstance(r, revlog.revlog):
1414 if isinstance(r, revlog.revlog):
1420 pass
1415 pass
1421 elif util.safehasattr(r, b'_revlog'):
1416 elif util.safehasattr(r, b'_revlog'):
1422 r = r._revlog # pytype: disable=attribute-error
1417 r = r._revlog # pytype: disable=attribute-error
1423 elif r is not None:
1418 elif r is not None:
1424 raise error.InputError(
1419 raise error.InputError(
1425 _(b'%r does not appear to be a revlog') % r
1420 _(b'%r does not appear to be a revlog') % r
1426 )
1421 )
1427
1422
1428 if not r:
1423 if not r:
1429 if not returnrevlog:
1424 if not returnrevlog:
1430 raise error.InputError(_(b'cannot give path to non-revlog'))
1425 raise error.InputError(_(b'cannot give path to non-revlog'))
1431
1426
1432 if not file_:
1427 if not file_:
1433 raise error.CommandError(cmd, _(b'invalid arguments'))
1428 raise error.CommandError(cmd, _(b'invalid arguments'))
1434 if not os.path.isfile(file_):
1429 if not os.path.isfile(file_):
1435 raise error.InputError(_(b"revlog '%s' not found") % file_)
1430 raise error.InputError(_(b"revlog '%s' not found") % file_)
1436 r = revlog.revlog(
1431 r = revlog.revlog(
1437 vfsmod.vfs(encoding.getcwd(), audit=False), file_[:-2] + b".i"
1432 vfsmod.vfs(encoding.getcwd(), audit=False), file_[:-2] + b".i"
1438 )
1433 )
1439 return r
1434 return r
1440
1435
1441
1436
1442 def openrevlog(repo, cmd, file_, opts):
1437 def openrevlog(repo, cmd, file_, opts):
1443 """Obtain a revlog backing storage of an item.
1438 """Obtain a revlog backing storage of an item.
1444
1439
1445 This is similar to ``openstorage()`` except it always returns a revlog.
1440 This is similar to ``openstorage()`` except it always returns a revlog.
1446
1441
1447 In most cases, a caller cares about the main storage object - not the
1442 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
1443 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
1444 that needs to examine low-level revlog implementation details. e.g. debug
1450 commands.
1445 commands.
1451 """
1446 """
1452 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1447 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1453
1448
1454
1449
1455 def copy(ui, repo, pats, opts, rename=False):
1450 def copy(ui, repo, pats, opts, rename=False):
1456 check_incompatible_arguments(opts, b'forget', [b'dry_run'])
1451 check_incompatible_arguments(opts, b'forget', [b'dry_run'])
1457
1452
1458 # called with the repo lock held
1453 # called with the repo lock held
1459 #
1454 #
1460 # hgsep => pathname that uses "/" to separate directories
1455 # hgsep => pathname that uses "/" to separate directories
1461 # ossep => pathname that uses os.sep to separate directories
1456 # ossep => pathname that uses os.sep to separate directories
1462 cwd = repo.getcwd()
1457 cwd = repo.getcwd()
1463 targets = {}
1458 targets = {}
1464 forget = opts.get(b"forget")
1459 forget = opts.get(b"forget")
1465 after = opts.get(b"after")
1460 after = opts.get(b"after")
1466 dryrun = opts.get(b"dry_run")
1461 dryrun = opts.get(b"dry_run")
1467 rev = opts.get(b'at_rev')
1462 rev = opts.get(b'at_rev')
1468 if rev:
1463 if rev:
1469 if not forget and not after:
1464 if not forget and not after:
1470 # TODO: Remove this restriction and make it also create the copy
1465 # TODO: Remove this restriction and make it also create the copy
1471 # targets (and remove the rename source if rename==True).
1466 # targets (and remove the rename source if rename==True).
1472 raise error.InputError(_(b'--at-rev requires --after'))
1467 raise error.InputError(_(b'--at-rev requires --after'))
1473 ctx = scmutil.revsingle(repo, rev)
1468 ctx = scmutil.revsingle(repo, rev)
1474 if len(ctx.parents()) > 1:
1469 if len(ctx.parents()) > 1:
1475 raise error.InputError(
1470 raise error.InputError(
1476 _(b'cannot mark/unmark copy in merge commit')
1471 _(b'cannot mark/unmark copy in merge commit')
1477 )
1472 )
1478 else:
1473 else:
1479 ctx = repo[None]
1474 ctx = repo[None]
1480
1475
1481 pctx = ctx.p1()
1476 pctx = ctx.p1()
1482
1477
1483 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1478 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1484
1479
1485 if forget:
1480 if forget:
1486 if ctx.rev() is None:
1481 if ctx.rev() is None:
1487 new_ctx = ctx
1482 new_ctx = ctx
1488 else:
1483 else:
1489 if len(ctx.parents()) > 1:
1484 if len(ctx.parents()) > 1:
1490 raise error.InputError(_(b'cannot unmark copy in merge commit'))
1485 raise error.InputError(_(b'cannot unmark copy in merge commit'))
1491 # avoid cycle context -> subrepo -> cmdutil
1486 # avoid cycle context -> subrepo -> cmdutil
1492 from . import context
1487 from . import context
1493
1488
1494 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1489 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1495 new_ctx = context.overlayworkingctx(repo)
1490 new_ctx = context.overlayworkingctx(repo)
1496 new_ctx.setbase(ctx.p1())
1491 new_ctx.setbase(ctx.p1())
1497 mergemod.graft(repo, ctx, wctx=new_ctx)
1492 mergemod.graft(repo, ctx, wctx=new_ctx)
1498
1493
1499 match = scmutil.match(ctx, pats, opts)
1494 match = scmutil.match(ctx, pats, opts)
1500
1495
1501 current_copies = ctx.p1copies()
1496 current_copies = ctx.p1copies()
1502 current_copies.update(ctx.p2copies())
1497 current_copies.update(ctx.p2copies())
1503
1498
1504 uipathfn = scmutil.getuipathfn(repo)
1499 uipathfn = scmutil.getuipathfn(repo)
1505 for f in ctx.walk(match):
1500 for f in ctx.walk(match):
1506 if f in current_copies:
1501 if f in current_copies:
1507 new_ctx[f].markcopied(None)
1502 new_ctx[f].markcopied(None)
1508 elif match.exact(f):
1503 elif match.exact(f):
1509 ui.warn(
1504 ui.warn(
1510 _(
1505 _(
1511 b'%s: not unmarking as copy - file is not marked as copied\n'
1506 b'%s: not unmarking as copy - file is not marked as copied\n'
1512 )
1507 )
1513 % uipathfn(f)
1508 % uipathfn(f)
1514 )
1509 )
1515
1510
1516 if ctx.rev() is not None:
1511 if ctx.rev() is not None:
1517 with repo.lock():
1512 with repo.lock():
1518 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1513 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1519 new_node = mem_ctx.commit()
1514 new_node = mem_ctx.commit()
1520
1515
1521 if repo.dirstate.p1() == ctx.node():
1516 if repo.dirstate.p1() == ctx.node():
1522 with repo.dirstate.parentchange():
1517 with repo.dirstate.parentchange():
1523 scmutil.movedirstate(repo, repo[new_node])
1518 scmutil.movedirstate(repo, repo[new_node])
1524 replacements = {ctx.node(): [new_node]}
1519 replacements = {ctx.node(): [new_node]}
1525 scmutil.cleanupnodes(
1520 scmutil.cleanupnodes(
1526 repo, replacements, b'uncopy', fixphase=True
1521 repo, replacements, b'uncopy', fixphase=True
1527 )
1522 )
1528
1523
1529 return
1524 return
1530
1525
1531 pats = scmutil.expandpats(pats)
1526 pats = scmutil.expandpats(pats)
1532 if not pats:
1527 if not pats:
1533 raise error.InputError(_(b'no source or destination specified'))
1528 raise error.InputError(_(b'no source or destination specified'))
1534 if len(pats) == 1:
1529 if len(pats) == 1:
1535 raise error.InputError(_(b'no destination specified'))
1530 raise error.InputError(_(b'no destination specified'))
1536 dest = pats.pop()
1531 dest = pats.pop()
1537
1532
1538 def walkpat(pat):
1533 def walkpat(pat):
1539 srcs = []
1534 srcs = []
1540 # TODO: Inline and simplify the non-working-copy version of this code
1535 # 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.
1536 # since it shares very little with the working-copy version of it.
1542 ctx_to_walk = ctx if ctx.rev() is None else pctx
1537 ctx_to_walk = ctx if ctx.rev() is None else pctx
1543 m = scmutil.match(ctx_to_walk, [pat], opts, globbed=True)
1538 m = scmutil.match(ctx_to_walk, [pat], opts, globbed=True)
1544 for abs in ctx_to_walk.walk(m):
1539 for abs in ctx_to_walk.walk(m):
1545 rel = uipathfn(abs)
1540 rel = uipathfn(abs)
1546 exact = m.exact(abs)
1541 exact = m.exact(abs)
1547 if abs not in ctx:
1542 if abs not in ctx:
1548 if abs in pctx:
1543 if abs in pctx:
1549 if not after:
1544 if not after:
1550 if exact:
1545 if exact:
1551 ui.warn(
1546 ui.warn(
1552 _(
1547 _(
1553 b'%s: not copying - file has been marked '
1548 b'%s: not copying - file has been marked '
1554 b'for remove\n'
1549 b'for remove\n'
1555 )
1550 )
1556 % rel
1551 % rel
1557 )
1552 )
1558 continue
1553 continue
1559 else:
1554 else:
1560 if exact:
1555 if exact:
1561 ui.warn(
1556 ui.warn(
1562 _(b'%s: not copying - file is not managed\n') % rel
1557 _(b'%s: not copying - file is not managed\n') % rel
1563 )
1558 )
1564 continue
1559 continue
1565
1560
1566 # abs: hgsep
1561 # abs: hgsep
1567 # rel: ossep
1562 # rel: ossep
1568 srcs.append((abs, rel, exact))
1563 srcs.append((abs, rel, exact))
1569 return srcs
1564 return srcs
1570
1565
1571 if ctx.rev() is not None:
1566 if ctx.rev() is not None:
1572 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1567 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1573 absdest = pathutil.canonpath(repo.root, cwd, dest)
1568 absdest = pathutil.canonpath(repo.root, cwd, dest)
1574 if ctx.hasdir(absdest):
1569 if ctx.hasdir(absdest):
1575 raise error.InputError(
1570 raise error.InputError(
1576 _(b'%s: --at-rev does not support a directory as destination')
1571 _(b'%s: --at-rev does not support a directory as destination')
1577 % uipathfn(absdest)
1572 % uipathfn(absdest)
1578 )
1573 )
1579 if absdest not in ctx:
1574 if absdest not in ctx:
1580 raise error.InputError(
1575 raise error.InputError(
1581 _(b'%s: copy destination does not exist in %s')
1576 _(b'%s: copy destination does not exist in %s')
1582 % (uipathfn(absdest), ctx)
1577 % (uipathfn(absdest), ctx)
1583 )
1578 )
1584
1579
1585 # avoid cycle context -> subrepo -> cmdutil
1580 # avoid cycle context -> subrepo -> cmdutil
1586 from . import context
1581 from . import context
1587
1582
1588 copylist = []
1583 copylist = []
1589 for pat in pats:
1584 for pat in pats:
1590 srcs = walkpat(pat)
1585 srcs = walkpat(pat)
1591 if not srcs:
1586 if not srcs:
1592 continue
1587 continue
1593 for abs, rel, exact in srcs:
1588 for abs, rel, exact in srcs:
1594 copylist.append(abs)
1589 copylist.append(abs)
1595
1590
1596 if not copylist:
1591 if not copylist:
1597 raise error.InputError(_(b'no files to copy'))
1592 raise error.InputError(_(b'no files to copy'))
1598 # TODO: Add support for `hg cp --at-rev . foo bar dir` and
1593 # 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
1594 # `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the
1600 # existing functions below.
1595 # existing functions below.
1601 if len(copylist) != 1:
1596 if len(copylist) != 1:
1602 raise error.InputError(_(b'--at-rev requires a single source'))
1597 raise error.InputError(_(b'--at-rev requires a single source'))
1603
1598
1604 new_ctx = context.overlayworkingctx(repo)
1599 new_ctx = context.overlayworkingctx(repo)
1605 new_ctx.setbase(ctx.p1())
1600 new_ctx.setbase(ctx.p1())
1606 mergemod.graft(repo, ctx, wctx=new_ctx)
1601 mergemod.graft(repo, ctx, wctx=new_ctx)
1607
1602
1608 new_ctx.markcopied(absdest, copylist[0])
1603 new_ctx.markcopied(absdest, copylist[0])
1609
1604
1610 with repo.lock():
1605 with repo.lock():
1611 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1606 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1612 new_node = mem_ctx.commit()
1607 new_node = mem_ctx.commit()
1613
1608
1614 if repo.dirstate.p1() == ctx.node():
1609 if repo.dirstate.p1() == ctx.node():
1615 with repo.dirstate.parentchange():
1610 with repo.dirstate.parentchange():
1616 scmutil.movedirstate(repo, repo[new_node])
1611 scmutil.movedirstate(repo, repo[new_node])
1617 replacements = {ctx.node(): [new_node]}
1612 replacements = {ctx.node(): [new_node]}
1618 scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True)
1613 scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True)
1619
1614
1620 return
1615 return
1621
1616
1622 # abssrc: hgsep
1617 # abssrc: hgsep
1623 # relsrc: ossep
1618 # relsrc: ossep
1624 # otarget: ossep
1619 # otarget: ossep
1625 def copyfile(abssrc, relsrc, otarget, exact):
1620 def copyfile(abssrc, relsrc, otarget, exact):
1626 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1621 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1627 if b'/' in abstarget:
1622 if b'/' in abstarget:
1628 # We cannot normalize abstarget itself, this would prevent
1623 # We cannot normalize abstarget itself, this would prevent
1629 # case only renames, like a => A.
1624 # case only renames, like a => A.
1630 abspath, absname = abstarget.rsplit(b'/', 1)
1625 abspath, absname = abstarget.rsplit(b'/', 1)
1631 abstarget = repo.dirstate.normalize(abspath) + b'/' + absname
1626 abstarget = repo.dirstate.normalize(abspath) + b'/' + absname
1632 reltarget = repo.pathto(abstarget, cwd)
1627 reltarget = repo.pathto(abstarget, cwd)
1633 target = repo.wjoin(abstarget)
1628 target = repo.wjoin(abstarget)
1634 src = repo.wjoin(abssrc)
1629 src = repo.wjoin(abssrc)
1635 state = repo.dirstate[abstarget]
1630 state = repo.dirstate[abstarget]
1636
1631
1637 scmutil.checkportable(ui, abstarget)
1632 scmutil.checkportable(ui, abstarget)
1638
1633
1639 # check for collisions
1634 # check for collisions
1640 prevsrc = targets.get(abstarget)
1635 prevsrc = targets.get(abstarget)
1641 if prevsrc is not None:
1636 if prevsrc is not None:
1642 ui.warn(
1637 ui.warn(
1643 _(b'%s: not overwriting - %s collides with %s\n')
1638 _(b'%s: not overwriting - %s collides with %s\n')
1644 % (
1639 % (
1645 reltarget,
1640 reltarget,
1646 repo.pathto(abssrc, cwd),
1641 repo.pathto(abssrc, cwd),
1647 repo.pathto(prevsrc, cwd),
1642 repo.pathto(prevsrc, cwd),
1648 )
1643 )
1649 )
1644 )
1650 return True # report a failure
1645 return True # report a failure
1651
1646
1652 # check for overwrites
1647 # check for overwrites
1653 exists = os.path.lexists(target)
1648 exists = os.path.lexists(target)
1654 samefile = False
1649 samefile = False
1655 if exists and abssrc != abstarget:
1650 if exists and abssrc != abstarget:
1656 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1651 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1657 abstarget
1652 abstarget
1658 ):
1653 ):
1659 if not rename:
1654 if not rename:
1660 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1655 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1661 return True # report a failure
1656 return True # report a failure
1662 exists = False
1657 exists = False
1663 samefile = True
1658 samefile = True
1664
1659
1665 if not after and exists or after and state in b'mn':
1660 if not after and exists or after and state in b'mn':
1666 if not opts[b'force']:
1661 if not opts[b'force']:
1667 if state in b'mn':
1662 if state in b'mn':
1668 msg = _(b'%s: not overwriting - file already committed\n')
1663 msg = _(b'%s: not overwriting - file already committed\n')
1669 if after:
1664 if after:
1670 flags = b'--after --force'
1665 flags = b'--after --force'
1671 else:
1666 else:
1672 flags = b'--force'
1667 flags = b'--force'
1673 if rename:
1668 if rename:
1674 hint = (
1669 hint = (
1675 _(
1670 _(
1676 b"('hg rename %s' to replace the file by "
1671 b"('hg rename %s' to replace the file by "
1677 b'recording a rename)\n'
1672 b'recording a rename)\n'
1678 )
1673 )
1679 % flags
1674 % flags
1680 )
1675 )
1681 else:
1676 else:
1682 hint = (
1677 hint = (
1683 _(
1678 _(
1684 b"('hg copy %s' to replace the file by "
1679 b"('hg copy %s' to replace the file by "
1685 b'recording a copy)\n'
1680 b'recording a copy)\n'
1686 )
1681 )
1687 % flags
1682 % flags
1688 )
1683 )
1689 else:
1684 else:
1690 msg = _(b'%s: not overwriting - file exists\n')
1685 msg = _(b'%s: not overwriting - file exists\n')
1691 if rename:
1686 if rename:
1692 hint = _(
1687 hint = _(
1693 b"('hg rename --after' to record the rename)\n"
1688 b"('hg rename --after' to record the rename)\n"
1694 )
1689 )
1695 else:
1690 else:
1696 hint = _(b"('hg copy --after' to record the copy)\n")
1691 hint = _(b"('hg copy --after' to record the copy)\n")
1697 ui.warn(msg % reltarget)
1692 ui.warn(msg % reltarget)
1698 ui.warn(hint)
1693 ui.warn(hint)
1699 return True # report a failure
1694 return True # report a failure
1700
1695
1701 if after:
1696 if after:
1702 if not exists:
1697 if not exists:
1703 if rename:
1698 if rename:
1704 ui.warn(
1699 ui.warn(
1705 _(b'%s: not recording move - %s does not exist\n')
1700 _(b'%s: not recording move - %s does not exist\n')
1706 % (relsrc, reltarget)
1701 % (relsrc, reltarget)
1707 )
1702 )
1708 else:
1703 else:
1709 ui.warn(
1704 ui.warn(
1710 _(b'%s: not recording copy - %s does not exist\n')
1705 _(b'%s: not recording copy - %s does not exist\n')
1711 % (relsrc, reltarget)
1706 % (relsrc, reltarget)
1712 )
1707 )
1713 return True # report a failure
1708 return True # report a failure
1714 elif not dryrun:
1709 elif not dryrun:
1715 try:
1710 try:
1716 if exists:
1711 if exists:
1717 os.unlink(target)
1712 os.unlink(target)
1718 targetdir = os.path.dirname(target) or b'.'
1713 targetdir = os.path.dirname(target) or b'.'
1719 if not os.path.isdir(targetdir):
1714 if not os.path.isdir(targetdir):
1720 os.makedirs(targetdir)
1715 os.makedirs(targetdir)
1721 if samefile:
1716 if samefile:
1722 tmp = target + b"~hgrename"
1717 tmp = target + b"~hgrename"
1723 os.rename(src, tmp)
1718 os.rename(src, tmp)
1724 os.rename(tmp, target)
1719 os.rename(tmp, target)
1725 else:
1720 else:
1726 # Preserve stat info on renames, not on copies; this matches
1721 # Preserve stat info on renames, not on copies; this matches
1727 # Linux CLI behavior.
1722 # Linux CLI behavior.
1728 util.copyfile(src, target, copystat=rename)
1723 util.copyfile(src, target, copystat=rename)
1729 srcexists = True
1724 srcexists = True
1730 except IOError as inst:
1725 except IOError as inst:
1731 if inst.errno == errno.ENOENT:
1726 if inst.errno == errno.ENOENT:
1732 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1727 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1733 srcexists = False
1728 srcexists = False
1734 else:
1729 else:
1735 ui.warn(
1730 ui.warn(
1736 _(b'%s: cannot copy - %s\n')
1731 _(b'%s: cannot copy - %s\n')
1737 % (relsrc, encoding.strtolocal(inst.strerror))
1732 % (relsrc, encoding.strtolocal(inst.strerror))
1738 )
1733 )
1739 return True # report a failure
1734 return True # report a failure
1740
1735
1741 if ui.verbose or not exact:
1736 if ui.verbose or not exact:
1742 if rename:
1737 if rename:
1743 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1738 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1744 else:
1739 else:
1745 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1740 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1746
1741
1747 targets[abstarget] = abssrc
1742 targets[abstarget] = abssrc
1748
1743
1749 # fix up dirstate
1744 # fix up dirstate
1750 scmutil.dirstatecopy(
1745 scmutil.dirstatecopy(
1751 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1746 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1752 )
1747 )
1753 if rename and not dryrun:
1748 if rename and not dryrun:
1754 if not after and srcexists and not samefile:
1749 if not after and srcexists and not samefile:
1755 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1750 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1756 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1751 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1757 ctx.forget([abssrc])
1752 ctx.forget([abssrc])
1758
1753
1759 # pat: ossep
1754 # pat: ossep
1760 # dest ossep
1755 # dest ossep
1761 # srcs: list of (hgsep, hgsep, ossep, bool)
1756 # srcs: list of (hgsep, hgsep, ossep, bool)
1762 # return: function that takes hgsep and returns ossep
1757 # return: function that takes hgsep and returns ossep
1763 def targetpathfn(pat, dest, srcs):
1758 def targetpathfn(pat, dest, srcs):
1764 if os.path.isdir(pat):
1759 if os.path.isdir(pat):
1765 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1760 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1766 abspfx = util.localpath(abspfx)
1761 abspfx = util.localpath(abspfx)
1767 if destdirexists:
1762 if destdirexists:
1768 striplen = len(os.path.split(abspfx)[0])
1763 striplen = len(os.path.split(abspfx)[0])
1769 else:
1764 else:
1770 striplen = len(abspfx)
1765 striplen = len(abspfx)
1771 if striplen:
1766 if striplen:
1772 striplen += len(pycompat.ossep)
1767 striplen += len(pycompat.ossep)
1773 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1768 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1774 elif destdirexists:
1769 elif destdirexists:
1775 res = lambda p: os.path.join(
1770 res = lambda p: os.path.join(
1776 dest, os.path.basename(util.localpath(p))
1771 dest, os.path.basename(util.localpath(p))
1777 )
1772 )
1778 else:
1773 else:
1779 res = lambda p: dest
1774 res = lambda p: dest
1780 return res
1775 return res
1781
1776
1782 # pat: ossep
1777 # pat: ossep
1783 # dest ossep
1778 # dest ossep
1784 # srcs: list of (hgsep, hgsep, ossep, bool)
1779 # srcs: list of (hgsep, hgsep, ossep, bool)
1785 # return: function that takes hgsep and returns ossep
1780 # return: function that takes hgsep and returns ossep
1786 def targetpathafterfn(pat, dest, srcs):
1781 def targetpathafterfn(pat, dest, srcs):
1787 if matchmod.patkind(pat):
1782 if matchmod.patkind(pat):
1788 # a mercurial pattern
1783 # a mercurial pattern
1789 res = lambda p: os.path.join(
1784 res = lambda p: os.path.join(
1790 dest, os.path.basename(util.localpath(p))
1785 dest, os.path.basename(util.localpath(p))
1791 )
1786 )
1792 else:
1787 else:
1793 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1788 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1794 if len(abspfx) < len(srcs[0][0]):
1789 if len(abspfx) < len(srcs[0][0]):
1795 # A directory. Either the target path contains the last
1790 # A directory. Either the target path contains the last
1796 # component of the source path or it does not.
1791 # component of the source path or it does not.
1797 def evalpath(striplen):
1792 def evalpath(striplen):
1798 score = 0
1793 score = 0
1799 for s in srcs:
1794 for s in srcs:
1800 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1795 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1801 if os.path.lexists(t):
1796 if os.path.lexists(t):
1802 score += 1
1797 score += 1
1803 return score
1798 return score
1804
1799
1805 abspfx = util.localpath(abspfx)
1800 abspfx = util.localpath(abspfx)
1806 striplen = len(abspfx)
1801 striplen = len(abspfx)
1807 if striplen:
1802 if striplen:
1808 striplen += len(pycompat.ossep)
1803 striplen += len(pycompat.ossep)
1809 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1804 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1810 score = evalpath(striplen)
1805 score = evalpath(striplen)
1811 striplen1 = len(os.path.split(abspfx)[0])
1806 striplen1 = len(os.path.split(abspfx)[0])
1812 if striplen1:
1807 if striplen1:
1813 striplen1 += len(pycompat.ossep)
1808 striplen1 += len(pycompat.ossep)
1814 if evalpath(striplen1) > score:
1809 if evalpath(striplen1) > score:
1815 striplen = striplen1
1810 striplen = striplen1
1816 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1811 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1817 else:
1812 else:
1818 # a file
1813 # a file
1819 if destdirexists:
1814 if destdirexists:
1820 res = lambda p: os.path.join(
1815 res = lambda p: os.path.join(
1821 dest, os.path.basename(util.localpath(p))
1816 dest, os.path.basename(util.localpath(p))
1822 )
1817 )
1823 else:
1818 else:
1824 res = lambda p: dest
1819 res = lambda p: dest
1825 return res
1820 return res
1826
1821
1827 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1822 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1828 if not destdirexists:
1823 if not destdirexists:
1829 if len(pats) > 1 or matchmod.patkind(pats[0]):
1824 if len(pats) > 1 or matchmod.patkind(pats[0]):
1830 raise error.InputError(
1825 raise error.InputError(
1831 _(
1826 _(
1832 b'with multiple sources, destination must be an '
1827 b'with multiple sources, destination must be an '
1833 b'existing directory'
1828 b'existing directory'
1834 )
1829 )
1835 )
1830 )
1836 if util.endswithsep(dest):
1831 if util.endswithsep(dest):
1837 raise error.InputError(
1832 raise error.InputError(
1838 _(b'destination %s is not a directory') % dest
1833 _(b'destination %s is not a directory') % dest
1839 )
1834 )
1840
1835
1841 tfn = targetpathfn
1836 tfn = targetpathfn
1842 if after:
1837 if after:
1843 tfn = targetpathafterfn
1838 tfn = targetpathafterfn
1844 copylist = []
1839 copylist = []
1845 for pat in pats:
1840 for pat in pats:
1846 srcs = walkpat(pat)
1841 srcs = walkpat(pat)
1847 if not srcs:
1842 if not srcs:
1848 continue
1843 continue
1849 copylist.append((tfn(pat, dest, srcs), srcs))
1844 copylist.append((tfn(pat, dest, srcs), srcs))
1850 if not copylist:
1845 if not copylist:
1851 raise error.InputError(_(b'no files to copy'))
1846 raise error.InputError(_(b'no files to copy'))
1852
1847
1853 errors = 0
1848 errors = 0
1854 for targetpath, srcs in copylist:
1849 for targetpath, srcs in copylist:
1855 for abssrc, relsrc, exact in srcs:
1850 for abssrc, relsrc, exact in srcs:
1856 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1851 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1857 errors += 1
1852 errors += 1
1858
1853
1859 return errors != 0
1854 return errors != 0
1860
1855
1861
1856
1862 ## facility to let extension process additional data into an import patch
1857 ## facility to let extension process additional data into an import patch
1863 # list of identifier to be executed in order
1858 # list of identifier to be executed in order
1864 extrapreimport = [] # run before commit
1859 extrapreimport = [] # run before commit
1865 extrapostimport = [] # run after commit
1860 extrapostimport = [] # run after commit
1866 # mapping from identifier to actual import function
1861 # mapping from identifier to actual import function
1867 #
1862 #
1868 # 'preimport' are run before the commit is made and are provided the following
1863 # 'preimport' are run before the commit is made and are provided the following
1869 # arguments:
1864 # arguments:
1870 # - repo: the localrepository instance,
1865 # - repo: the localrepository instance,
1871 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1866 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1872 # - extra: the future extra dictionary of the changeset, please mutate it,
1867 # - extra: the future extra dictionary of the changeset, please mutate it,
1873 # - opts: the import options.
1868 # - opts: the import options.
1874 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1869 # 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
1870 # mutation of in memory commit and more. Feel free to rework the code to get
1876 # there.
1871 # there.
1877 extrapreimportmap = {}
1872 extrapreimportmap = {}
1878 # 'postimport' are run after the commit is made and are provided the following
1873 # 'postimport' are run after the commit is made and are provided the following
1879 # argument:
1874 # argument:
1880 # - ctx: the changectx created by import.
1875 # - ctx: the changectx created by import.
1881 extrapostimportmap = {}
1876 extrapostimportmap = {}
1882
1877
1883
1878
1884 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1879 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1885 """Utility function used by commands.import to import a single patch
1880 """Utility function used by commands.import to import a single patch
1886
1881
1887 This function is explicitly defined here to help the evolve extension to
1882 This function is explicitly defined here to help the evolve extension to
1888 wrap this part of the import logic.
1883 wrap this part of the import logic.
1889
1884
1890 The API is currently a bit ugly because it a simple code translation from
1885 The API is currently a bit ugly because it a simple code translation from
1891 the import command. Feel free to make it better.
1886 the import command. Feel free to make it better.
1892
1887
1893 :patchdata: a dictionary containing parsed patch data (such as from
1888 :patchdata: a dictionary containing parsed patch data (such as from
1894 ``patch.extract()``)
1889 ``patch.extract()``)
1895 :parents: nodes that will be parent of the created commit
1890 :parents: nodes that will be parent of the created commit
1896 :opts: the full dict of option passed to the import command
1891 :opts: the full dict of option passed to the import command
1897 :msgs: list to save commit message to.
1892 :msgs: list to save commit message to.
1898 (used in case we need to save it when failing)
1893 (used in case we need to save it when failing)
1899 :updatefunc: a function that update a repo to a given node
1894 :updatefunc: a function that update a repo to a given node
1900 updatefunc(<repo>, <node>)
1895 updatefunc(<repo>, <node>)
1901 """
1896 """
1902 # avoid cycle context -> subrepo -> cmdutil
1897 # avoid cycle context -> subrepo -> cmdutil
1903 from . import context
1898 from . import context
1904
1899
1905 tmpname = patchdata.get(b'filename')
1900 tmpname = patchdata.get(b'filename')
1906 message = patchdata.get(b'message')
1901 message = patchdata.get(b'message')
1907 user = opts.get(b'user') or patchdata.get(b'user')
1902 user = opts.get(b'user') or patchdata.get(b'user')
1908 date = opts.get(b'date') or patchdata.get(b'date')
1903 date = opts.get(b'date') or patchdata.get(b'date')
1909 branch = patchdata.get(b'branch')
1904 branch = patchdata.get(b'branch')
1910 nodeid = patchdata.get(b'nodeid')
1905 nodeid = patchdata.get(b'nodeid')
1911 p1 = patchdata.get(b'p1')
1906 p1 = patchdata.get(b'p1')
1912 p2 = patchdata.get(b'p2')
1907 p2 = patchdata.get(b'p2')
1913
1908
1914 nocommit = opts.get(b'no_commit')
1909 nocommit = opts.get(b'no_commit')
1915 importbranch = opts.get(b'import_branch')
1910 importbranch = opts.get(b'import_branch')
1916 update = not opts.get(b'bypass')
1911 update = not opts.get(b'bypass')
1917 strip = opts[b"strip"]
1912 strip = opts[b"strip"]
1918 prefix = opts[b"prefix"]
1913 prefix = opts[b"prefix"]
1919 sim = float(opts.get(b'similarity') or 0)
1914 sim = float(opts.get(b'similarity') or 0)
1920
1915
1921 if not tmpname:
1916 if not tmpname:
1922 return None, None, False
1917 return None, None, False
1923
1918
1924 rejects = False
1919 rejects = False
1925
1920
1926 cmdline_message = logmessage(ui, opts)
1921 cmdline_message = logmessage(ui, opts)
1927 if cmdline_message:
1922 if cmdline_message:
1928 # pickup the cmdline msg
1923 # pickup the cmdline msg
1929 message = cmdline_message
1924 message = cmdline_message
1930 elif message:
1925 elif message:
1931 # pickup the patch msg
1926 # pickup the patch msg
1932 message = message.strip()
1927 message = message.strip()
1933 else:
1928 else:
1934 # launch the editor
1929 # launch the editor
1935 message = None
1930 message = None
1936 ui.debug(b'message:\n%s\n' % (message or b''))
1931 ui.debug(b'message:\n%s\n' % (message or b''))
1937
1932
1938 if len(parents) == 1:
1933 if len(parents) == 1:
1939 parents.append(repo[nullrev])
1934 parents.append(repo[nullrev])
1940 if opts.get(b'exact'):
1935 if opts.get(b'exact'):
1941 if not nodeid or not p1:
1936 if not nodeid or not p1:
1942 raise error.InputError(_(b'not a Mercurial patch'))
1937 raise error.InputError(_(b'not a Mercurial patch'))
1943 p1 = repo[p1]
1938 p1 = repo[p1]
1944 p2 = repo[p2 or nullrev]
1939 p2 = repo[p2 or nullrev]
1945 elif p2:
1940 elif p2:
1946 try:
1941 try:
1947 p1 = repo[p1]
1942 p1 = repo[p1]
1948 p2 = repo[p2]
1943 p2 = repo[p2]
1949 # Without any options, consider p2 only if the
1944 # Without any options, consider p2 only if the
1950 # patch is being applied on top of the recorded
1945 # patch is being applied on top of the recorded
1951 # first parent.
1946 # first parent.
1952 if p1 != parents[0]:
1947 if p1 != parents[0]:
1953 p1 = parents[0]
1948 p1 = parents[0]
1954 p2 = repo[nullrev]
1949 p2 = repo[nullrev]
1955 except error.RepoError:
1950 except error.RepoError:
1956 p1, p2 = parents
1951 p1, p2 = parents
1957 if p2.rev() == nullrev:
1952 if p2.rev() == nullrev:
1958 ui.warn(
1953 ui.warn(
1959 _(
1954 _(
1960 b"warning: import the patch as a normal revision\n"
1955 b"warning: import the patch as a normal revision\n"
1961 b"(use --exact to import the patch as a merge)\n"
1956 b"(use --exact to import the patch as a merge)\n"
1962 )
1957 )
1963 )
1958 )
1964 else:
1959 else:
1965 p1, p2 = parents
1960 p1, p2 = parents
1966
1961
1967 n = None
1962 n = None
1968 if update:
1963 if update:
1969 if p1 != parents[0]:
1964 if p1 != parents[0]:
1970 updatefunc(repo, p1.node())
1965 updatefunc(repo, p1.node())
1971 if p2 != parents[1]:
1966 if p2 != parents[1]:
1972 repo.setparents(p1.node(), p2.node())
1967 repo.setparents(p1.node(), p2.node())
1973
1968
1974 if opts.get(b'exact') or importbranch:
1969 if opts.get(b'exact') or importbranch:
1975 repo.dirstate.setbranch(branch or b'default')
1970 repo.dirstate.setbranch(branch or b'default')
1976
1971
1977 partial = opts.get(b'partial', False)
1972 partial = opts.get(b'partial', False)
1978 files = set()
1973 files = set()
1979 try:
1974 try:
1980 patch.patch(
1975 patch.patch(
1981 ui,
1976 ui,
1982 repo,
1977 repo,
1983 tmpname,
1978 tmpname,
1984 strip=strip,
1979 strip=strip,
1985 prefix=prefix,
1980 prefix=prefix,
1986 files=files,
1981 files=files,
1987 eolmode=None,
1982 eolmode=None,
1988 similarity=sim / 100.0,
1983 similarity=sim / 100.0,
1989 )
1984 )
1990 except error.PatchError as e:
1985 except error.PatchError as e:
1991 if not partial:
1986 if not partial:
1992 raise error.Abort(pycompat.bytestr(e))
1987 raise error.Abort(pycompat.bytestr(e))
1993 if partial:
1988 if partial:
1994 rejects = True
1989 rejects = True
1995
1990
1996 files = list(files)
1991 files = list(files)
1997 if nocommit:
1992 if nocommit:
1998 if message:
1993 if message:
1999 msgs.append(message)
1994 msgs.append(message)
2000 else:
1995 else:
2001 if opts.get(b'exact') or p2:
1996 if opts.get(b'exact') or p2:
2002 # If you got here, you either use --force and know what
1997 # If you got here, you either use --force and know what
2003 # you are doing or used --exact or a merge patch while
1998 # you are doing or used --exact or a merge patch while
2004 # being updated to its first parent.
1999 # being updated to its first parent.
2005 m = None
2000 m = None
2006 else:
2001 else:
2007 m = scmutil.matchfiles(repo, files or [])
2002 m = scmutil.matchfiles(repo, files or [])
2008 editform = mergeeditform(repo[None], b'import.normal')
2003 editform = mergeeditform(repo[None], b'import.normal')
2009 if opts.get(b'exact'):
2004 if opts.get(b'exact'):
2010 editor = None
2005 editor = None
2011 else:
2006 else:
2012 editor = getcommiteditor(
2007 editor = getcommiteditor(
2013 editform=editform, **pycompat.strkwargs(opts)
2008 editform=editform, **pycompat.strkwargs(opts)
2014 )
2009 )
2015 extra = {}
2010 extra = {}
2016 for idfunc in extrapreimport:
2011 for idfunc in extrapreimport:
2017 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2012 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2018 overrides = {}
2013 overrides = {}
2019 if partial:
2014 if partial:
2020 overrides[(b'ui', b'allowemptycommit')] = True
2015 overrides[(b'ui', b'allowemptycommit')] = True
2021 if opts.get(b'secret'):
2016 if opts.get(b'secret'):
2022 overrides[(b'phases', b'new-commit')] = b'secret'
2017 overrides[(b'phases', b'new-commit')] = b'secret'
2023 with repo.ui.configoverride(overrides, b'import'):
2018 with repo.ui.configoverride(overrides, b'import'):
2024 n = repo.commit(
2019 n = repo.commit(
2025 message, user, date, match=m, editor=editor, extra=extra
2020 message, user, date, match=m, editor=editor, extra=extra
2026 )
2021 )
2027 for idfunc in extrapostimport:
2022 for idfunc in extrapostimport:
2028 extrapostimportmap[idfunc](repo[n])
2023 extrapostimportmap[idfunc](repo[n])
2029 else:
2024 else:
2030 if opts.get(b'exact') or importbranch:
2025 if opts.get(b'exact') or importbranch:
2031 branch = branch or b'default'
2026 branch = branch or b'default'
2032 else:
2027 else:
2033 branch = p1.branch()
2028 branch = p1.branch()
2034 store = patch.filestore()
2029 store = patch.filestore()
2035 try:
2030 try:
2036 files = set()
2031 files = set()
2037 try:
2032 try:
2038 patch.patchrepo(
2033 patch.patchrepo(
2039 ui,
2034 ui,
2040 repo,
2035 repo,
2041 p1,
2036 p1,
2042 store,
2037 store,
2043 tmpname,
2038 tmpname,
2044 strip,
2039 strip,
2045 prefix,
2040 prefix,
2046 files,
2041 files,
2047 eolmode=None,
2042 eolmode=None,
2048 )
2043 )
2049 except error.PatchError as e:
2044 except error.PatchError as e:
2050 raise error.Abort(stringutil.forcebytestr(e))
2045 raise error.Abort(stringutil.forcebytestr(e))
2051 if opts.get(b'exact'):
2046 if opts.get(b'exact'):
2052 editor = None
2047 editor = None
2053 else:
2048 else:
2054 editor = getcommiteditor(editform=b'import.bypass')
2049 editor = getcommiteditor(editform=b'import.bypass')
2055 memctx = context.memctx(
2050 memctx = context.memctx(
2056 repo,
2051 repo,
2057 (p1.node(), p2.node()),
2052 (p1.node(), p2.node()),
2058 message,
2053 message,
2059 files=files,
2054 files=files,
2060 filectxfn=store,
2055 filectxfn=store,
2061 user=user,
2056 user=user,
2062 date=date,
2057 date=date,
2063 branch=branch,
2058 branch=branch,
2064 editor=editor,
2059 editor=editor,
2065 )
2060 )
2066
2061
2067 overrides = {}
2062 overrides = {}
2068 if opts.get(b'secret'):
2063 if opts.get(b'secret'):
2069 overrides[(b'phases', b'new-commit')] = b'secret'
2064 overrides[(b'phases', b'new-commit')] = b'secret'
2070 with repo.ui.configoverride(overrides, b'import'):
2065 with repo.ui.configoverride(overrides, b'import'):
2071 n = memctx.commit()
2066 n = memctx.commit()
2072 finally:
2067 finally:
2073 store.close()
2068 store.close()
2074 if opts.get(b'exact') and nocommit:
2069 if opts.get(b'exact') and nocommit:
2075 # --exact with --no-commit is still useful in that it does merge
2070 # --exact with --no-commit is still useful in that it does merge
2076 # and branch bits
2071 # and branch bits
2077 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
2072 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):
2073 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
2079 raise error.Abort(_(b'patch is damaged or loses information'))
2074 raise error.Abort(_(b'patch is damaged or loses information'))
2080 msg = _(b'applied to working directory')
2075 msg = _(b'applied to working directory')
2081 if n:
2076 if n:
2082 # i18n: refers to a short changeset id
2077 # i18n: refers to a short changeset id
2083 msg = _(b'created %s') % short(n)
2078 msg = _(b'created %s') % short(n)
2084 return msg, n, rejects
2079 return msg, n, rejects
2085
2080
2086
2081
2087 # facility to let extensions include additional data in an exported patch
2082 # facility to let extensions include additional data in an exported patch
2088 # list of identifiers to be executed in order
2083 # list of identifiers to be executed in order
2089 extraexport = []
2084 extraexport = []
2090 # mapping from identifier to actual export function
2085 # mapping from identifier to actual export function
2091 # function as to return a string to be added to the header or None
2086 # function as to return a string to be added to the header or None
2092 # it is given two arguments (sequencenumber, changectx)
2087 # it is given two arguments (sequencenumber, changectx)
2093 extraexportmap = {}
2088 extraexportmap = {}
2094
2089
2095
2090
2096 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2091 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2097 node = scmutil.binnode(ctx)
2092 node = scmutil.binnode(ctx)
2098 parents = [p.node() for p in ctx.parents() if p]
2093 parents = [p.node() for p in ctx.parents() if p]
2099 branch = ctx.branch()
2094 branch = ctx.branch()
2100 if switch_parent:
2095 if switch_parent:
2101 parents.reverse()
2096 parents.reverse()
2102
2097
2103 if parents:
2098 if parents:
2104 prev = parents[0]
2099 prev = parents[0]
2105 else:
2100 else:
2106 prev = repo.nullid
2101 prev = repo.nullid
2107
2102
2108 fm.context(ctx=ctx)
2103 fm.context(ctx=ctx)
2109 fm.plain(b'# HG changeset patch\n')
2104 fm.plain(b'# HG changeset patch\n')
2110 fm.write(b'user', b'# User %s\n', ctx.user())
2105 fm.write(b'user', b'# User %s\n', ctx.user())
2111 fm.plain(b'# Date %d %d\n' % ctx.date())
2106 fm.plain(b'# Date %d %d\n' % ctx.date())
2112 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2107 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2113 fm.condwrite(
2108 fm.condwrite(
2114 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2109 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2115 )
2110 )
2116 fm.write(b'node', b'# Node ID %s\n', hex(node))
2111 fm.write(b'node', b'# Node ID %s\n', hex(node))
2117 fm.plain(b'# Parent %s\n' % hex(prev))
2112 fm.plain(b'# Parent %s\n' % hex(prev))
2118 if len(parents) > 1:
2113 if len(parents) > 1:
2119 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2114 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2120 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2115 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2121
2116
2122 # TODO: redesign extraexportmap function to support formatter
2117 # TODO: redesign extraexportmap function to support formatter
2123 for headerid in extraexport:
2118 for headerid in extraexport:
2124 header = extraexportmap[headerid](seqno, ctx)
2119 header = extraexportmap[headerid](seqno, ctx)
2125 if header is not None:
2120 if header is not None:
2126 fm.plain(b'# %s\n' % header)
2121 fm.plain(b'# %s\n' % header)
2127
2122
2128 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2123 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2129 fm.plain(b'\n')
2124 fm.plain(b'\n')
2130
2125
2131 if fm.isplain():
2126 if fm.isplain():
2132 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2127 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2133 for chunk, label in chunkiter:
2128 for chunk, label in chunkiter:
2134 fm.plain(chunk, label=label)
2129 fm.plain(chunk, label=label)
2135 else:
2130 else:
2136 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2131 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2137 # TODO: make it structured?
2132 # TODO: make it structured?
2138 fm.data(diff=b''.join(chunkiter))
2133 fm.data(diff=b''.join(chunkiter))
2139
2134
2140
2135
2141 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2136 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2142 """Export changesets to stdout or a single file"""
2137 """Export changesets to stdout or a single file"""
2143 for seqno, rev in enumerate(revs, 1):
2138 for seqno, rev in enumerate(revs, 1):
2144 ctx = repo[rev]
2139 ctx = repo[rev]
2145 if not dest.startswith(b'<'):
2140 if not dest.startswith(b'<'):
2146 repo.ui.note(b"%s\n" % dest)
2141 repo.ui.note(b"%s\n" % dest)
2147 fm.startitem()
2142 fm.startitem()
2148 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2143 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2149
2144
2150
2145
2151 def _exportfntemplate(
2146 def _exportfntemplate(
2152 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2147 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2153 ):
2148 ):
2154 """Export changesets to possibly multiple files"""
2149 """Export changesets to possibly multiple files"""
2155 total = len(revs)
2150 total = len(revs)
2156 revwidth = max(len(str(rev)) for rev in revs)
2151 revwidth = max(len(str(rev)) for rev in revs)
2157 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2152 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2158
2153
2159 for seqno, rev in enumerate(revs, 1):
2154 for seqno, rev in enumerate(revs, 1):
2160 ctx = repo[rev]
2155 ctx = repo[rev]
2161 dest = makefilename(
2156 dest = makefilename(
2162 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2157 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2163 )
2158 )
2164 filemap.setdefault(dest, []).append((seqno, rev))
2159 filemap.setdefault(dest, []).append((seqno, rev))
2165
2160
2166 for dest in filemap:
2161 for dest in filemap:
2167 with formatter.maybereopen(basefm, dest) as fm:
2162 with formatter.maybereopen(basefm, dest) as fm:
2168 repo.ui.note(b"%s\n" % dest)
2163 repo.ui.note(b"%s\n" % dest)
2169 for seqno, rev in filemap[dest]:
2164 for seqno, rev in filemap[dest]:
2170 fm.startitem()
2165 fm.startitem()
2171 ctx = repo[rev]
2166 ctx = repo[rev]
2172 _exportsingle(
2167 _exportsingle(
2173 repo, ctx, fm, match, switch_parent, seqno, diffopts
2168 repo, ctx, fm, match, switch_parent, seqno, diffopts
2174 )
2169 )
2175
2170
2176
2171
2177 def _prefetchchangedfiles(repo, revs, match):
2172 def _prefetchchangedfiles(repo, revs, match):
2178 allfiles = set()
2173 allfiles = set()
2179 for rev in revs:
2174 for rev in revs:
2180 for file in repo[rev].files():
2175 for file in repo[rev].files():
2181 if not match or match(file):
2176 if not match or match(file):
2182 allfiles.add(file)
2177 allfiles.add(file)
2183 match = scmutil.matchfiles(repo, allfiles)
2178 match = scmutil.matchfiles(repo, allfiles)
2184 revmatches = [(rev, match) for rev in revs]
2179 revmatches = [(rev, match) for rev in revs]
2185 scmutil.prefetchfiles(repo, revmatches)
2180 scmutil.prefetchfiles(repo, revmatches)
2186
2181
2187
2182
2188 def export(
2183 def export(
2189 repo,
2184 repo,
2190 revs,
2185 revs,
2191 basefm,
2186 basefm,
2192 fntemplate=b'hg-%h.patch',
2187 fntemplate=b'hg-%h.patch',
2193 switch_parent=False,
2188 switch_parent=False,
2194 opts=None,
2189 opts=None,
2195 match=None,
2190 match=None,
2196 ):
2191 ):
2197 """export changesets as hg patches
2192 """export changesets as hg patches
2198
2193
2199 Args:
2194 Args:
2200 repo: The repository from which we're exporting revisions.
2195 repo: The repository from which we're exporting revisions.
2201 revs: A list of revisions to export as revision numbers.
2196 revs: A list of revisions to export as revision numbers.
2202 basefm: A formatter to which patches should be written.
2197 basefm: A formatter to which patches should be written.
2203 fntemplate: An optional string to use for generating patch file names.
2198 fntemplate: An optional string to use for generating patch file names.
2204 switch_parent: If True, show diffs against second parent when not nullid.
2199 switch_parent: If True, show diffs against second parent when not nullid.
2205 Default is false, which always shows diff against p1.
2200 Default is false, which always shows diff against p1.
2206 opts: diff options to use for generating the patch.
2201 opts: diff options to use for generating the patch.
2207 match: If specified, only export changes to files matching this matcher.
2202 match: If specified, only export changes to files matching this matcher.
2208
2203
2209 Returns:
2204 Returns:
2210 Nothing.
2205 Nothing.
2211
2206
2212 Side Effect:
2207 Side Effect:
2213 "HG Changeset Patch" data is emitted to one of the following
2208 "HG Changeset Patch" data is emitted to one of the following
2214 destinations:
2209 destinations:
2215 fntemplate specified: Each rev is written to a unique file named using
2210 fntemplate specified: Each rev is written to a unique file named using
2216 the given template.
2211 the given template.
2217 Otherwise: All revs will be written to basefm.
2212 Otherwise: All revs will be written to basefm.
2218 """
2213 """
2219 _prefetchchangedfiles(repo, revs, match)
2214 _prefetchchangedfiles(repo, revs, match)
2220
2215
2221 if not fntemplate:
2216 if not fntemplate:
2222 _exportfile(
2217 _exportfile(
2223 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2218 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2224 )
2219 )
2225 else:
2220 else:
2226 _exportfntemplate(
2221 _exportfntemplate(
2227 repo, revs, basefm, fntemplate, switch_parent, opts, match
2222 repo, revs, basefm, fntemplate, switch_parent, opts, match
2228 )
2223 )
2229
2224
2230
2225
2231 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2226 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2232 """Export changesets to the given file stream"""
2227 """Export changesets to the given file stream"""
2233 _prefetchchangedfiles(repo, revs, match)
2228 _prefetchchangedfiles(repo, revs, match)
2234
2229
2235 dest = getattr(fp, 'name', b'<unnamed>')
2230 dest = getattr(fp, 'name', b'<unnamed>')
2236 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2231 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2237 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2232 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2238
2233
2239
2234
2240 def showmarker(fm, marker, index=None):
2235 def showmarker(fm, marker, index=None):
2241 """utility function to display obsolescence marker in a readable way
2236 """utility function to display obsolescence marker in a readable way
2242
2237
2243 To be used by debug function."""
2238 To be used by debug function."""
2244 if index is not None:
2239 if index is not None:
2245 fm.write(b'index', b'%i ', index)
2240 fm.write(b'index', b'%i ', index)
2246 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2241 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2247 succs = marker.succnodes()
2242 succs = marker.succnodes()
2248 fm.condwrite(
2243 fm.condwrite(
2249 succs,
2244 succs,
2250 b'succnodes',
2245 b'succnodes',
2251 b'%s ',
2246 b'%s ',
2252 fm.formatlist(map(hex, succs), name=b'node'),
2247 fm.formatlist(map(hex, succs), name=b'node'),
2253 )
2248 )
2254 fm.write(b'flag', b'%X ', marker.flags())
2249 fm.write(b'flag', b'%X ', marker.flags())
2255 parents = marker.parentnodes()
2250 parents = marker.parentnodes()
2256 if parents is not None:
2251 if parents is not None:
2257 fm.write(
2252 fm.write(
2258 b'parentnodes',
2253 b'parentnodes',
2259 b'{%s} ',
2254 b'{%s} ',
2260 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2255 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2261 )
2256 )
2262 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2257 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2263 meta = marker.metadata().copy()
2258 meta = marker.metadata().copy()
2264 meta.pop(b'date', None)
2259 meta.pop(b'date', None)
2265 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2260 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2266 fm.write(
2261 fm.write(
2267 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2262 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2268 )
2263 )
2269 fm.plain(b'\n')
2264 fm.plain(b'\n')
2270
2265
2271
2266
2272 def finddate(ui, repo, date):
2267 def finddate(ui, repo, date):
2273 """Find the tipmost changeset that matches the given date spec"""
2268 """Find the tipmost changeset that matches the given date spec"""
2274 mrevs = repo.revs(b'date(%s)', date)
2269 mrevs = repo.revs(b'date(%s)', date)
2275 try:
2270 try:
2276 rev = mrevs.max()
2271 rev = mrevs.max()
2277 except ValueError:
2272 except ValueError:
2278 raise error.InputError(_(b"revision matching date not found"))
2273 raise error.InputError(_(b"revision matching date not found"))
2279
2274
2280 ui.status(
2275 ui.status(
2281 _(b"found revision %d from %s\n")
2276 _(b"found revision %d from %s\n")
2282 % (rev, dateutil.datestr(repo[rev].date()))
2277 % (rev, dateutil.datestr(repo[rev].date()))
2283 )
2278 )
2284 return b'%d' % rev
2279 return b'%d' % rev
2285
2280
2286
2281
2287 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2282 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2288 bad = []
2283 bad = []
2289
2284
2290 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2285 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2291 names = []
2286 names = []
2292 wctx = repo[None]
2287 wctx = repo[None]
2293 cca = None
2288 cca = None
2294 abort, warn = scmutil.checkportabilityalert(ui)
2289 abort, warn = scmutil.checkportabilityalert(ui)
2295 if abort or warn:
2290 if abort or warn:
2296 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2291 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2297
2292
2298 match = repo.narrowmatch(match, includeexact=True)
2293 match = repo.narrowmatch(match, includeexact=True)
2299 badmatch = matchmod.badmatch(match, badfn)
2294 badmatch = matchmod.badmatch(match, badfn)
2300 dirstate = repo.dirstate
2295 dirstate = repo.dirstate
2301 # We don't want to just call wctx.walk here, since it would return a lot of
2296 # 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.
2297 # clean files, which we aren't interested in and takes time.
2303 for f in sorted(
2298 for f in sorted(
2304 dirstate.walk(
2299 dirstate.walk(
2305 badmatch,
2300 badmatch,
2306 subrepos=sorted(wctx.substate),
2301 subrepos=sorted(wctx.substate),
2307 unknown=True,
2302 unknown=True,
2308 ignored=False,
2303 ignored=False,
2309 full=False,
2304 full=False,
2310 )
2305 )
2311 ):
2306 ):
2312 exact = match.exact(f)
2307 exact = match.exact(f)
2313 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2308 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2314 if cca:
2309 if cca:
2315 cca(f)
2310 cca(f)
2316 names.append(f)
2311 names.append(f)
2317 if ui.verbose or not exact:
2312 if ui.verbose or not exact:
2318 ui.status(
2313 ui.status(
2319 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2314 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2320 )
2315 )
2321
2316
2322 for subpath in sorted(wctx.substate):
2317 for subpath in sorted(wctx.substate):
2323 sub = wctx.sub(subpath)
2318 sub = wctx.sub(subpath)
2324 try:
2319 try:
2325 submatch = matchmod.subdirmatcher(subpath, match)
2320 submatch = matchmod.subdirmatcher(subpath, match)
2326 subprefix = repo.wvfs.reljoin(prefix, subpath)
2321 subprefix = repo.wvfs.reljoin(prefix, subpath)
2327 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2322 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2328 if opts.get('subrepos'):
2323 if opts.get('subrepos'):
2329 bad.extend(
2324 bad.extend(
2330 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2325 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2331 )
2326 )
2332 else:
2327 else:
2333 bad.extend(
2328 bad.extend(
2334 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2329 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2335 )
2330 )
2336 except error.LookupError:
2331 except error.LookupError:
2337 ui.status(
2332 ui.status(
2338 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2333 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2339 )
2334 )
2340
2335
2341 if not opts.get('dry_run'):
2336 if not opts.get('dry_run'):
2342 rejected = wctx.add(names, prefix)
2337 rejected = wctx.add(names, prefix)
2343 bad.extend(f for f in rejected if f in match.files())
2338 bad.extend(f for f in rejected if f in match.files())
2344 return bad
2339 return bad
2345
2340
2346
2341
2347 def addwebdirpath(repo, serverpath, webconf):
2342 def addwebdirpath(repo, serverpath, webconf):
2348 webconf[serverpath] = repo.root
2343 webconf[serverpath] = repo.root
2349 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2344 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2350
2345
2351 for r in repo.revs(b'filelog("path:.hgsub")'):
2346 for r in repo.revs(b'filelog("path:.hgsub")'):
2352 ctx = repo[r]
2347 ctx = repo[r]
2353 for subpath in ctx.substate:
2348 for subpath in ctx.substate:
2354 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2349 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2355
2350
2356
2351
2357 def forget(
2352 def forget(
2358 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2353 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2359 ):
2354 ):
2360 if dryrun and interactive:
2355 if dryrun and interactive:
2361 raise error.InputError(
2356 raise error.InputError(
2362 _(b"cannot specify both --dry-run and --interactive")
2357 _(b"cannot specify both --dry-run and --interactive")
2363 )
2358 )
2364 bad = []
2359 bad = []
2365 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2360 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2366 wctx = repo[None]
2361 wctx = repo[None]
2367 forgot = []
2362 forgot = []
2368
2363
2369 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2364 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2370 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2365 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2371 if explicitonly:
2366 if explicitonly:
2372 forget = [f for f in forget if match.exact(f)]
2367 forget = [f for f in forget if match.exact(f)]
2373
2368
2374 for subpath in sorted(wctx.substate):
2369 for subpath in sorted(wctx.substate):
2375 sub = wctx.sub(subpath)
2370 sub = wctx.sub(subpath)
2376 submatch = matchmod.subdirmatcher(subpath, match)
2371 submatch = matchmod.subdirmatcher(subpath, match)
2377 subprefix = repo.wvfs.reljoin(prefix, subpath)
2372 subprefix = repo.wvfs.reljoin(prefix, subpath)
2378 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2373 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2379 try:
2374 try:
2380 subbad, subforgot = sub.forget(
2375 subbad, subforgot = sub.forget(
2381 submatch,
2376 submatch,
2382 subprefix,
2377 subprefix,
2383 subuipathfn,
2378 subuipathfn,
2384 dryrun=dryrun,
2379 dryrun=dryrun,
2385 interactive=interactive,
2380 interactive=interactive,
2386 )
2381 )
2387 bad.extend([subpath + b'/' + f for f in subbad])
2382 bad.extend([subpath + b'/' + f for f in subbad])
2388 forgot.extend([subpath + b'/' + f for f in subforgot])
2383 forgot.extend([subpath + b'/' + f for f in subforgot])
2389 except error.LookupError:
2384 except error.LookupError:
2390 ui.status(
2385 ui.status(
2391 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2386 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2392 )
2387 )
2393
2388
2394 if not explicitonly:
2389 if not explicitonly:
2395 for f in match.files():
2390 for f in match.files():
2396 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2391 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2397 if f not in forgot:
2392 if f not in forgot:
2398 if repo.wvfs.exists(f):
2393 if repo.wvfs.exists(f):
2399 # Don't complain if the exact case match wasn't given.
2394 # Don't complain if the exact case match wasn't given.
2400 # But don't do this until after checking 'forgot', so
2395 # But don't do this until after checking 'forgot', so
2401 # that subrepo files aren't normalized, and this op is
2396 # that subrepo files aren't normalized, and this op is
2402 # purely from data cached by the status walk above.
2397 # purely from data cached by the status walk above.
2403 if repo.dirstate.normalize(f) in repo.dirstate:
2398 if repo.dirstate.normalize(f) in repo.dirstate:
2404 continue
2399 continue
2405 ui.warn(
2400 ui.warn(
2406 _(
2401 _(
2407 b'not removing %s: '
2402 b'not removing %s: '
2408 b'file is already untracked\n'
2403 b'file is already untracked\n'
2409 )
2404 )
2410 % uipathfn(f)
2405 % uipathfn(f)
2411 )
2406 )
2412 bad.append(f)
2407 bad.append(f)
2413
2408
2414 if interactive:
2409 if interactive:
2415 responses = _(
2410 responses = _(
2416 b'[Ynsa?]'
2411 b'[Ynsa?]'
2417 b'$$ &Yes, forget this file'
2412 b'$$ &Yes, forget this file'
2418 b'$$ &No, skip this file'
2413 b'$$ &No, skip this file'
2419 b'$$ &Skip remaining files'
2414 b'$$ &Skip remaining files'
2420 b'$$ Include &all remaining files'
2415 b'$$ Include &all remaining files'
2421 b'$$ &? (display help)'
2416 b'$$ &? (display help)'
2422 )
2417 )
2423 for filename in forget[:]:
2418 for filename in forget[:]:
2424 r = ui.promptchoice(
2419 r = ui.promptchoice(
2425 _(b'forget %s %s') % (uipathfn(filename), responses)
2420 _(b'forget %s %s') % (uipathfn(filename), responses)
2426 )
2421 )
2427 if r == 4: # ?
2422 if r == 4: # ?
2428 while r == 4:
2423 while r == 4:
2429 for c, t in ui.extractchoices(responses)[1]:
2424 for c, t in ui.extractchoices(responses)[1]:
2430 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2425 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2431 r = ui.promptchoice(
2426 r = ui.promptchoice(
2432 _(b'forget %s %s') % (uipathfn(filename), responses)
2427 _(b'forget %s %s') % (uipathfn(filename), responses)
2433 )
2428 )
2434 if r == 0: # yes
2429 if r == 0: # yes
2435 continue
2430 continue
2436 elif r == 1: # no
2431 elif r == 1: # no
2437 forget.remove(filename)
2432 forget.remove(filename)
2438 elif r == 2: # Skip
2433 elif r == 2: # Skip
2439 fnindex = forget.index(filename)
2434 fnindex = forget.index(filename)
2440 del forget[fnindex:]
2435 del forget[fnindex:]
2441 break
2436 break
2442 elif r == 3: # All
2437 elif r == 3: # All
2443 break
2438 break
2444
2439
2445 for f in forget:
2440 for f in forget:
2446 if ui.verbose or not match.exact(f) or interactive:
2441 if ui.verbose or not match.exact(f) or interactive:
2447 ui.status(
2442 ui.status(
2448 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2443 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2449 )
2444 )
2450
2445
2451 if not dryrun:
2446 if not dryrun:
2452 rejected = wctx.forget(forget, prefix)
2447 rejected = wctx.forget(forget, prefix)
2453 bad.extend(f for f in rejected if f in match.files())
2448 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)
2449 forgot.extend(f for f in forget if f not in rejected)
2455 return bad, forgot
2450 return bad, forgot
2456
2451
2457
2452
2458 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2453 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2459 ret = 1
2454 ret = 1
2460
2455
2461 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2456 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2462 if fm.isplain() and not needsfctx:
2457 if fm.isplain() and not needsfctx:
2463 # Fast path. The speed-up comes from skipping the formatter, and batching
2458 # Fast path. The speed-up comes from skipping the formatter, and batching
2464 # calls to ui.write.
2459 # calls to ui.write.
2465 buf = []
2460 buf = []
2466 for f in ctx.matches(m):
2461 for f in ctx.matches(m):
2467 buf.append(fmt % uipathfn(f))
2462 buf.append(fmt % uipathfn(f))
2468 if len(buf) > 100:
2463 if len(buf) > 100:
2469 ui.write(b''.join(buf))
2464 ui.write(b''.join(buf))
2470 del buf[:]
2465 del buf[:]
2471 ret = 0
2466 ret = 0
2472 if buf:
2467 if buf:
2473 ui.write(b''.join(buf))
2468 ui.write(b''.join(buf))
2474 else:
2469 else:
2475 for f in ctx.matches(m):
2470 for f in ctx.matches(m):
2476 fm.startitem()
2471 fm.startitem()
2477 fm.context(ctx=ctx)
2472 fm.context(ctx=ctx)
2478 if needsfctx:
2473 if needsfctx:
2479 fc = ctx[f]
2474 fc = ctx[f]
2480 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2475 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2481 fm.data(path=f)
2476 fm.data(path=f)
2482 fm.plain(fmt % uipathfn(f))
2477 fm.plain(fmt % uipathfn(f))
2483 ret = 0
2478 ret = 0
2484
2479
2485 for subpath in sorted(ctx.substate):
2480 for subpath in sorted(ctx.substate):
2486 submatch = matchmod.subdirmatcher(subpath, m)
2481 submatch = matchmod.subdirmatcher(subpath, m)
2487 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2482 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2488 if subrepos or m.exact(subpath) or any(submatch.files()):
2483 if subrepos or m.exact(subpath) or any(submatch.files()):
2489 sub = ctx.sub(subpath)
2484 sub = ctx.sub(subpath)
2490 try:
2485 try:
2491 recurse = m.exact(subpath) or subrepos
2486 recurse = m.exact(subpath) or subrepos
2492 if (
2487 if (
2493 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2488 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2494 == 0
2489 == 0
2495 ):
2490 ):
2496 ret = 0
2491 ret = 0
2497 except error.LookupError:
2492 except error.LookupError:
2498 ui.status(
2493 ui.status(
2499 _(b"skipping missing subrepository: %s\n")
2494 _(b"skipping missing subrepository: %s\n")
2500 % uipathfn(subpath)
2495 % uipathfn(subpath)
2501 )
2496 )
2502
2497
2503 return ret
2498 return ret
2504
2499
2505
2500
2506 def remove(
2501 def remove(
2507 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2502 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2508 ):
2503 ):
2509 ret = 0
2504 ret = 0
2510 s = repo.status(match=m, clean=True)
2505 s = repo.status(match=m, clean=True)
2511 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2506 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2512
2507
2513 wctx = repo[None]
2508 wctx = repo[None]
2514
2509
2515 if warnings is None:
2510 if warnings is None:
2516 warnings = []
2511 warnings = []
2517 warn = True
2512 warn = True
2518 else:
2513 else:
2519 warn = False
2514 warn = False
2520
2515
2521 subs = sorted(wctx.substate)
2516 subs = sorted(wctx.substate)
2522 progress = ui.makeprogress(
2517 progress = ui.makeprogress(
2523 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2518 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2524 )
2519 )
2525 for subpath in subs:
2520 for subpath in subs:
2526 submatch = matchmod.subdirmatcher(subpath, m)
2521 submatch = matchmod.subdirmatcher(subpath, m)
2527 subprefix = repo.wvfs.reljoin(prefix, subpath)
2522 subprefix = repo.wvfs.reljoin(prefix, subpath)
2528 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2523 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2529 if subrepos or m.exact(subpath) or any(submatch.files()):
2524 if subrepos or m.exact(subpath) or any(submatch.files()):
2530 progress.increment()
2525 progress.increment()
2531 sub = wctx.sub(subpath)
2526 sub = wctx.sub(subpath)
2532 try:
2527 try:
2533 if sub.removefiles(
2528 if sub.removefiles(
2534 submatch,
2529 submatch,
2535 subprefix,
2530 subprefix,
2536 subuipathfn,
2531 subuipathfn,
2537 after,
2532 after,
2538 force,
2533 force,
2539 subrepos,
2534 subrepos,
2540 dryrun,
2535 dryrun,
2541 warnings,
2536 warnings,
2542 ):
2537 ):
2543 ret = 1
2538 ret = 1
2544 except error.LookupError:
2539 except error.LookupError:
2545 warnings.append(
2540 warnings.append(
2546 _(b"skipping missing subrepository: %s\n")
2541 _(b"skipping missing subrepository: %s\n")
2547 % uipathfn(subpath)
2542 % uipathfn(subpath)
2548 )
2543 )
2549 progress.complete()
2544 progress.complete()
2550
2545
2551 # warn about failure to delete explicit files/dirs
2546 # warn about failure to delete explicit files/dirs
2552 deleteddirs = pathutil.dirs(deleted)
2547 deleteddirs = pathutil.dirs(deleted)
2553 files = m.files()
2548 files = m.files()
2554 progress = ui.makeprogress(
2549 progress = ui.makeprogress(
2555 _(b'deleting'), total=len(files), unit=_(b'files')
2550 _(b'deleting'), total=len(files), unit=_(b'files')
2556 )
2551 )
2557 for f in files:
2552 for f in files:
2558
2553
2559 def insubrepo():
2554 def insubrepo():
2560 for subpath in wctx.substate:
2555 for subpath in wctx.substate:
2561 if f.startswith(subpath + b'/'):
2556 if f.startswith(subpath + b'/'):
2562 return True
2557 return True
2563 return False
2558 return False
2564
2559
2565 progress.increment()
2560 progress.increment()
2566 isdir = f in deleteddirs or wctx.hasdir(f)
2561 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:
2562 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2568 continue
2563 continue
2569
2564
2570 if repo.wvfs.exists(f):
2565 if repo.wvfs.exists(f):
2571 if repo.wvfs.isdir(f):
2566 if repo.wvfs.isdir(f):
2572 warnings.append(
2567 warnings.append(
2573 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2568 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2574 )
2569 )
2575 else:
2570 else:
2576 warnings.append(
2571 warnings.append(
2577 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2572 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2578 )
2573 )
2579 # missing files will generate a warning elsewhere
2574 # missing files will generate a warning elsewhere
2580 ret = 1
2575 ret = 1
2581 progress.complete()
2576 progress.complete()
2582
2577
2583 if force:
2578 if force:
2584 list = modified + deleted + clean + added
2579 list = modified + deleted + clean + added
2585 elif after:
2580 elif after:
2586 list = deleted
2581 list = deleted
2587 remaining = modified + added + clean
2582 remaining = modified + added + clean
2588 progress = ui.makeprogress(
2583 progress = ui.makeprogress(
2589 _(b'skipping'), total=len(remaining), unit=_(b'files')
2584 _(b'skipping'), total=len(remaining), unit=_(b'files')
2590 )
2585 )
2591 for f in remaining:
2586 for f in remaining:
2592 progress.increment()
2587 progress.increment()
2593 if ui.verbose or (f in files):
2588 if ui.verbose or (f in files):
2594 warnings.append(
2589 warnings.append(
2595 _(b'not removing %s: file still exists\n') % uipathfn(f)
2590 _(b'not removing %s: file still exists\n') % uipathfn(f)
2596 )
2591 )
2597 ret = 1
2592 ret = 1
2598 progress.complete()
2593 progress.complete()
2599 else:
2594 else:
2600 list = deleted + clean
2595 list = deleted + clean
2601 progress = ui.makeprogress(
2596 progress = ui.makeprogress(
2602 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2597 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2603 )
2598 )
2604 for f in modified:
2599 for f in modified:
2605 progress.increment()
2600 progress.increment()
2606 warnings.append(
2601 warnings.append(
2607 _(
2602 _(
2608 b'not removing %s: file is modified (use -f'
2603 b'not removing %s: file is modified (use -f'
2609 b' to force removal)\n'
2604 b' to force removal)\n'
2610 )
2605 )
2611 % uipathfn(f)
2606 % uipathfn(f)
2612 )
2607 )
2613 ret = 1
2608 ret = 1
2614 for f in added:
2609 for f in added:
2615 progress.increment()
2610 progress.increment()
2616 warnings.append(
2611 warnings.append(
2617 _(
2612 _(
2618 b"not removing %s: file has been marked for add"
2613 b"not removing %s: file has been marked for add"
2619 b" (use 'hg forget' to undo add)\n"
2614 b" (use 'hg forget' to undo add)\n"
2620 )
2615 )
2621 % uipathfn(f)
2616 % uipathfn(f)
2622 )
2617 )
2623 ret = 1
2618 ret = 1
2624 progress.complete()
2619 progress.complete()
2625
2620
2626 list = sorted(list)
2621 list = sorted(list)
2627 progress = ui.makeprogress(
2622 progress = ui.makeprogress(
2628 _(b'deleting'), total=len(list), unit=_(b'files')
2623 _(b'deleting'), total=len(list), unit=_(b'files')
2629 )
2624 )
2630 for f in list:
2625 for f in list:
2631 if ui.verbose or not m.exact(f):
2626 if ui.verbose or not m.exact(f):
2632 progress.increment()
2627 progress.increment()
2633 ui.status(
2628 ui.status(
2634 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2629 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2635 )
2630 )
2636 progress.complete()
2631 progress.complete()
2637
2632
2638 if not dryrun:
2633 if not dryrun:
2639 with repo.wlock():
2634 with repo.wlock():
2640 if not after:
2635 if not after:
2641 for f in list:
2636 for f in list:
2642 if f in added:
2637 if f in added:
2643 continue # we never unlink added files on remove
2638 continue # we never unlink added files on remove
2644 rmdir = repo.ui.configbool(
2639 rmdir = repo.ui.configbool(
2645 b'experimental', b'removeemptydirs'
2640 b'experimental', b'removeemptydirs'
2646 )
2641 )
2647 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2642 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2648 repo[None].forget(list)
2643 repo[None].forget(list)
2649
2644
2650 if warn:
2645 if warn:
2651 for warning in warnings:
2646 for warning in warnings:
2652 ui.warn(warning)
2647 ui.warn(warning)
2653
2648
2654 return ret
2649 return ret
2655
2650
2656
2651
2657 def _catfmtneedsdata(fm):
2652 def _catfmtneedsdata(fm):
2658 return not fm.datahint() or b'data' in fm.datahint()
2653 return not fm.datahint() or b'data' in fm.datahint()
2659
2654
2660
2655
2661 def _updatecatformatter(fm, ctx, matcher, path, decode):
2656 def _updatecatformatter(fm, ctx, matcher, path, decode):
2662 """Hook for adding data to the formatter used by ``hg cat``.
2657 """Hook for adding data to the formatter used by ``hg cat``.
2663
2658
2664 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2659 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2665 this method first."""
2660 this method first."""
2666
2661
2667 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2662 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2668 # wasn't requested.
2663 # wasn't requested.
2669 data = b''
2664 data = b''
2670 if _catfmtneedsdata(fm):
2665 if _catfmtneedsdata(fm):
2671 data = ctx[path].data()
2666 data = ctx[path].data()
2672 if decode:
2667 if decode:
2673 data = ctx.repo().wwritedata(path, data)
2668 data = ctx.repo().wwritedata(path, data)
2674 fm.startitem()
2669 fm.startitem()
2675 fm.context(ctx=ctx)
2670 fm.context(ctx=ctx)
2676 fm.write(b'data', b'%s', data)
2671 fm.write(b'data', b'%s', data)
2677 fm.data(path=path)
2672 fm.data(path=path)
2678
2673
2679
2674
2680 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2675 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2681 err = 1
2676 err = 1
2682 opts = pycompat.byteskwargs(opts)
2677 opts = pycompat.byteskwargs(opts)
2683
2678
2684 def write(path):
2679 def write(path):
2685 filename = None
2680 filename = None
2686 if fntemplate:
2681 if fntemplate:
2687 filename = makefilename(
2682 filename = makefilename(
2688 ctx, fntemplate, pathname=os.path.join(prefix, path)
2683 ctx, fntemplate, pathname=os.path.join(prefix, path)
2689 )
2684 )
2690 # attempt to create the directory if it does not already exist
2685 # attempt to create the directory if it does not already exist
2691 try:
2686 try:
2692 os.makedirs(os.path.dirname(filename))
2687 os.makedirs(os.path.dirname(filename))
2693 except OSError:
2688 except OSError:
2694 pass
2689 pass
2695 with formatter.maybereopen(basefm, filename) as fm:
2690 with formatter.maybereopen(basefm, filename) as fm:
2696 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2691 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2697
2692
2698 # Automation often uses hg cat on single files, so special case it
2693 # Automation often uses hg cat on single files, so special case it
2699 # for performance to avoid the cost of parsing the manifest.
2694 # for performance to avoid the cost of parsing the manifest.
2700 if len(matcher.files()) == 1 and not matcher.anypats():
2695 if len(matcher.files()) == 1 and not matcher.anypats():
2701 file = matcher.files()[0]
2696 file = matcher.files()[0]
2702 mfl = repo.manifestlog
2697 mfl = repo.manifestlog
2703 mfnode = ctx.manifestnode()
2698 mfnode = ctx.manifestnode()
2704 try:
2699 try:
2705 if mfnode and mfl[mfnode].find(file)[0]:
2700 if mfnode and mfl[mfnode].find(file)[0]:
2706 if _catfmtneedsdata(basefm):
2701 if _catfmtneedsdata(basefm):
2707 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2702 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2708 write(file)
2703 write(file)
2709 return 0
2704 return 0
2710 except KeyError:
2705 except KeyError:
2711 pass
2706 pass
2712
2707
2713 if _catfmtneedsdata(basefm):
2708 if _catfmtneedsdata(basefm):
2714 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2709 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2715
2710
2716 for abs in ctx.walk(matcher):
2711 for abs in ctx.walk(matcher):
2717 write(abs)
2712 write(abs)
2718 err = 0
2713 err = 0
2719
2714
2720 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2715 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2721 for subpath in sorted(ctx.substate):
2716 for subpath in sorted(ctx.substate):
2722 sub = ctx.sub(subpath)
2717 sub = ctx.sub(subpath)
2723 try:
2718 try:
2724 submatch = matchmod.subdirmatcher(subpath, matcher)
2719 submatch = matchmod.subdirmatcher(subpath, matcher)
2725 subprefix = os.path.join(prefix, subpath)
2720 subprefix = os.path.join(prefix, subpath)
2726 if not sub.cat(
2721 if not sub.cat(
2727 submatch,
2722 submatch,
2728 basefm,
2723 basefm,
2729 fntemplate,
2724 fntemplate,
2730 subprefix,
2725 subprefix,
2731 **pycompat.strkwargs(opts)
2726 **pycompat.strkwargs(opts)
2732 ):
2727 ):
2733 err = 0
2728 err = 0
2734 except error.RepoLookupError:
2729 except error.RepoLookupError:
2735 ui.status(
2730 ui.status(
2736 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2731 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2737 )
2732 )
2738
2733
2739 return err
2734 return err
2740
2735
2741
2736
2742 def commit(ui, repo, commitfunc, pats, opts):
2737 def commit(ui, repo, commitfunc, pats, opts):
2743 '''commit the specified files or all outstanding changes'''
2738 '''commit the specified files or all outstanding changes'''
2744 date = opts.get(b'date')
2739 date = opts.get(b'date')
2745 if date:
2740 if date:
2746 opts[b'date'] = dateutil.parsedate(date)
2741 opts[b'date'] = dateutil.parsedate(date)
2747 message = logmessage(ui, opts)
2742 message = logmessage(ui, opts)
2748 matcher = scmutil.match(repo[None], pats, opts)
2743 matcher = scmutil.match(repo[None], pats, opts)
2749
2744
2750 dsguard = None
2745 dsguard = None
2751 # extract addremove carefully -- this function can be called from a command
2746 # extract addremove carefully -- this function can be called from a command
2752 # that doesn't support addremove
2747 # that doesn't support addremove
2753 if opts.get(b'addremove'):
2748 if opts.get(b'addremove'):
2754 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2749 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2755 with dsguard or util.nullcontextmanager():
2750 with dsguard or util.nullcontextmanager():
2756 if dsguard:
2751 if dsguard:
2757 relative = scmutil.anypats(pats, opts)
2752 relative = scmutil.anypats(pats, opts)
2758 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2753 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2759 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2754 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2760 raise error.Abort(
2755 raise error.Abort(
2761 _(b"failed to mark all new/missing files as added/removed")
2756 _(b"failed to mark all new/missing files as added/removed")
2762 )
2757 )
2763
2758
2764 return commitfunc(ui, repo, message, matcher, opts)
2759 return commitfunc(ui, repo, message, matcher, opts)
2765
2760
2766
2761
2767 def samefile(f, ctx1, ctx2):
2762 def samefile(f, ctx1, ctx2):
2768 if f in ctx1.manifest():
2763 if f in ctx1.manifest():
2769 a = ctx1.filectx(f)
2764 a = ctx1.filectx(f)
2770 if f in ctx2.manifest():
2765 if f in ctx2.manifest():
2771 b = ctx2.filectx(f)
2766 b = ctx2.filectx(f)
2772 return not a.cmp(b) and a.flags() == b.flags()
2767 return not a.cmp(b) and a.flags() == b.flags()
2773 else:
2768 else:
2774 return False
2769 return False
2775 else:
2770 else:
2776 return f not in ctx2.manifest()
2771 return f not in ctx2.manifest()
2777
2772
2778
2773
2779 def amend(ui, repo, old, extra, pats, opts):
2774 def amend(ui, repo, old, extra, pats, opts):
2780 # avoid cycle context -> subrepo -> cmdutil
2775 # avoid cycle context -> subrepo -> cmdutil
2781 from . import context
2776 from . import context
2782
2777
2783 # amend will reuse the existing user if not specified, but the obsolete
2778 # amend will reuse the existing user if not specified, but the obsolete
2784 # marker creation requires that the current user's name is specified.
2779 # marker creation requires that the current user's name is specified.
2785 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2780 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2786 ui.username() # raise exception if username not set
2781 ui.username() # raise exception if username not set
2787
2782
2788 ui.note(_(b'amending changeset %s\n') % old)
2783 ui.note(_(b'amending changeset %s\n') % old)
2789 base = old.p1()
2784 base = old.p1()
2790
2785
2791 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2786 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2792 # Participating changesets:
2787 # Participating changesets:
2793 #
2788 #
2794 # wctx o - workingctx that contains changes from working copy
2789 # wctx o - workingctx that contains changes from working copy
2795 # | to go into amending commit
2790 # | to go into amending commit
2796 # |
2791 # |
2797 # old o - changeset to amend
2792 # old o - changeset to amend
2798 # |
2793 # |
2799 # base o - first parent of the changeset to amend
2794 # base o - first parent of the changeset to amend
2800 wctx = repo[None]
2795 wctx = repo[None]
2801
2796
2802 # Copy to avoid mutating input
2797 # Copy to avoid mutating input
2803 extra = extra.copy()
2798 extra = extra.copy()
2804 # Update extra dict from amended commit (e.g. to preserve graft
2799 # Update extra dict from amended commit (e.g. to preserve graft
2805 # source)
2800 # source)
2806 extra.update(old.extra())
2801 extra.update(old.extra())
2807
2802
2808 # Also update it from the from the wctx
2803 # Also update it from the from the wctx
2809 extra.update(wctx.extra())
2804 extra.update(wctx.extra())
2810
2805
2811 # date-only change should be ignored?
2806 # date-only change should be ignored?
2812 datemaydiffer = resolvecommitoptions(ui, opts)
2807 datemaydiffer = resolvecommitoptions(ui, opts)
2813
2808
2814 date = old.date()
2809 date = old.date()
2815 if opts.get(b'date'):
2810 if opts.get(b'date'):
2816 date = dateutil.parsedate(opts.get(b'date'))
2811 date = dateutil.parsedate(opts.get(b'date'))
2817 user = opts.get(b'user') or old.user()
2812 user = opts.get(b'user') or old.user()
2818
2813
2819 if len(old.parents()) > 1:
2814 if len(old.parents()) > 1:
2820 # ctx.files() isn't reliable for merges, so fall back to the
2815 # ctx.files() isn't reliable for merges, so fall back to the
2821 # slower repo.status() method
2816 # slower repo.status() method
2822 st = base.status(old)
2817 st = base.status(old)
2823 files = set(st.modified) | set(st.added) | set(st.removed)
2818 files = set(st.modified) | set(st.added) | set(st.removed)
2824 else:
2819 else:
2825 files = set(old.files())
2820 files = set(old.files())
2826
2821
2827 # add/remove the files to the working copy if the "addremove" option
2822 # add/remove the files to the working copy if the "addremove" option
2828 # was specified.
2823 # was specified.
2829 matcher = scmutil.match(wctx, pats, opts)
2824 matcher = scmutil.match(wctx, pats, opts)
2830 relative = scmutil.anypats(pats, opts)
2825 relative = scmutil.anypats(pats, opts)
2831 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2826 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2832 if opts.get(b'addremove') and scmutil.addremove(
2827 if opts.get(b'addremove') and scmutil.addremove(
2833 repo, matcher, b"", uipathfn, opts
2828 repo, matcher, b"", uipathfn, opts
2834 ):
2829 ):
2835 raise error.Abort(
2830 raise error.Abort(
2836 _(b"failed to mark all new/missing files as added/removed")
2831 _(b"failed to mark all new/missing files as added/removed")
2837 )
2832 )
2838
2833
2839 # Check subrepos. This depends on in-place wctx._status update in
2834 # Check subrepos. This depends on in-place wctx._status update in
2840 # subrepo.precommit(). To minimize the risk of this hack, we do
2835 # subrepo.precommit(). To minimize the risk of this hack, we do
2841 # nothing if .hgsub does not exist.
2836 # nothing if .hgsub does not exist.
2842 if b'.hgsub' in wctx or b'.hgsub' in old:
2837 if b'.hgsub' in wctx or b'.hgsub' in old:
2843 subs, commitsubs, newsubstate = subrepoutil.precommit(
2838 subs, commitsubs, newsubstate = subrepoutil.precommit(
2844 ui, wctx, wctx._status, matcher
2839 ui, wctx, wctx._status, matcher
2845 )
2840 )
2846 # amend should abort if commitsubrepos is enabled
2841 # amend should abort if commitsubrepos is enabled
2847 assert not commitsubs
2842 assert not commitsubs
2848 if subs:
2843 if subs:
2849 subrepoutil.writestate(repo, newsubstate)
2844 subrepoutil.writestate(repo, newsubstate)
2850
2845
2851 ms = mergestatemod.mergestate.read(repo)
2846 ms = mergestatemod.mergestate.read(repo)
2852 mergeutil.checkunresolved(ms)
2847 mergeutil.checkunresolved(ms)
2853
2848
2854 filestoamend = {f for f in wctx.files() if matcher(f)}
2849 filestoamend = {f for f in wctx.files() if matcher(f)}
2855
2850
2856 changes = len(filestoamend) > 0
2851 changes = len(filestoamend) > 0
2857 if changes:
2852 if changes:
2858 # Recompute copies (avoid recording a -> b -> a)
2853 # Recompute copies (avoid recording a -> b -> a)
2859 copied = copies.pathcopies(base, wctx, matcher)
2854 copied = copies.pathcopies(base, wctx, matcher)
2860 if old.p2:
2855 if old.p2:
2861 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2856 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2862
2857
2863 # Prune files which were reverted by the updates: if old
2858 # Prune files which were reverted by the updates: if old
2864 # introduced file X and the file was renamed in the working
2859 # introduced file X and the file was renamed in the working
2865 # copy, then those two files are the same and
2860 # copy, then those two files are the same and
2866 # we can discard X from our list of files. Likewise if X
2861 # 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
2862 # was removed, it's no longer relevant. If X is missing (aka
2868 # deleted), old X must be preserved.
2863 # deleted), old X must be preserved.
2869 files.update(filestoamend)
2864 files.update(filestoamend)
2870 files = [
2865 files = [
2871 f
2866 f
2872 for f in files
2867 for f in files
2873 if (f not in filestoamend or not samefile(f, wctx, base))
2868 if (f not in filestoamend or not samefile(f, wctx, base))
2874 ]
2869 ]
2875
2870
2876 def filectxfn(repo, ctx_, path):
2871 def filectxfn(repo, ctx_, path):
2877 try:
2872 try:
2878 # If the file being considered is not amongst the files
2873 # If the file being considered is not amongst the files
2879 # to be amended, we should return the file context from the
2874 # to be amended, we should return the file context from the
2880 # old changeset. This avoids issues when only some files in
2875 # old changeset. This avoids issues when only some files in
2881 # the working copy are being amended but there are also
2876 # the working copy are being amended but there are also
2882 # changes to other files from the old changeset.
2877 # changes to other files from the old changeset.
2883 if path not in filestoamend:
2878 if path not in filestoamend:
2884 return old.filectx(path)
2879 return old.filectx(path)
2885
2880
2886 # Return None for removed files.
2881 # Return None for removed files.
2887 if path in wctx.removed():
2882 if path in wctx.removed():
2888 return None
2883 return None
2889
2884
2890 fctx = wctx[path]
2885 fctx = wctx[path]
2891 flags = fctx.flags()
2886 flags = fctx.flags()
2892 mctx = context.memfilectx(
2887 mctx = context.memfilectx(
2893 repo,
2888 repo,
2894 ctx_,
2889 ctx_,
2895 fctx.path(),
2890 fctx.path(),
2896 fctx.data(),
2891 fctx.data(),
2897 islink=b'l' in flags,
2892 islink=b'l' in flags,
2898 isexec=b'x' in flags,
2893 isexec=b'x' in flags,
2899 copysource=copied.get(path),
2894 copysource=copied.get(path),
2900 )
2895 )
2901 return mctx
2896 return mctx
2902 except KeyError:
2897 except KeyError:
2903 return None
2898 return None
2904
2899
2905 else:
2900 else:
2906 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2901 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2907
2902
2908 # Use version of files as in the old cset
2903 # Use version of files as in the old cset
2909 def filectxfn(repo, ctx_, path):
2904 def filectxfn(repo, ctx_, path):
2910 try:
2905 try:
2911 return old.filectx(path)
2906 return old.filectx(path)
2912 except KeyError:
2907 except KeyError:
2913 return None
2908 return None
2914
2909
2915 # See if we got a message from -m or -l, if not, open the editor with
2910 # See if we got a message from -m or -l, if not, open the editor with
2916 # the message of the changeset to amend.
2911 # the message of the changeset to amend.
2917 message = logmessage(ui, opts)
2912 message = logmessage(ui, opts)
2918
2913
2919 editform = mergeeditform(old, b'commit.amend')
2914 editform = mergeeditform(old, b'commit.amend')
2920
2915
2921 if not message:
2916 if not message:
2922 message = old.description()
2917 message = old.description()
2923 # Default if message isn't provided and --edit is not passed is to
2918 # 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
2919 # invoke editor, but allow --no-edit. If somehow we don't have any
2925 # description, let's always start the editor.
2920 # description, let's always start the editor.
2926 doedit = not message or opts.get(b'edit') in [True, None]
2921 doedit = not message or opts.get(b'edit') in [True, None]
2927 else:
2922 else:
2928 # Default if message is provided is to not invoke editor, but allow
2923 # Default if message is provided is to not invoke editor, but allow
2929 # --edit.
2924 # --edit.
2930 doedit = opts.get(b'edit') is True
2925 doedit = opts.get(b'edit') is True
2931 editor = getcommiteditor(edit=doedit, editform=editform)
2926 editor = getcommiteditor(edit=doedit, editform=editform)
2932
2927
2933 pureextra = extra.copy()
2928 pureextra = extra.copy()
2934 extra[b'amend_source'] = old.hex()
2929 extra[b'amend_source'] = old.hex()
2935
2930
2936 new = context.memctx(
2931 new = context.memctx(
2937 repo,
2932 repo,
2938 parents=[base.node(), old.p2().node()],
2933 parents=[base.node(), old.p2().node()],
2939 text=message,
2934 text=message,
2940 files=files,
2935 files=files,
2941 filectxfn=filectxfn,
2936 filectxfn=filectxfn,
2942 user=user,
2937 user=user,
2943 date=date,
2938 date=date,
2944 extra=extra,
2939 extra=extra,
2945 editor=editor,
2940 editor=editor,
2946 )
2941 )
2947
2942
2948 newdesc = changelog.stripdesc(new.description())
2943 newdesc = changelog.stripdesc(new.description())
2949 if (
2944 if (
2950 (not changes)
2945 (not changes)
2951 and newdesc == old.description()
2946 and newdesc == old.description()
2952 and user == old.user()
2947 and user == old.user()
2953 and (date == old.date() or datemaydiffer)
2948 and (date == old.date() or datemaydiffer)
2954 and pureextra == old.extra()
2949 and pureextra == old.extra()
2955 ):
2950 ):
2956 # nothing changed. continuing here would create a new node
2951 # nothing changed. continuing here would create a new node
2957 # anyway because of the amend_source noise.
2952 # anyway because of the amend_source noise.
2958 #
2953 #
2959 # This not what we expect from amend.
2954 # This not what we expect from amend.
2960 return old.node()
2955 return old.node()
2961
2956
2962 commitphase = None
2957 commitphase = None
2963 if opts.get(b'secret'):
2958 if opts.get(b'secret'):
2964 commitphase = phases.secret
2959 commitphase = phases.secret
2965 newid = repo.commitctx(new)
2960 newid = repo.commitctx(new)
2966 ms.reset()
2961 ms.reset()
2967
2962
2968 # Reroute the working copy parent to the new changeset
2963 # Reroute the working copy parent to the new changeset
2969 repo.setparents(newid, repo.nullid)
2964 repo.setparents(newid, repo.nullid)
2970
2965
2971 # Fixing the dirstate because localrepo.commitctx does not update
2966 # Fixing the dirstate because localrepo.commitctx does not update
2972 # it. This is rather convenient because we did not need to update
2967 # it. This is rather convenient because we did not need to update
2973 # the dirstate for all the files in the new commit which commitctx
2968 # the dirstate for all the files in the new commit which commitctx
2974 # could have done if it updated the dirstate. Now, we can
2969 # could have done if it updated the dirstate. Now, we can
2975 # selectively update the dirstate only for the amended files.
2970 # selectively update the dirstate only for the amended files.
2976 dirstate = repo.dirstate
2971 dirstate = repo.dirstate
2977
2972
2978 # Update the state of the files which were added and modified in the
2973 # Update the state of the files which were added and modified in the
2979 # amend to "normal" in the dirstate. We need to use "normallookup" since
2974 # amend to "normal" in the dirstate. We need to use "normallookup" since
2980 # the files may have changed since the command started; using "normal"
2975 # the files may have changed since the command started; using "normal"
2981 # would mark them as clean but with uncommitted contents.
2976 # would mark them as clean but with uncommitted contents.
2982 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2977 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2983 for f in normalfiles:
2978 for f in normalfiles:
2984 dirstate.normallookup(f)
2979 dirstate.normallookup(f)
2985
2980
2986 # Update the state of files which were removed in the amend
2981 # Update the state of files which were removed in the amend
2987 # to "removed" in the dirstate.
2982 # to "removed" in the dirstate.
2988 removedfiles = set(wctx.removed()) & filestoamend
2983 removedfiles = set(wctx.removed()) & filestoamend
2989 for f in removedfiles:
2984 for f in removedfiles:
2990 dirstate.drop(f)
2985 dirstate.drop(f)
2991
2986
2992 mapping = {old.node(): (newid,)}
2987 mapping = {old.node(): (newid,)}
2993 obsmetadata = None
2988 obsmetadata = None
2994 if opts.get(b'note'):
2989 if opts.get(b'note'):
2995 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
2990 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
2996 backup = ui.configbool(b'rewrite', b'backup-bundle')
2991 backup = ui.configbool(b'rewrite', b'backup-bundle')
2997 scmutil.cleanupnodes(
2992 scmutil.cleanupnodes(
2998 repo,
2993 repo,
2999 mapping,
2994 mapping,
3000 b'amend',
2995 b'amend',
3001 metadata=obsmetadata,
2996 metadata=obsmetadata,
3002 fixphase=True,
2997 fixphase=True,
3003 targetphase=commitphase,
2998 targetphase=commitphase,
3004 backup=backup,
2999 backup=backup,
3005 )
3000 )
3006
3001
3007 return newid
3002 return newid
3008
3003
3009
3004
3010 def commiteditor(repo, ctx, subs, editform=b''):
3005 def commiteditor(repo, ctx, subs, editform=b''):
3011 if ctx.description():
3006 if ctx.description():
3012 return ctx.description()
3007 return ctx.description()
3013 return commitforceeditor(
3008 return commitforceeditor(
3014 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3009 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3015 )
3010 )
3016
3011
3017
3012
3018 def commitforceeditor(
3013 def commitforceeditor(
3019 repo,
3014 repo,
3020 ctx,
3015 ctx,
3021 subs,
3016 subs,
3022 finishdesc=None,
3017 finishdesc=None,
3023 extramsg=None,
3018 extramsg=None,
3024 editform=b'',
3019 editform=b'',
3025 unchangedmessagedetection=False,
3020 unchangedmessagedetection=False,
3026 ):
3021 ):
3027 if not extramsg:
3022 if not extramsg:
3028 extramsg = _(b"Leave message empty to abort commit.")
3023 extramsg = _(b"Leave message empty to abort commit.")
3029
3024
3030 forms = [e for e in editform.split(b'.') if e]
3025 forms = [e for e in editform.split(b'.') if e]
3031 forms.insert(0, b'changeset')
3026 forms.insert(0, b'changeset')
3032 templatetext = None
3027 templatetext = None
3033 while forms:
3028 while forms:
3034 ref = b'.'.join(forms)
3029 ref = b'.'.join(forms)
3035 if repo.ui.config(b'committemplate', ref):
3030 if repo.ui.config(b'committemplate', ref):
3036 templatetext = committext = buildcommittemplate(
3031 templatetext = committext = buildcommittemplate(
3037 repo, ctx, subs, extramsg, ref
3032 repo, ctx, subs, extramsg, ref
3038 )
3033 )
3039 break
3034 break
3040 forms.pop()
3035 forms.pop()
3041 else:
3036 else:
3042 committext = buildcommittext(repo, ctx, subs, extramsg)
3037 committext = buildcommittext(repo, ctx, subs, extramsg)
3043
3038
3044 # run editor in the repository root
3039 # run editor in the repository root
3045 olddir = encoding.getcwd()
3040 olddir = encoding.getcwd()
3046 os.chdir(repo.root)
3041 os.chdir(repo.root)
3047
3042
3048 # make in-memory changes visible to external process
3043 # make in-memory changes visible to external process
3049 tr = repo.currenttransaction()
3044 tr = repo.currenttransaction()
3050 repo.dirstate.write(tr)
3045 repo.dirstate.write(tr)
3051 pending = tr and tr.writepending() and repo.root
3046 pending = tr and tr.writepending() and repo.root
3052
3047
3053 editortext = repo.ui.edit(
3048 editortext = repo.ui.edit(
3054 committext,
3049 committext,
3055 ctx.user(),
3050 ctx.user(),
3056 ctx.extra(),
3051 ctx.extra(),
3057 editform=editform,
3052 editform=editform,
3058 pending=pending,
3053 pending=pending,
3059 repopath=repo.path,
3054 repopath=repo.path,
3060 action=b'commit',
3055 action=b'commit',
3061 )
3056 )
3062 text = editortext
3057 text = editortext
3063
3058
3064 # strip away anything below this special string (used for editors that want
3059 # strip away anything below this special string (used for editors that want
3065 # to display the diff)
3060 # to display the diff)
3066 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3061 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3067 if stripbelow:
3062 if stripbelow:
3068 text = text[: stripbelow.start()]
3063 text = text[: stripbelow.start()]
3069
3064
3070 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3065 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3071 os.chdir(olddir)
3066 os.chdir(olddir)
3072
3067
3073 if finishdesc:
3068 if finishdesc:
3074 text = finishdesc(text)
3069 text = finishdesc(text)
3075 if not text.strip():
3070 if not text.strip():
3076 raise error.InputError(_(b"empty commit message"))
3071 raise error.InputError(_(b"empty commit message"))
3077 if unchangedmessagedetection and editortext == templatetext:
3072 if unchangedmessagedetection and editortext == templatetext:
3078 raise error.InputError(_(b"commit message unchanged"))
3073 raise error.InputError(_(b"commit message unchanged"))
3079
3074
3080 return text
3075 return text
3081
3076
3082
3077
3083 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3078 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3084 ui = repo.ui
3079 ui = repo.ui
3085 spec = formatter.reference_templatespec(ref)
3080 spec = formatter.reference_templatespec(ref)
3086 t = logcmdutil.changesettemplater(ui, repo, spec)
3081 t = logcmdutil.changesettemplater(ui, repo, spec)
3087 t.t.cache.update(
3082 t.t.cache.update(
3088 (k, templater.unquotestring(v))
3083 (k, templater.unquotestring(v))
3089 for k, v in repo.ui.configitems(b'committemplate')
3084 for k, v in repo.ui.configitems(b'committemplate')
3090 )
3085 )
3091
3086
3092 if not extramsg:
3087 if not extramsg:
3093 extramsg = b'' # ensure that extramsg is string
3088 extramsg = b'' # ensure that extramsg is string
3094
3089
3095 ui.pushbuffer()
3090 ui.pushbuffer()
3096 t.show(ctx, extramsg=extramsg)
3091 t.show(ctx, extramsg=extramsg)
3097 return ui.popbuffer()
3092 return ui.popbuffer()
3098
3093
3099
3094
3100 def hgprefix(msg):
3095 def hgprefix(msg):
3101 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3096 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3102
3097
3103
3098
3104 def buildcommittext(repo, ctx, subs, extramsg):
3099 def buildcommittext(repo, ctx, subs, extramsg):
3105 edittext = []
3100 edittext = []
3106 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3101 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3107 if ctx.description():
3102 if ctx.description():
3108 edittext.append(ctx.description())
3103 edittext.append(ctx.description())
3109 edittext.append(b"")
3104 edittext.append(b"")
3110 edittext.append(b"") # Empty line between message and comments.
3105 edittext.append(b"") # Empty line between message and comments.
3111 edittext.append(
3106 edittext.append(
3112 hgprefix(
3107 hgprefix(
3113 _(
3108 _(
3114 b"Enter commit message."
3109 b"Enter commit message."
3115 b" Lines beginning with 'HG:' are removed."
3110 b" Lines beginning with 'HG:' are removed."
3116 )
3111 )
3117 )
3112 )
3118 )
3113 )
3119 edittext.append(hgprefix(extramsg))
3114 edittext.append(hgprefix(extramsg))
3120 edittext.append(b"HG: --")
3115 edittext.append(b"HG: --")
3121 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3116 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3122 if ctx.p2():
3117 if ctx.p2():
3123 edittext.append(hgprefix(_(b"branch merge")))
3118 edittext.append(hgprefix(_(b"branch merge")))
3124 if ctx.branch():
3119 if ctx.branch():
3125 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3120 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3126 if bookmarks.isactivewdirparent(repo):
3121 if bookmarks.isactivewdirparent(repo):
3127 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3122 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3128 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3123 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3129 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3124 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3130 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3125 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3131 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3126 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3132 if not added and not modified and not removed:
3127 if not added and not modified and not removed:
3133 edittext.append(hgprefix(_(b"no files changed")))
3128 edittext.append(hgprefix(_(b"no files changed")))
3134 edittext.append(b"")
3129 edittext.append(b"")
3135
3130
3136 return b"\n".join(edittext)
3131 return b"\n".join(edittext)
3137
3132
3138
3133
3139 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3134 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3140 if opts is None:
3135 if opts is None:
3141 opts = {}
3136 opts = {}
3142 ctx = repo[node]
3137 ctx = repo[node]
3143 parents = ctx.parents()
3138 parents = ctx.parents()
3144
3139
3145 if tip is not None and repo.changelog.tip() == tip:
3140 if tip is not None and repo.changelog.tip() == tip:
3146 # avoid reporting something like "committed new head" when
3141 # avoid reporting something like "committed new head" when
3147 # recommitting old changesets, and issue a helpful warning
3142 # recommitting old changesets, and issue a helpful warning
3148 # for most instances
3143 # for most instances
3149 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3144 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3150 elif (
3145 elif (
3151 not opts.get(b'amend')
3146 not opts.get(b'amend')
3152 and bheads
3147 and bheads
3153 and node not in bheads
3148 and node not in bheads
3154 and not any(
3149 and not any(
3155 p.node() in bheads and p.branch() == branch for p in parents
3150 p.node() in bheads and p.branch() == branch for p in parents
3156 )
3151 )
3157 ):
3152 ):
3158 repo.ui.status(_(b'created new head\n'))
3153 repo.ui.status(_(b'created new head\n'))
3159 # The message is not printed for initial roots. For the other
3154 # The message is not printed for initial roots. For the other
3160 # changesets, it is printed in the following situations:
3155 # changesets, it is printed in the following situations:
3161 #
3156 #
3162 # Par column: for the 2 parents with ...
3157 # Par column: for the 2 parents with ...
3163 # N: null or no parent
3158 # N: null or no parent
3164 # B: parent is on another named branch
3159 # B: parent is on another named branch
3165 # C: parent is a regular non head changeset
3160 # C: parent is a regular non head changeset
3166 # H: parent was a branch head of the current branch
3161 # H: parent was a branch head of the current branch
3167 # Msg column: whether we print "created new head" message
3162 # Msg column: whether we print "created new head" message
3168 # In the following, it is assumed that there already exists some
3163 # In the following, it is assumed that there already exists some
3169 # initial branch heads of the current branch, otherwise nothing is
3164 # initial branch heads of the current branch, otherwise nothing is
3170 # printed anyway.
3165 # printed anyway.
3171 #
3166 #
3172 # Par Msg Comment
3167 # Par Msg Comment
3173 # N N y additional topo root
3168 # N N y additional topo root
3174 #
3169 #
3175 # B N y additional branch root
3170 # B N y additional branch root
3176 # C N y additional topo head
3171 # C N y additional topo head
3177 # H N n usual case
3172 # H N n usual case
3178 #
3173 #
3179 # B B y weird additional branch root
3174 # B B y weird additional branch root
3180 # C B y branch merge
3175 # C B y branch merge
3181 # H B n merge with named branch
3176 # H B n merge with named branch
3182 #
3177 #
3183 # C C y additional head from merge
3178 # C C y additional head from merge
3184 # C H n merge with a head
3179 # C H n merge with a head
3185 #
3180 #
3186 # H H n head merge: head count decreases
3181 # H H n head merge: head count decreases
3187
3182
3188 if not opts.get(b'close_branch'):
3183 if not opts.get(b'close_branch'):
3189 for r in parents:
3184 for r in parents:
3190 if r.closesbranch() and r.branch() == branch:
3185 if r.closesbranch() and r.branch() == branch:
3191 repo.ui.status(
3186 repo.ui.status(
3192 _(b'reopening closed branch head %d\n') % r.rev()
3187 _(b'reopening closed branch head %d\n') % r.rev()
3193 )
3188 )
3194
3189
3195 if repo.ui.debugflag:
3190 if repo.ui.debugflag:
3196 repo.ui.write(
3191 repo.ui.write(
3197 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3192 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3198 )
3193 )
3199 elif repo.ui.verbose:
3194 elif repo.ui.verbose:
3200 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3195 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3201
3196
3202
3197
3203 def postcommitstatus(repo, pats, opts):
3198 def postcommitstatus(repo, pats, opts):
3204 return repo.status(match=scmutil.match(repo[None], pats, opts))
3199 return repo.status(match=scmutil.match(repo[None], pats, opts))
3205
3200
3206
3201
3207 def revert(ui, repo, ctx, *pats, **opts):
3202 def revert(ui, repo, ctx, *pats, **opts):
3208 opts = pycompat.byteskwargs(opts)
3203 opts = pycompat.byteskwargs(opts)
3209 parent, p2 = repo.dirstate.parents()
3204 parent, p2 = repo.dirstate.parents()
3210 node = ctx.node()
3205 node = ctx.node()
3211
3206
3212 mf = ctx.manifest()
3207 mf = ctx.manifest()
3213 if node == p2:
3208 if node == p2:
3214 parent = p2
3209 parent = p2
3215
3210
3216 # need all matching names in dirstate and manifest of target rev,
3211 # need all matching names in dirstate and manifest of target rev,
3217 # so have to walk both. do not print errors if files exist in one
3212 # so have to walk both. do not print errors if files exist in one
3218 # but not other. in both cases, filesets should be evaluated against
3213 # but not other. in both cases, filesets should be evaluated against
3219 # workingctx to get consistent result (issue4497). this means 'set:**'
3214 # workingctx to get consistent result (issue4497). this means 'set:**'
3220 # cannot be used to select missing files from target rev.
3215 # cannot be used to select missing files from target rev.
3221
3216
3222 # `names` is a mapping for all elements in working copy and target revision
3217 # `names` is a mapping for all elements in working copy and target revision
3223 # The mapping is in the form:
3218 # The mapping is in the form:
3224 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3219 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3225 names = {}
3220 names = {}
3226 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3221 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3227
3222
3228 with repo.wlock():
3223 with repo.wlock():
3229 ## filling of the `names` mapping
3224 ## filling of the `names` mapping
3230 # walk dirstate to fill `names`
3225 # walk dirstate to fill `names`
3231
3226
3232 interactive = opts.get(b'interactive', False)
3227 interactive = opts.get(b'interactive', False)
3233 wctx = repo[None]
3228 wctx = repo[None]
3234 m = scmutil.match(wctx, pats, opts)
3229 m = scmutil.match(wctx, pats, opts)
3235
3230
3236 # we'll need this later
3231 # we'll need this later
3237 targetsubs = sorted(s for s in wctx.substate if m(s))
3232 targetsubs = sorted(s for s in wctx.substate if m(s))
3238
3233
3239 if not m.always():
3234 if not m.always():
3240 matcher = matchmod.badmatch(m, lambda x, y: False)
3235 matcher = matchmod.badmatch(m, lambda x, y: False)
3241 for abs in wctx.walk(matcher):
3236 for abs in wctx.walk(matcher):
3242 names[abs] = m.exact(abs)
3237 names[abs] = m.exact(abs)
3243
3238
3244 # walk target manifest to fill `names`
3239 # walk target manifest to fill `names`
3245
3240
3246 def badfn(path, msg):
3241 def badfn(path, msg):
3247 if path in names:
3242 if path in names:
3248 return
3243 return
3249 if path in ctx.substate:
3244 if path in ctx.substate:
3250 return
3245 return
3251 path_ = path + b'/'
3246 path_ = path + b'/'
3252 for f in names:
3247 for f in names:
3253 if f.startswith(path_):
3248 if f.startswith(path_):
3254 return
3249 return
3255 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3250 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3256
3251
3257 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3252 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3258 if abs not in names:
3253 if abs not in names:
3259 names[abs] = m.exact(abs)
3254 names[abs] = m.exact(abs)
3260
3255
3261 # Find status of all file in `names`.
3256 # Find status of all file in `names`.
3262 m = scmutil.matchfiles(repo, names)
3257 m = scmutil.matchfiles(repo, names)
3263
3258
3264 changes = repo.status(
3259 changes = repo.status(
3265 node1=node, match=m, unknown=True, ignored=True, clean=True
3260 node1=node, match=m, unknown=True, ignored=True, clean=True
3266 )
3261 )
3267 else:
3262 else:
3268 changes = repo.status(node1=node, match=m)
3263 changes = repo.status(node1=node, match=m)
3269 for kind in changes:
3264 for kind in changes:
3270 for abs in kind:
3265 for abs in kind:
3271 names[abs] = m.exact(abs)
3266 names[abs] = m.exact(abs)
3272
3267
3273 m = scmutil.matchfiles(repo, names)
3268 m = scmutil.matchfiles(repo, names)
3274
3269
3275 modified = set(changes.modified)
3270 modified = set(changes.modified)
3276 added = set(changes.added)
3271 added = set(changes.added)
3277 removed = set(changes.removed)
3272 removed = set(changes.removed)
3278 _deleted = set(changes.deleted)
3273 _deleted = set(changes.deleted)
3279 unknown = set(changes.unknown)
3274 unknown = set(changes.unknown)
3280 unknown.update(changes.ignored)
3275 unknown.update(changes.ignored)
3281 clean = set(changes.clean)
3276 clean = set(changes.clean)
3282 modadded = set()
3277 modadded = set()
3283
3278
3284 # We need to account for the state of the file in the dirstate,
3279 # We need to account for the state of the file in the dirstate,
3285 # even when we revert against something else than parent. This will
3280 # even when we revert against something else than parent. This will
3286 # slightly alter the behavior of revert (doing back up or not, delete
3281 # slightly alter the behavior of revert (doing back up or not, delete
3287 # or just forget etc).
3282 # or just forget etc).
3288 if parent == node:
3283 if parent == node:
3289 dsmodified = modified
3284 dsmodified = modified
3290 dsadded = added
3285 dsadded = added
3291 dsremoved = removed
3286 dsremoved = removed
3292 # store all local modifications, useful later for rename detection
3287 # store all local modifications, useful later for rename detection
3293 localchanges = dsmodified | dsadded
3288 localchanges = dsmodified | dsadded
3294 modified, added, removed = set(), set(), set()
3289 modified, added, removed = set(), set(), set()
3295 else:
3290 else:
3296 changes = repo.status(node1=parent, match=m)
3291 changes = repo.status(node1=parent, match=m)
3297 dsmodified = set(changes.modified)
3292 dsmodified = set(changes.modified)
3298 dsadded = set(changes.added)
3293 dsadded = set(changes.added)
3299 dsremoved = set(changes.removed)
3294 dsremoved = set(changes.removed)
3300 # store all local modifications, useful later for rename detection
3295 # store all local modifications, useful later for rename detection
3301 localchanges = dsmodified | dsadded
3296 localchanges = dsmodified | dsadded
3302
3297
3303 # only take into account for removes between wc and target
3298 # only take into account for removes between wc and target
3304 clean |= dsremoved - removed
3299 clean |= dsremoved - removed
3305 dsremoved &= removed
3300 dsremoved &= removed
3306 # distinct between dirstate remove and other
3301 # distinct between dirstate remove and other
3307 removed -= dsremoved
3302 removed -= dsremoved
3308
3303
3309 modadded = added & dsmodified
3304 modadded = added & dsmodified
3310 added -= modadded
3305 added -= modadded
3311
3306
3312 # tell newly modified apart.
3307 # tell newly modified apart.
3313 dsmodified &= modified
3308 dsmodified &= modified
3314 dsmodified |= modified & dsadded # dirstate added may need backup
3309 dsmodified |= modified & dsadded # dirstate added may need backup
3315 modified -= dsmodified
3310 modified -= dsmodified
3316
3311
3317 # We need to wait for some post-processing to update this set
3312 # We need to wait for some post-processing to update this set
3318 # before making the distinction. The dirstate will be used for
3313 # before making the distinction. The dirstate will be used for
3319 # that purpose.
3314 # that purpose.
3320 dsadded = added
3315 dsadded = added
3321
3316
3322 # in case of merge, files that are actually added can be reported as
3317 # in case of merge, files that are actually added can be reported as
3323 # modified, we need to post process the result
3318 # modified, we need to post process the result
3324 if p2 != repo.nullid:
3319 if p2 != repo.nullid:
3325 mergeadd = set(dsmodified)
3320 mergeadd = set(dsmodified)
3326 for path in dsmodified:
3321 for path in dsmodified:
3327 if path in mf:
3322 if path in mf:
3328 mergeadd.remove(path)
3323 mergeadd.remove(path)
3329 dsadded |= mergeadd
3324 dsadded |= mergeadd
3330 dsmodified -= mergeadd
3325 dsmodified -= mergeadd
3331
3326
3332 # if f is a rename, update `names` to also revert the source
3327 # if f is a rename, update `names` to also revert the source
3333 for f in localchanges:
3328 for f in localchanges:
3334 src = repo.dirstate.copied(f)
3329 src = repo.dirstate.copied(f)
3335 # XXX should we check for rename down to target node?
3330 # XXX should we check for rename down to target node?
3336 if src and src not in names and repo.dirstate[src] == b'r':
3331 if src and src not in names and repo.dirstate[src] == b'r':
3337 dsremoved.add(src)
3332 dsremoved.add(src)
3338 names[src] = True
3333 names[src] = True
3339
3334
3340 # determine the exact nature of the deleted changesets
3335 # determine the exact nature of the deleted changesets
3341 deladded = set(_deleted)
3336 deladded = set(_deleted)
3342 for path in _deleted:
3337 for path in _deleted:
3343 if path in mf:
3338 if path in mf:
3344 deladded.remove(path)
3339 deladded.remove(path)
3345 deleted = _deleted - deladded
3340 deleted = _deleted - deladded
3346
3341
3347 # distinguish between file to forget and the other
3342 # distinguish between file to forget and the other
3348 added = set()
3343 added = set()
3349 for abs in dsadded:
3344 for abs in dsadded:
3350 if repo.dirstate[abs] != b'a':
3345 if repo.dirstate[abs] != b'a':
3351 added.add(abs)
3346 added.add(abs)
3352 dsadded -= added
3347 dsadded -= added
3353
3348
3354 for abs in deladded:
3349 for abs in deladded:
3355 if repo.dirstate[abs] == b'a':
3350 if repo.dirstate[abs] == b'a':
3356 dsadded.add(abs)
3351 dsadded.add(abs)
3357 deladded -= dsadded
3352 deladded -= dsadded
3358
3353
3359 # For files marked as removed, we check if an unknown file is present at
3354 # For files marked as removed, we check if an unknown file is present at
3360 # the same path. If a such file exists it may need to be backed up.
3355 # the same path. If a such file exists it may need to be backed up.
3361 # Making the distinction at this stage helps have simpler backup
3356 # Making the distinction at this stage helps have simpler backup
3362 # logic.
3357 # logic.
3363 removunk = set()
3358 removunk = set()
3364 for abs in removed:
3359 for abs in removed:
3365 target = repo.wjoin(abs)
3360 target = repo.wjoin(abs)
3366 if os.path.lexists(target):
3361 if os.path.lexists(target):
3367 removunk.add(abs)
3362 removunk.add(abs)
3368 removed -= removunk
3363 removed -= removunk
3369
3364
3370 dsremovunk = set()
3365 dsremovunk = set()
3371 for abs in dsremoved:
3366 for abs in dsremoved:
3372 target = repo.wjoin(abs)
3367 target = repo.wjoin(abs)
3373 if os.path.lexists(target):
3368 if os.path.lexists(target):
3374 dsremovunk.add(abs)
3369 dsremovunk.add(abs)
3375 dsremoved -= dsremovunk
3370 dsremoved -= dsremovunk
3376
3371
3377 # action to be actually performed by revert
3372 # action to be actually performed by revert
3378 # (<list of file>, message>) tuple
3373 # (<list of file>, message>) tuple
3379 actions = {
3374 actions = {
3380 b'revert': ([], _(b'reverting %s\n')),
3375 b'revert': ([], _(b'reverting %s\n')),
3381 b'add': ([], _(b'adding %s\n')),
3376 b'add': ([], _(b'adding %s\n')),
3382 b'remove': ([], _(b'removing %s\n')),
3377 b'remove': ([], _(b'removing %s\n')),
3383 b'drop': ([], _(b'removing %s\n')),
3378 b'drop': ([], _(b'removing %s\n')),
3384 b'forget': ([], _(b'forgetting %s\n')),
3379 b'forget': ([], _(b'forgetting %s\n')),
3385 b'undelete': ([], _(b'undeleting %s\n')),
3380 b'undelete': ([], _(b'undeleting %s\n')),
3386 b'noop': (None, _(b'no changes needed to %s\n')),
3381 b'noop': (None, _(b'no changes needed to %s\n')),
3387 b'unknown': (None, _(b'file not managed: %s\n')),
3382 b'unknown': (None, _(b'file not managed: %s\n')),
3388 }
3383 }
3389
3384
3390 # "constant" that convey the backup strategy.
3385 # "constant" that convey the backup strategy.
3391 # All set to `discard` if `no-backup` is set do avoid checking
3386 # All set to `discard` if `no-backup` is set do avoid checking
3392 # no_backup lower in the code.
3387 # no_backup lower in the code.
3393 # These values are ordered for comparison purposes
3388 # These values are ordered for comparison purposes
3394 backupinteractive = 3 # do backup if interactively modified
3389 backupinteractive = 3 # do backup if interactively modified
3395 backup = 2 # unconditionally do backup
3390 backup = 2 # unconditionally do backup
3396 check = 1 # check if the existing file differs from target
3391 check = 1 # check if the existing file differs from target
3397 discard = 0 # never do backup
3392 discard = 0 # never do backup
3398 if opts.get(b'no_backup'):
3393 if opts.get(b'no_backup'):
3399 backupinteractive = backup = check = discard
3394 backupinteractive = backup = check = discard
3400 if interactive:
3395 if interactive:
3401 dsmodifiedbackup = backupinteractive
3396 dsmodifiedbackup = backupinteractive
3402 else:
3397 else:
3403 dsmodifiedbackup = backup
3398 dsmodifiedbackup = backup
3404 tobackup = set()
3399 tobackup = set()
3405
3400
3406 backupanddel = actions[b'remove']
3401 backupanddel = actions[b'remove']
3407 if not opts.get(b'no_backup'):
3402 if not opts.get(b'no_backup'):
3408 backupanddel = actions[b'drop']
3403 backupanddel = actions[b'drop']
3409
3404
3410 disptable = (
3405 disptable = (
3411 # dispatch table:
3406 # dispatch table:
3412 # file state
3407 # file state
3413 # action
3408 # action
3414 # make backup
3409 # make backup
3415 ## Sets that results that will change file on disk
3410 ## Sets that results that will change file on disk
3416 # Modified compared to target, no local change
3411 # Modified compared to target, no local change
3417 (modified, actions[b'revert'], discard),
3412 (modified, actions[b'revert'], discard),
3418 # Modified compared to target, but local file is deleted
3413 # Modified compared to target, but local file is deleted
3419 (deleted, actions[b'revert'], discard),
3414 (deleted, actions[b'revert'], discard),
3420 # Modified compared to target, local change
3415 # Modified compared to target, local change
3421 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3416 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3422 # Added since target
3417 # Added since target
3423 (added, actions[b'remove'], discard),
3418 (added, actions[b'remove'], discard),
3424 # Added in working directory
3419 # Added in working directory
3425 (dsadded, actions[b'forget'], discard),
3420 (dsadded, actions[b'forget'], discard),
3426 # Added since target, have local modification
3421 # Added since target, have local modification
3427 (modadded, backupanddel, backup),
3422 (modadded, backupanddel, backup),
3428 # Added since target but file is missing in working directory
3423 # Added since target but file is missing in working directory
3429 (deladded, actions[b'drop'], discard),
3424 (deladded, actions[b'drop'], discard),
3430 # Removed since target, before working copy parent
3425 # Removed since target, before working copy parent
3431 (removed, actions[b'add'], discard),
3426 (removed, actions[b'add'], discard),
3432 # Same as `removed` but an unknown file exists at the same path
3427 # Same as `removed` but an unknown file exists at the same path
3433 (removunk, actions[b'add'], check),
3428 (removunk, actions[b'add'], check),
3434 # Removed since targe, marked as such in working copy parent
3429 # Removed since targe, marked as such in working copy parent
3435 (dsremoved, actions[b'undelete'], discard),
3430 (dsremoved, actions[b'undelete'], discard),
3436 # Same as `dsremoved` but an unknown file exists at the same path
3431 # Same as `dsremoved` but an unknown file exists at the same path
3437 (dsremovunk, actions[b'undelete'], check),
3432 (dsremovunk, actions[b'undelete'], check),
3438 ## the following sets does not result in any file changes
3433 ## the following sets does not result in any file changes
3439 # File with no modification
3434 # File with no modification
3440 (clean, actions[b'noop'], discard),
3435 (clean, actions[b'noop'], discard),
3441 # Existing file, not tracked anywhere
3436 # Existing file, not tracked anywhere
3442 (unknown, actions[b'unknown'], discard),
3437 (unknown, actions[b'unknown'], discard),
3443 )
3438 )
3444
3439
3445 for abs, exact in sorted(names.items()):
3440 for abs, exact in sorted(names.items()):
3446 # target file to be touch on disk (relative to cwd)
3441 # target file to be touch on disk (relative to cwd)
3447 target = repo.wjoin(abs)
3442 target = repo.wjoin(abs)
3448 # search the entry in the dispatch table.
3443 # search the entry in the dispatch table.
3449 # if the file is in any of these sets, it was touched in the working
3444 # if the file is in any of these sets, it was touched in the working
3450 # directory parent and we are sure it needs to be reverted.
3445 # directory parent and we are sure it needs to be reverted.
3451 for table, (xlist, msg), dobackup in disptable:
3446 for table, (xlist, msg), dobackup in disptable:
3452 if abs not in table:
3447 if abs not in table:
3453 continue
3448 continue
3454 if xlist is not None:
3449 if xlist is not None:
3455 xlist.append(abs)
3450 xlist.append(abs)
3456 if dobackup:
3451 if dobackup:
3457 # If in interactive mode, don't automatically create
3452 # If in interactive mode, don't automatically create
3458 # .orig files (issue4793)
3453 # .orig files (issue4793)
3459 if dobackup == backupinteractive:
3454 if dobackup == backupinteractive:
3460 tobackup.add(abs)
3455 tobackup.add(abs)
3461 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3456 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3462 absbakname = scmutil.backuppath(ui, repo, abs)
3457 absbakname = scmutil.backuppath(ui, repo, abs)
3463 bakname = os.path.relpath(
3458 bakname = os.path.relpath(
3464 absbakname, start=repo.root
3459 absbakname, start=repo.root
3465 )
3460 )
3466 ui.note(
3461 ui.note(
3467 _(b'saving current version of %s as %s\n')
3462 _(b'saving current version of %s as %s\n')
3468 % (uipathfn(abs), uipathfn(bakname))
3463 % (uipathfn(abs), uipathfn(bakname))
3469 )
3464 )
3470 if not opts.get(b'dry_run'):
3465 if not opts.get(b'dry_run'):
3471 if interactive:
3466 if interactive:
3472 util.copyfile(target, absbakname)
3467 util.copyfile(target, absbakname)
3473 else:
3468 else:
3474 util.rename(target, absbakname)
3469 util.rename(target, absbakname)
3475 if opts.get(b'dry_run'):
3470 if opts.get(b'dry_run'):
3476 if ui.verbose or not exact:
3471 if ui.verbose or not exact:
3477 ui.status(msg % uipathfn(abs))
3472 ui.status(msg % uipathfn(abs))
3478 elif exact:
3473 elif exact:
3479 ui.warn(msg % uipathfn(abs))
3474 ui.warn(msg % uipathfn(abs))
3480 break
3475 break
3481
3476
3482 if not opts.get(b'dry_run'):
3477 if not opts.get(b'dry_run'):
3483 needdata = (b'revert', b'add', b'undelete')
3478 needdata = (b'revert', b'add', b'undelete')
3484 oplist = [actions[name][0] for name in needdata]
3479 oplist = [actions[name][0] for name in needdata]
3485 prefetch = scmutil.prefetchfiles
3480 prefetch = scmutil.prefetchfiles
3486 matchfiles = scmutil.matchfiles(
3481 matchfiles = scmutil.matchfiles(
3487 repo, [f for sublist in oplist for f in sublist]
3482 repo, [f for sublist in oplist for f in sublist]
3488 )
3483 )
3489 prefetch(
3484 prefetch(
3490 repo,
3485 repo,
3491 [(ctx.rev(), matchfiles)],
3486 [(ctx.rev(), matchfiles)],
3492 )
3487 )
3493 match = scmutil.match(repo[None], pats)
3488 match = scmutil.match(repo[None], pats)
3494 _performrevert(
3489 _performrevert(
3495 repo,
3490 repo,
3496 ctx,
3491 ctx,
3497 names,
3492 names,
3498 uipathfn,
3493 uipathfn,
3499 actions,
3494 actions,
3500 match,
3495 match,
3501 interactive,
3496 interactive,
3502 tobackup,
3497 tobackup,
3503 )
3498 )
3504
3499
3505 if targetsubs:
3500 if targetsubs:
3506 # Revert the subrepos on the revert list
3501 # Revert the subrepos on the revert list
3507 for sub in targetsubs:
3502 for sub in targetsubs:
3508 try:
3503 try:
3509 wctx.sub(sub).revert(
3504 wctx.sub(sub).revert(
3510 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3505 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3511 )
3506 )
3512 except KeyError:
3507 except KeyError:
3513 raise error.Abort(
3508 raise error.Abort(
3514 b"subrepository '%s' does not exist in %s!"
3509 b"subrepository '%s' does not exist in %s!"
3515 % (sub, short(ctx.node()))
3510 % (sub, short(ctx.node()))
3516 )
3511 )
3517
3512
3518
3513
3519 def _performrevert(
3514 def _performrevert(
3520 repo,
3515 repo,
3521 ctx,
3516 ctx,
3522 names,
3517 names,
3523 uipathfn,
3518 uipathfn,
3524 actions,
3519 actions,
3525 match,
3520 match,
3526 interactive=False,
3521 interactive=False,
3527 tobackup=None,
3522 tobackup=None,
3528 ):
3523 ):
3529 """function that actually perform all the actions computed for revert
3524 """function that actually perform all the actions computed for revert
3530
3525
3531 This is an independent function to let extension to plug in and react to
3526 This is an independent function to let extension to plug in and react to
3532 the imminent revert.
3527 the imminent revert.
3533
3528
3534 Make sure you have the working directory locked when calling this function.
3529 Make sure you have the working directory locked when calling this function.
3535 """
3530 """
3536 parent, p2 = repo.dirstate.parents()
3531 parent, p2 = repo.dirstate.parents()
3537 node = ctx.node()
3532 node = ctx.node()
3538 excluded_files = []
3533 excluded_files = []
3539
3534
3540 def checkout(f):
3535 def checkout(f):
3541 fc = ctx[f]
3536 fc = ctx[f]
3542 repo.wwrite(f, fc.data(), fc.flags())
3537 repo.wwrite(f, fc.data(), fc.flags())
3543
3538
3544 def doremove(f):
3539 def doremove(f):
3545 try:
3540 try:
3546 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3541 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3547 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3542 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3548 except OSError:
3543 except OSError:
3549 pass
3544 pass
3550 repo.dirstate.remove(f)
3545 repo.dirstate.remove(f)
3551
3546
3552 def prntstatusmsg(action, f):
3547 def prntstatusmsg(action, f):
3553 exact = names[f]
3548 exact = names[f]
3554 if repo.ui.verbose or not exact:
3549 if repo.ui.verbose or not exact:
3555 repo.ui.status(actions[action][1] % uipathfn(f))
3550 repo.ui.status(actions[action][1] % uipathfn(f))
3556
3551
3557 audit_path = pathutil.pathauditor(repo.root, cached=True)
3552 audit_path = pathutil.pathauditor(repo.root, cached=True)
3558 for f in actions[b'forget'][0]:
3553 for f in actions[b'forget'][0]:
3559 if interactive:
3554 if interactive:
3560 choice = repo.ui.promptchoice(
3555 choice = repo.ui.promptchoice(
3561 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3556 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3562 )
3557 )
3563 if choice == 0:
3558 if choice == 0:
3564 prntstatusmsg(b'forget', f)
3559 prntstatusmsg(b'forget', f)
3565 repo.dirstate.drop(f)
3560 repo.dirstate.drop(f)
3566 else:
3561 else:
3567 excluded_files.append(f)
3562 excluded_files.append(f)
3568 else:
3563 else:
3569 prntstatusmsg(b'forget', f)
3564 prntstatusmsg(b'forget', f)
3570 repo.dirstate.drop(f)
3565 repo.dirstate.drop(f)
3571 for f in actions[b'remove'][0]:
3566 for f in actions[b'remove'][0]:
3572 audit_path(f)
3567 audit_path(f)
3573 if interactive:
3568 if interactive:
3574 choice = repo.ui.promptchoice(
3569 choice = repo.ui.promptchoice(
3575 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3570 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3576 )
3571 )
3577 if choice == 0:
3572 if choice == 0:
3578 prntstatusmsg(b'remove', f)
3573 prntstatusmsg(b'remove', f)
3579 doremove(f)
3574 doremove(f)
3580 else:
3575 else:
3581 excluded_files.append(f)
3576 excluded_files.append(f)
3582 else:
3577 else:
3583 prntstatusmsg(b'remove', f)
3578 prntstatusmsg(b'remove', f)
3584 doremove(f)
3579 doremove(f)
3585 for f in actions[b'drop'][0]:
3580 for f in actions[b'drop'][0]:
3586 audit_path(f)
3581 audit_path(f)
3587 prntstatusmsg(b'drop', f)
3582 prntstatusmsg(b'drop', f)
3588 repo.dirstate.remove(f)
3583 repo.dirstate.remove(f)
3589
3584
3590 normal = None
3585 normal = None
3591 if node == parent:
3586 if node == parent:
3592 # We're reverting to our parent. If possible, we'd like status
3587 # We're reverting to our parent. If possible, we'd like status
3593 # to report the file as clean. We have to use normallookup for
3588 # to report the file as clean. We have to use normallookup for
3594 # merges to avoid losing information about merged/dirty files.
3589 # merges to avoid losing information about merged/dirty files.
3595 if p2 != repo.nullid:
3590 if p2 != repo.nullid:
3596 normal = repo.dirstate.normallookup
3591 normal = repo.dirstate.normallookup
3597 else:
3592 else:
3598 normal = repo.dirstate.normal
3593 normal = repo.dirstate.normal
3599
3594
3600 newlyaddedandmodifiedfiles = set()
3595 newlyaddedandmodifiedfiles = set()
3601 if interactive:
3596 if interactive:
3602 # Prompt the user for changes to revert
3597 # Prompt the user for changes to revert
3603 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3598 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3604 m = scmutil.matchfiles(repo, torevert)
3599 m = scmutil.matchfiles(repo, torevert)
3605 diffopts = patch.difffeatureopts(
3600 diffopts = patch.difffeatureopts(
3606 repo.ui,
3601 repo.ui,
3607 whitespace=True,
3602 whitespace=True,
3608 section=b'commands',
3603 section=b'commands',
3609 configprefix=b'revert.interactive.',
3604 configprefix=b'revert.interactive.',
3610 )
3605 )
3611 diffopts.nodates = True
3606 diffopts.nodates = True
3612 diffopts.git = True
3607 diffopts.git = True
3613 operation = b'apply'
3608 operation = b'apply'
3614 if node == parent:
3609 if node == parent:
3615 if repo.ui.configbool(
3610 if repo.ui.configbool(
3616 b'experimental', b'revert.interactive.select-to-keep'
3611 b'experimental', b'revert.interactive.select-to-keep'
3617 ):
3612 ):
3618 operation = b'keep'
3613 operation = b'keep'
3619 else:
3614 else:
3620 operation = b'discard'
3615 operation = b'discard'
3621
3616
3622 if operation == b'apply':
3617 if operation == b'apply':
3623 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3618 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3624 else:
3619 else:
3625 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3620 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3626 originalchunks = patch.parsepatch(diff)
3621 originalchunks = patch.parsepatch(diff)
3627
3622
3628 try:
3623 try:
3629
3624
3630 chunks, opts = recordfilter(
3625 chunks, opts = recordfilter(
3631 repo.ui, originalchunks, match, operation=operation
3626 repo.ui, originalchunks, match, operation=operation
3632 )
3627 )
3633 if operation == b'discard':
3628 if operation == b'discard':
3634 chunks = patch.reversehunks(chunks)
3629 chunks = patch.reversehunks(chunks)
3635
3630
3636 except error.PatchError as err:
3631 except error.PatchError as err:
3637 raise error.Abort(_(b'error parsing patch: %s') % err)
3632 raise error.Abort(_(b'error parsing patch: %s') % err)
3638
3633
3639 # FIXME: when doing an interactive revert of a copy, there's no way of
3634 # FIXME: when doing an interactive revert of a copy, there's no way of
3640 # performing a partial revert of the added file, the only option is
3635 # performing a partial revert of the added file, the only option is
3641 # "remove added file <name> (Yn)?", so we don't need to worry about the
3636 # "remove added file <name> (Yn)?", so we don't need to worry about the
3642 # alsorestore value. Ideally we'd be able to partially revert
3637 # alsorestore value. Ideally we'd be able to partially revert
3643 # copied/renamed files.
3638 # copied/renamed files.
3644 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3639 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3645 chunks, originalchunks
3640 chunks, originalchunks
3646 )
3641 )
3647 if tobackup is None:
3642 if tobackup is None:
3648 tobackup = set()
3643 tobackup = set()
3649 # Apply changes
3644 # Apply changes
3650 fp = stringio()
3645 fp = stringio()
3651 # chunks are serialized per file, but files aren't sorted
3646 # chunks are serialized per file, but files aren't sorted
3652 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3647 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3653 prntstatusmsg(b'revert', f)
3648 prntstatusmsg(b'revert', f)
3654 files = set()
3649 files = set()
3655 for c in chunks:
3650 for c in chunks:
3656 if ishunk(c):
3651 if ishunk(c):
3657 abs = c.header.filename()
3652 abs = c.header.filename()
3658 # Create a backup file only if this hunk should be backed up
3653 # Create a backup file only if this hunk should be backed up
3659 if c.header.filename() in tobackup:
3654 if c.header.filename() in tobackup:
3660 target = repo.wjoin(abs)
3655 target = repo.wjoin(abs)
3661 bakname = scmutil.backuppath(repo.ui, repo, abs)
3656 bakname = scmutil.backuppath(repo.ui, repo, abs)
3662 util.copyfile(target, bakname)
3657 util.copyfile(target, bakname)
3663 tobackup.remove(abs)
3658 tobackup.remove(abs)
3664 if abs not in files:
3659 if abs not in files:
3665 files.add(abs)
3660 files.add(abs)
3666 if operation == b'keep':
3661 if operation == b'keep':
3667 checkout(abs)
3662 checkout(abs)
3668 c.write(fp)
3663 c.write(fp)
3669 dopatch = fp.tell()
3664 dopatch = fp.tell()
3670 fp.seek(0)
3665 fp.seek(0)
3671 if dopatch:
3666 if dopatch:
3672 try:
3667 try:
3673 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3668 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3674 except error.PatchError as err:
3669 except error.PatchError as err:
3675 raise error.Abort(pycompat.bytestr(err))
3670 raise error.Abort(pycompat.bytestr(err))
3676 del fp
3671 del fp
3677 else:
3672 else:
3678 for f in actions[b'revert'][0]:
3673 for f in actions[b'revert'][0]:
3679 prntstatusmsg(b'revert', f)
3674 prntstatusmsg(b'revert', f)
3680 checkout(f)
3675 checkout(f)
3681 if normal:
3676 if normal:
3682 normal(f)
3677 normal(f)
3683
3678
3684 for f in actions[b'add'][0]:
3679 for f in actions[b'add'][0]:
3685 # Don't checkout modified files, they are already created by the diff
3680 # Don't checkout modified files, they are already created by the diff
3686 if f not in newlyaddedandmodifiedfiles:
3681 if f not in newlyaddedandmodifiedfiles:
3687 prntstatusmsg(b'add', f)
3682 prntstatusmsg(b'add', f)
3688 checkout(f)
3683 checkout(f)
3689 repo.dirstate.add(f)
3684 repo.dirstate.add(f)
3690
3685
3691 normal = repo.dirstate.normallookup
3686 normal = repo.dirstate.normallookup
3692 if node == parent and p2 == repo.nullid:
3687 if node == parent and p2 == repo.nullid:
3693 normal = repo.dirstate.normal
3688 normal = repo.dirstate.normal
3694 for f in actions[b'undelete'][0]:
3689 for f in actions[b'undelete'][0]:
3695 if interactive:
3690 if interactive:
3696 choice = repo.ui.promptchoice(
3691 choice = repo.ui.promptchoice(
3697 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3692 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3698 )
3693 )
3699 if choice == 0:
3694 if choice == 0:
3700 prntstatusmsg(b'undelete', f)
3695 prntstatusmsg(b'undelete', f)
3701 checkout(f)
3696 checkout(f)
3702 normal(f)
3697 normal(f)
3703 else:
3698 else:
3704 excluded_files.append(f)
3699 excluded_files.append(f)
3705 else:
3700 else:
3706 prntstatusmsg(b'undelete', f)
3701 prntstatusmsg(b'undelete', f)
3707 checkout(f)
3702 checkout(f)
3708 normal(f)
3703 normal(f)
3709
3704
3710 copied = copies.pathcopies(repo[parent], ctx)
3705 copied = copies.pathcopies(repo[parent], ctx)
3711
3706
3712 for f in (
3707 for f in (
3713 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3708 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3714 ):
3709 ):
3715 if f in copied:
3710 if f in copied:
3716 repo.dirstate.copy(copied[f], f)
3711 repo.dirstate.copy(copied[f], f)
3717
3712
3718
3713
3719 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3714 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3720 # commands.outgoing. "missing" is "missing" of the result of
3715 # commands.outgoing. "missing" is "missing" of the result of
3721 # "findcommonoutgoing()"
3716 # "findcommonoutgoing()"
3722 outgoinghooks = util.hooks()
3717 outgoinghooks = util.hooks()
3723
3718
3724 # a list of (ui, repo) functions called by commands.summary
3719 # a list of (ui, repo) functions called by commands.summary
3725 summaryhooks = util.hooks()
3720 summaryhooks = util.hooks()
3726
3721
3727 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3722 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3728 #
3723 #
3729 # functions should return tuple of booleans below, if 'changes' is None:
3724 # functions should return tuple of booleans below, if 'changes' is None:
3730 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3725 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3731 #
3726 #
3732 # otherwise, 'changes' is a tuple of tuples below:
3727 # otherwise, 'changes' is a tuple of tuples below:
3733 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3728 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3734 # - (desturl, destbranch, destpeer, outgoing)
3729 # - (desturl, destbranch, destpeer, outgoing)
3735 summaryremotehooks = util.hooks()
3730 summaryremotehooks = util.hooks()
3736
3731
3737
3732
3738 def checkunfinished(repo, commit=False, skipmerge=False):
3733 def checkunfinished(repo, commit=False, skipmerge=False):
3739 """Look for an unfinished multistep operation, like graft, and abort
3734 """Look for an unfinished multistep operation, like graft, and abort
3740 if found. It's probably good to check this right before
3735 if found. It's probably good to check this right before
3741 bailifchanged().
3736 bailifchanged().
3742 """
3737 """
3743 # Check for non-clearable states first, so things like rebase will take
3738 # Check for non-clearable states first, so things like rebase will take
3744 # precedence over update.
3739 # precedence over update.
3745 for state in statemod._unfinishedstates:
3740 for state in statemod._unfinishedstates:
3746 if (
3741 if (
3747 state._clearable
3742 state._clearable
3748 or (commit and state._allowcommit)
3743 or (commit and state._allowcommit)
3749 or state._reportonly
3744 or state._reportonly
3750 ):
3745 ):
3751 continue
3746 continue
3752 if state.isunfinished(repo):
3747 if state.isunfinished(repo):
3753 raise error.StateError(state.msg(), hint=state.hint())
3748 raise error.StateError(state.msg(), hint=state.hint())
3754
3749
3755 for s in statemod._unfinishedstates:
3750 for s in statemod._unfinishedstates:
3756 if (
3751 if (
3757 not s._clearable
3752 not s._clearable
3758 or (commit and s._allowcommit)
3753 or (commit and s._allowcommit)
3759 or (s._opname == b'merge' and skipmerge)
3754 or (s._opname == b'merge' and skipmerge)
3760 or s._reportonly
3755 or s._reportonly
3761 ):
3756 ):
3762 continue
3757 continue
3763 if s.isunfinished(repo):
3758 if s.isunfinished(repo):
3764 raise error.StateError(s.msg(), hint=s.hint())
3759 raise error.StateError(s.msg(), hint=s.hint())
3765
3760
3766
3761
3767 def clearunfinished(repo):
3762 def clearunfinished(repo):
3768 """Check for unfinished operations (as above), and clear the ones
3763 """Check for unfinished operations (as above), and clear the ones
3769 that are clearable.
3764 that are clearable.
3770 """
3765 """
3771 for state in statemod._unfinishedstates:
3766 for state in statemod._unfinishedstates:
3772 if state._reportonly:
3767 if state._reportonly:
3773 continue
3768 continue
3774 if not state._clearable and state.isunfinished(repo):
3769 if not state._clearable and state.isunfinished(repo):
3775 raise error.StateError(state.msg(), hint=state.hint())
3770 raise error.StateError(state.msg(), hint=state.hint())
3776
3771
3777 for s in statemod._unfinishedstates:
3772 for s in statemod._unfinishedstates:
3778 if s._opname == b'merge' or s._reportonly:
3773 if s._opname == b'merge' or s._reportonly:
3779 continue
3774 continue
3780 if s._clearable and s.isunfinished(repo):
3775 if s._clearable and s.isunfinished(repo):
3781 util.unlink(repo.vfs.join(s._fname))
3776 util.unlink(repo.vfs.join(s._fname))
3782
3777
3783
3778
3784 def getunfinishedstate(repo):
3779 def getunfinishedstate(repo):
3785 """Checks for unfinished operations and returns statecheck object
3780 """Checks for unfinished operations and returns statecheck object
3786 for it"""
3781 for it"""
3787 for state in statemod._unfinishedstates:
3782 for state in statemod._unfinishedstates:
3788 if state.isunfinished(repo):
3783 if state.isunfinished(repo):
3789 return state
3784 return state
3790 return None
3785 return None
3791
3786
3792
3787
3793 def howtocontinue(repo):
3788 def howtocontinue(repo):
3794 """Check for an unfinished operation and return the command to finish
3789 """Check for an unfinished operation and return the command to finish
3795 it.
3790 it.
3796
3791
3797 statemod._unfinishedstates list is checked for an unfinished operation
3792 statemod._unfinishedstates list is checked for an unfinished operation
3798 and the corresponding message to finish it is generated if a method to
3793 and the corresponding message to finish it is generated if a method to
3799 continue is supported by the operation.
3794 continue is supported by the operation.
3800
3795
3801 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3796 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3802 a boolean.
3797 a boolean.
3803 """
3798 """
3804 contmsg = _(b"continue: %s")
3799 contmsg = _(b"continue: %s")
3805 for state in statemod._unfinishedstates:
3800 for state in statemod._unfinishedstates:
3806 if not state._continueflag:
3801 if not state._continueflag:
3807 continue
3802 continue
3808 if state.isunfinished(repo):
3803 if state.isunfinished(repo):
3809 return contmsg % state.continuemsg(), True
3804 return contmsg % state.continuemsg(), True
3810 if repo[None].dirty(missing=True, merge=False, branch=False):
3805 if repo[None].dirty(missing=True, merge=False, branch=False):
3811 return contmsg % _(b"hg commit"), False
3806 return contmsg % _(b"hg commit"), False
3812 return None, None
3807 return None, None
3813
3808
3814
3809
3815 def checkafterresolved(repo):
3810 def checkafterresolved(repo):
3816 """Inform the user about the next action after completing hg resolve
3811 """Inform the user about the next action after completing hg resolve
3817
3812
3818 If there's a an unfinished operation that supports continue flag,
3813 If there's a an unfinished operation that supports continue flag,
3819 howtocontinue will yield repo.ui.warn as the reporter.
3814 howtocontinue will yield repo.ui.warn as the reporter.
3820
3815
3821 Otherwise, it will yield repo.ui.note.
3816 Otherwise, it will yield repo.ui.note.
3822 """
3817 """
3823 msg, warning = howtocontinue(repo)
3818 msg, warning = howtocontinue(repo)
3824 if msg is not None:
3819 if msg is not None:
3825 if warning:
3820 if warning:
3826 repo.ui.warn(b"%s\n" % msg)
3821 repo.ui.warn(b"%s\n" % msg)
3827 else:
3822 else:
3828 repo.ui.note(b"%s\n" % msg)
3823 repo.ui.note(b"%s\n" % msg)
3829
3824
3830
3825
3831 def wrongtooltocontinue(repo, task):
3826 def wrongtooltocontinue(repo, task):
3832 """Raise an abort suggesting how to properly continue if there is an
3827 """Raise an abort suggesting how to properly continue if there is an
3833 active task.
3828 active task.
3834
3829
3835 Uses howtocontinue() to find the active task.
3830 Uses howtocontinue() to find the active task.
3836
3831
3837 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3832 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3838 a hint.
3833 a hint.
3839 """
3834 """
3840 after = howtocontinue(repo)
3835 after = howtocontinue(repo)
3841 hint = None
3836 hint = None
3842 if after[1]:
3837 if after[1]:
3843 hint = after[0]
3838 hint = after[0]
3844 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3839 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3845
3840
3846
3841
3847 def abortgraft(ui, repo, graftstate):
3842 def abortgraft(ui, repo, graftstate):
3848 """abort the interrupted graft and rollbacks to the state before interrupted
3843 """abort the interrupted graft and rollbacks to the state before interrupted
3849 graft"""
3844 graft"""
3850 if not graftstate.exists():
3845 if not graftstate.exists():
3851 raise error.StateError(_(b"no interrupted graft to abort"))
3846 raise error.StateError(_(b"no interrupted graft to abort"))
3852 statedata = readgraftstate(repo, graftstate)
3847 statedata = readgraftstate(repo, graftstate)
3853 newnodes = statedata.get(b'newnodes')
3848 newnodes = statedata.get(b'newnodes')
3854 if newnodes is None:
3849 if newnodes is None:
3855 # and old graft state which does not have all the data required to abort
3850 # and old graft state which does not have all the data required to abort
3856 # the graft
3851 # the graft
3857 raise error.Abort(_(b"cannot abort using an old graftstate"))
3852 raise error.Abort(_(b"cannot abort using an old graftstate"))
3858
3853
3859 # changeset from which graft operation was started
3854 # changeset from which graft operation was started
3860 if len(newnodes) > 0:
3855 if len(newnodes) > 0:
3861 startctx = repo[newnodes[0]].p1()
3856 startctx = repo[newnodes[0]].p1()
3862 else:
3857 else:
3863 startctx = repo[b'.']
3858 startctx = repo[b'.']
3864 # whether to strip or not
3859 # whether to strip or not
3865 cleanup = False
3860 cleanup = False
3866
3861
3867 if newnodes:
3862 if newnodes:
3868 newnodes = [repo[r].rev() for r in newnodes]
3863 newnodes = [repo[r].rev() for r in newnodes]
3869 cleanup = True
3864 cleanup = True
3870 # checking that none of the newnodes turned public or is public
3865 # checking that none of the newnodes turned public or is public
3871 immutable = [c for c in newnodes if not repo[c].mutable()]
3866 immutable = [c for c in newnodes if not repo[c].mutable()]
3872 if immutable:
3867 if immutable:
3873 repo.ui.warn(
3868 repo.ui.warn(
3874 _(b"cannot clean up public changesets %s\n")
3869 _(b"cannot clean up public changesets %s\n")
3875 % b', '.join(bytes(repo[r]) for r in immutable),
3870 % b', '.join(bytes(repo[r]) for r in immutable),
3876 hint=_(b"see 'hg help phases' for details"),
3871 hint=_(b"see 'hg help phases' for details"),
3877 )
3872 )
3878 cleanup = False
3873 cleanup = False
3879
3874
3880 # checking that no new nodes are created on top of grafted revs
3875 # checking that no new nodes are created on top of grafted revs
3881 desc = set(repo.changelog.descendants(newnodes))
3876 desc = set(repo.changelog.descendants(newnodes))
3882 if desc - set(newnodes):
3877 if desc - set(newnodes):
3883 repo.ui.warn(
3878 repo.ui.warn(
3884 _(
3879 _(
3885 b"new changesets detected on destination "
3880 b"new changesets detected on destination "
3886 b"branch, can't strip\n"
3881 b"branch, can't strip\n"
3887 )
3882 )
3888 )
3883 )
3889 cleanup = False
3884 cleanup = False
3890
3885
3891 if cleanup:
3886 if cleanup:
3892 with repo.wlock(), repo.lock():
3887 with repo.wlock(), repo.lock():
3893 mergemod.clean_update(startctx)
3888 mergemod.clean_update(startctx)
3894 # stripping the new nodes created
3889 # stripping the new nodes created
3895 strippoints = [
3890 strippoints = [
3896 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3891 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3897 ]
3892 ]
3898 repair.strip(repo.ui, repo, strippoints, backup=False)
3893 repair.strip(repo.ui, repo, strippoints, backup=False)
3899
3894
3900 if not cleanup:
3895 if not cleanup:
3901 # we don't update to the startnode if we can't strip
3896 # we don't update to the startnode if we can't strip
3902 startctx = repo[b'.']
3897 startctx = repo[b'.']
3903 mergemod.clean_update(startctx)
3898 mergemod.clean_update(startctx)
3904
3899
3905 ui.status(_(b"graft aborted\n"))
3900 ui.status(_(b"graft aborted\n"))
3906 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
3901 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
3907 graftstate.delete()
3902 graftstate.delete()
3908 return 0
3903 return 0
3909
3904
3910
3905
3911 def readgraftstate(repo, graftstate):
3906 def readgraftstate(repo, graftstate):
3912 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3907 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3913 """read the graft state file and return a dict of the data stored in it"""
3908 """read the graft state file and return a dict of the data stored in it"""
3914 try:
3909 try:
3915 return graftstate.read()
3910 return graftstate.read()
3916 except error.CorruptedState:
3911 except error.CorruptedState:
3917 nodes = repo.vfs.read(b'graftstate').splitlines()
3912 nodes = repo.vfs.read(b'graftstate').splitlines()
3918 return {b'nodes': nodes}
3913 return {b'nodes': nodes}
3919
3914
3920
3915
3921 def hgabortgraft(ui, repo):
3916 def hgabortgraft(ui, repo):
3922 """ abort logic for aborting graft using 'hg abort'"""
3917 """ abort logic for aborting graft using 'hg abort'"""
3923 with repo.wlock():
3918 with repo.wlock():
3924 graftstate = statemod.cmdstate(repo, b'graftstate')
3919 graftstate = statemod.cmdstate(repo, b'graftstate')
3925 return abortgraft(ui, repo, graftstate)
3920 return abortgraft(ui, repo, graftstate)
General Comments 0
You need to be logged in to leave comments. Login now