#!/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 "$remote" "${refspecs[@]}" ;; get) git fetch "$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."