diff --git a/contrib/CMakeLists.txt b/contrib/CMakeLists.txt index 707104f01e..7a8d384b57 100644 --- a/contrib/CMakeLists.txt +++ b/contrib/CMakeLists.txt @@ -1,23 +1,24 @@ +add_subdirectory(arcanist) add_subdirectory(devtools) include(PackageHelper) exclude_from_source_package( # Subdirectories "debian/" "gitian/" "gitian-descriptors/" "gitian-signing/" "qos/" "seeds/" "teamcity/" "testgen/" # FIXME Can be packaged once it gets updated to work with Bitcoin ABC "verifybinaries/" "zmq/" # Files "bitcoin-qt.pro" "gitian-build.py" "README.md" "valgrind.supp" ) diff --git a/contrib/arcanist/CMakeLists.txt b/contrib/arcanist/CMakeLists.txt new file mode 100644 index 0000000000..0f118a34c9 --- /dev/null +++ b/contrib/arcanist/CMakeLists.txt @@ -0,0 +1,23 @@ +# Copyright (c) 2019 The Bitcoin developers + +add_custom_target(check-arcanist) + +find_program(BASH_EXECUTABLE bash) + +set(ARCANIST_TESTS + ./test/test-autopatch.sh +) + +foreach(TEST ${ARCANIST_TESTS}) + get_filename_component(FILENAME ${TEST} NAME) + set(TESTNAME "check-arcanist-${FILENAME}") + add_custom_target(${TESTNAME} + COMMAND + "${BASH_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/${TEST}" + ) + add_dependencies(check-arcanist ${TESTNAME}) +endforeach() + +add_dependencies(check check-arcanist) +add_dependencies(check-all check-arcanist) diff --git a/contrib/arcanist/autopatch.sh b/contrib/arcanist/autopatch.sh new file mode 100755 index 0000000000..579b9ff31f --- /dev/null +++ b/contrib/arcanist/autopatch.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C.UTF-8 + +set -euxo pipefail + +DEFAULT_PATCH_ARGS="--skip-dependencies" +DEFAULT_REMOTE="origin" +DEFAULT_BRANCH="master" + +help_message() { + set +x + echo "Apply a patch from Phabricator cleanly on latest master." + echo "" + echo "Options:" + echo "-b, --branch The git branch to fetch and rebase onto. Default: '${DEFAULT_BRANCH}'" + echo "-h, --help Display this help message." + echo "-o, --remote The git remote to fetch latest from. Default: '${DEFAULT_REMOTE}'" + echo "-p, --patch-args Args to pass to 'arc patch'. Default: '${DEFAULT_PATCH_ARGS}'" + echo "-r, --revision The Differential revision ID used in Phabricator that you want to land. (ex: D1234)" + echo " This argument is required if --patch-args does not specify a revision or diff ID." + set -x +} + +BRANCH="${DEFAULT_BRANCH}" +PATCH_ARGS="${DEFAULT_PATCH_ARGS}" +REMOTE="${DEFAULT_REMOTE}" +REVISION="" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do +case $1 in + -b|--branch) + BRANCH="$2" + shift # shift past argument + shift # shift past value + ;; + -h|--help) + help_message + exit 0 + shift # shift past argument + ;; + -o|--remote) + REMOTE="$2" + shift # shift past argument + shift # shift past value + ;; + -p|--patch-args) + PATCH_ARGS="$2" + shift # shift past argument + shift # shift past value + ;; + -r|--revision) + REVISION="$2" + shift # shift past argument + shift # shift past value + ;; + *) + echo "Unknown argument: $1" + help_message + exit 1 + shift # shift past argument + ;; +esac +done + +PATCH_ARGS="${REVISION} ${PATCH_ARGS}" + +# Make sure there are no unstaged changes, just in case this script is being run locally +if [ -n "$(git status --porcelain)" ]; then + echo "Error: The source tree has unexpected changes. Clean up any changes (try 'git stash') and try again." + exit 10 +fi + +# Fetch and checkout latest changes, bailing if the branch isn't an ancestor of the remote branch. +REMOTE_AND_BRANCH="${REMOTE}/${BRANCH}" +git fetch "${REMOTE}" "${BRANCH}:${REMOTE_AND_BRANCH}" +git checkout "${BRANCH}" +git merge-base --is-ancestor "${BRANCH}" "${REMOTE_AND_BRANCH}" || { + echo "Error: Branch '${BRANCH}' is not an ancestor of '${REMOTE_AND_BRANCH}'" + exit 11 +} +git pull "${REMOTE}" "${BRANCH}" + +( + # If arc fails, there may be a dangling branch. Clean it up before exiting. + cleanup() { + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + git checkout "${BRANCH}" + git branch -D "${CURRENT_BRANCH}" || true + } + trap "cleanup" ERR + + # Note: `: | arc ...` pipes an empty string to stdin incase arcanist prompts + # for user input. This fails and is treated as an error. + : | arc patch ${PATCH_ARGS} +) + +git rebase "${BRANCH}" diff --git a/contrib/arcanist/test/test-autopatch.sh b/contrib/arcanist/test/test-autopatch.sh new file mode 100755 index 0000000000..91c6f2c331 --- /dev/null +++ b/contrib/arcanist/test/test-autopatch.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C.UTF-8 + +set -euxo pipefail + +TOPLEVEL=$(git rev-parse --show-toplevel) +CURRENT_DIR=$(dirname $(readlink -f "$0")) +TEST_PATCH="${CURRENT_DIR}/test-commit.patch" +: "${REMOTE:=origin}" +: "${MASTER_BRANCH:=master}" +REMOTE_AND_BRANCH="${REMOTE}/${MASTER_BRANCH}" +LATEST_MASTER=$(git rev-parse "${REMOTE_AND_BRANCH}") + +test_autopatch() { + PATCH_FILE="$1" + EXPECTED_EXIT_CODE="$2" + PATCH_ARGS="--patch ${PATCH_FILE}" + # Setting the remote to this repo allows us to simulate an upstream without + # relying on external services for unit tests. + export EDITOR="${CURRENT_DIR}/test-commit-message.sh" + # Note: Do not use `-o ${REMOTE}` here because REMOTE may be on the local filesystem. + EXIT_CODE=0 + "${CURRENT_DIR}/../autopatch.sh" -o testorigin -b "${MASTER_BRANCH}" --patch-args "${PATCH_ARGS}" || EXIT_CODE=$? + if [ "${EXIT_CODE}" -ne "${EXPECTED_EXIT_CODE}" ]; then + echo "Error: autopatch exited with '${EXIT_CODE}' when '${EXPECTED_EXIT_CODE}' was expected." + exit 1 + fi + + # Autopatch failed as expected, so sanity checks are not necessary + if [ "${EXPECTED_EXIT_CODE}" -ne 0 ]; then + exit 0 + fi + + # Sanity checks + if [ -n "$(git status --porcelain)" ]; then + echo "Error: There should be no uncommitted changes." + exit 10 + fi + + if [ "${LATEST_MASTER}" != "$(git rev-parse HEAD~)" ]; then + echo "Error: Failed to patch on latest master." + exit 11 + fi + + # Note: Remove 'index ...' line from 'git diff' as the SHA1 hash is unlikely + # to match. + DIFF_HEAD_AGAINST_PATCH="$(git diff HEAD~ | grep -v "^index " | diff - "${PATCH_FILE}" || :)" + if [ -n "${DIFF_HEAD_AGAINST_PATCH}" ]; then + echo "Error: Rebased changes do not match the given patch. Difference was:" + echo "${DIFF_HEAD_AGAINST_PATCH}" + exit 12 + fi +} + +TEST_STATUS="FAILED" +final_cleanup() { + # Cleanup the temporary test directory + rm -rf "$1" + + # Nicely print the final test status + set +x + echo + echo "${0}:" + echo "${TEST_STATUS}" +} + +TEMP_DIR=$(mktemp -d) +trap 'final_cleanup ${TEMP_DIR}' RETURN EXIT +cd "${TEMP_DIR}" +git init +git remote add testorigin "${TOPLEVEL}" +git pull testorigin "${REMOTE_AND_BRANCH}" + +test_cleanup() { + # Cleanup current branch so that arcanist doesn't run out of branch names + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + git checkout "${MASTER_BRANCH}" + git reset --hard HEAD + git branch -D "${CURRENT_BRANCH}" || true +} + +( + trap 'test_cleanup' RETURN ERR EXIT + echo "TEST: Simply sanity check that autopatch fast-forwards as expected" + git reset --hard HEAD~10 + test_autopatch "${TEST_PATCH}" 0 +) + +test_file_not_present() { + if [ -n "$1" ] && [ -f "$1" ]; then + echo "Error: '$1' file was found but not expected!" + exit 51 + fi +} + +( + trap 'test_cleanup' RETURN ERR EXIT + echo "TEST: Locally committed changes cause the script to bail" + TEST_FILE="test-committed-changes" + touch "${TEST_FILE}" + git add "${TEST_FILE}" + git commit -m "test local commit" + + test_autopatch "${TEST_PATCH}" 11 +) + +( + trap 'test_cleanup' RETURN ERR EXIT + echo "TEST: Staged changes are not included after autopatching" + TEST_FILE="test-staged-changes" + touch "${TEST_FILE}" + git add "${TEST_FILE}" + + test_autopatch "${TEST_PATCH}" 10 + test_file_not_present "${TEST_FILE}" +) + +( + trap 'test_cleanup' RETURN ERR EXIT + echo "TEST: Unstaged changes are not included after autopatching" + TEST_FILE="test-unstaged-changes" + touch "${TEST_FILE}" + + test_autopatch "${TEST_PATCH}" 10 + test_file_not_present "${TEST_FILE}" +) + +TEST_STATUS="PASSED" diff --git a/contrib/arcanist/test/test-commit-message.sh b/contrib/arcanist/test/test-commit-message.sh new file mode 100755 index 0000000000..df59bd5910 --- /dev/null +++ b/contrib/arcanist/test/test-commit-message.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +export LC_ALL=C.UTF-8 +set -euxo pipefail +echo "This commit message is used for testing." > "$1" diff --git a/contrib/arcanist/test/test-commit.patch b/contrib/arcanist/test/test-commit.patch new file mode 100644 index 0000000000..1457f74921 --- /dev/null +++ b/contrib/arcanist/test/test-commit.patch @@ -0,0 +1,2 @@ +diff --git a/test-file b/test-file +new file mode 100644