/
synchronize-git-svn.sh
executable file
·318 lines (251 loc) · 8.51 KB
/
synchronize-git-svn.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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#!/bin/bash
# IT IS VITALLY IMPORTANT THAT LINE ENDINGS ARE CONSISTENTLY SET IN THE
# MIRROR REPO -- AS IT DOES A MERGE AND MESSES THINGS UP OTHERWISE
#
# For a Windows-only project:
#
# git config core.eol crlf
# git config core.autocrlf false
# git config core.safecrlf true
# git config core.whitespace cr-at-eol
#
# Avoid problems with case-sensitive files:
#
# git config core.ignorecase true
# TODO:
#
# function handle_error()
# {
# if mail has not yet been sent
# create guard for $1
# echo "$1 failed, see $LOGFILE"
# else if mail has been sent once for this error (i.e. guard exists)
# update count for $1 guard to 2 or add marker
# echo remainder and say that no more notices
# will be echoed until the guard has been removed
# else don't echo anything
#
# exit 1
# }
TRIGGERED_BY=${1:-NONE}
set -u
SCRIPT_FULL_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
# ----------------------
# --- CONFIGURATION ----
# ----------------------
# Configuration is sourced from scriptname.config
CONFIGFILE="${SCRIPT_FULL_PATH}.config"
if [[ ! -r "$CONFIGFILE" ]]
then
echo "Config file $CONFIGFILE does not exist or is unreadable" >&2
exit 1
fi
. $CONFIGFILE
# ----------------------
# --- IMPLEMENTATION ---
# ----------------------
function error_exit()
{
local THECMD="$1"
local LOGFILE="$2"
echo "$THECMD FAILED" >> "$LOGFILE"
echo "$THECMD failed, see $LOGFILE" >&2
exit 1
}
function check_status()
{
LAST_STATUS=$?
local THECMD="$1"
local LOGFILE="$2"
echo "=>" >> "$LOGFILE"
[[ $LAST_STATUS = 0 ]] || error_exit "$THECMD" "$LOGFILE"
[[ $LAST_STATUS = 0 ]] && echo "$THECMD successful" >> "$LOGFILE"
echo "----------------------------------------------" >> "$LOGFILE"
}
function check_failure()
{
local THECMD="$1"
local LOGFILE="$2"
tail -6 "$LOGFILE" | grep 'error: git-svn died'
[[ $? = 0 ]] && error_exit "$THECMD (probably wrong SVN credentials)" "$LOGFILE"
tail -6 "$LOGFILE" | grep 'uthorization failed'
[[ $? = 0 ]] && error_exit "$THECMD (probably wrong SVN credentials)" "$LOGFILE"
}
function rotate_logs()
{
local LOGFILE="$1"
if [[ -e "$LOGFILE" ]]; then
for i in 3 2 1; do
FROM="${LOGFILE}.${i}"
TO="${LOGFILE}.$((i + 1))"
[[ -e "$FROM" ]] && cp "$FROM" "$TO"
done
cp "$LOGFILE" "${LOGFILE}.1"
fi
> "$LOGFILE"
}
function release_lock()
{
local LOCKDIR="$1"
for toplevel_dir in /*; do
if [[ "$LOCKDIR" = "$toplevel_dir" ]]; then
echo "Refusing to rm -rf $LOCKDIR" >&2
exit 1
fi
done
rm -rf "$LOCKDIR"
}
function acquire_lock()
{
# Inspired by http://wiki.bash-hackers.org/howto/mutex
# and http://wiki.grzegorz.wierzowiecki.pl/code:mutex-in-bash
local LOCKDIR="$1"
local PIDFILE="${LOCKDIR}/pid"
if mkdir "$LOCKDIR" &>/dev/null; then
# lock succeeded
# remove $LOCKDIR on exit
trap 'release_lock "$LOCKDIR"' EXIT \
|| { echo 'trap exit failed' >&2; exit 1; }
# will trigger the EXIT trap above by `exit`
trap 'echo "Sync script killed" >&2; exit 1' HUP INT QUIT TERM \
|| { echo 'trap killsignals failed' >&2; exit 1; }
echo "$$" >"$PIDFILE"
return 0
else
# lock failed, now check if the other PID is alive
OTHERPID="$(cat "$PIDFILE" 2>/dev/null)"
if [[ $? != 0 ]]; then
# PID file does not exists - propably direcotry
# is being deleted
return 1
fi
if ! kill -0 $OTHERPID &>/dev/null; then
# lock is stale, remove it and restart
echo "Stale lock in sync script" >&2
release_lock "$LOCKDIR"
acquire_lock "$LOCKDIR"
return $?
else
# lock is valid and OTHERPID is active - exit,
# we're locked!
return 1
fi
fi
}
function wait_for_lock()
{
local LOCKDIR="$1"
# For timeout:
# - local WAIT_TIMEOUT=120
# - add `&& [[ $i -lt $WAIT_TIMEOUT ]]` to while condition
# - add ((i++)) into while body
while ! acquire_lock "$LOCKDIR"; do
sleep 1
done
}
LAST_SVN_USER_FILE="${SCRIPT_FULL_PATH}.last_svn_user"
function set_subversion_user()
{
local AUTHOR_EMAIL="$1"
local SVN_URL="$2"
local LOGFILE="$3"
# cache last user to avoid unneccessary git-svn-auth-manager calls
[[ -e "$LAST_SVN_USER_FILE" ]] \
&& [[ "$(cat $LAST_SVN_USER_FILE)" == "$AUTHOR_EMAIL" ]] \
&& return
echo -n $AUTHOR_EMAIL > $LAST_SVN_USER_FILE
check_status "echo -n $AUTHOR_EMAIL > $LAST_SVN_USER_FILE" "$LOGFILE"
# reset SVN auth cache with git-svn-auth-manager
if ! ~/bin/git-svn-auth-manager \
-r "$AUTHOR_EMAIL" "$SVN_URL" &>> "$LOGFILE"
then
echo "PROBLEM WITH SVN OR WITH $AUTHOR_EMAIL SVN CREDENTIALS" >&2
false
fi
check_status "~/bin/git-svn-auth-manager -r $AUTHOR_EMAIL $SVN_URL" "$LOGFILE"
}
function synchronize_svn_bridge_and_central_repo()
{
local BRIDGE_REPO_PATH="$1"
local LOGFILE="$2"
pushd "$BRIDGE_REPO_PATH" > /dev/null
check_status "pushd $BRIDGE_REPO_PATH" "$LOGFILE"
# GIT_DIR (and possibly GIT_WORK_TREE) have to be unset,
# otherwise the script will not work from post-update hook
# see http://serverfault.com/questions/107608/git-post-receive-hook-with-git-pull-failed-to-find-a-valid-git-directory/107703#107703
unset $(git rev-parse --local-env-vars)
# get new SVN changes
local AUTHORS_PROG="$HOME/bin/git-svn-auth-manager"
git svn --authors-prog="$AUTHORS_PROG" fetch &>> "$LOGFILE"
check_status "git svn --authors-prog=$AUTHORS_PROG fetch" "$LOGFILE"
check_failure "git svn --authors-prog=$AUTHORS_PROG fetch" "$LOGFILE"
# get new git changes
git checkout master &>> "$LOGFILE"
check_status "git checkout master" "$LOGFILE"
git pull --rebase git-central-repo master &>> "$LOGFILE"
check_status "git pull --rebase git-central-repo master" "$LOGFILE"
# store the SVN URL and author of the last commit for `git svn dcommit`
local SVN_URL=`git svn info --url`
check_status "git svn info --url" "$LOGFILE"
local AUTHOR_EMAIL=`git log -n 1 --format='%ae'`
check_status "git log -n 1 --format='%ae'" "$LOGFILE"
echo "Using SVN URL '$SVN_URL' and author email '$AUTHOR_EMAIL'" >> "$LOGFILE"
# checkout detached head
git checkout svn/git-svn &>> "$LOGFILE"
check_status "git checkout svn/git-svn" "$LOGFILE"
# create a merged log message for the merge commit to SVN
# => note that we squash log messages together for merge commits
# => experiment with the fomat, e.g. '%ai | [%an] %s' etc
local MESSAGE=`git log --pretty=format:'%ai | %B [%an]' HEAD..master`
check_status "git log --pretty=format:'%ai | %B [%an]' HEAD..master" "$LOGFILE"
# merge changes from master to the SVN-tracking branch and commit to SVN
# => note that we always record the merge with --no-ff
git merge --no-ff --no-log -m "$MESSAGE" master &>> $LOGFILE
check_status 'git merge --no-ff --no-log -m $MESSAGE master' $LOGFILE
# set the SVN user and commit changes to SVN
set_subversion_user "$AUTHOR_EMAIL" "$SVN_URL" "$LOGFILE"
git svn --authors-prog="$AUTHORS_PROG" dcommit &>> "$LOGFILE"
check_status "git svn --authors-prog=$AUTHORS_PROG dcommit" "$LOGFILE"
# merge changes from the SVN-tracking branch back to master
git checkout master &>> "$LOGFILE"
check_status "git checkout master" "$LOGFILE"
git merge svn/git-svn &>> "$LOGFILE"
check_status "git merge svn/git-svn" "$LOGFILE"
# fetch changes to central repo master from SVN bridge master
# (note that cannot just `git push git-central-repo master`
# as that would trigger the central repo update hook and deadlock)
local CENTRAL_REPO_PATH="`git remote -v show | awk 'NR > 1 { exit }; { print $2 };'`"
pushd "$CENTRAL_REPO_PATH" >/dev/null
check_status "pushd $CENTRAL_REPO_PATH" "$LOGFILE"
git fetch svn-bridge master:master &>> "$LOGFILE"
check_status "git fetch svn-bridge master:master" "$LOGFILE"
popd >/dev/null
popd >/dev/null
}
LOGFILE="${SCRIPT_FULL_PATH}.log"
LOCKDIR="${SCRIPT_FULL_PATH}.lock"
wait_for_lock "$LOCKDIR"
rotate_logs "$LOGFILE"
if [[ "$TRIGGERED_BY" != "NONE" ]]
then
echo "..................................................." >> "$LOGFILE"
echo "Triggered by $TRIGGERED_BY" >> "$LOGFILE"
echo "..................................................." >> "$LOGFILE"
fi
for repo in ${REPOS[@]}
do
echo -e "______________________________________________\n" >> "$LOGFILE"
if [[ -d "$repo" && -d "$repo/.git" ]] && \
grep -qFx '[svn-remote "svn"]' "$repo/.git/config"
then
echo "Synchronizing repo '$repo'" >> "$LOGFILE"
echo "Start: `date`" >> "$LOGFILE"
echo -e "......................................\n" >> "$LOGFILE"
synchronize_svn_bridge_and_central_repo "$repo" "$LOGFILE"
echo "End: `date`" >> "$LOGFILE"
else
echo "Repo '$repo' does not exist or is not a git-svn repo" >&2
echo "Repo '$repo' does not exist or is not a git-svn repo" >> "$LOGFILE"
fi
echo -e "______________________________________________\n" >> "$LOGFILE"
done