Last active
July 11, 2024 12:26
-
-
Save QNimbus/bbaf20553b9d733c2b3f1b95346dbe0f to your computer and use it in GitHub Desktop.
Git scripts & tools #git #scripts
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
#!/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 |
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
#!/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