cmdutil.py
3925 lines
| 128.6 KiB
| text/x-python
|
PythonLexer
/ mercurial / cmdutil.py
|
r2957 | # cmdutil.py - help for command processing in mercurial | ||
|
r2874 | # | ||
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
|
r2874 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r2874 | |||
|
r28322 | from __future__ import absolute_import | ||
|
r42143 | import copy as copymod | ||
|
r28322 | import errno | ||
import os | ||||
import re | ||||
from .i18n import _ | ||||
from .node import ( | ||||
hex, | ||||
|
r47600 | nullrev, | ||
|
r28322 | short, | ||
) | ||||
|
r43357 | from .pycompat import ( | ||
|
r43359 | getattr, | ||
|
r43357 | open, | ||
setattr, | ||||
) | ||||
|
r44293 | from .thirdparty import attr | ||
|
r28322 | |||
from . import ( | ||||
bookmarks, | ||||
changelog, | ||||
copies, | ||||
crecord as crecordmod, | ||||
|
r33619 | dirstateguard, | ||
|
r28322 | encoding, | ||
error, | ||||
formatter, | ||||
|
r35903 | logcmdutil, | ||
|
r28322 | match as matchmod, | ||
|
r36027 | merge as mergemod, | ||
|
r45383 | mergestate as mergestatemod, | ||
|
r36927 | mergeutil, | ||
|
r28322 | obsolete, | ||
patch, | ||||
pathutil, | ||||
|
r38442 | phases, | ||
|
r30519 | pycompat, | ||
|
r42768 | repair, | ||
|
r28322 | revlog, | ||
|
r35763 | rewriteutil, | ||
|
r28322 | scmutil, | ||
|
r42729 | state as statemod, | ||
|
r36026 | subrepoutil, | ||
|
r28322 | templatekw, | ||
templater, | ||||
util, | ||||
|
r31237 | vfs as vfsmod, | ||
|
r28322 | ) | ||
|
r37102 | |||
from .utils import ( | ||||
dateutil, | ||||
stringutil, | ||||
) | ||||
r47838 | from .revlogutils import ( | |||
constants as revlog_constants, | ||||
) | ||||
|
r44212 | if pycompat.TYPE_CHECKING: | ||
|
r44099 | from typing import ( | ||
Any, | ||||
Dict, | ||||
) | ||||
for t in (Any, Dict): | ||||
assert t | ||||
|
r28861 | stringio = util.stringio | ||
|
r2874 | |||
|
r32375 | # templates of common command options | ||
dryrunopts = [ | ||||
|
r43347 | (b'n', b'dry-run', None, _(b'do not perform actions, just print output')), | ||
|
r32375 | ] | ||
|
r38689 | confirmopts = [ | ||
|
r43347 | (b'', b'confirm', None, _(b'ask before applying actions')), | ||
|
r38689 | ] | ||
|
r32375 | remoteopts = [ | ||
|
r43347 | (b'e', b'ssh', b'', _(b'specify ssh command to use'), _(b'CMD')), | ||
|
r43346 | ( | ||
|
r43347 | b'', | ||
b'remotecmd', | ||||
b'', | ||||
_(b'specify hg command to run on the remote side'), | ||||
_(b'CMD'), | ||||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'', | ||
b'insecure', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'do not verify server certificate (ignoring web.cacerts config)'), | ||
|
r43346 | ), | ||
|
r32375 | ] | ||
walkopts = [ | ||||
|
r43346 | ( | ||
|
r43347 | b'I', | ||
b'include', | ||||
|
r43346 | [], | ||
|
r43347 | _(b'include names matching the given patterns'), | ||
_(b'PATTERN'), | ||||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'X', | ||
b'exclude', | ||||
|
r43346 | [], | ||
|
r43347 | _(b'exclude names matching the given patterns'), | ||
_(b'PATTERN'), | ||||
|
r43346 | ), | ||
|
r32375 | ] | ||
commitopts = [ | ||||
|
r43347 | (b'm', b'message', b'', _(b'use text as commit message'), _(b'TEXT')), | ||
(b'l', b'logfile', b'', _(b'read commit message from file'), _(b'FILE')), | ||||
|
r32375 | ] | ||
commitopts2 = [ | ||||
|
r43347 | ( | ||
b'd', | ||||
b'date', | ||||
b'', | ||||
_(b'record the specified date as commit date'), | ||||
_(b'DATE'), | ||||
), | ||||
( | ||||
b'u', | ||||
b'user', | ||||
b'', | ||||
_(b'record the specified user as committer'), | ||||
_(b'USER'), | ||||
), | ||||
|
r32375 | ] | ||
|
r43173 | commitopts3 = [ | ||
|
r43346 | (b'D', b'currentdate', None, _(b'record the current date as commit date')), | ||
(b'U', b'currentuser', None, _(b'record the current user as committer')), | ||||
|
r43173 | ] | ||
|
r32375 | formatteropts = [ | ||
|
r43347 | (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')), | ||
|
r32375 | ] | ||
templateopts = [ | ||||
|
r43346 | ( | ||
|
r43347 | b'', | ||
b'style', | ||||
b'', | ||||
_(b'display using template map file (DEPRECATED)'), | ||||
_(b'STYLE'), | ||||
|
r43346 | ), | ||
|
r43347 | (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')), | ||
|
r32375 | ] | ||
logopts = [ | ||||
|
r43347 | (b'p', b'patch', None, _(b'show patch')), | ||
(b'g', b'git', None, _(b'use git extended diff format')), | ||||
(b'l', b'limit', b'', _(b'limit number of changes displayed'), _(b'NUM')), | ||||
(b'M', b'no-merges', None, _(b'do not show merges')), | ||||
(b'', b'stat', None, _(b'output diffstat-style summary of changes')), | ||||
(b'G', b'graph', None, _(b"show the revision DAG")), | ||||
|
r32375 | ] + templateopts | ||
diffopts = [ | ||||
|
r43347 | (b'a', b'text', None, _(b'treat all files as text')), | ||
|
r44787 | ( | ||
b'g', | ||||
b'git', | ||||
None, | ||||
_(b'use git extended diff format (DEFAULT: diff.git)'), | ||||
), | ||||
|
r43347 | (b'', b'binary', None, _(b'generate binary diffs in git mode (default)')), | ||
(b'', b'nodates', None, _(b'omit dates from diff headers')), | ||||
|
r32375 | ] | ||
diffwsopts = [ | ||||
|
r43346 | ( | ||
|
r43347 | b'w', | ||
b'ignore-all-space', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'ignore white space when comparing lines'), | ||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'b', | ||
b'ignore-space-change', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'ignore changes in the amount of white space'), | ||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'B', | ||
b'ignore-blank-lines', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'ignore changes whose lines are all blank'), | ||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'Z', | ||
b'ignore-space-at-eol', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'ignore changes in whitespace at EOL'), | ||
|
r43346 | ), | ||
|
r32375 | ] | ||
|
r43346 | diffopts2 = ( | ||
[ | ||||
|
r43347 | (b'', b'noprefix', None, _(b'omit a/ and b/ prefixes from filenames')), | ||
|
r43346 | ( | ||
|
r43347 | b'p', | ||
b'show-function', | ||||
|
r43346 | None, | ||
|
r44787 | _( | ||
b'show which function each change is in (DEFAULT: diff.showfunc)' | ||||
), | ||||
|
r43346 | ), | ||
|
r43347 | (b'', b'reverse', None, _(b'produce a diff that undoes the changes')), | ||
|
r43346 | ] | ||
+ diffwsopts | ||||
+ [ | ||||
|
r43347 | ( | ||
b'U', | ||||
b'unified', | ||||
b'', | ||||
_(b'number of lines of context to show'), | ||||
_(b'NUM'), | ||||
), | ||||
(b'', b'stat', None, _(b'output diffstat-style summary of changes')), | ||||
( | ||||
b'', | ||||
b'root', | ||||
b'', | ||||
_(b'produce diffs relative to subdirectory'), | ||||
_(b'DIR'), | ||||
), | ||||
|
r43346 | ] | ||
) | ||||
|
r32375 | |||
mergetoolopts = [ | ||||
|
r43347 | (b't', b'tool', b'', _(b'specify merge tool'), _(b'TOOL')), | ||
|
r32375 | ] | ||
similarityopts = [ | ||||
|
r43346 | ( | ||
|
r43347 | b's', | ||
b'similarity', | ||||
b'', | ||||
_(b'guess renamed files by similarity (0<=s<=100)'), | ||||
_(b'SIMILARITY'), | ||||
|
r43346 | ) | ||
|
r32375 | ] | ||
|
r43347 | subrepoopts = [(b'S', b'subrepos', None, _(b'recurse into subrepositories'))] | ||
|
r32375 | |||
debugrevlogopts = [ | ||||
|
r43347 | (b'c', b'changelog', False, _(b'open changelog')), | ||
(b'm', b'manifest', False, _(b'open manifest')), | ||||
(b'', b'dir', b'', _(b'open directory manifest')), | ||||
|
r32375 | ] | ||
|
r30703 | # special string such that everything below this line will be ingored in the | ||
# editor text | ||||
|
r43347 | _linebelow = b"^HG: ------------------------ >8 ------------------------$" | ||
|
r30703 | |||
|
r43346 | |||
|
r44344 | def check_at_most_one_arg(opts, *args): | ||
|
r44351 | """abort if more than one of the arguments are in opts | ||
Returns the unique argument or None if none of them were specified. | ||||
""" | ||||
|
r44394 | |||
def to_display(name): | ||||
|
r44395 | return pycompat.sysbytes(name).replace(b'_', b'-') | ||
|
r44394 | |||
|
r44344 | previous = None | ||
for x in args: | ||||
if opts.get(x): | ||||
if previous: | ||||
|
r46431 | raise error.InputError( | ||
|
r44394 | _(b'cannot specify both --%s and --%s') | ||
% (to_display(previous), to_display(x)) | ||||
|
r44344 | ) | ||
previous = x | ||||
|
r44351 | return previous | ||
|
r44344 | |||
|
r44655 | def check_incompatible_arguments(opts, first, others): | ||
|
r44350 | """abort if the first argument is given along with any of the others | ||
Unlike check_at_most_one_arg(), `others` are not mutually exclusive | ||||
|
r44655 | among themselves, and they're passed as a single collection. | ||
|
r44350 | """ | ||
for other in others: | ||||
check_at_most_one_arg(opts, first, other) | ||||
|
r48225 | def resolve_commit_options(ui, opts): | ||
|
r43173 | """modify commit options dict to handle related options | ||
|
r43202 | |||
The return value indicates that ``rewrite.update-timestamp`` is the reason | ||||
the ``date`` option is set. | ||||
|
r43173 | """ | ||
|
r48225 | check_at_most_one_arg(opts, 'date', 'currentdate') | ||
check_at_most_one_arg(opts, 'user', 'currentuser') | ||||
|
r43191 | |||
|
r43202 | datemaydiffer = False # date-only change should be ignored? | ||
|
r48225 | if opts.get('currentdate'): | ||
opts['date'] = b'%d %d' % dateutil.makedate() | ||||
|
r43346 | elif ( | ||
|
r48225 | not opts.get('date') | ||
|
r43347 | and ui.configbool(b'rewrite', b'update-timestamp') | ||
|
r48225 | and opts.get('currentdate') is None | ||
|
r43346 | ): | ||
|
r48225 | opts['date'] = b'%d %d' % dateutil.makedate() | ||
|
r43202 | datemaydiffer = True | ||
|
r48225 | if opts.get('currentuser'): | ||
opts['user'] = ui.username() | ||||
|
r43173 | |||
|
r43202 | return datemaydiffer | ||
|
r43346 | |||
|
r48221 | def check_note_size(opts): | ||
|
r47856 | """make sure note is of valid format""" | ||
|
r43203 | |||
|
r48221 | note = opts.get('note') | ||
|
r43203 | if not note: | ||
return | ||||
if len(note) > 255: | ||||
|
r46431 | raise error.InputError(_(b"cannot store a note of more than 255 bytes")) | ||
|
r43203 | if b'\n' in note: | ||
|
r46431 | raise error.InputError(_(b"note cannot contain a newline")) | ||
|
r43203 | |||
|
r43346 | |||
|
r25256 | def ishunk(x): | ||
hunkclasses = (crecordmod.uihunk, patch.recordhunk) | ||||
return isinstance(x, hunkclasses) | ||||
|
r43346 | |||
|
r48365 | def isheader(x): | ||
headerclasses = (crecordmod.uiheader, patch.header) | ||||
return isinstance(x, headerclasses) | ||||
def newandmodified(chunks): | ||||
|
r25257 | newlyaddedandmodifiedfiles = set() | ||
|
r43122 | alsorestore = set() | ||
|
r25257 | for chunk in chunks: | ||
|
r48365 | if isheader(chunk) and chunk.isnewfile(): | ||
newlyaddedandmodifiedfiles.add(chunk.filename()) | ||||
alsorestore.update(set(chunk.files()) - {chunk.filename()}) | ||||
|
r43122 | return newlyaddedandmodifiedfiles, alsorestore | ||
|
r25257 | |||
|
r43346 | |||
|
r10401 | def parsealiases(cmd): | ||
r46858 | base_aliases = cmd.split(b"|") | |||
all_aliases = set(base_aliases) | ||||
extra_aliases = [] | ||||
for alias in base_aliases: | ||||
if b'-' in alias: | ||||
folded_alias = alias.replace(b'-', b'') | ||||
if folded_alias not in all_aliases: | ||||
all_aliases.add(folded_alias) | ||||
extra_aliases.append(folded_alias) | ||||
base_aliases.extend(extra_aliases) | ||||
return base_aliases | ||||
|
r10401 | |||
|
r43346 | |||
|
r24356 | def setupwrapcolorwrite(ui): | ||
# wrap ui.write so diff output can be labeled/colorized | ||||
def wrapwrite(orig, *args, **kw): | ||||
|
r43906 | label = kw.pop('label', b'') | ||
|
r24356 | for chunk, l in patch.difflabel(lambda: args): | ||
orig(chunk, label=label + l) | ||||
oldwrite = ui.write | ||||
|
r43346 | |||
|
r24356 | def wrap(*args, **kwargs): | ||
return wrapwrite(oldwrite, *args, **kwargs) | ||||
|
r43346 | |||
|
r24356 | setattr(ui, 'write', wrap) | ||
return oldwrite | ||||
|
r43346 | |||
def filterchunks(ui, originalhunks, usecurses, testfile, match, operation=None): | ||||
|
r38058 | try: | ||
if usecurses: | ||||
if testfile: | ||||
recordfn = crecordmod.testdecorator( | ||||
|
r43346 | testfile, crecordmod.testchunkselector | ||
) | ||||
|
r38058 | else: | ||
recordfn = crecordmod.chunkselector | ||||
|
r43346 | return crecordmod.filterpatch( | ||
ui, originalhunks, recordfn, operation | ||||
) | ||||
|
r38058 | except crecordmod.fallbackerror as e: | ||
|
r44557 | ui.warn(b'%s\n' % e) | ||
|
r43347 | ui.warn(_(b'falling back to text mode\n')) | ||
|
r38058 | |||
|
r42238 | return patch.filterpatch(ui, originalhunks, match, operation) | ||
|
r43346 | |||
|
r42238 | def recordfilter(ui, originalhunks, match, operation=None): | ||
|
r46554 | """Prompts the user to filter the originalhunks and return a list of | ||
|
r25310 | selected hunks. | ||
|
r29326 | *operation* is used for to build ui messages to indicate the user what | ||
kind of filtering they are doing: reverting, committing, shelving, etc. | ||||
(see patch.filterpatch). | ||||
|
r25310 | """ | ||
|
r27531 | usecurses = crecordmod.checkcurses(ui) | ||
|
r43347 | testfile = ui.config(b'experimental', b'crecordtest') | ||
|
r24358 | oldwrite = setupwrapcolorwrite(ui) | ||
try: | ||||
|
r43346 | newchunks, newopts = filterchunks( | ||
ui, originalhunks, usecurses, testfile, match, operation | ||||
) | ||||
|
r24358 | finally: | ||
ui.write = oldwrite | ||||
|
r27155 | return newchunks, newopts | ||
|
r24357 | |||
|
r43346 | |||
def dorecord( | ||||
ui, repo, commitfunc, cmdsuggest, backupall, filterfn, *pats, **opts | ||||
): | ||||
|
r32144 | opts = pycompat.byteskwargs(opts) | ||
|
r24272 | if not ui.interactive(): | ||
|
r25795 | if cmdsuggest: | ||
|
r43347 | msg = _(b'running non-interactively, use %s instead') % cmdsuggest | ||
|
r25795 | else: | ||
|
r43347 | msg = _(b'running non-interactively') | ||
|
r46431 | raise error.InputError(msg) | ||
|
r24272 | |||
# make sure username is set before going interactive | ||||
|
r43347 | if not opts.get(b'user'): | ||
|
r43346 | ui.username() # raise exception, username not provided | ||
|
r24272 | |||
def recordfunc(ui, repo, message, match, opts): | ||||
"""This is generic record driver. | ||||
Its job is to interactively filter local changes, and | ||||
accordingly prepare working directory into a state in which the | ||||
job can be delegated to a non-interactive commit command such as | ||||
'commit' or 'qrefresh'. | ||||
After the actual job is done by non-interactive command, the | ||||
working directory is restored to its original state. | ||||
In the end we'll record interesting changes, and everything else | ||||
will be left in place, so the user can continue working. | ||||
""" | ||||
|
r43347 | if not opts.get(b'interactive-unshelve'): | ||
|
r42835 | checkunfinished(repo, commit=True) | ||
|
r28815 | wctx = repo[None] | ||
merge = len(wctx.parents()) > 1 | ||||
|
r24272 | if merge: | ||
|
r46431 | raise error.InputError( | ||
|
r43346 | _( | ||
|
r43347 | b'cannot partially commit a merge ' | ||
b'(use "hg commit" instead)' | ||||
|
r43346 | ) | ||
) | ||||
|
r24272 | |||
|
r42444 | def fail(f, msg): | ||
|
r46450 | raise error.InputError(b'%s: %s' % (f, msg)) | ||
|
r43347 | |||
force = opts.get(b'force') | ||||
|
r42444 | if not force: | ||
|
r42456 | match = matchmod.badmatch(match, fail) | ||
|
r42444 | |||
|
r42143 | status = repo.status(match=match) | ||
overrides = {(b'ui', b'commitsubrepos'): True} | ||||
with repo.ui.configoverride(overrides, b'record'): | ||||
# subrepoutil.precommit() modifies the status | ||||
|
r43346 | tmpstatus = scmutil.status( | ||
|
r44043 | copymod.copy(status.modified), | ||
copymod.copy(status.added), | ||||
copymod.copy(status.removed), | ||||
copymod.copy(status.deleted), | ||||
copymod.copy(status.unknown), | ||||
copymod.copy(status.ignored), | ||||
copymod.copy(status.clean), # pytype: disable=wrong-arg-count | ||||
|
r43346 | ) | ||
|
r42143 | |||
# Force allows -X subrepo to skip the subrepo. | ||||
subs, commitsubs, newstate = subrepoutil.precommit( | ||||
|
r43346 | repo.ui, wctx, tmpstatus, match, force=True | ||
) | ||||
|
r42143 | for s in subs: | ||
if s in commitsubs: | ||||
dirtyreason = wctx.sub(s).dirtyreason(True) | ||||
raise error.Abort(dirtyreason) | ||||
|
r28815 | if not force: | ||
|
r44111 | repo.checkcommitpatterns(wctx, match, status, fail) | ||
|
r43346 | diffopts = patch.difffeatureopts( | ||
ui, | ||||
opts=opts, | ||||
whitespace=True, | ||||
|
r43347 | section=b'commands', | ||
configprefix=b'commit.interactive.', | ||||
|
r43346 | ) | ||
|
r24272 | diffopts.nodates = True | ||
diffopts.git = True | ||||
|
r27411 | diffopts.showfunc = True | ||
|
r27637 | originaldiff = patch.diff(repo, changes=status, opts=diffopts) | ||
|
r48365 | original_headers = patch.parsepatch(originaldiff) | ||
|
r42238 | match = scmutil.match(repo[None], pats) | ||
|
r24272 | |||
|
r28570 | # 1. filter patch, since we are intending to apply subset of it | ||
|
r24272 | try: | ||
|
r48365 | chunks, newopts = filterfn(ui, original_headers, match) | ||
|
r34252 | except error.PatchError as err: | ||
|
r46431 | raise error.InputError(_(b'error parsing patch: %s') % err) | ||
|
r27155 | opts.update(newopts) | ||
|
r24272 | |||
|
r24845 | # We need to keep a backup of files that have been newly added and | ||
# modified during the recording process because there is a previous | ||||
|
r43122 | # version without the edit in the workdir. We also will need to restore | ||
# files that were the sources of renames so that the patch application | ||||
# works. | ||||
|
r48365 | newlyaddedandmodifiedfiles, alsorestore = newandmodified(chunks) | ||
|
r24272 | contenders = set() | ||
for h in chunks: | ||||
|
r48365 | if isheader(h): | ||
|
r24272 | contenders.update(set(h.files())) | ||
changed = status.modified + status.added + status.removed | ||||
newfiles = [f for f in changed if f in contenders] | ||||
if not newfiles: | ||||
|
r43347 | ui.status(_(b'no changes to record\n')) | ||
|
r24272 | return 0 | ||
modified = set(status.modified) | ||||
# 2. backup changed files, so we can restore them in the end | ||||
if backupall: | ||||
tobackup = changed | ||||
else: | ||||
|
r43346 | tobackup = [ | ||
f | ||||
for f in newfiles | ||||
if f in modified or f in newlyaddedandmodifiedfiles | ||||
] | ||||
|
r24272 | backups = {} | ||
if tobackup: | ||||
|
r43347 | backupdir = repo.vfs.join(b'record-backups') | ||
|
r24272 | try: | ||
os.mkdir(backupdir) | ||||
|
r25660 | except OSError as err: | ||
|
r24272 | if err.errno != errno.EEXIST: | ||
raise | ||||
try: | ||||
# backup continues | ||||
for f in tobackup: | ||||
|
r43346 | fd, tmpname = pycompat.mkstemp( | ||
|
r46308 | prefix=os.path.basename(f) + b'.', dir=backupdir | ||
|
r43346 | ) | ||
|
r24272 | os.close(fd) | ||
|
r43347 | ui.debug(b'backup %r as %r\n' % (f, tmpname)) | ||
|
r27370 | util.copyfile(repo.wjoin(f), tmpname, copystat=True) | ||
|
r24272 | backups[f] = tmpname | ||
|
r28861 | fp = stringio() | ||
|
r24272 | for c in chunks: | ||
fname = c.filename() | ||||
|
r24837 | if fname in backups: | ||
|
r24272 | c.write(fp) | ||
dopatch = fp.tell() | ||||
fp.seek(0) | ||||
|
r28638 | # 2.5 optionally review / modify patch in text editor | ||
|
r43347 | if opts.get(b'review', False): | ||
|
r43346 | patchtext = ( | ||
crecordmod.diffhelptext | ||||
+ crecordmod.patchhelptext | ||||
+ fp.read() | ||||
) | ||||
reviewedpatch = ui.edit( | ||||
|
r43347 | patchtext, b"", action=b"diff", repopath=repo.path | ||
|
r43346 | ) | ||
|
r28638 | fp.truncate(0) | ||
fp.write(reviewedpatch) | ||||
fp.seek(0) | ||||
|
r24866 | [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles] | ||
|
r24272 | # 3a. apply filtered patch to clean repo (clean) | ||
if backups: | ||||
|
r43122 | m = scmutil.matchfiles(repo, set(backups.keys()) | alsorestore) | ||
|
r44744 | mergemod.revert_to(repo[b'.'], matcher=m) | ||
|
r24272 | |||
# 3b. (apply) | ||||
if dopatch: | ||||
try: | ||||
|
r43347 | ui.debug(b'applying patch\n') | ||
|
r24272 | ui.debug(fp.getvalue()) | ||
patch.internalpatch(ui, repo, fp, 1, eolmode=None) | ||||
|
r34252 | except error.PatchError as err: | ||
|
r46431 | raise error.InputError(pycompat.bytestr(err)) | ||
|
r24272 | del fp | ||
# 4. We prepared working directory according to filtered | ||||
# patch. Now is the time to delegate the job to | ||||
# commit/qrefresh or the like! | ||||
# Make all of the pathnames absolute. | ||||
newfiles = [repo.wjoin(nf) for nf in newfiles] | ||||
|
r35351 | return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts)) | ||
|
r24272 | finally: | ||
# 5. finally restore backed-up files | ||||
try: | ||||
|
r25759 | dirstate = repo.dirstate | ||
|
r43376 | for realname, tmpname in pycompat.iteritems(backups): | ||
|
r43347 | ui.debug(b'restoring %r to %r\n' % (tmpname, realname)) | ||
if dirstate[realname] == b'n': | ||||
|
r25759 | # without normallookup, restoring timestamp | ||
# may cause partially committed files | ||||
# to be treated as unmodified | ||||
dirstate.normallookup(realname) | ||||
|
r27370 | # copystat=True here and above are a hack to trick any | ||
# editors that have f open that we haven't modified them. | ||||
|
r24272 | # | ||
|
r27370 | # Also note that this racy as an editor could notice the | ||
# file's mtime before we've finished writing it. | ||||
util.copyfile(tmpname, repo.wjoin(realname), copystat=True) | ||||
|
r24272 | os.unlink(tmpname) | ||
if tobackup: | ||||
os.rmdir(backupdir) | ||||
except OSError: | ||||
pass | ||||
|
r25758 | def recordinwlock(ui, repo, message, match, opts): | ||
|
r27801 | with repo.wlock(): | ||
|
r25758 | return recordfunc(ui, repo, message, match, opts) | ||
return commit(ui, repo, recordinwlock, pats, opts) | ||||
|
r24272 | |||
|
r43346 | |||
|
r34683 | class dirnode(object): | ||
|
r33548 | """ | ||
|
r34699 | Represent a directory in user working copy with information required for | ||
the purpose of tersing its status. | ||||
|
r34683 | |||
|
r38221 | path is the path to the directory, without a trailing '/' | ||
|
r34683 | |||
statuses is a set of statuses of all files in this directory (this includes | ||||
all the files in all the subdirectories too) | ||||
files is a list of files which are direct child of this directory | ||||
subdirs is a dictionary of sub-directory name as the key and it's own | ||||
dirnode object as the value | ||||
""" | ||||
def __init__(self, dirpath): | ||||
self.path = dirpath | ||||
|
r42224 | self.statuses = set() | ||
|
r34683 | self.files = [] | ||
self.subdirs = {} | ||||
def _addfileindir(self, filename, status): | ||||
|
r34699 | """Add a file in this directory as a direct child.""" | ||
|
r34683 | self.files.append((filename, status)) | ||
def addfile(self, filename, status): | ||||
|
r33548 | """ | ||
|
r34699 | Add a file to this directory or to its direct parent directory. | ||
If the file is not direct child of this directory, we traverse to the | ||||
directory of which this file is a direct child of and add the file | ||||
there. | ||||
|
r33548 | """ | ||
|
r34683 | |||
# the filename contains a path separator, it means it's not the direct | ||||
# child of this directory | ||||
|
r43347 | if b'/' in filename: | ||
subdir, filep = filename.split(b'/', 1) | ||||
|
r34683 | |||
# does the dirnode object for subdir exists | ||||
if subdir not in self.subdirs: | ||||
|
r38221 | subdirpath = pathutil.join(self.path, subdir) | ||
|
r34683 | self.subdirs[subdir] = dirnode(subdirpath) | ||
# try adding the file in subdir | ||||
self.subdirs[subdir].addfile(filep, status) | ||||
else: | ||||
self._addfileindir(filename, status) | ||||
if status not in self.statuses: | ||||
self.statuses.add(status) | ||||
|
r34685 | def iterfilepaths(self): | ||
|
r34699 | """Yield (status, path) for files directly under this directory.""" | ||
|
r34684 | for f, st in self.files: | ||
|
r38221 | yield st, pathutil.join(self.path, f) | ||
|
r34685 | |||
def tersewalk(self, terseargs): | ||||
|
r34684 | """ | ||
|
r34699 | Yield (status, path) obtained by processing the status of this | ||
dirnode. | ||||
|
r34684 | |||
terseargs is the string of arguments passed by the user with `--terse` | ||||
flag. | ||||
Following are the cases which can happen: | ||||
1) All the files in the directory (including all the files in its | ||||
subdirectories) share the same status and the user has asked us to terse | ||||
|
r38221 | that status. -> yield (status, dirpath). dirpath will end in '/'. | ||
|
r34684 | |||
|
r34699 | 2) Otherwise, we do following: | ||
|
r34684 | |||
|
r34685 | a) Yield (status, filepath) for all the files which are in this | ||
directory (only the ones in this directory, not the subdirs) | ||||
|
r34684 | |||
b) Recurse the function on all the subdirectories of this | ||||
directory | ||||
""" | ||||
if len(self.statuses) == 1: | ||||
onlyst = self.statuses.pop() | ||||
# Making sure we terse only when the status abbreviation is | ||||
# passed as terse argument | ||||
if onlyst in terseargs: | ||||
|
r43347 | yield onlyst, self.path + b'/' | ||
|
r34684 | return | ||
# add the files to status list | ||||
|
r34685 | for st, fpath in self.iterfilepaths(): | ||
yield st, fpath | ||||
|
r34684 | |||
|
r43346 | # recurse on the subdirs | ||
|
r34684 | for dirobj in self.subdirs.values(): | ||
|
r34685 | for st, fpath in dirobj.tersewalk(terseargs): | ||
yield st, fpath | ||||
|
r34683 | |||
|
r43346 | |||
|
r34683 | def tersedir(statuslist, terseargs): | ||
""" | ||||
|
r34699 | Terse the status if all the files in a directory shares the same status. | ||
|
r34683 | |||
statuslist is scmutil.status() object which contains a list of files for | ||||
each status. | ||||
terseargs is string which is passed by the user as the argument to `--terse` | ||||
flag. | ||||
The function makes a tree of objects of dirnode class, and at each node it | ||||
stores the information required to know whether we can terse a certain | ||||
directory or not. | ||||
""" | ||||
# the order matters here as that is used to produce final list | ||||
|
r43347 | allst = (b'm', b'a', b'r', b'd', b'u', b'i', b'c') | ||
|
r34683 | |||
# checking the argument validity | ||||
|
r34894 | for s in pycompat.bytestr(terseargs): | ||
|
r34683 | if s not in allst: | ||
|
r46431 | raise error.InputError(_(b"'%s' not recognized") % s) | ||
|
r34683 | |||
# creating a dirnode object for the root of the repo | ||||
|
r43347 | rootobj = dirnode(b'') | ||
|
r43346 | pstatus = ( | ||
|
r43347 | b'modified', | ||
b'added', | ||||
b'deleted', | ||||
b'clean', | ||||
b'unknown', | ||||
b'ignored', | ||||
b'removed', | ||||
|
r43346 | ) | ||
|
r34683 | |||
tersedict = {} | ||||
for attrname in pstatus: | ||||
|
r34894 | statuschar = attrname[0:1] | ||
|
r34683 | for f in getattr(statuslist, attrname): | ||
|
r34894 | rootobj.addfile(f, statuschar) | ||
tersedict[statuschar] = [] | ||||
|
r34683 | |||
# we won't be tersing the root dir, so add files in it | ||||
|
r34685 | for st, fpath in rootobj.iterfilepaths(): | ||
tersedict[st].append(fpath) | ||||
|
r34683 | |||
# process each sub-directory and build tersedict | ||||
for subdir in rootobj.subdirs.values(): | ||||
|
r34685 | for st, f in subdir.tersewalk(terseargs): | ||
tersedict[st].append(f) | ||||
|
r34683 | |||
tersedlist = [] | ||||
for st in allst: | ||||
tersedict[st].sort() | ||||
tersedlist.append(tersedict[st]) | ||||
|
r44044 | return scmutil.status(*tersedlist) | ||
|
r33548 | |||
|
r43346 | |||
|
r33766 | def _commentlines(raw): | ||
'''Surround lineswith a comment char and a new line''' | ||||
lines = raw.splitlines() | ||||
|
r43347 | commentedlines = [b'# %s' % line for line in lines] | ||
return b'\n'.join(commentedlines) + b'\n' | ||||
|
r33766 | |||
|
r43346 | |||
|
r44293 | @attr.s(frozen=True) | ||
class morestatus(object): | ||||
reporoot = attr.ib() | ||||
unfinishedop = attr.ib() | ||||
unfinishedmsg = attr.ib() | ||||
|
r44390 | activemerge = attr.ib() | ||
|
r44293 | unresolvedpaths = attr.ib() | ||
|
r44392 | _formattedpaths = attr.ib(init=False, default=set()) | ||
|
r44293 | _label = b'status.morestatus' | ||
|
r44294 | def formatfile(self, path, fm): | ||
|
r44392 | self._formattedpaths.add(path) | ||
|
r44390 | if self.activemerge and path in self.unresolvedpaths: | ||
|
r44294 | fm.data(unresolved=True) | ||
|
r44293 | def formatfooter(self, fm): | ||
|
r44391 | if self.unfinishedop or self.unfinishedmsg: | ||
fm.startitem() | ||||
fm.data(itemtype=b'morestatus') | ||||
if self.unfinishedop: | ||||
fm.data(unfinished=self.unfinishedop) | ||||
statemsg = ( | ||||
_(b'The repository is in an unfinished *%s* state.') | ||||
% self.unfinishedop | ||||
) | ||||
fm.plain(b'%s\n' % _commentlines(statemsg), label=self._label) | ||||
if self.unfinishedmsg: | ||||
fm.data(unfinishedmsg=self.unfinishedmsg) | ||||
|
r44293 | |||
|
r44392 | # May also start new data items. | ||
|
r44293 | self._formatconflicts(fm) | ||
|
r44391 | |||
|
r44293 | if self.unfinishedmsg: | ||
|
r44307 | fm.plain( | ||
b'%s\n' % _commentlines(self.unfinishedmsg), label=self._label | ||||
) | ||||
|
r44293 | |||
def _formatconflicts(self, fm): | ||||
|
r44390 | if not self.activemerge: | ||
|
r44293 | return | ||
if self.unresolvedpaths: | ||||
mergeliststr = b'\n'.join( | ||||
[ | ||||
|
r44307 | b' %s' | ||
% util.pathto(self.reporoot, encoding.getcwd(), path) | ||||
|
r44293 | for path in self.unresolvedpaths | ||
] | ||||
) | ||||
msg = ( | ||||
_( | ||||
|
r47514 | b'''Unresolved merge conflicts: | ||
|
r33766 | |||
%s | ||||
|
r43346 | To mark files as resolved: hg resolve --mark FILE''' | ||
|
r44293 | ) | ||
% mergeliststr | ||||
|
r43346 | ) | ||
|
r44392 | |||
# If any paths with unresolved conflicts were not previously | ||||
# formatted, output them now. | ||||
for f in self.unresolvedpaths: | ||||
if f in self._formattedpaths: | ||||
# Already output. | ||||
continue | ||||
fm.startitem() | ||||
# We can't claim to know the status of the file - it may just | ||||
# have been in one of the states that were not requested for | ||||
# display, so it could be anything. | ||||
fm.data(itemtype=b'file', path=f, unresolved=True) | ||||
|
r44293 | else: | ||
msg = _(b'No unresolved merge conflicts.') | ||||
fm.plain(b'%s\n' % _commentlines(msg), label=self._label) | ||||
def readmorestatus(repo): | ||||
"""Returns a morestatus object if the repo has unfinished state.""" | ||||
|
r42731 | statetuple = statemod.getrepostate(repo) | ||
|
r45383 | mergestate = mergestatemod.mergestate.read(repo) | ||
|
r44390 | activemerge = mergestate.active() | ||
|
r44391 | if not statetuple and not activemerge: | ||
|
r44293 | return None | ||
|
r44391 | unfinishedop = unfinishedmsg = unresolved = None | ||
if statetuple: | ||||
unfinishedop, unfinishedmsg = statetuple | ||||
|
r44390 | if activemerge: | ||
|
r44293 | unresolved = sorted(mergestate.unresolved()) | ||
|
r44307 | return morestatus( | ||
|
r44390 | repo.root, unfinishedop, unfinishedmsg, activemerge, unresolved | ||
|
r44307 | ) | ||
|
r33766 | |||
|
r43346 | |||
|
r7213 | def findpossible(cmd, table, strict=False): | ||
|
r4549 | """ | ||
Return cmd -> (aliases, command table entry) | ||||
for each matching command. | ||||
Return debug commands (or their aliases) only if no normal command matches. | ||||
""" | ||||
choice = {} | ||||
debugchoice = {} | ||||
|
r15600 | |||
if cmd in table: | ||||
|
r40331 | # short-circuit exact matches, "log" alias beats "log|history" | ||
|
r15600 | keys = [cmd] | ||
else: | ||||
keys = table.keys() | ||||
|
r24222 | allcmds = [] | ||
|
r15600 | for e in keys: | ||
|
r10401 | aliases = parsealiases(e) | ||
|
r24222 | allcmds.extend(aliases) | ||
|
r4549 | found = None | ||
if cmd in aliases: | ||||
found = cmd | ||||
|
r7213 | elif not strict: | ||
|
r4549 | for a in aliases: | ||
if a.startswith(cmd): | ||||
found = a | ||||
break | ||||
if found is not None: | ||||
|
r43347 | if aliases[0].startswith(b"debug") or found.startswith(b"debug"): | ||
|
r5178 | debugchoice[found] = (aliases, table[e]) | ||
|
r4549 | else: | ||
|
r5178 | choice[found] = (aliases, table[e]) | ||
|
r4549 | |||
if not choice and debugchoice: | ||||
choice = debugchoice | ||||
|
r24222 | return choice, allcmds | ||
|
r4549 | |||
|
r43346 | |||
|
r7213 | def findcmd(cmd, table, strict=True): | ||
|
r4549 | """Return (aliases, command table entry) for command string.""" | ||
|
r24222 | choice, allcmds = findpossible(cmd, table, strict) | ||
|
r4549 | |||
|
r5915 | if cmd in choice: | ||
|
r4549 | return choice[cmd] | ||
if len(choice) > 1: | ||||
|
r32528 | clist = sorted(choice) | ||
|
r7643 | raise error.AmbiguousCommand(cmd, clist) | ||
|
r4549 | |||
if choice: | ||||
|
r32862 | return list(choice.values())[0] | ||
|
r4549 | |||
|
r24222 | raise error.UnknownCommand(cmd, allcmds) | ||
|
r4549 | |||
|
r43346 | |||
|
r44891 | def changebranch(ui, repo, revs, label, opts): | ||
|
r47856 | """Change the branch name of given revs to label""" | ||
|
r35763 | |||
|
r43347 | with repo.wlock(), repo.lock(), repo.transaction(b'branches'): | ||
|
r35763 | # abort in case of uncommitted merge or dirty wdir | ||
bailifchanged(repo) | ||||
revs = scmutil.revrange(repo, revs) | ||||
if not revs: | ||||
|
r46431 | raise error.InputError(b"empty revision set") | ||
|
r43347 | roots = repo.revs(b'roots(%ld)', revs) | ||
|
r35763 | if len(roots) > 1: | ||
|
r46431 | raise error.InputError( | ||
|
r43347 | _(b"cannot change branch of non-linear revisions") | ||
) | ||||
rewriteutil.precheck(repo, revs, b'change branch of') | ||||
|
r35764 | |||
root = repo[roots.first()] | ||||
r40702 | rpb = {parent.branch() for parent in root.parents()} | |||
|
r44891 | if ( | ||
not opts.get(b'force') | ||||
and label not in rpb | ||||
and label in repo.branchmap() | ||||
): | ||||
|
r46431 | raise error.InputError( | ||
_(b"a branch of the same name already exists") | ||||
) | ||||
|
r43347 | |||
|
r35763 | # make sure only topological heads | ||
|
r43347 | if repo.revs(b'heads(%ld) - head()', revs): | ||
|
r46431 | raise error.InputError( | ||
_(b"cannot change branch in middle of a stack") | ||||
) | ||||
|
r35763 | |||
replacements = {} | ||||
# avoid import cycle mercurial.cmdutil -> mercurial.context -> | ||||
# mercurial.subrepo -> mercurial.cmdutil | ||||
from . import context | ||||
|
r43346 | |||
|
r35763 | for rev in revs: | ||
ctx = repo[rev] | ||||
oldbranch = ctx.branch() | ||||
# check if ctx has same branch | ||||
if oldbranch == label: | ||||
continue | ||||
def filectxfn(repo, newctx, path): | ||||
try: | ||||
return ctx[path] | ||||
except error.ManifestLookupError: | ||||
return None | ||||
|
r43346 | ui.debug( | ||
|
r43347 | b"changing branch of '%s' from '%s' to '%s'\n" | ||
|
r43346 | % (hex(ctx.node()), oldbranch, label) | ||
) | ||||
|
r35763 | extra = ctx.extra() | ||
|
r43347 | extra[b'branch_change'] = hex(ctx.node()) | ||
|
r35763 | # While changing branch of set of linear commits, make sure that | ||
# we base our commits on new parent rather than old parent which | ||||
# was obsoleted while changing the branch | ||||
p1 = ctx.p1().node() | ||||
p2 = ctx.p2().node() | ||||
if p1 in replacements: | ||||
p1 = replacements[p1][0] | ||||
if p2 in replacements: | ||||
p2 = replacements[p2][0] | ||||
|
r43346 | mc = context.memctx( | ||
repo, | ||||
(p1, p2), | ||||
ctx.description(), | ||||
ctx.files(), | ||||
filectxfn, | ||||
user=ctx.user(), | ||||
date=ctx.date(), | ||||
extra=extra, | ||||
branch=label, | ||||
) | ||||
|
r35763 | |||
|
r38442 | newnode = repo.commitctx(mc) | ||
|
r35763 | replacements[ctx.node()] = (newnode,) | ||
|
r43347 | ui.debug(b'new node id is %s\n' % hex(newnode)) | ||
|
r35763 | |||
# create obsmarkers and move bookmarks | ||||
|
r43347 | scmutil.cleanupnodes( | ||
repo, replacements, b'branch-change', fixphase=True | ||||
) | ||||
|
r35763 | |||
# move the working copy too | ||||
wctx = repo[None] | ||||
# in-progress merge is a bit too complex for now. | ||||
if len(wctx.parents()) == 1: | ||||
newid = replacements.get(wctx.p1().node()) | ||||
if newid is not None: | ||||
# avoid import cycle mercurial.cmdutil -> mercurial.hg -> | ||||
# mercurial.cmdutil | ||||
from . import hg | ||||
|
r43346 | |||
|
r35763 | hg.update(repo, newid[0], quietempty=True) | ||
|
r43347 | ui.status(_(b"changed branch on %d changesets\n") % len(replacements)) | ||
|
r35763 | |||
|
r43346 | |||
|
r10402 | def findrepo(p): | ||
|
r43347 | while not os.path.isdir(os.path.join(p, b".hg")): | ||
|
r10402 | oldp, p = p, os.path.dirname(p) | ||
if p == oldp: | ||||
return None | ||||
return p | ||||
|
r43346 | |||
|
r30755 | def bailifchanged(repo, merge=True, hint=None): | ||
|
r46554 | """enforce the precondition that working directory must be clean. | ||
|
r30755 | |||
'merge' can be set to false if a pending uncommitted merge should be | ||||
ignored (such as when 'update --check' runs). | ||||
'hint' is the usual hint given to Abort exception. | ||||
""" | ||||
|
r47771 | if merge and repo.dirstate.p2() != repo.nullid: | ||
|
r46444 | raise error.StateError(_(b'outstanding uncommitted merge'), hint=hint) | ||
|
r44043 | st = repo.status() | ||
if st.modified or st.added or st.removed or st.deleted: | ||||
|
r46444 | raise error.StateError(_(b'uncommitted changes'), hint=hint) | ||
|
r15231 | ctx = repo[None] | ||
|
r18364 | for s in sorted(ctx.substate): | ||
|
r30755 | ctx.sub(s).bailifchanged(hint=hint) | ||
|
r4549 | |||
|
r43346 | |||
|
r14635 | def logmessage(ui, opts): | ||
|
r47856 | """get the log message according to -m and -l option""" | ||
|
r44346 | |||
check_at_most_one_arg(opts, b'message', b'logfile') | ||||
|
r43347 | message = opts.get(b'message') | ||
logfile = opts.get(b'logfile') | ||||
|
r4549 | |||
if not message and logfile: | ||||
try: | ||||
|
r32618 | if isstdiofilename(logfile): | ||
|
r14635 | message = ui.fin.read() | ||
|
r4549 | else: | ||
|
r43347 | message = b'\n'.join(util.readfile(logfile).splitlines()) | ||
|
r25660 | except IOError as inst: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"can't read commit message '%s': %s") | ||
|
r43346 | % (logfile, encoding.strtolocal(inst.strerror)) | ||
) | ||||
|
r4549 | return message | ||
|
r43346 | |||
|
r24180 | def mergeeditform(ctxorbool, baseformname): | ||
"""return appropriate editform name (referencing a committemplate) | ||||
'ctxorbool' is either a ctx to be committed, or a bool indicating whether | ||||
|
r22248 | merging is committed. | ||
|
r24180 | This returns baseformname with '.merge' appended if it is a merge, | ||
otherwise '.normal' is appended. | ||||
|
r22248 | """ | ||
if isinstance(ctxorbool, bool): | ||||
if ctxorbool: | ||||
|
r43347 | return baseformname + b".merge" | ||
|
r40065 | elif len(ctxorbool.parents()) > 1: | ||
|
r43347 | return baseformname + b".merge" | ||
return baseformname + b".normal" | ||||
|
r22248 | |||
|
r43346 | |||
def getcommiteditor( | ||||
|
r43347 | edit=False, finishdesc=None, extramsg=None, editform=b'', **opts | ||
|
r43346 | ): | ||
|
r21419 | """get appropriate commit message editor according to '--edit' option | ||
'finishdesc' is a function to be called with edited commit message | ||||
(= 'description' of the new changeset) just after editing, but | ||||
before checking empty-ness. It should return actual text to be | ||||
stored into history. This allows to change description before | ||||
storing. | ||||
'extramsg' is a extra message to be shown in the editor instead of | ||||
'Leave message empty to abort commit' line. 'HG: ' prefix and EOL | ||||
is automatically added. | ||||
|
r21999 | 'editform' is a dot-separated list of names, to distinguish | ||
the purpose of commit text editing. | ||||
|
r21419 | 'getcommiteditor' returns 'commitforceeditor' regardless of | ||
'edit', if one of 'finishdesc' or 'extramsg' is specified, because | ||||
they are specific for usage in MQ. | ||||
""" | ||||
if edit or finishdesc or extramsg: | ||||
|
r43346 | return lambda r, c, s: commitforceeditor( | ||
r, c, s, finishdesc=finishdesc, extramsg=extramsg, editform=editform | ||||
) | ||||
|
r21999 | elif editform: | ||
return lambda r, c, s: commiteditor(r, c, s, editform=editform) | ||||
|
r21405 | else: | ||
return commiteditor | ||||
|
r43346 | |||
|
r37792 | def _escapecommandtemplate(tmpl): | ||
parts = [] | ||||
for typ, start, end in templater.scantemplate(tmpl, raw=True): | ||||
if typ == b'string': | ||||
parts.append(stringutil.escapestr(tmpl[start:end])) | ||||
else: | ||||
parts.append(tmpl[start:end]) | ||||
return b''.join(parts) | ||||
|
r43346 | |||
|
r37792 | def rendercommandtemplate(ui, tmpl, props): | ||
r"""Expand a literal template 'tmpl' in a way suitable for command line | ||||
'\' in outermost string is not taken as an escape character because it | ||||
is a directory separator on Windows. | ||||
>>> from . import ui as uimod | ||||
>>> ui = uimod.ui() | ||||
>>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'}) | ||||
'c:\\foo' | ||||
>>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'}) | ||||
'c:{path}' | ||||
""" | ||||
if not tmpl: | ||||
return tmpl | ||||
t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl)) | ||||
return t.renderdefault(props) | ||||
|
r43346 | |||
|
r36528 | def rendertemplate(ctx, tmpl, props=None): | ||
"""Expand a literal template 'tmpl' byte-string against one changeset | ||||
Each props item must be a stringify-able value or a callable returning | ||||
such value, i.e. no bare list nor dict should be passed. | ||||
""" | ||||
repo = ctx.repo() | ||||
tres = formatter.templateresources(repo.ui, repo) | ||||
|
r43346 | t = formatter.maketemplater( | ||
repo.ui, tmpl, defaults=templatekw.keywords, resources=tres | ||||
) | ||||
|
r43347 | mapping = {b'ctx': ctx} | ||
|
r36528 | if props: | ||
mapping.update(props) | ||||
|
r37003 | return t.renderdefault(mapping) | ||
|
r36528 | |||
|
r43346 | |||
|
r46355 | def format_changeset_summary(ui, ctx, command=None, default_spec=None): | ||
"""Format a changeset summary (one line).""" | ||||
spec = None | ||||
if command: | ||||
spec = ui.config( | ||||
b'command-templates', b'oneline-summary.%s' % command, None | ||||
) | ||||
if not spec: | ||||
spec = ui.config(b'command-templates', b'oneline-summary') | ||||
if not spec: | ||||
spec = default_spec | ||||
if not spec: | ||||
|
r46356 | spec = ( | ||
b'{separate(" ", ' | ||||
|
r46386 | b'label("oneline-summary.changeset", "{rev}:{node|short}")' | ||
|
r46356 | b', ' | ||
|
r46386 | b'join(filter(namespaces % "{ifeq(namespace, "branches", "", join(names % "{label("oneline-summary.{namespace}", name)}", " "))}"), " ")' | ||
|
r46356 | b')} ' | ||
|
r46386 | b'"{label("oneline-summary.desc", desc|firstline)}"' | ||
|
r46356 | ) | ||
|
r46355 | text = rendertemplate(ctx, spec) | ||
return text.split(b'\n')[0] | ||||
|
r36528 | def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None): | ||
r"""Convert old-style filename format string to template string | ||||
>>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0) | ||||
'foo-{reporoot|basename}-{seqno}.patch' | ||||
>>> _buildfntemplate(b'%R{tags % "{tag}"}%H') | ||||
'{rev}{tags % "{tag}"}{node}' | ||||
'\' in outermost strings has to be escaped because it is a directory | ||||
separator on Windows: | ||||
>>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0) | ||||
'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch' | ||||
>>> _buildfntemplate(b'\\\\foo\\bar.patch') | ||||
'\\\\\\\\foo\\\\bar.patch' | ||||
>>> _buildfntemplate(b'\\{tags % "{tag}"}') | ||||
'\\\\{tags % "{tag}"}' | ||||
but inner strings follow the template rules (i.e. '\' is taken as an | ||||
escape character): | ||||
>>> _buildfntemplate(br'{"c:\tmp"}', seqno=0) | ||||
'{"c:\\tmp"}' | ||||
""" | ||||
|
r2874 | expander = { | ||
|
r36528 | b'H': b'{node}', | ||
b'R': b'{rev}', | ||||
b'h': b'{node|short}', | ||||
b'm': br'{sub(r"[^\w]", "_", desc|firstline)}', | ||||
b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}', | ||||
b'%': b'%', | ||||
b'b': b'{reporoot|basename}', | ||||
} | ||||
|
r36257 | if total is not None: | ||
|
r36528 | expander[b'N'] = b'{total}' | ||
|
r36257 | if seqno is not None: | ||
|
r36528 | expander[b'n'] = b'{seqno}' | ||
|
r36257 | if total is not None and seqno is not None: | ||
|
r36528 | expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}' | ||
|
r36257 | if pathname is not None: | ||
|
r36528 | expander[b's'] = b'{pathname|basename}' | ||
expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}' | ||||
expander[b'p'] = b'{pathname}' | ||||
|
r36257 | |||
newname = [] | ||||
|
r36528 | for typ, start, end in templater.scantemplate(pat, raw=True): | ||
if typ != b'string': | ||||
newname.append(pat[start:end]) | ||||
continue | ||||
i = start | ||||
while i < end: | ||||
n = pat.find(b'%', i, end) | ||||
if n < 0: | ||||
|
r37102 | newname.append(stringutil.escapestr(pat[i:end])) | ||
|
r36528 | break | ||
|
r37102 | newname.append(stringutil.escapestr(pat[i:n])) | ||
|
r36528 | if n + 2 > end: | ||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b"incomplete format spec in output filename") | ||
|
r43346 | ) | ||
c = pat[n + 1 : n + 2] | ||||
|
r36528 | i = n + 2 | ||
|
r36257 | try: | ||
|
r36528 | newname.append(expander[c]) | ||
|
r36257 | except KeyError: | ||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b"invalid format spec '%%%s' in output filename") % c | ||
|
r43346 | ) | ||
|
r43347 | return b''.join(newname) | ||
|
r2874 | |||
|
r43346 | |||
|
r36528 | def makefilename(ctx, pat, **props): | ||
if not pat: | ||||
return pat | ||||
tmpl = _buildfntemplate(pat, **props) | ||||
# BUG: alias expansion shouldn't be made against template fragments | ||||
# rewritten from %-format strings, but we have no easy way to partially | ||||
# disable the expansion. | ||||
return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props)) | ||||
|
r2874 | |||
|
r43346 | |||
|
r32539 | def isstdiofilename(pat): | ||
"""True if the given pat looks like a filename denoting stdin/stdout""" | ||||
|
r43347 | return not pat or pat == b'-' | ||
|
r32539 | |||
|
r43346 | |||
|
r27418 | class _unclosablefile(object): | ||
def __init__(self, fp): | ||||
self._fp = fp | ||||
def close(self): | ||||
pass | ||||
def __iter__(self): | ||||
return iter(self._fp) | ||||
def __getattr__(self, attr): | ||||
return getattr(self._fp, attr) | ||||
|
r30142 | def __enter__(self): | ||
return self | ||||
def __exit__(self, exc_type, exc_value, exc_tb): | ||||
pass | ||||
|
r43346 | |||
|
r43347 | def makefileobj(ctx, pat, mode=b'wb', **props): | ||
writable = mode not in (b'r', b'rb') | ||||
|
r7319 | |||
|
r32539 | if isstdiofilename(pat): | ||
|
r36223 | repo = ctx.repo() | ||
|
r24306 | if writable: | ||
fp = repo.ui.fout | ||||
else: | ||||
fp = repo.ui.fin | ||||
|
r27419 | return _unclosablefile(fp) | ||
|
r36526 | fn = makefilename(ctx, pat, **props) | ||
|
r18613 | return open(fn, mode) | ||
|
r2882 | |||
|
r43346 | |||
|
r39314 | def openstorage(repo, cmd, file_, opts, returnrevlog=False): | ||
|
r14323 | """opens the changelog, manifest, a filelog or a given revlog""" | ||
|
r43347 | cl = opts[b'changelog'] | ||
mf = opts[b'manifest'] | ||||
dir = opts[b'dir'] | ||||
|
r14323 | msg = None | ||
if cl and mf: | ||||
|
r43347 | msg = _(b'cannot specify --changelog and --manifest at the same time') | ||
|
r25119 | elif cl and dir: | ||
|
r43347 | msg = _(b'cannot specify --changelog and --dir at the same time') | ||
|
r29427 | elif cl or mf or dir: | ||
|
r14323 | if file_: | ||
|
r43347 | msg = _(b'cannot specify filename with --changelog or --manifest') | ||
|
r14323 | elif not repo: | ||
|
r43346 | msg = _( | ||
|
r43347 | b'cannot specify --changelog or --manifest or --dir ' | ||
b'without a repository' | ||||
|
r43346 | ) | ||
|
r14323 | if msg: | ||
|
r46431 | raise error.InputError(msg) | ||
|
r14323 | |||
r = None | ||||
if repo: | ||||
if cl: | ||||
|
r21033 | r = repo.unfiltered().changelog | ||
|
r25119 | elif dir: | ||
|
r46129 | if not scmutil.istreemanifest(repo): | ||
|
r46431 | raise error.InputError( | ||
|
r43346 | _( | ||
|
r43347 | b"--dir can only be used on repos with " | ||
b"treemanifest enabled" | ||||
|
r43346 | ) | ||
) | ||||
|
r43347 | if not dir.endswith(b'/'): | ||
dir = dir + b'/' | ||||
|
r39280 | dirlog = repo.manifestlog.getstorage(dir) | ||
|
r25119 | if len(dirlog): | ||
r = dirlog | ||||
|
r14323 | elif mf: | ||
|
r39280 | r = repo.manifestlog.getstorage(b'') | ||
|
r14323 | elif file_: | ||
filelog = repo.file(file_) | ||||
if len(filelog): | ||||
r = filelog | ||||
|
r39314 | |||
# Not all storage may be revlogs. If requested, try to return an actual | ||||
# revlog instance. | ||||
if returnrevlog: | ||||
if isinstance(r, revlog.revlog): | ||||
pass | ||||
|
r43347 | elif util.safehasattr(r, b'_revlog'): | ||
|
r43792 | r = r._revlog # pytype: disable=attribute-error | ||
|
r39314 | elif r is not None: | ||
|
r46431 | raise error.InputError( | ||
_(b'%r does not appear to be a revlog') % r | ||||
) | ||||
|
r39314 | |||
|
r14323 | if not r: | ||
|
r39314 | if not returnrevlog: | ||
|
r46431 | raise error.InputError(_(b'cannot give path to non-revlog')) | ||
|
r39314 | |||
|
r14323 | if not file_: | ||
|
r43347 | raise error.CommandError(cmd, _(b'invalid arguments')) | ||
|
r14323 | if not os.path.isfile(file_): | ||
|
r46431 | raise error.InputError(_(b"revlog '%s' not found") % file_) | ||
r47838 | ||||
target = (revlog_constants.KIND_OTHER, b'free-form:%s' % file_) | ||||
|
r43346 | r = revlog.revlog( | ||
r47838 | vfsmod.vfs(encoding.getcwd(), audit=False), | |||
target=target, | ||||
r47921 | radix=file_[:-2], | |||
|
r43346 | ) | ||
|
r14323 | return r | ||
|
r43346 | |||
|
r39314 | def openrevlog(repo, cmd, file_, opts): | ||
"""Obtain a revlog backing storage of an item. | ||||
This is similar to ``openstorage()`` except it always returns a revlog. | ||||
In most cases, a caller cares about the main storage object - not the | ||||
revlog backing it. Therefore, this function should only be used by code | ||||
that needs to examine low-level revlog implementation details. e.g. debug | ||||
commands. | ||||
""" | ||||
return openstorage(repo, cmd, file_, opts, returnrevlog=True) | ||||
|
r43346 | |||
|
r5610 | def copy(ui, repo, pats, opts, rename=False): | ||
|
r44844 | check_incompatible_arguments(opts, b'forget', [b'dry_run']) | ||
|
r5589 | # called with the repo lock held | ||
# | ||||
# hgsep => pathname that uses "/" to separate directories | ||||
# ossep => pathname that uses os.sep to separate directories | ||||
cwd = repo.getcwd() | ||||
targets = {} | ||||
|
r44844 | forget = opts.get(b"forget") | ||
|
r43347 | after = opts.get(b"after") | ||
dryrun = opts.get(b"dry_run") | ||||
|
r44847 | rev = opts.get(b'at_rev') | ||
if rev: | ||||
if not forget and not after: | ||||
# TODO: Remove this restriction and make it also create the copy | ||||
# targets (and remove the rename source if rename==True). | ||||
|
r46431 | raise error.InputError(_(b'--at-rev requires --after')) | ||
|
r44847 | ctx = scmutil.revsingle(repo, rev) | ||
if len(ctx.parents()) > 1: | ||||
|
r46431 | raise error.InputError( | ||
_(b'cannot mark/unmark copy in merge commit') | ||||
) | ||||
|
r44847 | else: | ||
ctx = repo[None] | ||||
|
r44840 | pctx = ctx.p1() | ||
|
r5589 | |||
|
r41806 | uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True) | ||
|
r43346 | |||
|
r44844 | if forget: | ||
|
r44845 | if ctx.rev() is None: | ||
new_ctx = ctx | ||||
else: | ||||
if len(ctx.parents()) > 1: | ||||
|
r46431 | raise error.InputError(_(b'cannot unmark copy in merge commit')) | ||
|
r44845 | # avoid cycle context -> subrepo -> cmdutil | ||
from . import context | ||||
rewriteutil.precheck(repo, [ctx.rev()], b'uncopy') | ||||
new_ctx = context.overlayworkingctx(repo) | ||||
new_ctx.setbase(ctx.p1()) | ||||
mergemod.graft(repo, ctx, wctx=new_ctx) | ||||
match = scmutil.match(ctx, pats, opts) | ||||
current_copies = ctx.p1copies() | ||||
current_copies.update(ctx.p2copies()) | ||||
uipathfn = scmutil.getuipathfn(repo) | ||||
for f in ctx.walk(match): | ||||
|
r44844 | if f in current_copies: | ||
|
r44845 | new_ctx[f].markcopied(None) | ||
|
r44844 | elif match.exact(f): | ||
ui.warn( | ||||
_( | ||||
b'%s: not unmarking as copy - file is not marked as copied\n' | ||||
) | ||||
% uipathfn(f) | ||||
) | ||||
|
r44845 | |||
if ctx.rev() is not None: | ||||
with repo.lock(): | ||||
mem_ctx = new_ctx.tomemctx_for_amend(ctx) | ||||
new_node = mem_ctx.commit() | ||||
if repo.dirstate.p1() == ctx.node(): | ||||
with repo.dirstate.parentchange(): | ||||
scmutil.movedirstate(repo, repo[new_node]) | ||||
replacements = {ctx.node(): [new_node]} | ||||
scmutil.cleanupnodes( | ||||
repo, replacements, b'uncopy', fixphase=True | ||||
) | ||||
|
r44844 | return | ||
|
r44846 | pats = scmutil.expandpats(pats) | ||
if not pats: | ||||
|
r46431 | raise error.InputError(_(b'no source or destination specified')) | ||
|
r44846 | if len(pats) == 1: | ||
|
r46431 | raise error.InputError(_(b'no destination specified')) | ||
|
r44846 | dest = pats.pop() | ||
|
r5605 | def walkpat(pat): | ||
srcs = [] | ||||
|
r45314 | # TODO: Inline and simplify the non-working-copy version of this code | ||
# since it shares very little with the working-copy version of it. | ||||
ctx_to_walk = ctx if ctx.rev() is None else pctx | ||||
m = scmutil.match(ctx_to_walk, [pat], opts, globbed=True) | ||||
for abs in ctx_to_walk.walk(m): | ||||
|
r41806 | rel = uipathfn(abs) | ||
|
r6584 | exact = m.exact(abs) | ||
|
r44840 | if abs not in ctx: | ||
|
r44839 | if abs in pctx: | ||
if not after: | ||||
if exact: | ||||
ui.warn( | ||||
_( | ||||
b'%s: not copying - file has been marked ' | ||||
b'for remove\n' | ||||
) | ||||
% rel | ||||
) | ||||
continue | ||||
else: | ||||
if exact: | ||||
ui.warn( | ||||
_(b'%s: not copying - file is not managed\n') % rel | ||||
|
r43346 | ) | ||
|
r44839 | continue | ||
|
r5605 | # abs: hgsep | ||
# rel: ossep | ||||
srcs.append((abs, rel, exact)) | ||||
return srcs | ||||
|
r5589 | |||
|
r44847 | if ctx.rev() is not None: | ||
rewriteutil.precheck(repo, [ctx.rev()], b'uncopy') | ||||
absdest = pathutil.canonpath(repo.root, cwd, dest) | ||||
if ctx.hasdir(absdest): | ||||
|
r46431 | raise error.InputError( | ||
|
r44847 | _(b'%s: --at-rev does not support a directory as destination') | ||
% uipathfn(absdest) | ||||
) | ||||
if absdest not in ctx: | ||||
|
r46431 | raise error.InputError( | ||
|
r44847 | _(b'%s: copy destination does not exist in %s') | ||
% (uipathfn(absdest), ctx) | ||||
) | ||||
# avoid cycle context -> subrepo -> cmdutil | ||||
from . import context | ||||
copylist = [] | ||||
for pat in pats: | ||||
srcs = walkpat(pat) | ||||
if not srcs: | ||||
continue | ||||
for abs, rel, exact in srcs: | ||||
copylist.append(abs) | ||||
|
r45316 | if not copylist: | ||
|
r46431 | raise error.InputError(_(b'no files to copy')) | ||
|
r44847 | # TODO: Add support for `hg cp --at-rev . foo bar dir` and | ||
# `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the | ||||
# existing functions below. | ||||
if len(copylist) != 1: | ||||
|
r46431 | raise error.InputError(_(b'--at-rev requires a single source')) | ||
|
r44847 | |||
new_ctx = context.overlayworkingctx(repo) | ||||
new_ctx.setbase(ctx.p1()) | ||||
mergemod.graft(repo, ctx, wctx=new_ctx) | ||||
new_ctx.markcopied(absdest, copylist[0]) | ||||
with repo.lock(): | ||||
mem_ctx = new_ctx.tomemctx_for_amend(ctx) | ||||
new_node = mem_ctx.commit() | ||||
if repo.dirstate.p1() == ctx.node(): | ||||
with repo.dirstate.parentchange(): | ||||
scmutil.movedirstate(repo, repo[new_node]) | ||||
replacements = {ctx.node(): [new_node]} | ||||
scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True) | ||||
return | ||||
|
r5589 | # abssrc: hgsep | ||
# relsrc: ossep | ||||
# otarget: ossep | ||||
|
r5605 | def copyfile(abssrc, relsrc, otarget, exact): | ||
|
r20033 | abstarget = pathutil.canonpath(repo.root, cwd, otarget) | ||
|
r43347 | if b'/' in abstarget: | ||
|
r16542 | # We cannot normalize abstarget itself, this would prevent | ||
# case only renames, like a => A. | ||||
|
r43347 | abspath, absname = abstarget.rsplit(b'/', 1) | ||
abstarget = repo.dirstate.normalize(abspath) + b'/' + absname | ||||
|
r5589 | reltarget = repo.pathto(abstarget, cwd) | ||
|
r5607 | target = repo.wjoin(abstarget) | ||
|
r5589 | src = repo.wjoin(abssrc) | ||
|
r5608 | state = repo.dirstate[abstarget] | ||
|
r5607 | |||
|
r13962 | scmutil.checkportable(ui, abstarget) | ||
|
r13945 | |||
|
r5607 | # check for collisions | ||
prevsrc = targets.get(abstarget) | ||||
|
r5589 | if prevsrc is not None: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'%s: not overwriting - %s collides with %s\n') | ||
|
r43346 | % ( | ||
reltarget, | ||||
repo.pathto(abssrc, cwd), | ||||
repo.pathto(prevsrc, cwd), | ||||
) | ||||
) | ||||
return True # report a failure | ||||
|
r5607 | |||
# check for overwrites | ||||
|
r12342 | exists = os.path.lexists(target) | ||
|
r16283 | samefile = False | ||
if exists and abssrc != abstarget: | ||||
|
r43346 | if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize( | ||
abstarget | ||||
): | ||||
|
r16283 | if not rename: | ||
|
r43347 | ui.warn(_(b"%s: can't copy - same file\n") % reltarget) | ||
|
r43346 | return True # report a failure | ||
|
r16283 | exists = False | ||
samefile = True | ||||
|
r43347 | if not after and exists or after and state in b'mn': | ||
if not opts[b'force']: | ||||
if state in b'mn': | ||||
msg = _(b'%s: not overwriting - file already committed\n') | ||||
|
r30151 | if after: | ||
|
r43347 | flags = b'--after --force' | ||
|
r30151 | else: | ||
|
r43347 | flags = b'--force' | ||
|
r30151 | if rename: | ||
|
r43346 | hint = ( | ||
_( | ||||
|
r43347 | b"('hg rename %s' to replace the file by " | ||
b'recording a rename)\n' | ||||
|
r43346 | ) | ||
% flags | ||||
) | ||||
|
r30151 | else: | ||
|
r43346 | hint = ( | ||
_( | ||||
|
r43347 | b"('hg copy %s' to replace the file by " | ||
b'recording a copy)\n' | ||||
|
r43346 | ) | ||
% flags | ||||
) | ||||
|
r30151 | else: | ||
|
r43347 | msg = _(b'%s: not overwriting - file exists\n') | ||
|
r30151 | if rename: | ||
|
r43347 | hint = _( | ||
b"('hg rename --after' to record the rename)\n" | ||||
) | ||||
|
r30151 | else: | ||
|
r43347 | hint = _(b"('hg copy --after' to record the copy)\n") | ||
|
r30151 | ui.warn(msg % reltarget) | ||
ui.warn(hint) | ||||
|
r43346 | return True # report a failure | ||
|
r5607 | |||
if after: | ||||
|
r5608 | if not exists: | ||
|
r11152 | if rename: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'%s: not recording move - %s does not exist\n') | ||
|
r43346 | % (relsrc, reltarget) | ||
) | ||||
|
r11152 | else: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'%s: not recording copy - %s does not exist\n') | ||
|
r43346 | % (relsrc, reltarget) | ||
) | ||||
return True # report a failure | ||||
|
r5608 | elif not dryrun: | ||
|
r5589 | try: | ||
|
r5608 | if exists: | ||
os.unlink(target) | ||||
|
r43347 | targetdir = os.path.dirname(target) or b'.' | ||
|
r5608 | if not os.path.isdir(targetdir): | ||
os.makedirs(targetdir) | ||||
|
r16283 | if samefile: | ||
|
r43347 | tmp = target + b"~hgrename" | ||
|
r16283 | os.rename(src, tmp) | ||
os.rename(tmp, target) | ||||
else: | ||||
|
r37106 | # Preserve stat info on renames, not on copies; this matches | ||
# Linux CLI behavior. | ||||
util.copyfile(src, target, copystat=rename) | ||||
|
r14518 | srcexists = True | ||
|
r25660 | except IOError as inst: | ||
|
r5589 | if inst.errno == errno.ENOENT: | ||
|
r43347 | ui.warn(_(b'%s: deleted in working directory\n') % relsrc) | ||
|
r14518 | srcexists = False | ||
|
r5589 | else: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'%s: cannot copy - %s\n') | ||
|
r43346 | % (relsrc, encoding.strtolocal(inst.strerror)) | ||
) | ||||
return True # report a failure | ||||
|
r5607 | |||
|
r5589 | if ui.verbose or not exact: | ||
|
r7894 | if rename: | ||
|
r43347 | ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget)) | ||
|
r7894 | else: | ||
|
r43347 | ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget)) | ||
|
r5608 | |||
|
r5589 | targets[abstarget] = abssrc | ||
|
r5607 | |||
# fix up dirstate | ||||
|
r43346 | scmutil.dirstatecopy( | ||
|
r44840 | ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd | ||
|
r43346 | ) | ||
|
r5610 | if rename and not dryrun: | ||
|
r16283 | if not after and srcexists and not samefile: | ||
|
r43347 | rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs') | ||
|
r38512 | repo.wvfs.unlinkpath(abssrc, rmdir=rmdir) | ||
|
r44840 | ctx.forget([abssrc]) | ||
|
r5589 | |||
# pat: ossep | ||||
# dest ossep | ||||
# srcs: list of (hgsep, hgsep, ossep, bool) | ||||
# return: function that takes hgsep and returns ossep | ||||
def targetpathfn(pat, dest, srcs): | ||||
if os.path.isdir(pat): | ||||
|
r20033 | abspfx = pathutil.canonpath(repo.root, cwd, pat) | ||
|
r5589 | abspfx = util.localpath(abspfx) | ||
if destdirexists: | ||||
striplen = len(os.path.split(abspfx)[0]) | ||||
else: | ||||
striplen = len(abspfx) | ||||
if striplen: | ||||
|
r30615 | striplen += len(pycompat.ossep) | ||
|
r5589 | res = lambda p: os.path.join(dest, util.localpath(p)[striplen:]) | ||
elif destdirexists: | ||||
|
r43346 | res = lambda p: os.path.join( | ||
dest, os.path.basename(util.localpath(p)) | ||||
) | ||||
|
r5589 | else: | ||
res = lambda p: dest | ||||
return res | ||||
# pat: ossep | ||||
# dest ossep | ||||
# srcs: list of (hgsep, hgsep, ossep, bool) | ||||
# return: function that takes hgsep and returns ossep | ||||
def targetpathafterfn(pat, dest, srcs): | ||||
|
r12085 | if matchmod.patkind(pat): | ||
|
r5589 | # a mercurial pattern | ||
|
r43346 | res = lambda p: os.path.join( | ||
dest, os.path.basename(util.localpath(p)) | ||||
) | ||||
|
r5589 | else: | ||
|
r20033 | abspfx = pathutil.canonpath(repo.root, cwd, pat) | ||
|
r5589 | if len(abspfx) < len(srcs[0][0]): | ||
# A directory. Either the target path contains the last | ||||
# component of the source path or it does not. | ||||
def evalpath(striplen): | ||||
score = 0 | ||||
for s in srcs: | ||||
t = os.path.join(dest, util.localpath(s[0])[striplen:]) | ||||
|
r12357 | if os.path.lexists(t): | ||
|
r5589 | score += 1 | ||
return score | ||||
abspfx = util.localpath(abspfx) | ||||
striplen = len(abspfx) | ||||
if striplen: | ||||
|
r30615 | striplen += len(pycompat.ossep) | ||
|
r5589 | if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])): | ||
score = evalpath(striplen) | ||||
striplen1 = len(os.path.split(abspfx)[0]) | ||||
if striplen1: | ||||
|
r30615 | striplen1 += len(pycompat.ossep) | ||
|
r5589 | if evalpath(striplen1) > score: | ||
striplen = striplen1 | ||||
|
r43346 | res = lambda p: os.path.join(dest, util.localpath(p)[striplen:]) | ||
|
r5589 | else: | ||
# a file | ||||
if destdirexists: | ||||
|
r43346 | res = lambda p: os.path.join( | ||
dest, os.path.basename(util.localpath(p)) | ||||
) | ||||
|
r5589 | else: | ||
res = lambda p: dest | ||||
return res | ||||
|
r6258 | destdirexists = os.path.isdir(dest) and not os.path.islink(dest) | ||
|
r5589 | if not destdirexists: | ||
|
r12085 | if len(pats) > 1 or matchmod.patkind(pats[0]): | ||
|
r46431 | raise error.InputError( | ||
|
r43346 | _( | ||
|
r43347 | b'with multiple sources, destination must be an ' | ||
b'existing directory' | ||||
|
r43346 | ) | ||
) | ||||
|
r5843 | if util.endswithsep(dest): | ||
|
r46431 | raise error.InputError( | ||
_(b'destination %s is not a directory') % dest | ||||
) | ||||
|
r5607 | |||
tfn = targetpathfn | ||||
if after: | ||||
|
r5589 | tfn = targetpathafterfn | ||
copylist = [] | ||||
for pat in pats: | ||||
|
r5605 | srcs = walkpat(pat) | ||
|
r5589 | if not srcs: | ||
continue | ||||
copylist.append((tfn(pat, dest, srcs), srcs)) | ||||
if not copylist: | ||||
|
r47897 | hint = None | ||
if rename: | ||||
hint = _(b'maybe you meant to use --after --at-rev=.') | ||||
raise error.InputError(_(b'no files to copy'), hint=hint) | ||||
|
r5589 | |||
|
r5606 | errors = 0 | ||
|
r5589 | for targetpath, srcs in copylist: | ||
|
r5605 | for abssrc, relsrc, exact in srcs: | ||
|
r5606 | if copyfile(abssrc, relsrc, targetpath(abssrc), exact): | ||
errors += 1 | ||||
|
r5589 | |||
|
r11177 | return errors != 0 | ||
|
r5589 | |||
|
r43346 | |||
|
r26561 | ## facility to let extension process additional data into an import patch | ||
# list of identifier to be executed in order | ||||
extrapreimport = [] # run before commit | ||||
|
r43346 | extrapostimport = [] # run after commit | ||
|
r26561 | # mapping from identifier to actual import function | ||
# | ||||
# 'preimport' are run before the commit is made and are provided the following | ||||
# arguments: | ||||
# - repo: the localrepository instance, | ||||
# - patchdata: data extracted from patch header (cf m.patch.patchheadermap), | ||||
|
r26781 | # - extra: the future extra dictionary of the changeset, please mutate it, | ||
|
r26561 | # - opts: the import options. | ||
# XXX ideally, we would just pass an ctx ready to be computed, that would allow | ||||
# mutation of in memory commit and more. Feel free to rework the code to get | ||||
# there. | ||||
extrapreimportmap = {} | ||||
|
r26562 | # 'postimport' are run after the commit is made and are provided the following | ||
# argument: | ||||
# - ctx: the changectx created by import. | ||||
extrapostimportmap = {} | ||||
|
r26561 | |||
|
r43346 | |||
|
r37638 | def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc): | ||
|
r20500 | """Utility function used by commands.import to import a single patch | ||
This function is explicitly defined here to help the evolve extension to | ||||
wrap this part of the import logic. | ||||
The API is currently a bit ugly because it a simple code translation from | ||||
the import command. Feel free to make it better. | ||||
|
r37638 | :patchdata: a dictionary containing parsed patch data (such as from | ||
``patch.extract()``) | ||||
|
r20500 | :parents: nodes that will be parent of the created commit | ||
:opts: the full dict of option passed to the import command | ||||
:msgs: list to save commit message to. | ||||
(used in case we need to save it when failing) | ||||
:updatefunc: a function that update a repo to a given node | ||||
updatefunc(<repo>, <node>) | ||||
""" | ||||
|
r25930 | # avoid cycle context -> subrepo -> cmdutil | ||
|
r28322 | from . import context | ||
|
r37638 | |||
|
r43347 | tmpname = patchdata.get(b'filename') | ||
message = patchdata.get(b'message') | ||||
user = opts.get(b'user') or patchdata.get(b'user') | ||||
date = opts.get(b'date') or patchdata.get(b'date') | ||||
branch = patchdata.get(b'branch') | ||||
nodeid = patchdata.get(b'nodeid') | ||||
p1 = patchdata.get(b'p1') | ||||
p2 = patchdata.get(b'p2') | ||||
nocommit = opts.get(b'no_commit') | ||||
importbranch = opts.get(b'import_branch') | ||||
update = not opts.get(b'bypass') | ||||
strip = opts[b"strip"] | ||||
prefix = opts[b"prefix"] | ||||
sim = float(opts.get(b'similarity') or 0) | ||||
|
r37639 | |||
|
r20500 | if not tmpname: | ||
|
r37639 | return None, None, False | ||
|
r20500 | |||
|
r21553 | rejects = False | ||
|
r37639 | cmdline_message = logmessage(ui, opts) | ||
if cmdline_message: | ||||
# pickup the cmdline msg | ||||
message = cmdline_message | ||||
elif message: | ||||
# pickup the patch msg | ||||
message = message.strip() | ||||
else: | ||||
# launch the editor | ||||
message = None | ||||
|
r43347 | ui.debug(b'message:\n%s\n' % (message or b'')) | ||
|
r37639 | |||
if len(parents) == 1: | ||||
|
r47600 | parents.append(repo[nullrev]) | ||
|
r43347 | if opts.get(b'exact'): | ||
|
r37639 | if not nodeid or not p1: | ||
|
r46431 | raise error.InputError(_(b'not a Mercurial patch')) | ||
|
r37639 | p1 = repo[p1] | ||
|
r47600 | p2 = repo[p2 or nullrev] | ||
|
r37639 | elif p2: | ||
try: | ||||
|
r20500 | p1 = repo[p1] | ||
|
r37639 | p2 = repo[p2] | ||
# Without any options, consider p2 only if the | ||||
# patch is being applied on top of the recorded | ||||
# first parent. | ||||
if p1 != parents[0]: | ||||
p1 = parents[0] | ||||
|
r47600 | p2 = repo[nullrev] | ||
|
r37639 | except error.RepoError: | ||
p1, p2 = parents | ||||
|
r47600 | if p2.rev() == nullrev: | ||
|
r43346 | ui.warn( | ||
_( | ||||
|
r43347 | b"warning: import the patch as a normal revision\n" | ||
b"(use --exact to import the patch as a merge)\n" | ||||
|
r43346 | ) | ||
) | ||||
|
r37639 | else: | ||
p1, p2 = parents | ||||
n = None | ||||
if update: | ||||
if p1 != parents[0]: | ||||
updatefunc(repo, p1.node()) | ||||
if p2 != parents[1]: | ||||
repo.setparents(p1.node(), p2.node()) | ||||
|
r43347 | if opts.get(b'exact') or importbranch: | ||
repo.dirstate.setbranch(branch or b'default') | ||||
partial = opts.get(b'partial', False) | ||||
|
r37639 | files = set() | ||
try: | ||||
|
r43346 | patch.patch( | ||
ui, | ||||
repo, | ||||
tmpname, | ||||
strip=strip, | ||||
prefix=prefix, | ||||
files=files, | ||||
eolmode=None, | ||||
similarity=sim / 100.0, | ||||
) | ||||
|
r37639 | except error.PatchError as e: | ||
if not partial: | ||||
raise error.Abort(pycompat.bytestr(e)) | ||||
if partial: | ||||
rejects = True | ||||
files = list(files) | ||||
if nocommit: | ||||
if message: | ||||
msgs.append(message) | ||||
|
r20500 | else: | ||
|
r43347 | if opts.get(b'exact') or p2: | ||
|
r37639 | # If you got here, you either use --force and know what | ||
# you are doing or used --exact or a merge patch while | ||||
# being updated to its first parent. | ||||
m = None | ||||
else: | ||||
m = scmutil.matchfiles(repo, files or []) | ||||
|
r43347 | editform = mergeeditform(repo[None], b'import.normal') | ||
if opts.get(b'exact'): | ||||
|
r37639 | editor = None | ||
else: | ||||
|
r43346 | editor = getcommiteditor( | ||
editform=editform, **pycompat.strkwargs(opts) | ||||
) | ||||
|
r37639 | extra = {} | ||
for idfunc in extrapreimport: | ||||
extrapreimportmap[idfunc](repo, patchdata, extra, opts) | ||||
overrides = {} | ||||
if partial: | ||||
|
r43347 | overrides[(b'ui', b'allowemptycommit')] = True | ||
|
r44118 | if opts.get(b'secret'): | ||
overrides[(b'phases', b'new-commit')] = b'secret' | ||||
|
r43347 | with repo.ui.configoverride(overrides, b'import'): | ||
|
r43346 | n = repo.commit( | ||
message, user, date, match=m, editor=editor, extra=extra | ||||
) | ||||
|
r37639 | for idfunc in extrapostimport: | ||
extrapostimportmap[idfunc](repo[n]) | ||||
else: | ||||
|
r43347 | if opts.get(b'exact') or importbranch: | ||
branch = branch or b'default' | ||||
|
r37639 | else: | ||
branch = p1.branch() | ||||
store = patch.filestore() | ||||
try: | ||||
|
r20500 | files = set() | ||
|
r21553 | try: | ||
|
r43346 | patch.patchrepo( | ||
ui, | ||||
repo, | ||||
p1, | ||||
store, | ||||
tmpname, | ||||
strip, | ||||
prefix, | ||||
files, | ||||
eolmode=None, | ||||
) | ||||
|
r34252 | except error.PatchError as e: | ||
|
r37639 | raise error.Abort(stringutil.forcebytestr(e)) | ||
|
r43347 | if opts.get(b'exact'): | ||
|
r37639 | editor = None | ||
|
r20500 | else: | ||
|
r43347 | editor = getcommiteditor(editform=b'import.bypass') | ||
|
r43346 | memctx = context.memctx( | ||
repo, | ||||
(p1.node(), p2.node()), | ||||
message, | ||||
files=files, | ||||
filectxfn=store, | ||||
user=user, | ||||
date=date, | ||||
branch=branch, | ||||
editor=editor, | ||||
) | ||||
|
r44849 | |||
overrides = {} | ||||
if opts.get(b'secret'): | ||||
overrides[(b'phases', b'new-commit')] = b'secret' | ||||
with repo.ui.configoverride(overrides, b'import'): | ||||
n = memctx.commit() | ||||
|
r37639 | finally: | ||
store.close() | ||||
|
r43347 | if opts.get(b'exact') and nocommit: | ||
|
r37639 | # --exact with --no-commit is still useful in that it does merge | ||
# and branch bits | ||||
|
r43347 | ui.warn(_(b"warning: can't check exact import with --no-commit\n")) | ||
elif opts.get(b'exact') and (not n or hex(n) != nodeid): | ||||
raise error.Abort(_(b'patch is damaged or loses information')) | ||||
msg = _(b'applied to working directory') | ||||
|
r37639 | if n: | ||
# i18n: refers to a short changeset id | ||||
|
r43347 | msg = _(b'created %s') % short(n) | ||
|
r37639 | return msg, n, rejects | ||
|
r20500 | |||
|
r43346 | |||
|
r26545 | # facility to let extensions include additional data in an exported patch | ||
# list of identifiers to be executed in order | ||||
extraexport = [] | ||||
# mapping from identifier to actual export function | ||||
# function as to return a string to be added to the header or None | ||||
# it is given two arguments (sequencenumber, changectx) | ||||
extraexportmap = {} | ||||
|
r43346 | |||
|
r37620 | def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts): | ||
|
r32660 | node = scmutil.binnode(ctx) | ||
|
r32433 | parents = [p.node() for p in ctx.parents() if p] | ||
branch = ctx.branch() | ||||
if switch_parent: | ||||
parents.reverse() | ||||
if parents: | ||||
prev = parents[0] | ||||
else: | ||||
|
r47771 | prev = repo.nullid | ||
|
r32433 | |||
|
r37620 | fm.context(ctx=ctx) | ||
|
r43347 | fm.plain(b'# HG changeset patch\n') | ||
fm.write(b'user', b'# User %s\n', ctx.user()) | ||||
fm.plain(b'# Date %d %d\n' % ctx.date()) | ||||
fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date())) | ||||
|
r43346 | fm.condwrite( | ||
|
r43347 | branch and branch != b'default', b'branch', b'# Branch %s\n', branch | ||
|
r43346 | ) | ||
|
r43347 | fm.write(b'node', b'# Node ID %s\n', hex(node)) | ||
fm.plain(b'# Parent %s\n' % hex(prev)) | ||||
|
r32433 | if len(parents) > 1: | ||
|
r43347 | fm.plain(b'# Parent %s\n' % hex(parents[1])) | ||
fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node')) | ||||
|
r37620 | |||
# TODO: redesign extraexportmap function to support formatter | ||||
|
r32433 | for headerid in extraexport: | ||
header = extraexportmap[headerid](seqno, ctx) | ||||
if header is not None: | ||||
|
r43347 | fm.plain(b'# %s\n' % header) | ||
fm.write(b'desc', b'%s\n', ctx.description().rstrip()) | ||||
fm.plain(b'\n') | ||||
|
r37620 | |||
if fm.isplain(): | ||||
chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts) | ||||
for chunk, label in chunkiter: | ||||
fm.plain(chunk, label=label) | ||||
else: | ||||
chunkiter = patch.diff(repo, prev, node, match, opts=diffopts) | ||||
# TODO: make it structured? | ||||
fm.data(diff=b''.join(chunkiter)) | ||||
|
r32433 | |||
|
r43346 | |||
|
r37622 | def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match): | ||
|
r37618 | """Export changesets to stdout or a single file""" | ||
for seqno, rev in enumerate(revs, 1): | ||||
ctx = repo[rev] | ||||
|
r43347 | if not dest.startswith(b'<'): | ||
repo.ui.note(b"%s\n" % dest) | ||||
|
r37620 | fm.startitem() | ||
_exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts) | ||||
|
r37622 | |||
|
r43346 | |||
def _exportfntemplate( | ||||
repo, revs, basefm, fntemplate, switch_parent, diffopts, match | ||||
): | ||||
|
r37618 | """Export changesets to possibly multiple files""" | ||
total = len(revs) | ||||
revwidth = max(len(str(rev)) for rev in revs) | ||||
|
r37619 | filemap = util.sortdict() # filename: [(seqno, rev), ...] | ||
|
r37618 | |||
for seqno, rev in enumerate(revs, 1): | ||||
ctx = repo[rev] | ||||
|
r43346 | dest = makefilename( | ||
ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth | ||||
) | ||||
|
r37619 | filemap.setdefault(dest, []).append((seqno, rev)) | ||
for dest in filemap: | ||||
|
r37622 | with formatter.maybereopen(basefm, dest) as fm: | ||
|
r43347 | repo.ui.note(b"%s\n" % dest) | ||
|
r37619 | for seqno, rev in filemap[dest]: | ||
|
r37620 | fm.startitem() | ||
|
r37619 | ctx = repo[rev] | ||
|
r43346 | _exportsingle( | ||
repo, ctx, fm, match, switch_parent, seqno, diffopts | ||||
) | ||||
|
r37618 | |||
|
r42673 | def _prefetchchangedfiles(repo, revs, match): | ||
allfiles = set() | ||||
for rev in revs: | ||||
for file in repo[rev].files(): | ||||
if not match or match(file): | ||||
allfiles.add(file) | ||||
|
r45632 | match = scmutil.matchfiles(repo, allfiles) | ||
revmatches = [(rev, match) for rev in revs] | ||||
scmutil.prefetchfiles(repo, revmatches) | ||||
|
r42673 | |||
|
r43346 | |||
def export( | ||||
repo, | ||||
revs, | ||||
basefm, | ||||
|
r43347 | fntemplate=b'hg-%h.patch', | ||
|
r43346 | switch_parent=False, | ||
opts=None, | ||||
match=None, | ||||
): | ||||
|
r46554 | """export changesets as hg patches | ||
|
r32430 | |||
Args: | ||||
repo: The repository from which we're exporting revisions. | ||||
revs: A list of revisions to export as revision numbers. | ||||
|
r37622 | basefm: A formatter to which patches should be written. | ||
|
r32431 | fntemplate: An optional string to use for generating patch file names. | ||
|
r32430 | switch_parent: If True, show diffs against second parent when not nullid. | ||
Default is false, which always shows diff against p1. | ||||
opts: diff options to use for generating the patch. | ||||
match: If specified, only export changes to files matching this matcher. | ||||
Returns: | ||||
Nothing. | ||||
Side Effect: | ||||
"HG Changeset Patch" data is emitted to one of the following | ||||
destinations: | ||||
|
r32431 | fntemplate specified: Each rev is written to a unique file named using | ||
|
r32430 | the given template. | ||
|
r37622 | Otherwise: All revs will be written to basefm. | ||
|
r46554 | """ | ||
|
r42673 | _prefetchchangedfiles(repo, revs, match) | ||
|
r37781 | |||
|
r37621 | if not fntemplate: | ||
|
r43347 | _exportfile( | ||
repo, revs, basefm, b'<unnamed>', switch_parent, opts, match | ||||
) | ||||
|
r37618 | else: | ||
|
r43346 | _exportfntemplate( | ||
repo, revs, basefm, fntemplate, switch_parent, opts, match | ||||
) | ||||
|
r10611 | |||
|
r37621 | def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None): | ||
"""Export changesets to the given file stream""" | ||||
|
r42673 | _prefetchchangedfiles(repo, revs, match) | ||
|
r37781 | |||
|
r43347 | dest = getattr(fp, 'name', b'<unnamed>') | ||
with formatter.formatter(repo.ui, fp, b'export', {}) as fm: | ||||
|
r37622 | _exportfile(repo, revs, fm, dest, switch_parent, opts, match) | ||
|
r37621 | |||
|
r43346 | |||
|
r29795 | def showmarker(fm, marker, index=None): | ||
|
r20470 | """utility function to display obsolescence marker in a readable way | ||
To be used by debug function.""" | ||||
|
r28613 | if index is not None: | ||
|
r43347 | fm.write(b'index', b'%i ', index) | ||
fm.write(b'prednode', b'%s ', hex(marker.prednode())) | ||||
|
r29795 | succs = marker.succnodes() | ||
|
r43346 | fm.condwrite( | ||
|
r43347 | succs, | ||
b'succnodes', | ||||
b'%s ', | ||||
fm.formatlist(map(hex, succs), name=b'node'), | ||||
|
r43346 | ) | ||
|
r43347 | fm.write(b'flag', b'%X ', marker.flags()) | ||
|
r22260 | parents = marker.parentnodes() | ||
if parents is not None: | ||||
|
r43346 | fm.write( | ||
|
r43347 | b'parentnodes', | ||
b'{%s} ', | ||||
fm.formatlist(map(hex, parents), name=b'node', sep=b', '), | ||||
|
r43346 | ) | ||
|
r43347 | fm.write(b'date', b'(%s) ', fm.formatdate(marker.date())) | ||
|
r29795 | meta = marker.metadata().copy() | ||
|
r43347 | meta.pop(b'date', None) | ||
|
r38594 | smeta = pycompat.rapply(pycompat.maybebytestr, meta) | ||
|
r43347 | fm.write( | ||
b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ') | ||||
) | ||||
fm.plain(b'\n') | ||||
|
r20470 | |||
|
r43346 | |||
|
r3814 | def finddate(ui, repo, date): | ||
"""Find the tipmost changeset that matches the given date spec""" | ||||
|
r46029 | mrevs = repo.revs(b'date(%s)', date) | ||
try: | ||||
rev = mrevs.max() | ||||
except ValueError: | ||||
|
r46431 | raise error.InputError(_(b"revision matching date not found")) | ||
|
r46029 | |||
ui.status( | ||||
_(b"found revision %d from %s\n") | ||||
% (rev, dateutil.datestr(repo[rev].date())) | ||||
) | ||||
return b'%d' % rev | ||||
|
r3814 | |||
|
r43346 | |||
|
r41799 | def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts): | ||
|
r12269 | bad = [] | ||
|
r25436 | |||
badfn = lambda x, y: bad.append(x) or match.bad(x, y) | ||||
|
r12269 | names = [] | ||
|
r12270 | wctx = repo[None] | ||
|
r14138 | cca = None | ||
abort, warn = scmutil.checkportabilityalert(ui) | ||||
if abort or warn: | ||||
|
r17201 | cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate) | ||
|
r25436 | |||
|
r40123 | match = repo.narrowmatch(match, includeexact=True) | ||
|
r26206 | badmatch = matchmod.badmatch(match, badfn) | ||
dirstate = repo.dirstate | ||||
# We don't want to just call wctx.walk here, since it would return a lot of | ||||
# clean files, which we aren't interested in and takes time. | ||||
|
r43346 | for f in sorted( | ||
dirstate.walk( | ||||
badmatch, | ||||
subrepos=sorted(wctx.substate), | ||||
unknown=True, | ||||
ignored=False, | ||||
full=False, | ||||
) | ||||
): | ||||
|
r12269 | exact = match.exact(f) | ||
|
r23462 | if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f): | ||
|
r14138 | if cca: | ||
cca(f) | ||||
|
r12269 | names.append(f) | ||
if ui.verbose or not exact: | ||||
|
r43346 | ui.status( | ||
|
r43347 | _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added' | ||
|
r43346 | ) | ||
|
r12270 | |||
|
r18364 | for subpath in sorted(wctx.substate): | ||
|
r15410 | sub = wctx.sub(subpath) | ||
try: | ||||
|
r28017 | submatch = matchmod.subdirmatcher(subpath, match) | ||
|
r41777 | subprefix = repo.wvfs.reljoin(prefix, subpath) | ||
|
r41799 | subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn) | ||
|
r43906 | if opts.get('subrepos'): | ||
|
r43346 | bad.extend( | ||
sub.add(ui, submatch, subprefix, subuipathfn, False, **opts) | ||||
) | ||||
|
r15410 | else: | ||
|
r43346 | bad.extend( | ||
sub.add(ui, submatch, subprefix, subuipathfn, True, **opts) | ||||
) | ||||
|
r15410 | except error.LookupError: | ||
|
r43346 | ui.status( | ||
|
r43347 | _(b"skipping missing subrepository: %s\n") % uipathfn(subpath) | ||
|
r43346 | ) | ||
|
r12270 | |||
|
r43906 | if not opts.get('dry_run'): | ||
|
r12270 | rejected = wctx.add(names, prefix) | ||
|
r12269 | bad.extend(f for f in rejected if f in match.files()) | ||
return bad | ||||
|
r43346 | |||
|
r32005 | def addwebdirpath(repo, serverpath, webconf): | ||
webconf[serverpath] = repo.root | ||||
|
r43347 | repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root)) | ||
for r in repo.revs(b'filelog("path:.hgsub")'): | ||||
|
r32005 | ctx = repo[r] | ||
for subpath in ctx.substate: | ||||
ctx.sub(subpath).addwebdirpath(serverpath, webconf) | ||||
|
r43346 | |||
def forget( | ||||
ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive | ||||
): | ||||
|
r37796 | if dryrun and interactive: | ||
|
r46431 | raise error.InputError( | ||
_(b"cannot specify both --dry-run and --interactive") | ||||
) | ||||
|
r15912 | bad = [] | ||
|
r25437 | badfn = lambda x, y: bad.append(x) or match.bad(x, y) | ||
|
r15912 | wctx = repo[None] | ||
forgot = [] | ||||
|
r25437 | |||
s = repo.status(match=matchmod.badmatch(match, badfn), clean=True) | ||||
|
r32174 | forget = sorted(s.modified + s.added + s.deleted + s.clean) | ||
|
r15912 | if explicitonly: | ||
forget = [f for f in forget if match.exact(f)] | ||||
|
r18364 | for subpath in sorted(wctx.substate): | ||
|
r15912 | sub = wctx.sub(subpath) | ||
|
r41776 | submatch = matchmod.subdirmatcher(subpath, match) | ||
subprefix = repo.wvfs.reljoin(prefix, subpath) | ||||
|
r41802 | subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn) | ||
|
r15912 | try: | ||
|
r43346 | subbad, subforgot = sub.forget( | ||
submatch, | ||||
subprefix, | ||||
subuipathfn, | ||||
dryrun=dryrun, | ||||
interactive=interactive, | ||||
) | ||||
|
r43347 | bad.extend([subpath + b'/' + f for f in subbad]) | ||
forgot.extend([subpath + b'/' + f for f in subforgot]) | ||||
|
r15912 | except error.LookupError: | ||
|
r43346 | ui.status( | ||
|
r43347 | _(b"skipping missing subrepository: %s\n") % uipathfn(subpath) | ||
|
r43346 | ) | ||
|
r15912 | |||
|
r16070 | if not explicitonly: | ||
for f in match.files(): | ||||
|
r23673 | if f not in repo.dirstate and not repo.wvfs.isdir(f): | ||
|
r15912 | if f not in forgot: | ||
|
r23673 | if repo.wvfs.exists(f): | ||
|
r24548 | # Don't complain if the exact case match wasn't given. | ||
# But don't do this until after checking 'forgot', so | ||||
# that subrepo files aren't normalized, and this op is | ||||
# purely from data cached by the status walk above. | ||||
if repo.dirstate.normalize(f) in repo.dirstate: | ||||
continue | ||||
|
r43346 | ui.warn( | ||
|
r43347 | _( | ||
b'not removing %s: ' | ||||
b'file is already untracked\n' | ||||
) | ||||
|
r43346 | % uipathfn(f) | ||
) | ||||
|
r15912 | bad.append(f) | ||
|
r37796 | if interactive: | ||
|
r43346 | responses = _( | ||
|
r43347 | b'[Ynsa?]' | ||
b'$$ &Yes, forget this file' | ||||
b'$$ &No, skip this file' | ||||
b'$$ &Skip remaining files' | ||||
b'$$ Include &all remaining files' | ||||
b'$$ &? (display help)' | ||||
|
r43346 | ) | ||
|
r37774 | for filename in forget[:]: | ||
|
r43346 | r = ui.promptchoice( | ||
|
r43347 | _(b'forget %s %s') % (uipathfn(filename), responses) | ||
|
r43346 | ) | ||
if r == 4: # ? | ||||
|
r37774 | while r == 4: | ||
for c, t in ui.extractchoices(responses)[1]: | ||||
|
r43347 | ui.write(b'%s - %s\n' % (c, encoding.lower(t))) | ||
|
r43346 | r = ui.promptchoice( | ||
|
r43347 | _(b'forget %s %s') % (uipathfn(filename), responses) | ||
|
r43346 | ) | ||
if r == 0: # yes | ||||
|
r37774 | continue | ||
|
r43346 | elif r == 1: # no | ||
|
r37774 | forget.remove(filename) | ||
|
r43346 | elif r == 2: # Skip | ||
|
r37774 | fnindex = forget.index(filename) | ||
del forget[fnindex:] | ||||
break | ||||
|
r43346 | elif r == 3: # All | ||
|
r37774 | break | ||
|
r15912 | for f in forget: | ||
|
r37796 | if ui.verbose or not match.exact(f) or interactive: | ||
|
r43346 | ui.status( | ||
|
r43347 | _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed' | ||
|
r43346 | ) | ||
|
r15912 | |||
|
r36957 | if not dryrun: | ||
rejected = wctx.forget(forget, prefix) | ||||
bad.extend(f for f in rejected if f in match.files()) | ||||
forgot.extend(f for f in forget if f not in rejected) | ||||
|
r15912 | return bad, forgot | ||
|
r43346 | |||
|
r41914 | def files(ui, ctx, m, uipathfn, fm, fmt, subrepos): | ||
|
r24275 | ret = 1 | ||
|
r43347 | needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint() | ||
|
r45388 | if fm.isplain() and not needsfctx: | ||
# Fast path. The speed-up comes from skipping the formatter, and batching | ||||
# calls to ui.write. | ||||
buf = [] | ||||
for f in ctx.matches(m): | ||||
buf.append(fmt % uipathfn(f)) | ||||
if len(buf) > 100: | ||||
ui.write(b''.join(buf)) | ||||
del buf[:] | ||||
ret = 0 | ||||
if buf: | ||||
ui.write(b''.join(buf)) | ||||
else: | ||||
for f in ctx.matches(m): | ||||
fm.startitem() | ||||
fm.context(ctx=ctx) | ||||
if needsfctx: | ||||
fc = ctx[f] | ||||
fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags()) | ||||
fm.data(path=f) | ||||
fm.plain(fmt % uipathfn(f)) | ||||
ret = 0 | ||||
|
r24275 | |||
|
r25228 | for subpath in sorted(ctx.substate): | ||
|
r29802 | submatch = matchmod.subdirmatcher(subpath, m) | ||
|
r41914 | subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn) | ||
|
r43346 | if subrepos or m.exact(subpath) or any(submatch.files()): | ||
|
r24413 | sub = ctx.sub(subpath) | ||
try: | ||||
|
r28387 | recurse = m.exact(subpath) or subrepos | ||
|
r43346 | if ( | ||
sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse) | ||||
== 0 | ||||
): | ||||
|
r24413 | ret = 0 | ||
except error.LookupError: | ||||
|
r43346 | ui.status( | ||
|
r43347 | _(b"skipping missing subrepository: %s\n") | ||
|
r43346 | % uipathfn(subpath) | ||
) | ||||
|
r24413 | |||
|
r24275 | return ret | ||
|
r43346 | |||
def remove( | ||||
ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None | ||||
): | ||||
|
r23289 | ret = 0 | ||
s = repo.status(match=m, clean=True) | ||||
|
r44043 | modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean | ||
|
r23289 | |||
|
r23325 | wctx = repo[None] | ||
|
r28607 | if warnings is None: | ||
warnings = [] | ||||
warn = True | ||||
else: | ||||
warn = False | ||||
|
r28608 | subs = sorted(wctx.substate) | ||
|
r43346 | progress = ui.makeprogress( | ||
|
r43347 | _(b'searching'), total=len(subs), unit=_(b'subrepos') | ||
|
r43346 | ) | ||
|
r28608 | for subpath in subs: | ||
|
r29802 | submatch = matchmod.subdirmatcher(subpath, m) | ||
|
r41775 | subprefix = repo.wvfs.reljoin(prefix, subpath) | ||
|
r41800 | subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn) | ||
|
r29802 | if subrepos or m.exact(subpath) or any(submatch.files()): | ||
|
r38366 | progress.increment() | ||
|
r23325 | sub = wctx.sub(subpath) | ||
try: | ||||
|
r43346 | if sub.removefiles( | ||
submatch, | ||||
subprefix, | ||||
subuipathfn, | ||||
after, | ||||
force, | ||||
subrepos, | ||||
dryrun, | ||||
warnings, | ||||
): | ||||
|
r23325 | ret = 1 | ||
except error.LookupError: | ||||
|
r43346 | warnings.append( | ||
|
r43347 | _(b"skipping missing subrepository: %s\n") | ||
|
r43346 | % uipathfn(subpath) | ||
) | ||||
|
r38392 | progress.complete() | ||
|
r23325 | |||
|
r23289 | # warn about failure to delete explicit files/dirs | ||
r43923 | deleteddirs = pathutil.dirs(deleted) | |||
|
r28608 | files = m.files() | ||
|
r43347 | progress = ui.makeprogress( | ||
_(b'deleting'), total=len(files), unit=_(b'files') | ||||
) | ||||
|
r28608 | for f in files: | ||
|
r43346 | |||
|
r23325 | def insubrepo(): | ||
for subpath in wctx.substate: | ||||
|
r43347 | if f.startswith(subpath + b'/'): | ||
|
r23325 | return True | ||
return False | ||||
|
r38366 | progress.increment() | ||
|
r24955 | isdir = f in deleteddirs or wctx.hasdir(f) | ||
|
r43347 | if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs: | ||
|
r23289 | continue | ||
|
r23325 | |||
|
r23674 | if repo.wvfs.exists(f): | ||
if repo.wvfs.isdir(f): | ||||
|
r43346 | warnings.append( | ||
|
r43347 | _(b'not removing %s: no tracked files\n') % uipathfn(f) | ||
|
r43346 | ) | ||
|
r23289 | else: | ||
|
r43346 | warnings.append( | ||
|
r43347 | _(b'not removing %s: file is untracked\n') % uipathfn(f) | ||
|
r43346 | ) | ||
|
r23289 | # missing files will generate a warning elsewhere | ||
ret = 1 | ||||
|
r38392 | progress.complete() | ||
|
r23289 | |||
if force: | ||||
list = modified + deleted + clean + added | ||||
elif after: | ||||
list = deleted | ||||
|
r28608 | remaining = modified + added + clean | ||
|
r43346 | progress = ui.makeprogress( | ||
|
r43347 | _(b'skipping'), total=len(remaining), unit=_(b'files') | ||
|
r43346 | ) | ||
|
r28608 | for f in remaining: | ||
|
r38366 | progress.increment() | ||
|
r35120 | if ui.verbose or (f in files): | ||
|
r43346 | warnings.append( | ||
|
r43347 | _(b'not removing %s: file still exists\n') % uipathfn(f) | ||
|
r43346 | ) | ||
|
r23289 | ret = 1 | ||
|
r38392 | progress.complete() | ||
|
r23289 | else: | ||
list = deleted + clean | ||||
|
r43346 | progress = ui.makeprogress( | ||
|
r43347 | _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files') | ||
|
r43346 | ) | ||
|
r23289 | for f in modified: | ||
|
r38366 | progress.increment() | ||
|
r43346 | warnings.append( | ||
_( | ||||
|
r43347 | b'not removing %s: file is modified (use -f' | ||
b' to force removal)\n' | ||||
|
r43346 | ) | ||
% uipathfn(f) | ||||
) | ||||
|
r23289 | ret = 1 | ||
for f in added: | ||||
|
r38366 | progress.increment() | ||
|
r43346 | warnings.append( | ||
_( | ||||
|
r43347 | b"not removing %s: file has been marked for add" | ||
b" (use 'hg forget' to undo add)\n" | ||||
|
r43346 | ) | ||
% uipathfn(f) | ||||
) | ||||
|
r23289 | ret = 1 | ||
|
r38392 | progress.complete() | ||
|
r28608 | |||
list = sorted(list) | ||||
|
r43347 | progress = ui.makeprogress( | ||
_(b'deleting'), total=len(list), unit=_(b'files') | ||||
) | ||||
|
r28608 | for f in list: | ||
|
r23289 | if ui.verbose or not m.exact(f): | ||
|
r38366 | progress.increment() | ||
|
r43346 | ui.status( | ||
|
r43347 | _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed' | ||
|
r43346 | ) | ||
|
r38392 | progress.complete() | ||
|
r23289 | |||
|
r37168 | if not dryrun: | ||
with repo.wlock(): | ||||
if not after: | ||||
for f in list: | ||||
if f in added: | ||||
|
r43346 | continue # we never unlink added files on remove | ||
rmdir = repo.ui.configbool( | ||||
|
r43347 | b'experimental', b'removeemptydirs' | ||
|
r43346 | ) | ||
|
r38512 | repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir) | ||
|
r37168 | repo[None].forget(list) | ||
|
r23289 | |||
|
r28607 | if warn: | ||
for warning in warnings: | ||||
ui.warn(warning) | ||||
|
r23289 | return ret | ||
|
r43346 | |||
|
r42678 | def _catfmtneedsdata(fm): | ||
|
r43347 | return not fm.datahint() or b'data' in fm.datahint() | ||
|
r42678 | |||
|
r43346 | |||
|
r35680 | def _updatecatformatter(fm, ctx, matcher, path, decode): | ||
"""Hook for adding data to the formatter used by ``hg cat``. | ||||
Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call | ||||
this method first.""" | ||||
|
r42678 | |||
# data() can be expensive to fetch (e.g. lfs), so don't fetch it if it | ||||
# wasn't requested. | ||||
data = b'' | ||||
if _catfmtneedsdata(fm): | ||||
data = ctx[path].data() | ||||
if decode: | ||||
data = ctx.repo().wwritedata(path, data) | ||||
|
r35680 | fm.startitem() | ||
|
r38557 | fm.context(ctx=ctx) | ||
|
r43347 | fm.write(b'data', b'%s', data) | ||
|
r39405 | fm.data(path=path) | ||
|
r35680 | |||
|
r43346 | |||
|
r32578 | def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts): | ||
|
r21040 | err = 1 | ||
|
r35351 | opts = pycompat.byteskwargs(opts) | ||
|
r21040 | |||
def write(path): | ||||
|
r32578 | filename = None | ||
|
r32576 | if fntemplate: | ||
|
r43346 | filename = makefilename( | ||
ctx, fntemplate, pathname=os.path.join(prefix, path) | ||||
) | ||||
|
r35010 | # attempt to create the directory if it does not already exist | ||
try: | ||||
os.makedirs(os.path.dirname(filename)) | ||||
except OSError: | ||||
pass | ||||
|
r37615 | with formatter.maybereopen(basefm, filename) as fm: | ||
|
r43347 | _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode')) | ||
|
r21040 | |||
# Automation often uses hg cat on single files, so special case it | ||||
# for performance to avoid the cost of parsing the manifest. | ||||
if len(matcher.files()) == 1 and not matcher.anypats(): | ||||
file = matcher.files()[0] | ||||
|
r30340 | mfl = repo.manifestlog | ||
|
r24718 | mfnode = ctx.manifestnode() | ||
|
r30340 | try: | ||
if mfnode and mfl[mfnode].find(file)[0]: | ||||
|
r42678 | if _catfmtneedsdata(basefm): | ||
|
r45632 | scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)]) | ||
|
r30340 | write(file) | ||
return 0 | ||||
except KeyError: | ||||
pass | ||||
|
r21040 | |||
|
r42678 | if _catfmtneedsdata(basefm): | ||
|
r45632 | scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)]) | ||
|
r37780 | |||
for abs in ctx.walk(matcher): | ||||
|
r21040 | write(abs) | ||
err = 0 | ||||
|
r21041 | |||
|
r41812 | uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True) | ||
|
r21041 | for subpath in sorted(ctx.substate): | ||
sub = ctx.sub(subpath) | ||||
try: | ||||
|
r28017 | submatch = matchmod.subdirmatcher(subpath, matcher) | ||
|
r41798 | subprefix = os.path.join(prefix, subpath) | ||
|
r43346 | if not sub.cat( | ||
submatch, | ||||
basefm, | ||||
fntemplate, | ||||
subprefix, | ||||
**pycompat.strkwargs(opts) | ||||
): | ||||
|
r21041 | err = 0 | ||
except error.RepoLookupError: | ||||
|
r43346 | ui.status( | ||
|
r43347 | _(b"skipping missing subrepository: %s\n") % uipathfn(subpath) | ||
|
r43346 | ) | ||
|
r21041 | |||
|
r21040 | return err | ||
|
r43346 | |||
|
r5034 | def commit(ui, repo, commitfunc, pats, opts): | ||
'''commit the specified files or all outstanding changes''' | ||||
|
r43347 | date = opts.get(b'date') | ||
|
r6139 | if date: | ||
|
r43347 | opts[b'date'] = dateutil.parsedate(date) | ||
|
r14635 | message = logmessage(ui, opts) | ||
|
r23533 | matcher = scmutil.match(repo[None], pats, opts) | ||
|
r5034 | |||
|
r33619 | dsguard = None | ||
|
r5829 | # extract addremove carefully -- this function can be called from a command | ||
# that doesn't support addremove | ||||
|
r43347 | if opts.get(b'addremove'): | ||
dsguard = dirstateguard.dirstateguard(repo, b'commit') | ||||
|
r33823 | with dsguard or util.nullcontextmanager(): | ||
|
r33822 | if dsguard: | ||
|
r41801 | relative = scmutil.anypats(pats, opts) | ||
|
r41834 | uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative) | ||
|
r43347 | if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0: | ||
|
r33619 | raise error.Abort( | ||
|
r43347 | _(b"failed to mark all new/missing files as added/removed") | ||
|
r43346 | ) | ||
|
r33619 | |||
|
r33823 | return commitfunc(ui, repo, message, matcher, opts) | ||
|
r8407 | |||
|
r43346 | |||
|
r29819 | def samefile(f, ctx1, ctx2): | ||
if f in ctx1.manifest(): | ||||
a = ctx1.filectx(f) | ||||
if f in ctx2.manifest(): | ||||
b = ctx2.filectx(f) | ||||
|
r43346 | return not a.cmp(b) and a.flags() == b.flags() | ||
|
r29819 | else: | ||
return False | ||||
else: | ||||
return f not in ctx2.manifest() | ||||
|
r43346 | |||
|
r34088 | def amend(ui, repo, old, extra, pats, opts): | ||
|
r25930 | # avoid cycle context -> subrepo -> cmdutil | ||
|
r28322 | from . import context | ||
|
r25930 | |||
|
r23101 | # amend will reuse the existing user if not specified, but the obsolete | ||
# marker creation requires that the current user's name is specified. | ||||
|
r24379 | if obsolete.isenabled(repo, obsolete.createmarkersopt): | ||
|
r43346 | ui.username() # raise exception if username not set | ||
|
r23101 | |||
|
r43347 | ui.note(_(b'amending changeset %s\n') % old) | ||
|
r16458 | base = old.p1() | ||
|
r43347 | with repo.wlock(), repo.lock(), repo.transaction(b'amend'): | ||
|
r33438 | # Participating changesets: | ||
# | ||||
|
r34087 | # wctx o - workingctx that contains changes from working copy | ||
# | to go into amending commit | ||||
|
r33438 | # | | ||
# old o - changeset to amend | ||||
# | | ||||
|
r34058 | # base o - first parent of the changeset to amend | ||
|
r34087 | wctx = repo[None] | ||
|
r33438 | |||
|
r35197 | # Copy to avoid mutating input | ||
extra = extra.copy() | ||||
|
r33438 | # Update extra dict from amended commit (e.g. to preserve graft | ||
# source) | ||||
extra.update(old.extra()) | ||||
|
r34087 | # Also update it from the from the wctx | ||
extra.update(wctx.extra()) | ||||
|
r43202 | # date-only change should be ignored? | ||
|
r48225 | datemaydiffer = resolve_commit_options(ui, opts) | ||
opts = pycompat.byteskwargs(opts) | ||||
|
r43202 | |||
date = old.date() | ||||
|
r43347 | if opts.get(b'date'): | ||
date = dateutil.parsedate(opts.get(b'date')) | ||||
user = opts.get(b'user') or old.user() | ||||
|
r34123 | |||
|
r33438 | if len(old.parents()) > 1: | ||
# ctx.files() isn't reliable for merges, so fall back to the | ||||
# slower repo.status() method | ||||
|
r44043 | st = base.status(old) | ||
files = set(st.modified) | set(st.added) | set(st.removed) | ||||
|
r33438 | else: | ||
files = set(old.files()) | ||||
|
r34087 | # add/remove the files to the working copy if the "addremove" option | ||
# was specified. | ||||
matcher = scmutil.match(wctx, pats, opts) | ||||
|
r41801 | relative = scmutil.anypats(pats, opts) | ||
|
r41834 | uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative) | ||
|
r43347 | if opts.get(b'addremove') and scmutil.addremove( | ||
repo, matcher, b"", uipathfn, opts | ||||
|
r43346 | ): | ||
|
r34087 | raise error.Abort( | ||
|
r43347 | _(b"failed to mark all new/missing files as added/removed") | ||
|
r43346 | ) | ||
|
r34087 | |||
|
r35019 | # Check subrepos. This depends on in-place wctx._status update in | ||
# subrepo.precommit(). To minimize the risk of this hack, we do | ||||
# nothing if .hgsub does not exist. | ||||
|
r43347 | if b'.hgsub' in wctx or b'.hgsub' in old: | ||
|
r36026 | subs, commitsubs, newsubstate = subrepoutil.precommit( | ||
|
r43346 | ui, wctx, wctx._status, matcher | ||
) | ||||
|
r35019 | # amend should abort if commitsubrepos is enabled | ||
assert not commitsubs | ||||
if subs: | ||||
|
r36026 | subrepoutil.writestate(repo, newsubstate) | ||
|
r35019 | |||
|
r45383 | ms = mergestatemod.mergestate.read(repo) | ||
|
r36927 | mergeutil.checkunresolved(ms) | ||
|
r44937 | filestoamend = {f for f in wctx.files() if matcher(f)} | ||
|
r34087 | |||
|
r43346 | changes = len(filestoamend) > 0 | ||
|
r34087 | if changes: | ||
|
r33438 | # Recompute copies (avoid recording a -> b -> a) | ||
|
r34087 | copied = copies.pathcopies(base, wctx, matcher) | ||
|
r33438 | if old.p2: | ||
|
r34087 | copied.update(copies.pathcopies(old.p2(), wctx, matcher)) | ||
|
r33438 | |||
# Prune files which were reverted by the updates: if old | ||||
|
r34087 | # introduced file X and the file was renamed in the working | ||
# copy, then those two files are the same and | ||||
|
r33438 | # we can discard X from our list of files. Likewise if X | ||
|
r35016 | # was removed, it's no longer relevant. If X is missing (aka | ||
# deleted), old X must be preserved. | ||||
|
r34087 | files.update(filestoamend) | ||
|
r43346 | files = [ | ||
f | ||||
for f in files | ||||
if (f not in filestoamend or not samefile(f, wctx, base)) | ||||
] | ||||
|
r33438 | |||
def filectxfn(repo, ctx_, path): | ||||
try: | ||||
|
r34087 | # If the file being considered is not amongst the files | ||
# to be amended, we should return the file context from the | ||||
# old changeset. This avoids issues when only some files in | ||||
# the working copy are being amended but there are also | ||||
# changes to other files from the old changeset. | ||||
if path not in filestoamend: | ||||
return old.filectx(path) | ||||
|
r35015 | # Return None for removed files. | ||
if path in wctx.removed(): | ||||
return None | ||||
|
r34087 | fctx = wctx[path] | ||
|
r33438 | flags = fctx.flags() | ||
|
r43346 | mctx = context.memfilectx( | ||
repo, | ||||
ctx_, | ||||
fctx.path(), | ||||
fctx.data(), | ||||
|
r43347 | islink=b'l' in flags, | ||
isexec=b'x' in flags, | ||||
|
r43346 | copysource=copied.get(path), | ||
) | ||||
|
r33438 | return mctx | ||
except KeyError: | ||||
return None | ||||
|
r43346 | |||
|
r33438 | else: | ||
|
r43347 | ui.note(_(b'copying changeset %s to %s\n') % (old, base)) | ||
|
r33438 | |||
# Use version of files as in the old cset | ||||
def filectxfn(repo, ctx_, path): | ||||
try: | ||||
return old.filectx(path) | ||||
except KeyError: | ||||
return None | ||||
|
r34087 | # See if we got a message from -m or -l, if not, open the editor with | ||
# the message of the changeset to amend. | ||||
message = logmessage(ui, opts) | ||||
|
r43347 | editform = mergeeditform(old, b'commit.amend') | ||
|
r34087 | |||
|
r33438 | if not message: | ||
message = old.description() | ||||
|
r42591 | # Default if message isn't provided and --edit is not passed is to | ||
# invoke editor, but allow --no-edit. If somehow we don't have any | ||||
# description, let's always start the editor. | ||||
|
r43347 | doedit = not message or opts.get(b'edit') in [True, None] | ||
|
r42591 | else: | ||
# Default if message is provided is to not invoke editor, but allow | ||||
# --edit. | ||||
|
r43347 | doedit = opts.get(b'edit') is True | ||
|
r42591 | editor = getcommiteditor(edit=doedit, editform=editform) | ||
|
r33438 | |||
pureextra = extra.copy() | ||||
|
r43347 | extra[b'amend_source'] = old.hex() | ||
|
r33438 | |||
|
r43346 | new = context.memctx( | ||
repo, | ||||
parents=[base.node(), old.p2().node()], | ||||
text=message, | ||||
files=files, | ||||
filectxfn=filectxfn, | ||||
user=user, | ||||
date=date, | ||||
extra=extra, | ||||
editor=editor, | ||||
) | ||||
|
r33438 | |||
newdesc = changelog.stripdesc(new.description()) | ||||
|
r43346 | if ( | ||
(not changes) | ||||
|
r33438 | and newdesc == old.description() | ||
and user == old.user() | ||||
|
r41159 | and (date == old.date() or datemaydiffer) | ||
|
r43346 | and pureextra == old.extra() | ||
): | ||||
|
r33438 | # nothing changed. continuing here would create a new node | ||
# anyway because of the amend_source noise. | ||||
|
r17472 | # | ||
|
r33438 | # This not what we expect from amend. | ||
|
r41159 | return old.node() | ||
|
r33438 | |||
|
r38442 | commitphase = None | ||
|
r43347 | if opts.get(b'secret'): | ||
|
r38442 | commitphase = phases.secret | ||
newid = repo.commitctx(new) | ||||
|
r45948 | ms.reset() | ||
|
r34057 | |||
# Reroute the working copy parent to the new changeset | ||||
|
r47771 | repo.setparents(newid, repo.nullid) | ||
|
r34057 | |||
|
r34087 | # Fixing the dirstate because localrepo.commitctx does not update | ||
# it. This is rather convenient because we did not need to update | ||||
# the dirstate for all the files in the new commit which commitctx | ||||
# could have done if it updated the dirstate. Now, we can | ||||
# selectively update the dirstate only for the amended files. | ||||
dirstate = repo.dirstate | ||||
|
r44238 | # Update the state of the files which were added and modified in the | ||
# amend to "normal" in the dirstate. We need to use "normallookup" since | ||||
# the files may have changed since the command started; using "normal" | ||||
# would mark them as clean but with uncommitted contents. | ||||
|
r34087 | normalfiles = set(wctx.modified() + wctx.added()) & filestoamend | ||
for f in normalfiles: | ||||
|
r44238 | dirstate.normallookup(f) | ||
|
r34087 | |||
# Update the state of files which were removed in the amend | ||||
# to "removed" in the dirstate. | ||||
removedfiles = set(wctx.removed()) & filestoamend | ||||
for f in removedfiles: | ||||
dirstate.drop(f) | ||||
|
r47499 | mapping = {old.node(): (newid,)} | ||
obsmetadata = None | ||||
if opts.get(b'note'): | ||||
obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])} | ||||
backup = ui.configbool(b'rewrite', b'backup-bundle') | ||||
scmutil.cleanupnodes( | ||||
repo, | ||||
mapping, | ||||
b'amend', | ||||
metadata=obsmetadata, | ||||
fixphase=True, | ||||
targetphase=commitphase, | ||||
backup=backup, | ||||
) | ||||
|
r16458 | return newid | ||
|
r43346 | |||
|
r43347 | def commiteditor(repo, ctx, subs, editform=b''): | ||
|
r8407 | if ctx.description(): | ||
return ctx.description() | ||||
|
r43346 | return commitforceeditor( | ||
repo, ctx, subs, editform=editform, unchangedmessagedetection=True | ||||
) | ||||
def commitforceeditor( | ||||
repo, | ||||
ctx, | ||||
subs, | ||||
finishdesc=None, | ||||
extramsg=None, | ||||
|
r43347 | editform=b'', | ||
|
r43346 | unchangedmessagedetection=False, | ||
): | ||||
|
r21923 | if not extramsg: | ||
|
r43347 | extramsg = _(b"Leave message empty to abort commit.") | ||
forms = [e for e in editform.split(b'.') if e] | ||||
forms.insert(0, b'changeset') | ||||
|
r26742 | templatetext = None | ||
|
r22012 | while forms: | ||
|
r43347 | ref = b'.'.join(forms) | ||
if repo.ui.config(b'committemplate', ref): | ||||
|
r26742 | templatetext = committext = buildcommittemplate( | ||
|
r43346 | repo, ctx, subs, extramsg, ref | ||
) | ||||
|
r22012 | break | ||
forms.pop() | ||||
|
r21924 | else: | ||
committext = buildcommittext(repo, ctx, subs, extramsg) | ||||
|
r21869 | |||
# run editor in the repository root | ||||
|
r39843 | olddir = encoding.getcwd() | ||
|
r21869 | os.chdir(repo.root) | ||
|
r26750 | |||
# make in-memory changes visible to external process | ||||
tr = repo.currenttransaction() | ||||
repo.dirstate.write(tr) | ||||
pending = tr and tr.writepending() and repo.root | ||||
|
r43346 | editortext = repo.ui.edit( | ||
committext, | ||||
ctx.user(), | ||||
ctx.extra(), | ||||
editform=editform, | ||||
pending=pending, | ||||
repopath=repo.path, | ||||
|
r43347 | action=b'commit', | ||
|
r43346 | ) | ||
|
r30724 | text = editortext | ||
|
r30703 | |||
# strip away anything below this special string (used for editors that want | ||||
# to display the diff) | ||||
|
r30724 | stripbelow = re.search(_linebelow, text, flags=re.MULTILINE) | ||
|
r30703 | if stripbelow: | ||
|
r43346 | text = text[: stripbelow.start()] | ||
|
r30724 | |||
|
r43347 | text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text) | ||
|
r21869 | os.chdir(olddir) | ||
if finishdesc: | ||||
text = finishdesc(text) | ||||
if not text.strip(): | ||||
|
r46431 | raise error.InputError(_(b"empty commit message")) | ||
|
r26742 | if unchangedmessagedetection and editortext == templatetext: | ||
|
r46431 | raise error.InputError(_(b"commit message unchanged")) | ||
|
r21869 | |||
return text | ||||
|
r43346 | |||
|
r32878 | def buildcommittemplate(repo, ctx, subs, extramsg, ref): | ||
|
r21924 | ui = repo.ui | ||
|
r45824 | spec = formatter.reference_templatespec(ref) | ||
|
r35972 | t = logcmdutil.changesettemplater(ui, repo, spec) | ||
|
r43346 | t.t.cache.update( | ||
(k, templater.unquotestring(v)) | ||||
|
r43347 | for k, v in repo.ui.configitems(b'committemplate') | ||
|
r43346 | ) | ||
|
r22013 | |||
|
r21924 | if not extramsg: | ||
|
r43347 | extramsg = b'' # ensure that extramsg is string | ||
|
r21924 | |||
ui.pushbuffer() | ||||
t.show(ctx, extramsg=extramsg) | ||||
return ui.popbuffer() | ||||
|
r43346 | |||
|
r26426 | def hgprefix(msg): | ||
|
r43347 | return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a]) | ||
|
r26426 | |||
|
r43346 | |||
|
r21869 | def buildcommittext(repo, ctx, subs, extramsg): | ||
|
r8407 | edittext = [] | ||
|
r8707 | modified, added, removed = ctx.modified(), ctx.added(), ctx.removed() | ||
|
r8407 | if ctx.description(): | ||
edittext.append(ctx.description()) | ||||
|
r43347 | edittext.append(b"") | ||
edittext.append(b"") # Empty line between message and comments. | ||||
|
r43346 | edittext.append( | ||
hgprefix( | ||||
_( | ||||
|
r43347 | b"Enter commit message." | ||
b" Lines beginning with 'HG:' are removed." | ||||
|
r43346 | ) | ||
) | ||||
) | ||||
|
r26426 | edittext.append(hgprefix(extramsg)) | ||
|
r43347 | edittext.append(b"HG: --") | ||
edittext.append(hgprefix(_(b"user: %s") % ctx.user())) | ||||
|
r8407 | if ctx.p2(): | ||
|
r43347 | edittext.append(hgprefix(_(b"branch merge"))) | ||
|
r8407 | if ctx.branch(): | ||
|
r43347 | edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch())) | ||
|
r24986 | if bookmarks.isactivewdirparent(repo): | ||
|
r43347 | edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark)) | ||
edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs]) | ||||
edittext.extend([hgprefix(_(b"added %s") % f) for f in added]) | ||||
edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified]) | ||||
edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed]) | ||||
|
r8707 | if not added and not modified and not removed: | ||
|
r43347 | edittext.append(hgprefix(_(b"no files changed"))) | ||
edittext.append(b"") | ||||
return b"\n".join(edittext) | ||||
|
r14297 | |||
|
r43346 | |||
|
r46414 | def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None): | ||
|
r26324 | if opts is None: | ||
opts = {} | ||||
|
r18688 | ctx = repo[node] | ||
parents = ctx.parents() | ||||
|
r46414 | if tip is not None and repo.changelog.tip() == tip: | ||
# avoid reporting something like "committed new head" when | ||||
# recommitting old changesets, and issue a helpful warning | ||||
# for most instances | ||||
r47204 | repo.ui.warn(_(b"warning: commit already existed in the repository!\n")) | |||
|
r46414 | elif ( | ||
|
r43347 | not opts.get(b'amend') | ||
|
r43346 | and bheads | ||
and node not in bheads | ||||
|
r45581 | and not any( | ||
|
r45582 | p.node() in bheads and p.branch() == branch for p in parents | ||
|
r45581 | ) | ||
|
r43346 | ): | ||
|
r43347 | repo.ui.status(_(b'created new head\n')) | ||
|
r18688 | # The message is not printed for initial roots. For the other | ||
# changesets, it is printed in the following situations: | ||||
# | ||||
# Par column: for the 2 parents with ... | ||||
# N: null or no parent | ||||
# B: parent is on another named branch | ||||
# C: parent is a regular non head changeset | ||||
# H: parent was a branch head of the current branch | ||||
# Msg column: whether we print "created new head" message | ||||
# In the following, it is assumed that there already exists some | ||||
# initial branch heads of the current branch, otherwise nothing is | ||||
# printed anyway. | ||||
# | ||||
# Par Msg Comment | ||||
# N N y additional topo root | ||||
# | ||||
# B N y additional branch root | ||||
# C N y additional topo head | ||||
# H N n usual case | ||||
# | ||||
# B B y weird additional branch root | ||||
# C B y branch merge | ||||
# H B n merge with named branch | ||||
# | ||||
# C C y additional head from merge | ||||
# C H n merge with a head | ||||
# | ||||
# H H n head merge: head count decreases | ||||
|
r43347 | if not opts.get(b'close_branch'): | ||
|
r18688 | for r in parents: | ||
if r.closesbranch() and r.branch() == branch: | ||||
|
r43347 | repo.ui.status( | ||
_(b'reopening closed branch head %d\n') % r.rev() | ||||
) | ||||
|
r18688 | |||
if repo.ui.debugflag: | ||||
|
r43347 | repo.ui.write( | ||
_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex()) | ||||
) | ||||
|
r18688 | elif repo.ui.verbose: | ||
|
r43347 | repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx)) | ||
|
r18688 | |||
|
r43346 | |||
|
r27943 | def postcommitstatus(repo, pats, opts): | ||
return repo.status(match=scmutil.match(repo[None], pats, opts)) | ||||
|
r43346 | |||
|
r45935 | def revert(ui, repo, ctx, *pats, **opts): | ||
|
r35148 | opts = pycompat.byteskwargs(opts) | ||
|
r45935 | parent, p2 = repo.dirstate.parents() | ||
|
r16304 | node = ctx.node() | ||
mf = ctx.manifest() | ||||
|
r21579 | if node == p2: | ||
parent = p2 | ||||
|
r16304 | |||
# need all matching names in dirstate and manifest of target rev, | ||||
# so have to walk both. do not print errors if files exist in one | ||||
|
r24451 | # but not other. in both cases, filesets should be evaluated against | ||
# workingctx to get consistent result (issue4497). this means 'set:**' | ||||
# cannot be used to select missing files from target rev. | ||||
|
r16304 | |||
|
r21575 | # `names` is a mapping for all elements in working copy and target revision | ||
# The mapping is in the form: | ||||
|
r39323 | # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>) | ||
|
r16304 | names = {} | ||
|
r41754 | uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True) | ||
|
r16304 | |||
|
r27803 | with repo.wlock(): | ||
|
r21575 | ## filling of the `names` mapping | ||
# walk dirstate to fill `names` | ||||
|
r16304 | |||
|
r43347 | interactive = opts.get(b'interactive', False) | ||
|
r24449 | wctx = repo[None] | ||
m = scmutil.match(wctx, pats, opts) | ||||
|
r24479 | |||
# we'll need this later | ||||
targetsubs = sorted(s for s in wctx.substate if m(s)) | ||||
|
r24450 | if not m.always(): | ||
|
r32362 | matcher = matchmod.badmatch(m, lambda x, y: False) | ||
|
r32382 | for abs in wctx.walk(matcher): | ||
|
r41754 | names[abs] = m.exact(abs) | ||
|
r22573 | |||
# walk target manifest to fill `names` | ||||
def badfn(path, msg): | ||||
if path in names: | ||||
return | ||||
if path in ctx.substate: | ||||
|
r16304 | return | ||
|
r43347 | path_ = path + b'/' | ||
|
r22573 | for f in names: | ||
if f.startswith(path_): | ||||
return | ||||
|
r43347 | ui.warn(b"%s: %s\n" % (uipathfn(path), msg)) | ||
|
r22573 | |||
|
r25439 | for abs in ctx.walk(matchmod.badmatch(m, badfn)): | ||
|
r22573 | if abs not in names: | ||
|
r41754 | names[abs] = m.exact(abs) | ||
|
r22573 | |||
# Find status of all file in `names`. | ||||
m = scmutil.matchfiles(repo, names) | ||||
|
r43346 | changes = repo.status( | ||
node1=node, match=m, unknown=True, ignored=True, clean=True | ||||
) | ||||
|
r22573 | else: | ||
|
r24450 | changes = repo.status(node1=node, match=m) | ||
|
r22573 | for kind in changes: | ||
for abs in kind: | ||||
|
r41754 | names[abs] = m.exact(abs) | ||
|
r22573 | |||
m = scmutil.matchfiles(repo, names) | ||||
|
r23374 | modified = set(changes.modified) | ||
|
r43346 | added = set(changes.added) | ||
removed = set(changes.removed) | ||||
|
r23374 | _deleted = set(changes.deleted) | ||
|
r43346 | unknown = set(changes.unknown) | ||
|
r23374 | unknown.update(changes.ignored) | ||
|
r43346 | clean = set(changes.clean) | ||
|
r22610 | modadded = set() | ||
|
r22185 | |||
|
r24180 | # We need to account for the state of the file in the dirstate, | ||
# even when we revert against something else than parent. This will | ||||
|
r22155 | # slightly alter the behavior of revert (doing back up or not, delete | ||
|
r23139 | # or just forget etc). | ||
|
r22155 | if parent == node: | ||
dsmodified = modified | ||||
dsadded = added | ||||
dsremoved = removed | ||||
|
r23403 | # store all local modifications, useful later for rename detection | ||
localchanges = dsmodified | dsadded | ||||
|
r22155 | modified, added, removed = set(), set(), set() | ||
else: | ||||
changes = repo.status(node1=parent, match=m) | ||||
|
r23374 | dsmodified = set(changes.modified) | ||
|
r43346 | dsadded = set(changes.added) | ||
dsremoved = set(changes.removed) | ||||
|
r23403 | # store all local modifications, useful later for rename detection | ||
localchanges = dsmodified | dsadded | ||||
|
r16304 | |||
|
r22188 | # only take into account for removes between wc and target | ||
clean |= dsremoved - removed | ||||
dsremoved &= removed | ||||
# distinct between dirstate remove and other | ||||
removed -= dsremoved | ||||
|
r22610 | modadded = added & dsmodified | ||
added -= modadded | ||||
|
r22190 | # tell newly modified apart. | ||
dsmodified &= modified | ||||
|
r43346 | dsmodified |= modified & dsadded # dirstate added may need backup | ||
|
r22190 | modified -= dsmodified | ||
|
r22488 | # We need to wait for some post-processing to update this set | ||
# before making the distinction. The dirstate will be used for | ||||
# that purpose. | ||||
|
r22208 | dsadded = added | ||
|
r22209 | # in case of merge, files that are actually added can be reported as | ||
# modified, we need to post process the result | ||||
|
r47771 | if p2 != repo.nullid: | ||
|
r31134 | mergeadd = set(dsmodified) | ||
for path in dsmodified: | ||||
if path in mf: | ||||
mergeadd.remove(path) | ||||
|
r22209 | dsadded |= mergeadd | ||
dsmodified -= mergeadd | ||||
|
r16304 | |||
|
r21575 | # if f is a rename, update `names` to also revert the source | ||
|
r23403 | for f in localchanges: | ||
|
r16304 | src = repo.dirstate.copied(f) | ||
|
r22213 | # XXX should we check for rename down to target node? | ||
|
r43347 | if src and src not in names and repo.dirstate[src] == b'r': | ||
|
r22154 | dsremoved.add(src) | ||
|
r41754 | names[src] = True | ||
|
r16304 | |||
|
r31157 | # determine the exact nature of the deleted changesets | ||
deladded = set(_deleted) | ||||
for path in _deleted: | ||||
if path in mf: | ||||
deladded.remove(path) | ||||
deleted = _deleted - deladded | ||||
|
r22488 | # distinguish between file to forget and the other | ||
added = set() | ||||
for abs in dsadded: | ||||
|
r43347 | if repo.dirstate[abs] != b'a': | ||
|
r22488 | added.add(abs) | ||
dsadded -= added | ||||
|
r22490 | for abs in deladded: | ||
|
r43347 | if repo.dirstate[abs] == b'a': | ||
|
r22490 | dsadded.add(abs) | ||
deladded -= dsadded | ||||
|
r22396 | # For files marked as removed, we check if an unknown file is present at | ||
# the same path. If a such file exists it may need to be backed up. | ||||
# Making the distinction at this stage helps have simpler backup | ||||
# logic. | ||||
removunk = set() | ||||
for abs in removed: | ||||
target = repo.wjoin(abs) | ||||
if os.path.lexists(target): | ||||
removunk.add(abs) | ||||
removed -= removunk | ||||
dsremovunk = set() | ||||
for abs in dsremoved: | ||||
target = repo.wjoin(abs) | ||||
if os.path.lexists(target): | ||||
dsremovunk.add(abs) | ||||
dsremoved -= dsremovunk | ||||
|
r16304 | |||
|
r21575 | # action to be actually performed by revert | ||
# (<list of file>, message>) tuple | ||||
|
r43346 | actions = { | ||
|
r43347 | b'revert': ([], _(b'reverting %s\n')), | ||
b'add': ([], _(b'adding %s\n')), | ||||
b'remove': ([], _(b'removing %s\n')), | ||||
b'drop': ([], _(b'removing %s\n')), | ||||
b'forget': ([], _(b'forgetting %s\n')), | ||||
b'undelete': ([], _(b'undeleting %s\n')), | ||||
b'noop': (None, _(b'no changes needed to %s\n')), | ||||
b'unknown': (None, _(b'file not managed: %s\n')), | ||||
|
r43346 | } | ||
|
r16304 | |||
|
r22608 | # "constant" that convey the backup strategy. | ||
# All set to `discard` if `no-backup` is set do avoid checking | ||||
# no_backup lower in the code. | ||||
|
r22609 | # These values are ordered for comparison purposes | ||
|
r43346 | backupinteractive = 3 # do backup if interactively modified | ||
|
r22608 | backup = 2 # unconditionally do backup | ||
|
r43346 | check = 1 # check if the existing file differs from target | ||
discard = 0 # never do backup | ||||
|
r43347 | if opts.get(b'no_backup'): | ||
|
r29498 | backupinteractive = backup = check = discard | ||
if interactive: | ||||
dsmodifiedbackup = backupinteractive | ||||
else: | ||||
dsmodifiedbackup = backup | ||||
tobackup = set() | ||||
|
r16304 | |||
|
r43347 | backupanddel = actions[b'remove'] | ||
if not opts.get(b'no_backup'): | ||||
backupanddel = actions[b'drop'] | ||||
|
r22611 | |||
|
r16304 | disptable = ( | ||
# dispatch table: | ||||
# file state | ||||
|
r22153 | # action | ||
# make backup | ||||
|
r22371 | ## Sets that results that will change file on disk | ||
# Modified compared to target, no local change | ||||
|
r43347 | (modified, actions[b'revert'], discard), | ||
|
r22397 | # Modified compared to target, but local file is deleted | ||
|
r43347 | (deleted, actions[b'revert'], discard), | ||
|
r22371 | # Modified compared to target, local change | ||
|
r43347 | (dsmodified, actions[b'revert'], dsmodifiedbackup), | ||
|
r22371 | # Added since target | ||
|
r43347 | (added, actions[b'remove'], discard), | ||
|
r22488 | # Added in working directory | ||
|
r43347 | (dsadded, actions[b'forget'], discard), | ||
|
r22610 | # Added since target, have local modification | ||
|
r43346 | (modadded, backupanddel, backup), | ||
|
r22490 | # Added since target but file is missing in working directory | ||
|
r43347 | (deladded, actions[b'drop'], discard), | ||
|
r22371 | # Removed since target, before working copy parent | ||
|
r43347 | (removed, actions[b'add'], discard), | ||
|
r22396 | # Same as `removed` but an unknown file exists at the same path | ||
|
r43347 | (removunk, actions[b'add'], check), | ||
|
r22371 | # Removed since targe, marked as such in working copy parent | ||
|
r43347 | (dsremoved, actions[b'undelete'], discard), | ||
|
r22396 | # Same as `dsremoved` but an unknown file exists at the same path | ||
|
r43347 | (dsremovunk, actions[b'undelete'], check), | ||
|
r22371 | ## the following sets does not result in any file changes | ||
# File with no modification | ||||
|
r43347 | (clean, actions[b'noop'], discard), | ||
|
r22371 | # Existing file, not tracked anywhere | ||
|
r43347 | (unknown, actions[b'unknown'], discard), | ||
|
r43346 | ) | ||
|
r16304 | |||
|
r41754 | for abs, exact in sorted(names.items()): | ||
|
r21575 | # target file to be touch on disk (relative to cwd) | ||
|
r16304 | target = repo.wjoin(abs) | ||
|
r21575 | # search the entry in the dispatch table. | ||
|
r22212 | # if the file is in any of these sets, it was touched in the working | ||
|
r21575 | # directory parent and we are sure it needs to be reverted. | ||
|
r22232 | for table, (xlist, msg), dobackup in disptable: | ||
|
r16304 | if abs not in table: | ||
continue | ||||
|
r22233 | if xlist is not None: | ||
xlist.append(abs) | ||||
|
r29498 | if dobackup: | ||
# If in interactive mode, don't automatically create | ||||
# .orig files (issue4793) | ||||
if dobackup == backupinteractive: | ||||
tobackup.add(abs) | ||||
|
r43346 | elif backup <= dobackup or wctx[abs].cmp(ctx[abs]): | ||
|
r41754 | absbakname = scmutil.backuppath(ui, repo, abs) | ||
|
r43346 | bakname = os.path.relpath( | ||
absbakname, start=repo.root | ||||
) | ||||
ui.note( | ||||
|
r43347 | _(b'saving current version of %s as %s\n') | ||
|
r43346 | % (uipathfn(abs), uipathfn(bakname)) | ||
) | ||||
|
r43347 | if not opts.get(b'dry_run'): | ||
|
r24475 | if interactive: | ||
|
r41754 | util.copyfile(target, absbakname) | ||
|
r24475 | else: | ||
|
r41754 | util.rename(target, absbakname) | ||
|
r43347 | if opts.get(b'dry_run'): | ||
|
r39442 | if ui.verbose or not exact: | ||
|
r41754 | ui.status(msg % uipathfn(abs)) | ||
|
r22233 | elif exact: | ||
|
r41754 | ui.warn(msg % uipathfn(abs)) | ||
|
r16304 | break | ||
|
r21575 | |||
|
r43347 | if not opts.get(b'dry_run'): | ||
needdata = (b'revert', b'add', b'undelete') | ||||
|
r35941 | oplist = [actions[name][0] for name in needdata] | ||
|
r37780 | prefetch = scmutil.prefetchfiles | ||
|
r45632 | matchfiles = scmutil.matchfiles( | ||
repo, [f for sublist in oplist for f in sublist] | ||||
) | ||||
|
r43346 | prefetch( | ||
|
r46554 | repo, | ||
[(ctx.rev(), matchfiles)], | ||||
|
r43346 | ) | ||
|
r42238 | match = scmutil.match(repo[None], pats) | ||
|
r43346 | _performrevert( | ||
repo, | ||||
ctx, | ||||
names, | ||||
uipathfn, | ||||
actions, | ||||
match, | ||||
interactive, | ||||
tobackup, | ||||
) | ||||
|
r19129 | |||
|
r24134 | if targetsubs: | ||
# Revert the subrepos on the revert list | ||||
for sub in targetsubs: | ||||
|
r24463 | try: | ||
|
r43346 | wctx.sub(sub).revert( | ||
ctx.substate[sub], *pats, **pycompat.strkwargs(opts) | ||||
) | ||||
|
r24463 | except KeyError: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | b"subrepository '%s' does not exist in %s!" | ||
|
r43346 | % (sub, short(ctx.node())) | ||
) | ||||
def _performrevert( | ||||
repo, | ||||
ctx, | ||||
names, | ||||
uipathfn, | ||||
actions, | ||||
match, | ||||
interactive=False, | ||||
tobackup=None, | ||||
): | ||||
|
r21576 | """function that actually perform all the actions computed for revert | ||
|
r20571 | |||
This is an independent function to let extension to plug in and react to | ||||
the imminent revert. | ||||
|
r21024 | Make sure you have the working directory locked when calling this function. | ||
|
r20571 | """ | ||
|
r45935 | parent, p2 = repo.dirstate.parents() | ||
|
r20571 | node = ctx.node() | ||
|
r27985 | excluded_files = [] | ||
|
r20571 | def checkout(f): | ||
fc = ctx[f] | ||||
|
r25755 | repo.wwrite(f, fc.data(), fc.flags()) | ||
|
r20571 | |||
|
r30532 | def doremove(f): | ||
try: | ||||
|
r43347 | rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs') | ||
|
r38512 | repo.wvfs.unlinkpath(f, rmdir=rmdir) | ||
|
r30532 | except OSError: | ||
pass | ||||
repo.dirstate.remove(f) | ||||
|
r39442 | def prntstatusmsg(action, f): | ||
|
r41754 | exact = names[f] | ||
|
r39442 | if repo.ui.verbose or not exact: | ||
|
r41754 | repo.ui.status(actions[action][1] % uipathfn(f)) | ||
|
r39442 | |||
|
r33722 | audit_path = pathutil.pathauditor(repo.root, cached=True) | ||
|
r43347 | for f in actions[b'forget'][0]: | ||
|
r27985 | if interactive: | ||
|
r30530 | choice = repo.ui.promptchoice( | ||
|
r43347 | _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f) | ||
|
r43346 | ) | ||
|
r27985 | if choice == 0: | ||
|
r43347 | prntstatusmsg(b'forget', f) | ||
|
r27985 | repo.dirstate.drop(f) | ||
else: | ||||
|
r36212 | excluded_files.append(f) | ||
|
r27985 | else: | ||
|
r43347 | prntstatusmsg(b'forget', f) | ||
|
r27985 | repo.dirstate.drop(f) | ||
|
r43347 | for f in actions[b'remove'][0]: | ||
|
r20571 | audit_path(f) | ||
|
r30532 | if interactive: | ||
choice = repo.ui.promptchoice( | ||||
|
r43347 | _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f) | ||
|
r43346 | ) | ||
|
r30532 | if choice == 0: | ||
|
r43347 | prntstatusmsg(b'remove', f) | ||
|
r30532 | doremove(f) | ||
else: | ||||
|
r36212 | excluded_files.append(f) | ||
|
r30532 | else: | ||
|
r43347 | prntstatusmsg(b'remove', f) | ||
|
r30532 | doremove(f) | ||
|
r43347 | for f in actions[b'drop'][0]: | ||
|
r22491 | audit_path(f) | ||
|
r43347 | prntstatusmsg(b'drop', f) | ||
|
r20571 | repo.dirstate.remove(f) | ||
normal = None | ||||
if node == parent: | ||||
# We're reverting to our parent. If possible, we'd like status | ||||
# to report the file as clean. We have to use normallookup for | ||||
# merges to avoid losing information about merged/dirty files. | ||||
|
r47771 | if p2 != repo.nullid: | ||
|
r20571 | normal = repo.dirstate.normallookup | ||
else: | ||||
normal = repo.dirstate.normal | ||||
|
r24359 | |||
|
r25259 | newlyaddedandmodifiedfiles = set() | ||
|
r24359 | if interactive: | ||
# Prompt the user for changes to revert | ||||
|
r43347 | torevert = [f for f in actions[b'revert'][0] if f not in excluded_files] | ||
|
r36212 | m = scmutil.matchfiles(repo, torevert) | ||
|
r43346 | diffopts = patch.difffeatureopts( | ||
repo.ui, | ||||
whitespace=True, | ||||
|
r43347 | section=b'commands', | ||
configprefix=b'revert.interactive.', | ||||
|
r43346 | ) | ||
|
r25258 | diffopts.nodates = True | ||
diffopts.git = True | ||||
|
r43347 | operation = b'apply' | ||
|
r42154 | if node == parent: | ||
|
r43346 | if repo.ui.configbool( | ||
|
r43347 | b'experimental', b'revert.interactive.select-to-keep' | ||
|
r43346 | ): | ||
|
r43347 | operation = b'keep' | ||
|
r42154 | else: | ||
|
r43347 | operation = b'discard' | ||
if operation == b'apply': | ||||
|
r42154 | diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts) | ||
else: | ||||
|
r25424 | diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts) | ||
|
r48365 | original_headers = patch.parsepatch(diff) | ||
|
r25424 | |||
|
r24359 | try: | ||
|
r25424 | |||
|
r43346 | chunks, opts = recordfilter( | ||
|
r48365 | repo.ui, original_headers, match, operation=operation | ||
|
r43346 | ) | ||
|
r43347 | if operation == b'discard': | ||
|
r25424 | chunks = patch.reversehunks(chunks) | ||
|
r34252 | except error.PatchError as err: | ||
|
r43347 | raise error.Abort(_(b'error parsing patch: %s') % err) | ||
|
r24359 | |||
|
r43122 | # FIXME: when doing an interactive revert of a copy, there's no way of | ||
# performing a partial revert of the added file, the only option is | ||||
# "remove added file <name> (Yn)?", so we don't need to worry about the | ||||
# alsorestore value. Ideally we'd be able to partially revert | ||||
# copied/renamed files. | ||||
|
r48365 | newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(chunks) | ||
|
r29498 | if tobackup is None: | ||
tobackup = set() | ||||
|
r24359 | # Apply changes | ||
|
r28861 | fp = stringio() | ||
|
r39451 | # chunks are serialized per file, but files aren't sorted | ||
|
r44937 | for f in sorted({c.header.filename() for c in chunks if ishunk(c)}): | ||
|
r43347 | prntstatusmsg(b'revert', f) | ||
|
r42154 | files = set() | ||
|
r24359 | for c in chunks: | ||
|
r39442 | if ishunk(c): | ||
|
r29498 | abs = c.header.filename() | ||
|
r39442 | # Create a backup file only if this hunk should be backed up | ||
if c.header.filename() in tobackup: | ||||
target = repo.wjoin(abs) | ||||
|
r41737 | bakname = scmutil.backuppath(repo.ui, repo, abs) | ||
|
r39442 | util.copyfile(target, bakname) | ||
tobackup.remove(abs) | ||||
|
r42154 | if abs not in files: | ||
files.add(abs) | ||||
|
r43347 | if operation == b'keep': | ||
|
r42154 | checkout(abs) | ||
|
r24359 | c.write(fp) | ||
dopatch = fp.tell() | ||||
fp.seek(0) | ||||
if dopatch: | ||||
try: | ||||
patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None) | ||||
|
r34252 | except error.PatchError as err: | ||
|
r36736 | raise error.Abort(pycompat.bytestr(err)) | ||
|
r24359 | del fp | ||
else: | ||||
|
r43347 | for f in actions[b'revert'][0]: | ||
prntstatusmsg(b'revert', f) | ||||
|
r25755 | checkout(f) | ||
|
r24359 | if normal: | ||
normal(f) | ||||
|
r20571 | |||
|
r43347 | for f in actions[b'add'][0]: | ||
|
r25259 | # Don't checkout modified files, they are already created by the diff | ||
if f not in newlyaddedandmodifiedfiles: | ||||
|
r43347 | prntstatusmsg(b'add', f) | ||
|
r25259 | checkout(f) | ||
r48396 | repo.dirstate.set_tracked(f) | |||
|
r20571 | |||
normal = repo.dirstate.normallookup | ||||
|
r47771 | if node == parent and p2 == repo.nullid: | ||
|
r20571 | normal = repo.dirstate.normal | ||
|
r43347 | for f in actions[b'undelete'][0]: | ||
|
r41663 | if interactive: | ||
choice = repo.ui.promptchoice( | ||||
|
r43347 | _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f | ||
|
r43346 | ) | ||
|
r41663 | if choice == 0: | ||
|
r43347 | prntstatusmsg(b'undelete', f) | ||
|
r41663 | checkout(f) | ||
normal(f) | ||||
else: | ||||
excluded_files.append(f) | ||||
else: | ||||
|
r43347 | prntstatusmsg(b'undelete', f) | ||
|
r41663 | checkout(f) | ||
normal(f) | ||||
|
r20571 | |||
copied = copies.pathcopies(repo[parent], ctx) | ||||
|
r43347 | for f in ( | ||
actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0] | ||||
): | ||||
|
r20571 | if f in copied: | ||
repo.dirstate.copy(copied[f], f) | ||||
|
r43346 | |||
|
r21051 | # a list of (ui, repo, otherpeer, opts, missing) functions called by | ||
# commands.outgoing. "missing" is "missing" of the result of | ||||
# "findcommonoutgoing()" | ||||
outgoinghooks = util.hooks() | ||||
|
r19211 | # a list of (ui, repo) functions called by commands.summary | ||
summaryhooks = util.hooks() | ||||
|
r19474 | |||
|
r21047 | # a list of (ui, repo, opts, changes) functions called by commands.summary. | ||
# | ||||
# functions should return tuple of booleans below, if 'changes' is None: | ||||
# (whether-incomings-are-needed, whether-outgoings-are-needed) | ||||
# | ||||
# otherwise, 'changes' is a tuple of tuples below: | ||||
# - (sourceurl, sourcebranch, sourcepeer, incoming) | ||||
# - (desturl, destbranch, destpeer, outgoing) | ||||
summaryremotehooks = util.hooks() | ||||
|
r42730 | |||
|
r42732 | def checkunfinished(repo, commit=False, skipmerge=False): | ||
|
r46554 | """Look for an unfinished multistep operation, like graft, and abort | ||
|
r19474 | if found. It's probably good to check this right before | ||
bailifchanged(). | ||||
|
r46554 | """ | ||
|
r38220 | # Check for non-clearable states first, so things like rebase will take | ||
# precedence over update. | ||||
|
r42730 | for state in statemod._unfinishedstates: | ||
|
r43346 | if ( | ||
state._clearable | ||||
or (commit and state._allowcommit) | ||||
or state._reportonly | ||||
): | ||||
|
r38220 | continue | ||
|
r42730 | if state.isunfinished(repo): | ||
|
r46444 | raise error.StateError(state.msg(), hint=state.hint()) | ||
|
r42730 | |||
for s in statemod._unfinishedstates: | ||||
|
r43346 | if ( | ||
not s._clearable | ||||
or (commit and s._allowcommit) | ||||
|
r43347 | or (s._opname == b'merge' and skipmerge) | ||
|
r43346 | or s._reportonly | ||
): | ||||
|
r19496 | continue | ||
|
r42730 | if s.isunfinished(repo): | ||
|
r46444 | raise error.StateError(s.msg(), hint=s.hint()) | ||
|
r19474 | |||
|
r43346 | |||
|
r19474 | def clearunfinished(repo): | ||
|
r46554 | """Check for unfinished operations (as above), and clear the ones | ||
|
r19474 | that are clearable. | ||
|
r46554 | """ | ||
|
r42730 | for state in statemod._unfinishedstates: | ||
|
r42732 | if state._reportonly: | ||
continue | ||||
|
r42730 | if not state._clearable and state.isunfinished(repo): | ||
|
r46444 | raise error.StateError(state.msg(), hint=state.hint()) | ||
|
r42730 | |||
for s in statemod._unfinishedstates: | ||||
|
r47731 | if s._opname == b'merge' or s._reportonly: | ||
|
r42732 | continue | ||
|
r42730 | if s._clearable and s.isunfinished(repo): | ||
util.unlink(repo.vfs.join(s._fname)) | ||||
|
r24991 | |||
|
r43346 | |||
|
r42780 | def getunfinishedstate(repo): | ||
|
r46554 | """Checks for unfinished operations and returns statecheck object | ||
for it""" | ||||
|
r42780 | for state in statemod._unfinishedstates: | ||
if state.isunfinished(repo): | ||||
return state | ||||
return None | ||||
|
r43346 | |||
|
r28120 | def howtocontinue(repo): | ||
|
r46554 | """Check for an unfinished operation and return the command to finish | ||
|
r28120 | it. | ||
|
r42777 | statemod._unfinishedstates list is checked for an unfinished operation | ||
and the corresponding message to finish it is generated if a method to | ||||
continue is supported by the operation. | ||||
|
r28120 | |||
Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is | ||||
a boolean. | ||||
|
r46554 | """ | ||
|
r43347 | contmsg = _(b"continue: %s") | ||
|
r42733 | for state in statemod._unfinishedstates: | ||
if not state._continueflag: | ||||
continue | ||||
if state.isunfinished(repo): | ||||
return contmsg % state.continuemsg(), True | ||||
|
r33362 | if repo[None].dirty(missing=True, merge=False, branch=False): | ||
|
r43347 | return contmsg % _(b"hg commit"), False | ||
|
r28120 | return None, None | ||
|
r43346 | |||
|
r28120 | def checkafterresolved(repo): | ||
|
r46554 | """Inform the user about the next action after completing hg resolve | ||
|
r28120 | |||
|
r42777 | If there's a an unfinished operation that supports continue flag, | ||
howtocontinue will yield repo.ui.warn as the reporter. | ||||
|
r28120 | |||
Otherwise, it will yield repo.ui.note. | ||||
|
r46554 | """ | ||
|
r28120 | msg, warning = howtocontinue(repo) | ||
if msg is not None: | ||||
if warning: | ||||
|
r43347 | repo.ui.warn(b"%s\n" % msg) | ||
|
r28120 | else: | ||
|
r43347 | repo.ui.note(b"%s\n" % msg) | ||
|
r28120 | |||
|
r43346 | |||
|
r28120 | def wrongtooltocontinue(repo, task): | ||
|
r46554 | """Raise an abort suggesting how to properly continue if there is an | ||
|
r28120 | active task. | ||
Uses howtocontinue() to find the active task. | ||||
If there's no task (repo.ui.note for 'hg commit'), it does not offer | ||||
a hint. | ||||
|
r46554 | """ | ||
|
r28120 | after = howtocontinue(repo) | ||
hint = None | ||||
if after[1]: | ||||
hint = after[0] | ||||
|
r46444 | raise error.StateError(_(b'no %s in progress') % task, hint=hint) | ||
|
r42768 | |||
|
r43346 | |||
|
r42768 | def abortgraft(ui, repo, graftstate): | ||
"""abort the interrupted graft and rollbacks to the state before interrupted | ||||
graft""" | ||||
if not graftstate.exists(): | ||||
|
r46444 | raise error.StateError(_(b"no interrupted graft to abort")) | ||
|
r42768 | statedata = readgraftstate(repo, graftstate) | ||
|
r43347 | newnodes = statedata.get(b'newnodes') | ||
|
r42768 | if newnodes is None: | ||
# and old graft state which does not have all the data required to abort | ||||
# the graft | ||||
|
r43347 | raise error.Abort(_(b"cannot abort using an old graftstate")) | ||
|
r42768 | |||
# changeset from which graft operation was started | ||||
if len(newnodes) > 0: | ||||
startctx = repo[newnodes[0]].p1() | ||||
else: | ||||
|
r43347 | startctx = repo[b'.'] | ||
|
r42768 | # whether to strip or not | ||
cleanup = False | ||||
|
r43346 | |||
|
r42768 | if newnodes: | ||
newnodes = [repo[r].rev() for r in newnodes] | ||||
cleanup = True | ||||
# checking that none of the newnodes turned public or is public | ||||
immutable = [c for c in newnodes if not repo[c].mutable()] | ||||
if immutable: | ||||
|
r43346 | repo.ui.warn( | ||
|
r43347 | _(b"cannot clean up public changesets %s\n") | ||
% b', '.join(bytes(repo[r]) for r in immutable), | ||||
hint=_(b"see 'hg help phases' for details"), | ||||
|
r43346 | ) | ||
|
r42768 | cleanup = False | ||
# checking that no new nodes are created on top of grafted revs | ||||
desc = set(repo.changelog.descendants(newnodes)) | ||||
if desc - set(newnodes): | ||||
|
r43346 | repo.ui.warn( | ||
_( | ||||
|
r43347 | b"new changesets detected on destination " | ||
b"branch, can't strip\n" | ||||
|
r43346 | ) | ||
) | ||||
|
r42768 | cleanup = False | ||
if cleanup: | ||||
with repo.wlock(), repo.lock(): | ||||
|
r46133 | mergemod.clean_update(startctx) | ||
|
r42768 | # stripping the new nodes created | ||
|
r43346 | strippoints = [ | ||
|
r43347 | c.node() for c in repo.set(b"roots(%ld)", newnodes) | ||
|
r43346 | ] | ||
|
r42768 | repair.strip(repo.ui, repo, strippoints, backup=False) | ||
if not cleanup: | ||||
# we don't update to the startnode if we can't strip | ||||
|
r43347 | startctx = repo[b'.'] | ||
|
r46133 | mergemod.clean_update(startctx) | ||
|
r42768 | |||
|
r43347 | ui.status(_(b"graft aborted\n")) | ||
ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12]) | ||||
|
r42768 | graftstate.delete() | ||
return 0 | ||||
|
r43346 | |||
|
r42768 | def readgraftstate(repo, graftstate): | ||
|
r44099 | # type: (Any, statemod.cmdstate) -> Dict[bytes, Any] | ||
|
r42768 | """read the graft state file and return a dict of the data stored in it""" | ||
try: | ||||
return graftstate.read() | ||||
except error.CorruptedState: | ||||
|
r43347 | nodes = repo.vfs.read(b'graftstate').splitlines() | ||
return {b'nodes': nodes} | ||||
|
r42785 | |||
|
r43346 | |||
|
r42785 | def hgabortgraft(ui, repo): | ||
|
r47856 | """abort logic for aborting graft using 'hg abort'""" | ||
|
r42785 | with repo.wlock(): | ||
|
r43347 | graftstate = statemod.cmdstate(repo, b'graftstate') | ||
|
r42785 | return abortgraft(ui, repo, graftstate) | ||