Skip to content

Commit

Permalink
Merge pull request #284 from stronk7/git_sync_branches
Browse files Browse the repository at this point in the history
New job to be able to keep two branches on sync
  • Loading branch information
stronk7 authored Oct 26, 2023
2 parents 00c925f + 9ce5d7a commit 77dd15a
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 0 deletions.
142 changes: 142 additions & 0 deletions git_sync_two_branches/git_sync_two_branches.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# WORKSPACE: Path to the workspace directory.
# $gitcmd: Path to git executable.
# $gitdir: Directory containing git repo.
# $gitremote: Remote name where the branches are located. Default: origin.
# $dryrun: If set to anything, the script will not perform any changes.
# $source: Source branch to sync from.
# $target: Target branch to sync to.

# Want exit on error.
set -e

# This script will sync two branches in a git repo given the target branch is an ancestor
# of the source branch. If the opposite is detected (source branch is an ancestor of the
# target branch), the script will exit with an error.
# Everything (compare, send changes...) to the "gitremote" remote, unconditionally.

# Verify everything is set
required="WORKSPACE gitdir gitcmd source target"
for var in $required; do
if [ -z "${!var}" ]; then
echo "Error: ${var} environment variable is not defined. See the script comments."
exit 1
fi
done

# Calculate some variables
mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
gitdir=${gitdir:-}
gitremote=${gitremote:-origin}
gitcmd=${gitcmd:-git}
source=${source:-master}
target=${target:-master}
dryrun=${dryrun:-}

# Load some functions
source "${mydir}/lib.sh"

# Verify that the git directory is valid and that the remote exits.
cd "${gitdir}"

if ! ${gitcmd} remote | grep -q "^${gitremote}$"; then
echo "Error: ${gitremote} is not a valid remote."
exit 1
fi

if ! ${gitcmd} rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: ${gitdir} is not a valid git directory."
exit 1
fi

# Get the url of the gitremote remote.
remote_url=$(${gitcmd} config --get "remote.${gitremote}.url")

# Verify that the source and target branches exist in the remote.
if ! ${gitcmd} ls-remote --exit-code --heads "${gitremote}" "${source}" > /dev/null 2>&1; then
echo "Error: ${source} branch does not exist in ${gitremote} remote (${remote_url})."
exit 1
fi

if ! ${gitcmd} ls-remote --exit-code --heads "${gitremote}" "${target}" > /dev/null 2>&1; then
echo "Error: ${target} branch does not exist in ${gitremote} remote (${remote_url})."
exit 1
fi

echo "Syncing ${gitremote} remote (${remote_url}): Set ${target} target branch to ${source} source branch..."

if [ -n "${dryrun}" ]; then
echo "Dry-run enabled, no changes will be applied to the ${target} branch in ${gitremote} (${remote_url})."
dryrun="DRY-RUN: "
fi

# Ensure that both the source and target branches exist locally, creating them if necessary.
if ! ${gitcmd} show-ref --verify --quiet "refs/heads/${source}"; then
echo "Creating local source branch ${source}..."
"${gitcmd}" fetch --quiet "${gitremote}" "${source}"
"${gitcmd}" branch --quiet --force "${source}" --track "${gitremote}/${source}"
else
echo "Updating local source branch ${source}..."
"${gitcmd}" fetch --quiet "${gitremote}" "${source}:${source}"
fi

if ! ${gitcmd} show-ref --verify --quiet "refs/heads/${target}"; then
echo "Creating local target branch ${target}..."
"${gitcmd}" fetch --quiet "${gitremote}" "${target}"
"${gitcmd}" branch --quiet --force "${target}" --track "${gitremote}/${target}"
else
echo "Updating local target branch ${target}..."
"${gitcmd}" fetch --quiet "${gitremote}" "${target}:${target}"
fi

# Let's calculate the commit that will be the HEAD at the end of the process. It's the one in the source branch.
commit_outcome=$(${gitcmd} rev-parse --short=16 "${source}")

# Verify if both branches are the same. If so, we are done.
if is_ancestor "${source}" "${target}" && is_ancestor "${target}" "${source}"; then
echo "Branches ${source} and ${target} are the same. Nothing to do. Current HEAD: ${commit_outcome}"
exit 0
fi

# Verify that the source branch is not an ancestor of the target branch.
if is_ancestor "${source}" "${target}"; then
echo "The target ${target} branch got new commits."
log "Error: target ${target} branch has some unexpected commits, not available in the source ${source} branch. Expected HEAD: ${commit_outcome}."
echo "Please, fix the target ${target} branch manually and try again."
exit 1
fi

# We are good to go with any of the remaining cases.

# Verify that the target branch is an ancestor of the source branch. If so, we can fast-forward.
if is_ancestor "${target}" "${source}"; then
echo "The source ${source} branch got new commits."
log "${dryrun}Fast-forwarding target branch ${target} to source branch ${source} at ${gitremote} remote (${remote_url}). New HEAD: ${commit_outcome}"
"${gitcmd}" fetch --quiet . "${source}:${target}"
# Self-assert that the operation happened.
new_commit=$(${gitcmd} rev-parse --short=16 "${target}")
if [[ "${new_commit}" != "${commit_outcome}" ]]; then
log "Error: fast-forwarding failed. Expected new HEAD: ${commit_outcome}. Actual new HEAD: ${new_commit}."
exit 1
fi
if [[ -z "${dryrun}" ]]; then
"${gitcmd}" push "${gitremote}" --quiet "${target}"
fi
exit 0
fi

# Arrived here, the source and target branches have diverged. We'll need to do a
# hard reset of the target branch to the source branch.
echo "Diverged branches (surely because of some rewrite in ${source})."
log "${dryrun}Hard-resetting target branch ${target} to source branch ${source} at ${gitremote} remote (${remote_url}). New HEAD: ${commit_outcome}"
"${gitcmd}" branch --quiet --force "${target}" "${source}"
# Self-assert that the operation happened.
new_commit=$(${gitcmd} rev-parse --short=16 "${target}")
if [[ "${new_commit}" != "${commit_outcome}" ]]; then
log "Error: hard-resetting failed. Expected new HEAD: ${commit_outcome}. Actual new HEAD: ${new_commit}."
exit 1
fi
if [[ -z "${dryrun}" ]]; then
"${gitcmd}" push "${gitremote}" --force --quiet "${target}"
fi
exit 0
30 changes: 30 additions & 0 deletions git_sync_two_branches/lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash

# Functions for git_sync_two_branches.sh

# Let's go strict (exit on error)
set -e

# Apply some defaults in case nobody defined them.
BUILD_NUMBER="${BUILD_NUMBER:-0}"
BUILD_TIMESTAMP="$(date +'%Y-%m-%d_%H-%M-%S')"
gitcmd="${gitcmd:-git}"
logfile="${WORKSPACE}/git_sync_two_branches.log"

# Utility function to check if a branch is an ancestor of another branch.
# Returns 0 if $1 is an ancestor of $2, 1 otherwise.
function is_ancestor() {
local branch1=$1
local branch2=$2
if ${gitcmd} merge-base --is-ancestor "${branch1}" "${branch2}"; then
return 0
else
return 1
fi
}

# Utility function to output something both to stdout and to a log file, with some extra information.
function log() {
echo "$1"
echo "$BUILD_NUMBER $BUILD_TIMESTAMP $1" >> "${logfile}"
}
106 changes: 106 additions & 0 deletions tests/3-git_sync_two_branches.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env bats

load libs/shared_setup

setup_file() {
# All these tests need a moodle git clone with a remote available, called "local_ci_tests"
# pointing to the https://git.in.moodle.com/integration/prechecker.git repository.
cd "${gitdir}"
git remote add local_ci_tests https://git.in.moodle.com/integration/prechecker.git
cd $OLDPWD

# Note that we'll be removing this custom remote at the end of the tests in this file.
}

teardown_file() {
# Remove the custom remote we added in setup_file.
cd "${gitdir}"
git remote remove local_ci_tests
cd $OLDPWD
}

setup() {
# Always perform dry runs when testing. We don't want to change the fixture branches.
cd "${gitdir}"
export dryrun=1

# Set the rest of env variables needed for the script.
export gitremote=local_ci_tests

# Let's checkout master, so we test everything without any local checkout of the branches.
git checkout master --quiet
cd $OLDPWD
}

teardown() {
# Remove the source and target local branches created for the test.
# (all them begin with local_ci_git_sync_ prefix).
cd "${gitdir}"
git branch --list 'local_ci_git_sync_*' | xargs -r git branch -D --quiet
cd $OLDPWD
}

@test "git_sync_two_branches: Both branches are in sync" {

export source=local_ci_git_sync_master
export target=local_ci_git_sync_main

run git_sync_two_branches/git_sync_two_branches.sh

# Assert result.
assert_success

assert_output --partial "Syncing local_ci_tests remote"
assert_output --partial "Set ${target} target branch to ${source} source branch..."
assert_output --partial "Dry-run enabled"
assert_output --partial "Creating local source branch ${source}"
assert_output --partial "Creating local target branch ${target}"
assert_output --partial "Branches ${source} and ${target} are the same"
assert_output --partial "Current HEAD: d76e211be6ae65fe"
}

@test "git_sync_two_branches: Both branches have diverged (reset)" {

export source=local_ci_git_sync_master_diverged
export target=local_ci_git_sync_main

run git_sync_two_branches/git_sync_two_branches.sh

# Assert result.
assert_success

assert_output --partial "Diverged branches (surely because of some rewrite"
assert_output --partial "Hard-resetting target branch ${target} to source branch ${source}"
assert_output --partial "New HEAD: 150d134fa2d519cd"
}

@test "git_sync_two_branches: There are new commits in source branch (fast forward)" {

export source=local_ci_git_sync_master_fast_forward
export target=local_ci_git_sync_main

run git_sync_two_branches/git_sync_two_branches.sh

# Assert result.
assert_success

assert_output --partial "The source ${source} branch got new commits."
assert_output --partial "Fast-forwarding target branch ${target} to source branch ${source}"
assert_output --partial "New HEAD: 5f3a55f6f01142d8"
}

@test "git_sync_two_branches: There are new commits in target branch (error)" {

export source=local_ci_git_sync_master
export target=local_ci_git_sync_main_advanced

run git_sync_two_branches/git_sync_two_branches.sh

# Assert result.
assert_failure

assert_output --partial "The target ${target} branch got new commits."
assert_output --partial "Error: target ${target} branch has some unexpected commits, not available in the source"
assert_output --partial "fix the target ${target} branch manually"
assert_output --partial "Expected HEAD: d76e211be6ae65fe"
}

0 comments on commit 77dd15a

Please sign in to comment.