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         msg="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         msg="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 dst_branches=( $(on_dst "$get_branches_rune") )
120 : "${dst_branches[@]}"
121
122
123 #----- check for nonequal overlaps -----
124
125 ok=true
126 for dst_check in "${dst_branches[@]}"; do
127         dst_ref="${dst_check%=*}"
128         for src_check in "${src_branches[@]}"; do
129                 case "$src_check" in
130                 "$dst_check")   ;;
131                 "$dst_ref"=*)
132                         ok=false
133                         echo >&2 "src: $src_check   dst: $dst_check"
134                         ;;
135                 esac
136         done
137 done
138
139 $ok || fail "would overwrite some destination branch(es)"
140
141
142 #----- do the transfer -----
143
144 refspecs=()
145 for src_xfer in "${src_branches[@]}"; do
146         src_ref="${src_xfer%=*}"
147         refspecs+=("$src_ref:$src_ref")
148 done
149
150 case "$op" in
151 put)    git push "$remote" "${refspecs[@]}"     ;;
152 get)    git fetch "$remote" "${refspecs[@]}"    ;;
153 *)      fail "unknown $op ???"                  ;;
154 esac
155
156
157 #----- delete the refs on the source -----
158
159 (
160         printf "%s\n" "$message"
161         for src_rm in "${src_branches[@]}"; do printf "%s\n" "$src_rm"; done
162 ) | on_src '
163         read message
164         while read src_rm; do
165                 src_ref="${src_rm%=*}"
166                 src_obj="${src_rm##*=}"
167                 git update-ref -m "$message" -d "$src_ref" "$src_obj"
168                 echo "move complete: $src_ref"
169         done
170 '
171
172 echo 'moved ${#src_branches[@]} branches.'