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