-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #284 from stronk7/git_sync_branches
New job to be able to keep two branches on sync
- Loading branch information
Showing
3 changed files
with
278 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |