cmdutil.py
3151 lines
| 112.6 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 | ||
import errno | ||||
import os | ||||
import re | ||||
import tempfile | ||||
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, | ||
|
r28322 | obsolete, | ||
patch, | ||||
pathutil, | ||||
|
r30519 | pycompat, | ||
|
r32337 | registrar, | ||
|
r28322 | revlog, | ||
|
r35763 | rewriteutil, | ||
|
r28322 | scmutil, | ||
|
r31023 | smartset, | ||
|
r36026 | subrepoutil, | ||
|
r28322 | templater, | ||
util, | ||||
|
r31237 | vfs as vfsmod, | ||
|
r28322 | ) | ||
|
r28861 | stringio = util.stringio | ||
|
r2874 | |||
|
r32375 | # templates of common command options | ||
dryrunopts = [ | ||||
('n', 'dry-run', None, | ||||
_('do not perform actions, just print output')), | ||||
] | ||||
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')), | ||||
] | ||||
# hidden for now | ||||
formatteropts = [ | ||||
('T', 'template', '', | ||||
_('display with template (EXPERIMENTAL)'), _('TEMPLATE')), | ||||
] | ||||
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 = [ | ||||
('t', 'tool', '', _('specify merge tool')), | ||||
] | ||||
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: | ||||
if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \ | ||||
originalchunks: | ||||
newlyaddedandmodifiedfiles.add(chunk.header.filename()) | ||||
return newlyaddedandmodifiedfiles | ||||
|
r10401 | def parsealiases(cmd): | ||
return cmd.lstrip("^").split("|") | ||||
|
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 | ||||
|
r25310 | def filterchunks(ui, originalhunks, usecurses, testfile, operation=None): | ||
|
r24343 | if usecurses: | ||
if testfile: | ||||
recordfn = crecordmod.testdecorator(testfile, | ||||
crecordmod.testchunkselector) | ||||
else: | ||||
recordfn = crecordmod.chunkselector | ||||
|
r30534 | return crecordmod.filterpatch(ui, originalhunks, recordfn, operation) | ||
|
r24343 | |||
else: | ||||
|
r25310 | return patch.filterpatch(ui, originalhunks, operation) | ||
def recordfilter(ui, originalhunks, operation=None): | ||||
""" 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, | ||
testfile, 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)')) | ||
|
r28815 | def fail(f, msg): | ||
raise error.Abort('%s: %s' % (f, msg)) | ||||
force = opts.get('force') | ||||
if not force: | ||||
vdirs = [] | ||||
match.explicitdir = vdirs.append | ||||
match.bad = fail | ||||
|
r24272 | status = repo.status(match=match) | ||
|
r28815 | if not force: | ||
repo.checkcommitpatterns(wctx, vdirs, match, status, fail) | ||||
|
r24272 | diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True) | ||
diffopts.nodates = True | ||||
diffopts.git = True | ||||
|
r27411 | diffopts.showfunc = True | ||
|
r27637 | originaldiff = patch.diff(repo, changes=status, opts=diffopts) | ||
|
r24341 | originalchunks = patch.parsepatch(originaldiff) | ||
|
r24272 | |||
|
r28570 | # 1. filter patch, since we are intending to apply subset of it | ||
|
r24272 | try: | ||
|
r27155 | chunks, newopts = filterfn(ui, originalchunks) | ||
|
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: | ||||
|
r24845 | 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: | ||||
fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', | ||||
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()) | ||
|
r24272 | mergemod.update(repo, repo.dirstate.p1(), | ||
|
r27344 | False, 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: | ||
|
r26587 | raise error.Abort(str(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 | |||
path is the path to the directory | ||||
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 | ||||
self.statuses = set([]) | ||||
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: | ||||
subdirpath = os.path.join(self.path, subdir) | ||||
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: | ||
|
r34685 | yield st, os.path.join(self.path, f) | ||
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 | ||||
|
r34685 | that status. -> yield (status, dirpath) | ||
|
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: | ||||
|
r34685 | yield onlyst, self.path + pycompat.ossep | ||
|
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( | ||||
|
r34982 | [' %s' % util.pathto(repo.root, pycompat.getcwd(), path) | ||
for path in 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 _helpmessage(continuecmd, abortcmd): | ||||
msg = _('To continue: %s\n' | ||||
'To abort: %s') % (continuecmd, abortcmd) | ||||
return _commentlines(msg) | ||||
def _rebasemsg(): | ||||
return _helpmessage('hg rebase --continue', 'hg rebase --abort') | ||||
def _histeditmsg(): | ||||
return _helpmessage('hg histedit --continue', 'hg histedit --abort') | ||||
def _unshelvemsg(): | ||||
return _helpmessage('hg unshelve --continue', 'hg unshelve --abort') | ||||
def _updatecleanmsg(dest=None): | ||||
warning = _('warning: this will discard uncommitted changes') | ||||
return 'hg update --clean %s (%s)' % (dest or '.', warning) | ||||
def _graftmsg(): | ||||
# tweakdefaults requires `update` to have a rev hence the `.` | ||||
return _helpmessage('hg graft --continue', _updatecleanmsg()) | ||||
def _mergemsg(): | ||||
# tweakdefaults requires `update` to have a rev hence the `.` | ||||
return _helpmessage('hg commit', _updatecleanmsg()) | ||||
def _bisectmsg(): | ||||
msg = _('To mark the changeset good: hg bisect --good\n' | ||||
'To mark the changeset bad: hg bisect --bad\n' | ||||
'To abort: hg bisect --reset\n') | ||||
return _commentlines(msg) | ||||
def fileexistspredicate(filename): | ||||
return lambda repo: repo.vfs.exists(filename) | ||||
def _mergepredicate(repo): | ||||
return len(repo[None].parents()) > 1 | ||||
STATES = ( | ||||
# (state, predicate to detect states, helpful message function) | ||||
('histedit', fileexistspredicate('histedit-state'), _histeditmsg), | ||||
('bisect', fileexistspredicate('bisect.state'), _bisectmsg), | ||||
('graft', fileexistspredicate('graftstate'), _graftmsg), | ||||
('unshelve', fileexistspredicate('unshelverebasestate'), _unshelvemsg), | ||||
('rebase', fileexistspredicate('rebasestate'), _rebasemsg), | ||||
# The merge state is part of a list that will be iterated over. | ||||
# They need to be last because some of the other unfinished states may also | ||||
# be in a merge or update state (eg. rebase, histedit, graft, etc). | ||||
# We want those to have priority. | ||||
('merge', _mergepredicate, _mergemsg), | ||||
) | ||||
def _getrepostate(repo): | ||||
# experimental config: commands.status.skipstates | ||||
skip = set(repo.ui.configlist('commands', 'status.skipstates')) | ||||
for state, statedetectionpredicate, msgfn in STATES: | ||||
if state in skip: | ||||
continue | ||||
if statedetectionpredicate(repo): | ||||
return (state, statedetectionpredicate, msgfn) | ||||
def morestatus(repo, fm): | ||||
statetuple = _getrepostate(repo) | ||||
label = 'status.morestatus' | ||||
if statetuple: | ||||
fm.startitem() | ||||
state, statedetectionpredicate, helpfulmsg = statetuple | ||||
statemsg = _('The repository is in an unfinished *%s* state.') % state | ||||
fm.write('statemsg', '%s\n', _commentlines(statemsg), label=label) | ||||
conmsg = _conflictsmsg(repo) | ||||
|
r33885 | if conmsg: | ||
fm.write('conflictsmsg', '%s\n', conmsg, label=label) | ||||
|
r33766 | if helpfulmsg: | ||
helpmsg = helpfulmsg() | ||||
fm.write('helpmsg', '%s\n', helpmsg, label=label) | ||||
|
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: | ||||
# short-circuit exact matches, "log" alias beats "^log|history" | ||||
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()] | ||||
if not root.p1().branch() == label and label in repo.branchmap(): | ||||
raise error.Abort(_("a branch of the same name already exists")) | ||||
|
r35763 | if repo.revs('merge() and %ld', revs): | ||
raise error.Abort(_("cannot change branch of a merge commit")) | ||||
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) | ||||
commitphase = ctx.phase() | ||||
overrides = {('phases', 'new-commit'): commitphase} | ||||
with repo.ui.configoverride(overrides, 'branch-change'): | ||||
newnode = repo.commitctx(mc) | ||||
replacements[ctx.node()] = (newnode,) | ||||
ui.debug('new node id is %s\n' % hex(newnode)) | ||||
# create obsmarkers and move bookmarks | ||||
scmutil.cleanupnodes(repo, replacements, 'branch-change') | ||||
# 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" | ||
|
r22248 | elif 1 < len(ctxorbool.parents()): | ||
|
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 | ||||
|
r36225 | def makefilename(ctx, pat, | ||
|
r2874 | total=None, seqno=None, revwidth=None, pathname=None): | ||
|
r36221 | expander = { | ||
|
r36224 | 'H': lambda: ctx.hex(), | ||
'R': lambda: '%d' % ctx.rev(), | ||||
'h': lambda: short(ctx.node()), | ||||
|
r36225 | 'm': lambda: re.sub('[^\w]', '_', | ||
ctx.description().rstrip().splitlines()[0]), | ||||
|
r36224 | 'r': lambda: ('%d' % ctx.rev()).zfill(revwidth or 0), | ||
|
r2874 | '%': lambda: '%', | ||
|
r36224 | 'b': lambda: os.path.basename(ctx.repo().root), | ||
|
r2874 | } | ||
|
r36257 | if total is not None: | ||
expander['N'] = lambda: '%d' % total | ||||
if seqno is not None: | ||||
expander['n'] = lambda: '%d' % seqno | ||||
if total is not None and seqno is not None: | ||||
expander['n'] = (lambda: ('%d' % seqno).zfill(len('%d' % total))) | ||||
if pathname is not None: | ||||
expander['s'] = lambda: os.path.basename(pathname) | ||||
expander['d'] = lambda: os.path.dirname(pathname) or '.' | ||||
expander['p'] = lambda: pathname | ||||
newname = [] | ||||
patlen = len(pat) | ||||
i = 0 | ||||
while i < patlen: | ||||
c = pat[i:i + 1] | ||||
if c == '%': | ||||
i += 1 | ||||
|
r32153 | c = pat[i:i + 1] | ||
|
r36257 | try: | ||
|
r2874 | c = expander[c]() | ||
|
r36257 | except KeyError: | ||
raise error.Abort(_("invalid format spec '%%%s' in output " | ||||
"filename") % c) | ||||
newname.append(c) | ||||
i += 1 | ||||
return ''.join(newname) | ||||
|
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 | ||||
|
r36225 | def makefileobj(ctx, pat, total=None, | ||
|
r19944 | seqno=None, revwidth=None, mode='wb', modemap=None, | ||
|
r18613 | pathname=None): | ||
|
r7319 | |||
|
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) | ||
|
r36225 | fn = makefilename(ctx, pat, total, seqno, revwidth, pathname) | ||
|
r19944 | if modemap is not None: | ||
mode = modemap.get(fn, mode) | ||||
if mode == 'wb': | ||||
modemap[fn] = 'ab' | ||||
|
r18613 | return open(fn, mode) | ||
|
r2882 | |||
|
r14323 | def openrevlog(repo, cmd, file_, opts): | ||
"""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")) | ||
|
r30371 | dirlog = repo.manifestlog._revlog.dirlog(dir) | ||
|
r25119 | if len(dirlog): | ||
r = dirlog | ||||
|
r14323 | elif mf: | ||
|
r30375 | r = repo.manifestlog._revlog | ||
|
r14323 | elif file_: | ||
filelog = repo.file(file_) | ||||
if len(filelog): | ||||
r = filelog | ||||
if not r: | ||||
if not file_: | ||||
raise error.CommandError(cmd, _('invalid arguments')) | ||||
if not os.path.isfile(file_): | ||||
|
r26587 | raise error.Abort(_("revlog '%s' not found") % file_) | ||
|
r31237 | r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False), | ||
|
r14323 | file_[:-2] + ".i") | ||
return r | ||||
|
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 | |||
|
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] | ||
|
r6584 | rel = m.rel(abs) | ||
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))) | ||||
return | ||||
|
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) | ||||
return | ||||
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: | ||||
hint = _('(hg rename %s to replace the file by ' | ||||
'recording a rename)\n') % flags | ||||
else: | ||||
hint = _('(hg copy %s to replace the file by ' | ||||
'recording a copy)\n') % flags | ||||
else: | ||||
msg = _('%s: not overwriting - file exists\n') | ||||
if rename: | ||||
hint = _('(hg rename --after to record the rename)\n') | ||||
else: | ||||
hint = _('(hg copy --after to record the copy)\n') | ||||
ui.warn(msg % reltarget) | ||||
ui.warn(hint) | ||||
|
r5589 | return | ||
|
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)) | ||||
|
r5589 | return | ||
|
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: | ||||
util.copyfile(src, target) | ||||
|
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: | ||
|
r31309 | repo.wvfs.unlinkpath(abssrc) | ||
|
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 | |||
if errors: | ||||
ui.warn(_('(consider using --after)\n')) | ||||
|
r5609 | |||
|
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 | |||
|
r20500 | def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc): | ||
"""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. | ||||
:hunk: a patch (as a binary string) | ||||
: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 | ||
|
r26547 | extractdata = patch.extract(ui, hunk) | ||
tmpname = extractdata.get('filename') | ||||
message = extractdata.get('message') | ||||
|
r27612 | user = opts.get('user') or extractdata.get('user') | ||
date = opts.get('date') or extractdata.get('date') | ||||
|
r26547 | branch = extractdata.get('branch') | ||
nodeid = extractdata.get('nodeid') | ||||
p1 = extractdata.get('p1') | ||||
p2 = extractdata.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) | ||
if not tmpname: | ||||
|
r21553 | return (None, None, False) | ||
|
r20500 | |||
|
r21553 | rejects = False | ||
|
r20500 | try: | ||
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) | ||||
if len(parents) == 1: | ||||
parents.append(repo[nullid]) | ||||
if opts.get('exact'): | ||||
if not nodeid or not p1: | ||||
|
r26587 | raise error.Abort(_('not a Mercurial patch')) | ||
|
r20500 | p1 = repo[p1] | ||
p2 = repo[p2 or nullid] | ||||
elif p2: | ||||
try: | ||||
p1 = repo[p1] | ||||
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 | ||||
|
r22303 | 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")) | ||||
|
r20500 | 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()) | ||||
|
r27613 | if opts.get('exact') or importbranch: | ||
|
r20500 | repo.dirstate.setbranch(branch or 'default') | ||
|
r21553 | partial = opts.get('partial', False) | ||
|
r20500 | files = set() | ||
|
r21553 | try: | ||
|
r24259 | patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix, | ||
files=files, eolmode=None, similarity=sim / 100.0) | ||||
|
r34252 | except error.PatchError as e: | ||
|
r21553 | if not partial: | ||
|
r26587 | raise error.Abort(str(e)) | ||
|
r21553 | if partial: | ||
rejects = True | ||||
|
r20500 | files = list(files) | ||
|
r27613 | if nocommit: | ||
|
r20500 | if message: | ||
msgs.append(message) | ||||
else: | ||||
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 []) | ||||
|
r22250 | editform = mergeeditform(repo[None], 'import.normal') | ||
|
r22278 | if opts.get('exact'): | ||
editor = None | ||||
else: | ||||
|
r35351 | editor = getcommiteditor(editform=editform, | ||
**pycompat.strkwargs(opts)) | ||||
|
r26561 | extra = {} | ||
for idfunc in extrapreimport: | ||||
extrapreimportmap[idfunc](repo, extractdata, extra, opts) | ||||
|
r31457 | overrides = {} | ||
if partial: | ||||
overrides[('ui', 'allowemptycommit')] = True | ||||
with repo.ui.configoverride(overrides, 'import'): | ||||
|
r27612 | n = repo.commit(message, user, | ||
date, match=m, | ||||
|
r26561 | editor=editor, extra=extra) | ||
|
r26562 | for idfunc in extrapostimport: | ||
extrapostimportmap[idfunc](repo[n]) | ||||
|
r20500 | else: | ||
|
r27613 | if opts.get('exact') or importbranch: | ||
|
r20500 | branch = branch or 'default' | ||
else: | ||||
branch = p1.branch() | ||||
store = patch.filestore() | ||||
try: | ||||
files = set() | ||||
try: | ||||
|
r24260 | patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix, | ||
|
r20500 | files, eolmode=None) | ||
|
r34252 | except error.PatchError as e: | ||
|
r26587 | raise error.Abort(str(e)) | ||
|
r22278 | if opts.get('exact'): | ||
editor = None | ||||
else: | ||||
editor = getcommiteditor(editform='import.bypass') | ||||
|
r32765 | memctx = context.memctx(repo, (p1.node(), p2.node()), | ||
|
r20500 | message, | ||
|
r32765 | files=files, | ||
filectxfn=store, | ||||
user=user, | ||||
date=date, | ||||
branch=branch, | ||||
|
r22011 | editor=editor) | ||
|
r20500 | n = memctx.commit() | ||
finally: | ||||
store.close() | ||||
|
r27613 | if opts.get('exact') and nocommit: | ||
|
r22485 | # --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")) | ||||
elif opts.get('exact') and hex(n) != nodeid: | ||||
|
r26587 | raise error.Abort(_('patch is damaged or loses information')) | ||
|
r27611 | msg = _('applied to working directory') | ||
|
r20500 | if n: | ||
# i18n: refers to a short changeset id | ||||
msg = _('created %s') % short(n) | ||||
|
r21553 | return (msg, n, rejects) | ||
|
r20500 | finally: | ||
os.unlink(tmpname) | ||||
|
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 = {} | ||||
|
r32433 | def _exportsingle(repo, ctx, match, switch_parent, rev, seqno, write, 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 | ||||
write("# HG changeset patch\n") | ||||
write("# User %s\n" % ctx.user()) | ||||
write("# Date %d %d\n" % ctx.date()) | ||||
write("# %s\n" % util.datestr(ctx.date())) | ||||
if branch and branch != 'default': | ||||
write("# Branch %s\n" % branch) | ||||
write("# Node ID %s\n" % hex(node)) | ||||
write("# Parent %s\n" % hex(prev)) | ||||
if len(parents) > 1: | ||||
write("# Parent %s\n" % hex(parents[1])) | ||||
for headerid in extraexport: | ||||
header = extraexportmap[headerid](seqno, ctx) | ||||
if header is not None: | ||||
write('# %s\n' % header) | ||||
write(ctx.description().rstrip()) | ||||
write("\n\n") | ||||
for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts): | ||||
write(chunk, label=label) | ||||
|
r32431 | def export(repo, revs, fntemplate='hg-%h.patch', fp=None, 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. | ||||
|
r32431 | fntemplate: An optional string to use for generating patch file names. | ||
|
r32430 | fp: An optional file-like object to which patches should be written. | ||
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: | ||||
fp is specified: All revs are written to the specified | ||||
file-like object. | ||||
|
r32431 | fntemplate specified: Each rev is written to a unique file named using | ||
|
r32430 | the given template. | ||
Neither fp nor template specified: All revs written to repo.ui.write() | ||||
''' | ||||
|
r10611 | |||
total = len(revs) | ||||
|
r32432 | revwidth = max(len(str(rev)) for rev in revs) | ||
|
r18613 | filemode = {} | ||
|
r10611 | |||
|
r32434 | write = None | ||
dest = '<unnamed>' | ||||
if fp: | ||||
dest = getattr(fp, 'name', dest) | ||||
def write(s, **kw): | ||||
fp.write(s) | ||||
elif not fntemplate: | ||||
write = repo.ui.write | ||||
|
r32433 | for seqno, rev in enumerate(revs, 1): | ||
|
r10611 | ctx = repo[rev] | ||
|
r32433 | fo = None | ||
|
r32434 | if not fp and fntemplate: | ||
|
r36225 | fo = makefileobj(ctx, fntemplate, | ||
|
r32433 | total=total, seqno=seqno, revwidth=revwidth, | ||
mode='wb', modemap=filemode) | ||||
dest = fo.name | ||||
def write(s, **kw): | ||||
fo.write(s) | ||||
if not dest.startswith('<'): | ||||
repo.ui.note("%s\n" % dest) | ||||
_exportsingle( | ||||
repo, ctx, match, switch_parent, rev, seqno, write, opts) | ||||
if fo is not None: | ||||
fo.close() | ||||
|
r10611 | |||
|
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) | ||||
|
r35922 | smeta = util.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 | |||
|
r5836 | df = util.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") % | ||
|
r9662 | (rev, util.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) | ||||
def filerevgen(filelog, last): | ||||
""" | ||||
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 = [] | ||||
for j in xrange(0, last + 1): | ||||
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 | ||||
for rev, flparentlinkrevs, copied in filerevgen(filelog, last): | ||||
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 | |||
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 = {} | ||
|
r16108 | change = repo.changectx | ||
|
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. | ||||
|
r24384 | if match.always(): | ||
|
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) | ||||
|
r36364 | 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]) | ||
|
r10282 | for x in 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 = [] | ||||
for i in 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(): | ||||
|
r9654 | for f in ctx.files(): | ||
|
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 | |||
|
r23885 | def add(ui, repo, match, prefix, explicitonly, **opts): | ||
|
r12270 | join = lambda f: os.path.join(prefix, f) | ||
|
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 | |||
|
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: | ||||
|
r23686 | ui.status(_('adding %s\n') % match.rel(f)) | ||
|
r12270 | |||
|
r18364 | for subpath in sorted(wctx.substate): | ||
|
r15410 | sub = wctx.sub(subpath) | ||
try: | ||||
|
r28017 | submatch = matchmod.subdirmatcher(subpath, match) | ||
|
r32147 | if opts.get(r'subrepos'): | ||
|
r23885 | bad.extend(sub.add(ui, submatch, prefix, False, **opts)) | ||
|
r15410 | else: | ||
|
r23885 | bad.extend(sub.add(ui, submatch, prefix, True, **opts)) | ||
|
r15410 | except error.LookupError: | ||
ui.status(_("skipping missing subrepository: %s\n") | ||||
% join(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) | ||||
|
r15912 | def forget(ui, repo, match, prefix, explicitonly): | ||
join = lambda f: os.path.join(prefix, f) | ||||
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) | ||
try: | ||||
|
r28017 | submatch = matchmod.subdirmatcher(subpath, match) | ||
|
r23577 | subbad, subforgot = sub.forget(submatch, prefix) | ||
|
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") | ||||
% join(subpath)) | ||||
|
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') | ||||
|
r23686 | % match.rel(f)) | ||
|
r15912 | bad.append(f) | ||
for f in forget: | ||||
if ui.verbose or not match.exact(f): | ||||
|
r23686 | ui.status(_('removing %s\n') % match.rel(f)) | ||
|
r15912 | |||
rejected = wctx.forget(forget, prefix) | ||||
bad.extend(f for f in rejected if f in match.files()) | ||||
|
r23838 | forgot.extend(f for f in forget if f not in rejected) | ||
|
r15912 | return bad, forgot | ||
|
r24413 | def files(ui, ctx, m, fm, fmt, subrepos): | ||
|
r24275 | rev = ctx.rev() | ||
ret = 1 | ||||
|
r24301 | ds = ctx.repo().dirstate | ||
|
r24275 | |||
for f in ctx.matches(m): | ||||
if rev is None and ds[f] == 'r': | ||||
continue | ||||
fm.startitem() | ||||
if ui.verbose: | ||||
fc = ctx[f] | ||||
fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags()) | ||||
fm.data(abspath=f) | ||||
fm.write('path', fmt, m.rel(f)) | ||||
ret = 0 | ||||
|
r25228 | for subpath in sorted(ctx.substate): | ||
|
r29802 | submatch = matchmod.subdirmatcher(subpath, m) | ||
if (subrepos or m.exact(subpath) or any(submatch.files())): | ||||
|
r24413 | sub = ctx.sub(subpath) | ||
try: | ||||
|
r28387 | recurse = m.exact(subpath) or subrepos | ||
if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0: | ||||
|
r24413 | ret = 0 | ||
except error.LookupError: | ||||
ui.status(_("skipping missing subrepository: %s\n") | ||||
% m.abs(subpath)) | ||||
|
r24275 | return ret | ||
|
r28607 | def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None): | ||
|
r23325 | join = lambda f: os.path.join(prefix, f) | ||
|
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) | ||
total = len(subs) | ||||
count = 0 | ||||
for subpath in subs: | ||||
count += 1 | ||||
|
r29802 | submatch = matchmod.subdirmatcher(subpath, m) | ||
if subrepos or m.exact(subpath) or any(submatch.files()): | ||||
|
r28608 | ui.progress(_('searching'), count, total=total, unit=_('subrepos')) | ||
|
r23325 | sub = wctx.sub(subpath) | ||
try: | ||||
|
r28607 | if sub.removefiles(submatch, prefix, after, force, subrepos, | ||
warnings): | ||||
|
r23325 | ret = 1 | ||
except error.LookupError: | ||||
|
r28607 | warnings.append(_("skipping missing subrepository: %s\n") | ||
|
r23325 | % join(subpath)) | ||
|
r28608 | ui.progress(_('searching'), None) | ||
|
r23325 | |||
|
r23289 | # warn about failure to delete explicit files/dirs | ||
|
r24635 | deleteddirs = util.dirs(deleted) | ||
|
r28608 | files = m.files() | ||
total = len(files) | ||||
count = 0 | ||||
for f in files: | ||||
|
r23325 | def insubrepo(): | ||
for subpath in wctx.substate: | ||||
|
r29622 | if f.startswith(subpath + '/'): | ||
|
r23325 | return True | ||
return False | ||||
|
r28608 | count += 1 | ||
ui.progress(_('deleting'), count, total=total, unit=_('files')) | ||||
|
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') | ||
|
r23686 | % m.rel(f)) | ||
|
r23289 | else: | ||
|
r28607 | warnings.append(_('not removing %s: file is untracked\n') | ||
|
r23686 | % m.rel(f)) | ||
|
r23289 | # missing files will generate a warning elsewhere | ||
ret = 1 | ||||
|
r28608 | ui.progress(_('deleting'), None) | ||
|
r23289 | |||
if force: | ||||
list = modified + deleted + clean + added | ||||
elif after: | ||||
list = deleted | ||||
|
r28608 | remaining = modified + added + clean | ||
total = len(remaining) | ||||
count = 0 | ||||
for f in remaining: | ||||
count += 1 | ||||
ui.progress(_('skipping'), count, total=total, unit=_('files')) | ||||
|
r35120 | if ui.verbose or (f in files): | ||
warnings.append(_('not removing %s: file still exists\n') | ||||
% m.rel(f)) | ||||
|
r23289 | ret = 1 | ||
|
r28608 | ui.progress(_('skipping'), None) | ||
|
r23289 | else: | ||
list = deleted + clean | ||||
|
r28608 | total = len(modified) + len(added) | ||
count = 0 | ||||
|
r23289 | for f in modified: | ||
|
r28608 | count += 1 | ||
ui.progress(_('skipping'), count, total=total, unit=_('files')) | ||||
|
r28607 | warnings.append(_('not removing %s: file is modified (use -f' | ||
|
r23686 | ' to force removal)\n') % m.rel(f)) | ||
|
r23289 | ret = 1 | ||
for f in added: | ||||
|
r28608 | count += 1 | ||
ui.progress(_('skipping'), count, total=total, unit=_('files')) | ||||
|
r29963 | warnings.append(_("not removing %s: file has been marked for add" | ||
" (use 'hg forget' to undo add)\n") % m.rel(f)) | ||||
|
r23289 | ret = 1 | ||
|
r28608 | ui.progress(_('skipping'), None) | ||
list = sorted(list) | ||||
total = len(list) | ||||
count = 0 | ||||
for f in list: | ||||
count += 1 | ||||
|
r23289 | if ui.verbose or not m.exact(f): | ||
|
r28608 | ui.progress(_('deleting'), count, total=total, unit=_('files')) | ||
|
r23686 | ui.status(_('removing %s\n') % m.rel(f)) | ||
|
r28608 | ui.progress(_('deleting'), None) | ||
|
r23289 | |||
|
r27802 | with repo.wlock(): | ||
|
r23289 | if not after: | ||
for f in list: | ||||
if f in added: | ||||
continue # we never unlink added files on remove | ||||
|
r31309 | repo.wvfs.unlinkpath(f, ignoremissing=True) | ||
|
r23289 | repo[None].forget(list) | ||
|
r28607 | if warn: | ||
for warning in warnings: | ||||
ui.warn(warning) | ||||
|
r23289 | return ret | ||
|
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.""" | ||||
data = ctx[path].data() | ||||
if decode: | ||||
data = ctx.repo().wwritedata(path, data) | ||||
fm.startitem() | ||||
fm.write('data', '%s', data) | ||||
fm.data(abspath=path, path=matcher.rel(path)) | ||||
|
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 | ||||
|
r32578 | with formatter.maybereopen(basefm, filename, opts) 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]: | ||||
|
r36157 | scmutil.fileprefetchhooks(repo, ctx, [file]) | ||
|
r30340 | write(file) | ||
return 0 | ||||
except KeyError: | ||||
pass | ||||
|
r21040 | |||
|
r35944 | files = [f for f in ctx.walk(matcher)] | ||
|
r36157 | scmutil.fileprefetchhooks(repo, ctx, files) | ||
|
r35944 | |||
for abs in files: | ||||
|
r21040 | write(abs) | ||
err = 0 | ||||
|
r21041 | |||
for subpath in sorted(ctx.substate): | ||||
sub = ctx.sub(subpath) | ||||
try: | ||||
|
r28017 | submatch = matchmod.subdirmatcher(subpath, matcher) | ||
|
r21041 | |||
|
r32578 | if not sub.cat(submatch, basefm, fntemplate, | ||
|
r35351 | os.path.join(prefix, sub._path), | ||
**pycompat.strkwargs(opts)): | ||||
|
r21041 | err = 0 | ||
except error.RepoLookupError: | ||||
ui.status(_("skipping missing subrepository: %s\n") | ||||
% os.path.join(prefix, subpath)) | ||||
|
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: | ||||
opts['date'] = util.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: | ||
|
r33619 | if scmutil.addremove(repo, matcher, "", opts) != 0: | ||
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() | ||||
date = opts.get('date') or old.date() | ||||
|
r33438 | |||
|
r34123 | # Parse the date to allow comparison between date and old.date() | ||
date = util.parsedate(date) | ||||
|
r33438 | if len(old.parents()) > 1: | ||
# ctx.files() isn't reliable for merges, so fall back to the | ||||
# slower repo.status() method | ||||
files = set([fn for st in repo.status(base, old)[:3] | ||||
for fn in st]) | ||||
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) | ||||
if (opts.get('addremove') | ||||
and scmutil.addremove(repo, matcher, "", opts)): | ||||
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 | |||
|
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, | ||||
copied=copied.get(path)) | ||||
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') | ||
editor = getcommiteditor(editform=editform, | ||||
**pycompat.strkwargs(opts)) | ||||
|
r34087 | |||
|
r33438 | if not message: | ||
editor = getcommiteditor(edit=True, editform=editform) | ||||
message = old.description() | ||||
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() | ||||
and date == old.date() | ||||
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. | ||
return old.node() | ||||
|
r34083 | if opts.get('secret'): | ||
commitphase = 'secret' | ||||
else: | ||||
commitphase = old.phase() | ||||
overrides = {('phases', 'new-commit'): commitphase} | ||||
with ui.configoverride(overrides, 'amend'): | ||||
|
r33438 | 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'): | ||||
obsmetadata = {'note': opts['note']} | ||||
scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata) | ||||
|
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 | ||||
|
r30519 | olddir = pycompat.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: | ||||
repo.ui.status(_('reopening closed branch head %d\n') % r) | ||||
if repo.ui.debugflag: | ||||
repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex())) | ||||
elif repo.ui.verbose: | ||||
repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx)) | ||||
|
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: | ||||
# <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>) | ||||
|
r16304 | names = {} | ||
|
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): | ||
|
r22573 | names[abs] = m.rel(abs), m.exact(abs) | ||
# 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 | ||||
ui.warn("%s: %s\n" % (m.rel(path), msg)) | ||||
|
r25439 | for abs in ctx.walk(matchmod.badmatch(m, badfn)): | ||
|
r22573 | if abs not in names: | ||
names[abs] = m.rel(abs), m.exact(abs) | ||||
# 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: | ||||
names[abs] = m.rel(abs), m.exact(abs) | ||||
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 | ||
|
r16304 | cwd = repo.getcwd() | ||
|
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) | ||
|
r16304 | names[src] = (repo.pathto(src, cwd), True) | ||
|
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 | ) | ||
for abs, (rel, 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])): | ||||
|
r27651 | bakname = scmutil.origpath(ui, repo, rel) | ||
|
r22609 | ui.note(_('saving current version of %s as %s\n') % | ||
(rel, bakname)) | ||||
if not opts.get('dry_run'): | ||||
|
r24475 | if interactive: | ||
util.copyfile(target, bakname) | ||||
else: | ||||
util.rename(target, bakname) | ||||
|
r22233 | if ui.verbose or not exact: | ||
|
r35198 | if not isinstance(msg, bytes): | ||
|
r22233 | msg = msg(abs) | ||
ui.status(msg % rel) | ||||
elif exact: | ||||
|
r22234 | ui.warn(msg % rel) | ||
|
r16304 | break | ||
|
r21575 | |||
|
r16304 | if not opts.get('dry_run'): | ||
|
r23965 | needdata = ('revert', 'add', 'undelete') | ||
|
r35941 | if _revertprefetch is not _revertprefetchstub: | ||
|
r36154 | ui.deprecwarn("'cmdutil._revertprefetch' is deprecated, " | ||
"add a callback to 'scmutil.fileprefetchhooks'", | ||||
'4.6', stacklevel=1) | ||||
|
r35941 | _revertprefetch(repo, ctx, | ||
*[actions[name][0] for name in needdata]) | ||||
oplist = [actions[name][0] for name in needdata] | ||||
|
r36158 | prefetch = scmutil.fileprefetchhooks | ||
prefetch(repo, ctx, [f for sublist in oplist for f in sublist]) | ||||
|
r29498 | _performrevert(repo, parents, ctx, actions, 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 | |||
|
r35941 | def _revertprefetchstub(repo, ctx, *files): | ||
"""Stub method for detecting extension wrapping of _revertprefetch(), to | ||||
issue a deprecation warning.""" | ||||
_revertprefetch = _revertprefetchstub | ||||
|
r29498 | def _performrevert(repo, parents, ctx, actions, 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: | ||||
|
r31309 | repo.wvfs.unlinkpath(f) | ||
|
r30532 | except OSError: | ||
pass | ||||
repo.dirstate.remove(f) | ||||
|
r33722 | audit_path = pathutil.pathauditor(repo.root, cached=True) | ||
|
r22489 | for f in actions['forget'][0]: | ||
|
r27985 | if interactive: | ||
|
r30530 | choice = repo.ui.promptchoice( | ||
|
r30531 | _("forget added file %s (Yn)?$$ &Yes $$ &No") % f) | ||
|
r27985 | if choice == 0: | ||
repo.dirstate.drop(f) | ||||
else: | ||||
|
r36212 | excluded_files.append(f) | ||
|
r27985 | else: | ||
repo.dirstate.drop(f) | ||||
|
r21576 | for f in actions['remove'][0]: | ||
|
r20571 | audit_path(f) | ||
|
r30532 | if interactive: | ||
choice = repo.ui.promptchoice( | ||||
_("remove added file %s (Yn)?$$ &Yes $$ &No") % f) | ||||
if choice == 0: | ||||
doremove(f) | ||||
else: | ||||
|
r36212 | excluded_files.append(f) | ||
|
r30532 | else: | ||
doremove(f) | ||||
|
r22491 | for f in actions['drop'][0]: | ||
audit_path(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) | ||||
|
r25258 | diffopts = patch.difffeatureopts(repo.ui, whitespace=True) | ||
diffopts.nodates = True | ||||
diffopts.git = True | ||||
|
r31196 | operation = 'discard' | ||
reversehunks = True | ||||
if node != parent: | ||||
|
r34969 | operation = 'apply' | ||
reversehunks = False | ||||
|
r25424 | if reversehunks: | ||
diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts) | ||||
else: | ||||
diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts) | ||||
|
r24359 | originalchunks = patch.parsepatch(diff) | ||
|
r25424 | |||
|
r24359 | try: | ||
|
r25424 | |||
|
r29283 | chunks, opts = recordfilter(repo.ui, originalchunks, | ||
operation=operation) | ||||
|
r25424 | if reversehunks: | ||
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() | ||
|
r24359 | for c in chunks: | ||
|
r29498 | # Create a backup file only if this hunk should be backed up | ||
if ishunk(c) and c.header.filename() in tobackup: | ||||
abs = c.header.filename() | ||||
target = repo.wjoin(abs) | ||||
bakname = scmutil.origpath(repo.ui, repo, m.rel(abs)) | ||||
util.copyfile(target, bakname) | ||||
tobackup.remove(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: | ||
|
r26587 | raise error.Abort(str(err)) | ||
|
r24359 | del fp | ||
else: | ||||
for f in actions['revert'][0]: | ||||
|
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: | ||||
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]: | ||
|
r20571 | checkout(f) | ||
normal(f) | ||||
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) | ||||
|
r32343 | class command(registrar.command): | ||
|
r35106 | """deprecated: used registrar.command instead""" | ||
|
r32343 | def _doregister(self, func, name, *args, **kwargs): | ||
func._deprecatedregistrar = True # flag for deprecwarn in extensions.py | ||||
return super(command, self)._doregister(func, name, *args, **kwargs) | ||||
|
r19211 | |||
|
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() | ||||
|
r19474 | # A list of state files kept by multistep operations like graft. | ||
# Since graft cannot be aborted, it is considered 'clearable' by update. | ||||
# note: bisect is intentionally excluded | ||||
|
r19496 | # (state file, clearable, allowcommit, error, hint) | ||
|
r19474 | unfinishedstates = [ | ||
|
r19496 | ('graftstate', True, False, _('graft in progress'), | ||
|
r19482 | _("use 'hg graft --continue' or 'hg update' to abort")), | ||
|
r19496 | ('updatestate', True, False, _('last update was interrupted'), | ||
|
r19482 | _("use 'hg update' to get a consistent checkout")) | ||
|
r19474 | ] | ||
|
r19496 | def checkunfinished(repo, commit=False): | ||
|
r19474 | '''Look for an unfinished multistep operation, like graft, and abort | ||
if found. It's probably good to check this right before | ||||
bailifchanged(). | ||||
''' | ||||
|
r19496 | for f, clearable, allowcommit, msg, hint in unfinishedstates: | ||
if commit and allowcommit: | ||||
continue | ||||
|
r19474 | if repo.vfs.exists(f): | ||
|
r26587 | raise error.Abort(msg, hint=hint) | ||
|
r19474 | |||
def clearunfinished(repo): | ||||
'''Check for unfinished operations (as above), and clear the ones | ||||
that are clearable. | ||||
''' | ||||
|
r19496 | for f, clearable, allowcommit, msg, hint in unfinishedstates: | ||
|
r19474 | if not clearable and repo.vfs.exists(f): | ||
|
r26587 | raise error.Abort(msg, hint=hint) | ||
|
r19496 | for f, clearable, allowcommit, msg, hint in unfinishedstates: | ||
|
r19474 | if clearable and repo.vfs.exists(f): | ||
|
r31320 | util.unlink(repo.vfs.join(f)) | ||
|
r24991 | |||
|
r27624 | afterresolvedstates = [ | ||
|
r27625 | ('graftstate', | ||
_('hg graft --continue')), | ||||
|
r27624 | ] | ||
|
r28120 | def howtocontinue(repo): | ||
'''Check for an unfinished operation and return the command to finish | ||||
it. | ||||
|
r30332 | afterresolvedstates tuples define a .hg/{file} and the corresponding | ||
|
r28120 | command needed to finish it. | ||
Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is | ||||
a boolean. | ||||
''' | ||||
contmsg = _("continue: %s") | ||||
|
r27624 | for f, msg in afterresolvedstates: | ||
if repo.vfs.exists(f): | ||||
|
r28120 | return contmsg % msg, 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 | ||||
If there's a matching afterresolvedstates, howtocontinue will yield | ||||
repo.ui.warn as the reporter. | ||||
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) | ||||
|
r35928 | |||
class changeset_printer(logcmdutil.changesetprinter): | ||||
def __init__(self, ui, *args, **kwargs): | ||||
msg = ("'cmdutil.changeset_printer' is deprecated, " | ||||
"use 'logcmdutil.logcmdutil'") | ||||
ui.deprecwarn(msg, "4.6") | ||||
super(changeset_printer, self).__init__(ui, *args, **kwargs) | ||||
def displaygraph(ui, *args, **kwargs): | ||||
msg = ("'cmdutil.displaygraph' is deprecated, " | ||||
"use 'logcmdutil.displaygraph'") | ||||
ui.deprecwarn(msg, "4.6") | ||||
return logcmdutil.displaygraph(ui, *args, **kwargs) | ||||
def show_changeset(ui, *args, **kwargs): | ||||
msg = ("'cmdutil.show_changeset' is deprecated, " | ||||
"use 'logcmdutil.changesetdisplayer'") | ||||
ui.deprecwarn(msg, "4.6") | ||||
return logcmdutil.changesetdisplayer(ui, *args, **kwargs) | ||||