Created
May 4, 2026 16:13
-
-
Save TerrorJack/3d2d3188178bbf6da7e60c39aec1cf8e to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| PROJECT_PATH="${PROJECT_PATH:-ghc/ghc}" | |
| AUTHOR_USERNAME="${AUTHOR_USERNAME:-TerrorJack}" | |
| ACCEPTED_ASSIGNEE_USERNAME="${ACCEPTED_ASSIGNEE_USERNAME:-marge-bot}" | |
| GLAB_HOST="${GLAB_HOST:-gitlab.haskell.org}" | |
| DELETE_REMOTE="${DELETE_REMOTE:-origin}" | |
| require_command() { | |
| local command_name=$1 | |
| if ! command -v "$command_name" >/dev/null 2>&1; then | |
| printf 'error: required command not found: %s\n' "$command_name" >&2 | |
| exit 1 | |
| fi | |
| } | |
| urlencode() { | |
| jq -nr --arg value "$1" '$value | @uri' | |
| } | |
| fetch_merge_requests() { | |
| local query=$1 | |
| glab api "${glab_api_flags[@]}" \ | |
| "projects/${project_path}/merge_requests?${query}&per_page=100" \ | |
| --paginate \ | |
| --output ndjson | |
| } | |
| list_local_remote_branches() { | |
| local remote_ref_prefix="refs/remotes/${DELETE_REMOTE}/" | |
| git for-each-ref --format='%(refname)' "$remote_ref_prefix" \ | |
| | jq -R --arg prefix "$remote_ref_prefix" ' | |
| select(startswith($prefix)) | |
| | .[($prefix | length):] | |
| | select(. != "HEAD") | |
| ' \ | |
| | jq -s 'unique | sort' | |
| } | |
| require_command glab | |
| require_command git | |
| require_command jq | |
| local_remote_branches_file=$(mktemp) | |
| trap 'rm -f "$local_remote_branches_file"' EXIT | |
| project_path=$(urlencode "$PROJECT_PATH") | |
| author_username=${AUTHOR_USERNAME#@} | |
| accepted_assignee_username=${ACCEPTED_ASSIGNEE_USERNAME#@} | |
| author_username_query=$(urlencode "$author_username") | |
| accepted_assignee_username_query=$(urlencode "$accepted_assignee_username") | |
| glab_api_flags=(--hostname "$GLAB_HOST") | |
| accepted_merge_requests_json=$( | |
| { | |
| fetch_merge_requests \ | |
| "state=merged&author_username=${author_username_query}" | |
| fetch_merge_requests \ | |
| "state=closed&author_username=${author_username_query}&assignee_username=${accepted_assignee_username_query}" | |
| } | jq -s \ | |
| --arg accepted_assignee_username "$accepted_assignee_username" \ | |
| ' | |
| map( | |
| select( | |
| .state == "merged" | |
| or ( | |
| .state == "closed" | |
| and any(.assignees[]?; .username == $accepted_assignee_username) | |
| ) | |
| ) | |
| ) | |
| | unique_by(.id) | |
| | map(select(.source_branch != null and .source_branch != "")) | |
| | sort_by(.iid) | |
| ' | |
| ) | |
| accepted_merge_request_count=$(jq 'length' <<<"$accepted_merge_requests_json") | |
| list_local_remote_branches >"$local_remote_branches_file" | |
| same_project_accepted_merge_requests_json=$( | |
| jq ' | |
| map( | |
| select( | |
| .source_project_id != null | |
| and .target_project_id != null | |
| and .source_project_id == .target_project_id | |
| and .source_branch != null | |
| and .source_branch != "" | |
| and .source_branch != .target_branch | |
| ) | |
| ) | |
| ' <<<"$accepted_merge_requests_json" | |
| ) | |
| same_project_accepted_merge_request_count=$(jq 'length' <<<"$same_project_accepted_merge_requests_json") | |
| accepted_merge_requests_with_local_ref_json=$( | |
| jq \ | |
| --slurpfile local_remote_branches "$local_remote_branches_file" \ | |
| ' | |
| map( | |
| select( | |
| .source_branch as $source_branch | |
| | $local_remote_branches[0] | |
| | index($source_branch) | |
| ) | |
| ) | |
| | sort_by(.source_branch, .iid) | |
| ' <<<"$same_project_accepted_merge_requests_json" | |
| ) | |
| accepted_merge_requests_with_local_ref_count=$(jq 'length' <<<"$accepted_merge_requests_with_local_ref_json") | |
| delete_branches_json=$( | |
| jq ' | |
| group_by(.source_branch) | |
| | map({ | |
| source_branch: .[0].source_branch, | |
| merge_requests: map({ | |
| iid, | |
| title, | |
| web_url, | |
| reference: (.references.full // ("!" + (.iid | tostring))), | |
| short_reference: (.references.short // ("!" + (.iid | tostring))) | |
| }) | |
| }) | |
| | sort_by(.source_branch) | |
| ' <<<"$accepted_merge_requests_with_local_ref_json" | |
| ) | |
| delete_branch_count=$(jq 'length' <<<"$delete_branches_json") | |
| quoted_delete_remote=$(jq -nr --arg value "$DELETE_REMOTE" '$value | @sh') | |
| cat <<SCRIPT | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # Generated by $(basename "$0") on $(date -u +'%Y-%m-%dT%H:%M:%SZ'). | |
| # Accepted merge requests found: ${accepted_merge_request_count} | |
| # Same-project accepted merge requests: ${same_project_accepted_merge_request_count} | |
| # Same-project accepted merge requests with local refs/remotes/${DELETE_REMOTE}/ refs: ${accepted_merge_requests_with_local_ref_count} | |
| # Unique remote branches emitted: ${delete_branch_count} | |
| # Source query: | |
| # host: ${GLAB_HOST} | |
| # project: ${PROJECT_PATH} | |
| # author: @${author_username} | |
| # accepted when state=merged, or state=closed and assignee=@${accepted_assignee_username} | |
| # emitted only when source_project_id == target_project_id | |
| # emitted only when the branch exists locally under refs/remotes/${DELETE_REMOTE}/ | |
| default_remote=${quoted_delete_remote} | |
| remote="\${REMOTE:-\$default_remote}" | |
| delete_branch() { | |
| local branch=\$1 | |
| local refs=\$2 | |
| local remote_ref="refs/remotes/\${remote}/\${branch}" | |
| if git show-ref --verify --quiet "\$remote_ref"; then | |
| printf 'Deleting %s for %s from %s\n' "\$branch" "\$refs" "\$remote" >&2 | |
| git push "\$remote" --delete "\$branch" | |
| else | |
| printf 'Skipping %s for %s: local remote ref not found: %s\n' "\$branch" "\$refs" "\$remote_ref" >&2 | |
| fi | |
| } | |
| SCRIPT | |
| jq -r ' | |
| def clean_comment: | |
| tostring | gsub("[\r\n]+"; " "); | |
| .[] | |
| | .merge_requests as $merge_requests | |
| | ($merge_requests | map(.short_reference) | join(", ")) as $short_references | |
| | ($merge_requests | map(.reference) | join(", ")) as $references | |
| | "# " + $references | |
| + if ($merge_requests | length) == 1 then | |
| ": " + ($merge_requests[0].title | clean_comment) | |
| else | |
| ": " + (($merge_requests | length) | tostring) + " accepted merge requests share this source branch" | |
| end | |
| + "\n" + ($merge_requests | map("# " + .web_url) | join("\n")) | |
| + "\ndelete_branch " | |
| + (.source_branch | @sh) | |
| + " " | |
| + ($short_references | @sh) | |
| + "\n" | |
| ' <<<"$delete_branches_json" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment