我们的Git存储库最初是一个大型SVN存储库的一部分,其中每个项目都有自己的树,如下所示:

project1/branches
        /tags
        /trunk
project2/branches
        /tags
        /trunk

显然,使用svn mv将文件从一个文件移动到另一个文件非常容易。但是在Git中,每个项目都在它自己的存储库中,今天我被要求将一个子目录从project2移动到project1。我是这样做的:

$ git clone project2 
$ cd project2
$ git filter-branch --subdirectory-filter deeply/buried/java/source/directory/A -- --all
$ git remote rm origin  # so I don't accidentally overwrite the repo ;-)
$ mkdir -p deeply/buried/different/java/source/directory/B
$ for f in *.java; do 
>  git mv $f deeply/buried/different/java/source/directory/B
>  done
$ git commit -m "moved files to new subdirectory"
$ cd ..
$
$ git clone project1
$ cd project1
$ git remote add p2 ../project2
$ git fetch p2
$ git branch p2 remotes/p2/master
$ git merge p2 # --allow-unrelated-histories for git 2.9+
$ git remote rm p2
$ git push

但这似乎相当复杂。有没有更好的方法来做这类事情呢?还是我采取了正确的方法?

注意,这涉及到将历史合并到现有的存储库中,而不是简单地从另一个存储库的一部分创建一个新的独立存储库(如前面的问题中所述)。


当前回答

我发现Ross Hendrickson的博客很有用。这是一种非常简单的方法,您可以创建应用于新回购的补丁。更多细节请参见链接页面。

它只包含三个步骤(复制自博客):

# Setup a directory to hold the patches
mkdir <patch-directory>

# Create the patches
git format-patch -o <patch-directory> --root /path/to/copy

# Apply the patches in the new repo using a 3 way merge in case of conflicts
# (merges from the other repo are not turned into patches). 
# The 3way can be omitted.
git am --3way <patch-directory>/*.patch

唯一的问题是我不能一次性应用所有补丁

git am --3way <patch-directory>/*.patch

在Windows下,我得到了一个InvalidArgument错误。所以我不得不一个接一个地打补丁。

其他回答

通过使用git-filter-repo,这变得更简单。

为了移动project2/sub/dir到project1/sub/dir:

# Create a new repo containing only the subdirectory:
git clone project2 project2_clone --no-local
cd project2_clone
git filter-repo --path sub/dir

# Merge the new repo:
cd ../project1
git remote add tmp ../project2_clone/
git fetch tmp master
git merge remotes/tmp/master --allow-unrelated-histories
git remote remove tmp

简单地安装工具:pip3 install git-filter-repo (更多细节和选项在README)

# Before: (root)
.
|-- project1
|   `-- 3
`-- project2
    |-- 1
    `-- sub
        `-- dir
            `-- 2

# After: (project1)
.
├── 3
└── sub
    └── dir
        └── 2

试试这个

cd repo1

这将删除除上述目录之外的所有目录,仅为这些目录保留历史记录

git filter-branch --index-filter 'git rm --ignore-unmatch --cached -qr -- . && git reset -q $GIT_COMMIT -- dir1/ dir2/ dir3/ ' --prune-empty -- --all

现在,您可以在git远程中添加新的repo并将其推到该位置

git remote remove origin <old-repo>
git remote add origin <new-repo>
git push origin <current-branch>

添加-f来覆盖

保留目录名

子目录过滤器(或更短的命令git子树)工作得很好,但对我来说并不管用,因为他们从提交信息中删除了目录名。在我的场景中,我只想将一个存储库的部分合并到另一个存储库中,并保留历史记录WITH完整的路径名。

我的解决方案是使用树筛选器,简单地从源存储库的临时克隆中删除不需要的文件和目录,然后通过5个简单步骤将该克隆拉入目标存储库。

# 1. clone the source
git clone ssh://<user>@<source-repo url>
cd <source-repo>
# 2. remove the stuff we want to exclude
git filter-branch --tree-filter "rm -rf <files to exclude>" --prune-empty HEAD
# 3. move to target repo and create a merge branch (for safety)
cd <path to target-repo>
git checkout -b <merge branch>
# 4. Add the source-repo as remote 
git remote add source-repo <path to source-repo>
# 5. fetch it
git pull source-repo master
# 6. check that you got it right (better safe than sorry, right?)
gitk

Git子树直观地工作,甚至保存历史。

使用示例: 将git repo添加为子目录:

git subtree add --prefix foo https://github.com/git/git.git master

解释:

#├── repo_bar
#│   ├── bar.txt
#└── repo_foo
#    └── foo.txt

cd repo_bar
git subtree add --prefix foo ../repo_foo master

#├── repo_bar
#│   ├── bar.txt
#│   └── foo
#│       └── foo.txt
#└── repo_foo
#    └── foo.txt

是的,点击filter-branch的——subdirectory-filter是关键。您使用它的事实本质上证明了没有更简单的方法—您别无选择,只能重写历史,因为您希望最终只得到文件的一个(重命名的)子集,而这根据定义改变了哈希值。由于没有任何标准命令(例如pull)重写历史,因此您无法使用它们来完成此任务。

当然,您可以细化细节—您的一些克隆和分支并不是严格必要的—但是总体方法是好的!遗憾的是它很复杂,但是git的意义当然不是让重写历史变得容易。