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