Last active
March 31, 2026 07:45
-
-
Save michael-rubel/32315e60b55ca215da871a575a93d017 to your computer and use it in GitHub Desktop.
Oro Product | Maintenance script
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 | |
| # 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