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