我们的 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
但这似乎很令人费解。一般来说,有没有更好的方法来做这种事情?还是我采用了正确的方法?
请注意,这涉及将历史合并到现有存储库中,而不是简单地从另一个存储库 (as in an earlier question) 的一部分创建新的独立存储库。
git fetch p2 && git merge p2
而不是 git fetch p2 && git branch .. && git merge p2
?编辑:好吧,看起来你想在一个名为 p2 的新分支中获取更改,而不是当前分支。
git filter-repo
是 2021 年执行此操作的正确工具,而不是 filter-branch
。
如果您的历史记录是健全的,您可以将提交作为补丁取出并将它们应用到新存储库中:
cd repository
git log --pretty=email --patch-with-stat --reverse --full-index --binary -- path/to/file_or_folder > patch
cd ../another_repository
git am --committer-date-is-author-date < ../repository/patch
或者在一行
git log --pretty=email --patch-with-stat --reverse --full-index --binary -- path/to/file_or_folder | (cd /path/to/new_repository && git am --committer-date-is-author-date)
(取自 Exherbo’s docs)
尝试了各种方法将文件或文件夹从一个 Git 存储库移动到另一个存储库后,下面概述了唯一一种似乎可靠工作的方法。
它涉及克隆要从中移动文件或文件夹的存储库,将该文件或文件夹移动到根目录,重写 Git 历史记录,克隆目标存储库并将具有历史记录的文件或文件夹直接拉到此目标存储库中。
第一阶段
制作存储库 A 的副本,因为以下步骤对此副本进行了重大更改,您不应该推送! git clone --branch
第二阶段
清理步骤 git reset --hard 清理步骤 git gc --aggressive 清理步骤 git prune
您可能希望将这些文件导入到存储库 B 中的目录而不是根目录中:
使该目录 mkdir
第三阶段
如果您还没有存储库 B,请复制存储库 B 的副本 git clone
是的,点击 filter-branch
的 --subdirectory-filter
是关键。您使用它的事实基本上证明了没有更简单的方法 - 您别无选择,只能重写历史记录,因为您希望最终只得到文件的(重命名)子集,并且根据定义,这会更改哈希值。由于没有任何标准命令(例如 pull
)重写历史记录,因此您无法使用它们来完成此操作。
当然,您可以改进细节 - 您的一些克隆和分支并不是绝对必要的 - 但整体方法很好!很遗憾它很复杂,但当然,git 的目的并不是让重写历史变得容易。
filter-branch
联机帮助页中的 --index-filter
。
这通过使用 git-filter-repo 变得更简单。
为了将 project2/sub/dir
移动到 project1/sub/dir
:
# Create a new repo containing only the subdirectory:
git clone project2 project2_clone --no-local
cd project2_clone
git filter-repo --path sub/dir
# Merge the new repo:
cd ../project1
git remote add tmp ../project2_clone/
git fetch tmp master
git merge remotes/tmp/master --allow-unrelated-histories
git remote remove tmp
要简单地安装该工具:pip3 install git-filter-repo
(more details and options in README)
# Before: (root)
.
|-- project1
| `-- 3
`-- project2
|-- 1
`-- sub
`-- dir
`-- 2
# After: (project1)
.
├── 3
└── sub
└── dir
└── 2
git remote add
和 git merge
之间运行 git fetch
以使目标存储库了解源存储库中的更改。
git filter-repo --path sub/dir --path-rename sub:newsub
以获得 /newsub/dir
的树。该工具使该过程非常简单。
git filter-repo --path CurrentPathAfterRename --path OldPathBeforeRename
。 git filter-repo --analyze
生成一个文件 renames.txt,有助于确定这些文件。或者,您可能会发现 script like this 很有帮助。
git filter-repo
命令参数中,只需为您要移动的每个单独文件或目录添加一个 --path
参数。
我发现 Ross Hendrickson's blog 非常有用。这是一种非常简单的方法,您可以在其中创建应用于新存储库的补丁。有关更多详细信息,请参阅链接页面。
它只包含三个步骤(从博客复制):
# 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 错误。所以我不得不一个接一个地应用所有补丁。
保留目录名称
子目录过滤器(或更短的命令 git subtree)效果很好,但对我不起作用,因为它们从提交信息中删除了目录名称。在我的场景中,我只想将一个存储库的一部分合并到另一个存储库中,并保留带有完整路径名的历史记录。
我的解决方案是使用树过滤器并简单地从源存储库的临时克隆中删除不需要的文件和目录,然后通过 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
我经常使用的是这里的 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
git log
以彩色显示,则 grep ^commit
可能不起作用。如果是这样,请将 --no-color
添加到该 git log
命令。 (例如,git log --no-color $reposrc
)
有类似的痒痒(尽管仅适用于给定存储库的某些文件),这个脚本被证明是非常有用的:git-import
简短的版本是它从现有存储库创建给定文件或目录 ($object
) 的补丁文件:
cd old_repo
git format-patch --thread -o "$temp" --root -- "$object"
然后将其应用于新的存储库:
cd new_repo
git am "$temp"/*.patch
详情请查阅:
记录的来源
git 格式补丁
我是
更新(来自另一位作者)以下 bash 函数可以使用这种有用的方法。这是一个示例用法:
gitcp <Repo1_basedir> <path_inside_repo1> <Repo2_basedir>
gitcp ()
{
fromdir="$1";
frompath="$2";
to="$3";
echo "Moving git files from "$fromdir" at "$frompath" to "$to" ..";
tmpdir=/tmp/gittmp;
cd "$fromdir";
git format-patch --thread -o $tmpdir --root -- "$frompath";
cd "$to";
git am $tmpdir/*.patch
}
此答案提供了基于 git am
的有趣命令,并通过示例逐步呈现。
客观的
您希望将部分或全部文件从一个存储库移动到另一个存储库。
你想保留他们的历史。
但是您并不关心保留标签和分支。
您接受重命名文件(以及重命名目录中的文件)的有限历史记录。
程序
使用 git log --pretty=email -p --reverse --full-index --binary 以电子邮件格式提取历史记录重新组织文件树并更新历史记录中的文件名更改 [可选] 使用 git am 应用新历史记录
1.以电子邮件格式提取历史记录
示例:提取 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
清理你的 repo 源
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
2.重新组织文件树并更新历史中的文件名更改[可选]
假设您想将这三个文件移动到另一个 repo 中(可以是同一个 repo)。
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"' {} ';'
注意:这会重写历史记录以反映路径和文件名的变化。 (即新仓库中新位置/名称的更改)
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
查看准备推送的提交数量 :-)
注意:由于历史记录已被重写以反映路径和文件名的更改:(即与上一个 repo 中的位置/名称相比)
无需 git mv 即可更改位置/文件名。
无需 git log --follow 即可访问完整历史记录。
额外技巧:在你的仓库中检测重命名/移动的文件
要列出已重命名的文件:
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'
更多自定义:您可以使用选项 --find-copies-harder
或 --reverse
完成命令 git log
。您还可以使用 cut -f3-
和 grepping 完整模式 '{.* => 删除前两列.*}'。
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
尝试这个
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
以覆盖
使用来自 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
分支上的文件。
我想要一些健壮且可重用的东西(一个命令并执行 + 撤消功能),所以我编写了以下 bash 脚本。多次为我工作,所以我想我会在这里分享。
它能够将任意文件夹 /path/to/foo
从 repo1
移动到 /some/other/folder/bar
到 repo2
(文件夹路径可以相同或不同,与根文件夹的距离可能不同)。
由于它只检查涉及输入文件夹中文件的提交(而不是源存储库的所有提交),因此即使在大型源存储库中它也应该非常快,如果您只是提取一个在每个文件中都没有触及的深层嵌套子文件夹犯罪。
因为这样做是用所有旧仓库的历史创建一个孤立的分支,然后将它合并到 HEAD,它甚至可以在文件名冲突的情况下工作(然后你必须在课程结束时解决合并) .
如果没有文件名冲突,您只需要最后的 git commit
即可完成合并。
缺点是它可能不会跟随源 repo 中的文件重命名(在 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
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。
git subtree
直观地工作,甚至保留历史。
示例用法:将 git repo 添加为子目录:
git subtree add --prefix foo https://github.com/git/git.git master
解释:
#├── repo_bar
#│ ├── bar.txt
#└── repo_foo
# └── foo.txt
cd repo_bar
git subtree add --prefix foo ../repo_foo master
#├── repo_bar
#│ ├── bar.txt
#│ └── foo
#│ └── foo.txt
#└── repo_foo
# └── foo.txt
就我而言,我不需要保留要从中迁移的存储库或保留任何以前的历史记录。我有一个相同分支的补丁,来自不同的遥控器
#Source directory
git remote rm origin
#Target directory
git remote add branch-name-from-old-repo ../source_directory
在这两个步骤中,我能够让另一个 repo 的分支出现在同一个 repo 中。
最后,我设置了这个分支(我从另一个仓库导入的)跟随目标仓库的主线(这样我就可以准确地区分它们)
git br --set-upstream-to=origin/mainline
现在它表现得好像它只是我针对同一个 repo 推送的另一个分支。
如果有问题的文件在两个存储库中的路径相同,并且您只想带入一个文件或一小组相关文件,一个简单的方法是使用 git cherry-pick
。
第一步是使用 git fetch <remote-url>
将来自其他存储库的提交带入您自己的本地存储库。这将使 FETCH_HEAD
指向另一个仓库的头部提交;如果您希望在完成其他提取后保留对该提交的引用,您可能需要使用 git tag other-head FETCH_HEAD
对其进行标记。
然后,您将需要为该文件创建一个初始提交(如果它不存在)或一个提交以使该文件进入一个可以使用您想要引入的其他存储库的第一个提交进行修补的状态。您可以如果 commit-0
引入了您想要的文件,则可以使用 git cherry-pick <commit-0>
执行此操作,或者您可能需要“手动”构建提交。如果您需要修改初始提交,例如,从该提交中删除您不想引入的文件,请将 -n
添加到樱桃选择选项中。
之后,您可以继续 git cherry-pick
后续提交,必要时再次使用 -n
。在最简单的情况下(所有提交都是您想要的并且干净地应用),您可以在cherry-pick 命令行上提供完整的提交列表:git cherry-pick <commit-1> <commit-2> <commit-3> ...
。
以下通过维护所有分支并保留历史记录将我的 GIT Stash 迁移到 GitLab 的方法。
将旧存储库克隆到本地。
git clone --bare <STASH-URL>
在 GitLab 中创建一个空存储库。
git push --mirror <GitLab-URL>
当我们将代码从 stash 迁移到 GitLab 时,我执行了上述操作,并且效果非常好。
git log --pretty=email --patch-with-stat --full-index --binary --reverse -- client > patch
。工作没有问题 AFAICT。--committer-date-is-author-date
选项来保留原始提交日期,而不是文件移动的日期。--follow
选项添加到git log
(一次只能处理一个文件)。