考虑以下场景:

我在自己的Git repo中开发了一个小型实验项目a。它现在已经成熟,我希望A成为更大的项目B的一部分,该项目有自己的大仓库。现在我想将A添加为B的子目录。

我如何将A合并为B,而不丢失任何方面的历史?


当前回答

git子树很好,但它可能不是您想要的。

例如,如果projectA是在B中创建的目录,在git子树之后,

git log projectA

仅列出一个提交:合并。合并项目的提交针对不同的路径,因此不会显示。

格雷格·休吉尔(Greg Hewgill)的答案最接近,但实际上并没有说明如何重写路径。


解决方案出奇地简单。

(1) 在A中,

PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed "s,\t,&'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

注意:这将重写历史;你可能想先备份a。

注意Bene:如果在文件名或路径中使用非ascii字符(或白色字符),则必须修改sed命令中的替代脚本。在这种情况下,“ls files-s”生成的记录中的文件位置以引号开头。

(2) 然后在B中,运行

git pull path/to/A

瞧!在B中有一个projectA目录。如果运行git-log-projectA,您将看到a中的所有提交。


在我的例子中,我需要两个子目录,projectA和projectB。在这种情况下,我也执行了步骤(1)到B。

其他回答

如果您试图简单地将两个存储库粘合在一起,那么子模块和子树合并是错误的工具,因为它们不能保留所有的文件历史记录(正如人们在其他答案中所指出的)。请参阅此处的答案,了解简单而正确的方法。

当您希望在一次提交中合并三个或更多项目时,请执行其他答案中所述的步骤(远程添加-f,合并)。然后,(软)将索引重置为旧头(没有合并)。添加所有文件(git-Add-A)并提交它们(消息“将项目A、B、C和D合并到一个项目中”)。这现在是master的提交id。

现在,使用以下内容创建.git/info/places:

<commit-id of master> <list of commit ids of all parents>

运行gitfilter分支--head^。。头头^2.头头^3.头。如果你有三个以上的分支,就加上同样多的头。。当你有树枝的时候,就把头伸过去。要更新标记,请追加--tag-namefilter cat。不要总是添加,因为这可能会导致某些提交的重写。有关详细信息,请参阅过滤器分支的手册页,搜索“移植物”。

现在,你的最后一次承诺与正确的父母相关。

如果要将项目a合并到项目b中:

cd path/to/project-b
git remote add project-a /path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

摘自:git合并不同的存储库?

这种方法对我来说效果很好,它更短,在我看来更干净。

如果您想将project-a放到子目录中,可以使用gitfilter repo(不建议使用过滤器分支)。在上述命令之前运行以下命令:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

合并两个大型存储库,将其中一个放在子目录中的示例:https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

注意:--allow非相关历史参数仅在git>=2.9之后存在。请参阅Git-Git merge文档/-允许不相关的历史记录

更新:按照@jstadler的建议添加了--tags,以便保留标签。

我知道事情已经过去很久了,但我对我在这里找到的其他答案并不满意,所以我写了这样一篇文章:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done

此函数将远程repo克隆到本地repo目录中,合并后将保存所有提交,git日志将显示原始提交和正确路径:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

如何使用:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

如果进行一些更改,您甚至可以将合并的repo的文件/目录移动到不同的路径中,例如:

repo="https://github.com/example/example"
path="$(pwd)"

tmp="$(mktemp -d)"
remote="$(echo "$tmp" | sed 's/\///g' | sed 's/\./_/g')"

git clone "$repo" "$tmp"
cd "$tmp"

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo "$1" | sed 's/\./\\./')"
    to="$(echo "$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed "'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
    mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

cd "$path"
git remote add -f "$remote" "file://$tmp/.git"
git pull "$remote/master"
git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
git remote remove "$remote"
rm -rf "$tmp"

通知路径将通过sed替换,因此请确保在合并后将其移动到正确的路径中。--allow非相关历史参数仅在git>=2.9之后存在。