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