diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md index 8c4e6ea8eabefddd0d1338b2380669482194331f..09210be70799629f788fc2d1e420c14d9adcf088 100644 --- a/doc/workflow/repository_mirroring.md +++ b/doc/workflow/repository_mirroring.md @@ -313,9 +313,82 @@ mitigated by reducing the mirroring delay by using a Push event webhook to trigger an immediate pull to GitLab. Push mirroring from GitLab is rate limited to once per minute when only push mirroring protected branches. -It may be possible to implement a locking mechanism using the server-side -`pre-receive` hook to prevent the race condition. Read about [configuring -custom Git hooks][hooks] on the GitLab server. +### Preventing conflicts using a `pre-receive` hook + +> **Warning:** The solution proposed will negatively impact the performance of +> Git push operations because they will be proxied to the upstream Git +> repository. + +A server-side `pre-receive` hook can be used to prevent the race condition +described above by only accepting the push after first pushing the commit to +the upstream Git repository. In this configuration one Git repository acts as +the authoritative upstream, and the other as downstream. The `pre-recieve` hook +will be installed on the downstream repository. + +Read about [configuring custom Git hooks][hooks] on the GitLab server. + +A sample `pre-recieve` hook is provided below. + +```bash +#!/usr/bin/env bash + +# --- Assume only one push mirror target +# Push mirroring remotes are named `remote_mirror_<id>`, this finds the first +# remote and uses that. +TARGET_REPO=$(git remote | grep -m 1 remote_mirror) + +proxy_push() +{ + # --- Arguments + OLDREV=$(git rev-parse $1) + NEWREV=$(git rev-parse $2) + REFNAME="$3" + + # --- TODO: only mirror protected branches + + case "$refname" in + refs/heads/*) + branch=$(expr "$refname" : "refs/heads/\(.*\)") + sandboxbranch=$(expr "$branch" : "\(sandbox/.*\)") + + if [ "$sandboxbranch" != "$branch" ]; then + error="$(git push --quiet $TARGET_REPO $NEWREV:$REFNAME 2>&1)" + fail=$? + + if [ "$fail" != "0" ]; then + echo >&2 "" + echo >&2 " Error: updates were rejected by upstream server" + echo >&2 " This is usually caused by another repository pushing changes" + echo >&2 " to the same ref. You may want to first integrate remote changes" + echo >&2 "" + return + fi + fi + ;; + refs/tags/*) + tag=$(expr "$refname" : "refs/tags/\(.*\)") + # --- TODO: handle tags + ;; + esac +} + +# Allow dual mode: run from the command line just like the update hook, or +# if no arguments are given then run as a hook script +if [ -n "$1" -a -n "$2" -a -n "$3" ]; then + # Output to the terminal in command line mode - if someone wanted to + # resend an email; they could redirect the output to sendmail + # themselves + PAGER= proxy_push $2 $3 $1 +else + # Push is proxied upstream one ref at a time. Because of this it is possible + # for some refs to succeed, and others to fail. This will result in a failed + # push. + while read oldrev newrev refname + do + proxy_push $oldrev $newrev $refname + done +fi +``` ### Mirroring with Perforce via GitFusion