lfutil.py
673 lines
| 23.7 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. | ||||
'''largefiles utility code: must not import other modules in this package.''' | ||||
|
r29309 | from __future__ import absolute_import | ||
|
r15168 | |||
|
r29309 | import copy | ||
|
r29341 | import hashlib | ||
|
r15168 | import os | ||
import stat | ||||
|
r29309 | |||
from mercurial.i18n import _ | ||||
|
r15168 | |||
|
r29309 | from mercurial import ( | ||
dirstate, | ||||
|
r30820 | encoding, | ||
|
r29309 | error, | ||
httpconnection, | ||||
|
r29320 | match as matchmod, | ||
|
r29309 | node, | ||
|
r30640 | pycompat, | ||
|
r29309 | scmutil, | ||
|
r33373 | sparse, | ||
|
r29309 | util, | ||
|
r31247 | vfs as vfsmod, | ||
|
r29309 | ) | ||
|
r15168 | |||
shortname = '.hglf' | ||||
|
r18151 | shortnameslash = shortname + '/' | ||
|
r15168 | longname = 'largefiles' | ||
# -- Private worker functions ------------------------------------------ | ||||
|
r15227 | def getminsize(ui, assumelfiles, opt, default=10): | ||
lfsize = opt | ||||
if not lfsize and assumelfiles: | ||||
|
r15304 | lfsize = ui.config(longname, 'minsize', default=default) | ||
|
r15227 | if lfsize: | ||
try: | ||||
|
r15228 | lfsize = float(lfsize) | ||
|
r15227 | except ValueError: | ||
|
r26587 | raise error.Abort(_('largefiles: size must be number (not %s)\n') | ||
|
r15227 | % lfsize) | ||
if lfsize is None: | ||||
|
r26587 | raise error.Abort(_('minimum size for largefiles must be specified')) | ||
|
r15227 | return lfsize | ||
|
r15168 | def link(src, dest): | ||
|
r28576 | """Try to create hardlink - if that fails, efficiently make a copy.""" | ||
|
r18998 | util.makedirs(os.path.dirname(dest)) | ||
|
r15168 | try: | ||
|
r15206 | util.oslink(src, dest) | ||
|
r15168 | except OSError: | ||
|
r15572 | # if hardlinks fail, fallback on atomic copy | ||
|
r33438 | with open(src, 'rb') as srcf, util.atomictempfile(dest) as dstf: | ||
for chunk in util.filechunkiter(srcf): | ||||
dstf.write(chunk) | ||||
|
r15168 | os.chmod(dest, os.stat(src).st_mode) | ||
|
r15316 | def usercachepath(ui, hash): | ||
|
r28574 | '''Return the correct location in the "global" largefiles cache for a file | ||
with the given hash. | ||||
This cache is used for sharing of largefiles across repositories - both | ||||
to preserve download bandwidth and storage space.''' | ||||
|
r28575 | return os.path.join(_usercachedir(ui), hash) | ||
|
r28574 | |||
|
r35280 | def _usercachedir(ui, name=longname): | ||
|
r28574 | '''Return the location of the "global" largefiles cache.''' | ||
|
r35280 | path = ui.configpath(name, 'usercache') | ||
|
r15168 | if path: | ||
|
r28574 | return path | ||
|
r34646 | if pycompat.iswindows: | ||
|
r30820 | appdata = encoding.environ.get('LOCALAPPDATA',\ | ||
encoding.environ.get('APPDATA')) | ||||
|
r28574 | if appdata: | ||
|
r35280 | return os.path.join(appdata, name) | ||
|
r34648 | elif pycompat.isdarwin: | ||
|
r30820 | home = encoding.environ.get('HOME') | ||
|
r28574 | if home: | ||
|
r35280 | return os.path.join(home, 'Library', 'Caches', name) | ||
|
r34647 | elif pycompat.isposix: | ||
|
r30820 | path = encoding.environ.get('XDG_CACHE_HOME') | ||
|
r28574 | if path: | ||
|
r35280 | return os.path.join(path, name) | ||
|
r30820 | home = encoding.environ.get('HOME') | ||
|
r28574 | if home: | ||
|
r35280 | return os.path.join(home, '.cache', name) | ||
|
r15168 | else: | ||
|
r30640 | raise error.Abort(_('unknown operating system: %s\n') | ||
% pycompat.osname) | ||||
|
r35280 | raise error.Abort(_('unknown %s usercache location') % name) | ||
|
r15168 | |||
|
r15316 | def inusercache(ui, hash): | ||
|
r15658 | path = usercachepath(ui, hash) | ||
|
r28575 | return os.path.exists(path) | ||
|
r15168 | |||
def findfile(repo, hash): | ||||
|
r28576 | '''Return store path of the largefile with the specified hash. | ||
As a side effect, the file might be linked from user cache. | ||||
Return None if the file can't be found locally.''' | ||||
|
r24631 | path, exists = findstorepath(repo, hash) | ||
if exists: | ||||
|
r16928 | repo.ui.note(_('found %s in store\n') % hash) | ||
|
r24631 | return path | ||
|
r15317 | elif inusercache(repo.ui, hash): | ||
|
r16928 | repo.ui.note(_('found %s in system cache\n') % hash) | ||
|
r15408 | path = storepath(repo, hash) | ||
link(usercachepath(repo.ui, hash), path) | ||||
|
r15913 | return path | ||
return None | ||||
|
r15168 | |||
|
r16247 | class largefilesdirstate(dirstate.dirstate): | ||
|
r15168 | def __getitem__(self, key): | ||
|
r16247 | return super(largefilesdirstate, self).__getitem__(unixpath(key)) | ||
|
r15168 | def normal(self, f): | ||
|
r16247 | return super(largefilesdirstate, self).normal(unixpath(f)) | ||
|
r15168 | def remove(self, f): | ||
|
r16247 | return super(largefilesdirstate, self).remove(unixpath(f)) | ||
|
r15168 | def add(self, f): | ||
|
r16247 | return super(largefilesdirstate, self).add(unixpath(f)) | ||
|
r15168 | def drop(self, f): | ||
|
r16247 | return super(largefilesdirstate, self).drop(unixpath(f)) | ||
|
r15168 | def forget(self, f): | ||
|
r16247 | return super(largefilesdirstate, self).forget(unixpath(f)) | ||
|
r15793 | def normallookup(self, f): | ||
|
r16247 | return super(largefilesdirstate, self).normallookup(unixpath(f)) | ||
|
r21085 | def _ignore(self, f): | ||
|
r18148 | return False | ||
|
r26749 | def write(self, tr=False): | ||
# (1) disable PENDING mode always | ||||
# (lfdirstate isn't yet managed as a part of the transaction) | ||||
# (2) avoid develwarn 'use dirstate.write with ....' | ||||
super(largefilesdirstate, self).write(None) | ||||
|
r15168 | |||
|
r17659 | def openlfdirstate(ui, repo, create=True): | ||
|
r15168 | ''' | ||
|
r15252 | Return a dirstate object that tracks largefiles: i.e. its root is | ||
the repo root, but it is saved in .hg/largefiles/dirstate. | ||||
|
r15168 | ''' | ||
|
r28560 | vfs = repo.vfs | ||
lfstoredir = longname | ||||
|
r31247 | opener = vfsmod.vfs(vfs.join(lfstoredir)) | ||
|
r16247 | lfdirstate = largefilesdirstate(opener, ui, repo.root, | ||
|
r33373 | repo.dirstate._validate, | ||
lambda: sparse.matcher(repo)) | ||||
|
r15168 | |||
|
r15252 | # If the largefiles dirstate does not exist, populate and create | ||
# it. This ensures that we create it on the first meaningful | ||||
|
r15794 | # largefiles operation in a new clone. | ||
|
r28560 | if create and not vfs.exists(vfs.join(lfstoredir, 'dirstate')): | ||
|
r15168 | matcher = getstandinmatcher(repo) | ||
|
r34344 | standins = repo.dirstate.walk(matcher, subrepos=[], unknown=False, | ||
ignored=False) | ||||
|
r21917 | |||
if len(standins) > 0: | ||||
|
r28560 | vfs.makedirs(lfstoredir) | ||
|
r21917 | |||
for standin in standins: | ||||
|
r15168 | lfile = splitstandin(standin) | ||
lfdirstate.normallookup(lfile) | ||||
return lfdirstate | ||||
|
r23039 | def lfdirstatestatus(lfdirstate, repo): | ||
|
r31657 | pctx = repo['.'] | ||
|
r29320 | match = matchmod.always(repo.root, repo.getcwd()) | ||
|
r34345 | unsure, s = lfdirstate.status(match, subrepos=[], ignored=False, | ||
clean=False, unknown=False) | ||||
|
r22919 | modified, clean = s.modified, s.clean | ||
|
r15794 | for lfile in unsure: | ||
|
r18299 | try: | ||
|
r31657 | fctx = pctx[standin(lfile)] | ||
|
r18299 | except LookupError: | ||
fctx = None | ||||
|
r31740 | if not fctx or readasstandin(fctx) != hashfile(repo.wjoin(lfile)): | ||
|
r15794 | modified.append(lfile) | ||
else: | ||||
clean.append(lfile) | ||||
lfdirstate.normal(lfile) | ||||
|
r22912 | return s | ||
|
r15168 | |||
def listlfiles(repo, rev=None, matcher=None): | ||||
|
r15252 | '''return a list of largefiles in the working copy or the | ||
specified changeset''' | ||||
|
r15168 | |||
if matcher is None: | ||||
matcher = getstandinmatcher(repo) | ||||
# ignore unknown files in working directory | ||||
|
r15255 | return [splitstandin(f) | ||
for f in repo[rev].walk(matcher) | ||||
|
r15168 | if rev is not None or repo.dirstate[f] != '?'] | ||
|
r24631 | def instore(repo, hash, forcelocal=False): | ||
|
r29419 | '''Return true if a largefile with the given hash exists in the store''' | ||
|
r24631 | return os.path.exists(storepath(repo, hash, forcelocal)) | ||
|
r15168 | |||
|
r24631 | def storepath(repo, hash, forcelocal=False): | ||
|
r29419 | '''Return the correct location in the repository largefiles store for a | ||
|
r28576 | file with the given hash.''' | ||
|
r24631 | if not forcelocal and repo.shared(): | ||
return repo.vfs.reljoin(repo.sharedpath, longname, hash) | ||||
|
r31332 | return repo.vfs.join(longname, hash) | ||
|
r15168 | |||
|
r24629 | def findstorepath(repo, hash): | ||
'''Search through the local store path(s) to find the file for the given | ||||
hash. If the file is not found, its path in the primary store is returned. | ||||
The return value is a tuple of (path, exists(path)). | ||||
''' | ||||
|
r24631 | # For shared repos, the primary store is in the share source. But for | ||
# backward compatibility, force a lookup in the local store if it wasn't | ||||
# found in the share source. | ||||
path = storepath(repo, hash, False) | ||||
if instore(repo, hash): | ||||
return (path, True) | ||||
elif repo.shared() and instore(repo, hash, True): | ||||
|
r29329 | return storepath(repo, hash, True), True | ||
|
r24631 | |||
return (path, False) | ||||
|
r24629 | |||
|
r15168 | def copyfromcache(repo, hash, filename): | ||
|
r15252 | '''Copy the specified largefile from the repo or system cache to | ||
filename in the repository. Return true on success or false if the | ||||
file was not found in either cache (which should not happened: | ||||
this is meant to be called only after ensuring that the needed | ||||
largefile exists in the cache).''' | ||||
|
r28560 | wvfs = repo.wvfs | ||
|
r15168 | path = findfile(repo, hash) | ||
if path is None: | ||||
return False | ||||
|
r28560 | wvfs.makedirs(wvfs.dirname(wvfs.join(filename))) | ||
|
r15570 | # The write may fail before the file is fully written, but we | ||
# don't use atomic writes in the working copy. | ||||
|
r33438 | with open(path, 'rb') as srcfd, wvfs(filename, 'wb') as destfd: | ||
gothash = copyandhash( | ||||
util.filechunkiter(srcfd), destfd) | ||||
|
r26823 | if gothash != hash: | ||
repo.ui.warn(_('%s: data corruption in %s with hash %s\n') | ||||
% (filename, path, gothash)) | ||||
|
r28560 | wvfs.unlink(filename) | ||
|
r26823 | return False | ||
|
r15168 | return True | ||
|
r31738 | def copytostore(repo, ctx, file, fstandin): | ||
|
r28560 | wvfs = repo.wvfs | ||
|
r31738 | hash = readasstandin(ctx[fstandin]) | ||
|
r15316 | if instore(repo, hash): | ||
|
r15168 | return | ||
|
r28560 | if wvfs.exists(file): | ||
copytostoreabsolute(repo, wvfs.join(file), hash) | ||||
|
r27903 | else: | ||
repo.ui.warn(_("%s: largefile %s not available from local store\n") % | ||||
(file, hash)) | ||||
|
r15168 | |||
|
r15796 | def copyalltostore(repo, node): | ||
'''Copy all largefiles in a given revision to the store''' | ||||
ctx = repo[node] | ||||
for filename in ctx.files(): | ||||
|
r31613 | realfile = splitstandin(filename) | ||
if realfile is not None and filename in ctx.manifest(): | ||||
|
r31736 | copytostore(repo, ctx, realfile, filename) | ||
|
r15796 | |||
|
r15316 | def copytostoreabsolute(repo, file, hash): | ||
if inusercache(repo.ui, hash): | ||||
link(usercachepath(repo.ui, hash), storepath(repo, hash)) | ||||
|
r23276 | else: | ||
|
r18998 | util.makedirs(os.path.dirname(storepath(repo, hash))) | ||
|
r30142 | with open(file, 'rb') as srcf: | ||
with util.atomictempfile(storepath(repo, hash), | ||||
createmode=repo.store.createmode) as dstf: | ||||
for chunk in util.filechunkiter(srcf): | ||||
dstf.write(chunk) | ||||
|
r15316 | linktousercache(repo, hash) | ||
|
r15168 | |||
|
r15316 | def linktousercache(repo, hash): | ||
|
r28576 | '''Link / copy the largefile with the specified hash from the store | ||
to the cache.''' | ||||
|
r15658 | path = usercachepath(repo.ui, hash) | ||
|
r28575 | link(storepath(repo, hash), path) | ||
|
r15168 | |||
|
r25292 | def getstandinmatcher(repo, rmatcher=None): | ||
'''Return a match object that applies rmatcher to the standin directory''' | ||||
|
r28560 | wvfs = repo.wvfs | ||
standindir = shortname | ||||
|
r25470 | |||
# no warnings about missing files or directories | ||||
badfn = lambda f, msg: None | ||||
|
r25293 | if rmatcher and not rmatcher.always(): | ||
|
r28560 | pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()] | ||
|
r26025 | if not pats: | ||
|
r28560 | pats = [wvfs.join(standindir)] | ||
|
r25470 | match = scmutil.match(repo[None], pats, badfn=badfn) | ||
|
r18724 | else: | ||
|
r15168 | # no patterns: relative to repo root | ||
|
r28560 | match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn) | ||
|
r15168 | return match | ||
def composestandinmatcher(repo, rmatcher): | ||||
|
r15252 | '''Return a matcher that accepts standins corresponding to the | ||
files accepted by rmatcher. Pass the list of files in the matcher | ||||
as the paths specified by the user.''' | ||||
|
r25292 | smatcher = getstandinmatcher(repo, rmatcher) | ||
|
r15168 | isstandin = smatcher.matchfn | ||
|
r16247 | def composedmatchfn(f): | ||
|
r15168 | return isstandin(f) and rmatcher.matchfn(splitstandin(f)) | ||
|
r16247 | smatcher.matchfn = composedmatchfn | ||
|
r15168 | |||
return smatcher | ||||
def standin(filename): | ||||
'''Return the repo-relative path to the standin for the specified big | ||||
file.''' | ||||
# Notes: | ||||
|
r17425 | # 1) Some callers want an absolute path, but for instance addlargefiles | ||
|
r18154 | # needs it repo-relative so it can be passed to repo[None].add(). So | ||
# leave it up to the caller to use repo.wjoin() to get an absolute path. | ||||
|
r15168 | # 2) Join with '/' because that's what dirstate always uses, even on | ||
# Windows. Change existing separator to '/' first in case we are | ||||
# passed filenames from an external source (like the command line). | ||||
|
r18151 | return shortnameslash + util.pconvert(filename) | ||
|
r15168 | |||
def isstandin(filename): | ||||
|
r15252 | '''Return true if filename is a big file standin. filename must be | ||
in Mercurial's internal form (slash-separated).''' | ||||
|
r18151 | return filename.startswith(shortnameslash) | ||
|
r15168 | |||
def splitstandin(filename): | ||||
# Split on / because that's what dirstate always uses, even on Windows. | ||||
# Change local separator to / first just in case we are passed filenames | ||||
# from an external source (like the command line). | ||||
|
r16066 | bits = util.pconvert(filename).split('/', 1) | ||
|
r15168 | if len(bits) == 2 and bits[0] == shortname: | ||
return bits[1] | ||||
else: | ||||
return None | ||||
|
r31659 | def updatestandin(repo, lfile, standin): | ||
"""Re-calculate hash value of lfile and write it into standin | ||||
This assumes that "lfutil.standin(lfile) == standin", for efficiency. | ||||
""" | ||||
|
r31615 | file = repo.wjoin(lfile) | ||
if repo.wvfs.exists(lfile): | ||||
|
r15168 | hash = hashfile(file) | ||
executable = getexecutable(file) | ||||
writestandin(repo, standin, hash, executable) | ||||
|
r27947 | else: | ||
|
r31615 | raise error.Abort(_('%s: file not found!') % lfile) | ||
|
r15168 | |||
|
r31734 | def readasstandin(fctx): | ||
'''read hex hash from given filectx of standin file | ||||
This encapsulates how "standin" data is stored into storage layer.''' | ||||
return fctx.data().strip() | ||||
|
r15168 | def writestandin(repo, standin, hash, executable): | ||
|
r15252 | '''write hash to <repo.root>/<standin>''' | ||
|
r19089 | repo.wwrite(standin, hash + '\n', executable and 'x' or '') | ||
|
r15168 | |||
def copyandhash(instream, outfile): | ||||
'''Read bytes from instream (iterable) and write them to outfile, | ||||
|
r19002 | computing the SHA-1 hash of the data along the way. Return the hash.''' | ||
|
r29341 | hasher = hashlib.sha1('') | ||
|
r15168 | for data in instream: | ||
hasher.update(data) | ||||
outfile.write(data) | ||||
|
r18999 | return hasher.hexdigest() | ||
|
r15168 | |||
def hashfile(file): | ||||
if not os.path.exists(file): | ||||
return '' | ||||
|
r30142 | with open(file, 'rb') as fd: | ||
|
r31652 | return hexsha1(fd) | ||
|
r15168 | |||
def getexecutable(filename): | ||||
mode = os.stat(filename).st_mode | ||||
|
r15255 | return ((mode & stat.S_IXUSR) and | ||
(mode & stat.S_IXGRP) and | ||||
(mode & stat.S_IXOTH)) | ||||
|
r15168 | |||
def urljoin(first, second, *arg): | ||||
def join(left, right): | ||||
if not left.endswith('/'): | ||||
left += '/' | ||||
if right.startswith('/'): | ||||
right = right[1:] | ||||
return left + right | ||||
url = join(first, second) | ||||
for a in arg: | ||||
url = join(url, a) | ||||
return url | ||||
|
r31652 | def hexsha1(fileobj): | ||
|
r15168 | """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like | ||
object data""" | ||||
|
r29341 | h = hashlib.sha1() | ||
|
r31652 | for chunk in util.filechunkiter(fileobj): | ||
|
r15168 | h.update(chunk) | ||
return h.hexdigest() | ||||
def httpsendfile(ui, filename): | ||||
|
r15224 | return httpconnection.httpsendfile(ui, filename, 'rb') | ||
|
r15168 | |||
def unixpath(path): | ||||
|
r15252 | '''Return a version of path normalized for use with the lfdirstate.''' | ||
|
r16066 | return util.pconvert(os.path.normpath(path)) | ||
|
r15168 | |||
def islfilesrepo(repo): | ||||
|
r28576 | '''Return true if the repo is a largefile repo.''' | ||
|
r17659 | if ('largefiles' in repo.requirements and | ||
|
r25149 | any(shortnameslash in f[0] for f in repo.store.datafiles())): | ||
|
r17659 | return True | ||
|
r25149 | return any(openlfdirstate(repo.ui, repo, False)) | ||
|
r15168 | |||
|
r15333 | class storeprotonotcapable(Exception): | ||
|
r15168 | def __init__(self, storetypes): | ||
self.storetypes = storetypes | ||||
|
r16103 | |||
|
r16120 | def getstandinsstate(repo): | ||
standins = [] | ||||
matcher = getstandinmatcher(repo) | ||||
|
r31735 | wctx = repo[None] | ||
|
r34344 | for standin in repo.dirstate.walk(matcher, subrepos=[], unknown=False, | ||
ignored=False): | ||||
|
r16120 | lfile = splitstandin(standin) | ||
|
r18300 | try: | ||
|
r31735 | hash = readasstandin(wctx[standin]) | ||
|
r18300 | except IOError: | ||
hash = None | ||||
standins.append((lfile, hash)) | ||||
|
r16120 | return standins | ||
|
r16245 | |||
|
r22095 | def synclfdirstate(repo, lfdirstate, lfile, normallookup): | ||
lfstandin = standin(lfile) | ||||
if lfstandin in repo.dirstate: | ||||
stat = repo.dirstate._map[lfstandin] | ||||
state, mtime = stat[0], stat[3] | ||||
else: | ||||
state, mtime = '?', -1 | ||||
if state == 'n': | ||||
|
r26627 | if (normallookup or mtime < 0 or | ||
|
r28560 | not repo.wvfs.exists(lfile)): | ||
|
r22095 | # state 'n' doesn't ensure 'clean' in this case | ||
lfdirstate.normallookup(lfile) | ||||
else: | ||||
lfdirstate.normal(lfile) | ||||
elif state == 'm': | ||||
lfdirstate.normallookup(lfile) | ||||
elif state == 'r': | ||||
lfdirstate.remove(lfile) | ||||
elif state == 'a': | ||||
lfdirstate.add(lfile) | ||||
elif state == '?': | ||||
lfdirstate.drop(lfile) | ||||
|
r23184 | def markcommitted(orig, ctx, node): | ||
|
r24336 | repo = ctx.repo() | ||
|
r23184 | |||
orig(node) | ||||
|
r23273 | # ATTENTION: "ctx.files()" may differ from "repo[node].files()" | ||
# because files coming from the 2nd parent are omitted in the latter. | ||||
# | ||||
# The former should be used to get targets of "synclfdirstate", | ||||
# because such files: | ||||
# - are marked as "a" by "patch.patch()" (e.g. via transplant), and | ||||
# - have to be marked as "n" after commit, but | ||||
# - aren't listed in "repo[node].files()" | ||||
|
r23184 | lfdirstate = openlfdirstate(repo.ui, repo) | ||
for f in ctx.files(): | ||||
|
r31613 | lfile = splitstandin(f) | ||
if lfile is not None: | ||||
|
r23184 | synclfdirstate(repo, lfdirstate, lfile, False) | ||
lfdirstate.write() | ||||
|
r23276 | # As part of committing, copy all of the largefiles into the cache. | ||
|
r31616 | # | ||
# Using "node" instead of "ctx" implies additional "repo[node]" | ||||
# lookup while copyalltostore(), but can omit redundant check for | ||||
# files comming from the 2nd parent, which should exist in store | ||||
# at merging. | ||||
|
r23276 | copyalltostore(repo, node) | ||
|
r16245 | def getlfilestoupdate(oldstandins, newstandins): | ||
changedstandins = set(oldstandins).symmetric_difference(set(newstandins)) | ||||
filelist = [] | ||||
for f in changedstandins: | ||||
if f[0] not in filelist: | ||||
filelist.append(f[0]) | ||||
return filelist | ||||
|
r21042 | |||
def getlfilestoupload(repo, missing, addfunc): | ||||
|
r23892 | for i, n in enumerate(missing): | ||
repo.ui.progress(_('finding outgoing largefiles'), i, | ||||
r28464 | unit=_('revisions'), total=len(missing)) | |||
|
r28877 | parents = [p for p in repo[n].parents() if p != node.nullid] | ||
|
r23657 | |||
oldlfstatus = repo.lfstatus | ||||
repo.lfstatus = False | ||||
try: | ||||
ctx = repo[n] | ||||
finally: | ||||
repo.lfstatus = oldlfstatus | ||||
|
r21042 | files = set(ctx.files()) | ||
if len(parents) == 2: | ||||
mc = ctx.manifest() | ||||
mp1 = ctx.parents()[0].manifest() | ||||
mp2 = ctx.parents()[1].manifest() | ||||
for f in mp1: | ||||
if f not in mc: | ||||
files.add(f) | ||||
for f in mp2: | ||||
if f not in mc: | ||||
files.add(f) | ||||
for f in mc: | ||||
if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None): | ||||
files.add(f) | ||||
for fn in files: | ||||
if isstandin(fn) and fn in ctx: | ||||
|
r31740 | addfunc(fn, readasstandin(ctx[fn])) | ||
|
r23892 | repo.ui.progress(_('finding outgoing largefiles'), None) | ||
|
r23185 | |||
def updatestandinsbymatch(repo, match): | ||||
'''Update standins in the working directory according to specified match | ||||
This returns (possibly modified) ``match`` object to be used for | ||||
subsequent commit process. | ||||
''' | ||||
ui = repo.ui | ||||
# Case 1: user calls commit with no specific files or | ||||
# include/exclude patterns: refresh and commit all files that | ||||
# are "dirty". | ||||
if match is None or match.always(): | ||||
# Spend a bit of time here to get a list of files we know | ||||
# are modified so we can compare only against those. | ||||
# It can cost a lot of time (several seconds) | ||||
# otherwise to update all standins if the largefiles are | ||||
# large. | ||||
lfdirstate = openlfdirstate(ui, repo) | ||||
|
r29320 | dirtymatch = matchmod.always(repo.root, repo.getcwd()) | ||
|
r34345 | unsure, s = lfdirstate.status(dirtymatch, subrepos=[], ignored=False, | ||
clean=False, unknown=False) | ||||
|
r23185 | modifiedfiles = unsure + s.modified + s.added + s.removed | ||
lfiles = listlfiles(repo) | ||||
# this only loops through largefiles that exist (not | ||||
# removed/renamed) | ||||
for lfile in lfiles: | ||||
if lfile in modifiedfiles: | ||||
|
r31618 | fstandin = standin(lfile) | ||
if repo.wvfs.exists(fstandin): | ||||
|
r23185 | # this handles the case where a rebase is being | ||
# performed and the working copy is not updated | ||||
# yet. | ||||
|
r28560 | if repo.wvfs.exists(lfile): | ||
|
r31659 | updatestandin(repo, lfile, fstandin) | ||
|
r23185 | |||
return match | ||||
lfiles = listlfiles(repo) | ||||
match._files = repo._subdirlfs(match.files(), lfiles) | ||||
# Case 2: user calls commit with specified patterns: refresh | ||||
# any matching big files. | ||||
smatcher = composestandinmatcher(repo, match) | ||||
|
r34344 | standins = repo.dirstate.walk(smatcher, subrepos=[], unknown=False, | ||
ignored=False) | ||||
|
r23185 | |||
# No matching big files: get out of the way and pass control to | ||||
# the usual commit() method. | ||||
if not standins: | ||||
return match | ||||
# Refresh all matching big files. It's possible that the | ||||
# commit will end up failing, in which case the big files will | ||||
# stay refreshed. No harm done: the user modified them and | ||||
# asked to commit them, so sooner or later we're going to | ||||
# refresh the standins. Might as well leave them refreshed. | ||||
lfdirstate = openlfdirstate(ui, repo) | ||||
for fstandin in standins: | ||||
lfile = splitstandin(fstandin) | ||||
if lfdirstate[lfile] != 'r': | ||||
|
r31659 | updatestandin(repo, lfile, fstandin) | ||
|
r23185 | |||
# Cook up a new matcher that only matches regular files or | ||||
# standins corresponding to the big files requested by the | ||||
# user. Have to modify _files to prevent commit() from | ||||
# complaining "not tracked" for big files. | ||||
match = copy.copy(match) | ||||
origmatchfn = match.matchfn | ||||
# Check both the list of largefiles and the list of | ||||
# standins because if a largefile was removed, it | ||||
# won't be in the list of largefiles at this point | ||||
match._files += sorted(standins) | ||||
actualfiles = [] | ||||
for f in match._files: | ||||
fstandin = standin(f) | ||||
|
r26817 | # For largefiles, only one of the normal and standin should be | ||
|
r27942 | # committed (except if one of them is a remove). In the case of a | ||
# standin removal, drop the normal file if it is unknown to dirstate. | ||||
|
r26817 | # Thus, skip plain largefile names but keep the standin. | ||
|
r27942 | if f in lfiles or fstandin in standins: | ||
if repo.dirstate[fstandin] != 'r': | ||||
if repo.dirstate[f] != 'r': | ||||
continue | ||||
elif repo.dirstate[f] == '?': | ||||
continue | ||||
|
r23185 | |||
actualfiles.append(f) | ||||
match._files = actualfiles | ||||
def matchfn(f): | ||||
if origmatchfn(f): | ||||
return f not in lfiles | ||||
else: | ||||
return f in standins | ||||
match.matchfn = matchfn | ||||
return match | ||||
|
r23187 | |||
class automatedcommithook(object): | ||||
|
r23543 | '''Stateful hook to update standins at the 1st commit of resuming | ||
|
r23187 | |||
For efficiency, updating standins in the working directory should | ||||
be avoided while automated committing (like rebase, transplant and | ||||
so on), because they should be updated before committing. | ||||
But the 1st commit of resuming automated committing (e.g. ``rebase | ||||
--continue``) should update them, because largefiles may be | ||||
modified manually. | ||||
''' | ||||
def __init__(self, resuming): | ||||
self.resuming = resuming | ||||
def __call__(self, repo, match): | ||||
if self.resuming: | ||||
self.resuming = False # avoids updating at subsequent commits | ||||
return updatestandinsbymatch(repo, match) | ||||
else: | ||||
return match | ||||
|
r23188 | |||
def getstatuswriter(ui, repo, forcibly=None): | ||||
'''Return the function to write largefiles specific status out | ||||
If ``forcibly`` is ``None``, this returns the last element of | ||||
|
r23543 | ``repo._lfstatuswriters`` as "default" writer function. | ||
|
r23188 | |||
Otherwise, this returns the function to always write out (or | ||||
ignore if ``not forcibly``) status. | ||||
''' | ||||
|
r24158 | if forcibly is None and util.safehasattr(repo, '_largefilesenabled'): | ||
|
r23188 | return repo._lfstatuswriters[-1] | ||
else: | ||||
if forcibly: | ||||
return ui.status # forcibly WRITE OUT | ||||
else: | ||||
return lambda *msg, **opts: None # forcibly IGNORE | ||||