diff --git a/contrib/source-control-tools/land-patch.sh b/contrib/source-control-tools/land-patch.sh --- a/contrib/source-control-tools/land-patch.sh +++ b/contrib/source-control-tools/land-patch.sh @@ -26,7 +26,7 @@ } DRY_RUN=no -GIT_ARGS=() +GIT_ARGS=("--porcelain") # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -59,6 +59,20 @@ : "${SANITY_CHECKS_COMMAND:=${TOPLEVEL}/contrib/devtools/smoke-tests.sh}" ${SANITY_CHECKS_COMMAND} -# Push the change. Phabricator will automatically close the associated revision. : "${GIT_COMMAND:=git}" -${GIT_COMMAND} push "${GIT_ARGS[@]}" origin master +while true; do + # Make sure master is up-to-date. If there is a merge conflict, this script + # will not attempt to resolve it and simply fail. + ${GIT_COMMAND} pull --rebase origin master + + # Push the change. Phabricator will automatically close the associated revision. + set +e + PUSH_OUTPUT=$(${GIT_COMMAND} push "${GIT_ARGS[@]}" origin master) + PUSH_EXIT_CODE=$? + set -e + if (( PUSH_EXIT_CODE == 0 )); then + exit 0 + else + echo "${PUSH_OUTPUT}" | grep "\! refs/heads/master:refs/heads/master \[rejected\] (non-fast-forward) Done" + fi +done diff --git a/contrib/source-control-tools/test/test-land-patch.sh b/contrib/source-control-tools/test/test-land-patch.sh --- a/contrib/source-control-tools/test/test-land-patch.sh +++ b/contrib/source-control-tools/test/test-land-patch.sh @@ -18,15 +18,22 @@ # Successful git push works as expected mock_git_push_success() { - echo 'Mock git push succeeded' + echo "Mock 'git $1' succeeded" } export -f mock_git_push_success GIT_COMMAND=mock_git_push_success "${SOURCE_CONTROL_TOOLS}"/land-patch.sh --dry-run # Unsuccessful git push fails to land mock_git_push_fail() { - echo 'Mock git push failed' - exit 1 + case $1 in + push) + echo 'Mock git push failed' + exit 1 + ;; + *) + echo "Mock 'git $1' succeeded" + ;; + esac } export -f mock_git_push_fail @@ -36,4 +43,60 @@ exit 1 } || true +# Unsuccessful git pull fails to land +mock_git_pull_rebase_fail() { + case $1 in + pull) + echo 'Mock git pull & rebase failed' + exit 1 + ;; + *) + echo "Mock 'git $1' succeeded" + ;; + esac +} +export -f mock_git_pull_rebase_fail + +# shellcheck disable=SC2015 +GIT_COMMAND=mock_git_pull_rebase_fail "${SOURCE_CONTROL_TOOLS}"/land-patch.sh --dry-run && { + echo "Error: A git pull (and rebase) failure is expected to fail" + exit 1 +} || true + +# non-fast-forwards eventually succeed +echo "0" > /tmp/abc-land-patch-temp-count +mock_git_push_nonff() { + case $1 in + push) + COUNT=$(cat /tmp/abc-land-patch-temp-count) + COUNT=$((COUNT + 1)) + echo "${COUNT}" > /tmp/abc-land-patch-temp-count + case ${COUNT} in + [12]) + echo 'To ssh://vcs@reviews.bitcoinabc.org:2221/source/bitcoin-abc.git ! refs/heads/master:refs/heads/master [rejected] (non-fast-forward) Done' + exit 2 + ;; + 3) + mock_git_push_success "$@" + ;; + *) + mock_git_push_fail "$@" + ;; + esac + ;; + *) + echo "Mock 'git $1' succeeded" + ;; + esac +} +export -f mock_git_push_nonff + +# shellcheck disable=SC2015 +GIT_COMMAND=mock_git_push_nonff "${SOURCE_CONTROL_TOOLS}"/land-patch.sh --dry-run + +if [[ $(cat /tmp/abc-land-patch-temp-count) -ne 3 ]]; then + echo "Error: git push did not succeed after a few non-fast-forward failures" + exit 1 +fi + echo "PASSED"