Last active
June 18, 2022 19:51
-
-
Save zambonin/866bf3506a3726c8a4b04a4f71e858df to your computer and use it in GitHub Desktop.
Merge multiple local Git repositories.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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