Skip to content

Instantly share code, notes, and snippets.

@zambonin
Last active June 18, 2022 19:51
Show Gist options
  • Save zambonin/866bf3506a3726c8a4b04a4f71e858df to your computer and use it in GitHub Desktop.
Save zambonin/866bf3506a3726c8a4b04a4f71e858df to your computer and use it in GitHub Desktop.
Merge multiple local Git repositories.
#!/usr/bin/env bash
# A shell script that merges local Git repositories into a new repository. The
# script accepts multiple directories and/or glob patterns as arguments. Each
# local repository will be moved into a subdirectory, named by the user via
# stdin.
#
# Dependencies: bash 4+, git and git-filter-repo.
GIT_REPOS=( "$@" )
if [ ${#GIT_REPOS[@]} -eq 0 ] ; then
echo "Please specify local Git repositories to merge."
exit 1
fi
declare -a ABS_GIT_REPOS
for FOLDER in "${GIT_REPOS[@]}" ; do
ABS_PATH="$(readlink -f "$FOLDER")"
if [ ! -d "$FOLDER/.git" ] ; then
echo "Skipped $ABS_PATH (no Git repository found)"
continue
fi
ABS_GIT_REPOS+=("$ABS_PATH")
done
NEW_REPO="$(mktemp --directory --suffix="-merge-repo")"
git init "$NEW_REPO"
(
cd "$NEW_REPO" || exit
git commit --allow-empty --message "Initial commit"
for FOLDER in "${ABS_GIT_REPOS[@]}" ; do
REMOTE_NAME="$(echo -n "$FOLDER" | sha256sum | cut -c-32)"
REMOTE_HEAD="$REMOTE_NAME/HEAD"
git remote add --fetch --tags "$REMOTE_NAME" "$FOLDER"
git remote set-head --auto "$REMOTE_NAME" 2>/dev/null
if [ "$?" -eq 1 ] ; then
echo "Skipped $FOLDER (no HEAD found)"
continue
fi
read -r -p "Path to move files from $FOLDER [$REMOTE_NAME]: " NEW_FOLDER
NEW_FOLDER="${NEW_FOLDER:-REMOTE_NAME}"
MESSAGE="commit.message = b'$NEW_FOLDER: ' + commit.message; return commit"
git-filter-repo --force --refs "$REMOTE_NAME" --prune-empty always \
--commit-callback "$MESSAGE" --to-subdirectory-filter "$NEW_FOLDER"
git merge --allow-unrelated-histories --no-edit "$REMOTE_HEAD"
done
printf -v LOG_FMT '%s' \
"%at pick %h %s %x07@@@@@@@@@@" \
" exec GIT_COMMITTER_NAME='%cn' GIT_COMMITTER_EMAIL='%ce'" \
" GIT_COMMITTER_DATE='%cd' git commit --amend --no-edit"
# The pipeline below acts on the git-log output via the following steps:
#
# * sort by commit author date obtained as Unix timestamps (this is
# a personal preference and can be changed as desired);
# * substitute the BEL characters to insert the `exec` git-rebase commands
# directly below their respective `pick`s. Inserting the timestamp before
# the `exec` screws up sorting if there are commits done at the same time;
# * remove the last `exec` command because it will fail on an empty
# commit. This cannot be done on the previous `sed` pass because it would
# delete the line before being split, and so git-rebase would complain
# about missing commits;
# * lastly, remove the first twelve characters corresponding to the
# timestamps and the repeated @ characters so that the git-rebase todo
# syntax is correct.
#
# A temporary file is needed because Git may fail due to an excessively long
# argument list.
REBASE_COMMANDS="$(mktemp --suffix="-rebase")"
git --no-pager log --no-merges --pretty="$LOG_FMT" \
| sort -nk1 \
| sed 's/\a/\n/' \
| sed '$d' \
| cut -c12- \
> "$REBASE_COMMANDS"
# Now we pass the organized git-rebase todo via the GIT_SEQUENCE_EDITOR
# environment variable, which redirects it to the correct file.
GIT_SEQUENCE_EDITOR="cat $REBASE_COMMANDS >" git rebase -i --root
echo "Finished creation of merged repository at $NEW_REPO"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment