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