如何将一些文件从一个 git repo 移动到另一个(不是克隆),保留历史记录

Posted

技术标签:

【中文标题】如何将一些文件从一个 git repo 移动到另一个(不是克隆),保留历史记录【英文标题】:How to move some files from one git repo to another (not a clone), preserving history 【发布时间】:2010-11-24 19:57:47 【问题描述】:

我们的 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 实际上确实使这变得简单(例如,我不想尝试在 Subversion 中的不同存储库之间移动文件目录)。 @ebneter - 我已经使用 shell 脚本手动完成了这个(将历史从一个 svn repo 移动到另一个)。基本上,我将特定文件/目录中的历史记录(差异、提交日志消息)重放到第二个存储库中。 我想知道你为什么不使用git fetch p2 && git merge p2 而不是git fetch p2 && git branch .. && git merge p2?编辑:好吧,看起来你想在一个名为 p2 的新分支中获取更改,而不是当前分支。 有没有办法防止 --filter-branch 破坏目录结构?这个“git mv”步骤会导致大量的文件删除和文件创建。 git filter-repo 是在 2021 年执行此操作的正确工具,而不是 filter-branch 【参考方案1】:

是的,点击filter-branch--subdirectory-filter 是关键。您使用它的事实基本上证明了没有更简单的方法 - 您别无选择,只能重写历史记录,因为您希望最终只得到文件的(重命名)子集​​,并且根据定义,这会更改哈希值。由于没有任何标准命令(例如pull)重写历史记录,因此您无法使用它们来完成此操作。

当然,您可以改进细节 - 您的一些克隆和分支并不是绝对必要的 - 但总体方法很好!很遗憾它很复杂,但当然,git 的目的并不是让重写历史变得容易。

【讨论】:

如果您的文件已通过多个目录移动,现在位于一个目录中,子目录过滤器是否仍然有效? (即我假设如果我只想移动一个文件,我可以将它移动到它自己的子目录中,这样可以吗?) @rogerdpack:不,这不会通过重命名来跟随文件。我相信它似乎是在它被移动到选定的子目录时创建的。如果您只想选择一个文件,请查看filter-branch 手册页中的--index-filter 我有什么方法可以关注重命名吗? 我认为维护和整理历史是 git 的重点之一。 关于以下重命名:***.com/questions/65220628/…(尚无答案,但希望将来会有)【参考方案2】:

如果您的历史记录正常,您可以将提交作为补丁取出并将它们应用到新的存储库中:

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 -- path/to/file_or_folder | (cd /path/to/new_repository && git am --committer-date-is-author-date)

(取自Exherbo’s docs)

【讨论】:

对于我需要移动的三个或四个文件,这是一个比接受的答案更简单的解决方案。我最终使用 find-replace 修剪了补丁文件中的路径,以使其适合我的新 repo 的目录结构。 我添加了选项,以便二进制文件(如图像)也可以正确迁移:git log --pretty=email --patch-with-stat --full-index --binary --reverse -- client &gt; patch。工作没有问题 AFAICT。 在应用步骤中,我使用了--committer-date-is-author-date 选项来保留原始提交日期,而不是文件被移动的日期。 不适用于已移动/重命名的文件。我假设您需要为每个文件制作单独的补丁并将--follow 选项添加到git log(一次只能处理一个文件)。 合并历史中的提交会破坏“am”命令。您可以在上面的 git log 命令中添加“-m --first-parent”,然后它对我有用。【参考方案3】:

尝试了各种方法将文件或文件夹从一个 Git 存储库移动到另一个存储库,下面列出了唯一一种似乎可靠工作的方法。

它涉及克隆要从中移动文件或文件夹的存储库,将该文件或文件夹移至根目录,重写 Git 历史记录,克隆目标存储库并将具有历史记录的文件或文件夹直接拉入此目标存储库。

第一阶段

    按照以下步骤制作存储库 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
    # (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
    

【讨论】:

我已经完成了这里列出的大部分步骤,但是它似乎只从主服务器(而不是从任何其他分支)复制文件或目录的提交历史记录。对吗? 我完成了这些步骤(感谢您对细节的关注!),但我注意到在 GitHub 中除了合并提交之外,它没有显示任何文件的历史记录。但是,如果我责怪或 gitk,我会看到提交历史记录。知道为什么吗? @mcarans 1. 我猜你的答案类似于Greg Bayer's blog 2. 我没有运行 第二阶段 我转到了将文件移动到新目录的步骤。 我是否还需要将文件夹.git移动到新目录中 3. 第二阶段中的修剪步骤我没有理解 有其他我不想碰的分支。 @mcarans 收到以下错误:fatal: Couldn't find remote ref repo-B-branch 在第三阶段的第 4 步申请时,git pull repo -A-branch repo-B-branch --allow-unrelated-histories 但是 repo-B-branch 存在于 repo B @mcarans 不幸的是,这不是可靠的方法,尽管它似乎是。它与所有其他解决方案存在相同的问题 - 它不保留过去重命名的历史记录。就我而言,第一次提交是我重命名目录/文件时。除此之外的一切都丢失了。【参考方案4】:

保留目录名称

子目录过滤器(或更短的命令 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

【讨论】:

此script 不会对您的原始存储库进行任何修改。如果映射文件中指定的 dest 存储库不存在,则此脚本将尝试创建它。 我还认为保持目录名称不变非常重要。否则,您将获得对目标存储库的额外重命名提交。【参考方案5】:

此答案提供了基于git am 的有趣命令,并通过示例逐步呈现。

目标

您希望将部分或全部文件从一个存储库移动到另一个存储库。 您想保留他们的历史记录。 但你并不关心保留标签和分支。 您接受重命名文件(以及重命名目录中的文件)的有限历史记录。

程序

    使用电子邮件格式提取历史记录git log --pretty=email -p --reverse --full-index --binary 重新组织文件树并更新历史中的文件名更改 [可选] 使用git am 应用新的历史记录

1。以电子邮件格式提取历史记录

示例:提取file3file4file5 的历史记录

my_repo
├── dirA
│   ├── file1
│   └── file2
├── dirB            ^
│   ├── subdir      | To be moved
│   │   ├── file3   | with history
│   │   └── file4   | 
│   └── file5       v
└── dirC
    ├── file6
    └── file7

清理临时目录destination

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 即可访问完整历史记录。

额外技巧:检测你的 repo 中重命名/移动的文件

列出已重命名的文件:

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 '.* => .*'

【讨论】:

【参考方案6】:

使用来自 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 分支上的文件。

【讨论】:

【参考方案7】:

我发现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 错误。所以我不得不一个接一个地应用所有的补丁。

【讨论】:

对我不起作用,因为在某些时候 sha-hashes 丢失了。这对我有帮助:***.com/questions/17371150/… 与“git log”方法不同,这个选项对我来说非常有效!谢谢! 尝试了不同的方法将项目移动到新的仓库。这是唯一对我有用的。不敢相信这么普通的任务竟然这么复杂。 感谢分享Ross Hendrickson's blog。这种方法对我有用。 这是一个非常优雅的解决方案,但是,它同样存在与所有其他解决方案相同的问题 - 它不会保留重命名后的历史记录。【参考方案8】:

曾经有过类似的挠痒痒(尽管仅适用于给定存储库的某些文件),但事实证明这个脚本非常有用:git-import

简短的版本是它从现有存储库创建给定文件或目录 ($object) 的补丁文件:

cd old_repo
git format-patch --thread -o "$temp" --root -- "$object"

然后将其应用到新的存储库:

cd new_repo
git am "$temp"/*.patch 

详情请查阅:

记录在案的source git format-patch git am

更新(来自另一位作者)以下bash 函数 可以使用这种有用的方法。这是一个示例用法:

gitcp &lt;Repo1_basedir&gt; &lt;path_inside_repo1&gt; &lt;Repo2_basedir&gt;

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

【讨论】:

【参考方案9】:

我经常使用的是http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/。简单快速。

为了符合 *** 标准,程序如下:

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【参考方案10】:

我想要一些健壮且可重用的东西(一个命令并执行 + 撤消功能),所以我编写了以下 bash 脚本。多次为我工作,所以我想我会在这里分享。

可以将任意文件夹/path/to/foorepo1移动到/some/other/folder/barrepo2(文件夹路径可以相同也可以不同,与根文件夹的距离可能不同)。

由于它只处理涉及输入文件夹中文件的提交(而不是源代码库的所有提交),因此即使在大型源代码库中它也应该非常快,如果您只是提取一个深度嵌套的子文件夹,而不是每次提交都涉及到。

由于这样做是创建一个包含所有旧仓库历史记录的孤立分支,然后将其合并到 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

【讨论】:

感谢这个脚本,它真的很有帮助。两个小修复: 1. 如果 REWRITE_TO 包含破折号,sed 表达式会失败。例如“我的文件夹”。因此,我将其修改为使用@作为分隔符: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 &amp;&amp; 我希望它对某人有所帮助,Ori。跨度> 【参考方案11】:

就我而言,我不需要保留要从中迁移的存储库或保留任何以前的历史记录。我有一个相同分支的补丁,来自不同的远程

#Source directory
git remote rm origin
#Target directory
git remote add branch-name-from-old-repo ../source_directory

在这两个步骤中,我能够让另一个 repo 的分支出现在同一个 repo 中。

最后,我设置了这个分支(我从另一个 repo 导入的)跟随目标 repo 的主线(这样我就可以准确地区分它们)

git br --set-upstream-to=origin/mainline

现在它的表现就好像它只是我针对同一个 repo 推送的另一个分支。

【讨论】:

【参考方案12】:

如果有问题的文件在两个存储库中的路径相同,并且您只想引入一个文件或一小组相关文件,一个简单的方法是使用git cherry-pick

第一步是使用git fetch &lt;remote-url&gt; 将来自其他存储库的提交带入您自己的本地存储库。这将使FETCH_HEAD 指向另一个仓库的头部提交;如果您想在完成其他提取后保留对该提交的引用,您可能需要使用git tag other-head FETCH_HEAD 标记它。

然后,您需要为该文件创建一个初始提交(如果它不存在)或一个提交以使该文件进入可以使用您想要引入的其他存储库的第一个提交修补的状态. 如果commit-0 引入了您想要的文件,您可能可以使用git cherry-pick &lt;commit-0&gt; 执行此操作,或者您可能需要“手动”构建提交。如果您需要修改初始提交,例如,从该提交中删除您不想引入的文件,请将 -n 添加到樱桃选择选项中。

之后,您可以继续git cherry-pick 后续提交,必要时再次使用-n。在最简单的情况下(所有提交都是您想要的并且干净地应用),您可以在cherry-pick 命令行上提供完整的提交列表:git cherry-pick &lt;commit-1&gt; &lt;commit-2&gt; &lt;commit-3&gt; ...

【讨论】:

【参考方案13】:

试试这个

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 覆盖

【讨论】:

警告:git-filter-branch 有大量的陷阱,会产生错误的历史重写。在继续中止之前按 Ctrl-C,然后使用替代过滤工具,例如 'git filter-repo' (github.com/newren/git-filter-repo)。有关详细信息,请参阅过滤器分支手册页;要消除此警告,请设置 FILTER_BRANCH_SQUELCH_WARNING=1。【参考方案14】:

通过维护所有分支并保留历史记录,将我的 GIT Stash 迁移到 GitLab 的以下方法。

将旧存储库克隆到本地。

git clone --bare <STASH-URL>

在 GitLab 中创建一个空存储库。

git push --mirror <GitLab-URL>

当我们将代码从 stash 迁移到 GitLab 时,我执行了上述操作,并且运行良好。

【讨论】:

【参考方案15】:

使用 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 addgit merge 之间,您需要运行git fetch 以使目标存储库知道源存储库中的更改。 我在临时 (project2) 克隆中一次性过滤并重命名:git filter-repo --path sub/dir --path-rename sub:newsub 以获得/newsub/dir 的树。这个工具使这个过程变得非常简单。 如果文件以前被移动/重命名,这不会自动保留移动/重命名之前的历史记录。但是,如果您在命令中包含原始路径/文件名,则不会删除该历史记录。例如,git filter-repo --path CurrentPathAfterRename --path OldPathBeforeRenamegit filter-repo --analyze 生成一个文件 renames.txt 可以帮助确定这些。或者,您可能会发现 script like this 有帮助。 这也适用于移动单个文件。在git filter-repo 命令参数中,只需为您要移动的每个文件或目录添加一个--path 参数。【参考方案16】:

我做了什么:

    将存储库克隆到文件夹 cd 现有项目 在这里打开一个 git 终端 git 远程设置 URL 来源 git push -u origin --all git push origin --tags

【讨论】:

这会将源存储库的所有文件复制到新存储库。本质上,它创建了完整源存储库的副本。 OP 只想要原始存储库的一部分。【参考方案17】:

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

【讨论】:

以上是关于如何将一些文件从一个 git repo 移动到另一个(不是克隆),保留历史记录的主要内容,如果未能解决你的问题,请参考以下文章

markdown 将git stashes从一个repo移动到另一个repo

如何删除git嵌套repo但保留其中一个分支的文件?

如何使用 Git / IntelliJ 将整个文件从一个分支移动/复制到另一个分支? [复制]

如何使用 ASP.NET 将文件从一台机器移动到另一台机器?

将对象从一页移动到另一页?

将 Git 存储库内容移动到另一个存储库,保留历史记录