git-cache 5.6 KB
#!/bin/sh

git_cache() {
    git -C "${GIT_CACHE_DIR}" $*
}

git_cache_initialized() {
    local _git_dir="$(git_cache rev-parse --git-dir 2>/dev/null)"
    test "$_git_dir" = "." -o "$_git_dir" = ".git"
}

init() {
    set -e
    git_cache_initialized || {
        mkdir -p "${GIT_CACHE_DIR}"

        git_cache init --bare
        git_cache config core.compression 1
    }
    set +e
}

add() {
    local repo="$1"

    _locked "$GIT_CACHE_DIR/$name.addlock" _add "$repo"
}

_add() {
    set -e
    local repo="$1"
    local name="$(_remote_name $repo)"

    if ! is_cached "$repo"; then
        git_cache remote add "$name" "$repo"
        git_cache config --add remote.${name}.fetch "+refs/tags/*:refs/tags/${name}/*"
    else
        echo "git-cache: $url already in cache"
    fi
    set +e
}

if [ "$(uname)" = Darwin ]; then
    _locked() {
        local lockfile="$1"
        shift

        while ! shlock -p $$ -f $lockfile; do
            sleep 0.2
        done

        $*

        rm $lockfile
    }
else
    _locked() {
        local lockfile="$1"
        shift

        (
        flock -w 600 9 || exit 1
        $*
        ) 9>"$lockfile"
    }
fi

update() {
    set -e

    local REMOTE=${1}
    if [ -n "$REMOTE" ]; then
        local REMOTES=$(_remote_name $REMOTE)
    else
        local REMOTES="$(git_cache remote show)"
    fi

    for remote in $REMOTES; do
        echo "git-cache: updating remote $remote"
        _locked "$GIT_CACHE_DIR/$remote.lock" git_cache --namespace $remote fetch -n $remote
    done

    set +e
}

is_cached() {
    set +e
    local url="$1"
    local REMOTES="$(git_cache remote show)"
    for remote in $REMOTES; do
        [ "$(git_cache ls-remote --get-url $remote)" = "$url" ] && return 0
    done
    set -e
    return 1
}

list() {
    local REMOTES="$(git_cache remote show)"
    for remote in $REMOTES; do
        echo "$(git_cache ls-remote --get-url $remote)"
    done
}

drop() {
    set -e
    local REMOTE=${1}
    [ -z "$REMOTE" ] && {
        echo "usage: git cache drop <url>"
        exit 1
    }
    local REMOTES="$(git_cache remote show)"
    for remote in $REMOTES; do
        [ "$(git_cache ls-remote --get-url $remote)" = "$REMOTE" ] && {
            git_cache remote remove $remote
            break
        }
    done
    set +e
}

_check_commit() {
    git_cache cat-file -e ${1}^{commit} 2>/dev/null
}

_remote_name() {
    echo "$*" | git hash-object --stdin
}

_tag_to_sha1() {
    local out="$(git_cache log -n 1 --pretty=oneline $1 -- 2>/dev/null)"
    [ -n "$out" ] && echo $out | cut -f 1 -d" "
}

_check_tag_or_commit() {
    local SHA1=$1
    local REMOTE_NAME=$2

    if _check_commit $SHA1 ; then
        git_cache tag commit$SHA1 $SHA1 2> /dev/null || true # ignore possibly already existing tag
        echo "commit$SHA1"
    elif _tag_to_sha1 ${REMOTE_NAME}/$SHA1 > /dev/null; then
        echo "${REMOTE_NAME}/$SHA1"
    fi
}

clone() {
    set -e
    local REMOTE="${1}"
    local SHA1="${2}"
    local REMOTE_NAME="$(_remote_name $REMOTE)"
    local TARGET_PATH="${3}"

    [ -z "$TARGET_PATH" ] && TARGET_PATH="$(basename $REMOTE)"

    # make sure git won't ask for credentials
    export GIT_TERMINAL_PROMPT=0

    if git_cache_initialized; then
        if ! is_cached "$REMOTE"; then
            echo "git cache: auto-adding $REMOTE"
            add "$REMOTE"
        fi

        local tag="$(_check_tag_or_commit $SHA1 $REMOTE_NAME)"
        if [ -z "$tag" ]; then
            # commit / tag not in cache, try updating repo
            update "$REMOTE" || true
            tag="$(_check_tag_or_commit $SHA1 $REMOTE_NAME)"
        fi

        if [ -n "$tag" ]; then
            echo "git-cache: cloning from cache."
            git -c advice.detachedHead=false clone --reference "${GIT_CACHE_DIR}" --shared "${GIT_CACHE_DIR}" "${TARGET_PATH}" --branch $tag
            git -C "${TARGET_PATH}" fetch origin "refs/tags/${REMOTE_NAME}/*:refs/tags/*" > /dev/null
        else
            echo "git-cache: trying checkout from source"
            git clone --reference "${GIT_CACHE_DIR}" --shared "${REMOTE}" "${TARGET_PATH}"
            git -c advice.detachedHead=false -C "${TARGET_PATH}" checkout $SHA1
        fi
    else
            git clone "${REMOTE}" "${TARGET_PATH}"
            git -c advice.detachedHead=false -C "${TARGET_PATH}" checkout $SHA1
    fi
    set +e
}

usage() {
    echo "git cache uses a bare git repository containing all objects from multiple"
    echo "upstream git repositories."
    echo ""
    echo "usage:"
    echo ""
    echo "    git cache init                initialize git cache"
    echo "    git cache add <url>           add repository <url>"
    echo "    git cache list                list cached repositories"
    echo "    git cache drop <url>          drop repo from cache"
    echo "    git cache update [<url>]      fetch repo <url> (or all)"
    echo "    git cache clone <url> <SHA1>  clone repository <url> from cache"
    echo "    git cache show-path           print's the path that can be used as "
    echo "                                  '--reference' parameter"
    echo ""
    echo "To retrieve objects from cache (will use remote repository if needed):"
    echo '    git clone --reference $(git cache show-path) <repo>'
}

[ $# -eq 0 ] && {
    usage
    exit 1
}

ACTION=$1
shift 1

export GIT_CACHE_DIR=${GIT_CACHE_DIR:-${HOME}/.gitcache}

case $ACTION in
    init)
        init $*
        ;;
    add)
        add $*
        ;;
    update)
        update $*
        ;;
    list)
        list $*
        ;;
    drop)
        drop $*
        ;;
    show-path)
        echo ${GIT_CACHE_DIR}
        ;;
    clone)
        clone $*
        ;;
    *)
        usage
        ;;
esac