f37 -> f39
[infrastructure.git] / scripts / git-mirror.sh
1 #!/bin/bash
2 COMMAND=$(basename $0)
3
4 # for sending emails (-a option)
5 ADMINS="Thierry.Parmentelat@inria.fr"
6 # the other end of the mirror (-r option)
7 REMOTE_GIT="git://git.planet-lab.org"
8 # options
9 QUIET=
10 VERBOSE=
11 FORCE=
12
13 # local path to the reference (bare) repos
14 MASTER_GIT="/git"
15 # local path to the working repos; can be trashed if needed
16 LOCAL_MIRROR_DIR="/git-mirror"
17 # a file in each repo to avoid too many notifications
18 NOTIFIED_FILE="NOTIFIED"
19 # global stamp to avoid multiple instances of this script
20 LOCKFILE=$LOCAL_MIRROR_DIR/LOCK
21 # global list - errors to report
22 FAILED_CMDS=()
23
24 function verbose () {
25     [ -n "$VERBOSE" ] && echo "--------------------" "$@"
26 }
27
28 function msg () {
29     [ -n "$QUIET" ] || echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "$@"
30 }
31
32 function failure () {
33     msg "Received signal - cleaning up $LOCKFILE and aborting"
34     rm -f $LOCKFILE
35     exit 1
36 }
37
38 function notify () {
39     local REPO_DIR=$1; shift
40     local SUBJECT=$1; shift
41     local BODY=$1; shift
42     verbose "notify" $SUBJECT
43
44     # already notified within a half day ?
45     GRACE=$((60*12))
46     is_recent=$(find $REPO_DIR/$NOTIFIED_FILE -mmin -$GRACE 2> /dev/null)
47     # not there or less than 12 our old: don't notify
48     if [ -n "$is_recent" ] ; then
49         verbose "skipping recent notification -- $SUBJECT"
50         return
51     fi
52
53     for admin in $ADMINS; do
54         echo -e "$BODY" | mail -s "$SUBJECT" $admin
55     done
56     touch $REPO_DIR/$NOTIFIED_FILE
57 }
58
59 function clear_notify () {
60     local REPO_DIR=$1; shift
61     rm -f $REPO_DIR/$NOTIFIED_FILE
62 }
63
64 # run directory command
65 #   -or- to ignore errors
66 # run -i directory "command"
67 function run () {
68     local IGNORE=""
69     case "$1" in -i) IGNORE=true ; shift ;; esac
70     local REPO_DIR=$1; shift
71     local COMMAND="$@"
72
73     pushd ${REPO_DIR} > /dev/null
74     # quiet mode: record stdout and err for the mail
75     ok=true
76     if [ -n "$QUIET" ] ; then
77         OUTPUT=$($COMMAND 2>&1) || ok=
78         TORECORD="[$REPO_DIR] $COMMAND\n$OUTPUT"
79     else
80         if [ -z "$IGNORE" ] ; then
81             echo "[$REPO_DIR] Running $COMMAND"
82             $COMMAND || ok=
83             TORECORD="$COMMAND"
84         else
85             echo "[$REPO_DIR] Running $COMMAND [ignored]"
86             $COMMAND >& /dev/null
87         fi
88     fi
89     
90     # failed ?
91     if [ -z "$ok" ]; then
92         # let's record the failure unless ignore is set
93         if [ -z "$IGNORE" ] ; then
94             FAILED_CMDS=("${FAILED_CMDS[@]}" "$TORECORD")
95         fi
96     fi
97     verbose "after run with $COMMAND" "ok=$ok" "#failed=${#FAILED_CMDS[@]}"
98     popd > /dev/null
99 }
100
101 function merge_all_branches () {
102     local REPO_DIR=$1; shift
103     local NAME=$1; shift
104     local REMOTE=$1; shift
105
106     pushd $REPO_DIR > /dev/null
107     BRANCHES=$(git branch -r | grep $REMOTE | grep -v HEAD | sed "s/.*\///g" | grep -v master)
108     HAS_MASTER=$(git branch -l | grep master)
109     popd > /dev/null
110
111     [ -n "$HAS_MASTER" ] && run ${REPO_DIR} git checkout master
112     [ -n "$HAS_MASTER" ] && run ${REPO_DIR} git merge --ff $REMOTE/master
113     for BRANCH in $BRANCHES ; do
114         run -i ${REPO_DIR} git branch $BRANCH $REMOTE/$BRANCH
115         run ${REPO_DIR} git checkout $BRANCH
116         run ${REPO_DIR} git merge --ff $REMOTE/$BRANCH
117     done
118 }
119
120 function push_all_branches () {
121     local REPO_DIR=$1; shift
122     local NAME=$1; shift
123     local PUSH_TO=$1; shift
124     local PUSH_FROM=$1; shift
125
126     pushd $REPO_DIR > /dev/null
127     BRANCHES=$(git branch -r | grep $PUSH_FROM | grep -v HEAD | sed "s/.*\///g" | grep -v master)
128     HAS_MASTER=$(git branch -l | grep master)
129     popd > /dev/null
130
131     [ -n "$HAS_MASTER" ] && run ${REPO_DIR} git push $PUSH_TO master:master
132     for BRANCH in $BRANCHES ; do
133         run ${REPO_DIR} git push $PUSH_TO $BRANCH:$BRANCH
134     done
135 }
136
137 function mirror_repo () {
138     local repo=$1; shift
139     FAILED_CMDS=()   # reset previous failure if any
140
141     NAME=$(basename ${repo} ".git")
142     GIT_NAME=${NAME}.git
143     REPO_DIR=${LOCAL_MIRROR_DIR}/${NAME}
144     REMOTE_REPO=${REMOTE_GIT}/${GIT_NAME}
145     MASTER_REPO=${MASTER_GIT}/${GIT_NAME}
146
147     # if the local master is a symlink (like /git/vserver-reference.git -> sliceref.git) 
148     # then skip it
149     # we use this for either aliases (like vserver-reference and sliceimage) or
150     # for repos managed in other locations (like /git-slave) but where
151     # the symlink is needed so they get served by git-daemon
152     [ -h ${MASTER_REPO} ] && return
153
154     # if there is no remote repository it may be that we only have
155     # the repository locally and don't need to mirror
156     git ls-remote $REMOTE_REPO &> /dev/null || return
157
158     if [ -d ${REPO_DIR} ] ; then
159         msg "pulling from ${NAME}"
160         run ${REPO_DIR} git fetch origin --tags
161         run ${REPO_DIR} git fetch origin
162         merge_all_branches $REPO_DIR $NAME origin
163         if [ -n "$FAILED_CMDS" ]; then
164             # format mail body
165             body="Can not fetch from ${MASTER_REPO}\n\n------------\n FAILED COMMANDS:\n"
166             for line in "${FAILED_CMDS[@]}"; do body="$body$line\n"; done
167             notify $REPO_DIR "git-mirror.sh failed to merge remote with module ${NAME}" "$body"
168             return
169         fi
170     else
171         msg "mirroring ${NAME} for the first time"
172         run ${LOCAL_MIRROR_DIR} git clone ${REMOTE_REPO}
173         run ${REPO_DIR} git remote add local_master ${MASTER_REPO}
174     fi
175
176     msg "pushing ${NAME} to local master"
177     run ${REPO_DIR} git fetch local_master --tags
178     run ${REPO_DIR} git fetch local_master
179     merge_all_branches $REPO_DIR $NAME local_master
180     if [ -n "$FAILED_CMDS" ]; then
181         pushd ${REPO_DIR} > /dev/null
182         STATUS_OUT=$(git status)
183         popd > /dev/null
184         # format mail body
185         body="STATUS in ${REPO_DIR}:\n${STATUS_OUT} \n\n------------\n FAILED COMMANDS:\n"
186         for line in "${FAILED_CMDS[@]}"; do body="$body$line\n"; done
187         notify $REPO_DIR "git-mirror.sh failed on with module ${NAME}" "$body"
188         return
189     fi
190     run ${REPO_DIR} git push --tags local_master
191     push_all_branches $REPO_DIR $NAME local_master origin 
192     if [ -n "$FAILED_CMDS" ]; then
193         # format mail body
194         body="FAILED COMMANDS:\n"
195         for line in "${FAILED_CMDS[@]}"; do body="$body$line\n"; done
196         notify $REPO_DIR "git-mirror.sh failed to push back with module ${NAME}" "$body"
197         return
198     fi
199     # success, remove previous check file if any
200     clear_notify $REPO_DIR
201     # touch a stamp so it's easier to figure out where/if things get stuck
202     touch ${REPO_DIR}/MIRRORED.stamp
203 }
204
205 function usage () {
206     echo "Usage $COMMAND [options] REPONAME*"
207     echo "  [-a admin-mails] : provide space-separated admins emails"
208     echo "  [-r remote-git-url] : e.g. -r git://git.onelab.eu/"
209     echo "  [-q] quiet mode for running under cron"
210     echo "  [-v] verbose mode"
211     echo "  [-f] force mode, runs even if the lock file is present"
212     echo "       see also manage-git-mirror.sh"
213     exit 1
214 }
215
216 while getopts "a:r:s:qvfh" opt; do
217   case $opt in
218       a) ADMINS=$OPTARG ;;
219       r) REMOTE_GIT=$OPTARG ;;
220       q) QUIET=true ;;
221       v) VERBOSE=true ;;
222       f) FORCE=true ;;
223       h) usage ;;
224       \?) echo "Invalid option: -$opt" >&2 ;;
225   esac
226 done
227 shift $((OPTIND-1))
228
229 # skip this if force is set
230 # the natural usage of force is, manage-git-mirror.sh touches the lock file
231 # which prevents the cron jobs from running during an hour
232 # then you can use with -f which leaves the lock file as it is
233 if [ -z "$FORCE" ] ; then
234     # is the stamp older than an hour ? 
235     GRACE=60
236     is_old=$(find $LOCKFILE -mmin +$GRACE 2> /dev/null)
237     if [ -n "$is_old" ] ; then
238         msg "$LOCKFILE is older than $GRACE minutes - removing"
239         rm -f $LOCKFILE
240     fi
241
242     if [ -f $LOCKFILE ] ; then
243         msg "Found $LOCKFILE - another git-mirror seems to be running. Aborting... " 
244         exit 1
245     fi
246 fi
247
248 trap failure INT
249
250 # if force is set we leave the lock file as is
251 [ -z "$FORCE" ] && date > $LOCKFILE
252 for gitrepo in "$@"; do mirror_repo $gitrepo ; done
253 [ -z "$FORCE" ] && rm -f $LOCKFILE