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

ebn*_*ter 447 git repository

我们的Git存储库最初是作为单个怪物SVN存储库的一部分开始的,其中每个项目都有自己的树,如下所示:

project1/branches
        /tags
        /trunk
project2/branches
        /tags
        /trunk
Run Code Online (Sandbox Code Playgroud)

显然,将文件从一个文件移动到另一个文件非常容易svn mv.但在Git中,每个项目都在自己的存储库中,今天我被要求将子目录从中移动project2project1.我做了这样的事情:

$ 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
Run Code Online (Sandbox Code Playgroud)

但这似乎很复杂.有没有更好的方法来做这种事情?或者我采用了正确的方法?

请注意,这涉及将历史记录合并到现有存储库中,而不是简单地从另一个存储库中创建一个新的独立存储库(如前面的问题).

Sma*_*mar 261

如果您的历史记录是正确的,您可以将提交作为补丁并将其应用于新的存储库:

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 
Run Code Online (Sandbox Code Playgroud)

或者在一行中

git log --pretty=email --patch-with-stat --reverse -- path/to/file_or_folder | (cd /path/to/new_repository && git am)
Run Code Online (Sandbox Code Playgroud)

(取自Exherbo的文档)

  • 在apply步骤中,我使用`--committer-date-is-author-date`选项来保留原始提交日期而不是文件移动的日期. (31认同)
  • 对于我需要移动的三个或四个文件,这是一个比接受的答案更简单的解决方案.我最终使用find-replace修剪补丁文件中的路径,使其适合我的新repo的目录结构. (20认同)
  • 我添加了选项,以便正确迁移二进制文件(如图像):`git log --pretty = email --patch-with-stat --full-index --binary --reverse - client> patch`.工作没有问题AFAICT. (8认同)
  • @Daniel Golden我已经设法解决了已被移动的文件的问题(这是`git log`中的一个错误的结果,因此它不适用于`--follow`和`--reverse '正确).我使用[这个答案](http://stackoverflow.com/a/35380344/2519373),这是[我现在用来移动文件的完整脚本](https://gist.github.com/tsayen/ f1c1c4d62d4fda77abf1586bd39f9b74) (6认同)
  • 对于已移动/重命名的文件不起作用.我假设您需要为每个文件制作单独的补丁,并将`--follow`选项添加到`git log`(一次仅适用于一个文件). (5认同)
  • 历史中的合并提交打破了"am"命令.你可以在上面的git log命令中添加"-m --first-parent",然后它对我有用. (5认同)
  • 这是我一直在使用的另一种类似方法:http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ (4认同)
  • 只是为了提供一些成本/收益分析:我试过这个收集了29个文件(总共3.5k行),补丁的应用大约需要15分钟!可能在几秒钟内完成了一个愚蠢的副本 - 要求实际复杂变化的问题是"我真的*需要历史吗?" (3认同)
  • 如果你在 gitconfig 中出于某种原因使用了 `--color=always`,那么你需要将 `--color=never` 添加到 `git log` 是没有意义的。 (2认同)
  • @Smar 我使用上面的命令创建了一个补丁。我尝试使用 `git am` 应用补丁,但我遇到了 `Patch is empty。分裂错了吗?`。该错误不是立即引发的,而是在应用一些更改后引发的。 (2认同)
  • @Smar,@phani - 我也遇到过那些“空补丁”错误,我认为这是因为 git log 处理合并提交的方式。正如 Gábor Lipták 建议的那样,值得添加 `-m --first-parent` 以在补丁中包含这些差异 (2认同)
  • 请注意,也可以将多个文件添加到列表中 (2认同)

mca*_*ans 72

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

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

第一阶段

  1. 制作存储库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)
    
    Run Code Online (Sandbox Code Playgroud)

    (假设myprojects是您要复制的存储库)

  2. 进入它

    cd <git repository A directory>
    #  eg. cd /c/Working/GIT/myprojects
    
    Run Code Online (Sandbox Code Playgroud)
  3. 删除原始存储库的链接,以避免意外进行任何远程更改(例如,通过推送)

    git remote rm origin
    
    Run Code Online (Sandbox Code Playgroud)
  4. 浏览历史记录和文件,删除目录1中没有的任何内容.结果是目录1的内容扩展到存储库A的基础.

    git filter-branch --subdirectory-filter <directory> -- --all
    # eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all
    
    Run Code Online (Sandbox Code Playgroud)
  5. 仅适用于单个文件移动:浏览左侧的内容并删除除所需文件之外的所有内容.(您可能需要使用相同的名称删除不需要的文件并提交.)

    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
    
    Run Code Online (Sandbox Code Playgroud)

    例如.FILE_TO_KEEP = pom.xml只保留FOLDER_TO_KEEP中的pom.xml文件

第二阶段

  1. 清理步骤

    git reset --hard
    
    Run Code Online (Sandbox Code Playgroud)
  2. 清理步骤

    git gc --aggressive
    
    Run Code Online (Sandbox Code Playgroud)
  3. 清理步骤

    git prune
    
    Run Code Online (Sandbox Code Playgroud)

您可能希望将这些文件导入到不是根目录的目录中的存储库B中:

  1. 制作该目录

    mkdir <base directory>             eg. mkdir FOLDER_TO_KEEP
    
    Run Code Online (Sandbox Code Playgroud)
  2. 将文件移动到该目录中

    git mv * <base directory>          eg. git mv * FOLDER_TO_KEEP
    
    Run Code Online (Sandbox Code Playgroud)
  3. 将文件添加到该目录

    git add .
    
    Run Code Online (Sandbox Code Playgroud)
  4. 提交您的更改,我们已准备好将这些文件合并到新存储库中

    git commit
    
    Run Code Online (Sandbox Code Playgroud)

第三阶段

  1. 如果您还没有存储库B,请复制存储库B.

    git clone <git repository B url>
    # eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git
    
    Run Code Online (Sandbox Code Playgroud)

    (假设FOLDER_TO_KEEP是您要复制到的新存储库的名称)

  2. 进入它

    cd <git repository B directory>
    #  eg. cd /c/Working/GIT/FOLDER_TO_KEEP
    
    Run Code Online (Sandbox Code Playgroud)
  3. 创建与存储库A的远程连接作为存储库B中的分支

    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
    
    Run Code Online (Sandbox Code Playgroud)

    (repo-A-branch可以是任何东西 - 它只是一个任意名称)

    git pull repo-A-branch master --allow-unrelated-histories
    
    Run Code Online (Sandbox Code Playgroud)
  4. 从此分支(仅包含您要移动的目录)拉入存储库B.

    git remote rm repo-A-branch
    
    Run Code Online (Sandbox Code Playgroud)

    pull会复制文件和历史记录.注意:您可以使用合并而不是拉动,但拉动效果更好.

  5. 最后,您可能希望通过删除与存储库A的远程连接来清理一下

    git push
    
    Run Code Online (Sandbox Code Playgroud)
  6. 推,你一切都准备好了.

    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)
    
    Run Code Online (Sandbox Code Playgroud)

  • @mcarans 不幸的是,这不是可靠的方式,尽管它似乎是。它遇到与所有其他解决方案相同的问题 - 它不保留重命名后的历史记录。就我而言,第一次提交是在我重命名目录/文件时。除此之外的一切都失去了。 (2认同)

Cas*_*bel 54

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

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

  • 我有什么关于如何重命名的方法吗? (6认同)
  • 我认为维护和管理历史是 git 的要点之一。 (3认同)
  • 如果您的文件已经移动了多个目录,现在驻留在一个目录中-- 子目录过滤器是否仍然有效?(即我假设如果我只想移动一个文件,我可以将它移动到它自己的子目录,这会起作用吗?) (2认同)

anh*_*ppe 20

我发现非常有用.这是一种非常简单的方法,您可以在其中创建应用于新仓库的修补程序.有关详细信息,请参阅链接页面.

它只包含三个步骤(从博客中复制):

# 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
Run Code Online (Sandbox Code Playgroud)

我遇到的唯一问题是我无法一次性应用所有补丁

git am --3way <patch-directory>/*.patch
Run Code Online (Sandbox Code Playgroud)

在Windows下,我收到了InvalidArgument错误.所以我不得不一个接一个地应用所有补丁.

  • 尝试了不同的方法将项目转移到新的仓库。这是唯一对我有用的。不敢相信这样一个常见的任务竟然如此复杂。 (3认同)

Tap*_*uzi 11

通过使用 git-filter-repo,这变得更简单。

为了移动project2/sub/dirproject1/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
Run Code Online (Sandbox Code Playgroud)

为了简单地安装该工具:pip3 install git-filter-repo在README更多详细信息和选项

# Before: (root)
.
|-- project1
|   `-- 3
`-- project2
    |-- 1
    `-- sub
        `-- dir
            `-- 2

# After: (project1)
.
??? 3
??? sub
    ??? dir
        ??? 2
Run Code Online (Sandbox Code Playgroud)

  • 如果文件之前被移动/重命名,则不会自动保留移动/重命名之前的历史记录。但是,如果您在命令中包含原始路径/文件名,则该历史记录将不会被删除。例如,`git filter-repo --path CurrentPathAfterRename --path OldPathBeforeRename`。`git filter-repo --analyze` 生成一个文件 renames.txt,有助于确定这些。或者,您可能会发现[这样的脚本](/sf/answers/4188291061/)很有帮助。 (3认同)
  • 在 Windows 上,请注意在 `git filter-repo --path sub/dir/` 中使用斜杠 (/) 而不是反斜杠 (\\) 进行路径选择。否则,它会为您提供一个空文件夹,因为没有任何匹配项。 (3认同)

Tap*_*uzi 8

git subtree直观地工作,甚至保留历史。

\n

用法示例:\n将 git 存储库添加为子目录:

\n
git subtree add --prefix foo https://github.com/git/git.git master\n
Run Code Online (Sandbox Code Playgroud)\n

解释:

\n
#\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 repo_bar\n#\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 bar.txt\n#\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 repo_foo\n#    \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 foo.txt\n\ncd repo_bar\ngit subtree add --prefix foo ../repo_foo master\n\n#\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 repo_bar\n#\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 bar.txt\n#\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 foo\n#\xe2\x94\x82\xc2\xa0\xc2\xa0     \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 foo.txt\n#\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 repo_foo\n#    \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 foo.txt\n
Run Code Online (Sandbox Code Playgroud)\n


Joa*_*son 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
Run Code Online (Sandbox Code Playgroud)

  • 我还认为保持目录名称完整非常重要。否则,您将获得对目标存储库的额外重命名提交。 (2认同)

oli*_*bre 5

这个答案提供有趣的命令,基于git am并逐步使用示例.

目的

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

程序

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

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

例如:提取的历史file3,file4file5

my_repo
??? dirA
?   ??? file1
?   ??? file2
??? dirB            ^
?   ??? subdir      | To be moved
?   ?   ??? file3   | with history
?   ?   ??? file4   | 
?   ??? file5       v
??? dirC
    ??? file6
    ??? file7
Run Code Online (Sandbox Code Playgroud)

清理临时目录目标

export historydir=/tmp/mail/dir  # Absolute path
rm -rf "$historydir"             # Caution when cleaning
Run Code Online (Sandbox Code Playgroud)

清理您的回购

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
Run Code Online (Sandbox Code Playgroud)

以电子邮件格式提取每个文件的历史记录

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"' {} ';'
Run Code Online (Sandbox Code Playgroud)

不幸的选择--follow--find-copies-harder不能结合--reverse.这就是重命名文件时(或重命名父目录时)切断历史记录的原因.

之后:电子邮件格式的临时历史记录

/tmp/mail/dir
    ??? subdir
    ?   ??? file3
    ?   ??? file4
    ??? file5
Run Code Online (Sandbox Code Playgroud)

2.重新组织文件树并更新历史记录中的文件名更改[可选]

假设您要在这个其他仓库中移动这三个文件(可以是相同的仓库).

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
Run Code Online (Sandbox Code Playgroud)

因此重新组织您的文件:

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
Run Code Online (Sandbox Code Playgroud)

您的临时历史记录现在是:

/tmp/mail/dir
    ??? dirB
        ??? dirB1
        ?   ??? file33
        ?   ??? file44
        ??? dirB2
             ??? file5
Run Code Online (Sandbox Code Playgroud)

更改历史记录中的文件名:

cd "$historydir"
find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';'
Run Code Online (Sandbox Code Playgroud)

注意:这会重写历史记录以反映路径和文件名的更改.
      (即新仓库中新位置/名称的更改)


3.应用新历史记录

你的其他回购是:

my_other_repo
??? dirF
?   ??? file55
?   ??? file56
??? dirH
    ??? file77
Run Code Online (Sandbox Code Playgroud)

从临时历史文件中应用提交:

cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am 
Run Code Online (Sandbox Code Playgroud)

你的其他回购现在是:

my_other_repo
??? dirF
?   ??? file55
?   ??? file56
??? dirB            ^
?   ??? dirB1       | New files
?   ?   ??? file33  | with
?   ?   ??? file44  | history
?   ??? dirB2       | kept
?        ??? file5  v
??? dirH
    ??? file77
Run Code Online (Sandbox Code Playgroud)

使用git status看量的承诺准备推:-)

注意:由于历史记录已被重写以反映路径和文件名更改:(
      即与上一个仓库中的位置/名称相比)

  • 无需git mv更改位置/文件名.
  • 无需git log --follow访问完整历史记录.

额外技巧:在您的仓库中检测重命名/移动的文件

列出已重命名的文件:

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'
Run Code Online (Sandbox Code Playgroud)

更多自定义:您可以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 '{.* => .*}'
Run Code Online (Sandbox Code Playgroud)


ViT*_*oni 5

有一个类似的痒痒(虽然只适用于给定存储库的某些文件)这个脚本被证明是非常有帮助的:git-import

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

cd old_repo
git format-patch --thread -o "$temp" --root -- "$object"
Run Code Online (Sandbox Code Playgroud)

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

cd new_repo
git am "$temp"/*.patch 
Run Code Online (Sandbox Code Playgroud)

详情请看:

更新(来自另一位作者)以下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
}
Run Code Online (Sandbox Code Playgroud)


Hug*_*ins 5

我一直使用的网址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
Run Code Online (Sandbox Code Playgroud)

  • 如果 `git log` 显示为彩色,则 `grep ^commit` 可能不起作用。如果是这样,请将“--no-color”添加到“git log”命令中。(例如,`git log --no-color $reposrc`) (3认同)

归档时间:

查看次数:

134492 次

最近记录:

6 年,5 月 前