Skip to content

Instantly share code, notes, and snippets.

@drmingdrmer
Last active March 25, 2019 05:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drmingdrmer/ff72ee00df7b7a359263fc3b15d093b0 to your computer and use it in GitHub Desktop.
Save drmingdrmer/ff72ee00df7b7a359263fc3b15d093b0 to your computer and use it in GitHub Desktop.
add/update git sub repository with git-subtree by reading a config file
#!/bin/sh
clr()
{
# clr light read blabla
local black=0
local white=7
local red=1
local green=2
local brown=3
local blue=4
local purple=5
local cyan=6
local light=""
local color=$1
shift
if [ "$color" == "light" ]; then
light="tput bold; "
color=$1
shift
fi
local code=$(eval 'echo $'$color)
local cmd="${light}tput setaf $code"
local color_str="$(eval "$cmd")"
echo $color_str"$@""$(tput sgr0)"
}
shlib_init_colors()
{
Black="$( tput setaf 0)"
BlackBG="$( tput setab 0)"
DarkGrey="$( tput bold; tput setaf 0)"
LightGrey="$( tput setaf 7)"
LightGreyBG="$( tput setab 7)"
White="$( tput bold; tput setaf 7)"
Red="$( tput setaf 1)"
RedBG="$( tput setab 1)"
LightRed="$( tput bold; tput setaf 1)"
Green="$( tput setaf 2)"
GreenBG="$( tput setab 2)"
LightGreen="$( tput bold; tput setaf 2)"
Brown="$( tput setaf 3)"
BrownBG="$( tput setab 3)"
Yellow="$( tput bold; tput setaf 3)"
Blue="$( tput setaf 4)"
BlueBG="$( tput setab 4)"
LightBlue="$( tput bold; tput setaf 4)"
Purple="$( tput setaf 5)"
PurpleBG="$( tput setab 5)"
Pink="$( tput bold; tput setaf 5)"
Cyan="$( tput setaf 6)"
CyanBG="$( tput setab 6)"
LightCyan="$( tput bold; tput setaf 6)"
NC="$( tput sgr0)" # No Color
}
screen_width()
{
local chr="${1--}"
chr="${chr:0:1}"
local width=$(tput cols 2||echo 80)
width="${COLUMNS:-$width}"
echo $width
}
hr()
{
# generate a full screen width horizontal ruler
local width=$(screen_width)
printf -vl "%${width}s\n" && echo ${l// /$chr};
}
remove_color()
{
# remove color control chars from stdin or first argument
local sed=gsed
which -s $sed || sed=sed
local s="$1"
if [ -z "$s" ]; then
$sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g"
else
echo "$s" | remove_color
fi
}
text_hr()
{
# generate a full screen width sperator line with text.
# text_hr "-" "a title"
# > a title -----------------------------------------
#
# variable LR=l|m|r controls alignment
local chr="$1"
shift
local bb="$(echo "$@" | remove_color)"
local text_len=${#bb}
local width=$(screen_width)
let width=width-text_len
local lr=${LR-m}
case $lr in
m)
let left=width/2
let right=width-left
echo "$(printf -vl "%${left}s\n" && echo ${l// /$chr})$@$(printf -vl "%${right}s\n" && echo ${l// /$chr})"
;;
r)
echo "$(printf -vl "%${width}s\n" && echo ${l// /$chr})$@"
;;
*)
# l by default
echo "$@$(printf -vl "%${width}s\n" && echo ${l// /$chr})"
;;
esac
}
SHLIB_LOG_VERBOSE=1
SHLIB_LOG_FORMAT='[$(date +"%Y-%m-%d %H:%M:%S")] $level $title $mes'
die()
{
err "$@" >&2
exit 1
}
die_empty()
{
if test -z "$1"
then
shift
die empty: "$@"
fi
}
set_verbose()
{
SHLIB_LOG_VERBOSE=${1-1}
}
log()
{
local color="$1"
local title="$2"
local level="$_LOG_LEVEL"
shift
shift
local mes="$@"
local NC="$(tput sgr0)"
if [ -t 1 ]; then
title="${color}${title}${NC}"
level="${color}${level}${NC}"
fi
eval "echo \"$SHLIB_LOG_FORMAT\""
}
dd()
{
debug "$@"
}
debug()
{
if [ ".$SHLIB_LOG_VERBOSE" = ".1" ]; then
local LightCyan="$(tput bold ; tput setaf 6)"
_LOG_LEVEL=DEBUG log "$LightCyan" "$@" >&2
fi
}
info()
{
local Brown="$(tput setaf 3)"
_LOG_LEVEL=" INFO" log "$Brown" "$@"
}
ok() {
local Green="$(tput setaf 2)"
_LOG_LEVEL=" OK" log "${Green}" "$@"
}
err() {
local Red="$(tput setaf 1)"
_LOG_LEVEL="ERROR" log "${Red}" "$@"
}
git_hash()
{
git rev-parse $1 \
|| die "'git_hash $@'"
}
git_is_merge()
{
test $(git cat-file -p "$1" | grep "^parent " | wc -l) -gt 1
}
git_parents()
{
git rev-list --parents -n 1 ${1-HEAD} | { read self parents; echo $parents; }
}
git_rev_list()
{
# --parents
# print parent in this form:
# <commit> <parent-1> <parent-2> ..
git rev-list \
--reverse \
--topo-order \
--default HEAD \
--simplify-merges \
"$@" \
|| die "'git rev-list $@'"
}
git_tree_hash()
{
git rev-parse "$1^{tree}"
}
git_ver()
{
local git_version=$(git --version | awk '{print $NF}')
local git_version_1=${git_version%%.*}
local git_version_2=${git_version#*.}
git_version_2=${git_version_2%%.*}
printf "%03d%03d" $git_version_1 $git_version_2
}
git_working_root()
{
git rev-parse --show-toplevel
}
git_rev_exist()
{
git rev-parse --verify --quiet "$1" >/dev/null
}
git_branch_default_remote()
{
local branchname=$1
git config --get branch.${branchname}.remote
}
git_branch_default_upstream_ref()
{
local branchname=$1
git config --get branch.${branchname}.merge
}
git_branch_default_upstream()
{
git rev-parse --abbrev-ref --symbolic-full-name "$1"@{upstream}
# OR
# git_branch_default_upstream_ref "$@" | sed 's/^refs\/heads\///'
}
git_branch_exist()
{
git_rev_exist "refs/heads/$1"
}
git_head_branch()
{
git symbolic-ref --short HEAD
}
git_commit_date()
{
# git_commit_date author|commit <ref> [date-format]
# by default output author-date
local what_date="%ad"
if [ "$1" = "commit" ]; then
# commit date instead of author date
what_date="%cd"
fi
shift
local ref=$1
shift
local fmt="%Y-%m-%d %H:%M:%S"
if [ "$#" -gt 0 ]; then
fmt="$1"
fi
shift
git log -n1 --format="$what_date" --date=format:"$fmt" "$ref"
}
git_commit_copy()
{
# We're going to set some environment vars here, so
# do it in a subshell to get rid of them safely later
dd copy_commit "{$1}" "{$2}" "{$3}"
git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
(
read GIT_AUTHOR_NAME
read GIT_AUTHOR_EMAIL
read GIT_AUTHOR_DATE
read GIT_COMMITTER_NAME
read GIT_COMMITTER_EMAIL
read GIT_COMMITTER_DATE
export GIT_AUTHOR_NAME \
GIT_AUTHOR_EMAIL \
GIT_AUTHOR_DATE \
GIT_COMMITTER_NAME \
GIT_COMMITTER_EMAIL \
GIT_COMMITTER_DATE
# (echo -n "$annotate"; cat ) |
git commit-tree "$2" $3 # reads the rest of stdin
) || die "Can't copy commit $1"
}
git_object_type()
{
# $0 ref|hash
# output "commit", "tree" etc
git cat-file -t "$@" 2>/dev/null
}
git_object_add_by_commit_path()
{
# add an blob or tree object to target_path in index
# the object to add is specified by commit and path
local target_path="$1"
local src_commit="$2"
local src_path="$3"
local src_dir="$(dirname "$src_path")/"
local src_name="$(basename "$src_path")"
local src_treeish="$(git rev-parse "$src_commit:$src_dir")"
git_object_add_by_tree_name "$target_path" "$src_treeish" "$src_name"
}
git_object_add_by_tree_name()
{
# add an blob or tree object to target_path in index
local target_path="$1"
local src_treeish="$2"
local src_name="$3"
dd "arg: target_path: ($target_path) src_treeish: ($src_treeish) src_name: ($src_name)"
local target_dir="$(dirname $target_path)/"
local target_fn="$(basename $target_path)"
local treeish
if [ -z "$src_name" ] || [ "$src_name" = "." ] || [ "$src_name" = "./" ]; then
treeish="$src_treeish"
else
treeish=$(git ls-tree "$src_treeish" "$src_name" | awk '{print $3}')
if [ -z "$treeish" ]; then
die "source treeish not found: in tree: ($src_treeish) name: ($src_name)"
fi
fi
dd "hash of object to add is: $treeish"
if [ "$(git_object_type $treeish)" = "blob" ]; then
# the treeish imported is a file, not a dir
# first create a wrapper tree or replace its containing tree
dd "object to add is blob"
local dir_treeish
local target_dir_treeish="$(git rev-parse "HEAD:$target_dir")"
if [ -n "target_dir_treeish" ]; then
dir_treeish="$(git rev-parse "HEAD:$target_dir")"
dd "target dir presents: $target_dir"
else
dd "target dir absent"
dir_treeish=""
fi
treeish=$(git_tree_add_blob "$dir_treeish" "$target_fn" $src_treeish $src_name) || die create wrapper treeish
target_path="$target_dir"
dd "wrapper treeish: $treeish"
dd "target_path set to: $target_path"
else
dd "object to add is tree"
fi
git_treeish_add_to_prefix "$target_path" "$treeish"
}
git_treeish_add_to_prefix()
{
local target_path="$1"
local treeish="$2"
dd treeish content:
git ls-tree $treeish
git rm "$target_path" -r --cached || dd removing target "$target_path"
if [ "$target_path" = "./" ]; then
git read-tree "$treeish" \
|| die "read-tree $target_path $treeish"
else
git read-tree --prefix="$target_path" "$treeish" \
|| die "read-tree $target_path $treeish"
fi
}
git_tree_add_tree()
{
# output new tree hash in stdout
# treeish can be empty
local treeish="$1"
local target_fn="$2"
local item_hash="$3"
local item_name="$4"
{
if [ -n "$treeish" ]; then
git ls-tree "$treeish" \
| fgrep -v " $item_name"
fi
cat "040000 tree $item_hash $target_fn"
} | git mktree
}
git_tree_add_blob()
{
# output new tree hash in stdout
# treeish can be empty
local treeish="$1"
local target_fn="$2"
local blob_treeish="$3"
local blob_name="$4"
{
if [ -n "$treeish" ]; then
git ls-tree "$treeish" \
| fgrep -v " $target_fn"
fi
git ls-tree "$blob_treeish" "$blob_name" \
| awk -v target_fn="$target_fn" -F" " '{print $1" "target_fn}'
} | git mktree
}
git_workdir_save()
{
local index_hash=$(git write-tree)
# add modified file to index and read index tree
git add -u
local working_hash=$(git write-tree)
# restore index tree
git read-tree $index_hash
echo $index_hash $working_hash
}
git_workdir_load()
{
local index_hash=$1
local working_hash=$2
git_object_type $index_hash || die "invalid index hash: $index_hash"
git_object_type $working_hash || die "invalid workdir hash: $working_hash"
# First create a temp commit to restore working tree.
#
# git-read-index to index and git-reset does not work because deleted file in
# index does not apply to working tree.
#
# But there is an issue with this:
# git checkout --orphan br
# git_workdir_load
# would fails, because ORIG_HEAD is not a commit.
local working_commit=$(echo "x" | git commit-tree $working_hash) || die get working commit
git reset --hard $working_commit || die reset to tmp commit
git reset --soft ORIG_HEAD || die reset to ORIG_HEAD
git read-tree $index_hash || die "load saved index tree from $index_hash"
}
git_workdir_is_clean()
{
local untracked="$1"
if [ "$untracked" == "untracked" ]; then
[ -z "$(git status --porcelain)" ]
else
[ -z "$(git status --porcelain --untracked-files=no)" ]
fi
}
git_copy_commit()
{
git_commit_copy "$@"
}
git_diff_ln_new()
{
# output changed line number of a file: <from> <end>; inclusive:
# 27 28
# 307 309
# 350 350
#
# Usage:
#
# diff working tree with HEAD:
# git_diff_ln_new HEAD -- <fn>
#
# diff working tree with staged:
# git_diff_ln_new -- <fn>
#
# diff staged(cached) with HEAD:
# git_diff_ln_new --cached -- <fn>
#
# in git-diff output:
# for add lines:
# @@ -53 +72,8
#
# for remove lines:
# @@ -155 +179,0
git diff -U0 "$@" \
| grep '^@@' \
| awk '{
# @@ -155 +179,0
# $1 $2 $3
l = $3
gsub("^+", "", l)
# add default offset: ",1"
split(l",1", x, ",")
# inclusive line range:
x[2] = x[1] + x[2] - 1
# line remove format: @@ -155, +179,0
# do need to output line range for removed.
if (x[2] >= x[1]) {
print x[1] " " x[2]
}
}'
}
os_detect()
{
local os
case $(uname -s) in
Linux)
os=linux ;;
*[bB][sS][dD])
os=bsd ;;
Darwin)
os=mac ;;
*)
os=unix ;;
esac
echo $os
}
mac_ac_power_connection()
{
# Connected: (Yes|No)
system_profiler SPPowerDataType \
| sed '1,/^ *AC Charger Information:/d' \
| grep Connected:
}
mac_power()
{
# $0 is-battery exit code 0 if using battery.
# $0 is-ac-power exit code 0 if using ac power.
local cmd="$1"
local os=$(os_detect)
if [ "$os" != "mac" ]; then
err "not mac but: $os"
return 1
fi
case $cmd in
is-battery)
mac_ac_power_connection | grep -q No
;;
is-ac-power)
mac_ac_power_connection | grep -q Yes
;;
*)
err "invalid cmd: $cmd"
return 1
;;
esac
}
fn_match()
{
# $0 a.txt *.txt
case "$1" in
$2)
return 0
;;
esac
return 1
}
main()
{
local cmd="${1-update}"
local match="$2"
case "$cmd" in
-h|--help)
usage
exit 0
;;
init)
init_config
exit 0
;;
esac
local root=$(git_working_root)
[ "x$root" = "x" ] && die 'looking for git repo root'
local conf_fn=./.gitsubrepo
cd "$root"
[ -f .gitsubrepo_refs ] && rm .gitsubrepo_refs
> .gitsubrepo_refs
local base=
local remote=
local remote_suffix=
local prefix=
local url=
local ref=
local localtag=
while read a b c d; do
if [ "x$a" = "x" ] || [ "${a:0:1}" = "#" ]; then
continue
fi
# [ base: xp/vim-d ]
# declare base dir for following sub repo
#
# [ remote: https://github.com/ ]
# declare remote base url for following sub repo
if test "${a:0:1}" = '['
then
case $b in
base:)
base="$c"
base="${base%%]}"
base="${base%/}/"
if test "$base" = "/"; then
base=
fi
;;
remote:)
remote="$c"
;;
remote_suffix:)
remote_suffix="$c"
;;
*)
echo "invalid tag: $b"
exit 1
esac
continue
fi
prefix="$a"
url="$b"
ref="$c"
dir="$d"
if [ "x$match" != "x" ] && ! fn_match "$prefix" *$match*; then
continue
fi
if [ ".$cmd" = ".add" ]; then
if [ -d "$prefix" ]; then
continue
fi
fi
# "xpt drmingdrmer/${prefix} master" is translated to "xpt drmingdrmer/xpt master"
url=$(eval "echo $url")
if test "${url%/}" != "$url"; then
url="${url}$prefix"
fi
if test "${url:0:8}" != "https://" && test "${url:0:4}" != "git@"
then
url="${remote}${url}${remote_suffix}"
fi
fetch_remote "$base$prefix" "$url" "$ref" "$dir" &
done<"$conf_fn"
wait
while read prefix url ref localtag dir; do
dd "$prefix, $url, $ref, $localtag, $dir"
local tag_hash=$(git_hash "$localtag") || die get hash of $localtag
local tag_author_date=$(git_commit_date author "$localtag") || die get authoer date of $localtag
local head_hash=$(git_hash HEAD)
dd "tag_hash: $tag_hash"
dd "head_hash: $head_hash"
# clear old $prefix
if [ -d "$prefix" ]; then
git rm -r "$prefix" >/dev/null || die rm "$prefix"
fi
dd "add to ($prefix): from ($dir)"
git_object_add_by_commit_path "$prefix" "$tag_hash" "$dir" || die git_object_add_by_commit_path
# remove not in index files
git checkout -- "$prefix" || die "checkout $prefix"
# get tree object
local tree=$(git write-tree) || die git write-tree
dd "tree: $tree"
if [ "$tree" = "$(git_tree_hash HEAD)" ]; then
info "Nothing changed: $prefix"
continue
fi
local changes=
local latest_hash=$(find_latest_squash "$prefix") || die find_latest_squash of "$prefix"
if [ -n "$latest_hash" ]; then
info "latest commit of $prefix: $latest_hash ($(git_commit_date author "$latest_hash"))"
# add 4 space indent
changes="$(git --no-pager shortlog $latest_hash..$tag_hash | awk '{print " "$0}')"
else
info "latest commit of $prefix: not found"
fi
local commit=$(
{
cat <<-END
Squashed $prefix $ref:$dir ${tag_hash:0:9} (${tag_author_date})
url: $url
ref: $ref
sub-dir: $dir
localtag: $localtag $tag_hash
git-subrepo-dir: $prefix
git-subrepo-hash: $tag_hash
changes: from ${latest_hash:0:9} ($(git_commit_date author "$latest_hash"))
$changes
END
} | git commit-tree $tree -p $head_hash
) || die commit
dd "commit: $commit"
git merge --ff-only --no-edit --commit $commit
done<".gitsubrepo_refs"
while read a b c localtag dir; do
dd $a, $b, $c, $localtag, $dir
git update-ref -d $localtag
done<".gitsubrepo_refs"
rm .gitsubrepo_refs
}
init_config()
{
local root=$(git_working_root)
[ "x$root" = "x" ] && die 'looking for git repo root'
(
cd $root
if [ -f .gitsubrepo ]; then
echo ".gitsubrepo already exists"
return 0
fi
cat >.gitsubrepo <<-END
[ remote_suffix: .git ]
[ remote: https://github.com/ ]
[ base: ]
git-subrepo baishancloud/git-subrepo master git-subrepo
[ base: deps ]
pykit baishancloud/pykit master
# git-subrepo
# for maintaining sub git repo
# https://github.com/baishancloud/git-subrepo
END
vim .gitsubrepo
git add .gitsubrepo
echo "git commit to persistent .gitsubrepo config"
)
}
find_latest_squash()
{
# learned from git-subtree: find_latest_squash
git log --grep="^Squashed $prefix " \
--pretty=format:'START %H%n%b%nEND%n' HEAD |
while read a b junk; do
case $a in
git-subrepo-hash:)
echo $b
return 0
;;
esac
done
}
fetch_remote()
{
local prefix="$1"
local url="$2"
local ref="$3"
local dir="$4"
local localtag=refs/tags/_gitsubrepo/$prefix
# git tag does not allow leading dot. E.g.: "refs/tags/_gitsubrepo/a/.b"
localtag="${localtag//./-}"
local mes="$url $ref -> $localtag"
dd "Start fetching $mes"
git fetch --no-tags "$url" "$ref:$localtag" || die fetch "$mes"
echo "$prefix $url $ref $localtag" "$dir" >> .gitsubrepo_refs
dd "Done fetching $mes"
}
usage()
{
cat <<-END
usage: git subrepo
Merge sub git repo into sub-directory in a parent git dir with git-submerge.
git-subrepo reads config from ".gitsubrepo" resides in the root of parent
git working dir.
Configure file ".gitsubrepo" syntax:
# set base of remote url to "https://github.com/"
[ remote: https://github.com/ ]
# set base of local dir to "plugin"
[ base: plugin ]
# <local dir> <remote uri> <ref to fetch> [<dir>]
gutter airblade/vim-gitgutter master src
# if <remote uri> ends with "/", <local dir> will be added after "/"
ansible-vim DavidWittman/ master
# change base to "root"
[ base: ]
# use a specific commit 1a2b3c4
ultisnips SirVer/ 1a2b3c4
# add a sub-directory
ultisnips SirVer/ 1a2b3c4 src
With above config, "git subrepo" will:
- fetch master of https://github.com/DavidWittman/ansible-vim
and put it in:
<git-root>/plugin/ansible-vim
- fetch master of https://github.com/airblade/vim-gitgutter
and put it in:
<git-root>/plugin/gutter
- fetch commit 1a2b3c4 of https://github.com/SirVer/ultisnips
and put it in:
<git-root>/ultisnips
END
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment