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