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