#!/bin/bash
#
# Moves a branch to or from the current git tree to or from
# another git tree
#
# usage:   git-branchmove get|put REMOTE PATTERN

set -e
set -o posix

fail () { echo >&2 "git-branchmove: $*"; exit 16; }
badusage () { fail "bad usage: $*"; }

if [ $# -lt 3 ]; then badusage "too few arguments"; fi

op="$1"; shift
case "$op" in get|put) ;; *) badusage "unknown operation \`$op'"; esac

remote="$1"; shift

# Plan of attack:
#  determine execute-sh runes for src and dst trees
#  list affected branches on source
#  check that source branches are not checked out
#  list affected branches on destination and moan if any nonequal overlap
#  transfer src->dst refs/heads/BRANCH:refs/heads/BRANCH
#  transfer and merge reflog(s) xxx todo
#  delete src refs

case "$remote" in
*:*)	remoteurl="$remote" ;;
[/.]*)	remoteurl="$remote" ;;
*)	remoteurl="$(
		git config remote."$remote".pushurl ||
		git config remote."$remote".url ||
		fail "no pushurl or url defined for remote $remote"
		)"
	remotename="$remote"
esac

remote_spec="$(perl -e '
    $_ = $ARGV[0];
    if (m#^ssh://([^:/]+)(?:\:(\w+))?#) {
	print "$'\''|ssh ";
	print " -p $3" if $2;
        print "$1\n";
    } elsif (m#^([-+_.0-9a-zA-Z\@]+):(?!//|:)#) {
        print "$'\''|ssh $1\n";
    } elsif (m#^[/.]#) {
        print "$_|sh -c $1\n";
    } else {
        die "git-branchmove: unsupported remote url \`$_'\''\n";
    }
' "$remoteurl")"

remote_path="${remote_spec%%|*}"
remote_rune="${remote_spec#*|}"

case $op in
get)
	src_rune="$remote_rune"
	src_path="$remote_path"
	dst_rune="sh -c"
	dst_path=.
	updatemsg="git-branchmove: moved to $remote ($remoteurl)"
	push_fetch=fetch
	;;
put)
	dst_rune="$remote_rune"
	dst_path="$remote_path"
	src_rune="sh -c"
	src_path=.
	updatemsg="git-branchmove; moved to `hostname -f` by `whoami`"
	push_fetch=push
	;;
esac

on_src () { $src_rune "set -e; cd $src_path; $*"; }
on_dst () { $dst_rune "set -e; cd $dst_path; $*"; }


#----- fetch the current refs from both sides -----

branch_pats=''
for branch_pat in "$@"; do
	branch_pats+=" '[r]efs/heads/$branch_pat'"
done

get_branches_rune='
	git for-each-ref --format="%(refname)=%(objectname)" '"$branch_pats"'
'

src_branches=( $(
	on_src '
		printf H
		git symbolic-ref -q HEAD || test $? = 1
		echo " "
		'"$get_branches_rune"'
	'	
))

src_head="${src_branches[0]}"
unset src_branches[0]
: "${src_branches[@]}"

case "$src_head" in
H) ;; # already detached
*)
	src_head="${src_head#H}"
	for check in "${src_branches[@]}"; do
		case "$check" in
		"$src_head"=*)
			fail "would delete checked-out branch $src_head"
			;;
		esac
	done
	;;
esac


if [ "${#src_branches[@]}" = 0 ]; then
	echo >&2 "git-branchmove: nothing to do"
	exit 1
fi

dst_branches=( $(on_dst "$get_branches_rune") )
: "${dst_branches[@]}"


#----- check for nonequal overlaps -----

ok=true
for dst_check in "${dst_branches[@]}"; do
	dst_ref="${dst_check%=*}"
	for src_check in "${src_branches[@]}"; do
		case "$src_check" in
		"$dst_check")	;;
		"$dst_ref"=*)
			ok=false
			echo >&2 "src: $src_check   dst: $dst_check"
			;;
		esac
	done
done

$ok || fail "would overwrite some destination branch(es)"


#----- do the transfer -----

refspecs=()
for src_xfer in "${src_branches[@]}"; do
	src_ref="${src_xfer%=*}"
	refspecs+=("$src_ref:$src_ref")
done

case "$op" in
put)	git push --no-follow-tags "$remote" "${refspecs[@]}"	;;
get)	git fetch --no-tags "$remote" "${refspecs[@]}"	;;
*)	fail "unknown $op ???"			;;
esac


#----- delete the refs on the source -----

(
	printf "%s\n" "$updatemsg"
	for src_rm in "${src_branches[@]}"; do printf "%s\n" "$src_rm"; done
) | on_src '
	read updatemsg
	while read src_rm; do
		src_ref="${src_rm%=*}"
		src_obj="${src_rm##*=}"
		git update-ref -m "$updatemsg" -d "$src_ref" "$src_obj"
		echo "moved: $src_ref"
	done
'

#----- update the remote tracking branches -----

if [ "x$remotename" != x ]; then
	for src_rm in "${src_branches[@]}"; do
		src_ref="${src_rm%=*}"
		src_obj="${src_rm##*=}"

		case "$src_ref" in
		refs/heads/*) ;;
		*) continue ;;
		esac

		branch="${src_ref#refs/heads/}"
		track_ref="refs/remotes/$remotename/$branch"
		case $op in
		get)	git update-ref -d "$track_ref"	;;
		put)	git update-ref "$track_ref" "$src_obj" ;;
		*)	fail "unknown $op ???"
		esac
	done
fi

echo "git-repomove: moved ${#src_branches[@]} branches."
