again
[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 baris@metin.org"
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     popd > /dev/null
109
110     run ${REPO_DIR} git checkout master
111     run ${REPO_DIR} git merge --ff $REMOTE/master
112     for BRANCH in $BRANCHES ; do
113         run -i ${REPO_DIR} git branch $BRANCH $REMOTE/$BRANCH
114         run ${REPO_DIR} git checkout $BRANCH
115         run ${REPO_DIR} git merge --ff $REMOTE/$BRANCH
116     done
117 }
118
119 function push_all_branches () {
120     local REPO_DIR=$1; shift
121     local NAME=$1; shift
122     local PUSH_TO=$1; shift
123     local PUSH_FROM=$1; shift
124
125     pushd $REPO_DIR > /dev/null
126     BRANCHES=$(git branch -r | grep $PUSH_FROM | grep -v HEAD | sed "s/.*\///g" | grep -v master)
127     popd > /dev/null
128
129     run ${REPO_DIR} git push $PUSH_TO master:master
130     for BRANCH in $BRANCHES ; do
131         run ${REPO_DIR} git push $PUSH_TO $BRANCH:$BRANCH
132     done
133 }
134
135 function mirror_repo () {
136     local repo=$1; shift
137     FAILED_CMDS=()   # reset previous failure if any
138
139     NAME=$(basename ${repo} ".git")
140     GIT_NAME=${NAME}.git
141     REPO_DIR=${LOCAL_MIRROR_DIR}/${NAME}
142     REMOTE_REPO=${REMOTE_GIT}/${GIT_NAME}
143     MASTER_REPO=${MASTER_GIT}/${GIT_NAME}
144
145     # if there is no remote repository it may be that we only have
146     # the repository locally and don't need to mirror
147     git ls-remote $REMOTE_REPO &> /dev/null || return
148
149     if [ -d ${REPO_DIR} ] ; then
150         msg "pulling from ${NAME}"
151         run ${REPO_DIR} git fetch origin --tags
152         run ${REPO_DIR} git fetch origin
153         merge_all_branches $REPO_DIR $NAME origin
154         if [ -n "$FAILED_CMDS" ]; then
155             # format mail body
156             body="Can not fetch from ${MASTER_REPO}\n\n------------\n FAILED COMMANDS:\n"
157             for line in "${FAILED_CMDS[@]}"; do body="$body$line\n"; done
158             notify $REPO_DIR "git-mirror.sh failed to merge remote with module ${NAME}" "$body"
159             return
160         fi
161     else
162         msg "mirroring ${NAME} for the first time"
163         run ${LOCAL_MIRROR_DIR} git clone ${REMOTE_REPO}
164         run ${REPO_DIR} git remote add local_master ${MASTER_REPO}
165     fi
166
167     msg "pushing ${NAME} to local master"
168     run ${REPO_DIR} git fetch local_master --tags
169     run ${REPO_DIR} git fetch local_master
170     merge_all_branches $REPO_DIR $NAME local_master
171     if [ -n "$FAILED_CMDS" ]; then
172         pushd ${REPO_DIR} > /dev/null
173         STATUS_OUT=$(git status)
174         popd > /dev/null
175         # format mail body
176         body="STATUS in ${REPO_DIR}:\n${STATUS_OUT} \n\n------------\n FAILED COMMANDS:\n"
177         for line in "${FAILED_CMDS[@]}"; do body="$body$line\n"; done
178         notify $REPO_DIR "git-mirror.sh failed on with module ${NAME}" "$body"
179         return
180     fi
181     run ${REPO_DIR} git push --tags local_master
182     push_all_branches $REPO_DIR $NAME local_master origin 
183     if [ -n "$FAILED_CMDS" ]; then
184         # format mail body
185         body="FAILED COMMANDS:\n"
186         for line in "${FAILED_CMDS[@]}"; do body="$body$line\n"; done
187         notify $REPO_DIR "git-mirror.sh failed to push back with module ${NAME}" "$body"
188         return
189     fi
190     # success, remove previous check file if any
191     clear_notify $REPO_DIR
192 }
193
194 function usage () {
195     echo "Usage $COMMAND [options] REPONAME*"
196     echo "  [-a admin-mails] : provide space-separated admins emails"
197     echo "  [-r remote-git-url] : e.g. -r git://git.onelab.eu/"
198     echo "  [-q] quiet mode for running under cron"
199     echo "  [-v] verbose mode"
200     echo "  [-f] force mode, runs even if the lock file is present"
201     echo "       see also manage-git-mirror.sh"
202     exit 1
203 }
204
205 while getopts "a:r:qvfh" opt; do
206   case $opt in
207       a) ADMINS=$OPTARG ;;
208       r) REMOTE_GIT=$OPTARG ;;
209       q) QUIET=true ;;
210       v) VERBOSE=true ;;
211       f) FORCE=true ;;
212       h) usage ;;
213       \?) echo "Invalid option: -$opt" >&2 ;;
214   esac
215 done
216 shift $((OPTIND-1))
217
218 # skip this if force is set
219 # the natural usage of force is, manage-git-mirror.sh touches the lock file
220 # which prevents the cron jobs from running during an hour
221 # then you can use with -f which leaves the lock file as it is
222 if [ -z "$FORCE" ] ; then
223     # is the stamp older than an hour ? 
224     GRACE=60
225     is_old=$(find $LOCKFILE -mmin +$GRACE 2> /dev/null)
226     if [ -n "$is_old" ] ; then
227         msg "$LOCKFILE is older than $GRACE minutes - removing"
228         rm -f $LOCKFILE
229     fi
230
231     if [ -f $LOCKFILE ] ; then
232         msg "Found $LOCKFILE - another git-mirror seems to be running. Aborting... " 
233         exit 1
234     fi
235 fi
236
237 trap failure INT
238
239 # if force is set we leave the lock file as is
240 [ -z "$FORCE" ] && date > $LOCKFILE
241 for gitrepo in "$@"; do mirror_repo $gitrepo ; done
242 [ -z "$FORCE" ] && rm -f $LOCKFILE