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