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