chiark / gitweb /
changelog: finalise 6.0.4
[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         remotename="$remote"
39 esac
40
41 remote_spec="$(perl -e '
42     $_ = $ARGV[0];
43     if (m#^ssh://([^:/]+)(?:\:(\w+))?#) {
44         print "$'\''|ssh ";
45         print " -p $3" if $2;
46         print "$1\n";
47     } elsif (m#^([-+_.0-9a-zA-Z\@]+):(?!//|:)#) {
48         print "$'\''|ssh $1\n";
49     } elsif (m#^[/.]#) {
50         print "$_|sh -c $1\n";
51     } else {
52         die "git-branchmove: unsupported remote url \`$_'\''\n";
53     }
54 ' "$remoteurl")"
55
56 remote_path="${remote_spec%%|*}"
57 remote_rune="${remote_spec#*|}"
58
59 case $op in
60 get)
61         src_rune="$remote_rune"
62         src_path="$remote_path"
63         dst_rune="sh -c"
64         dst_path=.
65         updatemsg="git-branchmove: moved to $remote ($remoteurl)"
66         push_fetch=fetch
67         ;;
68 put)
69         dst_rune="$remote_rune"
70         dst_path="$remote_path"
71         src_rune="sh -c"
72         src_path=.
73         updatemsg="git-branchmove; moved to `hostname -f` by `whoami`"
74         push_fetch=push
75         ;;
76 esac
77
78 on_src () { $src_rune "set -e; cd $src_path; $*"; }
79 on_dst () { $dst_rune "set -e; cd $dst_path; $*"; }
80
81
82 #----- fetch the current refs from both sides -----
83
84 branch_pats=''
85 for branch_pat in "$@"; do
86         branch_pats+=" '[r]efs/heads/$branch_pat'"
87 done
88
89 get_branches_rune='
90         git for-each-ref --format="%(refname)=%(objectname)" '"$branch_pats"'
91 '
92
93 src_branches=( $(
94         on_src '
95                 printf H
96                 git symbolic-ref -q HEAD || test $? = 1
97                 echo " "
98                 '"$get_branches_rune"'
99         '       
100 ))
101
102 src_head="${src_branches[0]}"
103 unset src_branches[0]
104 : "${src_branches[@]}"
105
106 case "$src_head" in
107 H) ;; # already detached
108 *)
109         src_head="${src_head#H}"
110         for check in "${src_branches[@]}"; do
111                 case "$check" in
112                 "$src_head"=*)
113                         fail "would delete checked-out branch $src_head"
114                         ;;
115                 esac
116         done
117         ;;
118 esac
119
120
121 if [ "${#src_branches[@]}" = 0 ]; then
122         echo >&2 "git-branchmove: nothing to do"
123         exit 1
124 fi
125
126 dst_branches=( $(on_dst "$get_branches_rune") )
127 : "${dst_branches[@]}"
128
129
130 #----- check for nonequal overlaps -----
131
132 ok=true
133 for dst_check in "${dst_branches[@]}"; do
134         dst_ref="${dst_check%=*}"
135         for src_check in "${src_branches[@]}"; do
136                 case "$src_check" in
137                 "$dst_check")   ;;
138                 "$dst_ref"=*)
139                         ok=false
140                         echo >&2 "src: $src_check   dst: $dst_check"
141                         ;;
142                 esac
143         done
144 done
145
146 $ok || fail "would overwrite some destination branch(es)"
147
148
149 #----- do the transfer -----
150
151 refspecs=()
152 for src_xfer in "${src_branches[@]}"; do
153         src_ref="${src_xfer%=*}"
154         refspecs+=("$src_ref:$src_ref")
155 done
156
157 case "$op" in
158 put)    git push --no-follow-tags "$remote" "${refspecs[@]}"    ;;
159 get)    git fetch --no-tags "$remote" "${refspecs[@]}"  ;;
160 *)      fail "unknown $op ???"                  ;;
161 esac
162
163
164 #----- delete the refs on the source -----
165
166 (
167         printf "%s\n" "$updatemsg"
168         for src_rm in "${src_branches[@]}"; do printf "%s\n" "$src_rm"; done
169 ) | on_src '
170         read updatemsg
171         while read src_rm; do
172                 src_ref="${src_rm%=*}"
173                 src_obj="${src_rm##*=}"
174                 git update-ref -m "$updatemsg" -d "$src_ref" "$src_obj"
175                 echo "moved: $src_ref"
176         done
177 '
178
179 #----- update the remote tracking branches -----
180
181 if [ "x$remotename" != x ]; then
182         for src_rm in "${src_branches[@]}"; do
183                 src_ref="${src_rm%=*}"
184                 src_obj="${src_rm##*=}"
185
186                 case "$src_ref" in
187                 refs/heads/*) ;;
188                 *) continue ;;
189                 esac
190
191                 branch="${src_ref#refs/heads/}"
192                 track_ref="refs/remotes/$remotename/$branch"
193                 case $op in
194                 get)    git update-ref -d "$track_ref"  ;;
195                 put)    git update-ref "$track_ref" "$src_obj" ;;
196                 *)      fail "unknown $op ???"
197                 esac
198         done
199 fi
200
201 echo "git-repomove: moved ${#src_branches[@]} branches."