cmdutil.py
3413 lines
| 123.8 KiB
| text/x-python
|
PythonLexer
/ mercurial / cmdutil.py
|
r2957 | # cmdutil.py - help for command processing in mercurial | ||
|
r2874 | # | ||
|
r4635 | # Copyright 2005-2007 Matt Mackall <mpm@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, | ||||
nullid, | ||||
nullrev, | ||||
short, | ||||
) | ||||
from . import ( | ||||
bookmarks, | ||||
changelog, | ||||
copies, | ||||
crecord as crecordmod, | ||||
|
r33619 | dirstateguard, | ||
|
r28322 | encoding, | ||
error, | ||||
formatter, | ||||
|
r35903 | logcmdutil, | ||
|
r28322 | match as matchmod, | ||
|
r36027 | merge as mergemod, | ||
|
r36927 | mergeutil, | ||
|
r28322 | obsolete, | ||
patch, | ||||
pathutil, | ||||
|
r38442 | phases, | ||
|
r30519 | pycompat, | ||
|
r42768 | repair, | ||
|
r28322 | revlog, | ||
|
r35763 | rewriteutil, | ||
|
r28322 | scmutil, | ||
|
r31023 | smartset, | ||
|
r42729 | state as statemod, | ||
|
r36026 | subrepoutil, | ||
|
r28322 | templatekw, | ||
templater, | ||||
util, | ||||
|
r31237 | vfs as vfsmod, | ||
|
r28322 | ) | ||
|
r37102 | |||
from .utils import ( | ||||
dateutil, | ||||
stringutil, | ||||
) | ||||
|
r28861 | stringio = util.stringio | ||
|
r2874 | |||
|
r32375 | # templates of common command options | ||
dryrunopts = [ | ||||
('n', 'dry-run', None, | ||||
_('do not perform actions, just print output')), | ||||
] | ||||
|
r38689 | confirmopts = [ | ||
('', 'confirm', None, | ||||
_('ask before applying actions')), | ||||
] | ||||
|
r32375 | remoteopts = [ | ||
('e', 'ssh', '', | ||||
_('specify ssh command to use'), _('CMD')), | ||||
('', 'remotecmd', '', | ||||
_('specify hg command to run on the remote side'), _('CMD')), | ||||
('', 'insecure', None, | ||||
_('do not verify server certificate (ignoring web.cacerts config)')), | ||||
] | ||||
walkopts = [ | ||||
('I', 'include', [], | ||||
_('include names matching the given patterns'), _('PATTERN')), | ||||
('X', 'exclude', [], | ||||
_('exclude names matching the given patterns'), _('PATTERN')), | ||||
] | ||||
commitopts = [ | ||||
('m', 'message', '', | ||||
_('use text as commit message'), _('TEXT')), | ||||
('l', 'logfile', '', | ||||
_('read commit message from file'), _('FILE')), | ||||
] | ||||
commitopts2 = [ | ||||
('d', 'date', '', | ||||
_('record the specified date as commit date'), _('DATE')), | ||||
('u', 'user', '', | ||||
_('record the specified user as committer'), _('USER')), | ||||
] | ||||
formatteropts = [ | ||||
('T', 'template', '', | ||||
|
r39967 | _('display with template'), _('TEMPLATE')), | ||
|
r32375 | ] | ||
templateopts = [ | ||||
('', 'style', '', | ||||
_('display using template map file (DEPRECATED)'), _('STYLE')), | ||||
('T', 'template', '', | ||||
_('display with template'), _('TEMPLATE')), | ||||
] | ||||
logopts = [ | ||||
('p', 'patch', None, _('show patch')), | ||||
('g', 'git', None, _('use git extended diff format')), | ||||
('l', 'limit', '', | ||||
_('limit number of changes displayed'), _('NUM')), | ||||
('M', 'no-merges', None, _('do not show merges')), | ||||
('', 'stat', None, _('output diffstat-style summary of changes')), | ||||
('G', 'graph', None, _("show the revision DAG")), | ||||
] + templateopts | ||||
diffopts = [ | ||||
('a', 'text', None, _('treat all files as text')), | ||||
('g', 'git', None, _('use git extended diff format')), | ||||
('', 'binary', None, _('generate binary diffs in git mode (default)')), | ||||
('', 'nodates', None, _('omit dates from diff headers')) | ||||
] | ||||
diffwsopts = [ | ||||
('w', 'ignore-all-space', None, | ||||
_('ignore white space when comparing lines')), | ||||
('b', 'ignore-space-change', None, | ||||
_('ignore changes in the amount of white space')), | ||||
('B', 'ignore-blank-lines', None, | ||||
_('ignore changes whose lines are all blank')), | ||||
|
r34015 | ('Z', 'ignore-space-at-eol', None, | ||
_('ignore changes in whitespace at EOL')), | ||||
|
r32375 | ] | ||
diffopts2 = [ | ||||
('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')), | ||||
('p', 'show-function', None, _('show which function each change is in')), | ||||
('', 'reverse', None, _('produce a diff that undoes the changes')), | ||||
] + diffwsopts + [ | ||||
('U', 'unified', '', | ||||
_('number of lines of context to show'), _('NUM')), | ||||
('', 'stat', None, _('output diffstat-style summary of changes')), | ||||
('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')), | ||||
] | ||||
mergetoolopts = [ | ||||
r40382 | ('t', 'tool', '', _('specify merge tool'), _('TOOL')), | |||
|
r32375 | ] | ||
similarityopts = [ | ||||
('s', 'similarity', '', | ||||
_('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY')) | ||||
] | ||||
subrepoopts = [ | ||||
('S', 'subrepos', None, | ||||
_('recurse into subrepositories')) | ||||
] | ||||
debugrevlogopts = [ | ||||
('c', 'changelog', False, _('open changelog')), | ||||
('m', 'manifest', False, _('open manifest')), | ||||
('', 'dir', '', _('open directory manifest')), | ||||
] | ||||
|
r30703 | # special string such that everything below this line will be ingored in the | ||
# editor text | ||||
_linebelow = "^HG: ------------------------ >8 ------------------------$" | ||||
|
r25256 | def ishunk(x): | ||
hunkclasses = (crecordmod.uihunk, patch.recordhunk) | ||||
return isinstance(x, hunkclasses) | ||||
|
r25257 | def newandmodified(chunks, originalchunks): | ||
newlyaddedandmodifiedfiles = set() | ||||
for chunk in chunks: | ||||
|
r41925 | if (ishunk(chunk) and chunk.header.isnewfile() and chunk not in | ||
originalchunks): | ||||
|
r25257 | newlyaddedandmodifiedfiles.add(chunk.header.filename()) | ||
return newlyaddedandmodifiedfiles | ||||
|
r10401 | def parsealiases(cmd): | ||
|
r40331 | return cmd.split("|") | ||
|
r10401 | |||
|
r24356 | def setupwrapcolorwrite(ui): | ||
# wrap ui.write so diff output can be labeled/colorized | ||||
def wrapwrite(orig, *args, **kw): | ||||
|
r35351 | label = kw.pop(r'label', '') | ||
|
r24356 | for chunk, l in patch.difflabel(lambda: args): | ||
orig(chunk, label=label + l) | ||||
oldwrite = ui.write | ||||
def wrap(*args, **kwargs): | ||||
return wrapwrite(oldwrite, *args, **kwargs) | ||||
setattr(ui, 'write', wrap) | ||||
return oldwrite | ||||
|
r42238 | def filterchunks(ui, originalhunks, usecurses, testfile, match, | ||
operation=None): | ||||
|
r38058 | try: | ||
if usecurses: | ||||
if testfile: | ||||
recordfn = crecordmod.testdecorator( | ||||
testfile, crecordmod.testchunkselector) | ||||
else: | ||||
recordfn = crecordmod.chunkselector | ||||
return crecordmod.filterpatch(ui, originalhunks, recordfn, | ||||
operation) | ||||
except crecordmod.fallbackerror as e: | ||||
ui.warn('%s\n' % e.message) | ||||
ui.warn(_('falling back to text mode\n')) | ||||
|
r42238 | return patch.filterpatch(ui, originalhunks, match, operation) | ||
def recordfilter(ui, originalhunks, match, operation=None): | ||||
|
r25310 | """ Prompts the user to filter the originalhunks and return a list of | ||
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) | ||
|
r33499 | testfile = ui.config('experimental', 'crecordtest') | ||
|
r24358 | oldwrite = setupwrapcolorwrite(ui) | ||
try: | ||||
|
r27155 | newchunks, newopts = filterchunks(ui, originalhunks, usecurses, | ||
|
r42238 | testfile, match, operation) | ||
|
r24358 | finally: | ||
ui.write = oldwrite | ||||
|
r27155 | return newchunks, newopts | ||
|
r24357 | |||
|
r24309 | def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, | ||
filterfn, *pats, **opts): | ||||
|
r32144 | opts = pycompat.byteskwargs(opts) | ||
|
r24272 | if not ui.interactive(): | ||
|
r25795 | if cmdsuggest: | ||
msg = _('running non-interactively, use %s instead') % cmdsuggest | ||||
else: | ||||
msg = _('running non-interactively') | ||||
|
r26587 | raise error.Abort(msg) | ||
|
r24272 | |||
# make sure username is set before going interactive | ||||
if not opts.get('user'): | ||||
ui.username() # raise exception, username not provided | ||||
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. | ||||
""" | ||||
checkunfinished(repo, commit=True) | ||||
|
r28815 | wctx = repo[None] | ||
merge = len(wctx.parents()) > 1 | ||||
|
r24272 | if merge: | ||
|
r26587 | raise error.Abort(_('cannot partially commit a merge ' | ||
|
r24272 | '(use "hg commit" instead)')) | ||
|
r42444 | def fail(f, msg): | ||
raise error.Abort('%s: %s' % (f, msg)) | ||||
force = opts.get('force') | ||||
if not force: | ||||
vdirs = [] | ||||
|
r42456 | match = matchmod.badmatch(match, fail) | ||
|
r42444 | match.explicitdir = vdirs.append | ||
|
r42143 | status = repo.status(match=match) | ||
overrides = {(b'ui', b'commitsubrepos'): True} | ||||
with repo.ui.configoverride(overrides, b'record'): | ||||
# subrepoutil.precommit() modifies the status | ||||
tmpstatus = scmutil.status(copymod.copy(status[0]), | ||||
copymod.copy(status[1]), | ||||
copymod.copy(status[2]), | ||||
copymod.copy(status[3]), | ||||
copymod.copy(status[4]), | ||||
copymod.copy(status[5]), | ||||
copymod.copy(status[6])) | ||||
# Force allows -X subrepo to skip the subrepo. | ||||
subs, commitsubs, newstate = subrepoutil.precommit( | ||||
repo.ui, wctx, tmpstatus, match, force=True) | ||||
for s in subs: | ||||
if s in commitsubs: | ||||
dirtyreason = wctx.sub(s).dirtyreason(True) | ||||
raise error.Abort(dirtyreason) | ||||
|
r28815 | if not force: | ||
repo.checkcommitpatterns(wctx, vdirs, match, status, fail) | ||||
|
r41701 | diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True, | ||
section='commands', | ||||
configprefix='commit.interactive.') | ||||
|
r24272 | diffopts.nodates = True | ||
diffopts.git = True | ||||
|
r27411 | diffopts.showfunc = True | ||
|
r27637 | originaldiff = patch.diff(repo, changes=status, opts=diffopts) | ||
|
r24341 | originalchunks = 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: | ||
|
r42238 | chunks, newopts = filterfn(ui, originalchunks, match) | ||
|
r34252 | except error.PatchError as err: | ||
|
r26587 | raise error.Abort(_('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 | ||||
# version without the edit in the workdir | ||||
|
r25257 | newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks) | ||
|
r24272 | contenders = set() | ||
for h in chunks: | ||||
try: | ||||
contenders.update(set(h.files())) | ||||
except AttributeError: | ||||
pass | ||||
changed = status.modified + status.added + status.removed | ||||
newfiles = [f for f in changed if f in contenders] | ||||
if not newfiles: | ||||
ui.status(_('no changes to record\n')) | ||||
return 0 | ||||
modified = set(status.modified) | ||||
# 2. backup changed files, so we can restore them in the end | ||||
if backupall: | ||||
tobackup = changed | ||||
else: | ||||
|
r41925 | tobackup = [f for f in newfiles if f in modified or f in | ||
newlyaddedandmodifiedfiles] | ||||
|
r24272 | backups = {} | ||
if tobackup: | ||||
|
r31320 | backupdir = repo.vfs.join('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: | ||||
|
r38182 | fd, tmpname = pycompat.mkstemp(prefix=f.replace('/', '_') + '.', | ||
|
r24272 | dir=backupdir) | ||
os.close(fd) | ||||
ui.debug('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 | ||
if opts.get('review', False): | ||||
patchtext = (crecordmod.diffhelptext | ||||
+ crecordmod.patchhelptext | ||||
+ fp.read()) | ||||
reviewedpatch = ui.edit(patchtext, "", | ||||
|
r34030 | action="diff", | ||
|
r30848 | repopath=repo.path) | ||
|
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: | ||||
# Equivalent to hg.revert | ||||
|
r27344 | m = scmutil.matchfiles(repo, backups.keys()) | ||
|
r40402 | mergemod.update(repo, repo.dirstate.p1(), branchmerge=False, | ||
force=True, matcher=m) | ||||
|
r24272 | |||
# 3b. (apply) | ||||
if dopatch: | ||||
try: | ||||
ui.debug('applying patch\n') | ||||
ui.debug(fp.getvalue()) | ||||
patch.internalpatch(ui, repo, fp, 1, eolmode=None) | ||||
|
r34252 | except error.PatchError as err: | ||
|
r36736 | raise error.Abort(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 | ||
|
r24272 | for realname, tmpname in backups.iteritems(): | ||
ui.debug('restoring %r to %r\n' % (tmpname, realname)) | ||||
|
r25759 | |||
if dirstate[realname] == 'n': | ||||
# 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 | |||
|
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 | ||||
|
r34929 | if '/' in filename: | ||
subdir, filep = filename.split('/', 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: | ||||
|
r38221 | yield onlyst, self.path + '/' | ||
|
r34684 | return | ||
# add the files to status list | ||||
|
r34685 | for st, fpath in self.iterfilepaths(): | ||
yield st, fpath | ||||
|
r34684 | |||
#recurse on the subdirs | ||||
for dirobj in self.subdirs.values(): | ||||
|
r34685 | for st, fpath in dirobj.tersewalk(terseargs): | ||
yield st, fpath | ||||
|
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 | ||||
allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c') | ||||
# checking the argument validity | ||||
|
r34894 | for s in pycompat.bytestr(terseargs): | ||
|
r34683 | if s not in allst: | ||
raise error.Abort(_("'%s' not recognized") % s) | ||||
# creating a dirnode object for the root of the repo | ||||
rootobj = dirnode('') | ||||
pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown', | ||||
'ignored', 'removed') | ||||
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]) | ||||
return tersedlist | ||||
|
r33548 | |||
|
r33766 | def _commentlines(raw): | ||
'''Surround lineswith a comment char and a new line''' | ||||
lines = raw.splitlines() | ||||
commentedlines = ['# %s' % line for line in lines] | ||||
return '\n'.join(commentedlines) + '\n' | ||||
def _conflictsmsg(repo): | ||||
mergestate = mergemod.mergestate.read(repo) | ||||
if not mergestate.active(): | ||||
return | ||||
m = scmutil.match(repo[None]) | ||||
|
r34003 | unresolvedlist = [f for f in mergestate.unresolved() if m(f)] | ||
|
r33766 | if unresolvedlist: | ||
mergeliststr = '\n'.join( | ||||
|
r39843 | [' %s' % util.pathto(repo.root, encoding.getcwd(), path) | ||
|
r40137 | for path in sorted(unresolvedlist)]) | ||
|
r33766 | msg = _('''Unresolved merge conflicts: | ||
%s | ||||
To mark files as resolved: hg resolve --mark FILE''') % mergeliststr | ||||
else: | ||||
msg = _('No unresolved merge conflicts.') | ||||
return _commentlines(msg) | ||||
def morestatus(repo, fm): | ||||
|
r42731 | statetuple = statemod.getrepostate(repo) | ||
|
r33766 | label = 'status.morestatus' | ||
if statetuple: | ||||
|
r42732 | state, helpfulmsg = statetuple | ||
|
r33766 | statemsg = _('The repository is in an unfinished *%s* state.') % state | ||
|
r39773 | fm.plain('%s\n' % _commentlines(statemsg), label=label) | ||
|
r33766 | conmsg = _conflictsmsg(repo) | ||
|
r33885 | if conmsg: | ||
|
r39773 | fm.plain('%s\n' % conmsg, label=label) | ||
|
r33766 | if helpfulmsg: | ||
|
r42732 | fm.plain('%s\n' % _commentlines(helpfulmsg), label=label) | ||
|
r33766 | |||
|
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: | ||||
if aliases[0].startswith("debug") or found.startswith("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 | |||
|
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 | |||
|
r35763 | def changebranch(ui, repo, revs, label): | ||
""" Change the branch name of given revs to label """ | ||||
with repo.wlock(), repo.lock(), repo.transaction('branches'): | ||||
# abort in case of uncommitted merge or dirty wdir | ||||
bailifchanged(repo) | ||||
revs = scmutil.revrange(repo, revs) | ||||
if not revs: | ||||
raise error.Abort("empty revision set") | ||||
roots = repo.revs('roots(%ld)', revs) | ||||
if len(roots) > 1: | ||||
raise error.Abort(_("cannot change branch of non-linear revisions")) | ||||
rewriteutil.precheck(repo, revs, 'change branch of') | ||||
|
r35764 | |||
root = repo[roots.first()] | ||||
r40702 | rpb = {parent.branch() for parent in root.parents()} | |||
if label not in rpb and label in repo.branchmap(): | ||||
|
r35764 | raise error.Abort(_("a branch of the same name already exists")) | ||
|
r35763 | if repo.revs('obsolete() and %ld', revs): | ||
raise error.Abort(_("cannot change branch of a obsolete changeset")) | ||||
# make sure only topological heads | ||||
if repo.revs('heads(%ld) - head()', revs): | ||||
raise error.Abort(_("cannot change branch in middle of a stack")) | ||||
replacements = {} | ||||
# avoid import cycle mercurial.cmdutil -> mercurial.context -> | ||||
# mercurial.subrepo -> mercurial.cmdutil | ||||
from . import context | ||||
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 | ||||
ui.debug("changing branch of '%s' from '%s' to '%s'\n" | ||||
% (hex(ctx.node()), oldbranch, label)) | ||||
extra = ctx.extra() | ||||
extra['branch_change'] = hex(ctx.node()) | ||||
# 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] | ||||
mc = context.memctx(repo, (p1, p2), | ||||
ctx.description(), | ||||
ctx.files(), | ||||
filectxfn, | ||||
user=ctx.user(), | ||||
date=ctx.date(), | ||||
extra=extra, | ||||
branch=label) | ||||
|
r38442 | newnode = repo.commitctx(mc) | ||
|
r35763 | replacements[ctx.node()] = (newnode,) | ||
ui.debug('new node id is %s\n' % hex(newnode)) | ||||
# create obsmarkers and move bookmarks | ||||
|
r38442 | scmutil.cleanupnodes(repo, replacements, '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 | ||||
hg.update(repo, newid[0], quietempty=True) | ||||
ui.status(_("changed branch on %d changesets\n") % len(replacements)) | ||||
|
r10402 | def findrepo(p): | ||
while not os.path.isdir(os.path.join(p, ".hg")): | ||||
oldp, p = p, os.path.dirname(p) | ||||
if p == oldp: | ||||
return None | ||||
return p | ||||
|
r30755 | def bailifchanged(repo, merge=True, hint=None): | ||
""" enforce the precondition that working directory must be clean. | ||||
'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. | ||||
""" | ||||
|
r24472 | if merge and repo.dirstate.p2() != nullid: | ||
|
r30755 | raise error.Abort(_('outstanding uncommitted merge'), hint=hint) | ||
|
r4549 | modified, added, removed, deleted = repo.status()[:4] | ||
if modified or added or removed or deleted: | ||||
|
r30755 | raise error.Abort(_('uncommitted changes'), hint=hint) | ||
|
r15231 | ctx = repo[None] | ||
|
r18364 | for s in sorted(ctx.substate): | ||
|
r30755 | ctx.sub(s).bailifchanged(hint=hint) | ||
|
r4549 | |||
|
r14635 | def logmessage(ui, opts): | ||
|
r4549 | """ get the log message according to -m and -l option """ | ||
|
r7667 | message = opts.get('message') | ||
logfile = opts.get('logfile') | ||||
|
r4549 | |||
if message and logfile: | ||||
|
r26587 | raise error.Abort(_('options --message and --logfile are mutually ' | ||
|
r4549 | 'exclusive')) | ||
if not message and logfile: | ||||
try: | ||||
|
r32618 | if isstdiofilename(logfile): | ||
|
r14635 | message = ui.fin.read() | ||
|
r4549 | else: | ||
|
r14249 | message = '\n'.join(util.readfile(logfile).splitlines()) | ||
|
r25660 | except IOError as inst: | ||
|
r26587 | raise error.Abort(_("can't read commit message '%s': %s") % | ||
|
r34024 | (logfile, encoding.strtolocal(inst.strerror))) | ||
|
r4549 | return message | ||
|
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: | ||||
|
r24180 | return baseformname + ".merge" | ||
|
r40065 | elif len(ctxorbool.parents()) > 1: | ||
|
r24180 | return baseformname + ".merge" | ||
return baseformname + ".normal" | ||||
|
r22248 | |||
|
r21999 | def getcommiteditor(edit=False, finishdesc=None, extramsg=None, | ||
editform='', **opts): | ||||
|
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: | ||||
return lambda r, c, s: commitforceeditor(r, c, s, | ||||
finishdesc=finishdesc, | ||||
|
r21999 | extramsg=extramsg, | ||
editform=editform) | ||||
elif editform: | ||||
return lambda r, c, s: commiteditor(r, c, s, editform=editform) | ||||
|
r21405 | else: | ||
return commiteditor | ||||
|
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) | ||||
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) | ||||
|
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) | ||||
t = formatter.maketemplater(repo.ui, tmpl, defaults=templatekw.keywords, | ||||
resources=tres) | ||||
|
r37121 | mapping = {'ctx': ctx} | ||
|
r36528 | if props: | ||
mapping.update(props) | ||||
|
r37003 | return t.renderdefault(mapping) | ||
|
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: | ||
raise error.Abort(_("incomplete format spec in output " | ||||
"filename")) | ||||
c = pat[n + 1:n + 2] | ||||
i = n + 2 | ||||
|
r36257 | try: | ||
|
r36528 | newname.append(expander[c]) | ||
|
r36257 | except KeyError: | ||
raise error.Abort(_("invalid format spec '%%%s' in output " | ||||
"filename") % c) | ||||
return ''.join(newname) | ||||
|
r2874 | |||
|
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 | |||
|
r32539 | def isstdiofilename(pat): | ||
"""True if the given pat looks like a filename denoting stdin/stdout""" | ||||
return not pat or pat == '-' | ||||
|
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 | ||||
|
r37619 | def makefileobj(ctx, pat, mode='wb', **props): | ||
|
r13769 | writable = mode not in ('r', '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 | |||
|
r39314 | def openstorage(repo, cmd, file_, opts, returnrevlog=False): | ||
|
r14323 | """opens the changelog, manifest, a filelog or a given revlog""" | ||
cl = opts['changelog'] | ||||
mf = opts['manifest'] | ||||
|
r25119 | dir = opts['dir'] | ||
|
r14323 | msg = None | ||
if cl and mf: | ||||
msg = _('cannot specify --changelog and --manifest at the same time') | ||||
|
r25119 | elif cl and dir: | ||
msg = _('cannot specify --changelog and --dir at the same time') | ||||
|
r29427 | elif cl or mf or dir: | ||
|
r14323 | if file_: | ||
msg = _('cannot specify filename with --changelog or --manifest') | ||||
elif not repo: | ||||
|
r25119 | msg = _('cannot specify --changelog or --manifest or --dir ' | ||
|
r14323 | 'without a repository') | ||
if msg: | ||||
|
r26587 | raise error.Abort(msg) | ||
|
r14323 | |||
r = None | ||||
if repo: | ||||
if cl: | ||||
|
r21033 | r = repo.unfiltered().changelog | ||
|
r25119 | elif dir: | ||
if 'treemanifest' not in repo.requirements: | ||||
|
r26587 | raise error.Abort(_("--dir can only be used on repos with " | ||
|
r25119 | "treemanifest enabled")) | ||
|
r37287 | if not dir.endswith('/'): | ||
dir = dir + '/' | ||||
|
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 | ||||
elif util.safehasattr(r, '_revlog'): | ||||
r = r._revlog | ||||
elif r is not None: | ||||
raise error.Abort(_('%r does not appear to be a revlog') % r) | ||||
|
r14323 | if not r: | ||
|
r39314 | if not returnrevlog: | ||
raise error.Abort(_('cannot give path to non-revlog')) | ||||
|
r14323 | if not file_: | ||
raise error.CommandError(cmd, _('invalid arguments')) | ||||
if not os.path.isfile(file_): | ||||
|
r26587 | raise error.Abort(_("revlog '%s' not found") % file_) | ||
|
r39843 | r = revlog.revlog(vfsmod.vfs(encoding.getcwd(), audit=False), | ||
|
r14323 | file_[:-2] + ".i") | ||
return r | ||||
|
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) | ||||
|
r5610 | def copy(ui, repo, pats, opts, rename=False): | ||
|
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 = {} | ||||
|
r5607 | after = opts.get("after") | ||
dryrun = opts.get("dry_run") | ||||
|
r11303 | wctx = repo[None] | ||
|
r5589 | |||
|
r41806 | uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True) | ||
|
r5605 | def walkpat(pat): | ||
srcs = [] | ||||
|
r24306 | if after: | ||
badstates = '?' | ||||
else: | ||||
badstates = '?r' | ||||
|
r32382 | m = scmutil.match(wctx, [pat], opts, globbed=True) | ||
for abs in wctx.walk(m): | ||||
|
r5605 | state = repo.dirstate[abs] | ||
|
r41806 | rel = uipathfn(abs) | ||
|
r6584 | exact = m.exact(abs) | ||
|
r11223 | if state in badstates: | ||
|
r5605 | if exact and state == '?': | ||
ui.warn(_('%s: not copying - file is not managed\n') % rel) | ||||
if exact and state == 'r': | ||||
ui.warn(_('%s: not copying - file has been marked for' | ||||
' remove\n') % rel) | ||||
continue | ||||
# abs: hgsep | ||||
# rel: ossep | ||||
srcs.append((abs, rel, exact)) | ||||
return srcs | ||||
|
r5589 | |||
# abssrc: hgsep | ||||
# relsrc: ossep | ||||
# otarget: ossep | ||||
|
r5605 | def copyfile(abssrc, relsrc, otarget, exact): | ||
|
r20033 | abstarget = pathutil.canonpath(repo.root, cwd, otarget) | ||
|
r16542 | if '/' in abstarget: | ||
# We cannot normalize abstarget itself, this would prevent | ||||
# case only renames, like a => A. | ||||
abspath, absname = abstarget.rsplit('/', 1) | ||||
abstarget = repo.dirstate.normalize(abspath) + '/' + 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: | ||
ui.warn(_('%s: not overwriting - %s collides with %s\n') % | ||||
(reltarget, repo.pathto(abssrc, cwd), | ||||
repo.pathto(prevsrc, cwd))) | ||||
|
r39386 | return True # report a failure | ||
|
r5607 | |||
# check for overwrites | ||||
|
r12342 | exists = os.path.lexists(target) | ||
|
r16283 | samefile = False | ||
if exists and abssrc != abstarget: | ||||
if (repo.dirstate.normalize(abssrc) == | ||||
repo.dirstate.normalize(abstarget)): | ||||
if not rename: | ||||
ui.warn(_("%s: can't copy - same file\n") % reltarget) | ||||
|
r39386 | return True # report a failure | ||
|
r16283 | exists = False | ||
samefile = True | ||||
|
r8117 | if not after and exists or after and state in 'mn': | ||
|
r5589 | if not opts['force']: | ||
|
r30151 | if state in 'mn': | ||
msg = _('%s: not overwriting - file already committed\n') | ||||
if after: | ||||
flags = '--after --force' | ||||
else: | ||||
flags = '--force' | ||||
if rename: | ||||
|
r39384 | hint = _("('hg rename %s' to replace the file by " | ||
|
r30151 | 'recording a rename)\n') % flags | ||
else: | ||||
|
r39384 | hint = _("('hg copy %s' to replace the file by " | ||
|
r30151 | 'recording a copy)\n') % flags | ||
else: | ||||
msg = _('%s: not overwriting - file exists\n') | ||||
if rename: | ||||
|
r39384 | hint = _("('hg rename --after' to record the rename)\n") | ||
|
r30151 | else: | ||
|
r39384 | hint = _("('hg copy --after' to record the copy)\n") | ||
|
r30151 | ui.warn(msg % reltarget) | ||
ui.warn(hint) | ||||
|
r39386 | return True # report a failure | ||
|
r5607 | |||
if after: | ||||
|
r5608 | if not exists: | ||
|
r11152 | if rename: | ||
ui.warn(_('%s: not recording move - %s does not exist\n') % | ||||
(relsrc, reltarget)) | ||||
else: | ||||
ui.warn(_('%s: not recording copy - %s does not exist\n') % | ||||
(relsrc, reltarget)) | ||||
|
r39386 | return True # report a failure | ||
|
r5608 | elif not dryrun: | ||
|
r5589 | try: | ||
|
r5608 | if exists: | ||
os.unlink(target) | ||||
targetdir = os.path.dirname(target) or '.' | ||||
if not os.path.isdir(targetdir): | ||||
os.makedirs(targetdir) | ||||
|
r16283 | if samefile: | ||
tmp = target + "~hgrename" | ||||
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: | ||
|
r24364 | ui.warn(_('%s: deleted in working directory\n') % relsrc) | ||
|
r14518 | srcexists = False | ||
|
r5589 | else: | ||
ui.warn(_('%s: cannot copy - %s\n') % | ||||
|
r34024 | (relsrc, encoding.strtolocal(inst.strerror))) | ||
|
r5606 | return True # report a failure | ||
|
r5607 | |||
|
r5589 | if ui.verbose or not exact: | ||
|
r7894 | if rename: | ||
ui.status(_('moving %s to %s\n') % (relsrc, reltarget)) | ||||
else: | ||||
ui.status(_('copying %s to %s\n') % (relsrc, reltarget)) | ||||
|
r5608 | |||
|
r5589 | targets[abstarget] = abssrc | ||
|
r5607 | |||
# fix up dirstate | ||||
|
r14321 | scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget, | ||
dryrun=dryrun, cwd=cwd) | ||||
|
r5610 | if rename and not dryrun: | ||
|
r16283 | if not after and srcexists and not samefile: | ||
|
r38512 | rmdir = repo.ui.configbool('experimental', 'removeemptydirs') | ||
repo.wvfs.unlinkpath(abssrc, rmdir=rmdir) | ||||
|
r14518 | wctx.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: | ||||
res = lambda p: os.path.join(dest, | ||||
os.path.basename(util.localpath(p))) | ||||
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 | ||
res = lambda p: os.path.join(dest, | ||||
os.path.basename(util.localpath(p))) | ||||
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 | ||||
res = lambda p: os.path.join(dest, | ||||
util.localpath(p)[striplen:]) | ||||
else: | ||||
# a file | ||||
if destdirexists: | ||||
res = lambda p: os.path.join(dest, | ||||
os.path.basename(util.localpath(p))) | ||||
else: | ||||
res = lambda p: dest | ||||
return res | ||||
|
r14321 | pats = scmutil.expandpats(pats) | ||
|
r5589 | if not pats: | ||
|
r26587 | raise error.Abort(_('no source or destination specified')) | ||
|
r5589 | if len(pats) == 1: | ||
|
r26587 | raise error.Abort(_('no destination specified')) | ||
|
r5589 | dest = pats.pop() | ||
|
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]): | ||
|
r26587 | raise error.Abort(_('with multiple sources, destination must be an ' | ||
|
r5589 | 'existing directory')) | ||
|
r5843 | if util.endswithsep(dest): | ||
|
r26587 | raise error.Abort(_('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: | ||||
|
r26587 | raise error.Abort(_('no files to copy')) | ||
|
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 | |||
|
r26561 | ## facility to let extension process additional data into an import patch | ||
# list of identifier to be executed in order | ||||
extrapreimport = [] # run before commit | ||||
|
r26562 | 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 | |||
|
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 | |||
tmpname = patchdata.get('filename') | ||||
message = patchdata.get('message') | ||||
user = opts.get('user') or patchdata.get('user') | ||||
date = opts.get('date') or patchdata.get('date') | ||||
branch = patchdata.get('branch') | ||||
nodeid = patchdata.get('nodeid') | ||||
p1 = patchdata.get('p1') | ||||
p2 = patchdata.get('p2') | ||||
|
r20500 | |||
|
r27613 | nocommit = opts.get('no_commit') | ||
importbranch = opts.get('import_branch') | ||||
|
r20500 | update = not opts.get('bypass') | ||
strip = opts["strip"] | ||||
|
r24259 | prefix = opts["prefix"] | ||
|
r20500 | sim = float(opts.get('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 | ||||
ui.debug('message:\n%s\n' % (message or '')) | ||||
if len(parents) == 1: | ||||
parents.append(repo[nullid]) | ||||
if opts.get('exact'): | ||||
if not nodeid or not p1: | ||||
raise error.Abort(_('not a Mercurial patch')) | ||||
p1 = repo[p1] | ||||
p2 = repo[p2 or nullid] | ||||
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] | ||||
p2 = repo[nullid] | ||||
except error.RepoError: | ||||
p1, p2 = parents | ||||
if p2.node() == nullid: | ||||
ui.warn(_("warning: import the patch as a normal revision\n" | ||||
"(use --exact to import the patch as a merge)\n")) | ||||
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()) | ||||
if opts.get('exact') or importbranch: | ||||
repo.dirstate.setbranch(branch or 'default') | ||||
partial = opts.get('partial', False) | ||||
files = set() | ||||
try: | ||||
patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix, | ||||
files=files, eolmode=None, similarity=sim / 100.0) | ||||
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: | ||
|
r37639 | if opts.get('exact') or p2: | ||
# 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 []) | ||||
editform = mergeeditform(repo[None], 'import.normal') | ||||
if opts.get('exact'): | ||||
editor = None | ||||
else: | ||||
editor = getcommiteditor(editform=editform, | ||||
**pycompat.strkwargs(opts)) | ||||
extra = {} | ||||
for idfunc in extrapreimport: | ||||
extrapreimportmap[idfunc](repo, patchdata, extra, opts) | ||||
overrides = {} | ||||
if partial: | ||||
overrides[('ui', 'allowemptycommit')] = True | ||||
with repo.ui.configoverride(overrides, 'import'): | ||||
n = repo.commit(message, user, | ||||
date, match=m, | ||||
editor=editor, extra=extra) | ||||
for idfunc in extrapostimport: | ||||
extrapostimportmap[idfunc](repo[n]) | ||||
else: | ||||
if opts.get('exact') or importbranch: | ||||
branch = branch or 'default' | ||||
else: | ||||
branch = p1.branch() | ||||
store = patch.filestore() | ||||
try: | ||||
|
r20500 | files = set() | ||
|
r21553 | try: | ||
|
r37639 | 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)) | ||
if opts.get('exact'): | ||||
editor = None | ||||
|
r20500 | else: | ||
|
r37639 | editor = getcommiteditor(editform='import.bypass') | ||
memctx = context.memctx(repo, (p1.node(), p2.node()), | ||||
message, | ||||
files=files, | ||||
filectxfn=store, | ||||
user=user, | ||||
date=date, | ||||
branch=branch, | ||||
editor=editor) | ||||
n = memctx.commit() | ||||
finally: | ||||
store.close() | ||||
if opts.get('exact') and nocommit: | ||||
# --exact with --no-commit is still useful in that it does merge | ||||
# and branch bits | ||||
ui.warn(_("warning: can't check exact import with --no-commit\n")) | ||||
|
r37833 | elif opts.get('exact') and (not n or hex(n) != nodeid): | ||
|
r37639 | raise error.Abort(_('patch is damaged or loses information')) | ||
msg = _('applied to working directory') | ||||
if n: | ||||
# i18n: refers to a short changeset id | ||||
msg = _('created %s') % short(n) | ||||
return msg, n, rejects | ||||
|
r20500 | |||
|
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 = {} | ||||
|
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: | ||||
prev = nullid | ||||
|
r37620 | fm.context(ctx=ctx) | ||
fm.plain('# HG changeset patch\n') | ||||
fm.write('user', '# User %s\n', ctx.user()) | ||||
fm.plain('# Date %d %d\n' % ctx.date()) | ||||
fm.write('date', '# %s\n', fm.formatdate(ctx.date())) | ||||
fm.condwrite(branch and branch != 'default', | ||||
'branch', '# Branch %s\n', branch) | ||||
fm.write('node', '# Node ID %s\n', hex(node)) | ||||
fm.plain('# Parent %s\n' % hex(prev)) | ||||
|
r32433 | if len(parents) > 1: | ||
|
r37620 | fm.plain('# Parent %s\n' % hex(parents[1])) | ||
fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name='node')) | ||||
# TODO: redesign extraexportmap function to support formatter | ||||
|
r32433 | for headerid in extraexport: | ||
header = extraexportmap[headerid](seqno, ctx) | ||||
if header is not None: | ||||
|
r37620 | fm.plain('# %s\n' % header) | ||
fm.write('desc', '%s\n', ctx.description().rstrip()) | ||||
fm.plain('\n') | ||||
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 | |||
|
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] | ||||
if not dest.startswith('<'): | ||||
repo.ui.note("%s\n" % dest) | ||||
|
r37620 | fm.startitem() | ||
_exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts) | ||||
|
r37622 | |||
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] | ||||
|
r37619 | dest = makefilename(ctx, fntemplate, | ||
total=total, seqno=seqno, revwidth=revwidth) | ||||
filemap.setdefault(dest, []).append((seqno, rev)) | ||||
for dest in filemap: | ||||
|
r37622 | with formatter.maybereopen(basefm, dest) as fm: | ||
|
r37619 | repo.ui.note("%s\n" % dest) | ||
for seqno, rev in filemap[dest]: | ||||
|
r37620 | fm.startitem() | ||
|
r37619 | ctx = repo[rev] | ||
|
r37620 | _exportsingle(repo, ctx, fm, match, switch_parent, seqno, | ||
|
r37619 | 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) | ||||
scmutil.prefetchfiles(repo, revs, scmutil.matchfiles(repo, allfiles)) | ||||
|
r37622 | def export(repo, revs, basefm, fntemplate='hg-%h.patch', switch_parent=False, | ||
|
r26229 | opts=None, match=None): | ||
|
r32430 | '''export changesets as hg patches | ||
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. | ||
|
r32430 | ''' | ||
|
r42673 | _prefetchchangedfiles(repo, revs, match) | ||
|
r37781 | |||
|
r37621 | if not fntemplate: | ||
|
r37622 | _exportfile(repo, revs, basefm, '<unnamed>', switch_parent, opts, match) | ||
|
r37618 | else: | ||
|
r37622 | _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 | |||
|
r37622 | dest = getattr(fp, 'name', '<unnamed>') | ||
with formatter.formatter(repo.ui, fp, 'export', {}) as fm: | ||||
_exportfile(repo, revs, fm, dest, switch_parent, opts, match) | ||||
|
r37621 | |||
|
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: | ||
|
r29795 | fm.write('index', '%i ', index) | ||
|
r33856 | fm.write('prednode', '%s ', hex(marker.prednode())) | ||
|
r29795 | succs = marker.succnodes() | ||
fm.condwrite(succs, 'succnodes', '%s ', | ||||
fm.formatlist(map(hex, succs), name='node')) | ||||
fm.write('flag', '%X ', marker.flags()) | ||||
|
r22260 | parents = marker.parentnodes() | ||
if parents is not None: | ||||
|
r29795 | fm.write('parentnodes', '{%s} ', | ||
fm.formatlist(map(hex, parents), name='node', sep=', ')) | ||||
fm.write('date', '(%s) ', fm.formatdate(marker.date())) | ||||
meta = marker.metadata().copy() | ||||
meta.pop('date', None) | ||||
|
r38594 | smeta = pycompat.rapply(pycompat.maybebytestr, meta) | ||
|
r35892 | fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', ')) | ||
|
r29795 | fm.plain('\n') | ||
|
r20470 | |||
|
r3814 | def finddate(ui, repo, date): | ||
"""Find the tipmost changeset that matches the given date spec""" | ||||
|
r9667 | |||
|
r36625 | df = dateutil.matchdate(date) | ||
|
r14322 | m = scmutil.matchall(repo) | ||
|
r3814 | results = {} | ||
|
r9662 | |||
def prep(ctx, fns): | ||||
d = ctx.date() | ||||
if df(d[0]): | ||||
|
r9668 | results[ctx.rev()] = d | ||
|
r9662 | |||
|
r9667 | for ctx in walkchangerevs(repo, m, {'rev': None}, prep): | ||
|
r9662 | rev = ctx.rev() | ||
if rev in results: | ||||
|
r16937 | ui.status(_("found revision %s from %s\n") % | ||
|
r36625 | (rev, dateutil.datestr(results[rev]))) | ||
|
r32155 | return '%d' % rev | ||
|
r3814 | |||
|
r26587 | raise error.Abort(_("revision matching date not found")) | ||
|
r3814 | |||
|
r20553 | def increasingwindows(windowsize=8, sizelimit=512): | ||
while True: | ||||
yield windowsize | ||||
if windowsize < sizelimit: | ||||
windowsize *= 2 | ||||
|
r16776 | |||
|
r35700 | def _walkrevs(repo, opts): | ||
# Default --rev value depends on --follow but --follow behavior | ||||
# depends on revisions resolved from --rev... | ||||
follow = opts.get('follow') or opts.get('follow_first') | ||||
if opts.get('rev'): | ||||
revs = scmutil.revrange(repo, opts['rev']) | ||||
elif follow and repo.dirstate.p1() == nullid: | ||||
revs = smartset.baseset() | ||||
elif follow: | ||||
revs = repo.revs('reverse(:.)') | ||||
else: | ||||
revs = smartset.spanset(repo) | ||||
revs.reverse() | ||||
return revs | ||||
|
r19290 | class FileWalkError(Exception): | ||
pass | ||||
def walkfilerevs(repo, match, follow, revs, fncache): | ||||
'''Walks the file history for the matched files. | ||||
Returns the changeset revs that are involved in the file history. | ||||
Throws FileWalkError if the file history can't be walked using | ||||
filelogs alone. | ||||
''' | ||||
wanted = set() | ||||
copies = [] | ||||
minrev, maxrev = min(revs), max(revs) | ||||
|
r42004 | def filerevs(filelog, last): | ||
|
r19290 | """ | ||
Only files, no patterns. Check the history of each file. | ||||
Examines filelog entries within minrev, maxrev linkrev range | ||||
Returns an iterator yielding (linkrev, parentlinkrevs, copied) | ||||
tuples in backwards order | ||||
""" | ||||
cl_count = len(repo) | ||||
revs = [] | ||||
|
r38806 | for j in pycompat.xrange(0, last + 1): | ||
|
r19290 | linkrev = filelog.linkrev(j) | ||
if linkrev < minrev: | ||||
continue | ||||
# only yield rev for which we have the changelog, it can | ||||
# happen while doing "hg log" during a pull or commit | ||||
if linkrev >= cl_count: | ||||
break | ||||
parentlinkrevs = [] | ||||
for p in filelog.parentrevs(j): | ||||
if p != nullrev: | ||||
parentlinkrevs.append(filelog.linkrev(p)) | ||||
n = filelog.node(j) | ||||
revs.append((linkrev, parentlinkrevs, | ||||
follow and filelog.renamed(n))) | ||||
return reversed(revs) | ||||
def iterfiles(): | ||||
pctx = repo['.'] | ||||
for filename in match.files(): | ||||
if follow: | ||||
if filename not in pctx: | ||||
|
r26587 | raise error.Abort(_('cannot follow file not in parent ' | ||
|
r19290 | 'revision: "%s"') % filename) | ||
yield filename, pctx[filename].filenode() | ||||
else: | ||||
yield filename, None | ||||
for filename_node in copies: | ||||
yield filename_node | ||||
for file_, node in iterfiles(): | ||||
filelog = repo.file(file_) | ||||
if not len(filelog): | ||||
if node is None: | ||||
# A zero count may be a directory or deleted file, so | ||||
# try to find matching entries on the slow path. | ||||
if follow: | ||||
|
r26587 | raise error.Abort( | ||
|
r19290 | _('cannot follow nonexistent file: "%s"') % file_) | ||
raise FileWalkError("Cannot walk via filelog") | ||||
else: | ||||
continue | ||||
if node is None: | ||||
last = len(filelog) - 1 | ||||
else: | ||||
last = filelog.rev(node) | ||||
# keep track of all ancestors of the file | ||||
|
r32291 | ancestors = {filelog.linkrev(last)} | ||
|
r19290 | |||
# iterate from latest to oldest revision | ||||
|
r42004 | for rev, flparentlinkrevs, copied in filerevs(filelog, last): | ||
|
r19290 | if not follow: | ||
if rev > maxrev: | ||||
continue | ||||
else: | ||||
# Note that last might not be the first interesting | ||||
# rev to us: | ||||
# if the file has been changed after maxrev, we'll | ||||
# have linkrev(last) > maxrev, and we still need | ||||
# to explore the file graph | ||||
if rev not in ancestors: | ||||
continue | ||||
# XXX insert 1327 fix here | ||||
if flparentlinkrevs: | ||||
ancestors.update(flparentlinkrevs) | ||||
fncache.setdefault(rev, []).append(file_) | ||||
wanted.add(rev) | ||||
if copied: | ||||
copies.append(copied) | ||||
return wanted | ||||
|
r24391 | class _followfilter(object): | ||
def __init__(self, repo, onlyfirst=False): | ||||
self.repo = repo | ||||
self.startrev = nullrev | ||||
self.roots = set() | ||||
self.onlyfirst = onlyfirst | ||||
def match(self, rev): | ||||
def realparents(rev): | ||||
if self.onlyfirst: | ||||
return self.repo.changelog.parentrevs(rev)[0:1] | ||||
else: | ||||
return filter(lambda x: x != nullrev, | ||||
self.repo.changelog.parentrevs(rev)) | ||||
if self.startrev == nullrev: | ||||
self.startrev = rev | ||||
return True | ||||
if rev > self.startrev: | ||||
# forward: all descendants | ||||
if not self.roots: | ||||
self.roots.add(self.startrev) | ||||
for parent in realparents(rev): | ||||
if parent in self.roots: | ||||
self.roots.add(rev) | ||||
return True | ||||
else: | ||||
# backwards: all parents | ||||
if not self.roots: | ||||
self.roots.update(realparents(self.startrev)) | ||||
if rev in self.roots: | ||||
self.roots.remove(rev) | ||||
self.roots.update(realparents(rev)) | ||||
return True | ||||
return False | ||||
|
r9665 | def walkchangerevs(repo, match, opts, prepare): | ||
|
r7807 | '''Iterate over files and the revs in which they changed. | ||
|
r3650 | |||
Callers most commonly need to iterate backwards over the history | ||||
|
r7807 | in which they are interested. Doing so has awful (quadratic-looking) | ||
|
r3650 | performance, so we use iterators in a "windowed" way. | ||
We walk a window of revisions in the desired order. Within the | ||||
window, we first walk forwards to gather data, then in the desired | ||||
order (usually backwards) to display it. | ||||
|
r9662 | This function returns an iterator yielding contexts. Before | ||
yielding each context, the iterator will first call the prepare | ||||
function on each context in the window in forward order.''' | ||||
|
r3650 | |||
|
r38670 | allfiles = opts.get('all_files') | ||
|
r3650 | follow = opts.get('follow') or opts.get('follow_first') | ||
|
r35700 | revs = _walkrevs(repo, opts) | ||
|
r11281 | if not revs: | ||
return [] | ||||
|
r8152 | wanted = set() | ||
|
r35474 | slowpath = match.anypats() or (not match.always() and opts.get('removed')) | ||
|
r3650 | fncache = {} | ||
|
r37319 | change = repo.__getitem__ | ||
|
r3650 | |||
|
r11632 | # First step is to fill wanted, the set of revisions that we want to yield. | ||
# When it does not induce extra cost, we also fill fncache for revisions in | ||||
# wanted: a cache of filenames that were changed (ctx.files()) and that | ||||
# match the file filtering conditions. | ||||
|
r38996 | if match.always() or allfiles: | ||
|
r3650 | # No files, no patterns. Display all revs. | ||
|
r20553 | wanted = revs | ||
|
r25271 | elif not slowpath: | ||
|
r11632 | # We only have to read through the filelog to find wanted revisions | ||
|
r19290 | try: | ||
wanted = walkfilerevs(repo, match, follow, revs, fncache) | ||||
except FileWalkError: | ||||
slowpath = True | ||||
|
r11608 | |||
|
r19290 | # We decided to fall back to the slowpath because at least one | ||
# of the paths was not a file. Check to see if at least one of them | ||||
# existed in history, otherwise simply return | ||||
|
r17746 | for path in match.files(): | ||
if path == '.' or path in repo.store: | ||||
break | ||||
|
r18340 | else: | ||
return [] | ||||
|
r17746 | |||
|
r3650 | if slowpath: | ||
|
r11632 | # We have to read the changelog to match filenames against | ||
# changed files | ||||
|
r3650 | if follow: | ||
|
r26587 | raise error.Abort(_('can only follow copies/renames for explicit ' | ||
|
r8761 | 'filenames')) | ||
|
r3650 | |||
# The slow path checks files modified in every changeset. | ||||
|
r19730 | # This is really slow on large repos, so compute the set lazily. | ||
class lazywantedset(object): | ||||
def __init__(self): | ||||
self.set = set() | ||||
self.revs = set(revs) | ||||
# No need to worry about locality here because it will be accessed | ||||
# in the same order as the increasing window below. | ||||
def __contains__(self, value): | ||||
if value in self.set: | ||||
return True | ||||
elif not value in self.revs: | ||||
return False | ||||
else: | ||||
self.revs.discard(value) | ||||
ctx = change(value) | ||||
|
r41832 | if allfiles: | ||
matches = list(ctx.manifest().walk(match)) | ||||
else: | ||||
matches = [f for f in ctx.files() if match(f)] | ||||
|
r19730 | if matches: | ||
fncache[value] = matches | ||||
self.set.add(value) | ||||
return True | ||||
return False | ||||
def discard(self, value): | ||||
self.revs.discard(value) | ||||
self.set.discard(value) | ||||
wanted = lazywantedset() | ||||
|
r3650 | |||
# it might be worthwhile to do this in the iterator if the rev range | ||||
# is descending and the prune args are all within that range | ||||
for rev in opts.get('prune', ()): | ||||
|
r16380 | rev = repo[rev].rev() | ||
|
r24391 | ff = _followfilter(repo) | ||
|
r3650 | stop = min(revs[0], revs[-1]) | ||
|
r38806 | for x in pycompat.xrange(rev, stop - 1, -1): | ||
|
r8152 | if ff.match(x): | ||
|
r20553 | wanted = wanted - [x] | ||
|
r18710 | |||
|
r11632 | # Now that wanted is correctly initialized, we can iterate over the | ||
# revision range, yielding only revisions in wanted. | ||||
|
r3650 | def iterate(): | ||
|
r25272 | if follow and match.always(): | ||
|
r24391 | ff = _followfilter(repo, onlyfirst=opts.get('follow_first')) | ||
|
r3650 | def want(rev): | ||
|
r8119 | return ff.match(rev) and rev in wanted | ||
|
r3650 | else: | ||
def want(rev): | ||||
return rev in wanted | ||||
|
r20553 | it = iter(revs) | ||
stopiteration = False | ||||
for windowsize in increasingwindows(): | ||||
nrevs = [] | ||||
|
r38806 | for i in pycompat.xrange(windowsize): | ||
|
r25147 | rev = next(it, None) | ||
if rev is None: | ||||
|
r20553 | stopiteration = True | ||
break | ||||
|
r25147 | elif want(rev): | ||
nrevs.append(rev) | ||||
|
r8209 | for rev in sorted(nrevs): | ||
|
r3650 | fns = fncache.get(rev) | ||
|
r9654 | ctx = change(rev) | ||
|
r3650 | if not fns: | ||
def fns_generator(): | ||||
|
r38361 | if allfiles: | ||
fiter = iter(ctx) | ||||
else: | ||||
fiter = ctx.files() | ||||
for f in fiter: | ||||
|
r9652 | if match(f): | ||
|
r3650 | yield f | ||
fns = fns_generator() | ||||
|
r9662 | prepare(ctx, fns) | ||
|
r3650 | for rev in nrevs: | ||
|
r9662 | yield change(rev) | ||
|
r20553 | |||
if stopiteration: | ||||
break | ||||
|
r9652 | return iterate() | ||
|
r5034 | |||
|
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. | ||||
|
r34344 | 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: | ||||
|
r41799 | ui.status(_('adding %s\n') % uipathfn(f), | ||
|
r40403 | label='ui.addremove.added') | ||
|
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) | ||
|
r32147 | if opts.get(r'subrepos'): | ||
|
r41799 | bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, False, | ||
**opts)) | ||||
|
r15410 | else: | ||
|
r41799 | bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, True, | ||
**opts)) | ||||
|
r15410 | except error.LookupError: | ||
ui.status(_("skipping missing subrepository: %s\n") | ||||
|
r41799 | % uipathfn(subpath)) | ||
|
r12270 | |||
|
r32147 | if not opts.get(r'dry_run'): | ||
|
r12270 | rejected = wctx.add(names, prefix) | ||
|
r12269 | bad.extend(f for f in rejected if f in match.files()) | ||
return bad | ||||
|
r32005 | def addwebdirpath(repo, serverpath, webconf): | ||
webconf[serverpath] = repo.root | ||||
repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root)) | ||||
for r in repo.revs('filelog("path:.hgsub")'): | ||||
ctx = repo[r] | ||||
for subpath in ctx.substate: | ||||
ctx.sub(subpath).addwebdirpath(serverpath, webconf) | ||||
|
r41802 | def forget(ui, repo, match, prefix, uipathfn, explicitonly, dryrun, | ||
interactive): | ||||
|
r37796 | if dryrun and interactive: | ||
raise error.Abort(_("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: | ||
|
r41802 | subbad, subforgot = sub.forget(submatch, subprefix, subuipathfn, | ||
dryrun=dryrun, | ||||
|
r37796 | interactive=interactive) | ||
|
r15912 | bad.extend([subpath + '/' + f for f in subbad]) | ||
forgot.extend([subpath + '/' + f for f in subforgot]) | ||||
except error.LookupError: | ||||
ui.status(_("skipping missing subrepository: %s\n") | ||||
|
r41802 | % uipathfn(subpath)) | ||
|
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 | ||||
|
r15912 | ui.warn(_('not removing %s: ' | ||
'file is already untracked\n') | ||||
|
r41802 | % uipathfn(f)) | ||
|
r15912 | bad.append(f) | ||
|
r37796 | if interactive: | ||
|
r37774 | responses = _('[Ynsa?]' | ||
'$$ &Yes, forget this file' | ||||
'$$ &No, skip this file' | ||||
'$$ &Skip remaining files' | ||||
'$$ Include &all remaining files' | ||||
'$$ &? (display help)') | ||||
for filename in forget[:]: | ||||
|
r41810 | r = ui.promptchoice(_('forget %s %s') % | ||
(uipathfn(filename), responses)) | ||||
|
r37774 | if r == 4: # ? | ||
while r == 4: | ||||
for c, t in ui.extractchoices(responses)[1]: | ||||
ui.write('%s - %s\n' % (c, encoding.lower(t))) | ||||
|
r41810 | r = ui.promptchoice(_('forget %s %s') % | ||
(uipathfn(filename), responses)) | ||||
|
r37774 | if r == 0: # yes | ||
continue | ||||
elif r == 1: # no | ||||
forget.remove(filename) | ||||
elif r == 2: # Skip | ||||
fnindex = forget.index(filename) | ||||
del forget[fnindex:] | ||||
break | ||||
elif r == 3: # All | ||||
break | ||||
|
r15912 | for f in forget: | ||
|
r37796 | if ui.verbose or not match.exact(f) or interactive: | ||
|
r41802 | ui.status(_('removing %s\n') % uipathfn(f), | ||
|
r40403 | label='ui.addremove.removed') | ||
|
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 | ||
|
r41914 | def files(ui, ctx, m, uipathfn, fm, fmt, subrepos): | ||
|
r24275 | ret = 1 | ||
|
r38559 | needsfctx = ui.verbose or {'size', 'flags'} & fm.datahint() | ||
|
r24275 | for f in ctx.matches(m): | ||
fm.startitem() | ||||
|
r38560 | fm.context(ctx=ctx) | ||
|
r38559 | if needsfctx: | ||
|
r24275 | fc = ctx[f] | ||
fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags()) | ||||
|
r39405 | fm.data(path=f) | ||
|
r41717 | fm.plain(fmt % uipathfn(f)) | ||
|
r24275 | ret = 0 | ||
|
r25228 | for subpath in sorted(ctx.substate): | ||
|
r29802 | submatch = matchmod.subdirmatcher(subpath, m) | ||
|
r41914 | subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn) | ||
|
r29802 | if (subrepos or m.exact(subpath) or any(submatch.files())): | ||
|
r24413 | sub = ctx.sub(subpath) | ||
try: | ||||
|
r28387 | recurse = m.exact(subpath) or subrepos | ||
|
r41914 | if sub.printfiles(ui, submatch, subuipathfn, fm, fmt, | ||
recurse) == 0: | ||||
|
r24413 | ret = 0 | ||
except error.LookupError: | ||||
ui.status(_("skipping missing subrepository: %s\n") | ||||
|
r41813 | % uipathfn(subpath)) | ||
|
r24413 | |||
|
r24275 | return ret | ||
|
r41800 | def remove(ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, | ||
warnings=None): | ||||
|
r23289 | ret = 0 | ||
s = repo.status(match=m, clean=True) | ||||
modified, added, deleted, clean = s[0], s[1], s[3], s[6] | ||||
|
r23325 | wctx = repo[None] | ||
|
r28607 | if warnings is None: | ||
warnings = [] | ||||
warn = True | ||||
else: | ||||
warn = False | ||||
|
r28608 | subs = sorted(wctx.substate) | ||
|
r38366 | progress = ui.makeprogress(_('searching'), total=len(subs), | ||
unit=_('subrepos')) | ||||
|
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: | ||||
|
r41800 | if sub.removefiles(submatch, subprefix, subuipathfn, after, | ||
force, subrepos, dryrun, warnings): | ||||
|
r23325 | ret = 1 | ||
except error.LookupError: | ||||
|
r28607 | warnings.append(_("skipping missing subrepository: %s\n") | ||
|
r41800 | % uipathfn(subpath)) | ||
|
r38392 | progress.complete() | ||
|
r23325 | |||
|
r23289 | # warn about failure to delete explicit files/dirs | ||
|
r24635 | deleteddirs = util.dirs(deleted) | ||
|
r28608 | files = m.files() | ||
|
r38366 | progress = ui.makeprogress(_('deleting'), total=len(files), | ||
unit=_('files')) | ||||
|
r28608 | for f in files: | ||
|
r23325 | def insubrepo(): | ||
for subpath in wctx.substate: | ||||
|
r29622 | if f.startswith(subpath + '/'): | ||
|
r23325 | return True | ||
return False | ||||
|
r38366 | progress.increment() | ||
|
r24955 | isdir = f in deleteddirs or wctx.hasdir(f) | ||
|
r29622 | if (f in repo.dirstate or isdir or f == '.' | ||
or insubrepo() or f in subs): | ||||
|
r23289 | continue | ||
|
r23325 | |||
|
r23674 | if repo.wvfs.exists(f): | ||
if repo.wvfs.isdir(f): | ||||
|
r28607 | warnings.append(_('not removing %s: no tracked files\n') | ||
|
r41800 | % uipathfn(f)) | ||
|
r23289 | else: | ||
|
r28607 | warnings.append(_('not removing %s: file is untracked\n') | ||
|
r41800 | % uipathfn(f)) | ||
|
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 | ||
|
r38366 | progress = ui.makeprogress(_('skipping'), total=len(remaining), | ||
unit=_('files')) | ||||
|
r28608 | for f in remaining: | ||
|
r38366 | progress.increment() | ||
|
r35120 | if ui.verbose or (f in files): | ||
warnings.append(_('not removing %s: file still exists\n') | ||||
|
r41800 | % uipathfn(f)) | ||
|
r23289 | ret = 1 | ||
|
r38392 | progress.complete() | ||
|
r23289 | else: | ||
list = deleted + clean | ||||
|
r38366 | progress = ui.makeprogress(_('skipping'), | ||
total=(len(modified) + len(added)), | ||||
unit=_('files')) | ||||
|
r23289 | for f in modified: | ||
|
r38366 | progress.increment() | ||
|
r28607 | warnings.append(_('not removing %s: file is modified (use -f' | ||
|
r41800 | ' to force removal)\n') % uipathfn(f)) | ||
|
r23289 | ret = 1 | ||
for f in added: | ||||
|
r38366 | progress.increment() | ||
|
r29963 | warnings.append(_("not removing %s: file has been marked for add" | ||
|
r41800 | " (use 'hg forget' to undo add)\n") % uipathfn(f)) | ||
|
r23289 | ret = 1 | ||
|
r38392 | progress.complete() | ||
|
r28608 | |||
list = sorted(list) | ||||
|
r38366 | progress = ui.makeprogress(_('deleting'), total=len(list), | ||
unit=_('files')) | ||||
|
r28608 | for f in list: | ||
|
r23289 | if ui.verbose or not m.exact(f): | ||
|
r38366 | progress.increment() | ||
|
r41800 | ui.status(_('removing %s\n') % uipathfn(f), | ||
|
r40403 | label='ui.addremove.removed') | ||
|
r38392 | progress.complete() | ||
|
r23289 | |||
|
r37168 | if not dryrun: | ||
with repo.wlock(): | ||||
if not after: | ||||
for f in list: | ||||
if f in added: | ||||
continue # we never unlink added files on remove | ||||
|
r38512 | rmdir = repo.ui.configbool('experimental', | ||
'removeemptydirs') | ||||
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 | ||
|
r42678 | def _catfmtneedsdata(fm): | ||
return not fm.datahint() or 'data' in fm.datahint() | ||||
|
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) | ||
|
r35680 | fm.write('data', '%s', data) | ||
|
r39405 | fm.data(path=path) | ||
|
r35680 | |||
|
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: | ||
|
r36222 | filename = makefilename(ctx, fntemplate, | ||
|
r32576 | 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: | ||
|
r35680 | _updatecatformatter(fm, ctx, matcher, path, opts.get('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): | ||
scmutil.prefetchfiles(repo, [ctx.rev()], matcher) | ||||
|
r30340 | write(file) | ||
return 0 | ||||
except KeyError: | ||||
pass | ||||
|
r21040 | |||
|
r42678 | if _catfmtneedsdata(basefm): | ||
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) | ||
|
r41774 | if not sub.cat(submatch, basefm, fntemplate, subprefix, | ||
|
r35351 | **pycompat.strkwargs(opts)): | ||
|
r21041 | err = 0 | ||
except error.RepoLookupError: | ||||
|
r41798 | ui.status(_("skipping missing subrepository: %s\n") % | ||
|
r41812 | uipathfn(subpath)) | ||
|
r21041 | |||
|
r21040 | return err | ||
|
r5034 | def commit(ui, repo, commitfunc, pats, opts): | ||
'''commit the specified files or all outstanding changes''' | ||||
|
r6139 | date = opts.get('date') | ||
if date: | ||||
|
r36625 | opts['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 | ||||
|
r33822 | if opts.get('addremove'): | ||
dsguard = dirstateguard.dirstateguard(repo, 'commit') | ||||
|
r33823 | with dsguard or util.nullcontextmanager(): | ||
|
r33822 | if dsguard: | ||
|
r41801 | relative = scmutil.anypats(pats, opts) | ||
|
r41834 | uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative) | ||
|
r41801 | if scmutil.addremove(repo, matcher, "", uipathfn, opts) != 0: | ||
|
r33619 | raise error.Abort( | ||
_("failed to mark all new/missing files as added/removed")) | ||||
|
r33823 | return commitfunc(ui, repo, message, matcher, opts) | ||
|
r8407 | |||
|
r29819 | def samefile(f, ctx1, ctx2): | ||
if f in ctx1.manifest(): | ||||
a = ctx1.filectx(f) | ||||
if f in ctx2.manifest(): | ||||
b = ctx2.filectx(f) | ||||
return (not a.cmp(b) | ||||
and a.flags() == b.flags()) | ||||
else: | ||||
return False | ||||
else: | ||||
return f not in ctx2.manifest() | ||||
|
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): | ||
|
r23101 | ui.username() # raise exception if username not set | ||
|
r16458 | ui.note(_('amending changeset %s\n') % old) | ||
base = old.p1() | ||||
|
r33509 | with repo.wlock(), repo.lock(), repo.transaction('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()) | ||||
user = opts.get('user') or old.user() | ||||
|
r41158 | |||
|
r41159 | datemaydiffer = False # date-only change should be ignored? | ||
|
r41160 | if opts.get('date') and opts.get('currentdate'): | ||
raise error.Abort(_('--date and --currentdate are mutually ' | ||||
'exclusive')) | ||||
|
r41158 | if opts.get('date'): | ||
date = dateutil.parsedate(opts.get('date')) | ||||
|
r41160 | elif opts.get('currentdate'): | ||
date = dateutil.makedate() | ||||
|
r41209 | elif (ui.configbool('rewrite', 'update-timestamp') | ||
and opts.get('currentdate') is None): | ||||
|
r41158 | date = dateutil.makedate() | ||
|
r41159 | datemaydiffer = True | ||
|
r41158 | else: | ||
date = old.date() | ||||
|
r34123 | |||
|
r33438 | if len(old.parents()) > 1: | ||
# ctx.files() isn't reliable for merges, so fall back to the | ||||
# slower repo.status() method | ||||
|
r42224 | files = {fn for st in base.status(old)[:3] for fn in st} | ||
|
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) | ||
|
r34087 | if (opts.get('addremove') | ||
|
r41801 | and scmutil.addremove(repo, matcher, "", uipathfn, opts)): | ||
|
r34087 | raise error.Abort( | ||
_("failed to mark all new/missing files as added/removed")) | ||||
|
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. | ||||
if '.hgsub' in wctx or '.hgsub' in old: | ||||
|
r36026 | subs, commitsubs, newsubstate = subrepoutil.precommit( | ||
|
r35019 | ui, wctx, wctx._status, matcher) | ||
# amend should abort if commitsubrepos is enabled | ||||
assert not commitsubs | ||||
if subs: | ||||
|
r36026 | subrepoutil.writestate(repo, newsubstate) | ||
|
r35019 | |||
|
r36927 | ms = mergemod.mergestate.read(repo) | ||
mergeutil.checkunresolved(ms) | ||||
|
r34087 | filestoamend = set(f for f in wctx.files() if matcher(f)) | ||
changes = (len(filestoamend) > 0) | ||||
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) | ||
|
r35016 | files = [f for f in files if (not samefile(f, wctx, base) | ||
or f in wctx.deleted())] | ||||
|
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() | ||
|
r35401 | mctx = context.memfilectx(repo, ctx_, | ||
|
r33438 | fctx.path(), fctx.data(), | ||
islink='l' in flags, | ||||
isexec='x' in flags, | ||||
|
r42161 | copysource=copied.get(path)) | ||
|
r33438 | return mctx | ||
except KeyError: | ||||
return None | ||||
else: | ||||
ui.note(_('copying changeset %s to %s\n') % (old, base)) | ||||
# 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) | ||||
|
r33438 | editform = mergeeditform(old, '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. | ||||
doedit = not message or opts.get('edit') in [True, None] | ||||
else: | ||||
# Default if message is provided is to not invoke editor, but allow | ||||
# --edit. | ||||
doedit = opts.get('edit') is True | ||||
editor = getcommiteditor(edit=doedit, editform=editform) | ||||
|
r33438 | |||
pureextra = extra.copy() | ||||
extra['amend_source'] = old.hex() | ||||
new = context.memctx(repo, | ||||
parents=[base.node(), old.p2().node()], | ||||
text=message, | ||||
files=files, | ||||
filectxfn=filectxfn, | ||||
user=user, | ||||
date=date, | ||||
extra=extra, | ||||
editor=editor) | ||||
newdesc = changelog.stripdesc(new.description()) | ||||
|
r34087 | if ((not changes) | ||
|
r33438 | and newdesc == old.description() | ||
and user == old.user() | ||||
|
r41159 | and (date == old.date() or datemaydiffer) | ||
|
r33438 | and pureextra == old.extra()): | ||
# 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 | ||
|
r34083 | if opts.get('secret'): | ||
|
r38442 | commitphase = phases.secret | ||
newid = repo.commitctx(new) | ||||
|
r34057 | |||
# Reroute the working copy parent to the new changeset | ||||
repo.setparents(newid, nullid) | ||||
mapping = {old.node(): (newid,)} | ||||
|
r34795 | obsmetadata = None | ||
if opts.get('note'): | ||||
|
r38729 | obsmetadata = {'note': encoding.fromlocal(opts['note'])} | ||
|
r41242 | backup = ui.configbool('rewrite', 'backup-bundle') | ||
|
r38442 | scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata, | ||
|
r38853 | fixphase=True, targetphase=commitphase, | ||
backup=backup) | ||||
|
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 | ||||
# Update the state of the files which were added and | ||||
# and modified in the amend to "normal" in the dirstate. | ||||
normalfiles = set(wctx.modified() + wctx.added()) & filestoamend | ||||
for f in normalfiles: | ||||
dirstate.normal(f) | ||||
# 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) | ||||
|
r16458 | return newid | ||
|
r21999 | def commiteditor(repo, ctx, subs, editform=''): | ||
|
r8407 | if ctx.description(): | ||
return ctx.description() | ||||
|
r26742 | return commitforceeditor(repo, ctx, subs, editform=editform, | ||
unchangedmessagedetection=True) | ||||
|
r8407 | |||
|
r21999 | def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None, | ||
|
r26742 | editform='', unchangedmessagedetection=False): | ||
|
r21923 | if not extramsg: | ||
extramsg = _("Leave message empty to abort commit.") | ||||
|
r22012 | |||
forms = [e for e in editform.split('.') if e] | ||||
forms.insert(0, 'changeset') | ||||
|
r26742 | templatetext = None | ||
|
r22012 | while forms: | ||
|
r32878 | ref = '.'.join(forms) | ||
if repo.ui.config('committemplate', ref): | ||||
|
r26742 | templatetext = committext = buildcommittemplate( | ||
|
r32878 | 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 | ||||
|
r26742 | editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(), | ||
|
r30836 | editform=editform, pending=pending, | ||
|
r34030 | repopath=repo.path, action='commit') | ||
|
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: | ||
|
r30724 | text = text[:stripbelow.start()] | ||
text = re.sub("(?m)^HG:.*(\n|$)", "", text) | ||||
|
r21869 | os.chdir(olddir) | ||
if finishdesc: | ||||
text = finishdesc(text) | ||||
if not text.strip(): | ||||
|
r26587 | raise error.Abort(_("empty commit message")) | ||
|
r26742 | if unchangedmessagedetection and editortext == templatetext: | ||
raise error.Abort(_("commit message unchanged")) | ||||
|
r21869 | |||
return text | ||||
|
r32878 | def buildcommittemplate(repo, ctx, subs, extramsg, ref): | ||
|
r21924 | ui = repo.ui | ||
|
r32878 | spec = formatter.templatespec(ref, None, None) | ||
|
r35972 | t = logcmdutil.changesettemplater(ui, repo, spec) | ||
|
r32878 | t.t.cache.update((k, templater.unquotestring(v)) | ||
for k, v in repo.ui.configitems('committemplate')) | ||||
|
r22013 | |||
|
r21924 | if not extramsg: | ||
extramsg = '' # ensure that extramsg is string | ||||
ui.pushbuffer() | ||||
t.show(ctx, extramsg=extramsg) | ||||
return ui.popbuffer() | ||||
|
r26426 | def hgprefix(msg): | ||
return "\n".join(["HG: %s" % a for a in msg.split("\n") if a]) | ||||
|
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()) | ||||
edittext.append("") | ||||
edittext.append("") # Empty line between message and comments. | ||||
|
r26426 | edittext.append(hgprefix(_("Enter commit message." | ||
" Lines beginning with 'HG:' are removed."))) | ||||
edittext.append(hgprefix(extramsg)) | ||||
|
r8407 | edittext.append("HG: --") | ||
|
r26426 | edittext.append(hgprefix(_("user: %s") % ctx.user())) | ||
|
r8407 | if ctx.p2(): | ||
|
r26426 | edittext.append(hgprefix(_("branch merge"))) | ||
|
r8407 | if ctx.branch(): | ||
|
r26426 | edittext.append(hgprefix(_("branch '%s'") % ctx.branch())) | ||
|
r24986 | if bookmarks.isactivewdirparent(repo): | ||
|
r26426 | edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark)) | ||
edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs]) | ||||
edittext.extend([hgprefix(_("added %s") % f) for f in added]) | ||||
edittext.extend([hgprefix(_("changed %s") % f) for f in modified]) | ||||
edittext.extend([hgprefix(_("removed %s") % f) for f in removed]) | ||||
|
r8707 | if not added and not modified and not removed: | ||
|
r26426 | edittext.append(hgprefix(_("no files changed"))) | ||
|
r8407 | edittext.append("") | ||
|
r21869 | return "\n".join(edittext) | ||
|
r14297 | |||
|
r26324 | def commitstatus(repo, node, branch, bheads=None, opts=None): | ||
if opts is None: | ||||
opts = {} | ||||
|
r18688 | ctx = repo[node] | ||
parents = ctx.parents() | ||||
if (not opts.get('amend') and bheads and node not in bheads and not | ||||
[x for x in parents if x.node() in bheads and x.branch() == branch]): | ||||
repo.ui.status(_('created new head\n')) | ||||
# 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 | ||||
if not opts.get('close_branch'): | ||||
for r in parents: | ||||
if r.closesbranch() and r.branch() == branch: | ||||
|
r36424 | repo.ui.status(_('reopening closed branch head %d\n') % r.rev()) | ||
|
r18688 | |||
if repo.ui.debugflag: | ||||
|
r36420 | repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())) | ||
|
r18688 | elif repo.ui.verbose: | ||
|
r36420 | repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx)) | ||
|
r18688 | |||
|
r27943 | def postcommitstatus(repo, pats, opts): | ||
return repo.status(match=scmutil.match(repo[None], pats, opts)) | ||||
|
r16304 | def revert(ui, repo, ctx, parents, *pats, **opts): | ||
|
r35148 | opts = pycompat.byteskwargs(opts) | ||
|
r16304 | parent, p2 = parents | ||
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 | |||
|
r24475 | interactive = opts.get('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 | ||
|
r22573 | path_ = path + '/' | ||
for f in names: | ||||
if f.startswith(path_): | ||||
return | ||||
|
r41813 | ui.warn("%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) | ||||
changes = repo.status(node1=node, match=m, | ||||
unknown=True, ignored=True, clean=True) | ||||
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) | ||
added = set(changes.added) | ||||
removed = set(changes.removed) | ||||
_deleted = set(changes.deleted) | ||||
unknown = set(changes.unknown) | ||||
unknown.update(changes.ignored) | ||||
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) | ||
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 | ||||
|
r29103 | 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 | ||||
if p2 != 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? | ||
|
r16304 | if src and src not in names and repo.dirstate[src] == '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: | ||||
if repo.dirstate[abs] != 'a': | ||||
added.add(abs) | ||||
dsadded -= added | ||||
|
r22490 | for abs in deladded: | ||
|
r16304 | if repo.dirstate[abs] == '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 | ||||
|
r21576 | actions = {'revert': ([], _('reverting %s\n')), | ||
'add': ([], _('adding %s\n')), | ||||
|
r22489 | 'remove': ([], _('removing %s\n')), | ||
|
r22491 | 'drop': ([], _('removing %s\n')), | ||
|
r22489 | 'forget': ([], _('forgetting %s\n')), | ||
|
r22231 | 'undelete': ([], _('undeleting %s\n')), | ||
|
r22234 | 'noop': (None, _('no changes needed to %s\n')), | ||
|
r22236 | 'unknown': (None, _('file not managed: %s\n')), | ||
|
r22231 | } | ||
|
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 | ||
|
r29498 | backupinteractive = 3 # do backup if interactively modified | ||
|
r22608 | backup = 2 # unconditionally do backup | ||
|
r22609 | check = 1 # check if the existing file differs from target | ||
|
r22608 | discard = 0 # never do backup | ||
if opts.get('no_backup'): | ||||
|
r29498 | backupinteractive = backup = check = discard | ||
if interactive: | ||||
dsmodifiedbackup = backupinteractive | ||||
else: | ||||
dsmodifiedbackup = backup | ||||
tobackup = set() | ||||
|
r16304 | |||
|
r22611 | backupanddel = actions['remove'] | ||
if not opts.get('no_backup'): | ||||
backupanddel = actions['drop'] | ||||
|
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 | ||||
|
r22372 | (modified, actions['revert'], discard), | ||
|
r22397 | # Modified compared to target, but local file is deleted | ||
(deleted, actions['revert'], discard), | ||||
|
r22371 | # Modified compared to target, local change | ||
|
r29498 | (dsmodified, actions['revert'], dsmodifiedbackup), | ||
|
r22371 | # Added since target | ||
|
r22488 | (added, actions['remove'], discard), | ||
# Added in working directory | ||||
|
r22489 | (dsadded, actions['forget'], discard), | ||
|
r22610 | # Added since target, have local modification | ||
|
r22611 | (modadded, backupanddel, backup), | ||
|
r22490 | # Added since target but file is missing in working directory | ||
|
r22491 | (deladded, actions['drop'], discard), | ||
|
r22371 | # Removed since target, before working copy parent | ||
|
r22396 | (removed, actions['add'], discard), | ||
# Same as `removed` but an unknown file exists at the same path | ||||
|
r22609 | (removunk, actions['add'], check), | ||
|
r22371 | # Removed since targe, marked as such in working copy parent | ||
|
r22396 | (dsremoved, actions['undelete'], discard), | ||
# Same as `dsremoved` but an unknown file exists at the same path | ||||
|
r22609 | (dsremovunk, actions['undelete'], check), | ||
|
r22371 | ## the following sets does not result in any file changes | ||
# File with no modification | ||||
|
r22372 | (clean, actions['noop'], discard), | ||
|
r22371 | # Existing file, not tracked anywhere | ||
|
r22372 | (unknown, actions['unknown'], discard), | ||
|
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) | ||||
elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])): | ||||
|
r41754 | absbakname = scmutil.backuppath(ui, repo, abs) | ||
bakname = os.path.relpath(absbakname, | ||||
start=repo.root) | ||||
|
r22609 | ui.note(_('saving current version of %s as %s\n') % | ||
|
r41754 | (uipathfn(abs), uipathfn(bakname))) | ||
|
r22609 | if not opts.get('dry_run'): | ||
|
r24475 | if interactive: | ||
|
r41754 | util.copyfile(target, absbakname) | ||
|
r24475 | else: | ||
|
r41754 | util.rename(target, absbakname) | ||
|
r39442 | if opts.get('dry_run'): | ||
if ui.verbose or not exact: | ||||
|
r41754 | ui.status(msg % uipathfn(abs)) | ||
|
r22233 | elif exact: | ||
|
r41754 | ui.warn(msg % uipathfn(abs)) | ||
|
r16304 | break | ||
|
r21575 | |||
|
r16304 | if not opts.get('dry_run'): | ||
|
r23965 | needdata = ('revert', 'add', 'undelete') | ||
|
r35941 | oplist = [actions[name][0] for name in needdata] | ||
|
r37780 | prefetch = scmutil.prefetchfiles | ||
matchfiles = scmutil.matchfiles | ||||
prefetch(repo, [ctx.rev()], | ||||
matchfiles(repo, | ||||
[f for sublist in oplist for f in sublist])) | ||||
|
r42238 | match = scmutil.match(repo[None], pats) | ||
|
r41754 | _performrevert(repo, parents, ctx, names, uipathfn, actions, | ||
|
r42238 | match, interactive, tobackup) | ||
|
r19129 | |||
|
r24134 | if targetsubs: | ||
# Revert the subrepos on the revert list | ||||
for sub in targetsubs: | ||||
|
r24463 | try: | ||
|
r35148 | wctx.sub(sub).revert(ctx.substate[sub], *pats, | ||
**pycompat.strkwargs(opts)) | ||||
|
r24463 | except KeyError: | ||
|
r26587 | raise error.Abort("subrepository '%s' does not exist in %s!" | ||
|
r24463 | % (sub, short(ctx.node()))) | ||
|
r16304 | |||
|
r41754 | def _performrevert(repo, parents, ctx, names, uipathfn, actions, | ||
|
r42238 | 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 | """ | ||
parent, p2 = parents | ||||
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: | ||||
|
r38512 | rmdir = repo.ui.configbool('experimental', 'removeemptydirs') | ||
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) | ||
|
r22489 | for f in actions['forget'][0]: | ||
|
r27985 | if interactive: | ||
|
r30530 | choice = repo.ui.promptchoice( | ||
|
r41754 | _("forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)) | ||
|
r27985 | if choice == 0: | ||
|
r39442 | prntstatusmsg('forget', f) | ||
|
r27985 | repo.dirstate.drop(f) | ||
else: | ||||
|
r36212 | excluded_files.append(f) | ||
|
r27985 | else: | ||
|
r39442 | prntstatusmsg('forget', f) | ||
|
r27985 | repo.dirstate.drop(f) | ||
|
r21576 | for f in actions['remove'][0]: | ||
|
r20571 | audit_path(f) | ||
|
r30532 | if interactive: | ||
choice = repo.ui.promptchoice( | ||||
|
r41754 | _("remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)) | ||
|
r30532 | if choice == 0: | ||
|
r39442 | prntstatusmsg('remove', f) | ||
|
r30532 | doremove(f) | ||
else: | ||||
|
r36212 | excluded_files.append(f) | ||
|
r30532 | else: | ||
|
r39442 | prntstatusmsg('remove', f) | ||
|
r30532 | doremove(f) | ||
|
r22491 | for f in actions['drop'][0]: | ||
audit_path(f) | ||||
|
r39442 | prntstatusmsg('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. | ||||
if p2 != nullid: | ||||
normal = repo.dirstate.normallookup | ||||
else: | ||||
normal = repo.dirstate.normal | ||||
|
r24359 | |||
|
r25259 | newlyaddedandmodifiedfiles = set() | ||
|
r24359 | if interactive: | ||
# Prompt the user for changes to revert | ||||
|
r36212 | torevert = [f for f in actions['revert'][0] if f not in excluded_files] | ||
m = scmutil.matchfiles(repo, torevert) | ||||
|
r41701 | diffopts = patch.difffeatureopts(repo.ui, whitespace=True, | ||
section='commands', | ||||
configprefix='revert.interactive.') | ||||
|
r25258 | diffopts.nodates = True | ||
diffopts.git = True | ||||
|
r42154 | operation = 'apply' | ||
if node == parent: | ||||
if repo.ui.configbool('experimental', | ||||
'revert.interactive.select-to-keep'): | ||||
operation = 'keep' | ||||
else: | ||||
operation = 'discard' | ||||
if operation == 'apply': | ||||
diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts) | ||||
else: | ||||
|
r25424 | diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts) | ||
|
r24359 | originalchunks = patch.parsepatch(diff) | ||
|
r25424 | |||
|
r24359 | try: | ||
|
r25424 | |||
|
r42238 | chunks, opts = recordfilter(repo.ui, originalchunks, match, | ||
|
r29283 | operation=operation) | ||
|
r42154 | if operation == 'discard': | ||
|
r25424 | chunks = patch.reversehunks(chunks) | ||
|
r34252 | except error.PatchError as err: | ||
|
r26587 | raise error.Abort(_('error parsing patch: %s') % err) | ||
|
r24359 | |||
|
r25259 | newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks) | ||
|
r29498 | if tobackup is None: | ||
tobackup = set() | ||||
|
r24359 | # Apply changes | ||
|
r28861 | fp = stringio() | ||
|
r39451 | # chunks are serialized per file, but files aren't sorted | ||
for f in sorted(set(c.header.filename() for c in chunks if ishunk(c))): | ||||
prntstatusmsg('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) | ||||
if operation == 'keep': | ||||
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: | ||||
for f in actions['revert'][0]: | ||||
|
r39442 | prntstatusmsg('revert', f) | ||
|
r25755 | checkout(f) | ||
|
r24359 | if normal: | ||
normal(f) | ||||
|
r20571 | |||
|
r21576 | for f in actions['add'][0]: | ||
|
r25259 | # Don't checkout modified files, they are already created by the diff | ||
if f not in newlyaddedandmodifiedfiles: | ||||
|
r39442 | prntstatusmsg('add', f) | ||
|
r25259 | checkout(f) | ||
repo.dirstate.add(f) | ||||
|
r20571 | |||
normal = repo.dirstate.normallookup | ||||
if node == parent and p2 == nullid: | ||||
normal = repo.dirstate.normal | ||||
|
r21576 | for f in actions['undelete'][0]: | ||
|
r41663 | if interactive: | ||
choice = repo.ui.promptchoice( | ||||
_("add back removed file %s (Yn)?$$ &Yes $$ &No") % f) | ||||
if choice == 0: | ||||
prntstatusmsg('undelete', f) | ||||
checkout(f) | ||||
normal(f) | ||||
else: | ||||
excluded_files.append(f) | ||||
else: | ||||
prntstatusmsg('undelete', f) | ||||
checkout(f) | ||||
normal(f) | ||||
|
r20571 | |||
copied = copies.pathcopies(repo[parent], ctx) | ||||
|
r21576 | for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]: | ||
|
r20571 | if f in copied: | ||
repo.dirstate.copy(copied[f], f) | ||||
|
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): | ||
|
r19474 | '''Look for an unfinished multistep operation, like graft, and abort | ||
if found. It's probably good to check this right before | ||||
bailifchanged(). | ||||
''' | ||||
|
r38220 | # Check for non-clearable states first, so things like rebase will take | ||
# precedence over update. | ||||
|
r42730 | for state in statemod._unfinishedstates: | ||
|
r42732 | if (state._clearable or (commit and state._allowcommit) or | ||
state._reportonly): | ||||
|
r38220 | continue | ||
|
r42730 | if state.isunfinished(repo): | ||
raise error.Abort(state.msg(), hint=state.hint()) | ||||
for s in statemod._unfinishedstates: | ||||
|
r42732 | if (not s._clearable or (commit and s._allowcommit) or | ||
(s._opname == 'merge' and skipmerge) or s._reportonly): | ||||
|
r19496 | continue | ||
|
r42730 | if s.isunfinished(repo): | ||
raise error.Abort(s.msg(), hint=s.hint()) | ||||
|
r19474 | |||
def clearunfinished(repo): | ||||
'''Check for unfinished operations (as above), and clear the ones | ||||
that are clearable. | ||||
''' | ||||
|
r42730 | for state in statemod._unfinishedstates: | ||
|
r42732 | if state._reportonly: | ||
continue | ||||
|
r42730 | if not state._clearable and state.isunfinished(repo): | ||
raise error.Abort(state.msg(), hint=state.hint()) | ||||
for s in statemod._unfinishedstates: | ||||
|
r42732 | if s._opname == 'merge' or state._reportonly: | ||
continue | ||||
|
r42730 | if s._clearable and s.isunfinished(repo): | ||
util.unlink(repo.vfs.join(s._fname)) | ||||
|
r24991 | |||
|
r28120 | def howtocontinue(repo): | ||
'''Check for an unfinished operation and return the command to finish | ||||
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. | ||||
''' | ||||
contmsg = _("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): | ||
|
r28120 | return contmsg % _("hg commit"), False | ||
return None, None | ||||
def checkafterresolved(repo): | ||||
'''Inform the user about the next action after completing hg resolve | ||||
|
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. | ||||
''' | ||||
msg, warning = howtocontinue(repo) | ||||
if msg is not None: | ||||
if warning: | ||||
repo.ui.warn("%s\n" % msg) | ||||
else: | ||||
repo.ui.note("%s\n" % msg) | ||||
def wrongtooltocontinue(repo, task): | ||||
'''Raise an abort suggesting how to properly continue if there is an | ||||
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. | ||||
''' | ||||
after = howtocontinue(repo) | ||||
hint = None | ||||
if after[1]: | ||||
hint = after[0] | ||||
raise error.Abort(_('no %s in progress') % task, hint=hint) | ||||
|
r42768 | |||
def abortgraft(ui, repo, graftstate): | ||||
"""abort the interrupted graft and rollbacks to the state before interrupted | ||||
graft""" | ||||
if not graftstate.exists(): | ||||
raise error.Abort(_("no interrupted graft to abort")) | ||||
statedata = readgraftstate(repo, graftstate) | ||||
newnodes = statedata.get('newnodes') | ||||
if newnodes is None: | ||||
# and old graft state which does not have all the data required to abort | ||||
# the graft | ||||
raise error.Abort(_("cannot abort using an old graftstate")) | ||||
# changeset from which graft operation was started | ||||
if len(newnodes) > 0: | ||||
startctx = repo[newnodes[0]].p1() | ||||
else: | ||||
startctx = repo['.'] | ||||
# whether to strip or not | ||||
cleanup = False | ||||
from . import hg | ||||
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: | ||||
repo.ui.warn(_("cannot clean up public changesets %s\n") | ||||
% ', '.join(bytes(repo[r]) for r in immutable), | ||||
hint=_("see 'hg help phases' for details")) | ||||
cleanup = False | ||||
# checking that no new nodes are created on top of grafted revs | ||||
desc = set(repo.changelog.descendants(newnodes)) | ||||
if desc - set(newnodes): | ||||
repo.ui.warn(_("new changesets detected on destination " | ||||
"branch, can't strip\n")) | ||||
cleanup = False | ||||
if cleanup: | ||||
with repo.wlock(), repo.lock(): | ||||
hg.updaterepo(repo, startctx.node(), overwrite=True) | ||||
# stripping the new nodes created | ||||
strippoints = [c.node() for c in repo.set("roots(%ld)", | ||||
newnodes)] | ||||
repair.strip(repo.ui, repo, strippoints, backup=False) | ||||
if not cleanup: | ||||
# we don't update to the startnode if we can't strip | ||||
startctx = repo['.'] | ||||
hg.updaterepo(repo, startctx.node(), overwrite=True) | ||||
ui.status(_("graft aborted\n")) | ||||
ui.status(_("working directory is now at %s\n") % startctx.hex()[:12]) | ||||
graftstate.delete() | ||||
return 0 | ||||
def readgraftstate(repo, graftstate): | ||||
"""read the graft state file and return a dict of the data stored in it""" | ||||
try: | ||||
return graftstate.read() | ||||
except error.CorruptedState: | ||||
nodes = repo.vfs.read('graftstate').splitlines() | ||||
return {'nodes': nodes} | ||||