Shell script to graft the history of a new git repo on to a remote
graft.sh
164 lines 5.8 kB view raw
1#!/usr/bin/env bash 2# ───────────────────────────────────────────────────────────── 3# 🌱 Git History Grafting Tool 4# Attach your rewrite branch to an existing remote's history. 5# 6# Author: Owais J <https://github.com/desertthunder> 7# Updated: 2025-11-03 8# 9# Description: 10# This script grafts a rewrite branch (e.g., refactor) onto 11# an existing Git remote history, preserving ancestry while 12# discarding old files. 13# ───────────────────────────────────────────────────────────── 14set -euo pipefail 15 16C_BLUE=110 # cyan-blue accents 17C_CYAN=117 # bright cyan 18C_PURPLE=141 # icy lavender 19C_GREY=249 # soft gray text 20C_DIM=244 # dimmer gray 21C_GREEN=78 # sea green success 22C_YELLOW=179 # muted yellow warning 23C_RED=203 # red error 24 25BASE_BRANCH="main" 26REMOTE="" 27NEW_BRANCH="" 28NON_INTERACTIVE=false 29 30header() { 31 gum style --foreground "$C_BLUE" --bold "🌱 Git History Grafting Tool" 32 gum style --foreground "$C_DIM" "Attach your rewrite branch to an existing remote's history safely." 33 echo 34} 35 36show_help() { 37 SCRIPT_NAME=$(basename "$0") 38 39 header 40 41 printf "Usage:\n %s [options]\n\n" "$SCRIPT_NAME" | gum style --foreground "$C_GREY" 42 43 printf "Options:\n" | gum style --foreground "$C_GREY" --bold 44 { 45 echo "--remote <name>" 46 echo " Git remote to graft onto (e.g. origin)" 47 echo 48 echo "--branch <name>" 49 echo " Your local rewrite branch (e.g. refactor)" 50 echo 51 echo "--base <branch>" 52 echo " Base branch on the remote (default: main)" 53 echo 54 echo "--yes, --non-interactive" 55 echo " Skip all confirmations (CI-safe)" 56 echo 57 echo "-h, --help" 58 echo " Show this help message and exit" 59 } | gum style --foreground "$C_DIM" 60 61 echo 62 printf "Examples:\n" | gum style --foreground "$C_GREY" --bold 63 printf " Interactive mode:\n" | gum style --foreground "$C_DIM" 64 printf " %s\n" "./$SCRIPT_NAME" | gum style --foreground "$C_CYAN" 65 echo 66 printf " Non-interactive (CI):\n" | gum style --foreground "$C_DIM" 67 printf " %s --remote origin --branch refactor --base main --yes\n" "./$SCRIPT_NAME" \ 68 | gum style --foreground "$C_CYAN" 69 70 echo 71 printf "Theme:\n" | gum style --foreground "$C_GREY" --bold 72 printf " • Iceberg.vim-inspired — subtle cyan, lavender, and gray tones\n" \ 73 | gum style --foreground "$C_PURPLE" 74 echo 75} 76 77while [[ $# -gt 0 ]]; do 78 case "$1" in 79 --remote) REMOTE="$2"; shift 2;; 80 --branch) NEW_BRANCH="$2"; shift 2;; 81 --base) BASE_BRANCH="$2"; shift 2;; 82 --yes|--non-interactive) NON_INTERACTIVE=true; shift;; 83 -h|--help) show_help; exit 0;; 84 -*) echo "Unknown option: $1" >&2; exit 1;; 85 *) shift;; 86 esac 87done 88 89header 90 91git rev-parse --is-inside-work-tree >/dev/null 92 93if [ -z "$REMOTE" ]; then 94 mapfile -t REMOTES < <(git remote) 95 if [ ${#REMOTES[@]} -eq 0 ]; then 96 gum style --foreground "$C_RED" "❌ No remotes found. Please add one (e.g., git remote add origin ...)." 97 exit 1 98 fi 99 REMOTE=$(gum choose "${REMOTES[@]}" --header "Select the remote to graft onto:") 100fi 101 102if [ -z "$NEW_BRANCH" ]; then 103 LOCAL_BRANCH=$(git branch --show-current || true) 104 DEFAULT_BRANCH=${LOCAL_BRANCH:-"refactor"} 105 NEW_BRANCH=$(gum input --placeholder "Enter the name of your rewrite branch" --value "$DEFAULT_BRANCH") 106fi 107 108if ! git show-ref --verify --quiet "refs/heads/$NEW_BRANCH"; then 109 gum style --foreground "$C_RED" "❌ Branch '$NEW_BRANCH' not found." 110 exit 1 111fi 112 113gum style --foreground "$C_BLUE" "Using remote: $REMOTE" 114gum style --foreground "$C_BLUE" "Using base branch: $REMOTE/$BASE_BRANCH" 115gum style --foreground "$C_BLUE" "Using rewrite branch: $NEW_BRANCH" 116 117if [ "$NON_INTERACTIVE" = false ]; then 118 gum confirm "Proceed to graft '$NEW_BRANCH' onto '$REMOTE/$BASE_BRANCH'?" || exit 0 119fi 120 121gum spin --spinner line --title "Fetching $REMOTE..." -- git fetch "$REMOTE" "$BASE_BRANCH" 122gum spin --spinner line --title "Checking out $NEW_BRANCH..." -- git checkout "$NEW_BRANCH" 123 124ROOT_COMMIT=$(git rev-list --max-parents=0 HEAD) 125BASE_COMMIT=$(git rev-parse "$REMOTE/$BASE_BRANCH") 126 127{ 128 echo -e "Key\tValue" 129 echo -e "$(gum style --foreground "$C_PURPLE" 'Root Commit')\t$ROOT_COMMIT" 130 echo -e "$(gum style --foreground "$C_PURPLE" 'Base Commit')\t$BASE_COMMIT" 131} | gum table 132 133gum spin --spinner minidot --title "Creating temporary graft..." -- \ 134 git replace --graft "$ROOT_COMMIT" "$BASE_COMMIT" 135 136gum style --foreground "$C_GREEN" "✓ Graft created. Showing recent commits..." 137 138git --no-pager log --oneline --graph --decorate -10 | gum format --theme=dark 139 140if command -v git-filter-repo >/dev/null; then 141 gum spin --spinner pulse --title "Rewriting branch '$NEW_BRANCH'..." -- \ 142 git filter-repo --force --refs "refs/heads/$NEW_BRANCH" --preserve-commit-hashes 143else 144 gum style --foreground "$C_YELLOW" "⚠ git-filter-repo not found; using slower git filter-branch" 145 gum spin --spinner pulse --title "Rewriting history..." -- \ 146 git filter-branch -- --refs "refs/heads/$NEW_BRANCH" 147fi 148 149if git show-ref --quiet "refs/replace/$ROOT_COMMIT"; then 150 git replace -d "$ROOT_COMMIT" 151fi 152 153if [ ! -d .git/refs/remotes ]; then 154 gum style --foreground "$C_DIM" "Restoring remote refs..." 155 git fetch --all 156fi 157 158if [ "$NON_INTERACTIVE" = true ] || gum confirm "Push '$NEW_BRANCH' to $REMOTE?"; then 159 gum spin --spinner line --title "Pushing to $REMOTE..." -- \ 160 git push "$REMOTE" "$NEW_BRANCH" --force-with-lease 161fi 162 163gum style --foreground "$C_GREEN" "✓ Done!" 164gum style --foreground "$C_DIM" "Your rewrite branch '$NEW_BRANCH' now descends from '$REMOTE/$BASE_BRANCH'."