如何将文件从一个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