reposetup.py
469 lines
| 18.3 KiB
| text/x-python
|
PythonLexer
|
r15168 | # Copyright 2009-2010 Gregory P. Ward | ||
# Copyright 2009-2010 Intelerad Medical Systems Incorporated | ||||
# Copyright 2010-2011 Fog Creek Software | ||||
# Copyright 2010-2011 Unity Technologies | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
'''setup for largefiles repositories: reposetup''' | ||||
|
r29314 | |||
|
r15168 | import copy | ||
from mercurial.i18n import _ | ||||
|
r29314 | from mercurial import ( | ||
error, | ||||
|
r43986 | extensions, | ||
|
r29314 | localrepo, | ||
|
r29319 | match as matchmod, | ||
|
r29314 | scmutil, | ||
|
r43984 | util, | ||
|
r29314 | ) | ||
r49205 | from mercurial.dirstateutils import timestamp | |||
|
r29314 | from . import ( | ||
lfcommands, | ||||
lfutil, | ||||
) | ||||
|
r15168 | |||
|
r43346 | |||
|
r15168 | def reposetup(ui, repo): | ||
|
r20858 | # wire repositories should be given new wireproto functions | ||
# by "proto.wirereposetup()" via "hg.wirepeersetupfuncs" | ||||
|
r15168 | if not repo.local(): | ||
|
r20858 | return | ||
|
r15168 | |||
|
r16247 | class lfilesrepo(repo.__class__): | ||
|
r24158 | # the mark to examine whether "repo" object enables largefiles or not | ||
_largefilesenabled = True | ||||
|
r15168 | lfstatus = False | ||
|
r43346 | |||
|
r15252 | # When lfstatus is set, return a context that gives the names | ||
# of largefiles instead of their corresponding standins and | ||||
# identifies the largefiles as always binary, regardless of | ||||
# their actual contents. | ||||
|
r15168 | def __getitem__(self, changeid): | ||
|
r16247 | ctx = super(lfilesrepo, self).__getitem__(changeid) | ||
|
r23958 | if self.lfstatus: | ||
|
r43346 | |||
|
r43986 | def files(orig): | ||
filenames = orig() | ||||
return [lfutil.splitstandin(f) or f for f in filenames] | ||||
|
r43346 | |||
|
r43986 | extensions.wrapfunction(ctx, 'files', files) | ||
def manifest(orig): | ||||
man1 = orig() | ||||
|
r43346 | |||
|
r43986 | class lfilesmanifest(man1.__class__): | ||
def __contains__(self, filename): | ||||
orig = super(lfilesmanifest, self).__contains__ | ||||
return orig(filename) or orig( | ||||
lfutil.standin(filename) | ||||
) | ||||
|
r43346 | |||
|
r43986 | man1.__class__ = lfilesmanifest | ||
return man1 | ||||
|
r43346 | |||
|
r43986 | extensions.wrapfunction(ctx, 'manifest', manifest) | ||
|
r43346 | |||
|
r43986 | def filectx(orig, path, fileid=None, filelog=None): | ||
try: | ||||
if filelog is not None: | ||||
result = orig(path, fileid, filelog) | ||||
else: | ||||
result = orig(path, fileid) | ||||
except error.LookupError: | ||||
# Adding a null character will cause Mercurial to | ||||
# identify this as a binary file. | ||||
if filelog is not None: | ||||
result = orig(lfutil.standin(path), fileid, filelog) | ||||
else: | ||||
result = orig(lfutil.standin(path), fileid) | ||||
olddata = result.data | ||||
result.data = lambda: olddata() + b'\0' | ||||
return result | ||||
extensions.wrapfunction(ctx, 'filectx', filectx) | ||||
|
r15168 | return ctx | ||
# Figure out the status of big files and insert them into the | ||||
|
r15252 | # appropriate list in the result. Also removes standin files | ||
# from the listing. Revert to the original status if | ||||
# self.lfstatus is False. | ||||
|
r23958 | # XXX large file status is buggy when used on repo proxy. | ||
# XXX this needs to be investigated. | ||||
@localrepo.unfilteredmethod | ||||
|
r43346 | def status( | ||
self, | ||||
|
r43347 | node1=b'.', | ||
|
r43346 | node2=None, | ||
match=None, | ||||
ignored=False, | ||||
clean=False, | ||||
unknown=False, | ||||
listsubrepos=False, | ||||
): | ||||
|
r15168 | listignored, listclean, listunknown = ignored, clean, unknown | ||
|
r22518 | orig = super(lfilesrepo, self).status | ||
|
r23958 | if not self.lfstatus: | ||
|
r43346 | return orig( | ||
node1, | ||||
node2, | ||||
match, | ||||
listignored, | ||||
listclean, | ||||
listunknown, | ||||
listsubrepos, | ||||
) | ||||
|
r22515 | |||
# some calls in this function rely on the old version of status | ||||
|
r23958 | self.lfstatus = False | ||
|
r22515 | ctx1 = self[node1] | ||
ctx2 = self[node2] | ||||
working = ctx2.rev() is None | ||||
|
r43347 | parentworking = working and ctx1 == self[b'.'] | ||
|
r15168 | |||
|
r22515 | if match is None: | ||
|
r41825 | match = matchmod.always() | ||
|
r15168 | |||
|
r22515 | try: | ||
|
r43984 | # updating the dirstate is optional | ||
# so we don't wait on the lock | ||||
wlock = self.wlock(False) | ||||
gotlock = True | ||||
except error.LockError: | ||||
wlock = util.nullcontextmanager() | ||||
gotlock = False | ||||
r51031 | with wlock, self.dirstate.running_status(self): | |||
|
r15653 | |||
|
r23146 | # First check if paths or patterns were specified on the | ||
# command line. If there were, and they don't match any | ||||
|
r22515 | # largefiles, we should just bail here and let super | ||
# handle it -- thus gaining a big performance boost. | ||||
lfdirstate = lfutil.openlfdirstate(ui, self) | ||||
|
r23146 | if not match.always(): | ||
|
r22515 | for f in lfdirstate: | ||
if match(f): | ||||
break | ||||
else: | ||||
|
r43346 | return orig( | ||
node1, | ||||
node2, | ||||
match, | ||||
listignored, | ||||
listclean, | ||||
listunknown, | ||||
listsubrepos, | ||||
) | ||||
|
r15168 | |||
|
r22515 | # Create a copy of match that matches standins instead | ||
# of largefiles. | ||||
def tostandins(files): | ||||
if not working: | ||||
return files | ||||
newfiles = [] | ||||
dirstate = self.dirstate | ||||
for f in files: | ||||
sf = lfutil.standin(f) | ||||
if sf in dirstate: | ||||
newfiles.append(sf) | ||||
|
r35083 | elif dirstate.hasdir(sf): | ||
|
r22515 | # Directory entries could be regular or | ||
# standin, check both | ||||
newfiles.extend((f, sf)) | ||||
else: | ||||
newfiles.append(f) | ||||
return newfiles | ||||
|
r18149 | |||
|
r22515 | m = copy.copy(match) | ||
m._files = tostandins(m._files) | ||||
|
r43346 | result = orig( | ||
node1, node2, m, ignored, clean, unknown, listsubrepos | ||||
) | ||||
|
r22515 | if working: | ||
|
r15617 | |||
|
r22515 | def sfindirstate(f): | ||
sf = lfutil.standin(f) | ||||
dirstate = self.dirstate | ||||
|
r35083 | return sf in dirstate or dirstate.hasdir(sf) | ||
|
r18149 | |||
|
r43346 | match._files = [f for f in match._files if sfindirstate(f)] | ||
|
r22515 | # Don't waste time getting the ignored and unknown | ||
# files from lfdirstate | ||||
r49213 | unsure, s, mtime_boundary = lfdirstate.status( | |||
|
r43346 | match, | ||
subrepos=[], | ||||
ignored=False, | ||||
clean=listclean, | ||||
unknown=False, | ||||
) | ||||
|
r30191 | (modified, added, removed, deleted, clean) = ( | ||
|
r43346 | s.modified, | ||
s.added, | ||||
s.removed, | ||||
s.deleted, | ||||
s.clean, | ||||
) | ||||
|
r22515 | if parentworking: | ||
r49205 | wctx = repo[None] | |||
|
r22515 | for lfile in unsure: | ||
standin = lfutil.standin(lfile) | ||||
if standin not in ctx1: | ||||
# from second parent | ||||
modified.append(lfile) | ||||
|
r43346 | elif lfutil.readasstandin( | ||
ctx1[standin] | ||||
) != lfutil.hashfile(self.wjoin(lfile)): | ||||
|
r22515 | modified.append(lfile) | ||
else: | ||||
|
r22523 | if listclean: | ||
clean.append(lfile) | ||||
r49205 | s = wctx[lfile].lstat() | |||
mode = s.st_mode | ||||
size = s.st_size | ||||
r49225 | mtime = timestamp.reliable_mtime_of( | |||
s, mtime_boundary | ||||
) | ||||
if mtime is not None: | ||||
cache_data = (mode, size, mtime) | ||||
lfdirstate.set_clean(lfile, cache_data) | ||||
|
r22515 | else: | ||
tocheck = unsure + modified + added + clean | ||||
modified, added, clean = [], [], [] | ||||
|
r23383 | checkexec = self.dirstate._checkexec | ||
|
r22515 | |||
for lfile in tocheck: | ||||
standin = lfutil.standin(lfile) | ||||
|
r23043 | if standin in ctx1: | ||
|
r23090 | abslfile = self.wjoin(lfile) | ||
|
r43346 | if ( | ||
lfutil.readasstandin(ctx1[standin]) | ||||
!= lfutil.hashfile(abslfile) | ||||
) or ( | ||||
checkexec | ||||
|
r43347 | and (b'x' in ctx1.flags(standin)) | ||
|
r43346 | != bool(lfutil.getexecutable(abslfile)) | ||
): | ||||
|
r15168 | modified.append(lfile) | ||
|
r22523 | elif listclean: | ||
|
r15168 | clean.append(lfile) | ||
|
r22515 | else: | ||
added.append(lfile) | ||||
|
r15168 | |||
|
r23089 | # at this point, 'removed' contains largefiles | ||
# marked as 'R' in the working context. | ||||
# then, largefiles not managed also in the target | ||||
# context should be excluded from 'removed'. | ||||
|
r43346 | removed = [ | ||
lfile | ||||
for lfile in removed | ||||
if lfutil.standin(lfile) in ctx1 | ||||
] | ||||
|
r23089 | |||
|
r30191 | # Standins no longer found in lfdirstate have been deleted | ||
|
r22525 | for standin in ctx1.walk(lfutil.getstandinmatcher(self)): | ||
|
r22515 | lfile = lfutil.splitstandin(standin) | ||
if not match(lfile): | ||||
continue | ||||
if lfile not in lfdirstate: | ||||
|
r30191 | deleted.append(lfile) | ||
# Sync "largefile has been removed" back to the | ||||
# standin. Removing a file as a side effect of | ||||
# running status is gross, but the alternatives (if | ||||
# any) are worse. | ||||
|
r30233 | self.wvfs.unlinkpath(standin, ignoremissing=True) | ||
|
r15663 | |||
|
r22515 | # Filter result lists | ||
result = list(result) | ||||
|
r19056 | |||
|
r22515 | # Largefiles are not really removed when they're | ||
# still in the normal dirstate. Likewise, normal | ||||
# files are not really removed if they are still in | ||||
# lfdirstate. This happens in merges where files | ||||
# change type. | ||||
|
r43346 | removed = [f for f in removed if f not in self.dirstate] | ||
result[2] = [f for f in result[2] if f not in lfdirstate] | ||||
|
r15663 | |||
|
r43196 | lfiles = set(lfdirstate) | ||
|
r22515 | # Unknown files | ||
result[4] = set(result[4]).difference(lfiles) | ||||
# Ignored files | ||||
result[5] = set(result[5]).difference(lfiles) | ||||
# combine normal files and largefiles | ||||
|
r43346 | normals = [ | ||
[fn for fn in filelist if not lfutil.isstandin(fn)] | ||||
for filelist in result | ||||
] | ||||
lfstatus = ( | ||||
modified, | ||||
added, | ||||
removed, | ||||
deleted, | ||||
[], | ||||
[], | ||||
clean, | ||||
) | ||||
result = [ | ||||
sorted(list1 + list2) | ||||
for (list1, list2) in zip(normals, lfstatus) | ||||
] | ||||
else: # not against working directory | ||||
result = [ | ||||
[lfutil.splitstandin(f) or f for f in items] | ||||
for items in result | ||||
] | ||||
|
r15663 | |||
|
r43984 | if gotlock: | ||
|
r48982 | lfdirstate.write(self.currenttransaction()) | ||
r51031 | else: | |||
lfdirstate.invalidate() | ||||
|
r22515 | |||
|
r23958 | self.lfstatus = True | ||
|
r22914 | return scmutil.status(*result) | ||
|
r15168 | |||
|
r23184 | def commitctx(self, ctx, *args, **kwargs): | ||
node = super(lfilesrepo, self).commitctx(ctx, *args, **kwargs) | ||||
|
r43346 | |||
|
r23184 | class lfilesctx(ctx.__class__): | ||
def markcommitted(self, node): | ||||
orig = super(lfilesctx, self).markcommitted | ||||
return lfutil.markcommitted(orig, self, node) | ||||
|
r43346 | |||
|
r23184 | ctx.__class__ = lfilesctx | ||
|
r15168 | return node | ||
|
r15254 | # Before commit, largefile standins have not had their | ||
# contents updated to reflect the hash of their largefile. | ||||
# Do that here. | ||||
|
r43346 | def commit( | ||
self, | ||||
|
r43347 | text=b"", | ||
|
r43346 | user=None, | ||
date=None, | ||||
match=None, | ||||
force=False, | ||||
editor=False, | ||||
extra=None, | ||||
): | ||||
|
r31410 | if extra is None: | ||
extra = {} | ||||
|
r16247 | orig = super(lfilesrepo, self).commit | ||
|
r15168 | |||
|
r27842 | with self.wlock(): | ||
|
r23186 | lfcommithook = self._lfcommithooks[-1] | ||
match = lfcommithook(self, match) | ||||
|
r43346 | result = orig( | ||
text=text, | ||||
user=user, | ||||
date=date, | ||||
match=match, | ||||
force=force, | ||||
editor=editor, | ||||
extra=extra, | ||||
) | ||||
|
r15794 | return result | ||
|
r15168 | |||
|
r23185 | # TODO: _subdirlfs should be moved into "lfutil.py", because | ||
# it is referred only from "lfutil.updatestandinsbymatch" | ||||
|
r18064 | def _subdirlfs(self, files, lfiles): | ||
|
r46554 | """ | ||
|
r18064 | Adjust matched file list | ||
|
r23543 | If we pass a directory to commit whose only committable files | ||
|
r18064 | are largefiles, the core commit code aborts before finding | ||
the largefiles. | ||||
So we do the following: | ||||
For directories that only have largefiles as matches, | ||||
|
r18644 | we explicitly add the largefiles to the match list and remove | ||
|
r18064 | the directory. | ||
In other cases, we leave the match list unmodified. | ||||
|
r46554 | """ | ||
|
r18064 | actualfiles = [] | ||
dirs = [] | ||||
regulars = [] | ||||
for f in files: | ||||
|
r43347 | if lfutil.isstandin(f + b'/'): | ||
|
r26587 | raise error.Abort( | ||
|
r43347 | _(b'file "%s" is a largefile standin') % f, | ||
hint=b'commit the largefile itself instead', | ||||
|
r43346 | ) | ||
|
r18064 | # Scan directories | ||
|
r28716 | if self.wvfs.isdir(f): | ||
|
r18064 | dirs.append(f) | ||
else: | ||||
regulars.append(f) | ||||
for f in dirs: | ||||
matcheddir = False | ||||
|
r43347 | d = self.dirstate.normalize(f) + b'/' | ||
|
r18064 | # Check for matched normal files | ||
for mf in regulars: | ||||
if self.dirstate.normalize(mf).startswith(d): | ||||
actualfiles.append(f) | ||||
matcheddir = True | ||||
break | ||||
if not matcheddir: | ||||
# If no normal match, manually append | ||||
# any matching largefiles | ||||
for lf in lfiles: | ||||
if self.dirstate.normalize(lf).startswith(d): | ||||
actualfiles.append(lf) | ||||
if not matcheddir: | ||||
|
r23923 | # There may still be normal files in the dir, so | ||
|
r24180 | # add a directory to the list, which | ||
|
r24007 | # forces status/dirstate to walk all files and | ||
# call the match function on the matcher, even | ||||
|
r24180 | # on case sensitive filesystems. | ||
|
r43347 | actualfiles.append(b'.') | ||
|
r18064 | matcheddir = True | ||
# Nothing in dir, so readd it | ||||
# and let commit reject it | ||||
if not matcheddir: | ||||
actualfiles.append(f) | ||||
# Always add normal files | ||||
actualfiles += regulars | ||||
return actualfiles | ||||
|
r16247 | repo.__class__ = lfilesrepo | ||
|
r15168 | |||
|
r23186 | # stack of hooks being executed before committing. | ||
# only last element ("_lfcommithooks[-1]") is used for each committing. | ||||
repo._lfcommithooks = [lfutil.updatestandinsbymatch] | ||||
|
r23188 | # Stack of status writer functions taking "*msg, **opts" arguments | ||
|
r23543 | # like "ui.status()". Only last element ("_lfstatuswriters[-1]") | ||
|
r23188 | # is used to write status out. | ||
repo._lfstatuswriters = [ui.status] | ||||
|
r28876 | def prepushoutgoinghook(pushop): | ||
|
r28878 | """Push largefiles for pushop before pushing revisions.""" | ||
lfrevs = pushop.lfrevs | ||||
if lfrevs is None: | ||||
lfrevs = pushop.outgoing.missing | ||||
if lfrevs: | ||||
|
r21044 | toupload = set() | ||
addfunc = lambda fn, lfhash: toupload.add(lfhash) | ||||
|
r43346 | lfutil.getlfilestoupload(pushop.repo, lfrevs, addfunc) | ||
|
r28876 | lfcommands.uploadlfiles(ui, pushop.repo, pushop.remote, toupload) | ||
|
r43346 | |||
|
r43347 | repo.prepushoutgoinghooks.add(b"largefiles", prepushoutgoinghook) | ||
|
r21044 | |||
|
r15168 | def checkrequireslfiles(ui, repo, **kwargs): | ||
r49510 | with repo.lock(): | |||
if b'largefiles' not in repo.requirements and any( | ||||
lfutil.shortname + b'/' in f[1] for f in repo.store.datafiles() | ||||
): | ||||
repo.requirements.add(b'largefiles') | ||||
scmutil.writereporequirements(repo) | ||||
|
r15168 | |||
|
r43346 | ui.setconfig( | ||
|
r43347 | b'hooks', b'changegroup.lfiles', checkrequireslfiles, b'largefiles' | ||
|
r43346 | ) | ||
|
r43347 | ui.setconfig(b'hooks', b'commit.lfiles', checkrequireslfiles, b'largefiles') | ||