我们的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
但这看起来很复杂。有没有更好的方法来做这种事情?或者我采用了正确的方法?
请注意,这涉及将历史记录合并到现有存储库中,而不是简单地从另一个存储库(as in an earlier question)的一部分创建新的独立存储库。
答案 0 :(得分:254)
如果你的历史是理智的,你可以把提交作为补丁并将它们应用到新的存储库中:
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’s docs)
答案 1 :(得分:66)
尝试了将文件或文件夹从一个Git存储库移动到另一个存储库的各种方法,下面概述了唯一一个似乎可靠工作的方法。
它涉及克隆要从中移动文件或文件夹的存储库,将该文件或文件夹移动到根目录,重写Git历史记录,克隆目标存储库以及将具有历史记录的文件或文件夹直接拖到此目标存储库中。 / p>
制作存储库A的副本,因为以下步骤为major 对此副本的更改,您不应该推送!
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
# (assuming myprojects is the repository you want to copy from)
cd进去吧
cd <git repository A directory>
# eg. cd /c/Working/GIT/myprojects
删除指向原始存储库的链接以避免意外 进行任何远程更改(例如通过推送)
git remote rm origin
浏览您的历史记录和文件,删除不在的内容 目录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
# eg. FILE_TO_KEEP = pom.xml to keep only the pom.xml file from FOLDER_TO_KEEP
清理步骤
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
创建与存储库A的远程连接,作为存储库中的分支 乙
git remote add repo-A-branch <git repository A directory>
# (repo-A-branch can be anything - it's just an arbitrary name)
# eg. git remote add repo-A-branch /c/Working/GIT/myprojects
从此分支拉出(仅包含您想要的目录) 移动到存储库B中。
git pull repo-A-branch master --allow-unrelated-histories
pull会同时复制文件和历史记录。注意:您可以使用合并而不是拉动,但拉动效果更好。
最后,您可能希望通过移除遥控器来清理一下 与存储库A的连接
git remote rm repo-A-branch
推,你已经完成了。
git push
答案 2 :(得分:51)
是的,点击--subdirectory-filter
的{{1}}是关键。您使用它的事实本质上证明没有更简单的方法 - 您别无选择,只能重写历史记录,因为您希望最终只得到文件的一个(重命名的)子集,并且根据定义更改哈希值。由于没有标准命令(例如filter-branch
)重写历史记录,因此您无法使用它们来完成此任务。
你可以改进细节,当然 - 你的一些克隆和分支并不是绝对必要的 - 但整体方法是好的!遗憾的是它很复杂,但当然,git的重点并不是要重写历史。
答案 3 :(得分:21)
我发现this非常有用。这是一种非常简单的方法,您可以在其中创建应用于新仓库的修补程序。有关详细信息,请参阅链接页面。
它只包含三个步骤(从博客中复制):
# 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错误。所以我不得不一个接一个地应用所有补丁。
答案 4 :(得分:6)
保留目录名称
子目录过滤器(或更短的命令git子树)运行良好,但对我来说不起作用,因为它们从提交信息中删除目录名称。在我的场景中,我只想将一个存储库的部分合并到另一个存储库中,并保留历史记录的完整路径名。
我的解决方案是使用树过滤器,只需从源存储库的临时克隆中删除不需要的文件和目录,然后通过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
答案 5 :(得分:5)
这个答案提供了基于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
清理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
假设您要在这个其他仓库中移动这三个文件(可以是相同的仓库)。
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-
删除前两列并点击完整模式'{。* =&gt; *}”。
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
答案 6 :(得分:4)
我一直使用的是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
答案 7 :(得分:3)
有一个类似于itch的痒(尽管只针对给定存储库的某些文件),这个脚本证明非常有用:git-import
简短版本是它从现有存储库创建给定文件或目录($object
)的补丁文件:
cd old_repo
git format-patch --thread -o "$temp" --root -- "$object"
然后应用于新的存储库:
cd new_repo
git am "$temp"/*.patch
详情请查询:
答案 8 :(得分:2)
尝试一下
let loadMoreCommentsVisible = await isElementVisible(page, loadMoreComments);
while(loadMoreCommentsVisible) {
await page
.click(loadMoreComments)
.catch((e) => {
console.log(e);
});
loadMoreCommentsVisible = await isElementVisible(page, loadMoreComments);
}
这将删除除提到的目录以外的所有目录,仅保留这些目录的历史记录
cd repo1
现在您可以在git遥控器中添加新的仓库,并将其推送到该仓库中。
git filter-branch --index-filter 'git rm --ignore-unmatch --cached -qr -- . && git reset -q $GIT_COMMIT -- dir1/ dir2/ dir3/ ' --prune-empty -- --all
git remote remove origin <old-repo>
git remote add origin <new-repo>
添加git push origin <current-branch>
以覆盖
答案 9 :(得分:1)
使用来自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
分支上的文件。
答案 10 :(得分:1)
我想要一些健壮且可重用的东西(one-command-and-go + undo函数),所以我写了下面的bash脚本。我曾经多次为我工作,所以我想我会在这里分享。
可以将/path/to/foo
中的任意文件夹repo1
移动到/some/other/folder/bar
到repo2
(文件夹路径可以相同也可以不同,与根文件夹的距离可以是不同)。
因为它只会覆盖触摸输入文件夹中的文件的提交(而不是源代码库的所有提交),所以即使在大型源代码库中它也应该非常快,如果你只提取一个非深层嵌套的子文件夹触及每一次提交。
由于这样做是为了创建一个包含所有旧repo历史的孤立分支,然后将其合并到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
答案 11 :(得分:1)
使用git-filter-repo变得更简单。
为了将project2/sub/dir
移至project1/sub/dir
:
# Create a new repo containing only the subdirectory:
git clone project2 project2_subdir
cd project2_subdir
git filter-repo --force --path sub/dir
# Merge the new repo:
cd ../project1
git remote add project2_subdir ../project2_subdir/
git merge remotes/project2_subdir/master --allow-unrelated-histories
git remote remove project2_subdir
要简单地安装该工具,请执行以下操作: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
答案 12 :(得分:0)
在我的情况下,我不需要保留我正在迁移的仓库或保留以前的历史记录。我有一个相同分支的补丁,来自不同的远程
#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
现在它表现得像 - 如果它只是我推动同一个回购的另一个分支。
答案 13 :(得分:0)
如果两个存储库中所讨论文件的路径相同,并且您只想移交一个文件或一小组相关文件,那么一种简单的方法是使用{{1} }。
第一步是使用git cherry-pick
将来自另一个仓库的提交带入您自己的本地仓库。这将使git fetch <remote-url>
指向另一个仓库中的头提交;如果要在完成其他提取操作后保留对该提交的引用,则可以使用FETCH_HEAD
对其进行标记。
然后,您需要为该文件创建一个初始提交(如果不存在),或者进行一次提交,以使文件进入可以与您要引入的其他存储库中的第一次提交打补丁的状态。 。如果git tag other-head FETCH_HEAD
引入了所需的文件,则可以使用git cherry-pick <commit-0>
进行此操作,或者您可能需要“手动”构造提交。如果您需要修改初始提交,例如从您不想引入的提交中删除文件,请在commit-0
中添加樱桃选择选项。
此后,您可以继续-n
进行后续提交,并在必要时再次使用git cherry-pick
。在最简单的情况下(所有提交正是您想要的并且可以干净地应用),您可以在cherry-pick命令行上提供完整的提交列表:-n
。
答案 14 :(得分:0)
我做了什么:
答案 15 :(得分:-2)
以下通过维护所有分支并保留历史记录将我的GIT Stash迁移到GitLab的方法。
将旧存储库克隆到本地。
git clone --bare <STASH-URL>
在GitLab中创建一个空的存储库。
git push --mirror <GitLab-URL>
上面的代码是我将代码从存储库迁移到GitLab时执行的,效果很好。