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