我正在一个新项目中使用Git,该项目有两个并行但目前处于试验阶段的开发分支:

master:导入现有的代码库,再加上一些我通常确信的修改exp1:实验分支#1exp2:实验分支#2

exp1和exp2表示两种非常不同的体系结构方法。在我进一步相处之前,我无法知道哪一个(如果有的话)会起作用。当我在一个分支中取得进展时,我有时会对另一个分支进行有用的编辑,并希望仅合并这些编辑。

将一个开发分支中的选择性更改合并到另一个分支中,同时保留其他所有内容的最佳方法是什么?

我考虑过的方法:

gitmerge——无需提交,然后手动取消大量编辑,我不想在分支之间共享这些编辑。手动将通用文件复制到临时目录中,然后git checkout移动到另一个分支,然后更多地手动将临时目录复制到工作树中。在上述基础上的变化。暂时放弃exp分支,使用另外两个本地存储库进行实验。这使得手动复制文件更加简单。

所有这三种方法都显得乏味且容易出错。我希望有更好的方法;类似于过滤器路径参数,可以使gitmerge更有选择性。


当前回答

虽然其中一些答案很好,但我觉得没有人真正回答了OP的原始约束:从特定分支中选择特定文件。这个解决方案可以做到这一点,但如果有很多文件,可能会很乏味。

假设您有master、exp1和exp2分支。您希望将每个实验分支中的一个文件合并到master中。我会这样做:

git checkout master
git checkout exp1 path/to/file_a
git checkout exp2 path/to/file_b

# Save these files as a stash
git stash

# Merge stash with master
git merge stash

这将为您提供所需的每个文件的文件差异。没什么了。一点也不差。在不同版本之间进行完全不同的文件更改非常有用——在我的例子中,将应用程序从RubyonRails 2更改为RubyonRails 3。

这将合并文件,但它会进行智能合并。我无法弄清楚如何使用此方法获取文件差异信息(也许对于极端的差异,它仍然会这样做。除非使用-s递归-X忽略所有空格选项,否则像空格这样的恼人的小东西会被合并回来)

其他回答

我编写了自己的脚本“pmerge”来部分合并目录。这是一项正在进行的工作,我仍然在学习Git和Bash脚本。

该命令使用gitmerge--no提交,然后取消应用与提供的路径不匹配的更改。

用法:git pmerge分支路径示例:gitmerge-devel-src/

我还没有对它进行广泛的测试。工作目录应该没有任何未提交的更改和未跟踪的文件。

#!/bin/bash

E_BADARGS=65

if [ $# -ne 2 ]
then
    echo "Usage: `basename $0` branch path"
    exit $E_BADARGS
fi

git merge $1 --no-commit
IFS=$'\n'

# List of changes due to merge | replace nulls with newlines | strip lines to just filenames | ensure lines are unique
for f in $(git status --porcelain -z -uno | tr '\000' '\n' | sed -e 's/^[[:graph:]][[:space:]]\{1,\}//' | uniq); do
    [[ $f == $2* ]] && continue
    if git reset $f >/dev/null 2>&1; then
        # Reset failed... file was previously unversioned
        echo Deleting $f
        rm $f
    else
        echo Reverting $f
        git checkout -- $f >/dev/null 2>&1
    fi
done
unset IFS

我将专注于我感兴趣的这个问题的子集:我有两个分支,我想将一个文件伪合并到另一个文件中。

(我说“伪合并”是因为我不需要或不希望合并提交;我只想以我认为合适的方式合并文件两个版本的贡献。)

我的方法基于https://stackoverflow.com/a/39916536/341994.不幸的是,这个问题是重复的(在我看来,这是错误的:它不是这个问题的重复,回答和重复是错误的,这是回答者在那里做的)。但这个答案有一些问题,所以我已经现代化并清理了方法。我使用恢复而不是签出和重置,并且我不必提交任何不需要的东西。

好吧,假设我有三个文件:

$ ls
a   b   f

但我只想伪合并其中一个,a,来自另一个分支。让我们看看他们,看看情况会是什么样子。这是我的版本:

$ cat a
line one
line two
line three
line four
line five

以下是其他分支机构的版本:

$ git show otherbranch:a
line one
line two edited
line three
line four
line five
line six

现在这里的技巧是,我们将使用索引作为草稿(毕竟,这是它的用途)。因此,我们首先(步骤1)确保将我们的版本复制到索引中:

$ git add a

现在(步骤2)我们可以使用restore从其他分支获取版本(现在,restore比checkout更好,因为它可以让我们更清楚地说话):

$ git restore --source otherbranch a

乍一看,这看起来很糟糕。我们现在已经用其他分支的版本完全覆盖了我们的a,如您所见:

$ cat a
line one
line two edited
line three
line four
line five
line six

但别担心!如您所见,以前版本的仍在索引中:

$ git diff a
diff --git a/a b/a
index abf51fa..333614b 100644
--- a/a
+++ b/a
@@ -1,6 +1,7 @@
 line one
-line two
+line two edited
 line three
 line four
 line five
+line six

很好,现在我们准备好了关键步骤(步骤3)。我们将文件从工作树添加到索引中进行交互式补丁。

我们可以说git add-p a来启动交互式补丁程序,在这种情况下,我们一次只能得到一个大块。但在这种情况下,只有一个帅哥,我无论如何都想编辑它,所以我说:

$ git add --e a

结果是我们在编辑器中打开了一个diff补丁文件!它看起来像这样:

 line one
-line two
+line two edited
 line three
 line four
 line five
+line six

通过仔细编辑,我们现在可以决定我们要接受哪些部分,不接受哪些部分。让我们接受“第六行”,而不是“第二行已编辑”。所以我们编辑如下:

 line one
 line two
 line three
 line four
 line five
+line six

我们关闭编辑器,补丁应用于a的索引版本。但我们还没有完全完成!a的另一个分支版本仍然位于工作树中:

$ cat a
line one
line two edited
line three
line four
line five
line six

我们喜欢的版本在索引中,记得吗?为了实现这一点,(步骤4)我们只需简单地调用gitrestore(同样,这是现代的方式;restore比reset更好,可以应用于单个文件):

$ git restore a

现在我们的a是正确的,我们都完成了:

$ cat a
line one
line two
line three
line four
line five
line six

我们现在可以承诺,但我们不必;我们已经完成了既定目标。

您可以使用cherry-pick命令从一个分支获取单个提交。

如果所需的更改不在单个提交中,则使用此处显示的方法将提交拆分为单个提交。大致来说,您使用git rebase-i获取原始提交以进行编辑,然后使用git reset HEAD^选择性地还原更改,然后使用git commit将该位提交为历史中的新提交。

Red Hat Magazine还有另一个很好的方法,他们使用git-add——补丁或者可能是git-add,如果你想将不同的更改拆分到一个单独的文件中(在该页面中搜索“拆分”),则可以只添加大块的一部分。

拆分更改后,您现在可以随意选择所需的更改。

我喜欢前面的“git交互式合并”答案,但有一个更简单。让Git使用交互式和到以下内容的重基组合为您实现这一点:

      A---C1---o---C2---o---o feature
     /
----o---o---o---o master

因此,您需要从“feature”分支(分支点“A”)中获取C1和C2,但目前没有其他分支。

# git branch temp feature
# git checkout master
# git rebase -i --onto HEAD A temp

正如前面的回答一样,这会让您进入交互式编辑器,在其中选择C1和C2的“pick”行(如上所述)。保存并退出,然后它将继续重新创建数据库,并为您提供分支“temp”和主机+C1+C2的HEAD:

      A---C1---o---C2---o---o feature
     /
----o---o---o---o-master--C1---C2 [HEAD, temp]

然后,您只需将master更新为HEAD并删除临时分支即可:

# git branch -f master HEAD
# git branch -d temp

下面是如何将master分支中的Myclass.java文件替换为feature1分支中的Myclass.java。即使master上不存在Myclass.java,它也可以工作。

git checkout master
git checkout feature1 Myclass.java

注意,这将覆盖-而不是合并-并忽略主分支中的本地更改。