如何将文件从一个git仓库移到另一个仓库(而不是克隆),保存历史logging

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

但是这似乎相当复杂。 总体来说有没有更好的方法来做这种事情? 还是我采用了正确的方法?

是的,打在--subdirectory-filter filter-branch--subdirectory-filter filter-branch是关键。 事实上,你使用它基本上certificate没有更简单的方法 – 你别无select,只能重写历史,因为你想最终只有一个(重命名)的文件的子集,这通过定义改变散列。 由于没有任何标准命令(例如pull )重写历史logging,所以你不可能使用它们来实现这一点。

当然,你可以细化细节 – 你的一些克隆和分支并不是绝对必要的 – 但总的来说很好! 这是一个令人遗憾的复杂的,但当然,git的重点不是要轻易重写历史。

如果你的历史是理智的,你可以把这个提交作为补丁来应用到新的仓库中:

 cd repository git log --pretty=email --patch-with-stat --reverse --full-index --binary -- path/to/file_or_folder > patch cd ../another_repository git am < ../repository/patch 

或在一行

 git log --pretty=email --patch-with-stat --reverse -- path/to/file_or_folder | (cd /path/to/new_repository && git am) 

(摘自Exherbo的文档 )

已经尝试了各种方法将文件或文件夹从一个Git存储库移动到另一个,似乎可靠工作的唯一方法概述如下。

它涉及克隆要移动文件或文件夹的存储库,将该文件或文件夹移动到根目录,重写Git历史logging,克隆目标存储库,并将包含历史logging的文件或文件夹直接拖到此目标存储库中。

第一阶段

  1. 制作一个存储库A的副本,以下步骤对该副本进行重大更改,您不应该这样做!

     git clone --branch <branch> --origin origin --progress -v <git repository A url> eg. git clone --branch master --origin origin --progress -v https://username@giturl/scm/projects/myprojects.git 

    (假设myprojects是要从中复制的存储库)

  2. CD进入它

     cd <git repository A directory> eg. cd /c/Working/GIT/myprojects 
  3. 删除原始存储库的链接,以避免意外进行任何远程更改(例如通过推送)

     git remote rm origin 
  4. 浏览历史logging和文件,删除不在目录1中的任何内容。结果是将目录1的内容散布到存储库A的基础中。

     git filter-branch --subdirectory-filter <directory> -- --all eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all 
  5. 仅适用于单个文件移动:浏览剩下的内容,移除除所需文件外的所有内容。 (您可能需要使用相同的名称删除不需要的文件并提交。)

     git filter-branch -f --index-filter \ 'git ls-files -s | grep $'\t'FILE_TO_KEEP$ | GIT_INDEX_FILE=$GIT_INDEX_FILE.new \ git update-index --index-info && \ mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE || echo "Nothing to do"' --prune-empty -- --all 

    例如。 FILE_TO_KEEP = pom.xml只保留来自FOLDER_TO_KEEP的pom.xml文件

第二阶段

  1. 清理步骤

     git reset --hard 
  2. 清理步骤

     git gc --aggressive 
  3. 清理步骤

     git prune 

您可能想要将这些文件导入到不是根目录的存储库B中:

  1. 制作该目录

     mkdir <base directory> eg. mkdir FOLDER_TO_KEEP 
  2. 将文件移动到该目录中

     git mv * <base directory> eg. git mv * FOLDER_TO_KEEP 
  3. 将文件添加到该目录

     git add . 
  4. 提交您的更改,我们准备将这些文件合并到新的存储库中

     git commit 

第三阶段

  1. 如果您还没有存储库B的副本

     git clone <git repository B url> eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git 

    (假设FOLDER_TO_KEEP是要复制到的新存储库的名称)

  2. CD进入它

     cd <git repository B directory> eg. cd /c/Working/GIT/FOLDER_TO_KEEP 
  3. 创build远程连接到存储库A作为存储库B中的一个分支

     git remote add repo-A-branch <git repository A directory> 

    (repo-A-branch可以是任何东西 – 只是一个任意的名字)

     eg. git remote add repo-A-branch /c/Working/GIT/myprojects 
  4. 从这个分支(只包含你想要移动的目录)拉到存储库B.

     git pull repo-A-branch master --allow-unrelated-histories 

    拉将复制文件和历史logging。 注意:您可以使用合并而不是拉,但拉更好。

  5. 最后,您可能需要通过删除与存储库A的远程连接来清理一下

     git remote rm repo-A-branch 
  6. 推,你全部设置。

     git push 

我发现这非常有用。 这是一个非常简单的方法,你创build补丁应用到新的回购。 请参阅链接页面了解更多详情。

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

 # 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错误。 所以我必须一个接一个地应用所有的补丁。

保持目录名称

子目录filter(或较短的命令git子树)工作良好,但没有为我工作,因为他们从提交信息中删除目录名称。 在我的scheme中,我只想将一个资源库的一部分合并到另一个资源库中,并保留具有完整path名的历史logging

我的解决scheme是使用树型filter,并简单地从源存储库的临时克隆中删除不需要的文件和目录,然后通过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 am有趣命令,并且使用示例一步一步呈现。

目的

  • 您想要将一些或全部文件从一个存储库移到另一个存储库。
  • 你想保持他们的历史。
  • 但你不关心保持标签和分支。
  • 您接受有限的历史重命名文件(和重命名的目录中的文件)。

程序

  1. 使用电子邮件格式提取历史logging
    git log --pretty=email -p --reverse --full-index --binary
  2. 重新组织文件树并更新历史logging中的文件名更改[可选]
  3. 使用git am应用新的历史logging

1.以电子邮件格式提取历史logging

示例:提取file3file4file5历史logging

 my_repo ├── dirA │ ├── file1 │ └── file2 ├── dirB ^ │ ├── subdir | To be moved │ │ ├── file3 | with history │ │ └── file4 | │ └── file5 v └── dirC ├── file6 └── file7 

清理临时目录的目的地

 export historydir=/tmp/mail/dir # Absolute path rm -rf "$historydir" # Caution when cleaning 

清理你的回购来源

 git commit ... # Commit your working files rm .gitignore # Disable gitignore git clean -n # Simulate removal git clean -f # Remove untracked file git checkout .gitignore # Restore gitignore 

以电子邮件格式提取每个文件的历史logging

 cd my_repo/dirB find -name .git -prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "$0" > "$historydir/$0"' {} ';' 

不幸的是,选项--follow--find-copies-harder不能与--reverse组合。 这就是当文件被重命名时(或者父目录被重命名时),历史被切断的原因。

之后:电子邮件格式的临时历史

 /tmp/mail/dir ├── subdir │ ├── file3 │ └── file4 └── file5 

2.重新组织文件树并更新历史logging中的文件名更改[可选]

假设你想在这个其他回购中移动这三个文件(可以是相同的回购)。

 my_other_repo ├── dirF │ ├── file55 │ └── file56 ├── dirB # New tree │ ├── dirB1 # was subdir │ │ ├── file33 # was file3 │ │ └── file44 # was file4 │ └── dirB2 # new dir │ └── file5 # = file5 └── dirH └── file77 

因此重新组织你的文件:

 cd /tmp/mail/dir mkdir dirB mv subdir dirB/dirB1 mv dirB/dirB1/file3 dirB/dirB1/file33 mv dirB/dirB1/file4 dirB/dirB1/file44 mkdir dirB/dirB2 mv file5 dirB/dirB2 

你的临时历史现在是:

 /tmp/mail/dir └── dirB ├── dirB1 │ ├── file33 │ └── file44 └── dirB2 └── file5 

更改历史logging中的文件名:

 cd "$historydir" find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';' 

注意:这会重写历史logging以反映path和文件名的更改。
(即在新回购中更改新的地点/名称)


3.申请新的历史

你的其他回购是:

 my_other_repo ├── dirF │ ├── file55 │ └── file56 └── dirH └── file77 

应用来自临时历史文件的提交:

 cd my_other_repo find "$historydir" -type f -exec cat {} + | git am 

您的其他回购现在是:

 my_other_repo ├── dirF │ ├── file55 │ └── file56 ├── dirB ^ │ ├── dirB1 | New files │ │ ├── file33 | with │ │ └── file44 | history │ └── dirB2 | kept │ └── file5 v └── dirH └── file77 

使用git status来查看提交数量准备推送:-)

注意:由于历史已被重写以反映path和文件名更改:
(即与前一次回购中的地点/名称相比)

  • 不需要git mv来改变位置/文件名。
  • 不需要git log --follow完整的历史logging。

额外的技巧:检测您的回购中重命名/移动的文件

要列出已被重命名的文件:

 find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>' 

更多定制:您可以使用选项--find-copies-harder --reverse--reverse来完成命令git log 。 您还可以使用cut -f3-cut -f3-完整模式“{。* =>。*}”删除前两列。

 find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}' 

有一个类似的痒从头开始(只有一些给定的存储库文件),这个脚本被certificate是非常有用的: git-import

简短的版本是它从现有的存储库中创build给定文件或目录( $object )的补丁文件:

 cd old_repo git format-patch --thread -o "$temp" --root -- "$object" 

然后将其应用于新的存储库:

 cd new_repo git am "$temp"/*.patch 

详情请查阅:

  • logging的来源
  • git format-patch
  • git am

我一直使用的是这里http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ 。 简单而快速。

为了符合stackoverflow标准,这里是程序:

 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 

使用来自http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/的灵感,我创build了这个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分支上的文件。

我想要一些健壮的,可重用的(一个命令,去+撤消function),所以我写了下面的bash脚本。 多次为我工作,所以我想我会在这里分享。

它可以将repo1的任意文件夹/path/to/foo repo1/some/other/folder/barrepo2 (文件夹path可以相同或不同,与根文件夹的距离可以不同)。

由于它只覆盖触及input文件夹中的文件的提交(不是源代码回购的所有提交),即使在大型源代码仓库中,如果您只是提取一个深度嵌套的子文件夹承诺。

既然这样做就是创build一个带有所有旧回购历史logging的孤立分支,然后将它合并到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