将目录移动到另一个仓库并保留历史记录的git操作

85

首先,很抱歉提出这个问题。已经有很多关于它的主题了。但是我并没有太多的帮助。因为我对Git不熟悉。

我正在将一个文件夹从一个Git仓库移动到另一个(已经存在的)Git仓库。例如:

repo-1
---- dir1
---- dir2
---- dir3
---- dir-to-move
---- dir5

repo-2
---- dir1
---- dir2
---- dir3

最终我希望仓库看起来像这样

repo-1
---- dir1
---- dir2
---- dir3
---- dir-to-move
---- dir5

repo-2
---- dir1
---- dir2
---- dir3
---- dir-to-move

即将在两个版本库中同时存在dir-to-move。但最终我将把最新更改迁移到repo-2并从repo-1中删除dir-to-move

我的初步研究让我相信我需要使用filter-branch。例如:

如何使用“git format-patch”和“git am”移动文件以保留历史记录从而将文件从一个git repo移动到另一个git repo

我后来了解到subtree已经取代了那种方法。然而它并没有做我期望的事情。我认为我应该能够做这样的事情:

repo-1工作区中

git subtree split -P dir-to-move -b split

split分支筛选为只包含dir-to-move及其历史记录。 然后在repo-2的工作区中。

git remote add repo-1 repo-1-url.git
git subtree add --prefix dir-to-move split
这确实将代码移动了。同时,它也有点包含了历史记录。
例如。
cd repo-2
git log

显示来自 repo-1 的提交记录

但是

cd repo-2
git log dir-to-move

仅显示“从提交中添加目录移动”的内容……

即,历史记录已包含,但在检查特定文件/目录时不会显示出来。

我该如何正确地做到这一点?


1
看起来 Git 无法跟随目录移动。如果您指定某个文件而不是目录,它会跟随其移动吗? - max630
10个回答

70

使用git subtree确实可以实现这一点。

在repo-1中创建一个子树:

git subtree split -P dir-to-move -b <split>

split分支现在只包含dir-to-move目录。你需要将该分支从repo-1拉取到repo-2的一个分支中。

如果repo-2是一个新的仓库(例如,刚初始化了git init),那么就像检出没有历史记录的分支一样简单,然后从repo-1拉取。

cd repo-2
git checkout <branch>
git pull <path-to-repo-1.git> <split>

但是,如果repo-2是一个已经存在并且已经有提交记录的仓库(就像这个问题中的情况),您需要从repo-2的孤立分支进行合并:

cd repo-2
git checkout --orphan <temp>                    # Create a branch with no history
git pull <path-to-repo-1.git> <split>           # Pull the commits from the subtree
git checkout <branch>                           # Go back to the original branch
git merge --allow-unrelated-histories <temp>    # Merge the unrelated commits back
git branch -d <temp>                            # Delete the temporary branch

3
对我来说,那是最有帮助的答案。我想补充一点:--allow-unrelated-histories也可以直接用于git pull命令中,这可能会导致某些合并冲突,如果您的repo-2中已经有相同的文件,但如果很容易解决这些冲突,那么这是最快的解决方案,我个人认为。 - xor_eq
3
这对我很有效,但丢失了父文件夹。在这里找到了解决方案。 - mivilar
这个页面对我来说完美无缺:我的 repo-2 是一个全新的,没有任何提交记录。 - Happy
这是我认为最简单的方法。甚至比我从https://www.johno.com/move-directory-between-repos-with-git-history所知道的更清晰。 - Maf
4
git pull <path-to-repo-1.git> <split> 会生成错误信息 fatal: Updating an unborn branch with changes added to the index. 目前还没有关于这个错误的文档资料。 - Arthur
显示剩余2条评论

34

我不能帮助你处理git subtree,但对于filter-branch,这是可能的。

首先,您需要创建一个包含源分支和目标分支的公共存储库。可以通过添加新的“remote”以及从新的“remote”获取来完成这个过程。

在源分支上使用filter-branch命令,将除了dir-to-move之外的所有目录rm -rf删除。之后,您将拥有一个可以清晰地重新基于或合并到目标分支的提交历史记录。我认为最简单的方法是从源分支中cherry-pick所有非空提交。可以通过运行git rev-list --reverse source-branch --dir-to-move命令获取这些提交的列表。

当然,如果dir-to-move的历史记录是非线性的(已经包含合并提交),那么cherry-pick将无法保留它,因此必须使用git merge代替。

例如,创建公共存储库:

cd repo-2
git remote add source ../repo-1
git fetch source

示例筛选分支

cd repo-2
git checkout -b source-master source/master
CMD="rm -rf dir1 dir2 dir3 dir5"
git filter-branch --tree-filter "$CMD"

示例: 将某个提交选入目标主分支

cd repo-2
git checkout master
git cherry-pick `git rev-list --reverse source-master -- dir-to-move`

3
看起来这是唯一的解决方案。我按照这里的方法进行操作:http://blog.mattsch.com/2015/06/19/move-directory-from-one-repository-to-another-preserving-history/。 - Shane Gannon
3
值得注意的是,如果“dir-to-move”中的内容以前曾在repo-1内移动过,则此解决方案将截断这些文件的历史记录,并仅保留它们在repo-1中“dir-to-move”位置存在的时间。 - mtalexan
2
对于2021年或之后来到这里的任何人,请注意,在过去的几年中,git约定已更改为使用“main”而不是“master”,因此您可能需要在本答案的代码中将“master”替换为“main”。 - mhucka
1
如果您不想从repo-1中拉取标签,可以使用git fetch source --no-tags命令。 - m4r73n
错误:提交<SHA1>是一个合并,但未给出-m选项。 致命错误:无法执行cherry-pick操作。 - BrandonL

25

经过多次尝试,以下方法对我有效。

将这两个仓库都克隆到临时工作区。

git clone <repourl>/repo-1.git 
git clone <repourl>/repo-2.git
cd repo-1
git remote rm origin # delete link to original repository to avoid any accidental remote changes
git filter-branch --subdirectory-filter dir-to-move -- --all  # dir-to-move is being moved to another repo.  This command goes through history and files, removing anything that is not in the folder.  The content of this folder will be moved to root of the repo as a result. 
# This folder has to be moved to another folder in the target repo.  So, move everything to another folder.
git filter-branch -f --index-filter \
'git ls-files -s | /usr/local/bin/sed -e "s/\t\"*/&dir-to-move\//" |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
        git update-index --index-info &&
 mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD
# Above command will go through history and rewrite the history by adding dir-to-move/ to each files.  As a result, all files will be moved to a subfolder /dir-to-move.  Make sure to use gnu-sed as OSX sed doesn't handle some extensions correctly.  For eg. \t

现在切换到目标仓库并从源中获取所有内容。
git clone repo-2
git remote add master ../repo-1/
git pull master master --allow-unrelated-histories
git push  # push everything to remote 

上述步骤假定源和目标都使用主分支,但标签和分支被忽略了。

2
我在Windows + Git Bash上使用这种方法遇到了问题。我用实际的sed命令替换了路径中的usr\bin路径,但是我得到了Rewrite ... mv: cannot stat my-repo-local-path/.git-rewrite/t/../index.new': No such file or directory`的错误提示。 - GabiM
很好的git技巧,用于纠正目标路径。完美地工作了。 - nharrer
1
在 macOS 上,我必须调整 /usr/local/bin/sed -e "s/\t\"*/&dir-to-move\//" 命令。 用 sed 替换 /usr/local/bin/sed 并将 \t 替换为提供的 ^+V Tab - Kuba
^+V Tab是什么?我尝试了^+V,但没有任何反应。然后在按住^+V的同时,我按下Tab键,它就会切换到一个新的不同的选项卡。 - Whitecat
1
你可能需要在这行命令中添加 "--no-ff" 标志:`git pull master master --no-ff --allow-unrelated-histories` - SergeyM

19

我移动 dir-to-moverepo 2 的方法。首先在新位置克隆 repo 1 并切换到 repo 1 文件夹。在我的情况下,我正在将文件夹从 repo 1 分支 develop 移动到 repo 2,其中 develop 还不存在。

git filter-branch --subdirectory-filter dir-to-move -- --all     #line 1
git remote -v                                                    #line 2
git remote set-url origin repo_2_remote_url                      #line 3
git remote -v                                                    #line 4
git push origin develop                                          #line 5

每行的说明:

  1. 清除与 dir-to-move 无关的所有提交,删除该文件夹以外的所有文件,将根文件夹 repo 1 中的所有内容移动。从git文档中引用:

只查看涉及给定子目录的历史记录。 结果将包含该目录(且仅包含该目录)作为其项目根目录

  1. 您仍在指向原始的 repo 1 url
  2. 将当前文件夹的原始url替换为指向 repo 2
  3. 检查URL是否已正确更新
  4. 将新创建的 develop 分支推送到 repo 2

现在您可以克隆或拉取或获取您的 repo 2 存储库。 您将在 repo 2 文件夹下按预期得到 dir-to-move 的内容以及相关历史记录。


这种方法的问题在于它无法移动所有远程分支和标签。 - andresp
它一次只能移动一个分支,如果您找不到更好的方法,可以手动为所有分支执行此操作。 (但是,您是否应该拥有长期存在的分支?!)标签基本上是提交标识符,因此还必须有其他命令。 - GabiM
这并不是关于拥有长期存在的分支,而只是你本地没有检出的远程分支(例如其他人创建的短期分支)。要做到这一点,你应该像这样操作:https://dev59.com/82Ii5IYBdhLWcg3w_AfV#20793890 - andresp
1
这些命令对我很有效!如果可能的话,您能否分享任何命令/设置,以便在将更改推送到另一个分支后可以撤消我的分支过滤器更改? - msayef

10

5

我的最爱解决方案,在使用多年后,是http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/。然而,它似乎已经从谷歌搜索结果中消失,让我担心有一天可能会消失,所以我在这里重现它:

mkdir /tmp/mergepatchs
cd ~/repo/org
export reposrc=myfile.c #or mydir
git format-patch -o /tmp/mergepatchs $(git log $reposrc|grep ^commit|tail -1|awk '{print $2}')^..HEAD $reposrc
cd ~/repo/dest
git am /tmp/mergepatchs/*.patch

2
更简单的方法是:使用git format-patch -o /tmp/mergepatchs --root $reposrc命令。 - sean
这对我来说很有效!只有一个小改变,我使用了git am -3 /tmp/mergepatchs/*.patch来进行三方合并以解决冲突。这里有一些文档:https://git-scm.com/docs/git-am#Documentation/git-am.txt--3 - undefined

2

git:将文件夹从一个仓库移动到另一个仓库并保留历史记录

cd target-repo
git remote add source ../source-repo
git fetch source
git checkout -b source source/master

# filter out history of a single folder
git filter-branch --subdirectory-filter ./dir -- --all

# put the folder history on top of your target repo history
git rebase master

工作正常,但提交历史的时间戳被覆盖了。 - Kay

0

我们这里有两种方法:

  • git filter-branch
  • git subtree

我使用了前一种方法,参考了https://www.johno.com/move-directory-between-repos-with-git-history,并发现了一些更好的简化方法(例如Basin的方法非常好)。

然而,后一种方法更好,我可以指出一些原因:

  1. "git filter-branch存在许多陷阱,可能会产生意外的历史重写混淆(并且由于其性能极差,可能会让您没有足够的时间来调查此类问题)。这些安全和性能问题无法向后兼容地修复,因此不建议使用。" - 您可以在https://git-scm.com/docs/git-filter-branch中了解更多原因。

  2. 当我看到git filter-branch --subdirectory-filter my-dir -- -- all不必要地重写了我的整个存储库历史记录时,我真的不喜欢使用git filter-branch的想法。

  3. 当我发现git subtree split -P dir-to-move -b 可以创建一个仅包含我的文件夹的干净分支时,我非常喜欢它,所以使用Thymo提出的方法真的很干净,我强烈推荐使用这种方法!

也许需要在githug上发布一个脚本。


-1

你甚至可以通过删除本地除dir-to-move以外的所有目录(这样就不需要使用filter-branch),然后将其推送到repo-2来提交repo-1

git clone https://path/to/repo-1
cd repo-1
rm dir1 dir2 dir3 dir5 -rf
git commit -m "Removed unwanted directory for repo-2
cd ../
git clone https://path/to/repo-2
cd repo-2
git remote add repo-1-src ../repo-1
git pull repo-1-src master --allow-unrelated-histories

注意:确保使用--allow-unrelated-histories选项时,git版本为2.9或更高版本。


1
所以,这确实有效,但只能做它所说的事情。基本上,从“repo-1”到删除其他所有内容的提交的历史提交将被塞入您的“repo-2”中。我认为这可以与“filter-branch”一起使用,以便保持repo-2的历史记录完整。@donnie和@basin的答案是完整的解决方案。 - Steven Lu

-11
笨拙的手动解决方案,如果其他方法都失败了:
  1. 从第一个仓库下载所有文件
  2. 创建一个新的仓库
  3. 将第二个仓库克隆到磁盘上
  4. 解压你想保留的第一个仓库文件(新的结构)
  5. 将更改推送到第二个仓库
  6. 确认你得到了想要的内容
  7. 从第一个仓库中删除你不想要的文件/文件夹

但是你会失去历史记录


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接