hgext/largefiles/overrides.py
author Matt Mackall <mpm@selenic.com>
Thu, 29 Sep 2011 16:48:48 -0500
changeset 15169 aa262fff87ac
parent 15168 cfccd3bee7b3
child 15170 c1a4a3220711
permissions -rw-r--r--
largefile: fix up hasattr usage

# 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.

'''Overridden Mercurial commands and functions for the largefiles extension'''

import os
import copy

from mercurial import hg, commands, util, cmdutil, match as match_, node, \
        archival, error, merge
from mercurial.i18n import _
from mercurial.node import hex
from hgext import rebase

try:
    from mercurial import scmutil
except ImportError:
    pass

import lfutil
import lfcommands

def installnormalfilesmatchfn(manifest):
    '''overrides scmutil.match so that the matcher it returns will ignore all
    largefiles'''
    oldmatch = None # for the closure
    def override_match(repo, pats=[], opts={}, globbed=False,
            default='relpath'):
        match = oldmatch(repo, pats, opts, globbed, default)
        m = copy.copy(match)
        notlfile = lambda f: not (lfutil.isstandin(f) or lfutil.standin(f) in
                manifest)
        m._files = filter(notlfile, m._files)
        m._fmap = set(m._files)
        orig_matchfn = m.matchfn
        m.matchfn = lambda f: notlfile(f) and orig_matchfn(f) or None
        return m
    oldmatch = installmatchfn(override_match)

def installmatchfn(f):
    try:
        # Mercurial >= 1.9
        oldmatch = scmutil.match
    except ImportError:
        # Mercurial <= 1.8
        oldmatch = cmdutil.match
    setattr(f, 'oldmatch', oldmatch)
    try:
        # Mercurial >= 1.9
        scmutil.match = f
    except ImportError:
        # Mercurial <= 1.8
        cmdutil.match = f
    return oldmatch

def restorematchfn():
    '''restores scmutil.match to what it was before installnormalfilesmatchfn
    was called.  no-op if scmutil.match is its original function.

    Note that n calls to installnormalfilesmatchfn will require n calls to
    restore matchfn to reverse'''
    try:
        # Mercurial >= 1.9
        scmutil.match = getattr(scmutil.match, 'oldmatch', scmutil.match)
    except ImportError:
        # Mercurial <= 1.8
        cmdutil.match = getattr(cmdutil.match, 'oldmatch', cmdutil.match)

# -- Wrappers: modify existing commands --------------------------------

# Add works by going through the files that the user wanted to add
# and checking if they should be added as lfiles. Then making a new
# matcher which matches only the normal files and running the original
# version of add.
def override_add(orig, ui, repo, *pats, **opts):
    large = opts.pop('large', None)

    lfsize = opts.pop('lfsize', None)
    if not lfsize and lfutil.islfilesrepo(repo):
        lfsize = ui.config(lfutil.longname, 'size', default='10')
    if lfsize:
        try:
            lfsize = int(lfsize)
        except ValueError:
            raise util.Abort(_('largefiles: size must be an integer, was %s\n') % lfsize)

    lfmatcher = None
    if os.path.exists(repo.wjoin(lfutil.shortname)):
        lfpats = ui.config(lfutil.longname, 'patterns', default=())
        if lfpats:
            lfpats = lfpats.split(' ')
            lfmatcher = match_.match(repo.root, '', list(lfpats))

    lfnames = []
    try:
        # Mercurial >= 1.9
        m = scmutil.match(repo[None], pats, opts)
    except ImportError:
        # Mercurial <= 1.8
        m = cmdutil.match(repo, pats, opts)
    m.bad = lambda x, y: None
    wctx = repo[None]
    for f in repo.walk(m):
        exact = m.exact(f)
        lfile = lfutil.standin(f) in wctx
        nfile = f in wctx
        exists = lfile or nfile

        # Don't warn the user when they attempt to add a normal tracked file.
        # The normal add code will do that for us.
        if exact and exists:
            if lfile:
                ui.warn(_('%s already a largefile\n') % f)
            continue

        if exact or not exists:
            if large or (lfsize and os.path.getsize(repo.wjoin(f)) >= \
                    lfsize * 1024 * 1024) or (lfmatcher and lfmatcher(f)):
                lfnames.append(f)
                if ui.verbose or not exact:
                    ui.status(_('adding %s as a largefile\n') % m.rel(f))

    bad = []
    standins = []

    # Need to lock otherwise there could be a race condition inbetween when
    # standins are created and added to the repo
    wlock = repo.wlock()
    try:
        if not opts.get('dry_run'):
            lfdirstate = lfutil.openlfdirstate(ui, repo)
            for f in lfnames:
                standinname = lfutil.standin(f)
                lfutil.writestandin(repo, standinname, hash='',
                    executable=lfutil.getexecutable(repo.wjoin(f)))
                standins.append(standinname)
                if lfdirstate[f] == 'r':
                    lfdirstate.normallookup(f)
                else:
                    lfdirstate.add(f)
            lfdirstate.write()
            bad += [lfutil.splitstandin(f) for f in lfutil.repo_add(repo,
                standins) if f in m.files()]
    finally:
        wlock.release()

    installnormalfilesmatchfn(repo[None].manifest())
    result = orig(ui, repo, *pats, **opts)
    restorematchfn()

    return (result == 1 or bad) and 1 or 0

def override_remove(orig, ui, repo, *pats, **opts):
    manifest = repo[None].manifest()
    installnormalfilesmatchfn(manifest)
    orig(ui, repo, *pats, **opts)
    restorematchfn()

    after, force = opts.get('after'), opts.get('force')
    if not pats and not after:
        raise util.Abort(_('no files specified'))
    try:
        # Mercurial >= 1.9
        m = scmutil.match(repo[None], pats, opts)
    except ImportError:
        # Mercurial <= 1.8
        m = cmdutil.match(repo, pats, opts)
    try:
        repo.lfstatus = True
        s = repo.status(match=m, clean=True)
    finally:
        repo.lfstatus = False
    modified, added, deleted, clean = [[f for f in list if lfutil.standin(f) \
        in manifest] for list in [s[0], s[1], s[3], s[6]]]

    def warn(files, reason):
        for f in files:
            ui.warn(_('not removing %s: file %s (use -f to force removal)\n')
                    % (m.rel(f), reason))

    if force:
        remove, forget = modified + deleted + clean, added
    elif after:
        remove, forget = deleted, []
        warn(modified + added + clean, _('still exists'))
    else:
        remove, forget = deleted + clean, []
        warn(modified, _('is modified'))
        warn(added, _('has been marked for add'))

    for f in sorted(remove + forget):
        if ui.verbose or not m.exact(f):
            ui.status(_('removing %s\n') % m.rel(f))

    # Need to lock because standin files are deleted then removed from the
    # repository and we could race inbetween.
    wlock = repo.wlock()
    try:
        lfdirstate = lfutil.openlfdirstate(ui, repo)
        for f in remove:
            if not after:
                os.unlink(repo.wjoin(f))
                currentdir = os.path.split(f)[0]
                while currentdir and not os.listdir(repo.wjoin(currentdir)):
                    os.rmdir(repo.wjoin(currentdir))
                    currentdir = os.path.split(currentdir)[0]
            lfdirstate.remove(f)
        lfdirstate.write()

        forget = [lfutil.standin(f) for f in forget]
        remove = [lfutil.standin(f) for f in remove]
        lfutil.repo_forget(repo, forget)
        lfutil.repo_remove(repo, remove, unlink=True)
    finally:
        wlock.release()

def override_status(orig, ui, repo, *pats, **opts):
    try:
        repo.lfstatus = True
        return orig(ui, repo, *pats, **opts)
    finally:
        repo.lfstatus = False

def override_log(orig, ui, repo, *pats, **opts):
    try:
        repo.lfstatus = True
        orig(ui, repo, *pats, **opts)
    finally:
        repo.lfstatus = False

def override_verify(orig, ui, repo, *pats, **opts):
    large = opts.pop('large', False)
    all = opts.pop('lfa', False)
    contents = opts.pop('lfc', False)

    result = orig(ui, repo, *pats, **opts)
    if large:
        result = result or lfcommands.verifylfiles(ui, repo, all, contents)
    return result

# Override needs to refresh standins so that update's normal merge
# will go through properly. Then the other update hook (overriding repo.update)
# will get the new files. Filemerge is also overriden so that the merge
# will merge standins correctly.
def override_update(orig, ui, repo, *pats, **opts):
    lfdirstate = lfutil.openlfdirstate(ui, repo)
    s = lfdirstate.status(match_.always(repo.root, repo.getcwd()), [], False,
        False, False)
    (unsure, modified, added, removed, missing, unknown, ignored, clean) = s

    # Need to lock between the standins getting updated and their lfiles
    # getting updated
    wlock = repo.wlock()
    try:
        if opts['check']:
            mod = len(modified) > 0
            for lfile in unsure:
                standin = lfutil.standin(lfile)
                if repo['.'][standin].data().strip() != \
                        lfutil.hashfile(repo.wjoin(lfile)):
                    mod = True
                else:
                    lfdirstate.normal(lfile)
            lfdirstate.write()
            if mod:
                raise util.Abort(_('uncommitted local changes'))
        # XXX handle removed differently
        if not opts['clean']:
            for lfile in unsure + modified + added:
                lfutil.updatestandin(repo, lfutil.standin(lfile))
    finally:
        wlock.release()
    return orig(ui, repo, *pats, **opts)

# Override filemerge to prompt the user about how they wish to merge lfiles.
# This will handle identical edits, and copy/rename + edit without prompting
# the user.
def override_filemerge(origfn, repo, mynode, orig, fcd, fco, fca):
    # Use better variable names here. Because this is a wrapper we cannot
    # change the variable names in the function declaration.
    fcdest, fcother, fcancestor = fcd, fco, fca
    if not lfutil.isstandin(orig):
        return origfn(repo, mynode, orig, fcdest, fcother, fcancestor)
    else:
        if not fcother.cmp(fcdest): # files identical?
            return None

        # backwards, use working dir parent as ancestor
        if fcancestor == fcother:
            fcancestor = fcdest.parents()[0]

        if orig != fcother.path():
            repo.ui.status(_('merging %s and %s to %s\n')
                           % (lfutil.splitstandin(orig),
                              lfutil.splitstandin(fcother.path()),
                              lfutil.splitstandin(fcdest.path())))
        else:
            repo.ui.status(_('merging %s\n')
                           % lfutil.splitstandin(fcdest.path()))

        if fcancestor.path() != fcother.path() and fcother.data() == \
                fcancestor.data():
            return 0
        if fcancestor.path() != fcdest.path() and fcdest.data() == \
                fcancestor.data():
            repo.wwrite(fcdest.path(), fcother.data(), fcother.flags())
            return 0

        if repo.ui.promptchoice(_('largefile %s has a merge conflict\n'
                             'keep (l)ocal or take (o)ther?') %
                             lfutil.splitstandin(orig),
                             (_('&Local'), _('&Other')), 0) == 0:
            return 0
        else:
            repo.wwrite(fcdest.path(), fcother.data(), fcother.flags())
            return 0

# Copy first changes the matchers to match standins instead of lfiles.
# Then it overrides util.copyfile in that function it checks if the destination
# lfile already exists. It also keeps a list of copied files so that the lfiles
# can be copied and the dirstate updated.
def override_copy(orig, ui, repo, pats, opts, rename=False):
    # doesn't remove lfile on rename
    if len(pats) < 2:
        # this isn't legal, let the original function deal with it
        return orig(ui, repo, pats, opts, rename)

    def makestandin(relpath):
        try:
            # Mercurial >= 1.9
            path = scmutil.canonpath(repo.root, repo.getcwd(), relpath)
        except ImportError:
            # Mercurial <= 1.8
            path = util.canonpath(repo.root, repo.getcwd(), relpath)
        return os.path.join(os.path.relpath('.', repo.getcwd()),
            lfutil.standin(path))

    try:
        # Mercurial >= 1.9
        fullpats = scmutil.expandpats(pats)
    except ImportError:
        # Mercurial <= 1.8
        fullpats = cmdutil.expandpats(pats)
    dest = fullpats[-1]

    if os.path.isdir(dest):
        if not os.path.isdir(makestandin(dest)):
            os.makedirs(makestandin(dest))
    # This could copy both lfiles and normal files in one command, but we don't
    # want to do that first replace their matcher to only match normal files
    # and run it then replace it to just match lfiles and run it again
    nonormalfiles = False
    nolfiles = False
    try:
        installnormalfilesmatchfn(repo[None].manifest())
        result = orig(ui, repo, pats, opts, rename)
    except util.Abort, e:
        if str(e) != 'no files to copy':
            raise e
        else:
            nonormalfiles = True
        result = 0
    finally:
        restorematchfn()

    # The first rename can cause our current working directory to be removed.
    # In that case there is nothing left to copy/rename so just quit.
    try:
        repo.getcwd()
    except OSError:
        return result

    try:
        # When we call orig below it creates the standins but we don't add them
        # to the dir state until later so lock during that time.
        wlock = repo.wlock()

        manifest = repo[None].manifest()
        oldmatch = None # for the closure
        def override_match(repo, pats=[], opts={}, globbed=False,
                default='relpath'):
            newpats = []
            # The patterns were previously mangled to add the standin
            # directory; we need to remove that now
            for pat in pats:
                if match_.patkind(pat) is None and lfutil.shortname in pat:
                    newpats.append(pat.replace(lfutil.shortname, ''))
                else:
                    newpats.append(pat)
            match = oldmatch(repo, newpats, opts, globbed, default)
            m = copy.copy(match)
            lfile = lambda f: lfutil.standin(f) in manifest
            m._files = [lfutil.standin(f) for f in m._files if lfile(f)]
            m._fmap = set(m._files)
            orig_matchfn = m.matchfn
            m.matchfn = lambda f: lfutil.isstandin(f) and \
                lfile(lfutil.splitstandin(f)) and \
                orig_matchfn(lfutil.splitstandin(f)) or None
            return m
        oldmatch = installmatchfn(override_match)
        listpats = []
        for pat in pats:
            if match_.patkind(pat) is not None:
                listpats.append(pat)
            else:
                listpats.append(makestandin(pat))

        try:
            origcopyfile = util.copyfile
            copiedfiles = []
            def override_copyfile(src, dest):
                if lfutil.shortname in src and lfutil.shortname in dest:
                    destlfile = dest.replace(lfutil.shortname, '')
                    if not opts['force'] and os.path.exists(destlfile):
                        raise IOError('',
                            _('destination largefile already exists'))
                copiedfiles.append((src, dest))
                origcopyfile(src, dest)

            util.copyfile = override_copyfile
            result += orig(ui, repo, listpats, opts, rename)
        finally:
            util.copyfile = origcopyfile

        lfdirstate = lfutil.openlfdirstate(ui, repo)
        for (src, dest) in copiedfiles:
            if lfutil.shortname in src and lfutil.shortname in dest:
                srclfile = src.replace(lfutil.shortname, '')
                destlfile = dest.replace(lfutil.shortname, '')
                destlfiledir = os.path.dirname(destlfile) or '.'
                if not os.path.isdir(destlfiledir):
                    os.makedirs(destlfiledir)
                if rename:
                    os.rename(srclfile, destlfile)
                    lfdirstate.remove(os.path.relpath(srclfile,
                        repo.root))
                else:
                    util.copyfile(srclfile, destlfile)
                lfdirstate.add(os.path.relpath(destlfile,
                    repo.root))
        lfdirstate.write()
    except util.Abort, e:
        if str(e) != 'no files to copy':
            raise e
        else:
            nolfiles = True
    finally:
        restorematchfn()
        wlock.release()

    if nolfiles and nonormalfiles:
        raise util.Abort(_('no files to copy'))

    return result

# When the user calls revert, we have to be careful to not revert any changes
# to other lfiles accidentally.  This means we have to keep track of the lfiles
# that are being reverted so we only pull down the necessary lfiles.
#
# Standins are only updated (to match the hash of lfiles) before commits.
# Update the standins then run the original revert (changing the matcher to hit
# standins instead of lfiles). Based on the resulting standins update the
# lfiles. Then return the standins to their proper state
def override_revert(orig, ui, repo, *pats, **opts):
    # Because we put the standins in a bad state (by updating them) and then
    # return them to a correct state we need to lock to prevent others from
    # changing them in their incorrect state.
    wlock = repo.wlock()
    try:
        lfdirstate = lfutil.openlfdirstate(ui, repo)
        (modified, added, removed, missing, unknown, ignored, clean) = \
            lfutil.lfdirstate_status(lfdirstate, repo, repo['.'].rev())
        for lfile in modified:
            lfutil.updatestandin(repo, lfutil.standin(lfile))

        try:
            ctx = repo[opts.get('rev')]
            oldmatch = None # for the closure
            def override_match(ctxorrepo, pats=[], opts={}, globbed=False,
                    default='relpath'):
                if util.safehasattr(ctxorrepo, 'match'):
                    ctx0 = ctxorrepo
                else:
                    ctx0 = ctxorrepo[None]
                match = oldmatch(ctxorrepo, pats, opts, globbed, default)
                m = copy.copy(match)
                def tostandin(f):
                    if lfutil.standin(f) in ctx0 or lfutil.standin(f) in ctx:
                        return lfutil.standin(f)
                    elif lfutil.standin(f) in repo[None]:
                        return None
                    return f
                m._files = [tostandin(f) for f in m._files]
                m._files = [f for f in m._files if f is not None]
                m._fmap = set(m._files)
                orig_matchfn = m.matchfn
                def matchfn(f):
                    if lfutil.isstandin(f):
                        # We need to keep track of what lfiles are being
                        # matched so we know which ones to update later
                        # (otherwise we revert changes to other lfiles
                        # accidentally).  This is repo specific, so duckpunch
                        # the repo object to keep the list of lfiles for us
                        # later.
                        if orig_matchfn(lfutil.splitstandin(f)) and \
                                (f in repo[None] or f in ctx):
                            lfileslist = getattr(repo, '_lfilestoupdate', [])
                            lfileslist.append(lfutil.splitstandin(f))
                            repo._lfilestoupdate = lfileslist
                            return True
                        else:
                            return False
                    return orig_matchfn(f)
                m.matchfn = matchfn
                return m
            oldmatch = installmatchfn(override_match)
            try:
                # Mercurial >= 1.9
                scmutil.match
                matches = override_match(repo[None], pats, opts)
            except ImportError:
                # Mercurial <= 1.8
                matches = override_match(repo, pats, opts)
            orig(ui, repo, *pats, **opts)
        finally:
            restorematchfn()
        lfileslist = getattr(repo, '_lfilestoupdate', [])
        lfcommands.updatelfiles(ui, repo, filelist=lfileslist, printmessage=False)
        # Empty out the lfiles list so we start fresh next time
        repo._lfilestoupdate = []
        for lfile in modified:
            if lfile in lfileslist:
                if os.path.exists(repo.wjoin(lfutil.standin(lfile))) and lfile\
                        in repo['.']:
                    lfutil.writestandin(repo, lfutil.standin(lfile),
                        repo['.'][lfile].data().strip(),
                        'x' in repo['.'][lfile].flags())
        lfdirstate = lfutil.openlfdirstate(ui, repo)
        for lfile in added:
            standin = lfutil.standin(lfile)
            if standin not in ctx and (standin in matches or opts.get('all')):
                if lfile in lfdirstate:
                    try:
                        # Mercurial >= 1.9
                        lfdirstate.drop(lfile)
                    except AttributeError:
                        # Mercurial <= 1.8
                        lfdirstate.forget(lfile)
                util.unlinkpath(repo.wjoin(standin))
        lfdirstate.write()
    finally:
        wlock.release()

def hg_update(orig, repo, node):
    result = orig(repo, node)
    # XXX check if it worked first
    lfcommands.updatelfiles(repo.ui, repo)
    return result

def hg_clean(orig, repo, node, show_stats=True):
    result = orig(repo, node, show_stats)
    lfcommands.updatelfiles(repo.ui, repo)
    return result

def hg_merge(orig, repo, node, force=None, remind=True):
    result = orig(repo, node, force, remind)
    lfcommands.updatelfiles(repo.ui, repo)
    return result

# When we rebase a repository with remotely changed lfiles, we need
# to take some extra care so that the lfiles are correctly updated
# in the working copy
def override_pull(orig, ui, repo, source=None, **opts):
    if opts.get('rebase', False):
        repo._isrebasing = True
        try:
            if opts.get('update'):
                 del opts['update']
                 ui.debug('--update and --rebase are not compatible, ignoring '
                          'the update flag\n')
            del opts['rebase']
            try:
                # Mercurial >= 1.9
                cmdutil.bailifchanged(repo)
            except AttributeError:
                # Mercurial <= 1.8
                cmdutil.bail_if_changed(repo)
            revsprepull = len(repo)
            origpostincoming = commands.postincoming
            def _dummy(*args, **kwargs):
                pass
            commands.postincoming = _dummy
            repo.lfpullsource = source
            if not source:
                source = 'default'
            try:
                result = commands.pull(ui, repo, source, **opts)
            finally:
                commands.postincoming = origpostincoming
            revspostpull = len(repo)
            if revspostpull > revsprepull:
                result = result or rebase.rebase(ui, repo)
        finally:
            repo._isrebasing = False
    else:
        repo.lfpullsource = source
        if not source:
            source = 'default'
        result = orig(ui, repo, source, **opts)
    return result

def override_rebase(orig, ui, repo, **opts):
    repo._isrebasing = True
    try:
        orig(ui, repo, **opts)
    finally:
        repo._isrebasing = False

def override_archive(orig, repo, dest, node, kind, decode=True, matchfn=None,
            prefix=None, mtime=None, subrepos=None):
    # No need to lock because we are only reading history and lfile caches
    # neither of which are modified

    lfcommands.cachelfiles(repo.ui, repo, node)

    if kind not in archival.archivers:
        raise util.Abort(_("unknown archive type '%s'") % kind)

    ctx = repo[node]

    # In Mercurial <= 1.5 the prefix is passed to the archiver so try that
    # if that doesn't work we are probably in Mercurial >= 1.6 where the
    # prefix is not handled by the archiver
    try:
        archiver = archival.archivers[kind](dest, prefix, mtime or \
                ctx.date()[0])

        def write(name, mode, islink, getdata):
            if matchfn and not matchfn(name):
                return
            data = getdata()
            if decode:
                data = repo.wwritedata(name, data)
            archiver.addfile(name, mode, islink, data)
    except TypeError:
        if kind == 'files':
            if prefix:
                raise util.Abort(
                    _('cannot give prefix when archiving to files'))
        else:
            prefix = archival.tidyprefix(dest, kind, prefix)

        def write(name, mode, islink, getdata):
            if matchfn and not matchfn(name):
                return
            data = getdata()
            if decode:
                data = repo.wwritedata(name, data)
            archiver.addfile(prefix + name, mode, islink, data)

        archiver = archival.archivers[kind](dest, mtime or ctx.date()[0])

    if repo.ui.configbool("ui", "archivemeta", True):
        def metadata():
            base = 'repo: %s\nnode: %s\nbranch: %s\n' % (
                hex(repo.changelog.node(0)), hex(node), ctx.branch())

            tags = ''.join('tag: %s\n' % t for t in ctx.tags()
                           if repo.tagtype(t) == 'global')
            if not tags:
                repo.ui.pushbuffer()
                opts = {'template': '{latesttag}\n{latesttagdistance}',
                        'style': '', 'patch': None, 'git': None}
                cmdutil.show_changeset(repo.ui, repo, opts).show(ctx)
                ltags, dist = repo.ui.popbuffer().split('\n')
                tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':'))
                tags += 'latesttagdistance: %s\n' % dist

            return base + tags

        write('.hg_archival.txt', 0644, False, metadata)

    for f in ctx:
        ff = ctx.flags(f)
        getdata = ctx[f].data
        if lfutil.isstandin(f):
            path = lfutil.findfile(repo, getdata().strip())
            f = lfutil.splitstandin(f)

            def getdatafn():
                try:
                    fd = open(path, 'rb')
                    return fd.read()
                finally:
                    fd.close()

            getdata = getdatafn
        write(f, 'x' in ff and 0755 or 0644, 'l' in ff, getdata)

    if subrepos:
        for subpath in ctx.substate:
            sub = ctx.sub(subpath)
            try:
                sub.archive(repo.ui, archiver, prefix)
            except TypeError:
                sub.archive(archiver, prefix)

    archiver.done()

# If a lfile is modified the change is not reflected in its standin until a
# commit.  cmdutil.bailifchanged raises an exception if the repo has
# uncommitted changes.  Wrap it to also check if lfiles were changed. This is
# used by bisect and backout.
def override_bailifchanged(orig, repo):
    orig(repo)
    repo.lfstatus = True
    modified, added, removed, deleted = repo.status()[:4]
    repo.lfstatus = False
    if modified or added or removed or deleted:
        raise util.Abort(_('outstanding uncommitted changes'))

# Fetch doesn't use cmdutil.bail_if_changed so override it to add the check
def override_fetch(orig, ui, repo, *pats, **opts):
    repo.lfstatus = True
    modified, added, removed, deleted = repo.status()[:4]
    repo.lfstatus = False
    if modified or added or removed or deleted:
        raise util.Abort(_('outstanding uncommitted changes'))
    return orig(ui, repo, *pats, **opts)

def override_forget(orig, ui, repo, *pats, **opts):
    installnormalfilesmatchfn(repo[None].manifest())
    orig(ui, repo, *pats, **opts)
    restorematchfn()
    try:
        # Mercurial >= 1.9
        m = scmutil.match(repo[None], pats, opts)
    except ImportError:
        # Mercurial <= 1.8
        m = cmdutil.match(repo, pats, opts)

    try:
        repo.lfstatus = True
        s = repo.status(match=m, clean=True)
    finally:
        repo.lfstatus = False
    forget = sorted(s[0] + s[1] + s[3] + s[6])
    forget = [f for f in forget if lfutil.standin(f) in repo[None].manifest()]

    for f in forget:
        if lfutil.standin(f) not in repo.dirstate and not \
                os.path.isdir(m.rel(lfutil.standin(f))):
            ui.warn(_('not removing %s: file is already untracked\n')
                    % m.rel(f))

    for f in forget:
        if ui.verbose or not m.exact(f):
            ui.status(_('removing %s\n') % m.rel(f))

    # Need to lock because standin files are deleted then removed from the
    # repository and we could race inbetween.
    wlock = repo.wlock()
    try:
        lfdirstate = lfutil.openlfdirstate(ui, repo)
        for f in forget:
            if lfdirstate[f] == 'a':
                lfdirstate.drop(f)
            else:
                lfdirstate.remove(f)
        lfdirstate.write()
        lfutil.repo_remove(repo, [lfutil.standin(f) for f in forget],
            unlink=True)
    finally:
        wlock.release()

def getoutgoinglfiles(ui, repo, dest=None, **opts):
    dest = ui.expandpath(dest or 'default-push', dest or 'default')
    dest, branches = hg.parseurl(dest, opts.get('branch'))
    revs, checkout = hg.addbranchrevs(repo, repo, branches, opts.get('rev'))
    if revs:
        revs = [repo.lookup(rev) for rev in revs]

    # Mercurial <= 1.5 had remoteui in cmdutil, then it moved to hg
    try:
        remoteui = cmdutil.remoteui
    except AttributeError:
        remoteui = hg.remoteui

    try:
        remote = hg.repository(remoteui(repo, opts), dest)
    except error.RepoError:
        return None
    o = lfutil.findoutgoing(repo, remote, False)
    if not o:
        return None
    o = repo.changelog.nodesbetween(o, revs)[0]
    if opts.get('newest_first'):
        o.reverse()

    toupload = set()
    for n in o:
        parents = [p for p in repo.changelog.parents(n) if p != node.nullid]
        ctx = repo[n]
        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)
        toupload = toupload.union(set([f for f in files if lfutil.isstandin(f)\
            and f in ctx]))
    return toupload

def override_outgoing(orig, ui, repo, dest=None, **opts):
    orig(ui, repo, dest, **opts)

    if opts.pop('large', None):
        toupload = getoutgoinglfiles(ui, repo, dest, **opts)
        if toupload is None:
            ui.status(_('largefiles: No remote repo\n'))
        else:
            ui.status(_('largefiles to upload:\n'))
            for file in toupload:
                ui.status(lfutil.splitstandin(file) + '\n')
            ui.status('\n')

def override_summary(orig, ui, repo, *pats, **opts):
    orig(ui, repo, *pats, **opts)

    if opts.pop('large', None):
        toupload = getoutgoinglfiles(ui, repo, None, **opts)
        if toupload is None:
            ui.status(_('largefiles: No remote repo\n'))
        else:
            ui.status(_('largefiles: %d to upload\n') % len(toupload))

def override_addremove(orig, ui, repo, *pats, **opts):
    # Check if the parent or child has lfiles if they do don't allow it.  If
    # there is a symlink in the manifest then getting the manifest throws an
    # exception catch it and let addremove deal with it. This happens in
    # Mercurial's test test-addremove-symlink
    try:
        manifesttip = set(repo['tip'].manifest())
    except util.Abort:
        manifesttip = set()
    try:
        manifestworking = set(repo[None].manifest())
    except util.Abort:
        manifestworking = set()

    # Manifests are only iterable so turn them into sets then union
    for file in manifesttip.union(manifestworking):
        if file.startswith(lfutil.shortname):
            raise util.Abort(
                _('addremove cannot be run on a repo with largefiles'))

    return orig(ui, repo, *pats, **opts)

# Calling purge with --all will cause the lfiles to be deleted.
# Override repo.status to prevent this from happening.
def override_purge(orig, ui, repo, *dirs, **opts):
    oldstatus = repo.status
    def override_status(node1='.', node2=None, match=None, ignored=False,
                        clean=False, unknown=False, listsubrepos=False):
        r = oldstatus(node1, node2, match, ignored, clean, unknown,
                      listsubrepos)
        lfdirstate = lfutil.openlfdirstate(ui, repo)
        modified, added, removed, deleted, unknown, ignored, clean = r
        unknown = [f for f in unknown if lfdirstate[f] == '?']
        ignored = [f for f in ignored if lfdirstate[f] == '?']
        return modified, added, removed, deleted, unknown, ignored, clean
    repo.status = override_status
    orig(ui, repo, *dirs, **opts)
    repo.status = oldstatus

def override_rollback(orig, ui, repo, **opts):
    result = orig(ui, repo, **opts)
    merge.update(repo, node=None, branchmerge=False, force=True,
        partial=lfutil.isstandin)
    lfdirstate = lfutil.openlfdirstate(ui, repo)
    lfiles = lfutil.listlfiles(repo)
    oldlfiles = lfutil.listlfiles(repo, repo[None].parents()[0].rev())
    for file in lfiles:
        if file in oldlfiles:
            lfdirstate.normallookup(file)
        else:
            lfdirstate.add(file)
    lfdirstate.write()
    return result