Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Created June 27, 2025 09:08
Show Gist options
  • Save mindplay-dk/55f8a8f81049b2fd7f44dff3623659b2 to your computer and use it in GitHub Desktop.
Save mindplay-dk/55f8a8f81049b2fd7f44dff3623659b2 to your computer and use it in GitHub Desktop.
Git monorepo merge script

Create a working directory and move into it.

Put the merge.sh script in the folder and chmod +x ./merge.sh.

Download git-filter-repo and put it in the folder, then chmod +x ./git-filter-repo.

Clone the repositories you want to merge, e.g.:

git clone https://____/frontend ./frontend
git clone https://____/backend ./backend
...

Then run the script with ./merge.sh.

#!/bin/bash
set -euo pipefail
# --- Configuration ---
# Get the absolute path of the script's location.
REPO_DIR=$(pwd)
# List of repository directories to merge, separated by spaces.
REPOS="frontend backend"
# The name of the main branch in the source repositories.
MAIN="release"
# The name of the final merged directory.
MERGED_DIR="merged"
# --- Setup ---
# Add current directory to PATH if git-filter-repo is present locally.
# This allows the script to find the tool without a system-wide installation.
if [ -x "./git-filter-repo" ]; then
export PATH="$(pwd):$PATH"
fi
# --- Sanity Checks ---
# Check if git-filter-repo is available.
if ! command -v git-filter-repo &> /dev/null; then
echo "Error: git-filter-repo could not be found." >&2
echo "Please install it or place the executable in the current directory." >&2
exit 1
fi
# Check if the specified repository directories exist.
for repo in $REPOS; do
if [ ! -d "$repo/.git" ]; then
echo "Error: Directory '$repo' does not exist or is not a git repository." >&2
exit 1
fi
done
echo "✅ All checks passed. Starting merge process..."
# --- Step 1: Rewrite history for each repo into a temporary location ---
# We use a temporary directory to avoid modifying the original repositories.
# The '$$' appends the process ID to make the directory name unique.
TEMP_DIR="/tmp/git-merge-repos-$$"
# Set up a trap to automatically clean up the temporary directory on script exit,
# regardless of whether it succeeds, fails, or is interrupted (e.g., by Ctrl+C).
trap 'echo "Exiting. Cleaning up temporary directory..." >&2; rm -rf "$TEMP_DIR"' EXIT
echo "Creating temporary directory at $TEMP_DIR"
mkdir -p "$TEMP_DIR"
for repo in $REPOS; do
echo "--------------------------------------------------"
echo "Processing: $repo"
echo "--------------------------------------------------"
# Create a fresh, clean clone in the temp directory to work on.
# Using file:// ensures it works correctly with local paths.
echo "Cloning '$repo' to a temporary location..."
git clone "file://$REPO_DIR/$repo" "$TEMP_DIR/$repo"
cd "$TEMP_DIR/$repo"
# Rewrite the history of the clone using the much improved method.
echo "Rewriting history for '$repo'..."
git-filter-repo \
--to-subdirectory-filter "$repo" \
--message-callback "return message" \
--commit-callback "commit.committer_date = commit.author_date" \
--force
echo "Finished processing '$repo'."
done
# --- Step 2: Create the new merged repository ---
echo "--------------------------------------------------"
echo "Creating the final '$MERGED_DIR' repository"
echo "--------------------------------------------------"
cd "$REPO_DIR"
# Clean up the target directory from any previous run before we start.
rm -rf "$MERGED_DIR"
mkdir "$MERGED_DIR"
cd "$MERGED_DIR"
# Initialize the repository with a modern default branch name.
git init -b main
# --- Step 3: Merge all the rewritten repositories ---
for repo in $REPOS; do
echo "Merging history from '$repo'..."
# Add the rewritten repo from our temp directory as a remote.
git remote add "$repo" "$TEMP_DIR/$repo"
# Fetch its content.
git fetch "$repo"
# Merge its main branch into our new history.
# --allow-unrelated-histories is essential because the repos
# do not share a common root commit.
# We create a clear merge commit message to document the action.
git merge "$repo/$MAIN" --allow-unrelated-histories -m "feat: Merge repository '$repo'"
# Clean up by removing the remote connection.
git remote remove "$repo"
done
# --- Step 4: Final Cleanup ---
# The temporary directory ($TEMP_DIR) is removed automatically by the 'trap'
# we set at the beginning of the script. This ensures cleanup happens
# even if the script fails partway through.
echo "--------------------------------------------------"
echo "✅ Success!"
echo "The merged repository is now available in the '$MERGED_DIR' directory."
echo "Run 'cd $MERGED_DIR && git log --oneline --graph' to inspect the new history."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment