|
#!/usr/bin/env bash |
|
# link-worktree-submodules.sh - Share submodule git data between main repo and worktrees |
|
# |
|
# When using git worktrees, each worktree gets its own copy of submodule git data |
|
# under .git/worktrees/<name>/modules/. This wastes disk space and causes "dirty" |
|
# submodule status when the worktree modules aren't properly initialized. |
|
# |
|
# This script links worktree submodules to share the main repo's submodule data |
|
# by updating each submodule's .git file to point to the main repo's modules. |
|
# |
|
# Usage: ./scripts/link-worktree-submodules.sh [OPTIONS] |
|
# |
|
# Safe to run multiple times - idempotent. |
|
|
|
set -euo pipefail |
|
|
|
# Script metadata |
|
SCRIPT_NAME=$(basename "${BASH_SOURCE[0]}") |
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) |
|
readonly SCRIPT_VERSION="1.0.0" |
|
readonly SCRIPT_NAME SCRIPT_DIR |
|
|
|
# Color and emoji definitions |
|
declare -A colors=( |
|
[green]=$'\033[0;32m' |
|
[red]=$'\033[0;31m' |
|
[yellow]=$'\033[1;33m' |
|
[reset]=$'\033[0m' |
|
) |
|
|
|
declare -A emojis=( |
|
[success]='✅' |
|
[error]='❌' |
|
[warning]='⚠️' |
|
[info]='ℹ️' |
|
) |
|
|
|
# Logging functions |
|
print_success() { printf '%b %b%b%b\n' "${emojis[success]}" "${colors[green]}" "$*" "${colors[reset]}"; } |
|
print_error() { printf '%b %b%b%b\n' "${emojis[error]}" "${colors[red]}" "$*" "${colors[reset]}" >&2; } |
|
print_warning() { printf '%b %b%b%b\n' "${emojis[warning]}" "${colors[yellow]}" "$*" "${colors[reset]}"; } |
|
print_info() { printf '%b %b\n' "${emojis[info]}" "$*"; } |
|
|
|
# Track backups for rollback on failure |
|
declare -a BACKUP_FILES=() |
|
|
|
# Error handler with rollback |
|
handle_error() { |
|
print_error "An error occurred on line $1" |
|
if [[ ${#BACKUP_FILES[@]} -gt 0 ]]; then |
|
print_warning "Rolling back changes..." |
|
for backup in "${BACKUP_FILES[@]}"; do |
|
original="${backup%.bak}" |
|
if [[ -f "${backup}" ]]; then |
|
mv "${backup}" "${original}" |
|
print_info "Restored: ${original}" |
|
fi |
|
done |
|
fi |
|
exit 1 |
|
} |
|
|
|
trap 'handle_error $LINENO' ERR |
|
|
|
# Cleanup backups on success |
|
cleanup_backups() { |
|
for backup in "${BACKUP_FILES[@]}"; do |
|
rm -f "${backup}" |
|
done |
|
} |
|
|
|
# Usage function |
|
usage() { |
|
cat <<EOF |
|
Usage: ${SCRIPT_NAME} [OPTIONS] |
|
|
|
Description: |
|
Link worktree submodules to share the main repo's git data. |
|
Saves disk space and fixes "dirty" submodule status in worktrees. |
|
|
|
Options: |
|
-h, --help Show this help message |
|
-v, --version Show version information |
|
-n, --dry-run Show what would be done without making changes |
|
|
|
Examples: |
|
${SCRIPT_NAME} # Link all submodules |
|
${SCRIPT_NAME} --dry-run # Preview changes |
|
|
|
EOF |
|
} |
|
|
|
# Main function |
|
main() { |
|
local dry_run="" |
|
|
|
# Parse arguments |
|
while [[ $# -gt 0 ]]; do |
|
case $1 in |
|
-h | --help) |
|
usage |
|
exit 0 |
|
;; |
|
-v | --version) |
|
printf '%s version %s\n' "${SCRIPT_NAME}" "${SCRIPT_VERSION}" |
|
exit 0 |
|
;; |
|
-n | --dry-run) |
|
dry_run=1 |
|
shift |
|
;; |
|
-*) |
|
print_error "Unknown option: $1" |
|
usage |
|
exit 1 |
|
;; |
|
*) break ;; |
|
esac |
|
done |
|
|
|
# Get the repo root (where we're running from) |
|
local repo_root git_dir git_common_dir main_repo_root |
|
repo_root="$(git rev-parse --show-toplevel)" |
|
cd "${repo_root}" |
|
|
|
# Get the git directory for this repo |
|
git_dir="$(git rev-parse --git-dir)" |
|
|
|
# Get the common git directory (shared across worktrees) |
|
git_common_dir="$(git rev-parse --git-common-dir)" |
|
|
|
# Convert to absolute paths |
|
git_dir="$(cd "${git_dir}" && pwd)" |
|
git_common_dir="$(cd "${git_common_dir}" && pwd)" |
|
|
|
# Check if we're in a worktree |
|
if [[ "${git_dir}" == "${git_common_dir}" ]]; then |
|
print_info "Not in a worktree (this is the main repo). Nothing to do." |
|
exit 0 |
|
fi |
|
|
|
print_info "Detected worktree at: ${repo_root}" |
|
print_info "Git dir: ${git_dir}" |
|
print_info "Main repo git dir: ${git_common_dir}" |
|
|
|
# The main repo root is the parent of .git |
|
main_repo_root="$(dirname "${git_common_dir}")" |
|
print_info "Main repo root: ${main_repo_root}" |
|
|
|
# Get list of submodules from .gitmodules |
|
if [[ ! -f .gitmodules ]]; then |
|
print_info "No .gitmodules file found. No submodules to link." |
|
exit 0 |
|
fi |
|
|
|
# Parse submodule paths safely using mapfile |
|
local submodules_output |
|
submodules_output=$(git config --file .gitmodules --get-regexp path | awk '{print $2}') |
|
|
|
if [[ -z "${submodules_output}" ]]; then |
|
print_info "No submodules found in .gitmodules" |
|
exit 0 |
|
fi |
|
|
|
# Read submodules into array safely |
|
local -a submodules |
|
mapfile -t submodules <<<"${submodules_output}" |
|
|
|
local linked=0 skipped=0 |
|
local submodule submodule_path main_module_git submodule_git_file current_gitdir |
|
|
|
for submodule in "${submodules[@]}"; do |
|
submodule_path="${repo_root}/${submodule}" |
|
main_module_git="${git_common_dir}/modules/${submodule}" |
|
submodule_git_file="${submodule_path}/.git" |
|
|
|
# Check if submodule directory exists |
|
if [[ ! -d "${submodule_path}" ]]; then |
|
print_warning "Submodule directory not found: ${submodule} (skipping)" |
|
skipped=$((skipped + 1)) |
|
continue |
|
fi |
|
|
|
# Check if main repo has this submodule's git data |
|
if [[ ! -d "${main_module_git}" ]]; then |
|
print_warning "Main repo doesn't have git data for: ${submodule} (skipping)" |
|
print_warning " Expected: ${main_module_git}" |
|
skipped=$((skipped + 1)) |
|
continue |
|
fi |
|
|
|
# Check current state |
|
if [[ -f "${submodule_git_file}" ]]; then |
|
current_gitdir=$(sed 's/^gitdir: //' <"${submodule_git_file}") |
|
|
|
# Check if already pointing to main repo (absolute path) |
|
if [[ "${current_gitdir}" == "${main_module_git}" ]]; then |
|
print_info "Already linked: ${submodule}" |
|
skipped=$((skipped + 1)) |
|
continue |
|
fi |
|
fi |
|
|
|
if [[ -n "${dry_run}" ]]; then |
|
print_info "[DRY-RUN] Would link: ${submodule} -> ${main_module_git}" |
|
linked=$((linked + 1)) |
|
continue |
|
fi |
|
|
|
# Backup existing .git file before modification |
|
if [[ -f "${submodule_git_file}" ]]; then |
|
cp "${submodule_git_file}" "${submodule_git_file}.bak" |
|
BACKUP_FILES+=("${submodule_git_file}.bak") |
|
fi |
|
|
|
# Write the .git file with absolute path to main repo's module |
|
printf 'gitdir: %s\n' "${main_module_git}" >"${submodule_git_file}" |
|
|
|
# Verify it works |
|
if (cd "${submodule_path}" && git status --short >/dev/null 2>&1); then |
|
print_success "Linked: ${submodule} -> ${main_module_git}" |
|
linked=$((linked + 1)) |
|
else |
|
print_error "Failed to link: ${submodule}" |
|
# Error trap will handle rollback |
|
exit 1 |
|
fi |
|
done |
|
|
|
printf '\n' |
|
if [[ -n "${dry_run}" ]]; then |
|
print_info "Dry-run summary: ${linked} would be linked, ${skipped} skipped" |
|
else |
|
print_success "Summary: ${linked} linked, ${skipped} skipped" |
|
|
|
# Clean up empty worktree module directories if they exist |
|
local worktree_modules="${git_dir}/modules" |
|
if [[ -d "${worktree_modules}" ]]; then |
|
print_info "Cleaning up unused worktree module directories..." |
|
rm -rf "${worktree_modules}" |
|
print_info "Removed: ${worktree_modules}" |
|
fi |
|
|
|
# Cleanup backup files on success |
|
cleanup_backups |
|
fi |
|
} |
|
|
|
# Run main function |
|
main "$@" |