##// END OF EJS Templates
copy: to find copy source, walk parent of revision we're marking copies in...
Martin von Zweigbergk -
r45314:02bf61bb default
parent child Browse files
Show More
@@ -1,4194 +1,4197 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 m = scmutil.match(ctx, [pat], opts, globbed=True)
1499 # TODO: Inline and simplify the non-working-copy version of this code
1500 for abs in ctx.walk(m):
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
1502 m = scmutil.match(ctx_to_walk, [pat], opts, globbed=True)
1503 for abs in ctx_to_walk.walk(m):
1501 rel = uipathfn(abs)
1504 rel = uipathfn(abs)
1502 exact = m.exact(abs)
1505 exact = m.exact(abs)
1503 if abs not in ctx:
1506 if abs not in ctx:
1504 if abs in pctx:
1507 if abs in pctx:
1505 if not after:
1508 if not after:
1506 if exact:
1509 if exact:
1507 ui.warn(
1510 ui.warn(
1508 _(
1511 _(
1509 b'%s: not copying - file has been marked '
1512 b'%s: not copying - file has been marked '
1510 b'for remove\n'
1513 b'for remove\n'
1511 )
1514 )
1512 % rel
1515 % rel
1513 )
1516 )
1514 continue
1517 continue
1515 else:
1518 else:
1516 if exact:
1519 if exact:
1517 ui.warn(
1520 ui.warn(
1518 _(b'%s: not copying - file is not managed\n') % rel
1521 _(b'%s: not copying - file is not managed\n') % rel
1519 )
1522 )
1520 continue
1523 continue
1521
1524
1522 # abs: hgsep
1525 # abs: hgsep
1523 # rel: ossep
1526 # rel: ossep
1524 srcs.append((abs, rel, exact))
1527 srcs.append((abs, rel, exact))
1525 return srcs
1528 return srcs
1526
1529
1527 if ctx.rev() is not None:
1530 if ctx.rev() is not None:
1528 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1531 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1529 absdest = pathutil.canonpath(repo.root, cwd, dest)
1532 absdest = pathutil.canonpath(repo.root, cwd, dest)
1530 if ctx.hasdir(absdest):
1533 if ctx.hasdir(absdest):
1531 raise error.Abort(
1534 raise error.Abort(
1532 _(b'%s: --at-rev does not support a directory as destination')
1535 _(b'%s: --at-rev does not support a directory as destination')
1533 % uipathfn(absdest)
1536 % uipathfn(absdest)
1534 )
1537 )
1535 if absdest not in ctx:
1538 if absdest not in ctx:
1536 raise error.Abort(
1539 raise error.Abort(
1537 _(b'%s: copy destination does not exist in %s')
1540 _(b'%s: copy destination does not exist in %s')
1538 % (uipathfn(absdest), ctx)
1541 % (uipathfn(absdest), ctx)
1539 )
1542 )
1540
1543
1541 # avoid cycle context -> subrepo -> cmdutil
1544 # avoid cycle context -> subrepo -> cmdutil
1542 from . import context
1545 from . import context
1543
1546
1544 copylist = []
1547 copylist = []
1545 for pat in pats:
1548 for pat in pats:
1546 srcs = walkpat(pat)
1549 srcs = walkpat(pat)
1547 if not srcs:
1550 if not srcs:
1548 continue
1551 continue
1549 for abs, rel, exact in srcs:
1552 for abs, rel, exact in srcs:
1550 copylist.append(abs)
1553 copylist.append(abs)
1551
1554
1552 # TODO: Add support for `hg cp --at-rev . foo bar dir` and
1555 # TODO: Add support for `hg cp --at-rev . foo bar dir` and
1553 # `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the
1556 # `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the
1554 # existing functions below.
1557 # existing functions below.
1555 if len(copylist) != 1:
1558 if len(copylist) != 1:
1556 raise error.Abort(_(b'--at-rev requires a single source'))
1559 raise error.Abort(_(b'--at-rev requires a single source'))
1557
1560
1558 new_ctx = context.overlayworkingctx(repo)
1561 new_ctx = context.overlayworkingctx(repo)
1559 new_ctx.setbase(ctx.p1())
1562 new_ctx.setbase(ctx.p1())
1560 mergemod.graft(repo, ctx, wctx=new_ctx)
1563 mergemod.graft(repo, ctx, wctx=new_ctx)
1561
1564
1562 new_ctx.markcopied(absdest, copylist[0])
1565 new_ctx.markcopied(absdest, copylist[0])
1563
1566
1564 with repo.lock():
1567 with repo.lock():
1565 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1568 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1566 new_node = mem_ctx.commit()
1569 new_node = mem_ctx.commit()
1567
1570
1568 if repo.dirstate.p1() == ctx.node():
1571 if repo.dirstate.p1() == ctx.node():
1569 with repo.dirstate.parentchange():
1572 with repo.dirstate.parentchange():
1570 scmutil.movedirstate(repo, repo[new_node])
1573 scmutil.movedirstate(repo, repo[new_node])
1571 replacements = {ctx.node(): [new_node]}
1574 replacements = {ctx.node(): [new_node]}
1572 scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True)
1575 scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True)
1573
1576
1574 return
1577 return
1575
1578
1576 # abssrc: hgsep
1579 # abssrc: hgsep
1577 # relsrc: ossep
1580 # relsrc: ossep
1578 # otarget: ossep
1581 # otarget: ossep
1579 def copyfile(abssrc, relsrc, otarget, exact):
1582 def copyfile(abssrc, relsrc, otarget, exact):
1580 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1583 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1581 if b'/' in abstarget:
1584 if b'/' in abstarget:
1582 # We cannot normalize abstarget itself, this would prevent
1585 # We cannot normalize abstarget itself, this would prevent
1583 # case only renames, like a => A.
1586 # case only renames, like a => A.
1584 abspath, absname = abstarget.rsplit(b'/', 1)
1587 abspath, absname = abstarget.rsplit(b'/', 1)
1585 abstarget = repo.dirstate.normalize(abspath) + b'/' + absname
1588 abstarget = repo.dirstate.normalize(abspath) + b'/' + absname
1586 reltarget = repo.pathto(abstarget, cwd)
1589 reltarget = repo.pathto(abstarget, cwd)
1587 target = repo.wjoin(abstarget)
1590 target = repo.wjoin(abstarget)
1588 src = repo.wjoin(abssrc)
1591 src = repo.wjoin(abssrc)
1589 state = repo.dirstate[abstarget]
1592 state = repo.dirstate[abstarget]
1590
1593
1591 scmutil.checkportable(ui, abstarget)
1594 scmutil.checkportable(ui, abstarget)
1592
1595
1593 # check for collisions
1596 # check for collisions
1594 prevsrc = targets.get(abstarget)
1597 prevsrc = targets.get(abstarget)
1595 if prevsrc is not None:
1598 if prevsrc is not None:
1596 ui.warn(
1599 ui.warn(
1597 _(b'%s: not overwriting - %s collides with %s\n')
1600 _(b'%s: not overwriting - %s collides with %s\n')
1598 % (
1601 % (
1599 reltarget,
1602 reltarget,
1600 repo.pathto(abssrc, cwd),
1603 repo.pathto(abssrc, cwd),
1601 repo.pathto(prevsrc, cwd),
1604 repo.pathto(prevsrc, cwd),
1602 )
1605 )
1603 )
1606 )
1604 return True # report a failure
1607 return True # report a failure
1605
1608
1606 # check for overwrites
1609 # check for overwrites
1607 exists = os.path.lexists(target)
1610 exists = os.path.lexists(target)
1608 samefile = False
1611 samefile = False
1609 if exists and abssrc != abstarget:
1612 if exists and abssrc != abstarget:
1610 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1613 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1611 abstarget
1614 abstarget
1612 ):
1615 ):
1613 if not rename:
1616 if not rename:
1614 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1617 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1615 return True # report a failure
1618 return True # report a failure
1616 exists = False
1619 exists = False
1617 samefile = True
1620 samefile = True
1618
1621
1619 if not after and exists or after and state in b'mn':
1622 if not after and exists or after and state in b'mn':
1620 if not opts[b'force']:
1623 if not opts[b'force']:
1621 if state in b'mn':
1624 if state in b'mn':
1622 msg = _(b'%s: not overwriting - file already committed\n')
1625 msg = _(b'%s: not overwriting - file already committed\n')
1623 if after:
1626 if after:
1624 flags = b'--after --force'
1627 flags = b'--after --force'
1625 else:
1628 else:
1626 flags = b'--force'
1629 flags = b'--force'
1627 if rename:
1630 if rename:
1628 hint = (
1631 hint = (
1629 _(
1632 _(
1630 b"('hg rename %s' to replace the file by "
1633 b"('hg rename %s' to replace the file by "
1631 b'recording a rename)\n'
1634 b'recording a rename)\n'
1632 )
1635 )
1633 % flags
1636 % flags
1634 )
1637 )
1635 else:
1638 else:
1636 hint = (
1639 hint = (
1637 _(
1640 _(
1638 b"('hg copy %s' to replace the file by "
1641 b"('hg copy %s' to replace the file by "
1639 b'recording a copy)\n'
1642 b'recording a copy)\n'
1640 )
1643 )
1641 % flags
1644 % flags
1642 )
1645 )
1643 else:
1646 else:
1644 msg = _(b'%s: not overwriting - file exists\n')
1647 msg = _(b'%s: not overwriting - file exists\n')
1645 if rename:
1648 if rename:
1646 hint = _(
1649 hint = _(
1647 b"('hg rename --after' to record the rename)\n"
1650 b"('hg rename --after' to record the rename)\n"
1648 )
1651 )
1649 else:
1652 else:
1650 hint = _(b"('hg copy --after' to record the copy)\n")
1653 hint = _(b"('hg copy --after' to record the copy)\n")
1651 ui.warn(msg % reltarget)
1654 ui.warn(msg % reltarget)
1652 ui.warn(hint)
1655 ui.warn(hint)
1653 return True # report a failure
1656 return True # report a failure
1654
1657
1655 if after:
1658 if after:
1656 if not exists:
1659 if not exists:
1657 if rename:
1660 if rename:
1658 ui.warn(
1661 ui.warn(
1659 _(b'%s: not recording move - %s does not exist\n')
1662 _(b'%s: not recording move - %s does not exist\n')
1660 % (relsrc, reltarget)
1663 % (relsrc, reltarget)
1661 )
1664 )
1662 else:
1665 else:
1663 ui.warn(
1666 ui.warn(
1664 _(b'%s: not recording copy - %s does not exist\n')
1667 _(b'%s: not recording copy - %s does not exist\n')
1665 % (relsrc, reltarget)
1668 % (relsrc, reltarget)
1666 )
1669 )
1667 return True # report a failure
1670 return True # report a failure
1668 elif not dryrun:
1671 elif not dryrun:
1669 try:
1672 try:
1670 if exists:
1673 if exists:
1671 os.unlink(target)
1674 os.unlink(target)
1672 targetdir = os.path.dirname(target) or b'.'
1675 targetdir = os.path.dirname(target) or b'.'
1673 if not os.path.isdir(targetdir):
1676 if not os.path.isdir(targetdir):
1674 os.makedirs(targetdir)
1677 os.makedirs(targetdir)
1675 if samefile:
1678 if samefile:
1676 tmp = target + b"~hgrename"
1679 tmp = target + b"~hgrename"
1677 os.rename(src, tmp)
1680 os.rename(src, tmp)
1678 os.rename(tmp, target)
1681 os.rename(tmp, target)
1679 else:
1682 else:
1680 # Preserve stat info on renames, not on copies; this matches
1683 # Preserve stat info on renames, not on copies; this matches
1681 # Linux CLI behavior.
1684 # Linux CLI behavior.
1682 util.copyfile(src, target, copystat=rename)
1685 util.copyfile(src, target, copystat=rename)
1683 srcexists = True
1686 srcexists = True
1684 except IOError as inst:
1687 except IOError as inst:
1685 if inst.errno == errno.ENOENT:
1688 if inst.errno == errno.ENOENT:
1686 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1689 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1687 srcexists = False
1690 srcexists = False
1688 else:
1691 else:
1689 ui.warn(
1692 ui.warn(
1690 _(b'%s: cannot copy - %s\n')
1693 _(b'%s: cannot copy - %s\n')
1691 % (relsrc, encoding.strtolocal(inst.strerror))
1694 % (relsrc, encoding.strtolocal(inst.strerror))
1692 )
1695 )
1693 return True # report a failure
1696 return True # report a failure
1694
1697
1695 if ui.verbose or not exact:
1698 if ui.verbose or not exact:
1696 if rename:
1699 if rename:
1697 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1700 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1698 else:
1701 else:
1699 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1702 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1700
1703
1701 targets[abstarget] = abssrc
1704 targets[abstarget] = abssrc
1702
1705
1703 # fix up dirstate
1706 # fix up dirstate
1704 scmutil.dirstatecopy(
1707 scmutil.dirstatecopy(
1705 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1708 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1706 )
1709 )
1707 if rename and not dryrun:
1710 if rename and not dryrun:
1708 if not after and srcexists and not samefile:
1711 if not after and srcexists and not samefile:
1709 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1712 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1710 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1713 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1711 ctx.forget([abssrc])
1714 ctx.forget([abssrc])
1712
1715
1713 # pat: ossep
1716 # pat: ossep
1714 # dest ossep
1717 # dest ossep
1715 # srcs: list of (hgsep, hgsep, ossep, bool)
1718 # srcs: list of (hgsep, hgsep, ossep, bool)
1716 # return: function that takes hgsep and returns ossep
1719 # return: function that takes hgsep and returns ossep
1717 def targetpathfn(pat, dest, srcs):
1720 def targetpathfn(pat, dest, srcs):
1718 if os.path.isdir(pat):
1721 if os.path.isdir(pat):
1719 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1722 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1720 abspfx = util.localpath(abspfx)
1723 abspfx = util.localpath(abspfx)
1721 if destdirexists:
1724 if destdirexists:
1722 striplen = len(os.path.split(abspfx)[0])
1725 striplen = len(os.path.split(abspfx)[0])
1723 else:
1726 else:
1724 striplen = len(abspfx)
1727 striplen = len(abspfx)
1725 if striplen:
1728 if striplen:
1726 striplen += len(pycompat.ossep)
1729 striplen += len(pycompat.ossep)
1727 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1730 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1728 elif destdirexists:
1731 elif destdirexists:
1729 res = lambda p: os.path.join(
1732 res = lambda p: os.path.join(
1730 dest, os.path.basename(util.localpath(p))
1733 dest, os.path.basename(util.localpath(p))
1731 )
1734 )
1732 else:
1735 else:
1733 res = lambda p: dest
1736 res = lambda p: dest
1734 return res
1737 return res
1735
1738
1736 # pat: ossep
1739 # pat: ossep
1737 # dest ossep
1740 # dest ossep
1738 # srcs: list of (hgsep, hgsep, ossep, bool)
1741 # srcs: list of (hgsep, hgsep, ossep, bool)
1739 # return: function that takes hgsep and returns ossep
1742 # return: function that takes hgsep and returns ossep
1740 def targetpathafterfn(pat, dest, srcs):
1743 def targetpathafterfn(pat, dest, srcs):
1741 if matchmod.patkind(pat):
1744 if matchmod.patkind(pat):
1742 # a mercurial pattern
1745 # a mercurial pattern
1743 res = lambda p: os.path.join(
1746 res = lambda p: os.path.join(
1744 dest, os.path.basename(util.localpath(p))
1747 dest, os.path.basename(util.localpath(p))
1745 )
1748 )
1746 else:
1749 else:
1747 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1750 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1748 if len(abspfx) < len(srcs[0][0]):
1751 if len(abspfx) < len(srcs[0][0]):
1749 # A directory. Either the target path contains the last
1752 # A directory. Either the target path contains the last
1750 # component of the source path or it does not.
1753 # component of the source path or it does not.
1751 def evalpath(striplen):
1754 def evalpath(striplen):
1752 score = 0
1755 score = 0
1753 for s in srcs:
1756 for s in srcs:
1754 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1757 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1755 if os.path.lexists(t):
1758 if os.path.lexists(t):
1756 score += 1
1759 score += 1
1757 return score
1760 return score
1758
1761
1759 abspfx = util.localpath(abspfx)
1762 abspfx = util.localpath(abspfx)
1760 striplen = len(abspfx)
1763 striplen = len(abspfx)
1761 if striplen:
1764 if striplen:
1762 striplen += len(pycompat.ossep)
1765 striplen += len(pycompat.ossep)
1763 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1766 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1764 score = evalpath(striplen)
1767 score = evalpath(striplen)
1765 striplen1 = len(os.path.split(abspfx)[0])
1768 striplen1 = len(os.path.split(abspfx)[0])
1766 if striplen1:
1769 if striplen1:
1767 striplen1 += len(pycompat.ossep)
1770 striplen1 += len(pycompat.ossep)
1768 if evalpath(striplen1) > score:
1771 if evalpath(striplen1) > score:
1769 striplen = striplen1
1772 striplen = striplen1
1770 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1773 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1771 else:
1774 else:
1772 # a file
1775 # a file
1773 if destdirexists:
1776 if destdirexists:
1774 res = lambda p: os.path.join(
1777 res = lambda p: os.path.join(
1775 dest, os.path.basename(util.localpath(p))
1778 dest, os.path.basename(util.localpath(p))
1776 )
1779 )
1777 else:
1780 else:
1778 res = lambda p: dest
1781 res = lambda p: dest
1779 return res
1782 return res
1780
1783
1781 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1784 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1782 if not destdirexists:
1785 if not destdirexists:
1783 if len(pats) > 1 or matchmod.patkind(pats[0]):
1786 if len(pats) > 1 or matchmod.patkind(pats[0]):
1784 raise error.Abort(
1787 raise error.Abort(
1785 _(
1788 _(
1786 b'with multiple sources, destination must be an '
1789 b'with multiple sources, destination must be an '
1787 b'existing directory'
1790 b'existing directory'
1788 )
1791 )
1789 )
1792 )
1790 if util.endswithsep(dest):
1793 if util.endswithsep(dest):
1791 raise error.Abort(_(b'destination %s is not a directory') % dest)
1794 raise error.Abort(_(b'destination %s is not a directory') % dest)
1792
1795
1793 tfn = targetpathfn
1796 tfn = targetpathfn
1794 if after:
1797 if after:
1795 tfn = targetpathafterfn
1798 tfn = targetpathafterfn
1796 copylist = []
1799 copylist = []
1797 for pat in pats:
1800 for pat in pats:
1798 srcs = walkpat(pat)
1801 srcs = walkpat(pat)
1799 if not srcs:
1802 if not srcs:
1800 continue
1803 continue
1801 copylist.append((tfn(pat, dest, srcs), srcs))
1804 copylist.append((tfn(pat, dest, srcs), srcs))
1802 if not copylist:
1805 if not copylist:
1803 raise error.Abort(_(b'no files to copy'))
1806 raise error.Abort(_(b'no files to copy'))
1804
1807
1805 errors = 0
1808 errors = 0
1806 for targetpath, srcs in copylist:
1809 for targetpath, srcs in copylist:
1807 for abssrc, relsrc, exact in srcs:
1810 for abssrc, relsrc, exact in srcs:
1808 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1811 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1809 errors += 1
1812 errors += 1
1810
1813
1811 return errors != 0
1814 return errors != 0
1812
1815
1813
1816
1814 ## facility to let extension process additional data into an import patch
1817 ## facility to let extension process additional data into an import patch
1815 # list of identifier to be executed in order
1818 # list of identifier to be executed in order
1816 extrapreimport = [] # run before commit
1819 extrapreimport = [] # run before commit
1817 extrapostimport = [] # run after commit
1820 extrapostimport = [] # run after commit
1818 # mapping from identifier to actual import function
1821 # mapping from identifier to actual import function
1819 #
1822 #
1820 # 'preimport' are run before the commit is made and are provided the following
1823 # 'preimport' are run before the commit is made and are provided the following
1821 # arguments:
1824 # arguments:
1822 # - repo: the localrepository instance,
1825 # - repo: the localrepository instance,
1823 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1826 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1824 # - extra: the future extra dictionary of the changeset, please mutate it,
1827 # - extra: the future extra dictionary of the changeset, please mutate it,
1825 # - opts: the import options.
1828 # - opts: the import options.
1826 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1829 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1827 # mutation of in memory commit and more. Feel free to rework the code to get
1830 # mutation of in memory commit and more. Feel free to rework the code to get
1828 # there.
1831 # there.
1829 extrapreimportmap = {}
1832 extrapreimportmap = {}
1830 # 'postimport' are run after the commit is made and are provided the following
1833 # 'postimport' are run after the commit is made and are provided the following
1831 # argument:
1834 # argument:
1832 # - ctx: the changectx created by import.
1835 # - ctx: the changectx created by import.
1833 extrapostimportmap = {}
1836 extrapostimportmap = {}
1834
1837
1835
1838
1836 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1839 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1837 """Utility function used by commands.import to import a single patch
1840 """Utility function used by commands.import to import a single patch
1838
1841
1839 This function is explicitly defined here to help the evolve extension to
1842 This function is explicitly defined here to help the evolve extension to
1840 wrap this part of the import logic.
1843 wrap this part of the import logic.
1841
1844
1842 The API is currently a bit ugly because it a simple code translation from
1845 The API is currently a bit ugly because it a simple code translation from
1843 the import command. Feel free to make it better.
1846 the import command. Feel free to make it better.
1844
1847
1845 :patchdata: a dictionary containing parsed patch data (such as from
1848 :patchdata: a dictionary containing parsed patch data (such as from
1846 ``patch.extract()``)
1849 ``patch.extract()``)
1847 :parents: nodes that will be parent of the created commit
1850 :parents: nodes that will be parent of the created commit
1848 :opts: the full dict of option passed to the import command
1851 :opts: the full dict of option passed to the import command
1849 :msgs: list to save commit message to.
1852 :msgs: list to save commit message to.
1850 (used in case we need to save it when failing)
1853 (used in case we need to save it when failing)
1851 :updatefunc: a function that update a repo to a given node
1854 :updatefunc: a function that update a repo to a given node
1852 updatefunc(<repo>, <node>)
1855 updatefunc(<repo>, <node>)
1853 """
1856 """
1854 # avoid cycle context -> subrepo -> cmdutil
1857 # avoid cycle context -> subrepo -> cmdutil
1855 from . import context
1858 from . import context
1856
1859
1857 tmpname = patchdata.get(b'filename')
1860 tmpname = patchdata.get(b'filename')
1858 message = patchdata.get(b'message')
1861 message = patchdata.get(b'message')
1859 user = opts.get(b'user') or patchdata.get(b'user')
1862 user = opts.get(b'user') or patchdata.get(b'user')
1860 date = opts.get(b'date') or patchdata.get(b'date')
1863 date = opts.get(b'date') or patchdata.get(b'date')
1861 branch = patchdata.get(b'branch')
1864 branch = patchdata.get(b'branch')
1862 nodeid = patchdata.get(b'nodeid')
1865 nodeid = patchdata.get(b'nodeid')
1863 p1 = patchdata.get(b'p1')
1866 p1 = patchdata.get(b'p1')
1864 p2 = patchdata.get(b'p2')
1867 p2 = patchdata.get(b'p2')
1865
1868
1866 nocommit = opts.get(b'no_commit')
1869 nocommit = opts.get(b'no_commit')
1867 importbranch = opts.get(b'import_branch')
1870 importbranch = opts.get(b'import_branch')
1868 update = not opts.get(b'bypass')
1871 update = not opts.get(b'bypass')
1869 strip = opts[b"strip"]
1872 strip = opts[b"strip"]
1870 prefix = opts[b"prefix"]
1873 prefix = opts[b"prefix"]
1871 sim = float(opts.get(b'similarity') or 0)
1874 sim = float(opts.get(b'similarity') or 0)
1872
1875
1873 if not tmpname:
1876 if not tmpname:
1874 return None, None, False
1877 return None, None, False
1875
1878
1876 rejects = False
1879 rejects = False
1877
1880
1878 cmdline_message = logmessage(ui, opts)
1881 cmdline_message = logmessage(ui, opts)
1879 if cmdline_message:
1882 if cmdline_message:
1880 # pickup the cmdline msg
1883 # pickup the cmdline msg
1881 message = cmdline_message
1884 message = cmdline_message
1882 elif message:
1885 elif message:
1883 # pickup the patch msg
1886 # pickup the patch msg
1884 message = message.strip()
1887 message = message.strip()
1885 else:
1888 else:
1886 # launch the editor
1889 # launch the editor
1887 message = None
1890 message = None
1888 ui.debug(b'message:\n%s\n' % (message or b''))
1891 ui.debug(b'message:\n%s\n' % (message or b''))
1889
1892
1890 if len(parents) == 1:
1893 if len(parents) == 1:
1891 parents.append(repo[nullid])
1894 parents.append(repo[nullid])
1892 if opts.get(b'exact'):
1895 if opts.get(b'exact'):
1893 if not nodeid or not p1:
1896 if not nodeid or not p1:
1894 raise error.Abort(_(b'not a Mercurial patch'))
1897 raise error.Abort(_(b'not a Mercurial patch'))
1895 p1 = repo[p1]
1898 p1 = repo[p1]
1896 p2 = repo[p2 or nullid]
1899 p2 = repo[p2 or nullid]
1897 elif p2:
1900 elif p2:
1898 try:
1901 try:
1899 p1 = repo[p1]
1902 p1 = repo[p1]
1900 p2 = repo[p2]
1903 p2 = repo[p2]
1901 # Without any options, consider p2 only if the
1904 # Without any options, consider p2 only if the
1902 # patch is being applied on top of the recorded
1905 # patch is being applied on top of the recorded
1903 # first parent.
1906 # first parent.
1904 if p1 != parents[0]:
1907 if p1 != parents[0]:
1905 p1 = parents[0]
1908 p1 = parents[0]
1906 p2 = repo[nullid]
1909 p2 = repo[nullid]
1907 except error.RepoError:
1910 except error.RepoError:
1908 p1, p2 = parents
1911 p1, p2 = parents
1909 if p2.node() == nullid:
1912 if p2.node() == nullid:
1910 ui.warn(
1913 ui.warn(
1911 _(
1914 _(
1912 b"warning: import the patch as a normal revision\n"
1915 b"warning: import the patch as a normal revision\n"
1913 b"(use --exact to import the patch as a merge)\n"
1916 b"(use --exact to import the patch as a merge)\n"
1914 )
1917 )
1915 )
1918 )
1916 else:
1919 else:
1917 p1, p2 = parents
1920 p1, p2 = parents
1918
1921
1919 n = None
1922 n = None
1920 if update:
1923 if update:
1921 if p1 != parents[0]:
1924 if p1 != parents[0]:
1922 updatefunc(repo, p1.node())
1925 updatefunc(repo, p1.node())
1923 if p2 != parents[1]:
1926 if p2 != parents[1]:
1924 repo.setparents(p1.node(), p2.node())
1927 repo.setparents(p1.node(), p2.node())
1925
1928
1926 if opts.get(b'exact') or importbranch:
1929 if opts.get(b'exact') or importbranch:
1927 repo.dirstate.setbranch(branch or b'default')
1930 repo.dirstate.setbranch(branch or b'default')
1928
1931
1929 partial = opts.get(b'partial', False)
1932 partial = opts.get(b'partial', False)
1930 files = set()
1933 files = set()
1931 try:
1934 try:
1932 patch.patch(
1935 patch.patch(
1933 ui,
1936 ui,
1934 repo,
1937 repo,
1935 tmpname,
1938 tmpname,
1936 strip=strip,
1939 strip=strip,
1937 prefix=prefix,
1940 prefix=prefix,
1938 files=files,
1941 files=files,
1939 eolmode=None,
1942 eolmode=None,
1940 similarity=sim / 100.0,
1943 similarity=sim / 100.0,
1941 )
1944 )
1942 except error.PatchError as e:
1945 except error.PatchError as e:
1943 if not partial:
1946 if not partial:
1944 raise error.Abort(pycompat.bytestr(e))
1947 raise error.Abort(pycompat.bytestr(e))
1945 if partial:
1948 if partial:
1946 rejects = True
1949 rejects = True
1947
1950
1948 files = list(files)
1951 files = list(files)
1949 if nocommit:
1952 if nocommit:
1950 if message:
1953 if message:
1951 msgs.append(message)
1954 msgs.append(message)
1952 else:
1955 else:
1953 if opts.get(b'exact') or p2:
1956 if opts.get(b'exact') or p2:
1954 # If you got here, you either use --force and know what
1957 # If you got here, you either use --force and know what
1955 # you are doing or used --exact or a merge patch while
1958 # you are doing or used --exact or a merge patch while
1956 # being updated to its first parent.
1959 # being updated to its first parent.
1957 m = None
1960 m = None
1958 else:
1961 else:
1959 m = scmutil.matchfiles(repo, files or [])
1962 m = scmutil.matchfiles(repo, files or [])
1960 editform = mergeeditform(repo[None], b'import.normal')
1963 editform = mergeeditform(repo[None], b'import.normal')
1961 if opts.get(b'exact'):
1964 if opts.get(b'exact'):
1962 editor = None
1965 editor = None
1963 else:
1966 else:
1964 editor = getcommiteditor(
1967 editor = getcommiteditor(
1965 editform=editform, **pycompat.strkwargs(opts)
1968 editform=editform, **pycompat.strkwargs(opts)
1966 )
1969 )
1967 extra = {}
1970 extra = {}
1968 for idfunc in extrapreimport:
1971 for idfunc in extrapreimport:
1969 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1972 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1970 overrides = {}
1973 overrides = {}
1971 if partial:
1974 if partial:
1972 overrides[(b'ui', b'allowemptycommit')] = True
1975 overrides[(b'ui', b'allowemptycommit')] = True
1973 if opts.get(b'secret'):
1976 if opts.get(b'secret'):
1974 overrides[(b'phases', b'new-commit')] = b'secret'
1977 overrides[(b'phases', b'new-commit')] = b'secret'
1975 with repo.ui.configoverride(overrides, b'import'):
1978 with repo.ui.configoverride(overrides, b'import'):
1976 n = repo.commit(
1979 n = repo.commit(
1977 message, user, date, match=m, editor=editor, extra=extra
1980 message, user, date, match=m, editor=editor, extra=extra
1978 )
1981 )
1979 for idfunc in extrapostimport:
1982 for idfunc in extrapostimport:
1980 extrapostimportmap[idfunc](repo[n])
1983 extrapostimportmap[idfunc](repo[n])
1981 else:
1984 else:
1982 if opts.get(b'exact') or importbranch:
1985 if opts.get(b'exact') or importbranch:
1983 branch = branch or b'default'
1986 branch = branch or b'default'
1984 else:
1987 else:
1985 branch = p1.branch()
1988 branch = p1.branch()
1986 store = patch.filestore()
1989 store = patch.filestore()
1987 try:
1990 try:
1988 files = set()
1991 files = set()
1989 try:
1992 try:
1990 patch.patchrepo(
1993 patch.patchrepo(
1991 ui,
1994 ui,
1992 repo,
1995 repo,
1993 p1,
1996 p1,
1994 store,
1997 store,
1995 tmpname,
1998 tmpname,
1996 strip,
1999 strip,
1997 prefix,
2000 prefix,
1998 files,
2001 files,
1999 eolmode=None,
2002 eolmode=None,
2000 )
2003 )
2001 except error.PatchError as e:
2004 except error.PatchError as e:
2002 raise error.Abort(stringutil.forcebytestr(e))
2005 raise error.Abort(stringutil.forcebytestr(e))
2003 if opts.get(b'exact'):
2006 if opts.get(b'exact'):
2004 editor = None
2007 editor = None
2005 else:
2008 else:
2006 editor = getcommiteditor(editform=b'import.bypass')
2009 editor = getcommiteditor(editform=b'import.bypass')
2007 memctx = context.memctx(
2010 memctx = context.memctx(
2008 repo,
2011 repo,
2009 (p1.node(), p2.node()),
2012 (p1.node(), p2.node()),
2010 message,
2013 message,
2011 files=files,
2014 files=files,
2012 filectxfn=store,
2015 filectxfn=store,
2013 user=user,
2016 user=user,
2014 date=date,
2017 date=date,
2015 branch=branch,
2018 branch=branch,
2016 editor=editor,
2019 editor=editor,
2017 )
2020 )
2018
2021
2019 overrides = {}
2022 overrides = {}
2020 if opts.get(b'secret'):
2023 if opts.get(b'secret'):
2021 overrides[(b'phases', b'new-commit')] = b'secret'
2024 overrides[(b'phases', b'new-commit')] = b'secret'
2022 with repo.ui.configoverride(overrides, b'import'):
2025 with repo.ui.configoverride(overrides, b'import'):
2023 n = memctx.commit()
2026 n = memctx.commit()
2024 finally:
2027 finally:
2025 store.close()
2028 store.close()
2026 if opts.get(b'exact') and nocommit:
2029 if opts.get(b'exact') and nocommit:
2027 # --exact with --no-commit is still useful in that it does merge
2030 # --exact with --no-commit is still useful in that it does merge
2028 # and branch bits
2031 # and branch bits
2029 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
2032 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
2030 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
2033 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
2031 raise error.Abort(_(b'patch is damaged or loses information'))
2034 raise error.Abort(_(b'patch is damaged or loses information'))
2032 msg = _(b'applied to working directory')
2035 msg = _(b'applied to working directory')
2033 if n:
2036 if n:
2034 # i18n: refers to a short changeset id
2037 # i18n: refers to a short changeset id
2035 msg = _(b'created %s') % short(n)
2038 msg = _(b'created %s') % short(n)
2036 return msg, n, rejects
2039 return msg, n, rejects
2037
2040
2038
2041
2039 # facility to let extensions include additional data in an exported patch
2042 # facility to let extensions include additional data in an exported patch
2040 # list of identifiers to be executed in order
2043 # list of identifiers to be executed in order
2041 extraexport = []
2044 extraexport = []
2042 # mapping from identifier to actual export function
2045 # mapping from identifier to actual export function
2043 # function as to return a string to be added to the header or None
2046 # function as to return a string to be added to the header or None
2044 # it is given two arguments (sequencenumber, changectx)
2047 # it is given two arguments (sequencenumber, changectx)
2045 extraexportmap = {}
2048 extraexportmap = {}
2046
2049
2047
2050
2048 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2051 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2049 node = scmutil.binnode(ctx)
2052 node = scmutil.binnode(ctx)
2050 parents = [p.node() for p in ctx.parents() if p]
2053 parents = [p.node() for p in ctx.parents() if p]
2051 branch = ctx.branch()
2054 branch = ctx.branch()
2052 if switch_parent:
2055 if switch_parent:
2053 parents.reverse()
2056 parents.reverse()
2054
2057
2055 if parents:
2058 if parents:
2056 prev = parents[0]
2059 prev = parents[0]
2057 else:
2060 else:
2058 prev = nullid
2061 prev = nullid
2059
2062
2060 fm.context(ctx=ctx)
2063 fm.context(ctx=ctx)
2061 fm.plain(b'# HG changeset patch\n')
2064 fm.plain(b'# HG changeset patch\n')
2062 fm.write(b'user', b'# User %s\n', ctx.user())
2065 fm.write(b'user', b'# User %s\n', ctx.user())
2063 fm.plain(b'# Date %d %d\n' % ctx.date())
2066 fm.plain(b'# Date %d %d\n' % ctx.date())
2064 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2067 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2065 fm.condwrite(
2068 fm.condwrite(
2066 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2069 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2067 )
2070 )
2068 fm.write(b'node', b'# Node ID %s\n', hex(node))
2071 fm.write(b'node', b'# Node ID %s\n', hex(node))
2069 fm.plain(b'# Parent %s\n' % hex(prev))
2072 fm.plain(b'# Parent %s\n' % hex(prev))
2070 if len(parents) > 1:
2073 if len(parents) > 1:
2071 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2074 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2072 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2075 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2073
2076
2074 # TODO: redesign extraexportmap function to support formatter
2077 # TODO: redesign extraexportmap function to support formatter
2075 for headerid in extraexport:
2078 for headerid in extraexport:
2076 header = extraexportmap[headerid](seqno, ctx)
2079 header = extraexportmap[headerid](seqno, ctx)
2077 if header is not None:
2080 if header is not None:
2078 fm.plain(b'# %s\n' % header)
2081 fm.plain(b'# %s\n' % header)
2079
2082
2080 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2083 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2081 fm.plain(b'\n')
2084 fm.plain(b'\n')
2082
2085
2083 if fm.isplain():
2086 if fm.isplain():
2084 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2087 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2085 for chunk, label in chunkiter:
2088 for chunk, label in chunkiter:
2086 fm.plain(chunk, label=label)
2089 fm.plain(chunk, label=label)
2087 else:
2090 else:
2088 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2091 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2089 # TODO: make it structured?
2092 # TODO: make it structured?
2090 fm.data(diff=b''.join(chunkiter))
2093 fm.data(diff=b''.join(chunkiter))
2091
2094
2092
2095
2093 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2096 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2094 """Export changesets to stdout or a single file"""
2097 """Export changesets to stdout or a single file"""
2095 for seqno, rev in enumerate(revs, 1):
2098 for seqno, rev in enumerate(revs, 1):
2096 ctx = repo[rev]
2099 ctx = repo[rev]
2097 if not dest.startswith(b'<'):
2100 if not dest.startswith(b'<'):
2098 repo.ui.note(b"%s\n" % dest)
2101 repo.ui.note(b"%s\n" % dest)
2099 fm.startitem()
2102 fm.startitem()
2100 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2103 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2101
2104
2102
2105
2103 def _exportfntemplate(
2106 def _exportfntemplate(
2104 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2107 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2105 ):
2108 ):
2106 """Export changesets to possibly multiple files"""
2109 """Export changesets to possibly multiple files"""
2107 total = len(revs)
2110 total = len(revs)
2108 revwidth = max(len(str(rev)) for rev in revs)
2111 revwidth = max(len(str(rev)) for rev in revs)
2109 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2112 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2110
2113
2111 for seqno, rev in enumerate(revs, 1):
2114 for seqno, rev in enumerate(revs, 1):
2112 ctx = repo[rev]
2115 ctx = repo[rev]
2113 dest = makefilename(
2116 dest = makefilename(
2114 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2117 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2115 )
2118 )
2116 filemap.setdefault(dest, []).append((seqno, rev))
2119 filemap.setdefault(dest, []).append((seqno, rev))
2117
2120
2118 for dest in filemap:
2121 for dest in filemap:
2119 with formatter.maybereopen(basefm, dest) as fm:
2122 with formatter.maybereopen(basefm, dest) as fm:
2120 repo.ui.note(b"%s\n" % dest)
2123 repo.ui.note(b"%s\n" % dest)
2121 for seqno, rev in filemap[dest]:
2124 for seqno, rev in filemap[dest]:
2122 fm.startitem()
2125 fm.startitem()
2123 ctx = repo[rev]
2126 ctx = repo[rev]
2124 _exportsingle(
2127 _exportsingle(
2125 repo, ctx, fm, match, switch_parent, seqno, diffopts
2128 repo, ctx, fm, match, switch_parent, seqno, diffopts
2126 )
2129 )
2127
2130
2128
2131
2129 def _prefetchchangedfiles(repo, revs, match):
2132 def _prefetchchangedfiles(repo, revs, match):
2130 allfiles = set()
2133 allfiles = set()
2131 for rev in revs:
2134 for rev in revs:
2132 for file in repo[rev].files():
2135 for file in repo[rev].files():
2133 if not match or match(file):
2136 if not match or match(file):
2134 allfiles.add(file)
2137 allfiles.add(file)
2135 scmutil.prefetchfiles(repo, revs, scmutil.matchfiles(repo, allfiles))
2138 scmutil.prefetchfiles(repo, revs, scmutil.matchfiles(repo, allfiles))
2136
2139
2137
2140
2138 def export(
2141 def export(
2139 repo,
2142 repo,
2140 revs,
2143 revs,
2141 basefm,
2144 basefm,
2142 fntemplate=b'hg-%h.patch',
2145 fntemplate=b'hg-%h.patch',
2143 switch_parent=False,
2146 switch_parent=False,
2144 opts=None,
2147 opts=None,
2145 match=None,
2148 match=None,
2146 ):
2149 ):
2147 '''export changesets as hg patches
2150 '''export changesets as hg patches
2148
2151
2149 Args:
2152 Args:
2150 repo: The repository from which we're exporting revisions.
2153 repo: The repository from which we're exporting revisions.
2151 revs: A list of revisions to export as revision numbers.
2154 revs: A list of revisions to export as revision numbers.
2152 basefm: A formatter to which patches should be written.
2155 basefm: A formatter to which patches should be written.
2153 fntemplate: An optional string to use for generating patch file names.
2156 fntemplate: An optional string to use for generating patch file names.
2154 switch_parent: If True, show diffs against second parent when not nullid.
2157 switch_parent: If True, show diffs against second parent when not nullid.
2155 Default is false, which always shows diff against p1.
2158 Default is false, which always shows diff against p1.
2156 opts: diff options to use for generating the patch.
2159 opts: diff options to use for generating the patch.
2157 match: If specified, only export changes to files matching this matcher.
2160 match: If specified, only export changes to files matching this matcher.
2158
2161
2159 Returns:
2162 Returns:
2160 Nothing.
2163 Nothing.
2161
2164
2162 Side Effect:
2165 Side Effect:
2163 "HG Changeset Patch" data is emitted to one of the following
2166 "HG Changeset Patch" data is emitted to one of the following
2164 destinations:
2167 destinations:
2165 fntemplate specified: Each rev is written to a unique file named using
2168 fntemplate specified: Each rev is written to a unique file named using
2166 the given template.
2169 the given template.
2167 Otherwise: All revs will be written to basefm.
2170 Otherwise: All revs will be written to basefm.
2168 '''
2171 '''
2169 _prefetchchangedfiles(repo, revs, match)
2172 _prefetchchangedfiles(repo, revs, match)
2170
2173
2171 if not fntemplate:
2174 if not fntemplate:
2172 _exportfile(
2175 _exportfile(
2173 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2176 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2174 )
2177 )
2175 else:
2178 else:
2176 _exportfntemplate(
2179 _exportfntemplate(
2177 repo, revs, basefm, fntemplate, switch_parent, opts, match
2180 repo, revs, basefm, fntemplate, switch_parent, opts, match
2178 )
2181 )
2179
2182
2180
2183
2181 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2184 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2182 """Export changesets to the given file stream"""
2185 """Export changesets to the given file stream"""
2183 _prefetchchangedfiles(repo, revs, match)
2186 _prefetchchangedfiles(repo, revs, match)
2184
2187
2185 dest = getattr(fp, 'name', b'<unnamed>')
2188 dest = getattr(fp, 'name', b'<unnamed>')
2186 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2189 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2187 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2190 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2188
2191
2189
2192
2190 def showmarker(fm, marker, index=None):
2193 def showmarker(fm, marker, index=None):
2191 """utility function to display obsolescence marker in a readable way
2194 """utility function to display obsolescence marker in a readable way
2192
2195
2193 To be used by debug function."""
2196 To be used by debug function."""
2194 if index is not None:
2197 if index is not None:
2195 fm.write(b'index', b'%i ', index)
2198 fm.write(b'index', b'%i ', index)
2196 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2199 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2197 succs = marker.succnodes()
2200 succs = marker.succnodes()
2198 fm.condwrite(
2201 fm.condwrite(
2199 succs,
2202 succs,
2200 b'succnodes',
2203 b'succnodes',
2201 b'%s ',
2204 b'%s ',
2202 fm.formatlist(map(hex, succs), name=b'node'),
2205 fm.formatlist(map(hex, succs), name=b'node'),
2203 )
2206 )
2204 fm.write(b'flag', b'%X ', marker.flags())
2207 fm.write(b'flag', b'%X ', marker.flags())
2205 parents = marker.parentnodes()
2208 parents = marker.parentnodes()
2206 if parents is not None:
2209 if parents is not None:
2207 fm.write(
2210 fm.write(
2208 b'parentnodes',
2211 b'parentnodes',
2209 b'{%s} ',
2212 b'{%s} ',
2210 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2213 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2211 )
2214 )
2212 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2215 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2213 meta = marker.metadata().copy()
2216 meta = marker.metadata().copy()
2214 meta.pop(b'date', None)
2217 meta.pop(b'date', None)
2215 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2218 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2216 fm.write(
2219 fm.write(
2217 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2220 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2218 )
2221 )
2219 fm.plain(b'\n')
2222 fm.plain(b'\n')
2220
2223
2221
2224
2222 def finddate(ui, repo, date):
2225 def finddate(ui, repo, date):
2223 """Find the tipmost changeset that matches the given date spec"""
2226 """Find the tipmost changeset that matches the given date spec"""
2224
2227
2225 df = dateutil.matchdate(date)
2228 df = dateutil.matchdate(date)
2226 m = scmutil.matchall(repo)
2229 m = scmutil.matchall(repo)
2227 results = {}
2230 results = {}
2228
2231
2229 def prep(ctx, fns):
2232 def prep(ctx, fns):
2230 d = ctx.date()
2233 d = ctx.date()
2231 if df(d[0]):
2234 if df(d[0]):
2232 results[ctx.rev()] = d
2235 results[ctx.rev()] = d
2233
2236
2234 for ctx in walkchangerevs(repo, m, {b'rev': None}, prep):
2237 for ctx in walkchangerevs(repo, m, {b'rev': None}, prep):
2235 rev = ctx.rev()
2238 rev = ctx.rev()
2236 if rev in results:
2239 if rev in results:
2237 ui.status(
2240 ui.status(
2238 _(b"found revision %d from %s\n")
2241 _(b"found revision %d from %s\n")
2239 % (rev, dateutil.datestr(results[rev]))
2242 % (rev, dateutil.datestr(results[rev]))
2240 )
2243 )
2241 return b'%d' % rev
2244 return b'%d' % rev
2242
2245
2243 raise error.Abort(_(b"revision matching date not found"))
2246 raise error.Abort(_(b"revision matching date not found"))
2244
2247
2245
2248
2246 def increasingwindows(windowsize=8, sizelimit=512):
2249 def increasingwindows(windowsize=8, sizelimit=512):
2247 while True:
2250 while True:
2248 yield windowsize
2251 yield windowsize
2249 if windowsize < sizelimit:
2252 if windowsize < sizelimit:
2250 windowsize *= 2
2253 windowsize *= 2
2251
2254
2252
2255
2253 def _walkrevs(repo, opts):
2256 def _walkrevs(repo, opts):
2254 # Default --rev value depends on --follow but --follow behavior
2257 # Default --rev value depends on --follow but --follow behavior
2255 # depends on revisions resolved from --rev...
2258 # depends on revisions resolved from --rev...
2256 follow = opts.get(b'follow') or opts.get(b'follow_first')
2259 follow = opts.get(b'follow') or opts.get(b'follow_first')
2257 if opts.get(b'rev'):
2260 if opts.get(b'rev'):
2258 revs = scmutil.revrange(repo, opts[b'rev'])
2261 revs = scmutil.revrange(repo, opts[b'rev'])
2259 elif follow and repo.dirstate.p1() == nullid:
2262 elif follow and repo.dirstate.p1() == nullid:
2260 revs = smartset.baseset()
2263 revs = smartset.baseset()
2261 elif follow:
2264 elif follow:
2262 revs = repo.revs(b'reverse(:.)')
2265 revs = repo.revs(b'reverse(:.)')
2263 else:
2266 else:
2264 revs = smartset.spanset(repo)
2267 revs = smartset.spanset(repo)
2265 revs.reverse()
2268 revs.reverse()
2266 return revs
2269 return revs
2267
2270
2268
2271
2269 class FileWalkError(Exception):
2272 class FileWalkError(Exception):
2270 pass
2273 pass
2271
2274
2272
2275
2273 def walkfilerevs(repo, match, follow, revs, fncache):
2276 def walkfilerevs(repo, match, follow, revs, fncache):
2274 '''Walks the file history for the matched files.
2277 '''Walks the file history for the matched files.
2275
2278
2276 Returns the changeset revs that are involved in the file history.
2279 Returns the changeset revs that are involved in the file history.
2277
2280
2278 Throws FileWalkError if the file history can't be walked using
2281 Throws FileWalkError if the file history can't be walked using
2279 filelogs alone.
2282 filelogs alone.
2280 '''
2283 '''
2281 wanted = set()
2284 wanted = set()
2282 copies = []
2285 copies = []
2283 minrev, maxrev = min(revs), max(revs)
2286 minrev, maxrev = min(revs), max(revs)
2284
2287
2285 def filerevs(filelog, last):
2288 def filerevs(filelog, last):
2286 """
2289 """
2287 Only files, no patterns. Check the history of each file.
2290 Only files, no patterns. Check the history of each file.
2288
2291
2289 Examines filelog entries within minrev, maxrev linkrev range
2292 Examines filelog entries within minrev, maxrev linkrev range
2290 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
2293 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
2291 tuples in backwards order
2294 tuples in backwards order
2292 """
2295 """
2293 cl_count = len(repo)
2296 cl_count = len(repo)
2294 revs = []
2297 revs = []
2295 for j in pycompat.xrange(0, last + 1):
2298 for j in pycompat.xrange(0, last + 1):
2296 linkrev = filelog.linkrev(j)
2299 linkrev = filelog.linkrev(j)
2297 if linkrev < minrev:
2300 if linkrev < minrev:
2298 continue
2301 continue
2299 # only yield rev for which we have the changelog, it can
2302 # only yield rev for which we have the changelog, it can
2300 # happen while doing "hg log" during a pull or commit
2303 # happen while doing "hg log" during a pull or commit
2301 if linkrev >= cl_count:
2304 if linkrev >= cl_count:
2302 break
2305 break
2303
2306
2304 parentlinkrevs = []
2307 parentlinkrevs = []
2305 for p in filelog.parentrevs(j):
2308 for p in filelog.parentrevs(j):
2306 if p != nullrev:
2309 if p != nullrev:
2307 parentlinkrevs.append(filelog.linkrev(p))
2310 parentlinkrevs.append(filelog.linkrev(p))
2308 n = filelog.node(j)
2311 n = filelog.node(j)
2309 revs.append(
2312 revs.append(
2310 (linkrev, parentlinkrevs, follow and filelog.renamed(n))
2313 (linkrev, parentlinkrevs, follow and filelog.renamed(n))
2311 )
2314 )
2312
2315
2313 return reversed(revs)
2316 return reversed(revs)
2314
2317
2315 def iterfiles():
2318 def iterfiles():
2316 pctx = repo[b'.']
2319 pctx = repo[b'.']
2317 for filename in match.files():
2320 for filename in match.files():
2318 if follow:
2321 if follow:
2319 if filename not in pctx:
2322 if filename not in pctx:
2320 raise error.Abort(
2323 raise error.Abort(
2321 _(
2324 _(
2322 b'cannot follow file not in parent '
2325 b'cannot follow file not in parent '
2323 b'revision: "%s"'
2326 b'revision: "%s"'
2324 )
2327 )
2325 % filename
2328 % filename
2326 )
2329 )
2327 yield filename, pctx[filename].filenode()
2330 yield filename, pctx[filename].filenode()
2328 else:
2331 else:
2329 yield filename, None
2332 yield filename, None
2330 for filename_node in copies:
2333 for filename_node in copies:
2331 yield filename_node
2334 yield filename_node
2332
2335
2333 for file_, node in iterfiles():
2336 for file_, node in iterfiles():
2334 filelog = repo.file(file_)
2337 filelog = repo.file(file_)
2335 if not len(filelog):
2338 if not len(filelog):
2336 if node is None:
2339 if node is None:
2337 # A zero count may be a directory or deleted file, so
2340 # A zero count may be a directory or deleted file, so
2338 # try to find matching entries on the slow path.
2341 # try to find matching entries on the slow path.
2339 if follow:
2342 if follow:
2340 raise error.Abort(
2343 raise error.Abort(
2341 _(b'cannot follow nonexistent file: "%s"') % file_
2344 _(b'cannot follow nonexistent file: "%s"') % file_
2342 )
2345 )
2343 raise FileWalkError(b"Cannot walk via filelog")
2346 raise FileWalkError(b"Cannot walk via filelog")
2344 else:
2347 else:
2345 continue
2348 continue
2346
2349
2347 if node is None:
2350 if node is None:
2348 last = len(filelog) - 1
2351 last = len(filelog) - 1
2349 else:
2352 else:
2350 last = filelog.rev(node)
2353 last = filelog.rev(node)
2351
2354
2352 # keep track of all ancestors of the file
2355 # keep track of all ancestors of the file
2353 ancestors = {filelog.linkrev(last)}
2356 ancestors = {filelog.linkrev(last)}
2354
2357
2355 # iterate from latest to oldest revision
2358 # iterate from latest to oldest revision
2356 for rev, flparentlinkrevs, copied in filerevs(filelog, last):
2359 for rev, flparentlinkrevs, copied in filerevs(filelog, last):
2357 if not follow:
2360 if not follow:
2358 if rev > maxrev:
2361 if rev > maxrev:
2359 continue
2362 continue
2360 else:
2363 else:
2361 # Note that last might not be the first interesting
2364 # Note that last might not be the first interesting
2362 # rev to us:
2365 # rev to us:
2363 # if the file has been changed after maxrev, we'll
2366 # if the file has been changed after maxrev, we'll
2364 # have linkrev(last) > maxrev, and we still need
2367 # have linkrev(last) > maxrev, and we still need
2365 # to explore the file graph
2368 # to explore the file graph
2366 if rev not in ancestors:
2369 if rev not in ancestors:
2367 continue
2370 continue
2368 # XXX insert 1327 fix here
2371 # XXX insert 1327 fix here
2369 if flparentlinkrevs:
2372 if flparentlinkrevs:
2370 ancestors.update(flparentlinkrevs)
2373 ancestors.update(flparentlinkrevs)
2371
2374
2372 fncache.setdefault(rev, []).append(file_)
2375 fncache.setdefault(rev, []).append(file_)
2373 wanted.add(rev)
2376 wanted.add(rev)
2374 if copied:
2377 if copied:
2375 copies.append(copied)
2378 copies.append(copied)
2376
2379
2377 return wanted
2380 return wanted
2378
2381
2379
2382
2380 class _followfilter(object):
2383 class _followfilter(object):
2381 def __init__(self, repo, onlyfirst=False):
2384 def __init__(self, repo, onlyfirst=False):
2382 self.repo = repo
2385 self.repo = repo
2383 self.startrev = nullrev
2386 self.startrev = nullrev
2384 self.roots = set()
2387 self.roots = set()
2385 self.onlyfirst = onlyfirst
2388 self.onlyfirst = onlyfirst
2386
2389
2387 def match(self, rev):
2390 def match(self, rev):
2388 def realparents(rev):
2391 def realparents(rev):
2389 if self.onlyfirst:
2392 if self.onlyfirst:
2390 return self.repo.changelog.parentrevs(rev)[0:1]
2393 return self.repo.changelog.parentrevs(rev)[0:1]
2391 else:
2394 else:
2392 return filter(
2395 return filter(
2393 lambda x: x != nullrev, self.repo.changelog.parentrevs(rev)
2396 lambda x: x != nullrev, self.repo.changelog.parentrevs(rev)
2394 )
2397 )
2395
2398
2396 if self.startrev == nullrev:
2399 if self.startrev == nullrev:
2397 self.startrev = rev
2400 self.startrev = rev
2398 return True
2401 return True
2399
2402
2400 if rev > self.startrev:
2403 if rev > self.startrev:
2401 # forward: all descendants
2404 # forward: all descendants
2402 if not self.roots:
2405 if not self.roots:
2403 self.roots.add(self.startrev)
2406 self.roots.add(self.startrev)
2404 for parent in realparents(rev):
2407 for parent in realparents(rev):
2405 if parent in self.roots:
2408 if parent in self.roots:
2406 self.roots.add(rev)
2409 self.roots.add(rev)
2407 return True
2410 return True
2408 else:
2411 else:
2409 # backwards: all parents
2412 # backwards: all parents
2410 if not self.roots:
2413 if not self.roots:
2411 self.roots.update(realparents(self.startrev))
2414 self.roots.update(realparents(self.startrev))
2412 if rev in self.roots:
2415 if rev in self.roots:
2413 self.roots.remove(rev)
2416 self.roots.remove(rev)
2414 self.roots.update(realparents(rev))
2417 self.roots.update(realparents(rev))
2415 return True
2418 return True
2416
2419
2417 return False
2420 return False
2418
2421
2419
2422
2420 def walkchangerevs(repo, match, opts, prepare):
2423 def walkchangerevs(repo, match, opts, prepare):
2421 '''Iterate over files and the revs in which they changed.
2424 '''Iterate over files and the revs in which they changed.
2422
2425
2423 Callers most commonly need to iterate backwards over the history
2426 Callers most commonly need to iterate backwards over the history
2424 in which they are interested. Doing so has awful (quadratic-looking)
2427 in which they are interested. Doing so has awful (quadratic-looking)
2425 performance, so we use iterators in a "windowed" way.
2428 performance, so we use iterators in a "windowed" way.
2426
2429
2427 We walk a window of revisions in the desired order. Within the
2430 We walk a window of revisions in the desired order. Within the
2428 window, we first walk forwards to gather data, then in the desired
2431 window, we first walk forwards to gather data, then in the desired
2429 order (usually backwards) to display it.
2432 order (usually backwards) to display it.
2430
2433
2431 This function returns an iterator yielding contexts. Before
2434 This function returns an iterator yielding contexts. Before
2432 yielding each context, the iterator will first call the prepare
2435 yielding each context, the iterator will first call the prepare
2433 function on each context in the window in forward order.'''
2436 function on each context in the window in forward order.'''
2434
2437
2435 allfiles = opts.get(b'all_files')
2438 allfiles = opts.get(b'all_files')
2436 follow = opts.get(b'follow') or opts.get(b'follow_first')
2439 follow = opts.get(b'follow') or opts.get(b'follow_first')
2437 revs = _walkrevs(repo, opts)
2440 revs = _walkrevs(repo, opts)
2438 if not revs:
2441 if not revs:
2439 return []
2442 return []
2440 wanted = set()
2443 wanted = set()
2441 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
2444 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
2442 fncache = {}
2445 fncache = {}
2443 change = repo.__getitem__
2446 change = repo.__getitem__
2444
2447
2445 # First step is to fill wanted, the set of revisions that we want to yield.
2448 # First step is to fill wanted, the set of revisions that we want to yield.
2446 # When it does not induce extra cost, we also fill fncache for revisions in
2449 # When it does not induce extra cost, we also fill fncache for revisions in
2447 # wanted: a cache of filenames that were changed (ctx.files()) and that
2450 # wanted: a cache of filenames that were changed (ctx.files()) and that
2448 # match the file filtering conditions.
2451 # match the file filtering conditions.
2449
2452
2450 if match.always() or allfiles:
2453 if match.always() or allfiles:
2451 # No files, no patterns. Display all revs.
2454 # No files, no patterns. Display all revs.
2452 wanted = revs
2455 wanted = revs
2453 elif not slowpath:
2456 elif not slowpath:
2454 # We only have to read through the filelog to find wanted revisions
2457 # We only have to read through the filelog to find wanted revisions
2455
2458
2456 try:
2459 try:
2457 wanted = walkfilerevs(repo, match, follow, revs, fncache)
2460 wanted = walkfilerevs(repo, match, follow, revs, fncache)
2458 except FileWalkError:
2461 except FileWalkError:
2459 slowpath = True
2462 slowpath = True
2460
2463
2461 # We decided to fall back to the slowpath because at least one
2464 # We decided to fall back to the slowpath because at least one
2462 # of the paths was not a file. Check to see if at least one of them
2465 # of the paths was not a file. Check to see if at least one of them
2463 # existed in history, otherwise simply return
2466 # existed in history, otherwise simply return
2464 for path in match.files():
2467 for path in match.files():
2465 if path == b'.' or path in repo.store:
2468 if path == b'.' or path in repo.store:
2466 break
2469 break
2467 else:
2470 else:
2468 return []
2471 return []
2469
2472
2470 if slowpath:
2473 if slowpath:
2471 # We have to read the changelog to match filenames against
2474 # We have to read the changelog to match filenames against
2472 # changed files
2475 # changed files
2473
2476
2474 if follow:
2477 if follow:
2475 raise error.Abort(
2478 raise error.Abort(
2476 _(b'can only follow copies/renames for explicit filenames')
2479 _(b'can only follow copies/renames for explicit filenames')
2477 )
2480 )
2478
2481
2479 # The slow path checks files modified in every changeset.
2482 # The slow path checks files modified in every changeset.
2480 # This is really slow on large repos, so compute the set lazily.
2483 # This is really slow on large repos, so compute the set lazily.
2481 class lazywantedset(object):
2484 class lazywantedset(object):
2482 def __init__(self):
2485 def __init__(self):
2483 self.set = set()
2486 self.set = set()
2484 self.revs = set(revs)
2487 self.revs = set(revs)
2485
2488
2486 # No need to worry about locality here because it will be accessed
2489 # No need to worry about locality here because it will be accessed
2487 # in the same order as the increasing window below.
2490 # in the same order as the increasing window below.
2488 def __contains__(self, value):
2491 def __contains__(self, value):
2489 if value in self.set:
2492 if value in self.set:
2490 return True
2493 return True
2491 elif not value in self.revs:
2494 elif not value in self.revs:
2492 return False
2495 return False
2493 else:
2496 else:
2494 self.revs.discard(value)
2497 self.revs.discard(value)
2495 ctx = change(value)
2498 ctx = change(value)
2496 if allfiles:
2499 if allfiles:
2497 matches = list(ctx.manifest().walk(match))
2500 matches = list(ctx.manifest().walk(match))
2498 else:
2501 else:
2499 matches = [f for f in ctx.files() if match(f)]
2502 matches = [f for f in ctx.files() if match(f)]
2500 if matches:
2503 if matches:
2501 fncache[value] = matches
2504 fncache[value] = matches
2502 self.set.add(value)
2505 self.set.add(value)
2503 return True
2506 return True
2504 return False
2507 return False
2505
2508
2506 def discard(self, value):
2509 def discard(self, value):
2507 self.revs.discard(value)
2510 self.revs.discard(value)
2508 self.set.discard(value)
2511 self.set.discard(value)
2509
2512
2510 wanted = lazywantedset()
2513 wanted = lazywantedset()
2511
2514
2512 # it might be worthwhile to do this in the iterator if the rev range
2515 # it might be worthwhile to do this in the iterator if the rev range
2513 # is descending and the prune args are all within that range
2516 # is descending and the prune args are all within that range
2514 for rev in opts.get(b'prune', ()):
2517 for rev in opts.get(b'prune', ()):
2515 rev = repo[rev].rev()
2518 rev = repo[rev].rev()
2516 ff = _followfilter(repo)
2519 ff = _followfilter(repo)
2517 stop = min(revs[0], revs[-1])
2520 stop = min(revs[0], revs[-1])
2518 for x in pycompat.xrange(rev, stop - 1, -1):
2521 for x in pycompat.xrange(rev, stop - 1, -1):
2519 if ff.match(x):
2522 if ff.match(x):
2520 wanted = wanted - [x]
2523 wanted = wanted - [x]
2521
2524
2522 # Now that wanted is correctly initialized, we can iterate over the
2525 # Now that wanted is correctly initialized, we can iterate over the
2523 # revision range, yielding only revisions in wanted.
2526 # revision range, yielding only revisions in wanted.
2524 def iterate():
2527 def iterate():
2525 if follow and match.always():
2528 if follow and match.always():
2526 ff = _followfilter(repo, onlyfirst=opts.get(b'follow_first'))
2529 ff = _followfilter(repo, onlyfirst=opts.get(b'follow_first'))
2527
2530
2528 def want(rev):
2531 def want(rev):
2529 return ff.match(rev) and rev in wanted
2532 return ff.match(rev) and rev in wanted
2530
2533
2531 else:
2534 else:
2532
2535
2533 def want(rev):
2536 def want(rev):
2534 return rev in wanted
2537 return rev in wanted
2535
2538
2536 it = iter(revs)
2539 it = iter(revs)
2537 stopiteration = False
2540 stopiteration = False
2538 for windowsize in increasingwindows():
2541 for windowsize in increasingwindows():
2539 nrevs = []
2542 nrevs = []
2540 for i in pycompat.xrange(windowsize):
2543 for i in pycompat.xrange(windowsize):
2541 rev = next(it, None)
2544 rev = next(it, None)
2542 if rev is None:
2545 if rev is None:
2543 stopiteration = True
2546 stopiteration = True
2544 break
2547 break
2545 elif want(rev):
2548 elif want(rev):
2546 nrevs.append(rev)
2549 nrevs.append(rev)
2547 for rev in sorted(nrevs):
2550 for rev in sorted(nrevs):
2548 fns = fncache.get(rev)
2551 fns = fncache.get(rev)
2549 ctx = change(rev)
2552 ctx = change(rev)
2550 if not fns:
2553 if not fns:
2551
2554
2552 def fns_generator():
2555 def fns_generator():
2553 if allfiles:
2556 if allfiles:
2554
2557
2555 def bad(f, msg):
2558 def bad(f, msg):
2556 pass
2559 pass
2557
2560
2558 for f in ctx.matches(matchmod.badmatch(match, bad)):
2561 for f in ctx.matches(matchmod.badmatch(match, bad)):
2559 yield f
2562 yield f
2560 else:
2563 else:
2561 for f in ctx.files():
2564 for f in ctx.files():
2562 if match(f):
2565 if match(f):
2563 yield f
2566 yield f
2564
2567
2565 fns = fns_generator()
2568 fns = fns_generator()
2566 prepare(ctx, fns)
2569 prepare(ctx, fns)
2567 for rev in nrevs:
2570 for rev in nrevs:
2568 yield change(rev)
2571 yield change(rev)
2569
2572
2570 if stopiteration:
2573 if stopiteration:
2571 break
2574 break
2572
2575
2573 return iterate()
2576 return iterate()
2574
2577
2575
2578
2576 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2579 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2577 bad = []
2580 bad = []
2578
2581
2579 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2582 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2580 names = []
2583 names = []
2581 wctx = repo[None]
2584 wctx = repo[None]
2582 cca = None
2585 cca = None
2583 abort, warn = scmutil.checkportabilityalert(ui)
2586 abort, warn = scmutil.checkportabilityalert(ui)
2584 if abort or warn:
2587 if abort or warn:
2585 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2588 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2586
2589
2587 match = repo.narrowmatch(match, includeexact=True)
2590 match = repo.narrowmatch(match, includeexact=True)
2588 badmatch = matchmod.badmatch(match, badfn)
2591 badmatch = matchmod.badmatch(match, badfn)
2589 dirstate = repo.dirstate
2592 dirstate = repo.dirstate
2590 # We don't want to just call wctx.walk here, since it would return a lot of
2593 # We don't want to just call wctx.walk here, since it would return a lot of
2591 # clean files, which we aren't interested in and takes time.
2594 # clean files, which we aren't interested in and takes time.
2592 for f in sorted(
2595 for f in sorted(
2593 dirstate.walk(
2596 dirstate.walk(
2594 badmatch,
2597 badmatch,
2595 subrepos=sorted(wctx.substate),
2598 subrepos=sorted(wctx.substate),
2596 unknown=True,
2599 unknown=True,
2597 ignored=False,
2600 ignored=False,
2598 full=False,
2601 full=False,
2599 )
2602 )
2600 ):
2603 ):
2601 exact = match.exact(f)
2604 exact = match.exact(f)
2602 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2605 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2603 if cca:
2606 if cca:
2604 cca(f)
2607 cca(f)
2605 names.append(f)
2608 names.append(f)
2606 if ui.verbose or not exact:
2609 if ui.verbose or not exact:
2607 ui.status(
2610 ui.status(
2608 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2611 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2609 )
2612 )
2610
2613
2611 for subpath in sorted(wctx.substate):
2614 for subpath in sorted(wctx.substate):
2612 sub = wctx.sub(subpath)
2615 sub = wctx.sub(subpath)
2613 try:
2616 try:
2614 submatch = matchmod.subdirmatcher(subpath, match)
2617 submatch = matchmod.subdirmatcher(subpath, match)
2615 subprefix = repo.wvfs.reljoin(prefix, subpath)
2618 subprefix = repo.wvfs.reljoin(prefix, subpath)
2616 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2619 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2617 if opts.get('subrepos'):
2620 if opts.get('subrepos'):
2618 bad.extend(
2621 bad.extend(
2619 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2622 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2620 )
2623 )
2621 else:
2624 else:
2622 bad.extend(
2625 bad.extend(
2623 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2626 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2624 )
2627 )
2625 except error.LookupError:
2628 except error.LookupError:
2626 ui.status(
2629 ui.status(
2627 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2630 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2628 )
2631 )
2629
2632
2630 if not opts.get('dry_run'):
2633 if not opts.get('dry_run'):
2631 rejected = wctx.add(names, prefix)
2634 rejected = wctx.add(names, prefix)
2632 bad.extend(f for f in rejected if f in match.files())
2635 bad.extend(f for f in rejected if f in match.files())
2633 return bad
2636 return bad
2634
2637
2635
2638
2636 def addwebdirpath(repo, serverpath, webconf):
2639 def addwebdirpath(repo, serverpath, webconf):
2637 webconf[serverpath] = repo.root
2640 webconf[serverpath] = repo.root
2638 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2641 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2639
2642
2640 for r in repo.revs(b'filelog("path:.hgsub")'):
2643 for r in repo.revs(b'filelog("path:.hgsub")'):
2641 ctx = repo[r]
2644 ctx = repo[r]
2642 for subpath in ctx.substate:
2645 for subpath in ctx.substate:
2643 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2646 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2644
2647
2645
2648
2646 def forget(
2649 def forget(
2647 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2650 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2648 ):
2651 ):
2649 if dryrun and interactive:
2652 if dryrun and interactive:
2650 raise error.Abort(_(b"cannot specify both --dry-run and --interactive"))
2653 raise error.Abort(_(b"cannot specify both --dry-run and --interactive"))
2651 bad = []
2654 bad = []
2652 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2655 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2653 wctx = repo[None]
2656 wctx = repo[None]
2654 forgot = []
2657 forgot = []
2655
2658
2656 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2659 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2657 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2660 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2658 if explicitonly:
2661 if explicitonly:
2659 forget = [f for f in forget if match.exact(f)]
2662 forget = [f for f in forget if match.exact(f)]
2660
2663
2661 for subpath in sorted(wctx.substate):
2664 for subpath in sorted(wctx.substate):
2662 sub = wctx.sub(subpath)
2665 sub = wctx.sub(subpath)
2663 submatch = matchmod.subdirmatcher(subpath, match)
2666 submatch = matchmod.subdirmatcher(subpath, match)
2664 subprefix = repo.wvfs.reljoin(prefix, subpath)
2667 subprefix = repo.wvfs.reljoin(prefix, subpath)
2665 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2668 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2666 try:
2669 try:
2667 subbad, subforgot = sub.forget(
2670 subbad, subforgot = sub.forget(
2668 submatch,
2671 submatch,
2669 subprefix,
2672 subprefix,
2670 subuipathfn,
2673 subuipathfn,
2671 dryrun=dryrun,
2674 dryrun=dryrun,
2672 interactive=interactive,
2675 interactive=interactive,
2673 )
2676 )
2674 bad.extend([subpath + b'/' + f for f in subbad])
2677 bad.extend([subpath + b'/' + f for f in subbad])
2675 forgot.extend([subpath + b'/' + f for f in subforgot])
2678 forgot.extend([subpath + b'/' + f for f in subforgot])
2676 except error.LookupError:
2679 except error.LookupError:
2677 ui.status(
2680 ui.status(
2678 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2681 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2679 )
2682 )
2680
2683
2681 if not explicitonly:
2684 if not explicitonly:
2682 for f in match.files():
2685 for f in match.files():
2683 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2686 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2684 if f not in forgot:
2687 if f not in forgot:
2685 if repo.wvfs.exists(f):
2688 if repo.wvfs.exists(f):
2686 # Don't complain if the exact case match wasn't given.
2689 # Don't complain if the exact case match wasn't given.
2687 # But don't do this until after checking 'forgot', so
2690 # But don't do this until after checking 'forgot', so
2688 # that subrepo files aren't normalized, and this op is
2691 # that subrepo files aren't normalized, and this op is
2689 # purely from data cached by the status walk above.
2692 # purely from data cached by the status walk above.
2690 if repo.dirstate.normalize(f) in repo.dirstate:
2693 if repo.dirstate.normalize(f) in repo.dirstate:
2691 continue
2694 continue
2692 ui.warn(
2695 ui.warn(
2693 _(
2696 _(
2694 b'not removing %s: '
2697 b'not removing %s: '
2695 b'file is already untracked\n'
2698 b'file is already untracked\n'
2696 )
2699 )
2697 % uipathfn(f)
2700 % uipathfn(f)
2698 )
2701 )
2699 bad.append(f)
2702 bad.append(f)
2700
2703
2701 if interactive:
2704 if interactive:
2702 responses = _(
2705 responses = _(
2703 b'[Ynsa?]'
2706 b'[Ynsa?]'
2704 b'$$ &Yes, forget this file'
2707 b'$$ &Yes, forget this file'
2705 b'$$ &No, skip this file'
2708 b'$$ &No, skip this file'
2706 b'$$ &Skip remaining files'
2709 b'$$ &Skip remaining files'
2707 b'$$ Include &all remaining files'
2710 b'$$ Include &all remaining files'
2708 b'$$ &? (display help)'
2711 b'$$ &? (display help)'
2709 )
2712 )
2710 for filename in forget[:]:
2713 for filename in forget[:]:
2711 r = ui.promptchoice(
2714 r = ui.promptchoice(
2712 _(b'forget %s %s') % (uipathfn(filename), responses)
2715 _(b'forget %s %s') % (uipathfn(filename), responses)
2713 )
2716 )
2714 if r == 4: # ?
2717 if r == 4: # ?
2715 while r == 4:
2718 while r == 4:
2716 for c, t in ui.extractchoices(responses)[1]:
2719 for c, t in ui.extractchoices(responses)[1]:
2717 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2720 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2718 r = ui.promptchoice(
2721 r = ui.promptchoice(
2719 _(b'forget %s %s') % (uipathfn(filename), responses)
2722 _(b'forget %s %s') % (uipathfn(filename), responses)
2720 )
2723 )
2721 if r == 0: # yes
2724 if r == 0: # yes
2722 continue
2725 continue
2723 elif r == 1: # no
2726 elif r == 1: # no
2724 forget.remove(filename)
2727 forget.remove(filename)
2725 elif r == 2: # Skip
2728 elif r == 2: # Skip
2726 fnindex = forget.index(filename)
2729 fnindex = forget.index(filename)
2727 del forget[fnindex:]
2730 del forget[fnindex:]
2728 break
2731 break
2729 elif r == 3: # All
2732 elif r == 3: # All
2730 break
2733 break
2731
2734
2732 for f in forget:
2735 for f in forget:
2733 if ui.verbose or not match.exact(f) or interactive:
2736 if ui.verbose or not match.exact(f) or interactive:
2734 ui.status(
2737 ui.status(
2735 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2738 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2736 )
2739 )
2737
2740
2738 if not dryrun:
2741 if not dryrun:
2739 rejected = wctx.forget(forget, prefix)
2742 rejected = wctx.forget(forget, prefix)
2740 bad.extend(f for f in rejected if f in match.files())
2743 bad.extend(f for f in rejected if f in match.files())
2741 forgot.extend(f for f in forget if f not in rejected)
2744 forgot.extend(f for f in forget if f not in rejected)
2742 return bad, forgot
2745 return bad, forgot
2743
2746
2744
2747
2745 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2748 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2746 ret = 1
2749 ret = 1
2747
2750
2748 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2751 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2749 for f in ctx.matches(m):
2752 for f in ctx.matches(m):
2750 fm.startitem()
2753 fm.startitem()
2751 fm.context(ctx=ctx)
2754 fm.context(ctx=ctx)
2752 if needsfctx:
2755 if needsfctx:
2753 fc = ctx[f]
2756 fc = ctx[f]
2754 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2757 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2755 fm.data(path=f)
2758 fm.data(path=f)
2756 fm.plain(fmt % uipathfn(f))
2759 fm.plain(fmt % uipathfn(f))
2757 ret = 0
2760 ret = 0
2758
2761
2759 for subpath in sorted(ctx.substate):
2762 for subpath in sorted(ctx.substate):
2760 submatch = matchmod.subdirmatcher(subpath, m)
2763 submatch = matchmod.subdirmatcher(subpath, m)
2761 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2764 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2762 if subrepos or m.exact(subpath) or any(submatch.files()):
2765 if subrepos or m.exact(subpath) or any(submatch.files()):
2763 sub = ctx.sub(subpath)
2766 sub = ctx.sub(subpath)
2764 try:
2767 try:
2765 recurse = m.exact(subpath) or subrepos
2768 recurse = m.exact(subpath) or subrepos
2766 if (
2769 if (
2767 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2770 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2768 == 0
2771 == 0
2769 ):
2772 ):
2770 ret = 0
2773 ret = 0
2771 except error.LookupError:
2774 except error.LookupError:
2772 ui.status(
2775 ui.status(
2773 _(b"skipping missing subrepository: %s\n")
2776 _(b"skipping missing subrepository: %s\n")
2774 % uipathfn(subpath)
2777 % uipathfn(subpath)
2775 )
2778 )
2776
2779
2777 return ret
2780 return ret
2778
2781
2779
2782
2780 def remove(
2783 def remove(
2781 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2784 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2782 ):
2785 ):
2783 ret = 0
2786 ret = 0
2784 s = repo.status(match=m, clean=True)
2787 s = repo.status(match=m, clean=True)
2785 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2788 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2786
2789
2787 wctx = repo[None]
2790 wctx = repo[None]
2788
2791
2789 if warnings is None:
2792 if warnings is None:
2790 warnings = []
2793 warnings = []
2791 warn = True
2794 warn = True
2792 else:
2795 else:
2793 warn = False
2796 warn = False
2794
2797
2795 subs = sorted(wctx.substate)
2798 subs = sorted(wctx.substate)
2796 progress = ui.makeprogress(
2799 progress = ui.makeprogress(
2797 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2800 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2798 )
2801 )
2799 for subpath in subs:
2802 for subpath in subs:
2800 submatch = matchmod.subdirmatcher(subpath, m)
2803 submatch = matchmod.subdirmatcher(subpath, m)
2801 subprefix = repo.wvfs.reljoin(prefix, subpath)
2804 subprefix = repo.wvfs.reljoin(prefix, subpath)
2802 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2805 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2803 if subrepos or m.exact(subpath) or any(submatch.files()):
2806 if subrepos or m.exact(subpath) or any(submatch.files()):
2804 progress.increment()
2807 progress.increment()
2805 sub = wctx.sub(subpath)
2808 sub = wctx.sub(subpath)
2806 try:
2809 try:
2807 if sub.removefiles(
2810 if sub.removefiles(
2808 submatch,
2811 submatch,
2809 subprefix,
2812 subprefix,
2810 subuipathfn,
2813 subuipathfn,
2811 after,
2814 after,
2812 force,
2815 force,
2813 subrepos,
2816 subrepos,
2814 dryrun,
2817 dryrun,
2815 warnings,
2818 warnings,
2816 ):
2819 ):
2817 ret = 1
2820 ret = 1
2818 except error.LookupError:
2821 except error.LookupError:
2819 warnings.append(
2822 warnings.append(
2820 _(b"skipping missing subrepository: %s\n")
2823 _(b"skipping missing subrepository: %s\n")
2821 % uipathfn(subpath)
2824 % uipathfn(subpath)
2822 )
2825 )
2823 progress.complete()
2826 progress.complete()
2824
2827
2825 # warn about failure to delete explicit files/dirs
2828 # warn about failure to delete explicit files/dirs
2826 deleteddirs = pathutil.dirs(deleted)
2829 deleteddirs = pathutil.dirs(deleted)
2827 files = m.files()
2830 files = m.files()
2828 progress = ui.makeprogress(
2831 progress = ui.makeprogress(
2829 _(b'deleting'), total=len(files), unit=_(b'files')
2832 _(b'deleting'), total=len(files), unit=_(b'files')
2830 )
2833 )
2831 for f in files:
2834 for f in files:
2832
2835
2833 def insubrepo():
2836 def insubrepo():
2834 for subpath in wctx.substate:
2837 for subpath in wctx.substate:
2835 if f.startswith(subpath + b'/'):
2838 if f.startswith(subpath + b'/'):
2836 return True
2839 return True
2837 return False
2840 return False
2838
2841
2839 progress.increment()
2842 progress.increment()
2840 isdir = f in deleteddirs or wctx.hasdir(f)
2843 isdir = f in deleteddirs or wctx.hasdir(f)
2841 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2844 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2842 continue
2845 continue
2843
2846
2844 if repo.wvfs.exists(f):
2847 if repo.wvfs.exists(f):
2845 if repo.wvfs.isdir(f):
2848 if repo.wvfs.isdir(f):
2846 warnings.append(
2849 warnings.append(
2847 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2850 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2848 )
2851 )
2849 else:
2852 else:
2850 warnings.append(
2853 warnings.append(
2851 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2854 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2852 )
2855 )
2853 # missing files will generate a warning elsewhere
2856 # missing files will generate a warning elsewhere
2854 ret = 1
2857 ret = 1
2855 progress.complete()
2858 progress.complete()
2856
2859
2857 if force:
2860 if force:
2858 list = modified + deleted + clean + added
2861 list = modified + deleted + clean + added
2859 elif after:
2862 elif after:
2860 list = deleted
2863 list = deleted
2861 remaining = modified + added + clean
2864 remaining = modified + added + clean
2862 progress = ui.makeprogress(
2865 progress = ui.makeprogress(
2863 _(b'skipping'), total=len(remaining), unit=_(b'files')
2866 _(b'skipping'), total=len(remaining), unit=_(b'files')
2864 )
2867 )
2865 for f in remaining:
2868 for f in remaining:
2866 progress.increment()
2869 progress.increment()
2867 if ui.verbose or (f in files):
2870 if ui.verbose or (f in files):
2868 warnings.append(
2871 warnings.append(
2869 _(b'not removing %s: file still exists\n') % uipathfn(f)
2872 _(b'not removing %s: file still exists\n') % uipathfn(f)
2870 )
2873 )
2871 ret = 1
2874 ret = 1
2872 progress.complete()
2875 progress.complete()
2873 else:
2876 else:
2874 list = deleted + clean
2877 list = deleted + clean
2875 progress = ui.makeprogress(
2878 progress = ui.makeprogress(
2876 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2879 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2877 )
2880 )
2878 for f in modified:
2881 for f in modified:
2879 progress.increment()
2882 progress.increment()
2880 warnings.append(
2883 warnings.append(
2881 _(
2884 _(
2882 b'not removing %s: file is modified (use -f'
2885 b'not removing %s: file is modified (use -f'
2883 b' to force removal)\n'
2886 b' to force removal)\n'
2884 )
2887 )
2885 % uipathfn(f)
2888 % uipathfn(f)
2886 )
2889 )
2887 ret = 1
2890 ret = 1
2888 for f in added:
2891 for f in added:
2889 progress.increment()
2892 progress.increment()
2890 warnings.append(
2893 warnings.append(
2891 _(
2894 _(
2892 b"not removing %s: file has been marked for add"
2895 b"not removing %s: file has been marked for add"
2893 b" (use 'hg forget' to undo add)\n"
2896 b" (use 'hg forget' to undo add)\n"
2894 )
2897 )
2895 % uipathfn(f)
2898 % uipathfn(f)
2896 )
2899 )
2897 ret = 1
2900 ret = 1
2898 progress.complete()
2901 progress.complete()
2899
2902
2900 list = sorted(list)
2903 list = sorted(list)
2901 progress = ui.makeprogress(
2904 progress = ui.makeprogress(
2902 _(b'deleting'), total=len(list), unit=_(b'files')
2905 _(b'deleting'), total=len(list), unit=_(b'files')
2903 )
2906 )
2904 for f in list:
2907 for f in list:
2905 if ui.verbose or not m.exact(f):
2908 if ui.verbose or not m.exact(f):
2906 progress.increment()
2909 progress.increment()
2907 ui.status(
2910 ui.status(
2908 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2911 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2909 )
2912 )
2910 progress.complete()
2913 progress.complete()
2911
2914
2912 if not dryrun:
2915 if not dryrun:
2913 with repo.wlock():
2916 with repo.wlock():
2914 if not after:
2917 if not after:
2915 for f in list:
2918 for f in list:
2916 if f in added:
2919 if f in added:
2917 continue # we never unlink added files on remove
2920 continue # we never unlink added files on remove
2918 rmdir = repo.ui.configbool(
2921 rmdir = repo.ui.configbool(
2919 b'experimental', b'removeemptydirs'
2922 b'experimental', b'removeemptydirs'
2920 )
2923 )
2921 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2924 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2922 repo[None].forget(list)
2925 repo[None].forget(list)
2923
2926
2924 if warn:
2927 if warn:
2925 for warning in warnings:
2928 for warning in warnings:
2926 ui.warn(warning)
2929 ui.warn(warning)
2927
2930
2928 return ret
2931 return ret
2929
2932
2930
2933
2931 def _catfmtneedsdata(fm):
2934 def _catfmtneedsdata(fm):
2932 return not fm.datahint() or b'data' in fm.datahint()
2935 return not fm.datahint() or b'data' in fm.datahint()
2933
2936
2934
2937
2935 def _updatecatformatter(fm, ctx, matcher, path, decode):
2938 def _updatecatformatter(fm, ctx, matcher, path, decode):
2936 """Hook for adding data to the formatter used by ``hg cat``.
2939 """Hook for adding data to the formatter used by ``hg cat``.
2937
2940
2938 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2941 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2939 this method first."""
2942 this method first."""
2940
2943
2941 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2944 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2942 # wasn't requested.
2945 # wasn't requested.
2943 data = b''
2946 data = b''
2944 if _catfmtneedsdata(fm):
2947 if _catfmtneedsdata(fm):
2945 data = ctx[path].data()
2948 data = ctx[path].data()
2946 if decode:
2949 if decode:
2947 data = ctx.repo().wwritedata(path, data)
2950 data = ctx.repo().wwritedata(path, data)
2948 fm.startitem()
2951 fm.startitem()
2949 fm.context(ctx=ctx)
2952 fm.context(ctx=ctx)
2950 fm.write(b'data', b'%s', data)
2953 fm.write(b'data', b'%s', data)
2951 fm.data(path=path)
2954 fm.data(path=path)
2952
2955
2953
2956
2954 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2957 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2955 err = 1
2958 err = 1
2956 opts = pycompat.byteskwargs(opts)
2959 opts = pycompat.byteskwargs(opts)
2957
2960
2958 def write(path):
2961 def write(path):
2959 filename = None
2962 filename = None
2960 if fntemplate:
2963 if fntemplate:
2961 filename = makefilename(
2964 filename = makefilename(
2962 ctx, fntemplate, pathname=os.path.join(prefix, path)
2965 ctx, fntemplate, pathname=os.path.join(prefix, path)
2963 )
2966 )
2964 # attempt to create the directory if it does not already exist
2967 # attempt to create the directory if it does not already exist
2965 try:
2968 try:
2966 os.makedirs(os.path.dirname(filename))
2969 os.makedirs(os.path.dirname(filename))
2967 except OSError:
2970 except OSError:
2968 pass
2971 pass
2969 with formatter.maybereopen(basefm, filename) as fm:
2972 with formatter.maybereopen(basefm, filename) as fm:
2970 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2973 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2971
2974
2972 # Automation often uses hg cat on single files, so special case it
2975 # Automation often uses hg cat on single files, so special case it
2973 # for performance to avoid the cost of parsing the manifest.
2976 # for performance to avoid the cost of parsing the manifest.
2974 if len(matcher.files()) == 1 and not matcher.anypats():
2977 if len(matcher.files()) == 1 and not matcher.anypats():
2975 file = matcher.files()[0]
2978 file = matcher.files()[0]
2976 mfl = repo.manifestlog
2979 mfl = repo.manifestlog
2977 mfnode = ctx.manifestnode()
2980 mfnode = ctx.manifestnode()
2978 try:
2981 try:
2979 if mfnode and mfl[mfnode].find(file)[0]:
2982 if mfnode and mfl[mfnode].find(file)[0]:
2980 if _catfmtneedsdata(basefm):
2983 if _catfmtneedsdata(basefm):
2981 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2984 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2982 write(file)
2985 write(file)
2983 return 0
2986 return 0
2984 except KeyError:
2987 except KeyError:
2985 pass
2988 pass
2986
2989
2987 if _catfmtneedsdata(basefm):
2990 if _catfmtneedsdata(basefm):
2988 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2991 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2989
2992
2990 for abs in ctx.walk(matcher):
2993 for abs in ctx.walk(matcher):
2991 write(abs)
2994 write(abs)
2992 err = 0
2995 err = 0
2993
2996
2994 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2997 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2995 for subpath in sorted(ctx.substate):
2998 for subpath in sorted(ctx.substate):
2996 sub = ctx.sub(subpath)
2999 sub = ctx.sub(subpath)
2997 try:
3000 try:
2998 submatch = matchmod.subdirmatcher(subpath, matcher)
3001 submatch = matchmod.subdirmatcher(subpath, matcher)
2999 subprefix = os.path.join(prefix, subpath)
3002 subprefix = os.path.join(prefix, subpath)
3000 if not sub.cat(
3003 if not sub.cat(
3001 submatch,
3004 submatch,
3002 basefm,
3005 basefm,
3003 fntemplate,
3006 fntemplate,
3004 subprefix,
3007 subprefix,
3005 **pycompat.strkwargs(opts)
3008 **pycompat.strkwargs(opts)
3006 ):
3009 ):
3007 err = 0
3010 err = 0
3008 except error.RepoLookupError:
3011 except error.RepoLookupError:
3009 ui.status(
3012 ui.status(
3010 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
3013 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
3011 )
3014 )
3012
3015
3013 return err
3016 return err
3014
3017
3015
3018
3016 def commit(ui, repo, commitfunc, pats, opts):
3019 def commit(ui, repo, commitfunc, pats, opts):
3017 '''commit the specified files or all outstanding changes'''
3020 '''commit the specified files or all outstanding changes'''
3018 date = opts.get(b'date')
3021 date = opts.get(b'date')
3019 if date:
3022 if date:
3020 opts[b'date'] = dateutil.parsedate(date)
3023 opts[b'date'] = dateutil.parsedate(date)
3021 message = logmessage(ui, opts)
3024 message = logmessage(ui, opts)
3022 matcher = scmutil.match(repo[None], pats, opts)
3025 matcher = scmutil.match(repo[None], pats, opts)
3023
3026
3024 dsguard = None
3027 dsguard = None
3025 # extract addremove carefully -- this function can be called from a command
3028 # extract addremove carefully -- this function can be called from a command
3026 # that doesn't support addremove
3029 # that doesn't support addremove
3027 if opts.get(b'addremove'):
3030 if opts.get(b'addremove'):
3028 dsguard = dirstateguard.dirstateguard(repo, b'commit')
3031 dsguard = dirstateguard.dirstateguard(repo, b'commit')
3029 with dsguard or util.nullcontextmanager():
3032 with dsguard or util.nullcontextmanager():
3030 if dsguard:
3033 if dsguard:
3031 relative = scmutil.anypats(pats, opts)
3034 relative = scmutil.anypats(pats, opts)
3032 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
3035 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
3033 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
3036 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
3034 raise error.Abort(
3037 raise error.Abort(
3035 _(b"failed to mark all new/missing files as added/removed")
3038 _(b"failed to mark all new/missing files as added/removed")
3036 )
3039 )
3037
3040
3038 return commitfunc(ui, repo, message, matcher, opts)
3041 return commitfunc(ui, repo, message, matcher, opts)
3039
3042
3040
3043
3041 def samefile(f, ctx1, ctx2):
3044 def samefile(f, ctx1, ctx2):
3042 if f in ctx1.manifest():
3045 if f in ctx1.manifest():
3043 a = ctx1.filectx(f)
3046 a = ctx1.filectx(f)
3044 if f in ctx2.manifest():
3047 if f in ctx2.manifest():
3045 b = ctx2.filectx(f)
3048 b = ctx2.filectx(f)
3046 return not a.cmp(b) and a.flags() == b.flags()
3049 return not a.cmp(b) and a.flags() == b.flags()
3047 else:
3050 else:
3048 return False
3051 return False
3049 else:
3052 else:
3050 return f not in ctx2.manifest()
3053 return f not in ctx2.manifest()
3051
3054
3052
3055
3053 def amend(ui, repo, old, extra, pats, opts):
3056 def amend(ui, repo, old, extra, pats, opts):
3054 # avoid cycle context -> subrepo -> cmdutil
3057 # avoid cycle context -> subrepo -> cmdutil
3055 from . import context
3058 from . import context
3056
3059
3057 # amend will reuse the existing user if not specified, but the obsolete
3060 # amend will reuse the existing user if not specified, but the obsolete
3058 # marker creation requires that the current user's name is specified.
3061 # marker creation requires that the current user's name is specified.
3059 if obsolete.isenabled(repo, obsolete.createmarkersopt):
3062 if obsolete.isenabled(repo, obsolete.createmarkersopt):
3060 ui.username() # raise exception if username not set
3063 ui.username() # raise exception if username not set
3061
3064
3062 ui.note(_(b'amending changeset %s\n') % old)
3065 ui.note(_(b'amending changeset %s\n') % old)
3063 base = old.p1()
3066 base = old.p1()
3064
3067
3065 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
3068 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
3066 # Participating changesets:
3069 # Participating changesets:
3067 #
3070 #
3068 # wctx o - workingctx that contains changes from working copy
3071 # wctx o - workingctx that contains changes from working copy
3069 # | to go into amending commit
3072 # | to go into amending commit
3070 # |
3073 # |
3071 # old o - changeset to amend
3074 # old o - changeset to amend
3072 # |
3075 # |
3073 # base o - first parent of the changeset to amend
3076 # base o - first parent of the changeset to amend
3074 wctx = repo[None]
3077 wctx = repo[None]
3075
3078
3076 # Copy to avoid mutating input
3079 # Copy to avoid mutating input
3077 extra = extra.copy()
3080 extra = extra.copy()
3078 # Update extra dict from amended commit (e.g. to preserve graft
3081 # Update extra dict from amended commit (e.g. to preserve graft
3079 # source)
3082 # source)
3080 extra.update(old.extra())
3083 extra.update(old.extra())
3081
3084
3082 # Also update it from the from the wctx
3085 # Also update it from the from the wctx
3083 extra.update(wctx.extra())
3086 extra.update(wctx.extra())
3084
3087
3085 # date-only change should be ignored?
3088 # date-only change should be ignored?
3086 datemaydiffer = resolvecommitoptions(ui, opts)
3089 datemaydiffer = resolvecommitoptions(ui, opts)
3087
3090
3088 date = old.date()
3091 date = old.date()
3089 if opts.get(b'date'):
3092 if opts.get(b'date'):
3090 date = dateutil.parsedate(opts.get(b'date'))
3093 date = dateutil.parsedate(opts.get(b'date'))
3091 user = opts.get(b'user') or old.user()
3094 user = opts.get(b'user') or old.user()
3092
3095
3093 if len(old.parents()) > 1:
3096 if len(old.parents()) > 1:
3094 # ctx.files() isn't reliable for merges, so fall back to the
3097 # ctx.files() isn't reliable for merges, so fall back to the
3095 # slower repo.status() method
3098 # slower repo.status() method
3096 st = base.status(old)
3099 st = base.status(old)
3097 files = set(st.modified) | set(st.added) | set(st.removed)
3100 files = set(st.modified) | set(st.added) | set(st.removed)
3098 else:
3101 else:
3099 files = set(old.files())
3102 files = set(old.files())
3100
3103
3101 # add/remove the files to the working copy if the "addremove" option
3104 # add/remove the files to the working copy if the "addremove" option
3102 # was specified.
3105 # was specified.
3103 matcher = scmutil.match(wctx, pats, opts)
3106 matcher = scmutil.match(wctx, pats, opts)
3104 relative = scmutil.anypats(pats, opts)
3107 relative = scmutil.anypats(pats, opts)
3105 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
3108 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
3106 if opts.get(b'addremove') and scmutil.addremove(
3109 if opts.get(b'addremove') and scmutil.addremove(
3107 repo, matcher, b"", uipathfn, opts
3110 repo, matcher, b"", uipathfn, opts
3108 ):
3111 ):
3109 raise error.Abort(
3112 raise error.Abort(
3110 _(b"failed to mark all new/missing files as added/removed")
3113 _(b"failed to mark all new/missing files as added/removed")
3111 )
3114 )
3112
3115
3113 # Check subrepos. This depends on in-place wctx._status update in
3116 # Check subrepos. This depends on in-place wctx._status update in
3114 # subrepo.precommit(). To minimize the risk of this hack, we do
3117 # subrepo.precommit(). To minimize the risk of this hack, we do
3115 # nothing if .hgsub does not exist.
3118 # nothing if .hgsub does not exist.
3116 if b'.hgsub' in wctx or b'.hgsub' in old:
3119 if b'.hgsub' in wctx or b'.hgsub' in old:
3117 subs, commitsubs, newsubstate = subrepoutil.precommit(
3120 subs, commitsubs, newsubstate = subrepoutil.precommit(
3118 ui, wctx, wctx._status, matcher
3121 ui, wctx, wctx._status, matcher
3119 )
3122 )
3120 # amend should abort if commitsubrepos is enabled
3123 # amend should abort if commitsubrepos is enabled
3121 assert not commitsubs
3124 assert not commitsubs
3122 if subs:
3125 if subs:
3123 subrepoutil.writestate(repo, newsubstate)
3126 subrepoutil.writestate(repo, newsubstate)
3124
3127
3125 ms = mergemod.mergestate.read(repo)
3128 ms = mergemod.mergestate.read(repo)
3126 mergeutil.checkunresolved(ms)
3129 mergeutil.checkunresolved(ms)
3127
3130
3128 filestoamend = {f for f in wctx.files() if matcher(f)}
3131 filestoamend = {f for f in wctx.files() if matcher(f)}
3129
3132
3130 changes = len(filestoamend) > 0
3133 changes = len(filestoamend) > 0
3131 if changes:
3134 if changes:
3132 # Recompute copies (avoid recording a -> b -> a)
3135 # Recompute copies (avoid recording a -> b -> a)
3133 copied = copies.pathcopies(base, wctx, matcher)
3136 copied = copies.pathcopies(base, wctx, matcher)
3134 if old.p2:
3137 if old.p2:
3135 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
3138 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
3136
3139
3137 # Prune files which were reverted by the updates: if old
3140 # Prune files which were reverted by the updates: if old
3138 # introduced file X and the file was renamed in the working
3141 # introduced file X and the file was renamed in the working
3139 # copy, then those two files are the same and
3142 # copy, then those two files are the same and
3140 # we can discard X from our list of files. Likewise if X
3143 # we can discard X from our list of files. Likewise if X
3141 # was removed, it's no longer relevant. If X is missing (aka
3144 # was removed, it's no longer relevant. If X is missing (aka
3142 # deleted), old X must be preserved.
3145 # deleted), old X must be preserved.
3143 files.update(filestoamend)
3146 files.update(filestoamend)
3144 files = [
3147 files = [
3145 f
3148 f
3146 for f in files
3149 for f in files
3147 if (f not in filestoamend or not samefile(f, wctx, base))
3150 if (f not in filestoamend or not samefile(f, wctx, base))
3148 ]
3151 ]
3149
3152
3150 def filectxfn(repo, ctx_, path):
3153 def filectxfn(repo, ctx_, path):
3151 try:
3154 try:
3152 # If the file being considered is not amongst the files
3155 # If the file being considered is not amongst the files
3153 # to be amended, we should return the file context from the
3156 # to be amended, we should return the file context from the
3154 # old changeset. This avoids issues when only some files in
3157 # old changeset. This avoids issues when only some files in
3155 # the working copy are being amended but there are also
3158 # the working copy are being amended but there are also
3156 # changes to other files from the old changeset.
3159 # changes to other files from the old changeset.
3157 if path not in filestoamend:
3160 if path not in filestoamend:
3158 return old.filectx(path)
3161 return old.filectx(path)
3159
3162
3160 # Return None for removed files.
3163 # Return None for removed files.
3161 if path in wctx.removed():
3164 if path in wctx.removed():
3162 return None
3165 return None
3163
3166
3164 fctx = wctx[path]
3167 fctx = wctx[path]
3165 flags = fctx.flags()
3168 flags = fctx.flags()
3166 mctx = context.memfilectx(
3169 mctx = context.memfilectx(
3167 repo,
3170 repo,
3168 ctx_,
3171 ctx_,
3169 fctx.path(),
3172 fctx.path(),
3170 fctx.data(),
3173 fctx.data(),
3171 islink=b'l' in flags,
3174 islink=b'l' in flags,
3172 isexec=b'x' in flags,
3175 isexec=b'x' in flags,
3173 copysource=copied.get(path),
3176 copysource=copied.get(path),
3174 )
3177 )
3175 return mctx
3178 return mctx
3176 except KeyError:
3179 except KeyError:
3177 return None
3180 return None
3178
3181
3179 else:
3182 else:
3180 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
3183 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
3181
3184
3182 # Use version of files as in the old cset
3185 # Use version of files as in the old cset
3183 def filectxfn(repo, ctx_, path):
3186 def filectxfn(repo, ctx_, path):
3184 try:
3187 try:
3185 return old.filectx(path)
3188 return old.filectx(path)
3186 except KeyError:
3189 except KeyError:
3187 return None
3190 return None
3188
3191
3189 # See if we got a message from -m or -l, if not, open the editor with
3192 # See if we got a message from -m or -l, if not, open the editor with
3190 # the message of the changeset to amend.
3193 # the message of the changeset to amend.
3191 message = logmessage(ui, opts)
3194 message = logmessage(ui, opts)
3192
3195
3193 editform = mergeeditform(old, b'commit.amend')
3196 editform = mergeeditform(old, b'commit.amend')
3194
3197
3195 if not message:
3198 if not message:
3196 message = old.description()
3199 message = old.description()
3197 # Default if message isn't provided and --edit is not passed is to
3200 # Default if message isn't provided and --edit is not passed is to
3198 # invoke editor, but allow --no-edit. If somehow we don't have any
3201 # invoke editor, but allow --no-edit. If somehow we don't have any
3199 # description, let's always start the editor.
3202 # description, let's always start the editor.
3200 doedit = not message or opts.get(b'edit') in [True, None]
3203 doedit = not message or opts.get(b'edit') in [True, None]
3201 else:
3204 else:
3202 # Default if message is provided is to not invoke editor, but allow
3205 # Default if message is provided is to not invoke editor, but allow
3203 # --edit.
3206 # --edit.
3204 doedit = opts.get(b'edit') is True
3207 doedit = opts.get(b'edit') is True
3205 editor = getcommiteditor(edit=doedit, editform=editform)
3208 editor = getcommiteditor(edit=doedit, editform=editform)
3206
3209
3207 pureextra = extra.copy()
3210 pureextra = extra.copy()
3208 extra[b'amend_source'] = old.hex()
3211 extra[b'amend_source'] = old.hex()
3209
3212
3210 new = context.memctx(
3213 new = context.memctx(
3211 repo,
3214 repo,
3212 parents=[base.node(), old.p2().node()],
3215 parents=[base.node(), old.p2().node()],
3213 text=message,
3216 text=message,
3214 files=files,
3217 files=files,
3215 filectxfn=filectxfn,
3218 filectxfn=filectxfn,
3216 user=user,
3219 user=user,
3217 date=date,
3220 date=date,
3218 extra=extra,
3221 extra=extra,
3219 editor=editor,
3222 editor=editor,
3220 )
3223 )
3221
3224
3222 newdesc = changelog.stripdesc(new.description())
3225 newdesc = changelog.stripdesc(new.description())
3223 if (
3226 if (
3224 (not changes)
3227 (not changes)
3225 and newdesc == old.description()
3228 and newdesc == old.description()
3226 and user == old.user()
3229 and user == old.user()
3227 and (date == old.date() or datemaydiffer)
3230 and (date == old.date() or datemaydiffer)
3228 and pureextra == old.extra()
3231 and pureextra == old.extra()
3229 ):
3232 ):
3230 # nothing changed. continuing here would create a new node
3233 # nothing changed. continuing here would create a new node
3231 # anyway because of the amend_source noise.
3234 # anyway because of the amend_source noise.
3232 #
3235 #
3233 # This not what we expect from amend.
3236 # This not what we expect from amend.
3234 return old.node()
3237 return old.node()
3235
3238
3236 commitphase = None
3239 commitphase = None
3237 if opts.get(b'secret'):
3240 if opts.get(b'secret'):
3238 commitphase = phases.secret
3241 commitphase = phases.secret
3239 newid = repo.commitctx(new)
3242 newid = repo.commitctx(new)
3240
3243
3241 # Reroute the working copy parent to the new changeset
3244 # Reroute the working copy parent to the new changeset
3242 repo.setparents(newid, nullid)
3245 repo.setparents(newid, nullid)
3243 mapping = {old.node(): (newid,)}
3246 mapping = {old.node(): (newid,)}
3244 obsmetadata = None
3247 obsmetadata = None
3245 if opts.get(b'note'):
3248 if opts.get(b'note'):
3246 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3249 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3247 backup = ui.configbool(b'rewrite', b'backup-bundle')
3250 backup = ui.configbool(b'rewrite', b'backup-bundle')
3248 scmutil.cleanupnodes(
3251 scmutil.cleanupnodes(
3249 repo,
3252 repo,
3250 mapping,
3253 mapping,
3251 b'amend',
3254 b'amend',
3252 metadata=obsmetadata,
3255 metadata=obsmetadata,
3253 fixphase=True,
3256 fixphase=True,
3254 targetphase=commitphase,
3257 targetphase=commitphase,
3255 backup=backup,
3258 backup=backup,
3256 )
3259 )
3257
3260
3258 # Fixing the dirstate because localrepo.commitctx does not update
3261 # Fixing the dirstate because localrepo.commitctx does not update
3259 # it. This is rather convenient because we did not need to update
3262 # it. This is rather convenient because we did not need to update
3260 # the dirstate for all the files in the new commit which commitctx
3263 # the dirstate for all the files in the new commit which commitctx
3261 # could have done if it updated the dirstate. Now, we can
3264 # could have done if it updated the dirstate. Now, we can
3262 # selectively update the dirstate only for the amended files.
3265 # selectively update the dirstate only for the amended files.
3263 dirstate = repo.dirstate
3266 dirstate = repo.dirstate
3264
3267
3265 # Update the state of the files which were added and modified in the
3268 # Update the state of the files which were added and modified in the
3266 # amend to "normal" in the dirstate. We need to use "normallookup" since
3269 # amend to "normal" in the dirstate. We need to use "normallookup" since
3267 # the files may have changed since the command started; using "normal"
3270 # the files may have changed since the command started; using "normal"
3268 # would mark them as clean but with uncommitted contents.
3271 # would mark them as clean but with uncommitted contents.
3269 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3272 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
3270 for f in normalfiles:
3273 for f in normalfiles:
3271 dirstate.normallookup(f)
3274 dirstate.normallookup(f)
3272
3275
3273 # Update the state of files which were removed in the amend
3276 # Update the state of files which were removed in the amend
3274 # to "removed" in the dirstate.
3277 # to "removed" in the dirstate.
3275 removedfiles = set(wctx.removed()) & filestoamend
3278 removedfiles = set(wctx.removed()) & filestoamend
3276 for f in removedfiles:
3279 for f in removedfiles:
3277 dirstate.drop(f)
3280 dirstate.drop(f)
3278
3281
3279 return newid
3282 return newid
3280
3283
3281
3284
3282 def commiteditor(repo, ctx, subs, editform=b''):
3285 def commiteditor(repo, ctx, subs, editform=b''):
3283 if ctx.description():
3286 if ctx.description():
3284 return ctx.description()
3287 return ctx.description()
3285 return commitforceeditor(
3288 return commitforceeditor(
3286 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3289 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3287 )
3290 )
3288
3291
3289
3292
3290 def commitforceeditor(
3293 def commitforceeditor(
3291 repo,
3294 repo,
3292 ctx,
3295 ctx,
3293 subs,
3296 subs,
3294 finishdesc=None,
3297 finishdesc=None,
3295 extramsg=None,
3298 extramsg=None,
3296 editform=b'',
3299 editform=b'',
3297 unchangedmessagedetection=False,
3300 unchangedmessagedetection=False,
3298 ):
3301 ):
3299 if not extramsg:
3302 if not extramsg:
3300 extramsg = _(b"Leave message empty to abort commit.")
3303 extramsg = _(b"Leave message empty to abort commit.")
3301
3304
3302 forms = [e for e in editform.split(b'.') if e]
3305 forms = [e for e in editform.split(b'.') if e]
3303 forms.insert(0, b'changeset')
3306 forms.insert(0, b'changeset')
3304 templatetext = None
3307 templatetext = None
3305 while forms:
3308 while forms:
3306 ref = b'.'.join(forms)
3309 ref = b'.'.join(forms)
3307 if repo.ui.config(b'committemplate', ref):
3310 if repo.ui.config(b'committemplate', ref):
3308 templatetext = committext = buildcommittemplate(
3311 templatetext = committext = buildcommittemplate(
3309 repo, ctx, subs, extramsg, ref
3312 repo, ctx, subs, extramsg, ref
3310 )
3313 )
3311 break
3314 break
3312 forms.pop()
3315 forms.pop()
3313 else:
3316 else:
3314 committext = buildcommittext(repo, ctx, subs, extramsg)
3317 committext = buildcommittext(repo, ctx, subs, extramsg)
3315
3318
3316 # run editor in the repository root
3319 # run editor in the repository root
3317 olddir = encoding.getcwd()
3320 olddir = encoding.getcwd()
3318 os.chdir(repo.root)
3321 os.chdir(repo.root)
3319
3322
3320 # make in-memory changes visible to external process
3323 # make in-memory changes visible to external process
3321 tr = repo.currenttransaction()
3324 tr = repo.currenttransaction()
3322 repo.dirstate.write(tr)
3325 repo.dirstate.write(tr)
3323 pending = tr and tr.writepending() and repo.root
3326 pending = tr and tr.writepending() and repo.root
3324
3327
3325 editortext = repo.ui.edit(
3328 editortext = repo.ui.edit(
3326 committext,
3329 committext,
3327 ctx.user(),
3330 ctx.user(),
3328 ctx.extra(),
3331 ctx.extra(),
3329 editform=editform,
3332 editform=editform,
3330 pending=pending,
3333 pending=pending,
3331 repopath=repo.path,
3334 repopath=repo.path,
3332 action=b'commit',
3335 action=b'commit',
3333 )
3336 )
3334 text = editortext
3337 text = editortext
3335
3338
3336 # strip away anything below this special string (used for editors that want
3339 # strip away anything below this special string (used for editors that want
3337 # to display the diff)
3340 # to display the diff)
3338 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3341 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3339 if stripbelow:
3342 if stripbelow:
3340 text = text[: stripbelow.start()]
3343 text = text[: stripbelow.start()]
3341
3344
3342 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3345 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3343 os.chdir(olddir)
3346 os.chdir(olddir)
3344
3347
3345 if finishdesc:
3348 if finishdesc:
3346 text = finishdesc(text)
3349 text = finishdesc(text)
3347 if not text.strip():
3350 if not text.strip():
3348 raise error.Abort(_(b"empty commit message"))
3351 raise error.Abort(_(b"empty commit message"))
3349 if unchangedmessagedetection and editortext == templatetext:
3352 if unchangedmessagedetection and editortext == templatetext:
3350 raise error.Abort(_(b"commit message unchanged"))
3353 raise error.Abort(_(b"commit message unchanged"))
3351
3354
3352 return text
3355 return text
3353
3356
3354
3357
3355 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3358 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3356 ui = repo.ui
3359 ui = repo.ui
3357 spec = formatter.templatespec(ref, None, None)
3360 spec = formatter.templatespec(ref, None, None)
3358 t = logcmdutil.changesettemplater(ui, repo, spec)
3361 t = logcmdutil.changesettemplater(ui, repo, spec)
3359 t.t.cache.update(
3362 t.t.cache.update(
3360 (k, templater.unquotestring(v))
3363 (k, templater.unquotestring(v))
3361 for k, v in repo.ui.configitems(b'committemplate')
3364 for k, v in repo.ui.configitems(b'committemplate')
3362 )
3365 )
3363
3366
3364 if not extramsg:
3367 if not extramsg:
3365 extramsg = b'' # ensure that extramsg is string
3368 extramsg = b'' # ensure that extramsg is string
3366
3369
3367 ui.pushbuffer()
3370 ui.pushbuffer()
3368 t.show(ctx, extramsg=extramsg)
3371 t.show(ctx, extramsg=extramsg)
3369 return ui.popbuffer()
3372 return ui.popbuffer()
3370
3373
3371
3374
3372 def hgprefix(msg):
3375 def hgprefix(msg):
3373 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3376 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3374
3377
3375
3378
3376 def buildcommittext(repo, ctx, subs, extramsg):
3379 def buildcommittext(repo, ctx, subs, extramsg):
3377 edittext = []
3380 edittext = []
3378 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3381 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3379 if ctx.description():
3382 if ctx.description():
3380 edittext.append(ctx.description())
3383 edittext.append(ctx.description())
3381 edittext.append(b"")
3384 edittext.append(b"")
3382 edittext.append(b"") # Empty line between message and comments.
3385 edittext.append(b"") # Empty line between message and comments.
3383 edittext.append(
3386 edittext.append(
3384 hgprefix(
3387 hgprefix(
3385 _(
3388 _(
3386 b"Enter commit message."
3389 b"Enter commit message."
3387 b" Lines beginning with 'HG:' are removed."
3390 b" Lines beginning with 'HG:' are removed."
3388 )
3391 )
3389 )
3392 )
3390 )
3393 )
3391 edittext.append(hgprefix(extramsg))
3394 edittext.append(hgprefix(extramsg))
3392 edittext.append(b"HG: --")
3395 edittext.append(b"HG: --")
3393 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3396 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3394 if ctx.p2():
3397 if ctx.p2():
3395 edittext.append(hgprefix(_(b"branch merge")))
3398 edittext.append(hgprefix(_(b"branch merge")))
3396 if ctx.branch():
3399 if ctx.branch():
3397 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3400 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3398 if bookmarks.isactivewdirparent(repo):
3401 if bookmarks.isactivewdirparent(repo):
3399 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3402 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3400 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3403 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3401 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3404 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3402 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3405 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3403 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3406 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3404 if not added and not modified and not removed:
3407 if not added and not modified and not removed:
3405 edittext.append(hgprefix(_(b"no files changed")))
3408 edittext.append(hgprefix(_(b"no files changed")))
3406 edittext.append(b"")
3409 edittext.append(b"")
3407
3410
3408 return b"\n".join(edittext)
3411 return b"\n".join(edittext)
3409
3412
3410
3413
3411 def commitstatus(repo, node, branch, bheads=None, opts=None):
3414 def commitstatus(repo, node, branch, bheads=None, opts=None):
3412 if opts is None:
3415 if opts is None:
3413 opts = {}
3416 opts = {}
3414 ctx = repo[node]
3417 ctx = repo[node]
3415 parents = ctx.parents()
3418 parents = ctx.parents()
3416
3419
3417 if (
3420 if (
3418 not opts.get(b'amend')
3421 not opts.get(b'amend')
3419 and bheads
3422 and bheads
3420 and node not in bheads
3423 and node not in bheads
3421 and not [
3424 and not [
3422 x for x in parents if x.node() in bheads and x.branch() == branch
3425 x for x in parents if x.node() in bheads and x.branch() == branch
3423 ]
3426 ]
3424 ):
3427 ):
3425 repo.ui.status(_(b'created new head\n'))
3428 repo.ui.status(_(b'created new head\n'))
3426 # The message is not printed for initial roots. For the other
3429 # The message is not printed for initial roots. For the other
3427 # changesets, it is printed in the following situations:
3430 # changesets, it is printed in the following situations:
3428 #
3431 #
3429 # Par column: for the 2 parents with ...
3432 # Par column: for the 2 parents with ...
3430 # N: null or no parent
3433 # N: null or no parent
3431 # B: parent is on another named branch
3434 # B: parent is on another named branch
3432 # C: parent is a regular non head changeset
3435 # C: parent is a regular non head changeset
3433 # H: parent was a branch head of the current branch
3436 # H: parent was a branch head of the current branch
3434 # Msg column: whether we print "created new head" message
3437 # Msg column: whether we print "created new head" message
3435 # In the following, it is assumed that there already exists some
3438 # In the following, it is assumed that there already exists some
3436 # initial branch heads of the current branch, otherwise nothing is
3439 # initial branch heads of the current branch, otherwise nothing is
3437 # printed anyway.
3440 # printed anyway.
3438 #
3441 #
3439 # Par Msg Comment
3442 # Par Msg Comment
3440 # N N y additional topo root
3443 # N N y additional topo root
3441 #
3444 #
3442 # B N y additional branch root
3445 # B N y additional branch root
3443 # C N y additional topo head
3446 # C N y additional topo head
3444 # H N n usual case
3447 # H N n usual case
3445 #
3448 #
3446 # B B y weird additional branch root
3449 # B B y weird additional branch root
3447 # C B y branch merge
3450 # C B y branch merge
3448 # H B n merge with named branch
3451 # H B n merge with named branch
3449 #
3452 #
3450 # C C y additional head from merge
3453 # C C y additional head from merge
3451 # C H n merge with a head
3454 # C H n merge with a head
3452 #
3455 #
3453 # H H n head merge: head count decreases
3456 # H H n head merge: head count decreases
3454
3457
3455 if not opts.get(b'close_branch'):
3458 if not opts.get(b'close_branch'):
3456 for r in parents:
3459 for r in parents:
3457 if r.closesbranch() and r.branch() == branch:
3460 if r.closesbranch() and r.branch() == branch:
3458 repo.ui.status(
3461 repo.ui.status(
3459 _(b'reopening closed branch head %d\n') % r.rev()
3462 _(b'reopening closed branch head %d\n') % r.rev()
3460 )
3463 )
3461
3464
3462 if repo.ui.debugflag:
3465 if repo.ui.debugflag:
3463 repo.ui.write(
3466 repo.ui.write(
3464 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3467 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3465 )
3468 )
3466 elif repo.ui.verbose:
3469 elif repo.ui.verbose:
3467 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3470 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3468
3471
3469
3472
3470 def postcommitstatus(repo, pats, opts):
3473 def postcommitstatus(repo, pats, opts):
3471 return repo.status(match=scmutil.match(repo[None], pats, opts))
3474 return repo.status(match=scmutil.match(repo[None], pats, opts))
3472
3475
3473
3476
3474 def revert(ui, repo, ctx, parents, *pats, **opts):
3477 def revert(ui, repo, ctx, parents, *pats, **opts):
3475 opts = pycompat.byteskwargs(opts)
3478 opts = pycompat.byteskwargs(opts)
3476 parent, p2 = parents
3479 parent, p2 = parents
3477 node = ctx.node()
3480 node = ctx.node()
3478
3481
3479 mf = ctx.manifest()
3482 mf = ctx.manifest()
3480 if node == p2:
3483 if node == p2:
3481 parent = p2
3484 parent = p2
3482
3485
3483 # need all matching names in dirstate and manifest of target rev,
3486 # need all matching names in dirstate and manifest of target rev,
3484 # so have to walk both. do not print errors if files exist in one
3487 # so have to walk both. do not print errors if files exist in one
3485 # but not other. in both cases, filesets should be evaluated against
3488 # but not other. in both cases, filesets should be evaluated against
3486 # workingctx to get consistent result (issue4497). this means 'set:**'
3489 # workingctx to get consistent result (issue4497). this means 'set:**'
3487 # cannot be used to select missing files from target rev.
3490 # cannot be used to select missing files from target rev.
3488
3491
3489 # `names` is a mapping for all elements in working copy and target revision
3492 # `names` is a mapping for all elements in working copy and target revision
3490 # The mapping is in the form:
3493 # The mapping is in the form:
3491 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3494 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3492 names = {}
3495 names = {}
3493 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3496 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3494
3497
3495 with repo.wlock():
3498 with repo.wlock():
3496 ## filling of the `names` mapping
3499 ## filling of the `names` mapping
3497 # walk dirstate to fill `names`
3500 # walk dirstate to fill `names`
3498
3501
3499 interactive = opts.get(b'interactive', False)
3502 interactive = opts.get(b'interactive', False)
3500 wctx = repo[None]
3503 wctx = repo[None]
3501 m = scmutil.match(wctx, pats, opts)
3504 m = scmutil.match(wctx, pats, opts)
3502
3505
3503 # we'll need this later
3506 # we'll need this later
3504 targetsubs = sorted(s for s in wctx.substate if m(s))
3507 targetsubs = sorted(s for s in wctx.substate if m(s))
3505
3508
3506 if not m.always():
3509 if not m.always():
3507 matcher = matchmod.badmatch(m, lambda x, y: False)
3510 matcher = matchmod.badmatch(m, lambda x, y: False)
3508 for abs in wctx.walk(matcher):
3511 for abs in wctx.walk(matcher):
3509 names[abs] = m.exact(abs)
3512 names[abs] = m.exact(abs)
3510
3513
3511 # walk target manifest to fill `names`
3514 # walk target manifest to fill `names`
3512
3515
3513 def badfn(path, msg):
3516 def badfn(path, msg):
3514 if path in names:
3517 if path in names:
3515 return
3518 return
3516 if path in ctx.substate:
3519 if path in ctx.substate:
3517 return
3520 return
3518 path_ = path + b'/'
3521 path_ = path + b'/'
3519 for f in names:
3522 for f in names:
3520 if f.startswith(path_):
3523 if f.startswith(path_):
3521 return
3524 return
3522 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3525 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3523
3526
3524 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3527 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3525 if abs not in names:
3528 if abs not in names:
3526 names[abs] = m.exact(abs)
3529 names[abs] = m.exact(abs)
3527
3530
3528 # Find status of all file in `names`.
3531 # Find status of all file in `names`.
3529 m = scmutil.matchfiles(repo, names)
3532 m = scmutil.matchfiles(repo, names)
3530
3533
3531 changes = repo.status(
3534 changes = repo.status(
3532 node1=node, match=m, unknown=True, ignored=True, clean=True
3535 node1=node, match=m, unknown=True, ignored=True, clean=True
3533 )
3536 )
3534 else:
3537 else:
3535 changes = repo.status(node1=node, match=m)
3538 changes = repo.status(node1=node, match=m)
3536 for kind in changes:
3539 for kind in changes:
3537 for abs in kind:
3540 for abs in kind:
3538 names[abs] = m.exact(abs)
3541 names[abs] = m.exact(abs)
3539
3542
3540 m = scmutil.matchfiles(repo, names)
3543 m = scmutil.matchfiles(repo, names)
3541
3544
3542 modified = set(changes.modified)
3545 modified = set(changes.modified)
3543 added = set(changes.added)
3546 added = set(changes.added)
3544 removed = set(changes.removed)
3547 removed = set(changes.removed)
3545 _deleted = set(changes.deleted)
3548 _deleted = set(changes.deleted)
3546 unknown = set(changes.unknown)
3549 unknown = set(changes.unknown)
3547 unknown.update(changes.ignored)
3550 unknown.update(changes.ignored)
3548 clean = set(changes.clean)
3551 clean = set(changes.clean)
3549 modadded = set()
3552 modadded = set()
3550
3553
3551 # We need to account for the state of the file in the dirstate,
3554 # We need to account for the state of the file in the dirstate,
3552 # even when we revert against something else than parent. This will
3555 # even when we revert against something else than parent. This will
3553 # slightly alter the behavior of revert (doing back up or not, delete
3556 # slightly alter the behavior of revert (doing back up or not, delete
3554 # or just forget etc).
3557 # or just forget etc).
3555 if parent == node:
3558 if parent == node:
3556 dsmodified = modified
3559 dsmodified = modified
3557 dsadded = added
3560 dsadded = added
3558 dsremoved = removed
3561 dsremoved = removed
3559 # store all local modifications, useful later for rename detection
3562 # store all local modifications, useful later for rename detection
3560 localchanges = dsmodified | dsadded
3563 localchanges = dsmodified | dsadded
3561 modified, added, removed = set(), set(), set()
3564 modified, added, removed = set(), set(), set()
3562 else:
3565 else:
3563 changes = repo.status(node1=parent, match=m)
3566 changes = repo.status(node1=parent, match=m)
3564 dsmodified = set(changes.modified)
3567 dsmodified = set(changes.modified)
3565 dsadded = set(changes.added)
3568 dsadded = set(changes.added)
3566 dsremoved = set(changes.removed)
3569 dsremoved = set(changes.removed)
3567 # store all local modifications, useful later for rename detection
3570 # store all local modifications, useful later for rename detection
3568 localchanges = dsmodified | dsadded
3571 localchanges = dsmodified | dsadded
3569
3572
3570 # only take into account for removes between wc and target
3573 # only take into account for removes between wc and target
3571 clean |= dsremoved - removed
3574 clean |= dsremoved - removed
3572 dsremoved &= removed
3575 dsremoved &= removed
3573 # distinct between dirstate remove and other
3576 # distinct between dirstate remove and other
3574 removed -= dsremoved
3577 removed -= dsremoved
3575
3578
3576 modadded = added & dsmodified
3579 modadded = added & dsmodified
3577 added -= modadded
3580 added -= modadded
3578
3581
3579 # tell newly modified apart.
3582 # tell newly modified apart.
3580 dsmodified &= modified
3583 dsmodified &= modified
3581 dsmodified |= modified & dsadded # dirstate added may need backup
3584 dsmodified |= modified & dsadded # dirstate added may need backup
3582 modified -= dsmodified
3585 modified -= dsmodified
3583
3586
3584 # We need to wait for some post-processing to update this set
3587 # We need to wait for some post-processing to update this set
3585 # before making the distinction. The dirstate will be used for
3588 # before making the distinction. The dirstate will be used for
3586 # that purpose.
3589 # that purpose.
3587 dsadded = added
3590 dsadded = added
3588
3591
3589 # in case of merge, files that are actually added can be reported as
3592 # in case of merge, files that are actually added can be reported as
3590 # modified, we need to post process the result
3593 # modified, we need to post process the result
3591 if p2 != nullid:
3594 if p2 != nullid:
3592 mergeadd = set(dsmodified)
3595 mergeadd = set(dsmodified)
3593 for path in dsmodified:
3596 for path in dsmodified:
3594 if path in mf:
3597 if path in mf:
3595 mergeadd.remove(path)
3598 mergeadd.remove(path)
3596 dsadded |= mergeadd
3599 dsadded |= mergeadd
3597 dsmodified -= mergeadd
3600 dsmodified -= mergeadd
3598
3601
3599 # if f is a rename, update `names` to also revert the source
3602 # if f is a rename, update `names` to also revert the source
3600 for f in localchanges:
3603 for f in localchanges:
3601 src = repo.dirstate.copied(f)
3604 src = repo.dirstate.copied(f)
3602 # XXX should we check for rename down to target node?
3605 # XXX should we check for rename down to target node?
3603 if src and src not in names and repo.dirstate[src] == b'r':
3606 if src and src not in names and repo.dirstate[src] == b'r':
3604 dsremoved.add(src)
3607 dsremoved.add(src)
3605 names[src] = True
3608 names[src] = True
3606
3609
3607 # determine the exact nature of the deleted changesets
3610 # determine the exact nature of the deleted changesets
3608 deladded = set(_deleted)
3611 deladded = set(_deleted)
3609 for path in _deleted:
3612 for path in _deleted:
3610 if path in mf:
3613 if path in mf:
3611 deladded.remove(path)
3614 deladded.remove(path)
3612 deleted = _deleted - deladded
3615 deleted = _deleted - deladded
3613
3616
3614 # distinguish between file to forget and the other
3617 # distinguish between file to forget and the other
3615 added = set()
3618 added = set()
3616 for abs in dsadded:
3619 for abs in dsadded:
3617 if repo.dirstate[abs] != b'a':
3620 if repo.dirstate[abs] != b'a':
3618 added.add(abs)
3621 added.add(abs)
3619 dsadded -= added
3622 dsadded -= added
3620
3623
3621 for abs in deladded:
3624 for abs in deladded:
3622 if repo.dirstate[abs] == b'a':
3625 if repo.dirstate[abs] == b'a':
3623 dsadded.add(abs)
3626 dsadded.add(abs)
3624 deladded -= dsadded
3627 deladded -= dsadded
3625
3628
3626 # For files marked as removed, we check if an unknown file is present at
3629 # For files marked as removed, we check if an unknown file is present at
3627 # the same path. If a such file exists it may need to be backed up.
3630 # the same path. If a such file exists it may need to be backed up.
3628 # Making the distinction at this stage helps have simpler backup
3631 # Making the distinction at this stage helps have simpler backup
3629 # logic.
3632 # logic.
3630 removunk = set()
3633 removunk = set()
3631 for abs in removed:
3634 for abs in removed:
3632 target = repo.wjoin(abs)
3635 target = repo.wjoin(abs)
3633 if os.path.lexists(target):
3636 if os.path.lexists(target):
3634 removunk.add(abs)
3637 removunk.add(abs)
3635 removed -= removunk
3638 removed -= removunk
3636
3639
3637 dsremovunk = set()
3640 dsremovunk = set()
3638 for abs in dsremoved:
3641 for abs in dsremoved:
3639 target = repo.wjoin(abs)
3642 target = repo.wjoin(abs)
3640 if os.path.lexists(target):
3643 if os.path.lexists(target):
3641 dsremovunk.add(abs)
3644 dsremovunk.add(abs)
3642 dsremoved -= dsremovunk
3645 dsremoved -= dsremovunk
3643
3646
3644 # action to be actually performed by revert
3647 # action to be actually performed by revert
3645 # (<list of file>, message>) tuple
3648 # (<list of file>, message>) tuple
3646 actions = {
3649 actions = {
3647 b'revert': ([], _(b'reverting %s\n')),
3650 b'revert': ([], _(b'reverting %s\n')),
3648 b'add': ([], _(b'adding %s\n')),
3651 b'add': ([], _(b'adding %s\n')),
3649 b'remove': ([], _(b'removing %s\n')),
3652 b'remove': ([], _(b'removing %s\n')),
3650 b'drop': ([], _(b'removing %s\n')),
3653 b'drop': ([], _(b'removing %s\n')),
3651 b'forget': ([], _(b'forgetting %s\n')),
3654 b'forget': ([], _(b'forgetting %s\n')),
3652 b'undelete': ([], _(b'undeleting %s\n')),
3655 b'undelete': ([], _(b'undeleting %s\n')),
3653 b'noop': (None, _(b'no changes needed to %s\n')),
3656 b'noop': (None, _(b'no changes needed to %s\n')),
3654 b'unknown': (None, _(b'file not managed: %s\n')),
3657 b'unknown': (None, _(b'file not managed: %s\n')),
3655 }
3658 }
3656
3659
3657 # "constant" that convey the backup strategy.
3660 # "constant" that convey the backup strategy.
3658 # All set to `discard` if `no-backup` is set do avoid checking
3661 # All set to `discard` if `no-backup` is set do avoid checking
3659 # no_backup lower in the code.
3662 # no_backup lower in the code.
3660 # These values are ordered for comparison purposes
3663 # These values are ordered for comparison purposes
3661 backupinteractive = 3 # do backup if interactively modified
3664 backupinteractive = 3 # do backup if interactively modified
3662 backup = 2 # unconditionally do backup
3665 backup = 2 # unconditionally do backup
3663 check = 1 # check if the existing file differs from target
3666 check = 1 # check if the existing file differs from target
3664 discard = 0 # never do backup
3667 discard = 0 # never do backup
3665 if opts.get(b'no_backup'):
3668 if opts.get(b'no_backup'):
3666 backupinteractive = backup = check = discard
3669 backupinteractive = backup = check = discard
3667 if interactive:
3670 if interactive:
3668 dsmodifiedbackup = backupinteractive
3671 dsmodifiedbackup = backupinteractive
3669 else:
3672 else:
3670 dsmodifiedbackup = backup
3673 dsmodifiedbackup = backup
3671 tobackup = set()
3674 tobackup = set()
3672
3675
3673 backupanddel = actions[b'remove']
3676 backupanddel = actions[b'remove']
3674 if not opts.get(b'no_backup'):
3677 if not opts.get(b'no_backup'):
3675 backupanddel = actions[b'drop']
3678 backupanddel = actions[b'drop']
3676
3679
3677 disptable = (
3680 disptable = (
3678 # dispatch table:
3681 # dispatch table:
3679 # file state
3682 # file state
3680 # action
3683 # action
3681 # make backup
3684 # make backup
3682 ## Sets that results that will change file on disk
3685 ## Sets that results that will change file on disk
3683 # Modified compared to target, no local change
3686 # Modified compared to target, no local change
3684 (modified, actions[b'revert'], discard),
3687 (modified, actions[b'revert'], discard),
3685 # Modified compared to target, but local file is deleted
3688 # Modified compared to target, but local file is deleted
3686 (deleted, actions[b'revert'], discard),
3689 (deleted, actions[b'revert'], discard),
3687 # Modified compared to target, local change
3690 # Modified compared to target, local change
3688 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3691 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3689 # Added since target
3692 # Added since target
3690 (added, actions[b'remove'], discard),
3693 (added, actions[b'remove'], discard),
3691 # Added in working directory
3694 # Added in working directory
3692 (dsadded, actions[b'forget'], discard),
3695 (dsadded, actions[b'forget'], discard),
3693 # Added since target, have local modification
3696 # Added since target, have local modification
3694 (modadded, backupanddel, backup),
3697 (modadded, backupanddel, backup),
3695 # Added since target but file is missing in working directory
3698 # Added since target but file is missing in working directory
3696 (deladded, actions[b'drop'], discard),
3699 (deladded, actions[b'drop'], discard),
3697 # Removed since target, before working copy parent
3700 # Removed since target, before working copy parent
3698 (removed, actions[b'add'], discard),
3701 (removed, actions[b'add'], discard),
3699 # Same as `removed` but an unknown file exists at the same path
3702 # Same as `removed` but an unknown file exists at the same path
3700 (removunk, actions[b'add'], check),
3703 (removunk, actions[b'add'], check),
3701 # Removed since targe, marked as such in working copy parent
3704 # Removed since targe, marked as such in working copy parent
3702 (dsremoved, actions[b'undelete'], discard),
3705 (dsremoved, actions[b'undelete'], discard),
3703 # Same as `dsremoved` but an unknown file exists at the same path
3706 # Same as `dsremoved` but an unknown file exists at the same path
3704 (dsremovunk, actions[b'undelete'], check),
3707 (dsremovunk, actions[b'undelete'], check),
3705 ## the following sets does not result in any file changes
3708 ## the following sets does not result in any file changes
3706 # File with no modification
3709 # File with no modification
3707 (clean, actions[b'noop'], discard),
3710 (clean, actions[b'noop'], discard),
3708 # Existing file, not tracked anywhere
3711 # Existing file, not tracked anywhere
3709 (unknown, actions[b'unknown'], discard),
3712 (unknown, actions[b'unknown'], discard),
3710 )
3713 )
3711
3714
3712 for abs, exact in sorted(names.items()):
3715 for abs, exact in sorted(names.items()):
3713 # target file to be touch on disk (relative to cwd)
3716 # target file to be touch on disk (relative to cwd)
3714 target = repo.wjoin(abs)
3717 target = repo.wjoin(abs)
3715 # search the entry in the dispatch table.
3718 # search the entry in the dispatch table.
3716 # if the file is in any of these sets, it was touched in the working
3719 # if the file is in any of these sets, it was touched in the working
3717 # directory parent and we are sure it needs to be reverted.
3720 # directory parent and we are sure it needs to be reverted.
3718 for table, (xlist, msg), dobackup in disptable:
3721 for table, (xlist, msg), dobackup in disptable:
3719 if abs not in table:
3722 if abs not in table:
3720 continue
3723 continue
3721 if xlist is not None:
3724 if xlist is not None:
3722 xlist.append(abs)
3725 xlist.append(abs)
3723 if dobackup:
3726 if dobackup:
3724 # If in interactive mode, don't automatically create
3727 # If in interactive mode, don't automatically create
3725 # .orig files (issue4793)
3728 # .orig files (issue4793)
3726 if dobackup == backupinteractive:
3729 if dobackup == backupinteractive:
3727 tobackup.add(abs)
3730 tobackup.add(abs)
3728 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3731 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3729 absbakname = scmutil.backuppath(ui, repo, abs)
3732 absbakname = scmutil.backuppath(ui, repo, abs)
3730 bakname = os.path.relpath(
3733 bakname = os.path.relpath(
3731 absbakname, start=repo.root
3734 absbakname, start=repo.root
3732 )
3735 )
3733 ui.note(
3736 ui.note(
3734 _(b'saving current version of %s as %s\n')
3737 _(b'saving current version of %s as %s\n')
3735 % (uipathfn(abs), uipathfn(bakname))
3738 % (uipathfn(abs), uipathfn(bakname))
3736 )
3739 )
3737 if not opts.get(b'dry_run'):
3740 if not opts.get(b'dry_run'):
3738 if interactive:
3741 if interactive:
3739 util.copyfile(target, absbakname)
3742 util.copyfile(target, absbakname)
3740 else:
3743 else:
3741 util.rename(target, absbakname)
3744 util.rename(target, absbakname)
3742 if opts.get(b'dry_run'):
3745 if opts.get(b'dry_run'):
3743 if ui.verbose or not exact:
3746 if ui.verbose or not exact:
3744 ui.status(msg % uipathfn(abs))
3747 ui.status(msg % uipathfn(abs))
3745 elif exact:
3748 elif exact:
3746 ui.warn(msg % uipathfn(abs))
3749 ui.warn(msg % uipathfn(abs))
3747 break
3750 break
3748
3751
3749 if not opts.get(b'dry_run'):
3752 if not opts.get(b'dry_run'):
3750 needdata = (b'revert', b'add', b'undelete')
3753 needdata = (b'revert', b'add', b'undelete')
3751 oplist = [actions[name][0] for name in needdata]
3754 oplist = [actions[name][0] for name in needdata]
3752 prefetch = scmutil.prefetchfiles
3755 prefetch = scmutil.prefetchfiles
3753 matchfiles = scmutil.matchfiles
3756 matchfiles = scmutil.matchfiles
3754 prefetch(
3757 prefetch(
3755 repo,
3758 repo,
3756 [ctx.rev()],
3759 [ctx.rev()],
3757 matchfiles(repo, [f for sublist in oplist for f in sublist]),
3760 matchfiles(repo, [f for sublist in oplist for f in sublist]),
3758 )
3761 )
3759 match = scmutil.match(repo[None], pats)
3762 match = scmutil.match(repo[None], pats)
3760 _performrevert(
3763 _performrevert(
3761 repo,
3764 repo,
3762 parents,
3765 parents,
3763 ctx,
3766 ctx,
3764 names,
3767 names,
3765 uipathfn,
3768 uipathfn,
3766 actions,
3769 actions,
3767 match,
3770 match,
3768 interactive,
3771 interactive,
3769 tobackup,
3772 tobackup,
3770 )
3773 )
3771
3774
3772 if targetsubs:
3775 if targetsubs:
3773 # Revert the subrepos on the revert list
3776 # Revert the subrepos on the revert list
3774 for sub in targetsubs:
3777 for sub in targetsubs:
3775 try:
3778 try:
3776 wctx.sub(sub).revert(
3779 wctx.sub(sub).revert(
3777 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3780 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3778 )
3781 )
3779 except KeyError:
3782 except KeyError:
3780 raise error.Abort(
3783 raise error.Abort(
3781 b"subrepository '%s' does not exist in %s!"
3784 b"subrepository '%s' does not exist in %s!"
3782 % (sub, short(ctx.node()))
3785 % (sub, short(ctx.node()))
3783 )
3786 )
3784
3787
3785
3788
3786 def _performrevert(
3789 def _performrevert(
3787 repo,
3790 repo,
3788 parents,
3791 parents,
3789 ctx,
3792 ctx,
3790 names,
3793 names,
3791 uipathfn,
3794 uipathfn,
3792 actions,
3795 actions,
3793 match,
3796 match,
3794 interactive=False,
3797 interactive=False,
3795 tobackup=None,
3798 tobackup=None,
3796 ):
3799 ):
3797 """function that actually perform all the actions computed for revert
3800 """function that actually perform all the actions computed for revert
3798
3801
3799 This is an independent function to let extension to plug in and react to
3802 This is an independent function to let extension to plug in and react to
3800 the imminent revert.
3803 the imminent revert.
3801
3804
3802 Make sure you have the working directory locked when calling this function.
3805 Make sure you have the working directory locked when calling this function.
3803 """
3806 """
3804 parent, p2 = parents
3807 parent, p2 = parents
3805 node = ctx.node()
3808 node = ctx.node()
3806 excluded_files = []
3809 excluded_files = []
3807
3810
3808 def checkout(f):
3811 def checkout(f):
3809 fc = ctx[f]
3812 fc = ctx[f]
3810 repo.wwrite(f, fc.data(), fc.flags())
3813 repo.wwrite(f, fc.data(), fc.flags())
3811
3814
3812 def doremove(f):
3815 def doremove(f):
3813 try:
3816 try:
3814 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3817 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3815 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3818 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3816 except OSError:
3819 except OSError:
3817 pass
3820 pass
3818 repo.dirstate.remove(f)
3821 repo.dirstate.remove(f)
3819
3822
3820 def prntstatusmsg(action, f):
3823 def prntstatusmsg(action, f):
3821 exact = names[f]
3824 exact = names[f]
3822 if repo.ui.verbose or not exact:
3825 if repo.ui.verbose or not exact:
3823 repo.ui.status(actions[action][1] % uipathfn(f))
3826 repo.ui.status(actions[action][1] % uipathfn(f))
3824
3827
3825 audit_path = pathutil.pathauditor(repo.root, cached=True)
3828 audit_path = pathutil.pathauditor(repo.root, cached=True)
3826 for f in actions[b'forget'][0]:
3829 for f in actions[b'forget'][0]:
3827 if interactive:
3830 if interactive:
3828 choice = repo.ui.promptchoice(
3831 choice = repo.ui.promptchoice(
3829 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3832 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3830 )
3833 )
3831 if choice == 0:
3834 if choice == 0:
3832 prntstatusmsg(b'forget', f)
3835 prntstatusmsg(b'forget', f)
3833 repo.dirstate.drop(f)
3836 repo.dirstate.drop(f)
3834 else:
3837 else:
3835 excluded_files.append(f)
3838 excluded_files.append(f)
3836 else:
3839 else:
3837 prntstatusmsg(b'forget', f)
3840 prntstatusmsg(b'forget', f)
3838 repo.dirstate.drop(f)
3841 repo.dirstate.drop(f)
3839 for f in actions[b'remove'][0]:
3842 for f in actions[b'remove'][0]:
3840 audit_path(f)
3843 audit_path(f)
3841 if interactive:
3844 if interactive:
3842 choice = repo.ui.promptchoice(
3845 choice = repo.ui.promptchoice(
3843 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3846 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3844 )
3847 )
3845 if choice == 0:
3848 if choice == 0:
3846 prntstatusmsg(b'remove', f)
3849 prntstatusmsg(b'remove', f)
3847 doremove(f)
3850 doremove(f)
3848 else:
3851 else:
3849 excluded_files.append(f)
3852 excluded_files.append(f)
3850 else:
3853 else:
3851 prntstatusmsg(b'remove', f)
3854 prntstatusmsg(b'remove', f)
3852 doremove(f)
3855 doremove(f)
3853 for f in actions[b'drop'][0]:
3856 for f in actions[b'drop'][0]:
3854 audit_path(f)
3857 audit_path(f)
3855 prntstatusmsg(b'drop', f)
3858 prntstatusmsg(b'drop', f)
3856 repo.dirstate.remove(f)
3859 repo.dirstate.remove(f)
3857
3860
3858 normal = None
3861 normal = None
3859 if node == parent:
3862 if node == parent:
3860 # We're reverting to our parent. If possible, we'd like status
3863 # We're reverting to our parent. If possible, we'd like status
3861 # to report the file as clean. We have to use normallookup for
3864 # to report the file as clean. We have to use normallookup for
3862 # merges to avoid losing information about merged/dirty files.
3865 # merges to avoid losing information about merged/dirty files.
3863 if p2 != nullid:
3866 if p2 != nullid:
3864 normal = repo.dirstate.normallookup
3867 normal = repo.dirstate.normallookup
3865 else:
3868 else:
3866 normal = repo.dirstate.normal
3869 normal = repo.dirstate.normal
3867
3870
3868 newlyaddedandmodifiedfiles = set()
3871 newlyaddedandmodifiedfiles = set()
3869 if interactive:
3872 if interactive:
3870 # Prompt the user for changes to revert
3873 # Prompt the user for changes to revert
3871 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3874 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3872 m = scmutil.matchfiles(repo, torevert)
3875 m = scmutil.matchfiles(repo, torevert)
3873 diffopts = patch.difffeatureopts(
3876 diffopts = patch.difffeatureopts(
3874 repo.ui,
3877 repo.ui,
3875 whitespace=True,
3878 whitespace=True,
3876 section=b'commands',
3879 section=b'commands',
3877 configprefix=b'revert.interactive.',
3880 configprefix=b'revert.interactive.',
3878 )
3881 )
3879 diffopts.nodates = True
3882 diffopts.nodates = True
3880 diffopts.git = True
3883 diffopts.git = True
3881 operation = b'apply'
3884 operation = b'apply'
3882 if node == parent:
3885 if node == parent:
3883 if repo.ui.configbool(
3886 if repo.ui.configbool(
3884 b'experimental', b'revert.interactive.select-to-keep'
3887 b'experimental', b'revert.interactive.select-to-keep'
3885 ):
3888 ):
3886 operation = b'keep'
3889 operation = b'keep'
3887 else:
3890 else:
3888 operation = b'discard'
3891 operation = b'discard'
3889
3892
3890 if operation == b'apply':
3893 if operation == b'apply':
3891 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3894 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3892 else:
3895 else:
3893 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3896 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3894 originalchunks = patch.parsepatch(diff)
3897 originalchunks = patch.parsepatch(diff)
3895
3898
3896 try:
3899 try:
3897
3900
3898 chunks, opts = recordfilter(
3901 chunks, opts = recordfilter(
3899 repo.ui, originalchunks, match, operation=operation
3902 repo.ui, originalchunks, match, operation=operation
3900 )
3903 )
3901 if operation == b'discard':
3904 if operation == b'discard':
3902 chunks = patch.reversehunks(chunks)
3905 chunks = patch.reversehunks(chunks)
3903
3906
3904 except error.PatchError as err:
3907 except error.PatchError as err:
3905 raise error.Abort(_(b'error parsing patch: %s') % err)
3908 raise error.Abort(_(b'error parsing patch: %s') % err)
3906
3909
3907 # FIXME: when doing an interactive revert of a copy, there's no way of
3910 # FIXME: when doing an interactive revert of a copy, there's no way of
3908 # performing a partial revert of the added file, the only option is
3911 # performing a partial revert of the added file, the only option is
3909 # "remove added file <name> (Yn)?", so we don't need to worry about the
3912 # "remove added file <name> (Yn)?", so we don't need to worry about the
3910 # alsorestore value. Ideally we'd be able to partially revert
3913 # alsorestore value. Ideally we'd be able to partially revert
3911 # copied/renamed files.
3914 # copied/renamed files.
3912 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3915 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3913 chunks, originalchunks
3916 chunks, originalchunks
3914 )
3917 )
3915 if tobackup is None:
3918 if tobackup is None:
3916 tobackup = set()
3919 tobackup = set()
3917 # Apply changes
3920 # Apply changes
3918 fp = stringio()
3921 fp = stringio()
3919 # chunks are serialized per file, but files aren't sorted
3922 # chunks are serialized per file, but files aren't sorted
3920 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3923 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3921 prntstatusmsg(b'revert', f)
3924 prntstatusmsg(b'revert', f)
3922 files = set()
3925 files = set()
3923 for c in chunks:
3926 for c in chunks:
3924 if ishunk(c):
3927 if ishunk(c):
3925 abs = c.header.filename()
3928 abs = c.header.filename()
3926 # Create a backup file only if this hunk should be backed up
3929 # Create a backup file only if this hunk should be backed up
3927 if c.header.filename() in tobackup:
3930 if c.header.filename() in tobackup:
3928 target = repo.wjoin(abs)
3931 target = repo.wjoin(abs)
3929 bakname = scmutil.backuppath(repo.ui, repo, abs)
3932 bakname = scmutil.backuppath(repo.ui, repo, abs)
3930 util.copyfile(target, bakname)
3933 util.copyfile(target, bakname)
3931 tobackup.remove(abs)
3934 tobackup.remove(abs)
3932 if abs not in files:
3935 if abs not in files:
3933 files.add(abs)
3936 files.add(abs)
3934 if operation == b'keep':
3937 if operation == b'keep':
3935 checkout(abs)
3938 checkout(abs)
3936 c.write(fp)
3939 c.write(fp)
3937 dopatch = fp.tell()
3940 dopatch = fp.tell()
3938 fp.seek(0)
3941 fp.seek(0)
3939 if dopatch:
3942 if dopatch:
3940 try:
3943 try:
3941 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3944 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3942 except error.PatchError as err:
3945 except error.PatchError as err:
3943 raise error.Abort(pycompat.bytestr(err))
3946 raise error.Abort(pycompat.bytestr(err))
3944 del fp
3947 del fp
3945 else:
3948 else:
3946 for f in actions[b'revert'][0]:
3949 for f in actions[b'revert'][0]:
3947 prntstatusmsg(b'revert', f)
3950 prntstatusmsg(b'revert', f)
3948 checkout(f)
3951 checkout(f)
3949 if normal:
3952 if normal:
3950 normal(f)
3953 normal(f)
3951
3954
3952 for f in actions[b'add'][0]:
3955 for f in actions[b'add'][0]:
3953 # Don't checkout modified files, they are already created by the diff
3956 # Don't checkout modified files, they are already created by the diff
3954 if f not in newlyaddedandmodifiedfiles:
3957 if f not in newlyaddedandmodifiedfiles:
3955 prntstatusmsg(b'add', f)
3958 prntstatusmsg(b'add', f)
3956 checkout(f)
3959 checkout(f)
3957 repo.dirstate.add(f)
3960 repo.dirstate.add(f)
3958
3961
3959 normal = repo.dirstate.normallookup
3962 normal = repo.dirstate.normallookup
3960 if node == parent and p2 == nullid:
3963 if node == parent and p2 == nullid:
3961 normal = repo.dirstate.normal
3964 normal = repo.dirstate.normal
3962 for f in actions[b'undelete'][0]:
3965 for f in actions[b'undelete'][0]:
3963 if interactive:
3966 if interactive:
3964 choice = repo.ui.promptchoice(
3967 choice = repo.ui.promptchoice(
3965 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3968 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3966 )
3969 )
3967 if choice == 0:
3970 if choice == 0:
3968 prntstatusmsg(b'undelete', f)
3971 prntstatusmsg(b'undelete', f)
3969 checkout(f)
3972 checkout(f)
3970 normal(f)
3973 normal(f)
3971 else:
3974 else:
3972 excluded_files.append(f)
3975 excluded_files.append(f)
3973 else:
3976 else:
3974 prntstatusmsg(b'undelete', f)
3977 prntstatusmsg(b'undelete', f)
3975 checkout(f)
3978 checkout(f)
3976 normal(f)
3979 normal(f)
3977
3980
3978 copied = copies.pathcopies(repo[parent], ctx)
3981 copied = copies.pathcopies(repo[parent], ctx)
3979
3982
3980 for f in (
3983 for f in (
3981 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3984 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3982 ):
3985 ):
3983 if f in copied:
3986 if f in copied:
3984 repo.dirstate.copy(copied[f], f)
3987 repo.dirstate.copy(copied[f], f)
3985
3988
3986
3989
3987 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3990 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3988 # commands.outgoing. "missing" is "missing" of the result of
3991 # commands.outgoing. "missing" is "missing" of the result of
3989 # "findcommonoutgoing()"
3992 # "findcommonoutgoing()"
3990 outgoinghooks = util.hooks()
3993 outgoinghooks = util.hooks()
3991
3994
3992 # a list of (ui, repo) functions called by commands.summary
3995 # a list of (ui, repo) functions called by commands.summary
3993 summaryhooks = util.hooks()
3996 summaryhooks = util.hooks()
3994
3997
3995 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3998 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3996 #
3999 #
3997 # functions should return tuple of booleans below, if 'changes' is None:
4000 # functions should return tuple of booleans below, if 'changes' is None:
3998 # (whether-incomings-are-needed, whether-outgoings-are-needed)
4001 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3999 #
4002 #
4000 # otherwise, 'changes' is a tuple of tuples below:
4003 # otherwise, 'changes' is a tuple of tuples below:
4001 # - (sourceurl, sourcebranch, sourcepeer, incoming)
4004 # - (sourceurl, sourcebranch, sourcepeer, incoming)
4002 # - (desturl, destbranch, destpeer, outgoing)
4005 # - (desturl, destbranch, destpeer, outgoing)
4003 summaryremotehooks = util.hooks()
4006 summaryremotehooks = util.hooks()
4004
4007
4005
4008
4006 def checkunfinished(repo, commit=False, skipmerge=False):
4009 def checkunfinished(repo, commit=False, skipmerge=False):
4007 '''Look for an unfinished multistep operation, like graft, and abort
4010 '''Look for an unfinished multistep operation, like graft, and abort
4008 if found. It's probably good to check this right before
4011 if found. It's probably good to check this right before
4009 bailifchanged().
4012 bailifchanged().
4010 '''
4013 '''
4011 # Check for non-clearable states first, so things like rebase will take
4014 # Check for non-clearable states first, so things like rebase will take
4012 # precedence over update.
4015 # precedence over update.
4013 for state in statemod._unfinishedstates:
4016 for state in statemod._unfinishedstates:
4014 if (
4017 if (
4015 state._clearable
4018 state._clearable
4016 or (commit and state._allowcommit)
4019 or (commit and state._allowcommit)
4017 or state._reportonly
4020 or state._reportonly
4018 ):
4021 ):
4019 continue
4022 continue
4020 if state.isunfinished(repo):
4023 if state.isunfinished(repo):
4021 raise error.Abort(state.msg(), hint=state.hint())
4024 raise error.Abort(state.msg(), hint=state.hint())
4022
4025
4023 for s in statemod._unfinishedstates:
4026 for s in statemod._unfinishedstates:
4024 if (
4027 if (
4025 not s._clearable
4028 not s._clearable
4026 or (commit and s._allowcommit)
4029 or (commit and s._allowcommit)
4027 or (s._opname == b'merge' and skipmerge)
4030 or (s._opname == b'merge' and skipmerge)
4028 or s._reportonly
4031 or s._reportonly
4029 ):
4032 ):
4030 continue
4033 continue
4031 if s.isunfinished(repo):
4034 if s.isunfinished(repo):
4032 raise error.Abort(s.msg(), hint=s.hint())
4035 raise error.Abort(s.msg(), hint=s.hint())
4033
4036
4034
4037
4035 def clearunfinished(repo):
4038 def clearunfinished(repo):
4036 '''Check for unfinished operations (as above), and clear the ones
4039 '''Check for unfinished operations (as above), and clear the ones
4037 that are clearable.
4040 that are clearable.
4038 '''
4041 '''
4039 for state in statemod._unfinishedstates:
4042 for state in statemod._unfinishedstates:
4040 if state._reportonly:
4043 if state._reportonly:
4041 continue
4044 continue
4042 if not state._clearable and state.isunfinished(repo):
4045 if not state._clearable and state.isunfinished(repo):
4043 raise error.Abort(state.msg(), hint=state.hint())
4046 raise error.Abort(state.msg(), hint=state.hint())
4044
4047
4045 for s in statemod._unfinishedstates:
4048 for s in statemod._unfinishedstates:
4046 if s._opname == b'merge' or state._reportonly:
4049 if s._opname == b'merge' or state._reportonly:
4047 continue
4050 continue
4048 if s._clearable and s.isunfinished(repo):
4051 if s._clearable and s.isunfinished(repo):
4049 util.unlink(repo.vfs.join(s._fname))
4052 util.unlink(repo.vfs.join(s._fname))
4050
4053
4051
4054
4052 def getunfinishedstate(repo):
4055 def getunfinishedstate(repo):
4053 ''' Checks for unfinished operations and returns statecheck object
4056 ''' Checks for unfinished operations and returns statecheck object
4054 for it'''
4057 for it'''
4055 for state in statemod._unfinishedstates:
4058 for state in statemod._unfinishedstates:
4056 if state.isunfinished(repo):
4059 if state.isunfinished(repo):
4057 return state
4060 return state
4058 return None
4061 return None
4059
4062
4060
4063
4061 def howtocontinue(repo):
4064 def howtocontinue(repo):
4062 '''Check for an unfinished operation and return the command to finish
4065 '''Check for an unfinished operation and return the command to finish
4063 it.
4066 it.
4064
4067
4065 statemod._unfinishedstates list is checked for an unfinished operation
4068 statemod._unfinishedstates list is checked for an unfinished operation
4066 and the corresponding message to finish it is generated if a method to
4069 and the corresponding message to finish it is generated if a method to
4067 continue is supported by the operation.
4070 continue is supported by the operation.
4068
4071
4069 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
4072 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
4070 a boolean.
4073 a boolean.
4071 '''
4074 '''
4072 contmsg = _(b"continue: %s")
4075 contmsg = _(b"continue: %s")
4073 for state in statemod._unfinishedstates:
4076 for state in statemod._unfinishedstates:
4074 if not state._continueflag:
4077 if not state._continueflag:
4075 continue
4078 continue
4076 if state.isunfinished(repo):
4079 if state.isunfinished(repo):
4077 return contmsg % state.continuemsg(), True
4080 return contmsg % state.continuemsg(), True
4078 if repo[None].dirty(missing=True, merge=False, branch=False):
4081 if repo[None].dirty(missing=True, merge=False, branch=False):
4079 return contmsg % _(b"hg commit"), False
4082 return contmsg % _(b"hg commit"), False
4080 return None, None
4083 return None, None
4081
4084
4082
4085
4083 def checkafterresolved(repo):
4086 def checkafterresolved(repo):
4084 '''Inform the user about the next action after completing hg resolve
4087 '''Inform the user about the next action after completing hg resolve
4085
4088
4086 If there's a an unfinished operation that supports continue flag,
4089 If there's a an unfinished operation that supports continue flag,
4087 howtocontinue will yield repo.ui.warn as the reporter.
4090 howtocontinue will yield repo.ui.warn as the reporter.
4088
4091
4089 Otherwise, it will yield repo.ui.note.
4092 Otherwise, it will yield repo.ui.note.
4090 '''
4093 '''
4091 msg, warning = howtocontinue(repo)
4094 msg, warning = howtocontinue(repo)
4092 if msg is not None:
4095 if msg is not None:
4093 if warning:
4096 if warning:
4094 repo.ui.warn(b"%s\n" % msg)
4097 repo.ui.warn(b"%s\n" % msg)
4095 else:
4098 else:
4096 repo.ui.note(b"%s\n" % msg)
4099 repo.ui.note(b"%s\n" % msg)
4097
4100
4098
4101
4099 def wrongtooltocontinue(repo, task):
4102 def wrongtooltocontinue(repo, task):
4100 '''Raise an abort suggesting how to properly continue if there is an
4103 '''Raise an abort suggesting how to properly continue if there is an
4101 active task.
4104 active task.
4102
4105
4103 Uses howtocontinue() to find the active task.
4106 Uses howtocontinue() to find the active task.
4104
4107
4105 If there's no task (repo.ui.note for 'hg commit'), it does not offer
4108 If there's no task (repo.ui.note for 'hg commit'), it does not offer
4106 a hint.
4109 a hint.
4107 '''
4110 '''
4108 after = howtocontinue(repo)
4111 after = howtocontinue(repo)
4109 hint = None
4112 hint = None
4110 if after[1]:
4113 if after[1]:
4111 hint = after[0]
4114 hint = after[0]
4112 raise error.Abort(_(b'no %s in progress') % task, hint=hint)
4115 raise error.Abort(_(b'no %s in progress') % task, hint=hint)
4113
4116
4114
4117
4115 def abortgraft(ui, repo, graftstate):
4118 def abortgraft(ui, repo, graftstate):
4116 """abort the interrupted graft and rollbacks to the state before interrupted
4119 """abort the interrupted graft and rollbacks to the state before interrupted
4117 graft"""
4120 graft"""
4118 if not graftstate.exists():
4121 if not graftstate.exists():
4119 raise error.Abort(_(b"no interrupted graft to abort"))
4122 raise error.Abort(_(b"no interrupted graft to abort"))
4120 statedata = readgraftstate(repo, graftstate)
4123 statedata = readgraftstate(repo, graftstate)
4121 newnodes = statedata.get(b'newnodes')
4124 newnodes = statedata.get(b'newnodes')
4122 if newnodes is None:
4125 if newnodes is None:
4123 # and old graft state which does not have all the data required to abort
4126 # and old graft state which does not have all the data required to abort
4124 # the graft
4127 # the graft
4125 raise error.Abort(_(b"cannot abort using an old graftstate"))
4128 raise error.Abort(_(b"cannot abort using an old graftstate"))
4126
4129
4127 # changeset from which graft operation was started
4130 # changeset from which graft operation was started
4128 if len(newnodes) > 0:
4131 if len(newnodes) > 0:
4129 startctx = repo[newnodes[0]].p1()
4132 startctx = repo[newnodes[0]].p1()
4130 else:
4133 else:
4131 startctx = repo[b'.']
4134 startctx = repo[b'.']
4132 # whether to strip or not
4135 # whether to strip or not
4133 cleanup = False
4136 cleanup = False
4134 from . import hg
4137 from . import hg
4135
4138
4136 if newnodes:
4139 if newnodes:
4137 newnodes = [repo[r].rev() for r in newnodes]
4140 newnodes = [repo[r].rev() for r in newnodes]
4138 cleanup = True
4141 cleanup = True
4139 # checking that none of the newnodes turned public or is public
4142 # checking that none of the newnodes turned public or is public
4140 immutable = [c for c in newnodes if not repo[c].mutable()]
4143 immutable = [c for c in newnodes if not repo[c].mutable()]
4141 if immutable:
4144 if immutable:
4142 repo.ui.warn(
4145 repo.ui.warn(
4143 _(b"cannot clean up public changesets %s\n")
4146 _(b"cannot clean up public changesets %s\n")
4144 % b', '.join(bytes(repo[r]) for r in immutable),
4147 % b', '.join(bytes(repo[r]) for r in immutable),
4145 hint=_(b"see 'hg help phases' for details"),
4148 hint=_(b"see 'hg help phases' for details"),
4146 )
4149 )
4147 cleanup = False
4150 cleanup = False
4148
4151
4149 # checking that no new nodes are created on top of grafted revs
4152 # checking that no new nodes are created on top of grafted revs
4150 desc = set(repo.changelog.descendants(newnodes))
4153 desc = set(repo.changelog.descendants(newnodes))
4151 if desc - set(newnodes):
4154 if desc - set(newnodes):
4152 repo.ui.warn(
4155 repo.ui.warn(
4153 _(
4156 _(
4154 b"new changesets detected on destination "
4157 b"new changesets detected on destination "
4155 b"branch, can't strip\n"
4158 b"branch, can't strip\n"
4156 )
4159 )
4157 )
4160 )
4158 cleanup = False
4161 cleanup = False
4159
4162
4160 if cleanup:
4163 if cleanup:
4161 with repo.wlock(), repo.lock():
4164 with repo.wlock(), repo.lock():
4162 hg.updaterepo(repo, startctx.node(), overwrite=True)
4165 hg.updaterepo(repo, startctx.node(), overwrite=True)
4163 # stripping the new nodes created
4166 # stripping the new nodes created
4164 strippoints = [
4167 strippoints = [
4165 c.node() for c in repo.set(b"roots(%ld)", newnodes)
4168 c.node() for c in repo.set(b"roots(%ld)", newnodes)
4166 ]
4169 ]
4167 repair.strip(repo.ui, repo, strippoints, backup=False)
4170 repair.strip(repo.ui, repo, strippoints, backup=False)
4168
4171
4169 if not cleanup:
4172 if not cleanup:
4170 # we don't update to the startnode if we can't strip
4173 # we don't update to the startnode if we can't strip
4171 startctx = repo[b'.']
4174 startctx = repo[b'.']
4172 hg.updaterepo(repo, startctx.node(), overwrite=True)
4175 hg.updaterepo(repo, startctx.node(), overwrite=True)
4173
4176
4174 ui.status(_(b"graft aborted\n"))
4177 ui.status(_(b"graft aborted\n"))
4175 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
4178 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
4176 graftstate.delete()
4179 graftstate.delete()
4177 return 0
4180 return 0
4178
4181
4179
4182
4180 def readgraftstate(repo, graftstate):
4183 def readgraftstate(repo, graftstate):
4181 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
4184 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
4182 """read the graft state file and return a dict of the data stored in it"""
4185 """read the graft state file and return a dict of the data stored in it"""
4183 try:
4186 try:
4184 return graftstate.read()
4187 return graftstate.read()
4185 except error.CorruptedState:
4188 except error.CorruptedState:
4186 nodes = repo.vfs.read(b'graftstate').splitlines()
4189 nodes = repo.vfs.read(b'graftstate').splitlines()
4187 return {b'nodes': nodes}
4190 return {b'nodes': nodes}
4188
4191
4189
4192
4190 def hgabortgraft(ui, repo):
4193 def hgabortgraft(ui, repo):
4191 """ abort logic for aborting graft using 'hg abort'"""
4194 """ abort logic for aborting graft using 'hg abort'"""
4192 with repo.wlock():
4195 with repo.wlock():
4193 graftstate = statemod.cmdstate(repo, b'graftstate')
4196 graftstate = statemod.cmdstate(repo, b'graftstate')
4194 return abortgraft(ui, repo, graftstate)
4197 return abortgraft(ui, repo, graftstate)
@@ -1,74 +1,72 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 recoded copy, one copy to record after commit
14 # One recoded 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 destination
27 # Errors out with non-existent destination
28 $ hg cp -A --at-rev . d1/b d1/non-existent
28 $ hg cp -A --at-rev . d1/b d1/non-existent
29 abort: d1/non-existent: copy destination does not exist in 8a9d70fa20c9
29 abort: d1/non-existent: copy destination does not exist in 8a9d70fa20c9
30 [255]
30 [255]
31 # Successful invocation
31 # Successful invocation
32 $ hg cp -A --at-rev . d1/b d1/d
32 $ hg cp -A --at-rev . d1/b d1/d
33 saved backup bundle to $TESTTMP/.hg/strip-backup/8a9d70fa20c9-973ae357-copy.hg
33 saved backup bundle to $TESTTMP/.hg/strip-backup/8a9d70fa20c9-973ae357-copy.hg
34 # New copy is recorded, and previously recorded copy is also still there
34 # New copy is recorded, and previously recorded copy is also still there
35 $ hg st -C --change .
35 $ hg st -C --change .
36 A d1/c
36 A d1/c
37 d1/b
37 d1/b
38 A d1/d
38 A d1/d
39 d1/b
39 d1/b
40
40
41 Test moved file (not copied)
41 Test moved file (not copied)
42
42
43 $ hg co 0
43 $ hg co 0
44 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
44 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
45 $ mv d1/b d1/d
45 $ mv d1/b d1/d
46 $ hg rm -A d1/b
46 $ hg rm -A d1/b
47 $ hg add d1/d
47 $ hg add d1/d
48 $ hg ci -m 'move d1/b to d1/d'
48 $ hg ci -m 'move d1/b to d1/d'
49 created new head
49 created new head
50 BROKEN: this should work
51 $ hg cp -A --at-rev . d1/b d1/d
50 $ hg cp -A --at-rev . d1/b d1/d
52 d1/b: no such file in rev 519850c3ea27
51 saved backup bundle to $TESTTMP/.hg/strip-backup/519850c3ea27-153c8fbb-copy.hg
53 abort: --at-rev requires a single source
54 [255]
55 $ hg st -C --change .
52 $ hg st -C --change .
56 A d1/d
53 A d1/d
54 d1/b
57 R d1/b
55 R d1/b
58
56
59 Test using directory as destination
57 Test using directory as destination
60
58
61 $ hg co 0
59 $ hg co 0
62 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
60 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
63 $ cp -R d1 d3
61 $ cp -R d1 d3
64 $ hg add d3
62 $ hg add d3
65 adding d3/a
63 adding d3/a
66 adding d3/b
64 adding d3/b
67 adding d3/ba
65 adding d3/ba
68 adding d3/d11/a1
66 adding d3/d11/a1
69 $ hg ci -m 'copy d1/ to d3/'
67 $ hg ci -m 'copy d1/ to d3/'
70 created new head
68 created new head
71 $ hg cp -A --at-rev . d1 d3
69 $ hg cp -A --at-rev . d1 d3
72 abort: d3: --at-rev does not support a directory as destination
70 abort: d3: --at-rev does not support a directory as destination
73 [255]
71 [255]
74
72
General Comments 0
You need to be logged in to leave comments. Login now