如何将文件从一个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的文件或文件夹直接拖到此目标存储库中。
第一阶段
- 
制作一个存储库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是要从中复制的存储库) 
- 
CD进入它 cd <git repository A directory> eg. cd /c/Working/GIT/myprojects
- 
删除原始存储库的链接,以避免意外进行任何远程更改(例如通过推送) git remote rm origin
- 
浏览历史logging和文件,删除不在目录1中的任何内容。结果是将目录1的内容散布到存储库A的基础中。 git filter-branch --subdirectory-filter <directory> -- --all eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all
- 
仅适用于单个文件移动:浏览剩下的内容,移除除所需文件外的所有内容。 (您可能需要使用相同的名称删除不需要的文件并提交。) 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文件 
第二阶段
- 
清理步骤 git reset --hard
- 
清理步骤 git gc --aggressive
- 
清理步骤 git prune
您可能想要将这些文件导入到不是根目录的存储库B中:
- 
制作该目录 mkdir <base directory> eg. mkdir FOLDER_TO_KEEP
- 
将文件移动到该目录中 git mv * <base directory> eg. git mv * FOLDER_TO_KEEP
- 
将文件添加到该目录 git add .
- 
提交您的更改,我们准备将这些文件合并到新的存储库中 git commit
第三阶段
- 
如果您还没有存储库B的副本 git clone <git repository B url> eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git(假设FOLDER_TO_KEEP是要复制到的新存储库的名称) 
- 
CD进入它 cd <git repository B directory> eg. cd /c/Working/GIT/FOLDER_TO_KEEP
- 
创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
- 
从这个分支(只包含你想要移动的目录)拉到存储库B. git pull repo-A-branch master --allow-unrelated-histories拉将复制文件和历史logging。 注意:您可以使用合并而不是拉,但拉更好。 
- 
最后,您可能需要通过删除与存储库A的远程连接来清理一下 git remote rm repo-A-branch
- 
推,你全部设置。 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有趣命令,并且使用示例一步一步呈现。 
目的
- 您想要将一些或全部文件从一个存储库移到另一个存储库。
- 你想保持他们的历史。
- 但你不关心保持标签和分支。
- 您接受有限的历史重命名文件(和重命名的目录中的文件)。
程序
-  使用电子邮件格式提取历史logging 
 git log --pretty=email -p --reverse --full-index --binary
- 重新组织文件树并更新历史logging中的文件名更改[可选]
-  使用git am应用新的历史logging
1.以电子邮件格式提取历史logging
 示例:提取file3 , file4和file5历史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 
 # 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/bar到repo2 (文件夹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