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

好久不见. 提交于 2020-02-25 18:41:27

我们的Git储存库是作为单个Monster 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

但这似乎令人费解。 总的来说,有没有更好的方法来做这种事情? 还是我采用了正确的方法?

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


#1楼

如果您的历史记录是合理的,则可以将提交作为补丁取出,并将其应用到新的存储库中:

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的文档


#2楼

是的,击中filter-branch--subdirectory-filter是关键。 使用它的事实实质上证明,没有其他方法可以更简便-只能重写历史记录,因为您只想保留文件的一个(重命名的)子集,并且根据定义,这可以更改哈希值。 由于没有标准命令(例如pull )重写历史记录,因此您无法使用它们来完成此操作。

当然,您可以细化细节-并非必须进行某些克隆和分支操作-但总体方法是好的! 太复杂了,真是可惜,但是,当然git的目的并不是要使重写历史记录变得容易。


#3楼

尝试了各种方法将文件或文件夹从一个Git存储库移动到另一个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)
  2. CD进入

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

    git remote rm origin
  4. 浏览您的历史记录和文件,删除目录1中没有的任何内容。结果是目录1的内容被喷入存储库A的库中。

    git filter-branch --subdirectory-filter <directory> -- --all # eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all
  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

第二阶段

  1. 清理步骤

    git reset --hard
  2. 清理步骤

    git gc --aggressive
  3. 清理步骤

    git prune

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

  1. 建立该目录

    mkdir <base directory> eg. mkdir FOLDER_TO_KEEP
  2. 将文件移到该目录

    git mv * <base directory> eg. git mv * FOLDER_TO_KEEP
  3. 将文件添加到该目录

    git add .
  4. 提交更改,我们准备将这些文件合并到新的存储库中

    git commit

第三阶段

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

    git clone <git repository B url> # eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git

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

  2. CD进入

    cd <git repository B directory> # eg. cd /c/Working/GIT/FOLDER_TO_KEEP
  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
  4. 从该分支(仅包含要移动的目录)中拉入存储库B。

    git pull repo-A-branch master --allow-unrelated-histories

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

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

    git remote rm repo-A-branch
  6. 按下即可,一切就绪。

    git push

#4楼

保留目录名称

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

这个答案提供了基于git am有趣命令,并通过示例逐步介绍。

目的

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

程序

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

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

例如:提取的历史file3file4file5

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

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

因此,重新组织您的文件:

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查看准备推送的提交量:-)

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

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

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

列出已重命名的文件:

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'

更多自定义设置:您可以使用--find-copies-harder --reverse--reverse选项完成命令git log 。 您还可以使用cut -f3-cut -f3-完整模式'{。* =>。*}'删除前两列。

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!