summaryrefslogblamecommitdiffstats
path: root/gitmirror.sh
blob: 0bb1df651c7f8b0266b105442f54db1f5ac5cf3b (plain) (tree)
1
2
3
4
5
6
7
8


                   




                                                              




















                                                           

                     



























                                                                              
                                                                                                     



               

                                       


              



                                                                   
 
                        










                                                                     
                                                    
                                                     











                                                  

                                                            









                                                  




                                                                              















                                                                               




















                                                                                           


                                                           


                                                                           

                                                            
        




                                          
#!/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