Skip to content

Instantly share code, notes, and snippets.

@michael-rubel
Last active March 31, 2026 07:45
Show Gist options
  • Select an option

  • Save michael-rubel/32315e60b55ca215da871a575a93d017 to your computer and use it in GitHub Desktop.

Select an option

Save michael-rubel/32315e60b55ca215da871a575a93d017 to your computer and use it in GitHub Desktop.
Oro Product | Maintenance script
#!/bin/bash
# Maintenance script: cherry-pick commits from feature branch to maintenance branches.
# Usage: ./maintenance.sh [--dry-run]
# --- Configuration ---
branches=("maintenance/7.0" "maintenance/6.1" "maintenance/6.0")
DRY_RUN=false
if [[ "$1" == "--dry-run" ]]; then
DRY_RUN=true
fi
# --- Tracking arrays ---
created_branches=()
skipped_branches=()
pr_results=()
squash_branch=""
# --- Helper: cleanup all created branches and exit ---
cleanup_and_exit() {
echo ""
echo "Aborting. Cleaning up..."
git cherry-pick --abort </dev/null 2>/dev/null
git merge --abort </dev/null 2>/dev/null
for entry in "${created_branches[@]}"; do
local branch_name="${entry%%|*}"
echo " Deleting local branch: $branch_name"
git branch -D "$branch_name" </dev/null 2>/dev/null
done
# Cleanup squash branch if it was created
if [[ -n "$squash_branch" ]]; then
git checkout "$current_branch" </dev/null >/dev/null 2>&1
git branch -D "$squash_branch" </dev/null 2>/dev/null
fi
git checkout "$current_branch" </dev/null >/dev/null 2>&1
echo "Returned to $current_branch. Nothing was pushed."
exit 1
}
# --- Helper: handle cherry-pick conflict ---
handle_conflict() {
local target="$1"
echo ""
echo "======================================="
echo " CHERRY-PICK CONFLICT on: $target"
echo "======================================="
echo " Conflicted files:"
git diff --name-only --diff-filter=U | sed 's/^/ /'
echo "======================================="
echo " 1) Abort ALL — delete all created branches and exit"
echo " 2) I fixed it manually — continue"
echo "======================================="
while true; do
read -rp " Choose [1/2]: " choice
case "$choice" in
1)
cleanup_and_exit
;;
2)
if [[ -n $(git diff --name-only --diff-filter=U) ]]; then
echo ""
echo " Still unresolved conflicts:"
git diff --name-only --diff-filter=U | sed 's/^/ /'
echo " Please resolve them in another terminal, then choose again."
echo ""
echo " 1) Abort ALL"
echo " 2) I fixed it — continue"
continue
fi
if ! git cherry-pick --continue --no-edit </dev/null 2>/dev/null; then
echo " cherry-pick --continue failed. Please check the state and try again."
continue
fi
break
;;
*)
echo " Invalid choice. Enter 1 or 2."
;;
esac
done
}
# --- Helper: handle existing local branch ---
# Returns 0 = recreate, 1 = skip
handle_existing_branch() {
local branch_name="$1"
echo ""
echo " Local branch '$branch_name' already exists."
echo " 1) Skip this branch"
echo " 2) Delete and recreate"
while true; do
read -rp " Choose [1/2]: " choice
case "$choice" in
1) return 1 ;;
2)
git branch -D "$branch_name" </dev/null 2>/dev/null
echo " Deleted: $branch_name"
return 0
;;
*)
echo " Invalid choice. Enter 1 or 2."
;;
esac
done
}
# --- Helper: perform squash ---
# Creates a temporary squash branch, returns the squash commit hash
perform_squash() {
local commit_title="$1"
squash_branch="${current_branch}--squash-tmp"
# Cleanup if temp branch exists from previous failed run
git branch -D "$squash_branch" </dev/null 2>/dev/null
echo " Creating squash branch from origin/master..."
git checkout -b "$squash_branch" origin/master </dev/null >/dev/null 2>&1
echo " Squashing all changes from $current_branch..."
if ! git merge --squash "$current_branch" </dev/null 2>/dev/null; then
# Squash merge had conflicts — resolve automatically (accept theirs for all)
echo " Resolving squash merge conflicts..."
git checkout --theirs . </dev/null 2>/dev/null
git add -A </dev/null 2>/dev/null
fi
git commit -m "$commit_title" --no-edit </dev/null 2>/dev/null
squash_commit=$(git rev-parse HEAD)
echo " Squash commit: $(git log -1 --oneline "$squash_commit")"
# Return to original branch
git checkout "$current_branch" </dev/null >/dev/null 2>&1
}
# =============================================
# PRE-CHECKS
# =============================================
echo "=== Pre-checks ==="
# 1. Clean working directory (tracked files only)
if [[ -n $(git status --porcelain --untracked-files=no) ]]; then
echo "ERROR: Working directory is not clean. Commit or stash changes first."
exit 1
fi
echo " Working directory: clean"
# 2. Current branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
echo " Current branch: $current_branch"
# 3. Not on master or maintenance
if [[ "$current_branch" == "master" ]] || [[ "$current_branch" == maintenance/* ]]; then
echo "ERROR: Cannot run on '$current_branch'. Switch to your feature branch."
exit 1
fi
# 4. gh CLI authenticated
if ! gh auth status &>/dev/null; then
echo "ERROR: GitHub CLI not authenticated. Run 'gh auth login'."
exit 1
fi
echo " GitHub CLI: authenticated"
# 5. Fetch latest
echo " Fetching latest from origin..."
git fetch origin master </dev/null 2>/dev/null
for b in "${branches[@]}"; do
git fetch origin "$b" </dev/null 2>/dev/null
done
echo " Fetch: done"
# =============================================
# ANALYZE BRANCH & DECIDE ON SQUASH
# =============================================
echo ""
echo "=== Analyzing branch ==="
recent_commit_title=$(git log -1 --pretty=%B "$current_branch" | head -n 1)
echo " Last commit title: $recent_commit_title"
merge_base=$(git merge-base origin/master "$current_branch")
# Count all commits on branch (excluding merges)
all_commits=$(git log --no-merges --format="%H" "$merge_base..$current_branch" --reverse)
commit_count=0
if [[ -n "$all_commits" ]]; then
commit_count=$(echo "$all_commits" | wc -l)
fi
# Count merge commits
merge_count=$(git log --merges --format="%H" "$merge_base..$current_branch" | wc -l)
if [[ "$commit_count" -eq 0 ]]; then
echo "ERROR: No commits found on branch since divergence from master."
exit 1
fi
echo " Commits: $commit_count"
echo " Back-merges: $merge_count"
# Warn if branch seems to not be created from master
if [[ "$commit_count" -gt 100 ]]; then
echo ""
echo " WARNING: Found $commit_count commits. This script is designed for branches"
echo " created from master. Your branch may have been created from another branch."
if [[ "$DRY_RUN" != true ]]; then
echo ""
echo " Continue anyway? [y/N]"
read -rp " " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo " Aborted."
exit 0
fi
fi
fi
# Show commits (first 5 + last 5 if more than 10)
if [[ "$commit_count" -le 10 ]]; then
echo " Commit list:"
echo "$all_commits" | while read -r c; do echo " $(git log -1 --oneline "$c")"; done
else
echo " Commit list (first 5):"
echo "$all_commits" | head -5 | while read -r c; do echo " $(git log -1 --oneline "$c")"; done
echo " ... ($((commit_count - 10)) more) ..."
echo " Last 5:"
echo "$all_commits" | tail -5 | while read -r c; do echo " $(git log -1 --oneline "$c")"; done
fi
# =============================================
# DRY RUN
# =============================================
if [[ "$DRY_RUN" == true ]]; then
echo ""
echo "=== DRY RUN — no changes will be made ==="
if [[ "$commit_count" -eq 1 && "$merge_count" -eq 0 ]]; then
echo " Action: cherry-pick 1 commit (no squash needed)"
elif [[ "$merge_count" -gt 0 ]]; then
echo " Action: would ask to squash ($commit_count commits, $merge_count back-merges)"
else
echo " Action: would ask to squash ($commit_count commits)"
fi
echo " Commits to cherry-pick: $commit_count"
for target in "${branches[@]}"; do
target_suffix="${target//\//-}"
new_branch="${current_branch}-${target_suffix}"
echo " $new_branch (base: $target) -> PR to $target"
done
# Cleanup squash branch if created
if [[ -n "$squash_branch" ]]; then
git branch -D "$squash_branch" 2>/dev/null
fi
exit 0
fi
# =============================================
# DECIDE ON SQUASH
# =============================================
do_squash=false
if [[ "$commit_count" -eq 1 && "$merge_count" -eq 0 ]]; then
commits="$all_commits"
echo ""
echo " Single commit, no back-merges — ready to go."
else
echo ""
if [[ "$merge_count" -gt 0 ]]; then
echo " Found $commit_count commit(s) and $merge_count back-merge(s)."
echo " Squash is recommended to avoid issues with back-merge dependencies."
else
echo " Found $commit_count commit(s)."
fi
echo ""
echo " 1) Squash into one commit (recommended)"
echo " 2) Cherry-pick all $commit_count commits as-is"
while true; do
read -rp " Choose [1/2]: " choice
case "$choice" in
1) do_squash=true; break ;;
2) commits="$all_commits"; break ;;
*)
echo " Invalid choice. Enter 1 or 2."
;;
esac
done
fi
if [[ "$do_squash" == true ]]; then
echo ""
echo "=== Squashing ==="
perform_squash "$recent_commit_title"
commits="$squash_commit"
commit_count=1
fi
# =============================================
# PHASE 1: Prepare branches locally
# =============================================
echo ""
echo "=== Phase 1: Preparing branches locally ==="
for target in "${branches[@]}"; do
echo ""
echo "---------------------------------------"
echo "Processing: $target"
target_suffix="${target//\//-}"
new_branch="${current_branch}-${target_suffix}"
# Check if local branch already exists
if git show-ref --verify --quiet "refs/heads/$new_branch"; then
handle_existing_branch "$new_branch"
if [[ $? -eq 1 ]]; then
skipped_branches+=("$target (user skipped, local branch exists)")
continue
fi
fi
# Check if remote branch already exists
if git ls-remote --heads origin "$new_branch" 2>/dev/null | grep -q "$new_branch"; then
echo " Remote branch '$new_branch' already exists. Skipping."
skipped_branches+=("$target (remote branch exists)")
continue
fi
# Checkout target and update
git checkout "$target" </dev/null >/dev/null 2>&1
git pull origin "$target" </dev/null >/dev/null 2>&1
# Create new branch
echo " Creating branch: $new_branch"
git checkout -b "$new_branch" </dev/null >/dev/null 2>&1
# Cherry-pick commits
for commit in $commits; do
echo " Cherry-picking: $(git log -1 --oneline "$commit")"
if ! git cherry-pick "$commit" </dev/null 2>/dev/null; then
handle_conflict "$target"
fi
done
created_branches+=("$new_branch|$target|$target_suffix")
echo " OK: $new_branch ready"
done
# Return to original branch
git checkout "$current_branch" </dev/null >/dev/null 2>&1
# Cleanup squash branch
if [[ -n "$squash_branch" ]]; then
git branch -D "$squash_branch" </dev/null 2>/dev/null
fi
# Check if anything was prepared
if [[ ${#created_branches[@]} -eq 0 ]]; then
echo ""
echo "No branches were prepared. Nothing to push."
exit 1
fi
# =============================================
# PHASE 2: Push and create PRs
# =============================================
echo ""
echo "=== Phase 2: Push and create PRs ==="
echo " Ready to push ${#created_branches[@]} branch(es):"
for entry in "${created_branches[@]}"; do
echo " ${entry%%|*}"
done
echo ""
read -rp " Continue? [y/N]: " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo " Aborted. Local branches are kept — you can push them manually."
exit 0
fi
for entry in "${created_branches[@]}"; do
IFS='|' read -r new_branch target target_suffix <<< "$entry"
echo ""
echo " Pushing: $new_branch"
git push -u origin "$new_branch" </dev/null 2>&1
# Check if PR already exists
existing_pr=$(gh pr list --head "$new_branch" --base "$target" --json number --jq '.[0].number' 2>/dev/null)
if [[ -n "$existing_pr" && "$existing_pr" != "null" ]]; then
echo " PR #$existing_pr already exists. Skipping."
pr_results+=("[EXISTING] $target -> PR #$existing_pr")
continue
fi
# Create PR
pr_title="${recent_commit_title} (${target_suffix})"
pr_body="Automated PR to apply commits from '$current_branch' to '$target'."
pr_url=$(gh pr create --base "$target" --head "$new_branch" --title "$pr_title" --body "$pr_body")
echo " Created PR: $pr_url"
pr_results+=("[CREATED] $target -> $pr_url")
done
# =============================================
# SUMMARY
# =============================================
echo ""
echo "======================================="
echo " SUMMARY"
echo "======================================="
if [[ ${#pr_results[@]} -gt 0 ]]; then
echo ""
echo " PRs:"
for pr in "${pr_results[@]}"; do
echo " $pr"
done
fi
if [[ ${#skipped_branches[@]} -gt 0 ]]; then
echo ""
echo " Skipped:"
for skip in "${skipped_branches[@]}"; do
echo " $skip"
done
fi
echo ""
echo "======================================="
echo " Process completed!"
echo "======================================="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment