Created
May 23, 2024 17:47
-
-
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
This file contains hidden or 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
#!/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