chiark / gitweb /
git-branchmove: new script, still work in progress
[chiark-utils.git] / scripts / git-branchmove
1 #!/bin/bash
2 #
3 # Moves a branch to or from the current git tree to or from
4 # another git tree
5 #
6 # usage:   git-branchmove get|put REMOTE PATTERN
7
8 set -e
9 set -o posix
10
11 fail () { echo >&2 "git-branchmove: $*"; exit 16; }
12 badusage () { fail "bad usage: $*"; }
13
14 if [ $# -lt 3 ]; then badusage "too few arguments"; fi
15
16 op="$1"; shift
17 case "$op" in get|put) ;; *) badusage "unknown operation \`$op'"; esac
18
19 remote="$1"; shift
20
21 # Plan of attack:
22 #  determine execute-sh runes for src and dst trees
23 #  list affected branches on source
24 #  check that source branches are not checked out
25 #  list affected branches on destination and moan if any nonequal overlap
26 #  transfer src->dst refs/heads/BRANCH:refs/heads/BRANCH
27 #  transfer and merge reflog(s) xxx todo
28 #  delete src refs
29
30 case "$remote" in
31 *:*)    remoteurl="$remote" ;;
32 [/.]*)  remoteurl="$remote" ;;
33 *)      remoteurl="$(
34                 git config remote."$remote".pushurl ||
35                 git config remote."$remote".url ||
36                 fail "no pushurl or url defined for remote $remote"
37                 )"
38 esac
39
40 remote_spec="$(perl -e '
41     $_ = $ARGV[0];
42     if (m#^ssh://([^:/]+)(?:\:(\w+))?#) {
43         print "$'\''|ssh ";
44         print " -p $3" if $2;
45         print "$1\n";
46     } elsif (m#^([-+_.0-9a-zA-Z\@]+):(?!//|:)#) {
47         print "$'\''|ssh $1\n";
48     } elsif (m#^[/.]#) {
49         print "$&|sh -c $1\n";
50     } else {
51         die "git-branchmove: unsupported remote url \`$_'\''\n";
52     }
53 ' "$remoteurl")"
54
55 remote_path="${remote_spec%%|*}"
56 remote_rune="${remote_spec#*|}"
57
58 case $op in
59 get)
60         src_rune="$remote_rune"
61         src_path="$remote_path"
62         dst_rune="sh -c"
63         dst_path=.
64         updatemsg="git-branchmove: moved to $remote ($remoteurl)"
65         push_fetch=fetch
66         ;;
67 put)
68         dst_rune="$remote_rune"
69         dst_path="$remote_path"
70         src_rune="sh -c"
71         src_path=.
72         updatemsg="git-branchmove; moved to `hostname -f` by `whoami`"
73         push_fetch=push
74         ;;
75 esac
76
77 on_src () { $src_rune "set -e; cd $src_path; $*"; }
78 on_dst () { $dst_rune "set -e; cd $dst_path; $*"; }
79
80
81 #----- fetch the current refs from both sides -----
82
83 branch_pats=''
84 for branch_pat in "$@"; do
85         branch_pats+=" '[r]efs/heads/$branch_pat'"
86 done
87
88 get_branches_rune='
89         git for-each-ref --format="%(refname)=%(objectname)" '"$branch_pats"'
90 '
91
92 src_branches=( $(
93         on_src '
94                 printf H
95                 git symbolic-ref -q HEAD || test $? = 1
96                 echo " "
97                 '"$get_branches_rune"'
98         '       
99 ))
100
101 src_head="${src_branches[0]}"
102 unset src_branches[0]
103 : "${src_branches[@]}"
104
105 case "$src_head" in
106 H) ;; # already detached
107 *)
108         src_head="${src_head#H}"
109         for check in "${src_branches[@]}"; do
110                 case "$check" in
111                 "$src_head"=*)
112                         fail "would delete checked-out branch $src_head"
113                         ;;
114                 esac
115         done
116         ;;
117 esac
118
119
120 if [ "${#src_branches[@]}" = 0 ]; then
121         echo >&2 "git-branchmove: nothing to do"
122         exit 1
123 fi
124
125 dst_branches=( $(on_dst "$get_branches_rune") )
126 : "${dst_branches[@]}"
127
128
129 #----- check for nonequal overlaps -----
130
131 ok=true
132 for dst_check in "${dst_branches[@]}"; do
133         dst_ref="${dst_check%=*}"
134         for src_check in "${src_branches[@]}"; do
135                 case "$src_check" in
136                 "$dst_check")   ;;
137                 "$dst_ref"=*)
138                         ok=false
139                         echo >&2 "src: $src_check   dst: $dst_check"
140                         ;;
141                 esac
142         done
143 done
144
145 $ok || fail "would overwrite some destination branch(es)"
146
147
148 #----- do the transfer -----
149
150 refspecs=()
151 for src_xfer in "${src_branches[@]}"; do
152         src_ref="${src_xfer%=*}"
153         refspecs+=("$src_ref:$src_ref")
154 done
155
156 case "$op" in
157 put)    git push "$remote" "${refspecs[@]}"     ;;
158 get)    git fetch "$remote" "${refspecs[@]}"    ;;
159 *)      fail "unknown $op ???"                  ;;
160 esac
161
162
163 #----- delete the refs on the source -----
164
165 (
166         printf "%s\n" "$updatemsg"
167         for src_rm in "${src_branches[@]}"; do printf "%s\n" "$src_rm"; done
168 ) | on_src '
169         read updatemsg
170         while read src_rm; do
171                 src_ref="${src_rm%=*}"
172                 src_obj="${src_rm##*=}"
173                 git update-ref -m "$updatemsg" -d "$src_ref" "$src_obj"
174                 echo "move complete: $src_ref"
175         done
176 '
177
178 echo "moved ${#src_branches[@]} branches."