Skip to content

Instantly share code, notes, and snippets.

@CourteousCoder
Last active September 21, 2021 02:11
Show Gist options
  • Save CourteousCoder/5da5ab148d088ab4d8087bfb99875178 to your computer and use it in GitHub Desktop.
Save CourteousCoder/5da5ab148d088ab4d8087bfb99875178 to your computer and use it in GitHub Desktop.
Script to merge pull requests from github, in order, into a locally cloned github repository
#!/usr/bin/env zsh
# Merge pull requests in the given order into a locally cloned github repository.
# Run with no arguments to print help text.
SCRIPT_NAME="$0"
# Path to repository root.
REPOSITORY_ROOT="$1"
# An array of pull-request IDs to merge locally
PULL_REQUEST_IDS="${@:2}"
# Handle conflicts by accepting changes already merged, disregarding the PR's conflicitng changes.
# Change to "theirs" to accept the PR's changes when they conflict with the already-merged changes.
CONFLICT_RESOLUTION_STRATEGY="ours"
# Print help text.
show_help() {
cat << END_OF_HELP_TEXT
Merge pull requests in the given order into a locally cloned github repository
usage: $SCRIPT_NAME repository_root pr_number...
Or run with no arguments to show this help text.
exit codes:
0 all given PRs were merged successfully into the local repository
1 error due to invalid input
2 one or more the PRs were not merged for git- or github-related reasons
END_OF_HELP_TEXT
}
# Print an error message stderr, show hep text, and then exit.
# If the first argument is a positive integer less than 256, it is used as an exit code, and the remainaing arguments are treated as the message.
# Otherwise, all arguments are treated as the message, and the value `1' is used as the exit code.
error_exit() {
local exit_code="$1"
[[ $exit_code =~ '^[0-9]+$' ]] && (( exit_code > 0 && exit_code < 256 )) && shift || exit_code=1
>&2 echo 'ERROR' "$exit_code:" "$@"
show_help
echo 'Exiting now.'
exit $exit_code
}
# Run git commands as if the current working directory were $REPOSITORY_ROOT
cgit() {
git -C "$REPOSITORY_ROOT" "$@"
}
# Return 0 if the source branch was already merged into the destination branch. Return 1 otherwise.
is_already_merged() {
local source_branch=$(cgit rev-parse "$1")
local destination_branch=$(cgit rev-parse "$2")
cgit merge-base --is-ancestor "$source_branch" "$destination_branch"
}
# Merge the given pr into the destination branch. Return 0 if the pr has been merged. Return 1 otherwise.
merge_pr_locally() {
local pr_id="$1"
local pr_branch_name="pr-${pr_id}"
local destination_branch_name="$2"
cgit fetch --quiet origin pull/"${pr_id}"/head:"${pr_branch_name}" \
&& cgit checkout --quiet "${pr_branch_name}" \
&& cgit checkout --quiet "$destination_branch_name" \
&& cgit merge --quiet --no-commit --strategy recursive --strategy-option patience --strategy-option "$CONFLICT_RESOLUTION_STRATEGY" "$pr_branch_name" \
|| cgit checkout --quiet "$destination_branch_name"
is_already_merged "$pr_branch_name" "$destination_branch_name"
}
# Handle and validate input.
process_arguments() {
[[ "$#" -eq "0" ]] && show_help && exit
[[ "$#" -lt "2" ]] && error_exit "Expected arguments \`repository_root' and at least one \`pr_number'"
[[ $(cgit rev-parse --show-toplevel 2>/dev/null) = "$REPOSITORY_ROOT" ]] || error_exit "$REPOSITORY_ROOT is not the root of a git repository"
[[ $(cgit config --get remote.origin.url) == *github.com* ]] || error_exit "$REPOSITORY_ROOT does not have a remote \`origin' from github.com"
}
main() {
process_arguments "$@"
local start_branch="$(cgit rev-parse --abbrev-ref HEAD)"
local failed=()
for pr_id in "$PULL_REQUEST_IDS"; do
merge_pr_locally "$pr_id" "$start_branch" || failed+=("$pr_id")
done
[[ -n "$failed" ]] && echo "$failed" && error_exit 2 "Could not merge pull-request IDs \`$failed'" || true
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment