不使用检出即可合并、更新和拉取Git分支

851

我正在一个有两个分支A和B的项目上工作。我通常在分支A上工作,然后从分支B合并内容。对于合并,我通常会执行以下操作:

git merge origin/branchB

不过,我也想保留分支B的本地副本,因为偶尔我会在未先与我的A分支合并的情况下切换到该分支。为此,我将执行以下操作:

git checkout branchB
git pull
git checkout branchA

有没有一种方法可以用一个命令完成上述操作,而不必来回切换分支?我应该使用 git update-ref 吗?如何操作?


5
相关链接:https://dev59.com/fnM_5IYBdhLWcg3wXyDZ 和 https://dev59.com/L3A75IYBdhLWcg3wOWVd - Cascabel
1
Jakub的回答解释了为什么通常不可能实现Git checkout and merge without touching working tree。另一个事后的解释是,你不能在一个裸库中合并,因此显然需要工作树。 - Cascabel
3
@Eric:常见的原因是对于大型仓库来说,检出需要耗费时间,并且即使您返回相同的版本,它们也会更新时间戳,因此会认为所有内容都需要重新构建。 - Cascabel
我链接的第二个问题是关于一个不寻常的情况 - 合并可能是快进的,但OP想要使用--no-ff选项合并,这将导致合并提交被记录。如果您对此感兴趣,我的答案展示了如何做到这一点 - 不像我在这里发布的答案那样强大,但两者的优点肯定可以结合起来。 - Cascabel
20个回答

1306

简短回答

只要你进行的是快进合并,那么你可以简单地使用

git fetch <remote> <sourceBranch>:<destinationBranch>

例子:

# Merge local branch foo into local branch master,
# without having to checkout master first.
# Here `.` means to use the local repository as the "remote":
git fetch . foo:master

# Merge remote branch origin/foo into local branch foo,
# without having to checkout foo first:
git fetch origin foo:foo

虽然Amber的回答在快进情况下也可以起作用,但是这种方式使用git fetch比强制移动分支引用要安全一些,因为只要在refspec中不使用+git fetch会自动防止意外的非快进操作。

详细解答

如果没有首先检出A分支,就将B分支合并到A分支并导致非快进合并,则需要工作副本来解决潜在的冲突,因此这是不可能的。

然而,在快进合并的情况下,这是可能的,因为根据定义,这样的合并永远不会导致冲突。要在不首先检出分支的情况下执行此操作,可以使用带有refspec的git fetch

以下是一个示例,如果您检出了另一个分支feature,则更新master(禁止非快进更改):

git fetch upstream master:master

这个用例非常常见,您可能希望在git配置文件中为其创建一个别名,例如此示例:

[alias]
    sync = !sh -c 'git checkout --quiet HEAD; git fetch upstream master:master; git checkout --quiet -'

这个别名的作用如下:

  1. git checkout HEAD:将你的工作副本切换到分离头状态。如果你想要更新 master,同时又想保留当前所在分支,那么这很有用。我认为必须这样做,否则 master 的分支引用不会移动,但是我不确定是否确实如此。

  2. git fetch upstream master:master:将你的本地 master 快进到与 upstream/master 相同的位置。

  3. git checkout -:检出你之前所选择的分支(在这种情况下,- 的作用就是这个)。

git fetch 命令的语法(非快进式合并)

如果你想要在更新时使 fetch 命令失败(非快进式合并),那么你可以使用以下形式的 refspec:

git fetch <remote> <remoteBranch>:<localBranch>
如果您想允许非快进更新,则需在 refspec 的前面添加一个 +
git fetch <remote> +<remoteBranch>:<localBranch>

请注意,您可以使用 . 将本地仓库作为“remote”参数传递:

git fetch . <sourceBranch>:<destinationBranch>

文档

git fetch的文档中解释了这个语法(强调是我的):

<refspec>

<refspec>参数的格式是可选的加号+,后跟源引用<src>,再后跟冒号:,再后跟目标引用<dst>

匹配<src>的远程引用被获取,如果<dst>不为空字符串,则使用<src>快进匹配的本地引用。 如果使用可选的加号+,则即使不导致快进更新,也会更新本地引用。

另请参阅

  1. 不触及工作树的Git checkout和merge

  2. 合并而不更改工作目录


9
自 Git 1.7.5 版本以来,git checkout --quiet HEAD 等同于 git checkout --quiet --detach - Rafa Viotti
8
我发现我需要执行以下命令来更新本地的 foo 分支到与本地的 origin/foo 分支相同:git fetch . origin/foo:foo - weston
4
为什么长答案包含了"git checkout HEAD --quiet"和"git checkout --quiet -",但是短答案没有?我猜想这是因为当你已经检出主分支时,脚本可能会被运行,尽管你可以只执行"git pull"。 - Sean
4
git fetch upstream master:master 真是太棒了!因为我不需要先进行检出操作,在我的仓库中进行检出是一项代价高昂的任务。 - off99555
25
为什么这里用'fetch'命令来执行'merge',这根本没有意义;如果'pull'是'fetch'后跟着'merge',那么应该有一个更合乎逻辑的'merge --ff-only'等效命令,可以在已经运行了'fetch'的情况下从'origin/branch'更新'branch'本地分支。 - Ed Randall
显示剩余12条评论

94
不,没有这种情况。你需要检出目标分支以解决冲突,此外还有其他一些事情要做(如果Git无法自动合并它们)。但是,如果合并是快进的,那么你不需要检出目标分支,因为你实际上不需要合并任何东西——你所要做的就是更新分支以指向新的头引用。你可以使用git branch -f完成这个操作。
git branch -f branch-b branch-a

branch-b 更新为指向 branch-a 的头部。

-f 选项代表 --force,这意味着 branch-b 将被覆盖。

注意:一个更安全的选择是使用 git fetch ,它只允许进行快速合并

可以按如下方式使用此方法:

git branch -f branch-b branch-b@{Upstream}
或者更短
git branch -f branch-b branch-b@{U}

如果要强制更新一个分支而不用检出它(比如在变基后分支已经发生了分叉),可以使用以下命令:


51
除非你完全确认合并会是一个快进合并,否则一定要非常小心,不要这样做!否则你可能会在后面意识到自己已经错放了提交记录。 - Cascabel
11
这个回答中可以看到,通过执行git fetch upstream branch-b:branch-b也可以获得相同的结果(快进). - Oliver
8
继续解释@Oliver的评论,您还可以执行git fetch <remote> B:A,其中B和A是完全不同的分支,但B可以快进合并到A中。您还可以将本地仓库作为“remote”传递,使用.作为远程别名:git fetch . B:A - user456814
11
根据提问者的问题,可以很清楚地看出合并确实将是快进模式。但是,正如你所指出的那样,“branch -f”可能是危险的。 因此,不要使用它!使用“fetch origin branchB:branchB”,如果合并不是快进模式,它会安全地失败。 - Bennett McElwee
3
我不同意建议在日常操作中使用--force。 - ANeves
显示剩余6条评论

32

正如Amber所说,只有在进行快进式合并时才可能这样做。任何其他合并都需要进行完整的三方合并,应用修补程序,解决冲突等等 - 这意味着必须存在文件。

我碰巧有一个脚本可用于完全执行此操作:在不触及工作树的情况下执行快进式合并(除非您正在合并到HEAD)。它有点长,因为至少要具有一定的鲁棒性 - 它会检查合并是否可以是快进式的,然后执行合并而无需检出分支,但产生的结果与您检出分支后的结果相同 - 您将看到diff --stat更改摘要和reflog中的条目完全像快进式合并一样,而不是如果使用branch -f则会得到"重置"的那个。如果将其命名为git-merge-ff并放入您的bin目录中,则可以将其作为git命令调用:git merge-ff

#!/bin/bash

_usage() {
    echo "Usage: git merge-ff <branch> <committish-to-merge>" 1>&2
    exit 1
}

_merge_ff() {
    branch="$1"
    commit="$2"

    branch_orig_hash="$(git show-ref -s --verify refs/heads/$branch 2> /dev/null)"
    if [ $? -ne 0 ]; then
        echo "Error: unknown branch $branch" 1>&2
        _usage
    fi

    commit_orig_hash="$(git rev-parse --verify $commit 2> /dev/null)"
    if [ $? -ne 0 ]; then
        echo "Error: unknown revision $commit" 1>&2
        _usage
    fi

    if [ "$(git symbolic-ref HEAD)" = "refs/heads/$branch" ]; then
        git merge $quiet --ff-only "$commit"
    else
        if [ "$(git merge-base $branch_orig_hash $commit_orig_hash)" != "$branch_orig_hash" ]; then
            echo "Error: merging $commit into $branch would not be a fast-forward" 1>&2
            exit 1
        fi
        echo "Updating ${branch_orig_hash:0:7}..${commit_orig_hash:0:7}"
        if git update-ref -m "merge $commit: Fast forward" "refs/heads/$branch" "$commit_orig_hash" "$branch_orig_hash"; then
            if [ -z $quiet ]; then
                echo "Fast forward"
                git diff --stat "$branch@{1}" "$branch"
            fi
        else
            echo "Error: fast forward using update-ref failed" 1>&2
        fi
    fi
}

while getopts "q" opt; do
    case $opt in
        q ) quiet="-q";;
        * ) ;;
    esac
done
shift $((OPTIND-1))

case $# in
    2 ) _merge_ff "$1" "$2";;
    * ) _usage
esac

顺便说一下,如果有人发现这个脚本有任何问题,请评论! 这只是一个编写并忘记的任务,但我很乐意改进它。


你可能会对 https://dev59.com/um435IYBdhLWcg3w50f4#5148202 感兴趣,以便进行比较。 - Philip Oakley
从快速查看来看,它的核心与我的完全相同。但是我的代码没有硬编码到单个分支对上,它具有更多的错误处理,模拟了git-merge的输出,并且如果你在分支上调用它,它会做你想要的事情。 - Cascabel
我已经将你的文件放在我的\bin目录中了;-) 我只是忘记了它,四处寻找时看到了那个脚本,这激发了我的回忆!我的副本确实有这个链接和#或git branch-f localbranch remote/remotebranch来提醒我源和选项。我给你在另一个链接上的评论点了一个+1。 - Philip Oakley
3
+1也可以默认为"$branch@{u}"作为合并的提交记录,以获取上游分支(来自http://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html) - orip
感谢这个好脚本(8年后仍然有用)!我做了两个小改进:使用 git show-ref --abbrev ... 代替硬编码的 0:7,因为现代版本的 Git 默认使用更长的 SHA-1 前缀,所以它使输出更加一致;并且检查 $branch_orig_hash != $commit_orig_hash,以避免误导性的 reflog 条目和 diff 输出,如果实际上没有任何更改。 - VZ.

20

只有在合并是“快速向前”的情况下才能这样做。如果不是,则git需要将文件检出以便合并它们!

要仅针对“快进式”执行此操作:

git fetch <branch that would be pulled for branchB>
git update-ref -m "merge <commit>: Fast forward" refs/heads/<branch> <commit>

其中<commit>是所获取的提交,你想要快进到该提交。这基本上类似于使用git branch -f来移动分支,但它还将其记录在reflog中,就像您实际执行了合并操作一样。

请,请,千万不要对非快进操作使用此命令,否则您将重置分支到其他提交。(检查方法:查看git merge-base <branch> <commit>是否给出了该分支的SHA1值)


5
如果无法快进,有没有让它自动失败的方式? - gman
2
@gman 你可以使用 git merge-base --is-ancestor <A> <B> 命令。其中 "B" 是需要合并到 "A" 的内容。例如,A=master,B=develop,确保 develop 可以快进合并到 master。注意:如果不是快进合并,则返回0;如果是快进合并,则返回1。 - eddiemoya
1
在git-scm中没有记录,但在kernal.org上有。 https://www.kernel.org/pub/software/scm/git/docs/git-merge-base.html - eddiemoya
我制作了一个快速的要点概括(实际上没有测试,但应该基本符合要求)。https://gist.github.com/eddiemoya/ad4285b2d8a6bdabf432 ---- 另外,我已经准备好了大部分内容,我有一个脚本可以检查ff,如果没有让您首先重新定位所需的分支 - 然后进行ff合并 - 所有这些都不需要检出任何内容。 - eddiemoya

18
在您的情况下,您可以使用:

git fetch origin branchB:branchB

假设合并是快进的,git pull 将执行您想要的操作。如果分支无法更新,因为它需要进行非快进式合并,则会显示错误信息。

这种形式的 fetch 还具有一些更有用的选项:

git fetch <remote> <sourceBranch>:<destinationBranch>
注意 <remote> 可以是本地仓库,<sourceBranch> 可以是跟踪分支。因此,即使未检出本地分支,也可以更新本地分支而无需访问网络。请保留原文格式

目前,我的上游服务器访问需要通过缓慢的 VPN 进行,因此我会周期性地连接并使用 git fetch 命令更新所有远程分支,然后断开连接。然后,如果例如远程主分支发生了变更,我可以执行以下操作:

git fetch . remotes/origin/master:master

即使我当前有其他分支被检出,也可以安全地将我的本地主分支更新到最新版本,无需网络访问。


12

另一种方法是相当原始的,即重新创建分支:

git fetch remote
git branch -f localbranch remote/remotebranch

这会丢弃本地过期的分支并重新创建一个同名的分支,因此使用时请小心...


我刚才看到原始答案已经提到了 branch -f ... 不过,我仍然没有看到在最初描述的用例中将合并记录在 reflog 中的优势。 - kkoehne

7

您可以克隆存储库并在新存储库中进行合并。在同一文件系统上,这将硬链接而不是复制大部分数据。最后将结果拉回到原始存储库中。


6

输入 git-forward-merge:

git-forward-merge <source> <destination> 可以将源分支合并到目标分支,无需检出目标分支。

https://github.com/schuyler1d/git-forward-merge

仅适用于自动合并,如果存在冲突,则需要使用常规合并。


3
我认为这比使用“git fetch <remote> <source>:<destination>”更好,因为快进是合并操作而不是提取操作,而且写起来更容易。不过坏的一点是,它不在默认的Git中。 - Binarian

6

对于许多GitFlow用户来说,最有用的命令是:

git fetch origin master:master --update-head-ok
git fetch origin dev:dev --update-head-ok
--update-head-ok标志允许在devmaster分支上使用相同的命令。
.gitconfig中有一个方便的别名:
[alias]
    f=!git fetch origin master:master --update-head-ok && git fetch origin dev:dev --update-head-ok

注意,git fetch的“--update-head-ok”文档说:“这仅用于git pull与git fetch通信的内部使用,除非您正在实现自己的Porcelain,否则不应使用它。” - Lynax

6
问题中还有一些信息我不太清楚,为什么你需要检出本地的branchB,如果你没有意图在上面工作?在你的示例中,你只是想要在保持在branchA的情况下更新本地的branchB
最初我认为您这样做是为了获取origin/branchB,您可以使用git fetch来完成,因此这个答案就是基于此的。在您需要合并origin/branchB之前,没有必要拉取branchB,而且您始终可以在需要从origin/branchB合并时获取origin
如果您想要跟踪branchB在某个时间点的位置,您可以从最新的origin/branchB获取创建一个标签或其他分支。
因此,您应该永远只需要从origin/branchB合并:
git fetch
git merge origin/branchB

下次您需要处理 branchB 时:

git checkout branchB
git pull

在这个时候,您将会得到一个更新后的本地副本。虽然有一些不需要检出即可完成此操作的方法,但这些方法很少使用,在某些情况下可能不安全。已经有现成的答案来涵盖这个问题。

详细回答:

git pull 执行一个拉取+合并(fetch + merge)的操作。它大致相当于下面两个命令,其中 <remote> 通常为 origin(默认值),而远程跟踪分支以 <remote>/ 开头,后面跟着远程分支名称:

git fetch [<remote>]
git merge @{u}
@{u} 表示当前分支所配置的远程跟踪分支。如果 branchB 追踪 origin/branchB,那么从 branchB 输入 @{u} 就相当于输入 origin/branchB(详细信息请参见 git rev-parse --help)。
既然你已经与 origin/branchB 合并了,那么只要运行一下 git fetch(可以在任何分支中运行)来更新该远程跟踪分支即可。
但是需要注意的是,如果在将本地 branchB 拉取时存在任何合并操作,那么您应该在从 branchB 拉取后将 branchB 合并到 branchA 中(最终将更改推回到 orign/branchB,但只要它们是快进式的,它们就会保持不变)。
请记住,只有在切换到本地 branchB 并执行实际拉取操作之后,该分支才会被更新。然而,只要没有向此分支添加本地提交,它就会保持与远程分支的快进关系。

不,OP要求将上游的origin/branchB合并到本地的branchB,同时在本地检出branchA - Holger Böhnke
@HolgerBöhnke 你说得对,我看错了,但同时我也不能理解 OP 的问题。如果没有意图在 branchB 上工作,他为什么要 checkout 它呢?我感觉缺少了一些东西... 他不需要 checkout 本地的 branchB 来与 origin/branchB 合并,这是我认为问题所在。无论如何,我会澄清我的回答,谢谢。 - Thomas Guyot-Sionnest
你说得对。如果你不使用branchB,那么最好就不要将其检出。 该分支会自动跟踪为origin/branchB,一旦需要它,您将创建一个本地分支并将其检出。如果您将分支视为指向git提交图中特定提交的可移动指针,则可以更好地理解。 - Holger Böhnke
有完全合理的理由更新本地分支,而不想要检出。这可能不是最常见的情况,但它可能是适当的。 - CervEd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接