Skip to content

Instantly share code, notes, and snippets.

@mbautin
Created September 30, 2011 23:05
Show Gist options
  • Save mbautin/1255268 to your computer and use it in GitHub Desktop.
Save mbautin/1255268 to your computer and use it in GitHub Desktop.
Port git commits from one repository to another
#!/bin/bash
# Created by Mikhail Bautin on 09/30/2011.
#
# Copyright (c) 2011 Facebook Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# Neither the name of the project's author nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
PROG_NAME=${0##*/}
usage() {
cat >&2 <<EOT
Usage: $PROG_NAME -r1 <git_repo1> -d1 <rel_dir1> -c1 <commit1>
-r2 <git_repo2> -d2 <rel_dir2> -b2 <branch2> [--create_branch]
Does a cherry-pick of the given commit in one git repo and puts it on top of
the given branch in another repo. Supports mapping a subdirectory in the first
repository to a different subdirectory in the other repository. Can be used for
porting patches between two branches of the same project that live in two
different repositories. The advantage of this approach over creating a patch
from a commit in the first repository and applying it to the second repository
is that git's three-way merge functionality is utilized, helping reduce the
number of conflicts.
Options:
-r1
First Git repository path.
-d1
Relative directory within the first repository. Only the part of the commit
within this directory is cherry-picked over to the other repository.
-c1
The commit in the first repository to cherry-pick.
-r2
Second Git repository path
-b2
The branch in the second repository to cherry-pick the specified commit on
top of.
-d2
The relative directory within the second repository corresponding to the
specified directory in the first repository.
-cb, --create_branch
Automatically create the target branch in the second repository if it does
not exist yet.
EOT
exit 1
}
check_git_repo() {
local REPO=$1
if [ ! -d "$REPO" ]; then
echo "Directory $REPO does not exist" >&2
exit 1
fi
if [ ! -d "$REPO/.git" ]; then
echo "Directory $REPO does not appear to be a git repository" >&2
exit 1
fi
cd $REPO || exit 1
# Check that there are no unstaged changes
local UNSTAGED_FILES=$(git ls-files -m)
if [ -n "$UNSTAGED_FILES" ]; then
echo "There are unstaged files in repository $REPO." \
"Commit or stash them first." >&2
exit 1
fi
}
abs_dir_path() {
local D=$1
if [ ! -d "$D" ]; then
echo "Directory $D does not exist" >&2
exit 1
fi
local ABS_D=$(cd $D && pwd) || exit 1
if [[ "$ABS_D" != "$D" && ! -d "$ABS_D" ]]; then
echo "Absolute directory $ABS_D for $D does not exist" >&2
exit 1
fi
echo "$ABS_D"
}
check_option() {
local OPTION=$1
local DESC=$2
local VALUE=$3
if [ -z "$VALUE" ]; then
echo "$DESC (option $OPTION) is not specified" >&2
exit 1
fi
}
ensure_branch_exists() {
local REPO=$1
local BRANCH=$2
cd $REPO || exit 1
git branch | perl -p -e 's/^\*{0,1}?\s*//g' | egrep "^$BRANCH\$" >/dev/null
if [[ "${PIPESTATUS[@]}" != "0 0 0" ]]; then
if [ "$CREATE_BRANCH" ]; then
git checkout -b $BRANCH master || exit 1
if [ $? -ne 0 ]; then
echo "Failed to create branch $BRANCH in repository $REPO" >&2
exit 1
fi
else
echo "Branch $BRANCH could not be found within repository $REPO" >&2
exit 1
fi
fi
}
current_branch() {
local REPO=$1
cd "$REPO" || exit 1
git branch 2>/dev/null | awk '/^* / {print $2}'
if [[ ${PIPESTATUS[@]} != "0 0" ]]; then
echo "Could not identify current branch within repository $REPO" >&2
exit 1
fi
}
switch_branch() {
local REPO=$1
local BRANCH=$2
cd $REPO
if [[ $? -eq 0 && -d "$REPO" && -n "$BRANCH" &&
$(current_branch) != "$BRANCH" ]]; then
echo "Switching to branch $BRANCH in repository $REPO"
git checkout "$BRANCH"
fi
}
cleanup() {
echo
echo "Restoring original state."
# Switch back to old branches in each repository
switch_branch "$REPO1" "$PREV_BRANCH1"
switch_branch "$REPO2" "$PREV_BRANCH2"
}
check_rel_dir_exists () {
local DIR="$PWD/$1"
shift
if [ ! -d "$DIR" ]; then
echo "Directory $DIR does not exist $@" >&2
exit 1
fi
}
if [ $# -eq 0 ]; then
usage
fi
unset REPO1 REL_DIR1 COMMIT1 REPO2 REL_DIR2 BRANCH2 CREATE_BRANCH
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) usage ;;
-r1) REPO1=$(cd $2; pwd) || exit 1; check_git_repo "$REPO1"; shift ;;
-d1) REL_DIR1=$2; shift ;;
-c1) COMMIT1=$2; shift ;;
-r2) REPO2=$(cd $2; pwd) || exit 1; check_git_repo "$REPO2"; shift ;;
-d2) REL_DIR2=$2; shift ;;
-b2) BRANCH2=$2; shift ;;
-cb|--create_branch) CREATE_BRANCH=1 ;;
*) echo "Unknown option $1"; exit 1 ;;
esac
shift
done
REPO1=`abs_dir_path $REPO1` || exit 1
REPO2=`abs_dir_path $REPO2` || exit 1
check_option -r1 "First repository path" "$REPO1"
check_option -d1 "Relative directory within first repository" "$REL_DIR1"
check_option -c1 "Commit within first repository" "$COMMIT1"
check_option -r2 "Second repository path" "$REPO2"
check_option -d2 "Relative directory within second repository" "$REL_DIR2"
check_option -b2 "Destination branch in the second repository" "$BRANCH2"
ensure_branch_exists "$REPO2" "$BRANCH2"
# Save current branches in both repositories
PREV_BRANCH1=`current_branch $REPO1` || exit 1
PREV_BRANCH2=`current_branch $REPO2` || exit 1
# Make sure we switch back to old branches on exit (if there is no conflict).
trap "cleanup; exit" INT TERM EXIT
# Switch to the state immediately before the commit of interest in repo 1
cd "$REPO1" || exit 1
git checkout -q "$COMMIT1^" || exit 1
check_rel_dir_exists "$REL_DIR1"
# Create a temporary branch in the second repository and replace the directory
# of interest in that branch with the corresponding directory from the first
# repository immediately before the commit that we want to cherry-pick.
cd "$REPO2" || exit 1
TIMESTAMP=`date +%Y-%m-%d_%H_%M_%S`
TMP_BR2=crosspick_${BRANCH2}_$TIMESTAMP
git checkout -b "$TMP_BR2" "$BRANCH2" || exit 1
check_rel_dir_exists "$REL_DIR2" "in revision $BRANCH2 in repository $REPO2"
rm -rf "$REL_DIR2" || exit 1
cp -R "$REPO1/$REL_DIR1" "$REL_DIR2" || exit 1
git add -A || exit 1
git commit -m "Automatic commit by $PROG_NAME on $TIMESTAMP: replacing the
contents of $REL_DIR2 with the contents of $REPO1/$REL_DIR1 immediately
before the commit $COMMIT1."
# Now advance to the commit of interest in the first repository.
cd "$REPO1" || exit 1
git checkout "$COMMIT1" || exit 1
check_rel_dir_exists "$REL_DIR1" "in revision $COMMIT1 in repository $REPO1"
COMMIT1_MSG=$(git show -s --format=%B) || exit 1
# Sync the changes from the commit of interest to the second repository
rsync -az "$REPO1/$REL_DIR1/" "$REPO2/$REL_DIR2/" || exit 1
# Go to the second repository and create another commit (this time much
# smaller) representing the commit of interest in the first repository.
cd "$REPO2" || exit 1
git add -A || exit 1
git commit -m "$COMMIT1_MSG" || exit 1
# Switch to the target branch in the second repository and cherry-pick the
# newly created commit similar to the original commit in the first repo.
# This is where we might encounter conflicts.
git checkout "$BRANCH2" || exit 1
git cherry-pick "$TMP_BR2"
if [ $? -ne 0 ]; then
echo
echo "WARNING: cherry-pick failed, most likely due to merge conflicts." >&2
echo "Please resolve the conflicts in the directory below and commit:" >&2
echo >&2
echo "$REPO2" >&2
echo >&2
echo "To abort the merge: git reset --merge"
echo >&2
git status
# Avoid changing to the previous branch in the second repository.
unset PREV_BRANCH2
fi
echo "Deleting temporary branch $TMP_BR2"
git branch -D "$TMP_BR2"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment