#!/usr/bin/env bash set -Eeuo pipefail REPOSITORY_PATH_FORMAT="/var/lib/gitolite/repositories/%s.git" MERGE_JOB_FORMAT="%s-merge-patch" JENKINS_SSH_PORT=38844 JENKINS_SSH_HOST="localhost" msg() { echo "==> $1" } read_branches() { local prefix="refs/$1/" local -n read_branches_target="$2" local refname local objname local -i name_length local -i max_length=0 while IFS=$'\t' read -r refname objname; do local branch_name="${refname#"${prefix}"}" read_branches_target["${branch_name}"]="${objname}" name_length=${#branch_name} if (( name_length > max_length )); then max_length=$name_length fi done < <(git for-each-ref "${prefix}**" \ --format=$'%(refname)\t%(objectname)' \ --color=never) local branch_name for branch_name in "${!read_branches_target[@]}"; do printf " %*s = %s\n" \ $((-max_length)) \ "${branch_name}" \ "${read_branches_target["${branch_name}"]}" done } get_or_empty() { local -n get_or_empty_array="$1" local key="$2" local -n get_or_empty_target="$3" if [[ -v get_or_empty_array["${key}"] ]]; then get_or_empty_target="${get_or_empty_array["${key}"]}" else # shellcheck disable=SC2034 get_or_empty_target='' fi } archive_if_needed() { local archive_prefix="$1" local branch_name="$2" local old_tip="$3" local new_tip="$4" if ! git merge-base --is-ancestor "${old_tip}" "${new_tip}"; then local archive_name="${archive_prefix}${branch_name}" msg "Refusing to clobber ${branch_name}, archiving as ${archive_name}" git tag -a -m "Archive before synchronization with ${new_tip}" "${archive_name}" "${old_tip}" fi } sync_mirror() { if (( $# != 1 )); then echo "Usage: $0 REPOSITORY" >&2 exit 1 fi local project="$1" local repository # shellcheck disable=2059 repository="$(printf "${REPOSITORY_PATH_FORMAT}" "${project}")" local archive_prefix archive_prefix="$(TZ='UTC' printf 'archive/%(%Y%m%dT%H%M%S)TZ/')" cd "${repository}" local remote remote="$(git config --get gitmirror.remoteName)" local remote_url remote_url="$(git config --get gitmirror.remoteUrl)" if [[ -z "${remote}" || -z "${remote_url}" ]]; then msg "FATAL: ${repository} not configured for mirroring" >&2 exit 2 fi msg "Synchronizing ${repository} with ${remote}" git config "remote.${remote}.url" "${remote_url}" msg "Remote tracking branches before fetch" # shellcheck disable=SC2034 local -A old_tracking read_branches "remotes/${remote}" old_tracking msg "Local branches" # shellcheck disable=SC2034 local -A local_branches read_branches "heads" local_branches msg "Fetch from ${remote}" git fetch --tags --force --progress -- "${remote_url}" \ "+refs/heads/*:refs/remotes/${remote}/*" msg "Remote tracking branches after fetch" local -A new_tracking read_branches "remotes/${remote}" new_tracking local -a to_patch=() local branch_name for branch_name in "${!new_tracking[@]}"; do if [[ "${branch_name}" =~ ^patch-for/.* ]]; then msg "FATAL: Refusing to pull patch ${branch_name} from remote" >&2 exit 2 fi local new_tip="${new_tracking["${branch_name}"]}" local old_tip get_or_empty old_tracking "${branch_name}" old_tip local local_tip get_or_empty local_branches "${branch_name}" local_tip local patch_tip get_or_empty local_branches "patch-for/${branch_name}" patch_tip if [[ -n "${old_tip}" ]]; then archive_if_needed "${archive_prefix}" "${remote}/${branch_name}" \ "${old_tip}" "${new_tip}" fi if [[ -z "${local_tip}" ]]; then msg "Create local branch ${branch_name} at ${new_tip}" git branch --no-track "${branch_name}" "${patch_tip:-"${new_tip}"}" if [[ -n "${patch_tip}" ]]; then msg "Will patch newly created local branch ${branch_name}" to_patch+=("${branch_name}") fi else if [[ -z "${patch_tip}" ]]; then if [[ "${local_tip}" != "${new_tip}" ]]; then archive_if_needed "${archive_prefix}" "${branch_name}" \ "${local_tip}" "${old_tip:-"${new_tip}"}" msg "Update local branch ${branch_name} to ${new_tip}" git update-ref "refs/heads/${branch_name}" "${new_tip}" "${local_tip}" fi else if ! git merge-base --is-ancestor "${new_tip}" "${local_tip}"; then msg "Will update local branch ${branch_name} to ${new_tip} by patching" to_patch+=("${branch_name}") fi fi fi done local job_name # shellcheck disable=2059 job_name="$(printf "${MERGE_JOB_FORMAT}" "${project}")" for branch_name in "${to_patch[@]}"; do msg "Triggering Jenkings job ${job_name} for branch ${branch_name}" ssh -p "${JENKINS_SSH_PORT}" "${JENKINS_SSH_HOST}" \ build "${job_name}" -p "BRANCH=${branch_name}" done } if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then sync_mirror "$@" fi