|
| 1 | +#!/bin/sh |
| 2 | + |
| 3 | +# This script wants to synchronize multiple public Git repositories with each |
| 4 | +# other |
| 5 | +# |
| 6 | +# Use it (e.g. in a Jenkins job) like this: |
| 7 | +# |
| 8 | +# $0 <Git-URL>... |
| 9 | +# |
| 10 | +# where Git-URL is either a push URL, or a pair of a fetch and a push URL |
| 11 | +# separated by an equal sign. |
| 12 | +# |
| 13 | +# Example: |
| 14 | +# |
| 15 | +# git-synchronizer.sh \ |
| 16 | +# git://fiji.sc/imglib.git=fiji.sc:/srv/git/imglib.git \ |
| 17 | +# git://github.com/imagej/imglib=github.com:imagej/imglib |
| 18 | + |
| 19 | +errors= |
| 20 | +add_error () { |
| 21 | + errors="$(printf "%s\n\n%s\n\n" "$errors" "$*")" |
| 22 | +} |
| 23 | + |
| 24 | +url2remotename () { |
| 25 | + echo "${1%=*}" | |
| 26 | + sed 's/[^-A-Za-z0-9._]/_/g' |
| 27 | +} |
| 28 | + |
| 29 | +nullsha1=0000000000000000000000000000000000000000 |
| 30 | +find_deleted () { |
| 31 | + printf '%s\n%s\n%s\n' "$1" "$2" "$2" | |
| 32 | + sort -k 3 | |
| 33 | + uniq -u -f 2 | |
| 34 | + sed "s/^.\{40\}/$nullsha1/" |
| 35 | +} |
| 36 | + |
| 37 | +find_modified () { |
| 38 | + printf '%s\n%s\n' "$2" "$1" | |
| 39 | + sort -s -k 3 | |
| 40 | + uniq -u | |
| 41 | + uniq -d -f 2 | |
| 42 | + uniq -f 2 |
| 43 | +} |
| 44 | + |
| 45 | +find_new () { |
| 46 | + printf '%s\n%s\n%s\n' "$1" "$1" "$2" | |
| 47 | + sort -k 3 | |
| 48 | + uniq -u -f 2 |
| 49 | +} |
| 50 | + |
| 51 | +get_remote_branches () { |
| 52 | + name="$1" |
| 53 | + |
| 54 | + git for-each-ref refs/remotes/$name/\* | |
| 55 | + sed "s|\trefs/remotes/$name/|\t|" |
| 56 | +} |
| 57 | + |
| 58 | +fetch_from () { |
| 59 | + url="${1%=*}" |
| 60 | + pushurl="${1#*=}" |
| 61 | + name="$(url2remotename "$url")" |
| 62 | + |
| 63 | + if test "$url" != "$(git config remote.$name.url 2> /dev/null)" |
| 64 | + then |
| 65 | + git remote add $name $url >&2 || { |
| 66 | + add_error "Could not add remote $name ($url)" |
| 67 | + return 1 |
| 68 | + } |
| 69 | + test -n "$pushurl" && |
| 70 | + test "$pushurl" != "$url" && |
| 71 | + git config remote.$name.pushURL "$pushurl" |
| 72 | + fi |
| 73 | + previous="$(get_remote_branches $name)" |
| 74 | + git fetch --prune $name >&2 || { |
| 75 | + add_error "Could not fetch $name" |
| 76 | + return 1 |
| 77 | + } |
| 78 | + current="$(get_remote_branches $name)" |
| 79 | + |
| 80 | + find_deleted "$previous" "$current" |
| 81 | + |
| 82 | + # force modified branches |
| 83 | + find_modified "$previous" "$current" | |
| 84 | + sed 's/^/+/' |
| 85 | + |
| 86 | + find_new "$previous" "$current" |
| 87 | +} |
| 88 | + |
| 89 | +has_spaces () { |
| 90 | + test $# -gt 1 |
| 91 | +} |
| 92 | + |
| 93 | +get_common_fast_forward () { |
| 94 | + test $# -le 1 && { |
| 95 | + echo "$*" |
| 96 | + return |
| 97 | + } |
| 98 | + head= |
| 99 | + while test $# -gt 0 |
| 100 | + do |
| 101 | + commit=$1 |
| 102 | + shift |
| 103 | + test -z "$(eval git rev-list --no-walk ^$commit $head $*)" && { |
| 104 | + echo $commit |
| 105 | + return |
| 106 | + } |
| 107 | + head="$head $commit" |
| 108 | + done |
| 109 | + echo $head |
| 110 | +} |
| 111 | + |
| 112 | +# Parameter check |
| 113 | + |
| 114 | +test $# -lt 2 && { |
| 115 | + echo "Usage: $0 <Git-URL>[=<push-URL>] <Git-URL>[=<push-URL>]..." >&2 |
| 116 | + exit 1 |
| 117 | +} |
| 118 | + |
| 119 | +test -d .git || |
| 120 | +git init || |
| 121 | +exit |
| 122 | + |
| 123 | +# Fetch |
| 124 | + |
| 125 | +todo= |
| 126 | +for urlpair |
| 127 | +do |
| 128 | + url="${urlpair%=*}" |
| 129 | + has_spaces $url && { |
| 130 | + add_error "Error: Ignoring URL with spaces: $url" |
| 131 | + continue |
| 132 | + } |
| 133 | + |
| 134 | + echo "Getting updates from $url..." |
| 135 | + thistodo="$(fetch_from $urlpair)" || { |
| 136 | + add_error "$thistodo" |
| 137 | + continue |
| 138 | + } |
| 139 | + test -z "$thistodo" && continue |
| 140 | + printf "Updates from $url:\n%s\n" "$thistodo" |
| 141 | + todo="$(printf "%s\n%s\n" "$todo" "$thistodo")" |
| 142 | +done |
| 143 | + |
| 144 | +remote_branches="$(for url |
| 145 | +do |
| 146 | + url="${url%=*}" |
| 147 | + has_spaces $url && continue |
| 148 | + name=$(url2remotename $url) |
| 149 | + git for-each-ref refs/remotes/$name/\* | |
| 150 | + sed "s|^\(.*\)\trefs/remotes/\($name\)/|\2 \1 |" |
| 151 | +done)" |
| 152 | + |
| 153 | +for ref in $(echo "$remote_branches" | |
| 154 | + sed 's/.* //' | |
| 155 | + sort | |
| 156 | + uniq) |
| 157 | +do |
| 158 | + echo "$todo" | grep " $ref$" > /dev/null 2>&1 && continue |
| 159 | + sha1="$(echo "$remote_branches" | |
| 160 | + sed -n "s|^[^ ]* \([^ ]*\) [^ ]* $ref$|\1|p" | |
| 161 | + sort | |
| 162 | + uniq)" |
| 163 | + sha1=$(eval get_common_fast_forward $sha1) |
| 164 | + case "$sha1" in |
| 165 | + *\ *) |
| 166 | + add_error "$(printf "Ref $ref is diverging:\n%s\n\n" "$(echo "$remote_branches" | |
| 167 | + grep " $ref$")")" |
| 168 | + continue |
| 169 | + ;; |
| 170 | + *) |
| 171 | + |
| 172 | + if test $# = $(echo "$remote_branches" | |
| 173 | + grep "$sha1 [^ ]* $ref$" | |
| 174 | + wc -l) |
| 175 | + then |
| 176 | + # all refs agree on one sha1 |
| 177 | + continue |
| 178 | + fi |
| 179 | + ;; |
| 180 | + esac |
| 181 | + echo "Need to fast-forward $ref to $sha1" |
| 182 | + todo="$(printf "%s\n%s\n" "$todo" "$sha1 commit $ref")" |
| 183 | +done |
| 184 | + |
| 185 | +# Verify |
| 186 | + |
| 187 | +# normalize todo |
| 188 | + |
| 189 | +todo="$(echo "$todo" | |
| 190 | + sort -k 3 | |
| 191 | + uniq | |
| 192 | + grep -v '^$')" |
| 193 | + |
| 194 | +# test for disagreeing updates |
| 195 | + |
| 196 | +refs=$(echo "$todo" | |
| 197 | + sed 's/^[^ ]* [^ ]* //' | |
| 198 | + sort | |
| 199 | + uniq -d) |
| 200 | +for ref in $refs |
| 201 | +do |
| 202 | + sha1=$(echo "$todo" | |
| 203 | + sed -n "s|^\([^ ]*\) [^ ]* $ref$|\1|p") |
| 204 | + sha1=$(get_common_fast_forward $sha1) |
| 205 | + has_spaces $sha1 || |
| 206 | + todo="$(echo "$todo" | |
| 207 | + sed "s|^[^ ]* \([^ ]* $ref\)$|$sha1 \1|" | |
| 208 | + uniq)" |
| 209 | +done |
| 210 | + |
| 211 | +disagreeing="$(echo "$todo" | |
| 212 | + sort -k 3 | |
| 213 | + uniq -D -f 2)" |
| 214 | + |
| 215 | +if test -n "$disagreeing" |
| 216 | +then |
| 217 | + add_error "$(printf "Incompatible updates:\n%s\n\n" "$disagreeing")" |
| 218 | +fi |
| 219 | + |
| 220 | +# Push |
| 221 | + |
| 222 | +test -z "$todo" || |
| 223 | +for url |
| 224 | +do |
| 225 | + url="${url%=*}" |
| 226 | + has_spaces $url && continue |
| 227 | + name="$(url2remotename $url)" |
| 228 | + pushopts=$(echo "$todo" | |
| 229 | + while read sha1 type ref |
| 230 | + do |
| 231 | + test -z "$sha1" && continue |
| 232 | + if echo "$disagreeing" | grep " $ref$" > /dev/null 2>&1 |
| 233 | + then |
| 234 | + continue |
| 235 | + fi |
| 236 | + remoteref=refs/remotes/$name/$ref |
| 237 | + if test $sha1 = $nullsha1 |
| 238 | + then |
| 239 | + # to delete |
| 240 | + if git rev-parse $remoteref > /dev/null 2>&1 |
| 241 | + then |
| 242 | + echo ":refs/heads/$ref" |
| 243 | + fi |
| 244 | + else |
| 245 | + if test ${sha1#+} != "$(git rev-parse $remoteref 2> /dev/null)" |
| 246 | + then |
| 247 | + echo "$sha1:refs/heads/$ref" |
| 248 | + fi |
| 249 | + fi |
| 250 | + done) |
| 251 | + test -z "$pushopts" && continue |
| 252 | + git push $name $pushopts || |
| 253 | + add_error "Could not push to $url" |
| 254 | +done |
| 255 | +
|
| 256 | +# Maybe error out |
| 257 | +
|
| 258 | +test -z "$errors" || { |
| 259 | + printf "\n\nErrors:\n%s\n" "$errors" >&2 |
| 260 | + exit 1 |
| 261 | +} |
0 commit comments