我们的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
但这似乎很复杂.有没有更好的方法来做这种事情?或者我采用了正确的方法?
请注意,这涉及将历史记录合并到现有存储库中,而不是简单地从另一个存储库中创建一个新的独立存储库(如前面的问题).
如果您的历史记录是正确的,您可以将提交作为补丁并将其应用于新的存储库:
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历史记录,克隆目标存储库以及将具有历史记录的文件或文件夹直接拖到此目标存储库中.
制作存储库A的副本,因为以下步骤对此副本进行了重大更改,您不应该推送!
git clone --branch--origin origin --progress \ -v # eg. git clone --branch master --origin origin --progress \ # -v https://username@giturl/scm/projects/myprojects.git # (assuming myprojects is the repository you want to copy from)
(假设myprojects是您要复制的存储库)
进入它
cd# eg. cd /c/Working/GIT/myprojects
删除原始存储库的链接,以避免意外进行任何远程更改(例如,通过推送)
git remote rm origin
浏览历史记录和文件,删除目录1中没有的任何内容.结果是目录1的内容扩展到存储库A的基础.
git filter-branch --subdirectory-filter-- --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 # eg. FILE_TO_KEEP = pom.xml to keep only the pom.xml file from FOLDER_TO_KEEP
例如.FILE_TO_KEEP = pom.xml只保留FOLDER_TO_KEEP中的pom.xml文件
清理步骤
git reset --hard
清理步骤
git gc --aggressive
清理步骤
git prune
您可能希望将这些文件导入到不是根目录的目录中的存储库B中:
制作该目录
mkdireg. mkdir FOLDER_TO_KEEP
将文件移动到该目录中
git mv *eg. git mv * FOLDER_TO_KEEP
将文件添加到该目录
git add .
提交您的更改,我们已准备好将这些文件合并到新存储库中
git commit
如果您还没有存储库B,请复制存储库B.
git clone# eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git
(假设FOLDER_TO_KEEP是您要复制到的新存储库的名称)
进入它
cd# eg. cd /c/Working/GIT/FOLDER_TO_KEEP
创建与存储库A的远程连接作为存储库B中的分支
git remote add repo-A-branch# (repo-A-branch can be anything - it's just an arbitrary name) # eg. git remote add repo-A-branch /c/Working/GIT/myprojects
(repo-A-branch可以是任何东西 - 它只是一个任意名称)
git pull repo-A-branch master --allow-unrelated-histories
从此分支(仅包含您要移动的目录)拉入存储库B.
git remote rm repo-A-branch
pull会复制文件和历史记录.注意:您可以使用合并而不是拉动,但拉动效果更好.
最后,您可能希望通过删除与存储库A的远程连接来清理一下
git push
推,你一切都准备好了.
git clone --branch--origin origin --progress \ -v # eg. git clone --branch master --origin origin --progress \ # -v https://username@giturl/scm/projects/myprojects.git # (assuming myprojects is the repository you want to copy from)
是的,打的--subdirectory-filter
的filter-branch
是关键.您使用它的事实本质上证明没有更简单的方法 - 您别无选择,只能重写历史记录,因为您希望最终只得到文件的一个(重命名的)子集,并且根据定义更改哈希值.由于没有标准命令(例如pull
)重写历史记录,因此您无法使用它们来完成此任务.
当然,你可以改进细节 - 你的一些克隆和分支并不是绝对必要的 - 但整体方法是好的!遗憾的是它很复杂,但当然,git的重点并不是要让重写历史变得容易.
我发现这非常有用.这是一种非常简单的方法,您可以在其中创建应用于新仓库的修补程序.有关详细信息,请参阅链接页面.
它只包含三个步骤(从博客中复制):
# Setup a directory to hold the patches mkdir# Create the patches git format-patch -o --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
我遇到的唯一问题是我无法一次性应用所有补丁
git am --3way/*.patch
在Windows下,我收到了InvalidArgument错误.所以我不得不一个接一个地应用所有补丁.
保留目录名称
子目录过滤器(或更短的命令git子树)工作正常,但对我来说不起作用,因为它们从提交信息中删除目录名称.在我的场景中,我只想将一个存储库的部分合并到另一个存储库中,并保留历史记录的完整路径名.
我的解决方案是使用树过滤器并简单地从源存储库的临时克隆中删除不需要的文件和目录,然后通过5个简单步骤从该克隆拉到我的目标存储库.
# 1. clone the source git clone ssh://@ cd # 2. remove the stuff we want to exclude git filter-branch --tree-filter "rm -rf " --prune-empty HEAD # 3. move to target repo and create a merge branch (for safety) cd git checkout -b # 4. Add the source-repo as remote git remote add 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
并逐步使用示例.
您希望将一些或所有文件从一个存储库移动到另一个存储库.
你想保留他们的历史.
但是你不关心保留标签和分支.
您接受重命名文件(以及重命名目录中的文件)的有限历史记录.
使用以电子邮件格式提取历史记录
git log --pretty=email -p --reverse --full-index --binary
重新组织文件树并更新历史记录中的文件名更改[可选]
使用应用新历史记录 git am
例如:提取的历史file3
,file4
和file5
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
以电子邮件格式提取每个文件的历史记录
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
假设您要在这个其他仓库中移动这三个文件(可以是相同的仓库).
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
更改历史记录中的文件名:
cd "$historydir" find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';'
注意:这会重写历史记录以反映路径和文件名的更改.
(即新仓库中新位置/名称的更改)
你的其他回购是:
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
看量的承诺准备推:-)
注意:由于历史记录已被重写以反映路径和文件名更改:(
即与上一个仓库中的位置/名称相比)
无需git mv
更改位置/文件名.
无需git log --follow
访问完整历史记录.
列出已重命名的文件:
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'
更多自定义:您可以git log
使用选项--find-copies-harder
或完成命令--reverse
.您还可以使用cut -f3-
和grepping完整模式'{.*=>.*}' 删除前两列.
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
我一直使用的网址是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