Created
September 30, 2011 23:05
-
-
Save mbautin/1255268 to your computer and use it in GitHub Desktop.
Port git commits from one repository to another
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
#!/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