我们的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子树直观地工作,甚至保存历史。

使用示例: 将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 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存储库移动到另一个存储库的各种方法后,下面概述了唯一可靠的方法。

它包括克隆要从中移动文件或文件夹的存储库,将该文件或文件夹移动到根目录,重写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

对于类似的问题(尽管只针对特定存储库中的某些文件),这个脚本被证明是非常有用的:git-import

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

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

然后应用到一个新的存储库:

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

详情请查阅:

文档化的来源 git format-patch git是

下面的bash函数可以使用这种有用的方法。下面是一个用法示例:

gitcp <Repo1_basedir> <path_inside_repo1> <Repo2_basedir> . gitcp <Repo1_basedir> <path_inside_repo1>

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
}

我想要一些健壮和可重用的东西(一个命令行+撤销函数),所以我写了下面的bash脚本。我用过几次,所以我想在这里分享一下。

它能够将任意文件夹/路径/to/foo从repo1移动到/some/other/folder/bar到repo2(文件夹路径可以相同或不同,与根文件夹的距离可能不同)。

由于它只遍历输入文件夹中涉及文件的提交(而不是源回购的所有提交),即使在大的源回购上,如果你只是提取一个在每次提交中都没有触及的嵌套很深的子文件夹,它也应该相当快。

因为这样做是创建一个带有所有旧的回购历史的孤立分支,然后将其合并到HEAD,它甚至可以在文件名冲突的情况下工作(当然,然后您必须在最后解决合并)。

如果没有文件名冲突,您只需要在最后提交git来完成合并。

缺点是它可能不会遵循文件重命名(REWRITE_FROM文件夹之外)在源repo - pull请求欢迎GitHub来适应这一点。

GitHub链接:git-move-folder-between- restore -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