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