Skip to content

Instantly share code, notes, and snippets.

@QNimbus
Last active July 11, 2024 12:26
Show Gist options
  • Save QNimbus/bbaf20553b9d733c2b3f1b95346dbe0f to your computer and use it in GitHub Desktop.
Save QNimbus/bbaf20553b9d733c2b3f1b95346dbe0f to your computer and use it in GitHub Desktop.
Git scripts & tools #git #scripts
#!/bin/bash
#
# name: gh-clean.sh
# version: 1.2.0
# date: 2024-07-11
# author: B. van Wetten
# url: https://gist.githubusercontent.com/QNimbus/bbaf20553b9d733c2b3f1b95346dbe0f/raw/gh-clean.sh
# usage: Cleans a specified GitHub repository by removing the entire commit history of each branch, then recommitting each branch with the message 'Initial Commit'.
# Optionally, it can also clean the local repository to match the cleaned remote repository and clear all pull requests and PR branches.
#
# example: Next command will get all repositories for a user or organization, then clean each repository:
# gh repo list <user-or-org-name> --limit 100 --json name,sshUrl | jq -r '.[].sshUrl' | tr '\n' '\0' | xargs -0 -I {} ~/gh-clean.sh --force --clear-prs --repo {}
#
# To download:
#
# curl -L -O --progress-bar https://gist.githubusercontent.com/QNimbus/bbaf20553b9d733c2b3f1b95346dbe0f/raw/gh-clean.sh
# Prevent the script commands from ending up in bash history
HISTFILE=~/.bash_history
set +o history
trap 'set -o history' EXIT HUP INT QUIT PIPE TERM
# Color codes for success and error messages
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Function to display usage message
usage() {
echo -e "Usage: $0 [--repo <git-url>] [--also-clean-local] [--clear-prs] [--force]"
echo ""
echo -e " -h, --help Display this help message"
echo -e " --also-clean-local Also clean the local repository"
echo -e " --clear-prs Clear all pull requests and their branches"
echo -e " --clear-branches Clean all branches in the remote repository"
echo -e " -r, --repo <url> The URL of the GitHub repository"
echo -e " -f, --force Disable all confirmation prompts"
exit 1
}
# Check if no arguments were provided
if [ "$#" -eq 0 ]; then
usage
exit 1
fi
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to check GitHub authentication status
check_gh_auth() {
if ! gh auth status >/dev/null 2>&1; then
echo -e "${RED}Error: You are not logged in to GitHub CLI.${NC}" >&2
echo -e "${RED}Please login using the following command:${NC}" >&2
echo -e "${GREEN} gh auth login${NC}" >&2
exit 1
fi
}
# Check for required commands
if ! command_exists gh; then
echo -e "${RED}Error: GitHub CLI (gh) is not installed or not in PATH.${NC}" >&2
exit 1
fi
# Check GitHub authentication status
check_gh_auth
# Parse command line arguments
also_clean_local=false
clear_prs=false
clear_branches=false
force=false
while [[ "$#" -gt 0 ]]; do
case $1 in
-r|--repo) repo_url="$2"; shift ;;
--also-clean-local) also_clean_local=true ;;
--clear-branches) clear_branches=true ;;
--clear-prs) clear_prs=true ;;
-f|--force) force=true ;;
-h|--help) usage ;;
*) usage ;;
esac
shift
done
# Check that the --clear-branches flag is provided when the --also-clean-local flag is used
if $also_clean_local && ! $clear_branches; then
echo -e "${RED}Error: The --also-clean-local flag requires the --clear-branches flag.${NC}"
exit 1
fi
# Function to normalize a repository URL to a comparable format
normalize_url() {
local url=$1
# Convert SSH URL to HTTPS URL
if [[ $url =~ ^git@github\.com:([^\.]+)(\.git)?$ ]]; then
echo "https://github.com/${BASH_REMATCH[1]}"
else
echo "$url"
fi
}
# Function to get the repository name from the normalized URL
get_repo_name() {
local url=$1
echo $(basename -s .git "$url")
}
# If repo_url is not provided, use the origin remote URL of the current repository
if [ -z "$repo_url" ]; then
if [ -d .git ]; then
repo_url=$(git config --get remote.origin.url)
if [ -z "$repo_url" ]; then
echo -e "${RED}Error: No remote URL found for the current repository.${NC}"
exit 1
fi
repo_url=$(normalize_url "$repo_url")
else
echo -e "${RED}Error: No git repository found in the current directory, and no repository URL provided.${NC}"
usage
fi
fi
# Function to clean the git repository using gh CLI
clean () {
local repo_url=$1
local currDir=$(pwd)
local tmpDir=$(mktemp -d -t gh-clean-XXXXXX)
if ! $force; then
read -p "Are you sure you want to clean the remote repository? (y/n) " confirm
if [[ "$confirm" != "y" ]]; then
echo -e "${RED}Aborting remote clean.${NC}"
exit 0
fi
fi
local normalized_repo_url=$(normalize_url "$repo_url")
local repo_name=$(get_repo_name "$normalized_repo_url")
echo "Cloning repository... $repo_name"
if ! gh repo clone "$repo_url" "$tmpDir" &> /dev/null; then
echo -e "${RED}Failed to clone repository${NC}"
exit 1
fi
cd "$tmpDir" || { echo -e "${RED}Failed to change directory${NC}"; exit 1; }
for BR in $(git branch -r | grep -v '\->' | grep -v 'master' | sed 's/origin\///'); do
echo "Working on branch $BR"
if ! git checkout "$BR" &> /dev/null; then
echo -e "${RED}Failed to checkout branch $BR${NC}"
exit 1
fi
if ! git checkout --orphan cross &> /dev/null; then
echo -e "${RED}Failed to create orphan branch${NC}"
exit 1
fi
if ! git add -A &> /dev/null; then
echo -e "${RED}Failed to add files${NC}"
exit 1
fi
if ! git commit -m "Initial Commit" &> /dev/null; then
echo -e "${RED}Failed to commit${NC}"
exit 1
fi
if ! git branch -D "$BR" &> /dev/null; then
echo -e "${RED}Failed to delete branch $BR${NC}"
exit 1
fi
if ! git branch -m "$BR" &> /dev/null; then
echo -e "${RED}Failed to rename branch${NC}"
exit 1
fi
if ! git push -f origin "$BR" &> /dev/null; then
echo -e "${RED}Failed to push branch $BR${NC}"
exit 1
fi
if ! git gc --aggressive --prune=all &> /dev/null; then
echo -e "${RED}Failed to run git gc${NC}"
exit 1
fi
echo -e "${GREEN}Successfully cleaned branch $BR${NC}"
done
cd "$currDir" || { echo -e "${RED}Failed to change directory${NC}"; exit 1; }
rm -rf "$tmpDir"
}
# Function to also clean the local git repository
also_clean_local_repo () {
local repo_url=$1
if [ ! -d .git ]; then
echo -e "${RED}Error: No git repository found in the current directory.${NC}"
exit 1
fi
local remote_url
remote_url=$(git config --get remote.origin.url)
# Normalize both URLs
local normalized_remote_url
local normalized_repo_url
normalized_remote_url=$(normalize_url "$remote_url")
normalized_repo_url=$(normalize_url "$repo_url")
if [ "$normalized_remote_url" != "$normalized_repo_url" ]; then
echo -e "${RED}Error: The remote URL of the current repository ($normalized_remote_url) does not match the specified URL ($normalized_repo_url).${NC}"
exit 1
fi
if ! $force; then
read -p "Are you sure you want to clean the current local repository? (y/n) " confirm
if [[ "$confirm" != "y" ]]; then
echo -e "${RED}Aborting local clean.${NC}"
exit 0
fi
fi
echo "Cleaning the current local repository..."
# Remove all files and directories
find ./ -mindepth 1 -exec rm -rf {} +
# Re-clone the repository
if ! git clone "$repo_url" . &> /dev/null; then
echo -e "${RED}Failed to clone repository${NC}"
exit 1
fi
echo -e "${GREEN}Successfully cleaned the local repository${NC}"
}
# Function to clear all pull requests and their branches
clear_pull_requests () {
local repo_url=$1
local repo_name=$(basename -s .git "$repo_url")
if ! $force; then
read -p "Are you sure you want to clear all pull requests and their branches? (y/n) " confirm
if [[ "$confirm" != "y" ]]; then
echo -e "${RED}Aborting pull request clearing.${NC}"
exit 0
fi
fi
echo "Fetching list of pull requests..."
pr_list=$(gh pr list -R "$repo_url" --json number,headRefName -q '.[] | "\(.number) \(.headRefName)"')
if [ -z "$pr_list" ]; then
echo -e "${GREEN}No pull requests found to clear.${NC}"
return
fi
while IFS= read -r pr; do
pr_number=$(echo "$pr" | awk '{print $1}')
pr_branch=$(echo "$pr" | awk '{print $2}')
echo "Closing pull request #$pr_number..."
if ! gh pr close "$pr_number" -R "$repo_url" --delete-branch &> /dev/null; then
echo -e "${RED}Failed to close pull request #$pr_number${NC}"
else
echo -e "${GREEN}Successfully closed pull request #$pr_number and deleted branch $pr_branch${NC}"
fi
done <<< "$pr_list"
}
# Check if the --clear-prs flag is provided
if $clear_prs; then
clear_pull_requests "$repo_url"
fi
# Check if the --clear-branches flag is provided and clean the repository if so
if $clear_branches; then
clean "$repo_url"
fi
# Check if the --also-clean-local flag is provided and clean the local repository if so
if $also_clean_local; then
also_clean_local_repo "$repo_url"
fi
#!/bin/bash
#
# name: gh-workflow-clean.sh
# version: 1.0.1
# date: 2024-07-05
# author: B. van Wetten
# purpose: Remove all workflow runs from a given GitHub repository or organization
# url: https://gist.githubusercontent.com/QNimbus/bbaf20553b9d733c2b3f1b95346dbe0f/raw/gh-workflow-clean.sh
# usage: ./gh-workflow-clean.sh [-o <org>] [-r <repo>] [-l|--list] [--no-dry-run] [-e <workflow_id>] [-d <workflow_id>] [--enable-all] [--disable-all] [-h|--help]
#
# -h, --help Display this help message
# -l, --list List available workflows
# --no-dry-run Actually delete the workflow runs
# --enable-all Enable all workflows
# --disable-all Disable all workflows
# -o <org>, --org <org> GitHub organization (can also be set via GH_ORG environment variable)
# -r <repo>, --repo <repo> GitHub repository (can also be set via GH_REPO environment variable)
# -e <workflow_id>, --enable <workflow_id> Enable the specified workflow
# -d <workflow_id>, --disable <workflow_id> Disable the specified workflow
#
# Note: Command line arguments take precedence over environment variables.
#
# To download:
#
# curl -L -O --progress-bar https://gist.githubusercontent.com/QNimbus/bbaf20553b9d733c2b3f1b95346dbe0f/raw/gh-workflow-clean.sh
# Prevent the script commands from ending up in bash history
HISTFILE=~/.bash_history
set +o history
trap 'set -o history' EXIT HUP INT QUIT PIPE TERM
# Color codes for success and error messages
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Function to display usage message
usage() {
echo -e "Usage: $0 [-o <org>] [-r <repo>] [-l|--list] [--no-dry-run] [-e <workflow_id>] [-d <workflow_id>] [--enable-all] [--disable-all] [-h|--help]\n"
echo " -h, --help Display this help message"
echo " -l, --list List available workflows"
echo " --no-dry-run Actually delete the workflow runs"
echo " --enable-all Enable all workflows"
echo " --disable-all Disable all workflows"
echo " -o <org>, --org <org> GitHub organization (can also be set via GH_ORG environment variable)"
echo " -r <repo>, --repo <repo> GitHub repository (can also be set via GH_REPO environment variable)"
echo " -e <workflow_id>, --enable <workflow_id> Enable the specified workflow"
echo " -d <workflow_id>, --disable <workflow_id> Disable the specified workflow"
echo ""
echo "Note: Command line arguments take precedence over environment variables."
exit 1
}
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to check GitHub authentication status
check_gh_auth() {
if ! gh auth status >/dev/null 2>&1; then
echo -e "${RED}Error: You are not logged in to GitHub CLI.${NC}" >&2
echo -e "${RED}Please login using the following command:${NC}" >&2
echo -e " gh auth login" >&2
exit 1
fi
}
# Check for required commands
if ! command_exists gh; then
echo -e "${RED}Error: GitHub CLI (gh) is not installed or not in PATH.${NC}" >&2
exit 1
fi
if ! command_exists jq; then
echo -e "${RED}Error: jq is not installed or not in PATH.${NC}" >&2
exit 1
fi
# Check GitHub authentication status
check_gh_auth
# Initialize variables
list_workflows=false
no_dry_run=false
enable_workflow_id=""
disable_workflow_id=""
enable_all_workflows=false
disable_all_workflows=false
dry_run_notified=false
# Parse command line arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
-o|--org) org="$2"; shift ;;
-r|--repo) repo="$2"; shift ;;
-l|--list) list_workflows=true ;;
--no-dry-run) no_dry_run=true ;;
-e|--enable) enable_workflow_id="$2"; shift ;;
-d|--disable) disable_workflow_id="$2"; shift ;;
--enable-all) enable_all_workflows=true ;;
--disable-all) disable_all_workflows=true ;;
-h|--help) usage ;;
*) usage ;;
esac
shift
done
# If command line arguments are not provided, use environment variables
org="${org:-$GH_ORG}"
repo="${repo:-$GH_REPO}"
# Validate that org is set
if [ -z "$org" ]; then
echo "${RED}Error: Organization must be specified.${NC}"
usage
fi
# Function to list, enable, disable, or delete workflow runs for a given repository
process_repository() {
local repo=$1
# List available workflows if the -l or --list option is specified
if $list_workflows; then
echo -e "\nWorkflows for repository --> $org/$repo:"
echo
(echo -e "ID\tNAME\tSTATUS"; echo -e "---\t----\t------"; gh api repos/$org/$repo/actions/workflows --paginate | jq -r '.workflows[] | [.id, .name, .state] | @tsv') | column -t -s$'\t'
return
fi
# Enable the specified workflow if the -e or --enable option is provided
if [ -n "$enable_workflow_id" ]; then
echo -e "\nEnabling workflow ID $enable_workflow_id for repository $org/$repo:"
if gh api repos/$org/$repo/actions/workflows/$enable_workflow_id/enable -X PUT >/dev/null; then
echo -e "${GREEN}✔ Workflow ID $enable_workflow_id enabled${NC}"
else
echo -e "${RED}✖ Failed to enable Workflow ID $enable_workflow_id${NC}" >&2
fi
return
fi
# Disable the specified workflow if the -d or --disable option is provided
if [ -n "$disable_workflow_id" ]; then
echo "Disabling workflow ID $disable_workflow_id for repository $org/$repo:"
if gh api repos/$org/$repo/actions/workflows/$disable_workflow_id/disable -X PUT >/dev/null; then
echo -e "${GREEN}✔ Workflow ID $disable_workflow_id disabled${NC}"
else
echo -e "${RED}✖ Failed to disable Workflow ID $disable_workflow_id${NC}" >&2
fi
return
fi
# Enable all workflows if the --enable-all option is provided
if $enable_all_workflows; then
echo -e "\nEnabling all workflows for repository $org/$repo:"
workflow_ids=($(gh api repos/$org/$repo/actions/workflows --paginate | jq -r '.workflows[].id'))
for workflow_id in "${workflow_ids[@]}"; do
if gh api repos/$org/$repo/actions/workflows/$workflow_id/enable -X PUT >/dev/null; then
echo -e "${GREEN}✔ Enabled workflow ID $workflow_id${NC}"
else
echo -e "${RED}✖ Failed to enable workflow ID $workflow_id${NC}" >&2
fi
done
return
fi
# Disable all workflows if the --disable-all option is provided
if $disable_all_workflows; then
echo "Disabling all workflows for repository $org/$repo:"
workflow_ids=($(gh api repos/$org/$repo/actions/workflows --paginate | jq -r '.workflows[].id'))
for workflow_id in "${workflow_ids[@]}"; do
if gh api repos/$org/$repo/actions/workflows/$workflow_id/disable -X PUT >/dev/null; then
echo -e "${GREEN}✔ Disabled workflow ID $workflow_id${NC}"
else
echo -e "${RED}✖ Failed to disable workflow ID $workflow_id${NC}" >&2
fi
done
return
fi
# Notify about dry run if --no-dry-run is not provided and notify only once
if ! $no_dry_run && ! $dry_run_notified; then
echo -e "\nDry run: No changes will be made. To actually delete the workflow runs, use the --no-dry-run flag.\n"
dry_run_notified=true
fi
# Get workflow IDs with status "disabled_manually"
# Fetch the list of workflows and filter those which are manually disabled
workflow_ids=($(gh api repos/$org/$repo/actions/workflows --paginate | jq -r '.workflows[] | select(.state == "disabled_manually") | .id'))
# Loop through each workflow ID that was found
for workflow_id in "${workflow_ids[@]}"
do
# Fetch all run IDs for the current workflow
run_ids=($(gh api repos/$org/$repo/actions/workflows/$workflow_id/runs --paginate | jq -r '.workflow_runs[].id'))
if [ ${#run_ids[@]} -eq 0 ]; then
echo "No runs found for the workflow ID $workflow_id in repository $repo"
continue
fi
echo "Listing runs for the workflow ID $workflow_id in repository $repo"
# Loop through each run ID for the current workflow
for run_id in "${run_ids[@]}"
do
if $no_dry_run; then
echo -n "Deleting Run ID $run_id from repository $repo... "
# Delete the run and check for success
if gh api repos/$org/$repo/actions/runs/$run_id -X DELETE >/dev/null; then
echo -e "${GREEN}✔${NC}"
else
echo -e "${RED}✖${NC}" >&2
fi
else
echo "Dry run: Would delete Run ID $run_id from repository $repo"
fi
done
done
}
# If repo is not specified, select all repositories
if [ -z "$repo" ]; then
repos=($(gh api orgs/$org/repos --paginate | jq -r '.[].name'))
else
repos=($repo)
fi
# Process each repository
for repo in "${repos[@]}"; do
process_repository $repo
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment