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