如何将某些文件从一个git仓库移动到另一个(不是克隆),并保留历史记录。

653

我们的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

但这似乎相当复杂。一般情况下有更好的方法来做这样的事情吗?或者我采取了正确的方法吗?

请注意,这涉及将历史记录合并到现有存储库中,而不是仅从另一个存储库的一部分创建新的独立存储库(如在早期的问题中)。


1
这对我来说听起来是一个合理的方法;我想不出任何明显的方法来显著改进你的方法。很好,Git确实使这变得容易(例如,在Subversion中,我不想尝试在*不同的存储库之间移动文件目录)。 - Greg Hewgill
1
@ebneter - 我已经手动完成了这个操作(将一个svn仓库的历史记录移动到另一个仓库),使用了shell脚本。基本上,我将特定文件/目录的历史记录(差异、提交日志消息)重新应用到第二个仓库中。 - Adam Monsen
1
我想知道为什么你不用 git fetch p2 && git merge p2 而是用 git fetch p2 && git branch .. && git merge p2?编辑:好的,看起来你想在一个名为p2的新分支中获取更改,而不是当前分支。 - Lekensteyn
1
有没有办法防止 --filter-branch 破坏目录结构?那个 "git mv" 步骤会导致一个包含文件删除和创建的大量提交。 - Edward Falk
9
2021年,使用git filter-repo是进行此操作的正确工具,而非filter-branch - Ed Randall
显示剩余7条评论
16个回答

2

参考http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/的灵感,我创建了这个PowerShell函数,用于执行相同的操作,在我目前的使用中效果非常好:

# Migrates the git history of a file or directory from one Git repo to another.
# Start in the root directory of the source repo.
# Also, before running this, I recommended that $destRepoDir be on a new branch that the history will be migrated to.
# Inspired by: http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/
function Migrate-GitHistory
{
    # The file or directory within the current Git repo to migrate.
    param([string] $fileOrDir)
    # Path to the destination repo
    param([string] $destRepoDir)
    # A temp directory to use for storing the patch file (optional)
    param([string] $tempDir = "\temp\migrateGit")

    mkdir $tempDir

    # git log $fileOrDir -- to list commits that will be migrated
    Write-Host "Generating patch files for the history of $fileOrDir ..." -ForegroundColor Cyan
    git format-patch -o $tempDir --root -- $fileOrDir

    cd $destRepoDir
    Write-Host "Applying patch files to restore the history of $fileOrDir ..." -ForegroundColor Cyan
    ls $tempDir -Filter *.patch  `
        | foreach { git am $_.FullName }
}

本示例的用法:

git clone project2
git clone project1
cd project1
# Create a new branch to migrate to
git checkout -b migrate-from-project2
cd ..\project2
Migrate-GitHistory "deeply\buried\java\source\directory\A" "..\project1"

在完成此操作后,您可以在合并 migrate-from-project2 分支之前重新组织文件。


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远程,并将其推送到那里。
git remote remove origin <old-repo>

git remote add origin <new-repo>

git push origin <current-branch>

加上 -f 以覆盖


2
警告:git-filter-branch存在大量陷阱,会生成混乱的历史重写。在继续之前按Ctrl-C中止,然后使用其他过滤工具,如'git filter-repo'(https://github.com/newren/git-filter-repo/)。有关更多详细信息,请参见filter-branch手册页面;要消除此警告,请设置FILTER_BRANCH_SQUELCH_WARNING = 1。 - Colin

2
我希望有一个坚固且可重复使用(一键式 + 撤销功能)的东西,所以我编写了以下 bash 脚本。在多个场合中对我很有用,所以我想在这里分享。
它能够将任意文件夹 /path/to/foorepo1 移动到 repo2/some/other/folder/bar 中(文件夹路径可以相同或不同,距离根目录的距离可能不同)。
由于它只涉及与输入文件夹中的文件有关的提交(而不是源存储库中的所有提交),因此即使在提取未在每次提交中触摸的深度嵌套子文件夹时,它应该也很快。
由于它所做的事情是创建一个孤立的分支,并将旧存储库的所有历史记录合并到 HEAD 中,所以即使出现文件名冲突,它也可以工作(当然,最后你需要解决合并)。
如果没有文件名冲突,你只需在最后进行 git commit 即可完成合并。
缺点是它可能无法遵循源存储库中的文件重命名(除了 REWRITE_FROM 文件夹之外),欢迎在 GitHub 上提出拉取请求以适应这一点。
GitHub链接:git-move-folder-between-repos-keep-history
#!/bin/bash

# Copy a folder from one git repo to another git repo,
# preserving full history of the folder.

SRC_GIT_REPO='/d/git-experimental/your-old-webapp'
DST_GIT_REPO='/d/git-experimental/your-new-webapp'
SRC_BRANCH_NAME='master'
DST_BRANCH_NAME='import-stuff-from-old-webapp'
# Most likely you want the REWRITE_FROM and REWRITE_TO to have a trailing slash!
REWRITE_FROM='app/src/main/static/'
REWRITE_TO='app/src/main/static/'

verifyPreconditions() {
    #echo 'Checking if SRC_GIT_REPO is a git repo...' &&
      { test -d "${SRC_GIT_REPO}/.git" || { echo "Fatal: SRC_GIT_REPO is not a git repo"; exit; } } &&
    #echo 'Checking if DST_GIT_REPO is a git repo...' &&
      { test -d "${DST_GIT_REPO}/.git" || { echo "Fatal: DST_GIT_REPO is not a git repo"; exit; } } &&
    #echo 'Checking if REWRITE_FROM is not empty...' &&
      { test -n "${REWRITE_FROM}" || { echo "Fatal: REWRITE_FROM is empty"; exit; } } &&
    #echo 'Checking if REWRITE_TO is not empty...' &&
      { test -n "${REWRITE_TO}" || { echo "Fatal: REWRITE_TO is empty"; exit; } } &&
    #echo 'Checking if REWRITE_FROM folder exists in SRC_GIT_REPO' &&
      { test -d "${SRC_GIT_REPO}/${REWRITE_FROM}" || { echo "Fatal: REWRITE_FROM does not exist inside SRC_GIT_REPO"; exit; } } &&
    #echo 'Checking if SRC_GIT_REPO has a branch SRC_BRANCH_NAME' &&
      { cd "${SRC_GIT_REPO}"; git rev-parse --verify "${SRC_BRANCH_NAME}" || { echo "Fatal: SRC_BRANCH_NAME does not exist inside SRC_GIT_REPO"; exit; } } &&
    #echo 'Checking if DST_GIT_REPO has a branch DST_BRANCH_NAME' &&
      { cd "${DST_GIT_REPO}"; git rev-parse --verify "${DST_BRANCH_NAME}" || { echo "Fatal: DST_BRANCH_NAME does not exist inside DST_GIT_REPO"; exit; } } &&
    echo '[OK] All preconditions met'
}

# Import folder from one git repo to another git repo, including full history.
#
# Internally, it rewrites the history of the src repo (by creating
# a temporary orphaned branch; isolating all the files from REWRITE_FROM path
# to the root of the repo, commit by commit; and rewriting them again
# to the original path).
#
# Then it creates another temporary branch in the dest repo,
# fetches the commits from the rewritten src repo, and does a merge.
#
# Before any work is done, all the preconditions are verified: all folders
# and branches must exist (except REWRITE_TO folder in dest repo, which
# can exist, but does not have to).
#
# The code should work reasonably on repos with reasonable git history.
# I did not test pathological cases, like folder being created, deleted,
# created again etc. but probably it will work fine in that case too.
#
# In case you realize something went wrong, you should be able to reverse
# the changes by calling `undoImportFolderFromAnotherGitRepo` function.
# However, to be safe, please back up your repos just in case, before running
# the script. `git filter-branch` is a powerful but dangerous command.
importFolderFromAnotherGitRepo(){
    SED_COMMAND='s-\t\"*-\t'${REWRITE_TO}'-'

    verifyPreconditions &&
    cd "${SRC_GIT_REPO}" &&
      echo "Current working directory: ${SRC_GIT_REPO}" &&
      git checkout "${SRC_BRANCH_NAME}" &&
      echo 'Backing up current branch as FILTER_BRANCH_BACKUP' &&
      git branch -f FILTER_BRANCH_BACKUP &&
      SRC_BRANCH_NAME_EXPORTED="${SRC_BRANCH_NAME}-exported" &&
      echo "Creating temporary branch '${SRC_BRANCH_NAME_EXPORTED}'..." &&
      git checkout -b "${SRC_BRANCH_NAME_EXPORTED}" &&
      echo 'Rewriting history, step 1/2...' &&
      git filter-branch -f --prune-empty --subdirectory-filter ${REWRITE_FROM} &&
      echo 'Rewriting history, step 2/2...' &&
      git filter-branch -f --index-filter \
       "git ls-files -s | sed \"$SED_COMMAND\" |
        GIT_INDEX_FILE=\$GIT_INDEX_FILE.new git update-index --index-info &&
        mv \$GIT_INDEX_FILE.new \$GIT_INDEX_FILE" HEAD &&
    cd - &&
    cd "${DST_GIT_REPO}" &&
      echo "Current working directory: ${DST_GIT_REPO}" &&
      echo "Adding git remote pointing to SRC_GIT_REPO..." &&
      git remote add old-repo ${SRC_GIT_REPO} &&
      echo "Fetching from SRC_GIT_REPO..." &&
      git fetch old-repo "${SRC_BRANCH_NAME_EXPORTED}" &&
      echo "Checking out DST_BRANCH_NAME..." &&
      git checkout "${DST_BRANCH_NAME}" &&
      echo "Merging SRC_GIT_REPO/" &&
      git merge "old-repo/${SRC_BRANCH_NAME}-exported" --no-commit &&
    cd -
}

# If something didn't work as you'd expect, you can undo, tune the params, and try again
undoImportFolderFromAnotherGitRepo(){
  cd "${SRC_GIT_REPO}" &&
    SRC_BRANCH_NAME_EXPORTED="${SRC_BRANCH_NAME}-exported" &&
    git checkout "${SRC_BRANCH_NAME}" &&
    git branch -D "${SRC_BRANCH_NAME_EXPORTED}" &&
  cd - &&
  cd "${DST_GIT_REPO}" &&
    git remote rm old-repo &&
    git merge --abort
  cd -
}

importFolderFromAnotherGitRepo
#undoImportFolderFromAnotherGitRepo

1
谢谢这个脚本,它帮了我很多忙。两个小改进点:
  1. 如果REWRITE_TO里包含破折号,例如"my-folder",sed表达式将会失败。因此,我修改了分隔符为@: SED_COMMAND='s@\t\"*@\t'${REWRITE_TO}'@'
  2. 在现代的git中,你必须提供--allow-unrelated-histories标志来合并: git merge "old-repo/${SRC_BRANCH_NAME}-exported" --no-commit --allow-unrelated-histories &&
希望对某些人有所帮助, Ori.
- orid

0
在我的情况下,我不需要保留我正在迁移的存储库或保留任何先前的历史记录。我有一个来自不同远程的相同分支的补丁。
#Source directory
git remote rm origin
#Target directory
git remote add branch-name-from-old-repo ../source_directory

在这两个步骤中,我能够使另一个仓库的分支显示在同一个仓库中。
最后,我将这个从其他仓库导入的分支设置为跟踪目标仓库的主线(这样我可以准确地比较它们)。
git br --set-upstream-to=origin/mainline

现在它的行为就像我推送到同一仓库的另一个分支一样。


0

如果在两个仓库中,需要的文件路径相同,且你只想移植一个或一小组相关的文件,有一个简单的方法可以使用git cherry-pick

第一步是使用git fetch<remote-url>将另一个仓库的提交带到您自己的本地仓库。这将使FETCH_HEAD指向来自另一个仓库的头提交;如果你想在做其他拉取后保留对该提交的引用,你可能想用git tag other-head FETCH_HEAD来标记它。

然后,您需要为该文件创建一个初始提交(如果不存在),或者进行提交,以使该文件处于可以使用要引入的另一个仓库的第一个提交进行修补的状态。如果commit-0引入了您想要的文件,则可以使用git cherry-pick <commit-0>完成此操作;否则,您可能需要手动构建提交。如果需要修改初始提交以删除您不想引入的文件等操作,请在cherry-pick选项中添加-n

接下来,您可以继续使用-n,如有必要,以git cherry-pick后续提交。在最简单的情况下(所有提交恰好符合要求并且应用干净),您可以在cherry-pick命令行上给出完整的提交列表:git cherry-pick <commit-1> <commit-2> <commit-3> ...


-3
以下是将我的GIT Stash迁移到GitLab的方法,保留所有分支并保留历史记录。
将旧仓库克隆到本地。
git clone --bare <STASH-URL>

在GitLab中创建一个空的代码库。

git push --mirror <GitLab-URL>

当我们将代码从Stash迁移到GitLab时,我执行了上述操作,效果非常好。


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