我们的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

但这似乎相当复杂。有没有更好的方法来做这类事情呢?还是我采取了正确的方法?

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


当前回答

这个答案提供了基于git am的有趣命令,并使用示例一步一步地展示。

客观的

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

过程

提取历史在电子邮件格式使用 Git日志——pretty=email -p——reverse——full-index——二进制 重新组织文件树并更新历史记录中的文件名更改[可选] 使用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

清理你的回购源

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"' {} ';'

不幸的是,“跟随”或“更难找到副本”选项不能与“反向”组合。这就是为什么重命名文件(或重命名父目录)时删除历史记录的原因。

After:邮件格式的临时历史

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

注意:由于历史已经被重写,以反映路径和文件名的变化: (即与上一份回购合约内的地点/名称比较)

不需要git mv来更改位置/文件名。 不需要git log -follow来访问完整的历史记录。


额外的技巧:检测重命名/移动文件在你的回购

列出已重命名的文件。

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

更多自定义:您可以使用选项——find-copies-harder或——reverse来完成命令git日志。您还可以使用cut -f3-和grepping complete pattern '{删除前两列。* => .*}'。

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'

其他回答

下面是通过维护所有分支和保存历史记录来将我的GIT Stash迁移到GitLab的方法。

将旧的存储库克隆到本地。

git clone --bare <STASH-URL>

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

git push --mirror <GitLab-URL>

当我们将代码从stash迁移到GitLab时,我执行了上述操作,效果非常好。

在尝试了将文件或文件夹从一个Git存储库移动到另一个存储库的各种方法后,下面概述了唯一可靠的方法。

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

阶段一

Make a copy of repository A as the following steps make major changes to this copy which you should not push! 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 into it cd <git repository A directory> # eg. cd /c/Working/GIT/myprojects Delete the link to the original repository to avoid accidentally making any remote changes (eg. by pushing) git remote rm origin Go through your history and files, removing anything that is not in directory 1. The result is the contents of directory 1 spewed out into to the base of repository A. git filter-branch --subdirectory-filter <directory> -- --all # eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all For single file move only: go through what's left and remove everything except the desired file. (You may need to delete files you don't want with the same name and commit.) 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重置——很难 清理步骤 Git gc -aggressive 清理步骤 git修剪

你可能想要将这些文件导入存储库B中的一个目录,而不是根目录:

创建那个目录 Mkdir <基本目录>;mkdir FOLDER_TO_KEEP 将文件移动到该目录 Git mv * <基本目录>git mv *文件夹to_keep 将文件添加到该目录 Git添加。 提交您的更改,我们准备将这些文件合并到 新的存储库 git提交

第三阶段

Make a copy of repository B if you don’t have one already git clone <git repository B url> # eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git (assuming FOLDER_TO_KEEP is the name of the new repository you are copying to) cd into it cd <git repository B directory> # eg. cd /c/Working/GIT/FOLDER_TO_KEEP Create a remote connection to repository A as a branch in repository 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 Pull from this branch (containing only the directory you want to move) into repository B. git pull repo-A-branch master --allow-unrelated-histories The pull copies both files and history. Note: You can use a merge instead of a pull, but pull works better. Finally, you probably want to clean up a bit by removing the remote connection to repository A git remote rm repo-A-branch Push and you’re all set. git push

我发现Ross Hendrickson的博客很有用。这是一种非常简单的方法,您可以创建应用于新回购的补丁。更多细节请参见链接页面。

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

# 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 am的有趣命令,并使用示例一步一步地展示。

客观的

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

过程

提取历史在电子邮件格式使用 Git日志——pretty=email -p——reverse——full-index——二进制 重新组织文件树并更新历史记录中的文件名更改[可选] 使用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

清理你的回购源

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"' {} ';'

不幸的是,“跟随”或“更难找到副本”选项不能与“反向”组合。这就是为什么重命名文件(或重命名父目录)时删除历史记录的原因。

After:邮件格式的临时历史

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

注意:由于历史已经被重写,以反映路径和文件名的变化: (即与上一份回购合约内的地点/名称比较)

不需要git mv来更改位置/文件名。 不需要git log -follow来访问完整的历史记录。


额外的技巧:检测重命名/移动文件在你的回购

列出已重命名的文件。

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

更多自定义:您可以使用选项——find-copies-harder或——reverse来完成命令git日志。您还可以使用cut -f3-和grepping complete pattern '{删除前两列。* => .*}'。

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'

通过使用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 (更多细节和选项在README)

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

# After: (project1)
.
├── 3
└── sub
    └── dir
        └── 2