#!/usr/bin/env bash set -e [ "$GIT_CACHE_VERBOSE" != "1" ] && Q=-q 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() { git_cache_initialized || { mkdir -p "${GIT_CACHE_DIR}" git_cache init --bare git_cache config core.compression 1 } } startswith() { case "$1" in "${2}"*) return 0 ;; *) return 1 esac } add() { local repo="$1" _locked "$GIT_CACHE_DIR/$name.addlock" _add "$repo" } _add() { 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 } 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() { 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 $Q -n $remote done } is_cached() { 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 return 1 } list() { local REMOTES="$(git_cache remote show)" for remote in $REMOTES; do echo "$(git_cache ls-remote --get-url $remote)" done } drop() { local REMOTE=${1} [ -z "$REMOTE" ] && { echo "usage: git cache drop " 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 } _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 || true)" [ -n "$out" ] && echo $out | cut -f 1 -d" " } _check_tag_or_commit() { local SHA1=$1 local REMOTE_NAME=$2 if _check_commit $SHA1 ; then local tag=commit$SHA1-$$ git_cache tag $tag $SHA1 2> /dev/null || true # ignore possibly already existing tag echo "$tag" elif _tag_to_sha1 ${REMOTE_NAME}/$SHA1 > /dev/null; then echo "${REMOTE_NAME}/$SHA1" fi } clone() { 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 pull=0 if startswith "$SHA1" "pull/"; then pull=1 fi if [ $pull -eq 0 ]; then local tag="$(_check_tag_or_commit $SHA1 $REMOTE_NAME)" if [ -z "$tag" ]; then # commit / tag not in cache, try updating repo update "$REMOTE" tag="$(_check_tag_or_commit $SHA1 $REMOTE_NAME)" fi else local tag="remotes/$REMOTE_NAME/master" if [ -z "$(_tag_to_sha1 $tag)" ]; then update "$REMOTE" fi tag="$(_check_tag_or_commit $(_tag_to_sha1 $tag) $REMOTE_NAME)" if [ -z "$tag" ]; then echo "git-cache: cannot checkout master branch of $REMOTE" false fi fi if [ -n "$tag" ]; then echo "git-cache: cloning from cache. tag=$tag" git -c advice.detachedHead=false clone $Q --reference "${GIT_CACHE_DIR}" --shared "${GIT_CACHE_DIR}" "${TARGET_PATH}" --branch $tag # rename tags from /* to * git -C "${TARGET_PATH}" fetch $Q origin "refs/tags/${REMOTE_NAME}/*:refs/tags/*" # remove all commit* and /* tags git -C "${TARGET_PATH}" tag -l \ | grep -P '(^[a-f0-9]{40}/|^commit[a-f0-9]{40}(-\d+)?$)' \ | xargs git -C "${TARGET_PATH}" tag -d > /dev/null # cleanup possibly created helper tag case $tag in commit*) git_cache tag -d $tag 2>&1 > /dev/null || true ;; esac if [ $pull -eq 1 ]; then git -C "${TARGET_PATH}" fetch $Q $REMOTE $SHA1:$SHA1 git -C "${TARGET_PATH}" checkout $Q $SHA1 fi else echo "git-cache: trying checkout from source" git clone $Q --reference "${GIT_CACHE_DIR}" --shared "${REMOTE}" "${TARGET_PATH}" git -c advice.detachedHead=false -C "${TARGET_PATH}" checkout $Q $SHA1 fi else git clone "${REMOTE}" "${TARGET_PATH}" git -c advice.detachedHead=false -C "${TARGET_PATH}" checkout $SHA1 fi } cleanup() { git_cache tag -l \ | grep -P '(^commit[a-f0-9]{40}(-\d+)?$)' \ | xargs git -C "${GIT_CACHE_DIR}" tag -d > /dev/null } 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 add repository " echo " git cache list list cached repositories" echo " git cache drop drop repo from cache" echo " git cache update [] fetch repo (or all)" echo " git cache clone clone repository from cache" echo " git cache show-path print's the path that can be used as " echo " '--reference' parameter" echo " git cache cleanup cleanup dangling temporary tags" echo " (appear if git-cache gets inter-" echo " rupted, but are harmless)" echo "" echo "To retrieve objects from cache (will use remote repository if needed):" echo ' git clone --reference $(git cache show-path) ' } [ $# -eq 0 ] && { usage exit 1 } ACTION=$1 shift 1 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 $* ;; cleanup) cleanup ;; *) usage ;; esac