"""
BitBake 'Fetch' git submodules implementation

Inherits from and extends the Git fetcher to retrieve submodules of a git repository
after cloning.

SRC_URI = "gitsm://<see Git fetcher for syntax>"

See the Git fetcher, git://, for usage documentation.

NOTE: Switching a SRC_URI from "git://" to "gitsm://" requires a clean of your recipe.

"""

# Copyright (C) 2013 Richard Purdie
#
# SPDX-License-Identifier: GPL-2.0-only
#

import os
import bb
import copy
import tempfile
from   bb.fetch2.git import Git
from   bb.fetch2 import runfetchcmd
from   bb.fetch2 import logger
from   bb.fetch2 import Fetch

class GitSM(Git):
    def supports(self, ud, d):
        """
        Check to see if a given url can be fetched with git.
        """
        return ud.type in ['gitsm']

    def process_submodules(self, ud, workdir, d):
        """
        Iterate over all of the submodules in this repository and generate a
        fetch object for all of them.
        """

        submodules = []
        paths = {}
        revision = {}
        uris = {}
        subrevision = {}

        def parse_gitmodules(gitmodules):
            modules = {}
            module = ""
            for line in gitmodules.splitlines():
                if line.startswith('[submodule'):
                    module = line.split('"')[1]
                    modules[module] = {}
                elif module and line.strip().startswith('path'):
                    path = line.split('=')[1].strip()
                    modules[module]['path'] = path
                elif module and line.strip().startswith('url'):
                    url = line.split('=')[1].strip()
                    modules[module]['url'] = url
            return modules

        # Collect the defined submodules, and their attributes
        try:
            gitmodules = runfetchcmd("%s show %s:.gitmodules" % (ud.basecmd, ud.revision), d, quiet=True, workdir=workdir)
        except:
            # No submodules to update
            gitmodules = ""

        for m, md in parse_gitmodules(gitmodules).items():
            try:
                module_hash = runfetchcmd("%s ls-tree -z -d %s %s" % (ud.basecmd, ud.revision, md['path']), d, quiet=True, workdir=workdir)
            except:
                # If the command fails, we don't have a valid file to check.  If it doesn't
                # fail -- it still might be a failure, see next check...
                module_hash = ""

            if not module_hash:
                logger.debug("submodule %s is defined, but is not initialized in the repository. Skipping", m)
                continue

            submodules.append(m)
            paths[m] = md['path']
            revision[m] = ud.revision
            uris[m] = md['url']
            subrevision[m] = module_hash.split()[2]

            # Convert relative to absolute uri based on parent uri
            if  uris[m].startswith('..') or uris[m].startswith('./'):
                newud = copy.copy(ud)
                newud.path = os.path.normpath(os.path.join(newud.path, uris[m]))
                uris[m] = Git._get_repo_url(self, newud)

        urls = []
        for module in submodules:
            # Translate the module url into a SRC_URI

            if "://" in uris[module]:
                # Properly formated URL already
                proto = uris[module].split(':', 1)[0]
                url = uris[module].replace('%s:' % proto, 'gitsm:', 1)
            else:
                if ":" in uris[module]:
                    # Most likely an SSH style reference
                    proto = "ssh"
                    if ":/" in uris[module]:
                        # Absolute reference, easy to convert..
                        url = "gitsm://" + uris[module].replace(':/', '/', 1)
                    else:
                        # Relative reference, no way to know if this is right!
                        logger.warning("Submodule included by %s refers to relative ssh reference %s.  References may fail if not absolute." % (ud.url, uris[module]))
                        url = "gitsm://" + uris[module].replace(':', '/', 1)
                else:
                    # This has to be a file reference
                    proto = "file"
                    url = "gitsm://" + uris[module]
            if url.endswith("{}{}".format(ud.host, ud.path)):
                raise bb.fetch2.FetchError("Submodule refers to the parent repository. This will cause deadlock situation in current version of Bitbake." \
                                           "Consider using git fetcher instead.")

            url += ';protocol=%s' % proto
            url += ";name=%s" % module
            url += ";subpath=%s" % module
            url += ";nobranch=1"
            url += ";lfs=%s" % ("1" if self._need_lfs(ud) else "0")
            url += ";rev=%s" % subrevision[module]
            url += ";bareclone=1"

            parentdir = self.destdir(ud, '', d)
            url += ";parentdir=%s" % parentdir
            gitdir = '' if ud.bareclone else '.git'
            subdir = os.path.join(parentdir, gitdir, 'modules', module)
            url += ";subdir=%s" % subdir

            # Note that adding "user=" here to give credentials to the
            # submodule is not supported. Since using SRC_URI to give git://
            # URL a password is not supported, one have to use one of the
            # recommended way (eg. ~/.netrc or SSH config) which does specify
            # the user (See comment in git.py).
            # So, we will not take patches adding "user=" support here.

            urls.append(url)

        return Fetch(urls, d, cache=False) if urls else None

    def call_process_submodules(self, ud, d):
        # If we're using a shallow mirror tarball it needs to be
        # unpacked temporarily so that we can examine the .gitmodules file
        # Unpack even when ud.clonedir is not available,
        # which may occur during a fast shallow clone
        if os.path.exists(ud.clonedir):
            return self.process_submodules(ud, ud.clonedir, d)
        elif ud.shallow and os.path.exists(ud.fullshallow):
            with tempfile.TemporaryDirectory(dir=d.getVar("DL_DIR")) as tmpdir:
                runfetchcmd("tar -xzf %s" % ud.fullshallow, d, workdir=tmpdir)
                return self.process_submodules(ud, tmpdir, d)
        else:
            raise bb.fetch2.FetchError("Submodule source not available.")

    def need_update(self, ud, d):
        if Git.need_update(self, ud, d):
            return True

        need_update = False
        fetch = self.call_process_submodules(ud, d)
        if fetch:
            for url in fetch.urls:
                urldata = fetch.ud[url]
                if urldata.method.need_update(urldata, d):
                    need_update = True

        return need_update

    def download(self, ud, d):
        Git.download(self, ud, d)

        fetch = self.call_process_submodules(ud, d)
        if fetch:
            fetch.download()

    def unpack(self, ud, destdir, d):
        fulldestdir = self.destdir(ud, destdir, d)

        Git.unpack(self, ud, destdir, d)

        parentdir = ud.parm.get('parentdir')
        if parentdir:
            module = ud.parm['subpath']
            if not os.path.isabs(parentdir):
                parentdir = os.path.join(destdir, parentdir)

            ud.setup_localpath(d)

            # Correct the submodule references to the local download version...
            runfetchcmd("%(basecmd)s config submodule.%(module)s.url %(url)s" % {'basecmd': ud.basecmd, 'module': module, 'url' : ud.localpath}, d, workdir=parentdir)

            if ud.shallow:
                runfetchcmd("%(basecmd)s config submodule.%(module)s.shallow true" % {'basecmd': ud.basecmd, 'module': module}, d, workdir=parentdir)

            # Ensure the submodule repository is NOT set to bare, since we're checking it out...
            try:
                runfetchcmd("%s config core.bare false" % (ud.basecmd), d, quiet=True, workdir=fulldestdir)
            except:
                logger.error("Unable to set git config core.bare to false for %s" % fulldestdir)
                raise

        fetch = self.process_submodules(ud, fulldestdir, d)
        if fetch:
            fetch.unpack(destdir)

        if not ud.bareclone and fetch:
            cmdprefix = ""
            # Avoid LFS smudging (replacing the LFS pointers with the actual content) when LFS shouldn't be used but git-lfs is installed.
            if not self._need_lfs(ud):
                cmdprefix = "GIT_LFS_SKIP_SMUDGE=1 "
            runfetchcmd("%s%s submodule update --recursive --no-fetch" % (cmdprefix, ud.basecmd), d, quiet=True, workdir=fulldestdir)
    def clean(self, ud, d):
        fetch = self.call_process_submodules(ud, d)
        if fetch:
            fetch.clean()
        Git.clean(self, ud, d)

    def implicit_urldata(self, ud, d):
        fetch = self.call_process_submodules(ud, d)
        if fetch:
            return fetch.expanded_urldata()

        return []
