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