Skip to content

Instantly share code, notes, and snippets.

@liamhuber
Created May 23, 2024 17:47
Show Gist options
  • Save liamhuber/73a5ff0592d250686245741283c52702 to your computer and use it in GitHub Desktop.
Save liamhuber/73a5ff0592d250686245741283c52702 to your computer and use it in GitHub Desktop.
Copy select files/directories from one repo to another repo with their commit history
#!/bin/bash
docstring="This script copies specific paths from one Git repository to a new branch in another Git repository, including git commit history.
It creates temporary branches and uses git-filter-repo to filter and merge the specified paths.
Arguments:
\$1: Source repository directory (absolute or relative path).
\$2: Target repository directory (absolute or relative path).
--source-starting-branch | -ssb: Branch in the source repository to start from (default: \"main\").
--target-starting-branch | -tsb: Branch in the target repository to start from (default: \"main\").
--source-temporary-branch | -stb: Temporary branch name in the source repository (default: \"git-copy-source\").
--target-new-branch | -tnb: New branch name to be created in the target repository (default: \"git-copy-target\").
--target-temporary-source | -tts: Temporary remote name in the target repository for the source repo (default: \"git-copy-repo-source\").
--target-temporary-branch | -ttb: Temporary branch name in the target repository for the filtered content (default: \"git-copy-branch-source\").
--source-filter-paths | -sfp: Space-separated list of paths to be copied from the source repository.
Usage Example:
./git-copy.sh /path/to/source/repo /path/to/target/repo --source-starting-branch dev --target-new-branch new-feature --source-filter-paths src/docs src/tests"
# PARSE ARGS
## Function to get the full path of a directory
get_full_path() {
local dir="$1"
if [ -d "$dir" ]; then
(cd "$dir" && pwd)
else
echo "Error: $dir is not a directory."
exit 1
fi
}
## Function to check if a path exists
check_path_existence() {
if [ ! -e "$1" ]; then
echo "Error: $1 does not exist."
exit 1
fi
}
## Check for at least two positional arguments
if [ $# -lt 2 ]; then
echo "${docstring}"
exit 1
fi
## Positional arguments: verify and resolve to full paths
source_repo=$(get_full_path "$1") || { echo "Error: $1 is not a directory."; exit 1; }
target_repo=$(get_full_path "$2") || { echo "Error: $2 is not a directory."; exit 1; }
here=`pwd`
## Default values for named arguments
source_starting_branch="main"
target_starting_branch="main"
source_temporary_branch="git-copy-source"
target_new_branch="git-copy-target"
target_temporary_source="git-copy-repo-source"
target_temporary_branch="git-copy-branch-source"
filter_paths=""
## Parse named arguments
while [[ $# -gt 0 ]]; do
case $1 in
--source-starting-branch | -ssb)
source_starting_branch="$2"
shift 2
;;
--target-starting-branch | -tsb)
target_starting_branch="$2"
shift 2
;;
--source-temporary-branch | -stb)
source_temporary_branch="$2"
shift 2
;;
--target-new-branch | -tnb)
target_new_branch="$2"
shift 2
;;
--target-temporary-source | -tts)
target_temporary_source="$2"
shift 2
;;
--target-temporary-branch | -ttb)
target_temporary_branch="$2"
shift 2
;;
--source-filter-paths | -sfp)
shift
filter_paths="$@"
break
;;
*)
shift
;;
esac
done
## Fail if there is no filter
if [ -z "$filter_paths" ]; then
echo "Error: --source-filter-paths cannot be empty."
exit 1
fi
## Verify filter selections exist
for path in ${filter_paths}; do
check_path_existence "${source_repo}/${path}"
done
## Get the current branch of the source
cd ${source_repo}
source_cleanup_branch=`git rev-parse --abbrev-ref HEAD`
cd ${here}
## Convert paths into git filter-repo arguments
## i.e. --path p1 --path p2, etc.
filter_args=""
for arg in ${filter_paths}; do
filter_args+=" --path $arg"
done
# DO WORK
cleanup_source() {
echo " ...Cleaning up source"
local source_repo=$1
local source_cleanup_branch=$2
local source_temporary_branch=$3
local here=$4
cd ${source_repo}
git checkout ${source_cleanup_branch}
git branch -D ${source_temporary_branch}
cd ${here}
}
echo "git-copy copying ${source_repo}:${source_starting_branch} (${filter_paths}) to ${target_repo}:${target_new_branch}"
## Make and filter a temporary branch in the source
echo " ...Checking out source ${source_repo}:${source_starting_branch}"
cd ${source_repo}
git checkout "${source_starting_branch}" 2>/dev/null
if [[ $? -ne 0 ]]; then
echo "Error: ${source_repo}:${source_starting_branch} not found."
exit 1
fi
echo " ...Making a new temporary source branch ${source_repo}:${source_temporary_branch}"
git checkout -b ${source_temporary_branch}
echo " ...Filtering content ${filter_paths}"
git filter-repo ${filter_args} --refs refs/heads/${source_temporary_branch} --force
cd ${here}
## Make a new branch in the target and merge the source in
echo " ...Checking out target ${target_repo}:${target_starting_branch}"
cd ${target_repo}
echo `pwd`
echo " ...Making a new target branch ${target_repo}:${target_new_branch}"
git checkout "${target_starting_branch}"
checkout_exit=$?
if [[ ${checkout_exit} -ne 0 ]]; then
echo "Error: ${target_repo}:${target_starting_branch} not found."
cleanup_source ${source_repo} ${source_cleanup_branch} ${source_temporary_branch} ${here}
exit 1
fi
git checkout -b ${target_new_branch}
echo " ...Creating temporary remote ${target_temporary_source} and branch ${target_temporary_branch}"
git remote add ${target_temporary_source} ${source_repo}
git fetch ${target_temporary_source}
git branch ${target_temporary_branch} remotes/${target_temporary_source}/${source_temporary_branch}
echo " ...Merging in temporary branch with remote filtered content"
git merge ${target_temporary_branch} --allow-unrelated-histories
echo " ...Cleaning up target"
git branch -D ${target_temporary_branch}
git remote remove ${target_temporary_source}
cd ${here}
## Clean up the source repo
cleanup_source ${source_repo} ${source_cleanup_branch} ${source_temporary_branch} ${here}
echo "git-copy complete"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment