graft.sh
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'."