使用Git,显示仅存在于一个特定分支上的所有提交,而不是任何其他分支。

107
给定一个分支,我想要查看仅存在于该分支中的提交记录列表。在这个问题中,我们讨论了如何查看哪些提交记录在一个分支上,但不在另一个或多个指定的分支上。
这有些不同。我想要查看哪些提交记录在一个分支上,但不在任何其他分支上。
用例是在分支策略中,某些分支应仅合并,而永远不应直接提交。这将用于检查是否已在“仅合并”分支上直接提交了任何提交记录。
编辑:以下是设置虚拟git存储库进行测试的步骤:
git init
echo foo1 >> foo.txt
git add foo.txt
git commit -am "initial valid commit"
git checkout -b merge-only
echo bar >> bar.txt
git add bar.txt
git commit -am "bad commit directly on merge-only"
git checkout master
echo foo2 >> foo.txt 
git commit -am "2nd valid commit on master"
git checkout merge-only 
git merge master

只有那个带有消息“bad commit directly on merge-only”、直接在合并-only分支上进行的提交才应该显示出来。


1
这个问题假设所有合并的分支都当前可用于仓库中,一旦完全合并就不会被删除,并且可能永远不会使用快进方式合并。如果我漏掉了什么,请告诉我,但在我看来,这只适用于相对较小的允许合并的分支集,那为什么不直接使用 git log ^branch1 ^branch2 merge-only-branch 语法呢? - Karl Bielefeldt
1
git log ^branch1 ^branch2 merge-only-branch需要列出每个分支。可以通过巧妙地使用bash/grep(请参见我的答案)避免这种情况,但我希望git有一些内置的支持。您正确地假设所有合并自分支都是远程的(仅本地的分支对其他开发人员来说就像不存在一样)。使用--no-merges将省略任何已合并并且原始合并自分支已删除的提交,因此这意味着合并自分支会保留到它们被合并到非仅合并分支(即主分支)。 - jimmyorr
8个回答

88

我们刚刚找到了这个优雅的解决方案

git log --first-parent --no-merges
在您的示例中,当然还是会显示最初的提交。
这个答案并没有完全回答问题,因为最初的提交仍然会显示出来。另一方面,许多来到这里的人似乎已经找到了他们正在寻找的答案。

1
由于主分支上的初始提交仍然显示出来,这并没有回答问题。 - jimmyorr
9
这单独的操作并不能满足“仅存在于该分支的提交”条件-它显示了“合并专用”和“主线”分支都包含的“最初有效提交”。但是,如果在当前分支名后面加上^-前缀的已知当前分支源头的分支名,就可以解决一半的问题(不包括已合并的内容)。例如:git log --first-parent --no-merges merge-only ^master - Slipp D. Thompson
27
我不确定为什么这篇文章被投票得如此之高,它似乎与问题根本不相关。它肯定没有提供作者正在寻找的信息。 - Chris Rasys
2
这个答案可能不完美,但它很简单且肯定在某种程度上有效。我发现将分支名称添加到命令中非常有用 - 即过滤所有属于给定分支的提交记录:git log --first-parent --no-merges | grep <branch_name> - artm
1
谢谢。我认为这是最佳解决方案。 - Jakub Keller
显示剩余2条评论

33

这是我亲爱的朋友Redmumba提供的:

git log --no-merges origin/merge-only \
    --not $(git for-each-ref --format="%(refname)" refs/remotes/origin |
    grep -Fv refs/remotes/origin/merge-only)

...其中origin/merge-only是您的远程合并仅分支名称。如果在本地仓库中操作,请将refs/remotes/origin替换为refs/heads,并将远程分支名称origin/merge-only替换为本地分支名称merge-only,即:

git log --no-merges merge-only \
    --not $(git for-each-ref --format="%(refname)" refs/heads |
    grep -Fv refs/heads/merge-only)

2
我希望有人能够提供一个不使用grep,仅使用git的解决方案,但如果没有,这个解决方案看起来相当优雅。 - jimmyorr
1
使用 git for-each-ref 列出 origin 中的每个引用名称,并使用 grep -v 排除仅合并的分支。git log 命令带有一个 --not 选项,我们将所有引用(除了仅合并的分支)的列表传递给它。如果您对这个问题有更优雅的解决方案,请分享出来。 - jimmyorr
2
哦,我相信这是最优雅的答案。我只是认为它有点“冗长/复杂”,不够优雅。 :-) 我并不是要贬低您的方法,先生! - Chris K
1
git for-each-refs 命令中的尾随 /* 依赖于不匹配某些现有文件并且未设置 failglobnullglobbash 选项,其他 shell 可能不同)。您应该引用/转义星号或者只是省略尾随的 /*git for-each-ref 模式可以匹配“从开头到斜杠”)。也许使用 grep -Fv refs/remotes/origin/foo (refs/heads/foo) 更加严格地消除哪些引用。 - Chris Johnsen
4
如果你只想查看一个分支与另一个分支不同的提交记录,可以使用以下命令进行简化:git log --no-merges B1 --not B2。其中,B1 是你感兴趣的分支,B2 是你要将 B1 与其进行比较的分支。B1 和 B2 可以是本地或远程分支,因此你可以指定 git log --no-merges master --not origin/master,甚至指定两个远程分支。 - mr.b
显示剩余2条评论

26
git log origin/dev..HEAD

这将向您展示在您的分支中进行的所有提交。


3
@Prakash origin/branchName 将指向远程分支的最新提交,而 HEAD 将指向该分支中最后一次本地提交的 commitid。因此,在使用 git push 时,这种方法将不起作用。 - Bharat Pahalwani
你可以使用这个来比较另一个本地分支。--no-merges标志也可能对解决OP的原始问题有帮助。 - Paul Whipp

22

@Prakash的回答可行。只是为了更清晰明了......

git checkout feature-branch
git log master..HEAD

列出了feature-branch上的提交记录,但不包括上游分支(通常是您的主分支)。


11

3
虽然理论上这可以回答问题,但最好在此处包括答案的重要部分,并提供参考链接。 - Vladimir Panteleev
这实际上非常有帮助。请参见https://dev59.com/pmsz5IYBdhLWcg3w17jQ#7623339,其中提供了一个更详细的示例和一些相关答案。 - tripleee

8

试试这个:

git rev-list --all --not $(git rev-list --all ^branch)

基本上,git rev-list --all ^branch获取所有不在分支中的版本,然后您获取仓库中的所有版本并减去前面的列表,这是仅存在于分支的版本。
根据@Brian的评论:
从git rev-list的文档中获得: 列出可通过从给定提交点遵循父链接到达的提交 因此,像git rev-list A这样的命令将列出可以从A到达的提交,包括A。
有了这个想法,类似以下的命令 git rev-list --all ^A 将列出不可从A到达的提交
所以git rev-list --all ^branch将列出所有远离分支尖端的提交。 这将删除分支中的所有提交,换句话说就是仅存在于其他分支中的提交。
现在让我们来看看git rev-list --all --not $(git rev-list --all ^branch) 这将类似于git rev-list --all --not {仅存在于其他分支中的提交} 因此,我们要列出所有不能从只存在于其他分支中的所有提交到达的提交
这就是仅存在于分支中的提交集。 让我们举一个简单的例子:
             master

             |

A------------B

  \

   \

    C--------D--------E

                      |

                      branch

这里的目标是获取D和E,这些提交不在任何其他分支中。 git rev-list --all ^branch 只给出B。
现在,git rev-list --all --not B 是我们要用到的。也就是说,我们想要所有从B无法到达的提交,这种情况下是D和E。这正是我们想要的。
希望这样解释后能让命令正确运行。
评论后编辑:
git init
echo foo1 >> foo.txt
git add foo.txt
git commit -am "initial valid commit"
git checkout -b merge-only
echo bar >> bar.txt
git add bar.txt
git commit -am "bad commit directly on merge-only"
git checkout master
echo foo2 >> foo.txt 
git commit -am "2nd valid commit on master"

在完成以上步骤后,如果您执行 git rev-list --all --not $(git rev-list --all ^merge-only) 命令,您将得到所寻找的提交记录 - 即位于“仅合并”(merge-only)分支上的“坏提交”(bad commit)。
但是,在您执行最后一步 - git merge master 后,命令将不能产生预期的输出。因为现在已经没有不在“仅合并”(merge-only)分支里的提交,由于主分支中的一个额外的提交也已合并到了“仅合并”(merge-only)分支。所以,git rev-list --all ^branch 将返回空结果,因此 git rev-list -all --not $(git rev-list --all ^branch) 将会返回“仅合并”(merge-only)分支上所有提交记录。

1
嗯...不确定为什么,但这好像还不能正常工作。将命令的输出导管到 xargs -L 1 -t git branch -a --contains 会显示很多误报(实际上是在其他分支中的提交)。我尝试过使用和不使用 --no-merges。无论如何,感谢您的回答! - jimmyorr
在一个虚拟的 git 仓库里,就我所看到的情况来说,它似乎可以正常工作。 - manojlds
我已经添加了创建虚拟Git仓库的步骤,以帮助演示您的答案中存在的问题。 - jimmyorr
1
@manojlds "(所有修订版)-(不在分支中的所有修订版)=分支中的修订版。" 是的,这可以获取branch中的所有修订版,但是git rev-list branch也可以做到。你只是用一种更复杂(而且更慢)的方式编写了git rev-list branch。它无法回答问题,即如何找到所有在branch中的提交而不在任何其他分支中 - Brian Campbell
谢谢,这对我很有用,当我创建了一个新分支并且只想查看此分支上的新提交时。可以添加“--online”以获得更好的信息。 - Juan Antonio Tubío
显示剩余8条评论

5
这并不是一个确切的答案,但我需要访问格式和大量空间。我尝试描述我认为的两个最佳答案背后的理论:被接受的答案当前排名最高的答案。但实际上,它们回答了不同的问题。
在Git中,提交往往同时“存在于”多个分支上。事实上,这正是这个问题的主要内容。给定:
...--F--G--H   <-- master
         \
          I--J   <-- develop

在实际的Git哈希ID中,大写字母代表了它们所表示的提交。因此,在我们的git log输出中,我们经常会寻找仅包含提交H或仅包含提交I-J的情况。提交从G开始都在两个分支上,因此我们想要排除它们。
(请注意,在这样绘制的图形中,新的提交位于右侧。名称选择该行最右边的单个提交。每个提交都有一个父提交,即其左侧的提交:H的父提交是G,J的父提交是I。I的父提交再次是G。G的父提交是F,而F的父提交在此处未显示:它是“...”部分的一部分。)
对于这种特别简单的情况,我们可以使用以下命令:
git log master..develop    # note: two dots

查看I-J,或者:

git log develop..master    # note: two dots

仅查看H。冒号后的右侧名称告诉Git:“是的,这些提交”。冒号前的左侧名称告诉Git:“不,不是这些提交”。Git从结尾——从提交H或提交J开始,并向工作。有关此更多信息,请参见像Git一样思考

原问题的表述方式是要查找可以从一个特定名称到达的提交,但不能从同一类别中的任何其他名称到达。也就是说,如果我们有一个更复杂的图形:

               O--P   <-- name5
              /
             N   <-- name4
            /
...--F--G--H--I---M   <-- name1
         \       /
          J-----K   <-- name2
           \
            L   <-- name3

我们可以挑选其中一个名称,例如name4name3,然后问:哪些提交可以通过该名称找到,但不能通过任何其他名称找到?如果我们选择name3,答案是提交L。如果我们选择name4,则根本没有提交:名称name4指的提交是提交N,但是可以通过从name5开始向后工作来找到提交N
接受的答案使用远程跟踪名称而不是分支名称,并允许您指定一个名称(拼写为origin/merge-only)作为所选名称,并查看该命名空间中的所有其他名称。它还避免显示合并:如果我们将name1作为“感兴趣的名称”,并说显示可从name1到达但不能从任何其他名称到达的提交,我们将看到合并提交M以及常规提交I
最流行的答案与此大不相同。它完全是关于在提交图中遍历而不跟随合并的两个分支,以及不显示任何是合并的提交。例如,如果我们以 name1 开始,我们就不会显示 M(它是一个合并),但是假设合并 M 的第一个父提交是提交 I,我们甚至不会查看提交 JK。我们最终会显示提交 I,以及提交 HGF等,这些都不是合并提交,并且都可以通过从 M 开始向后工作,只访问每个合并提交的第一个父提交来到达。
最受欢迎的答案非常适合查看 master,当 master 旨在成为仅合并的分支时。如果所有的“真正工作”都是在侧分支上完成,然后将其合并到 master 中,我们将会有以下模式:
I---------M---------N   <-- master
 \       / \       /
  o--o--o   o--o--o

在这里,所有未命名为字母的o提交都是普通(非合并)提交,MN是合并提交。提交I是初始提交:最初的提交,也是唯一一个不应该是合并提交的主分支上的提交。如果git log --first-parent --no-merges master显示任何提交,而不是I,我们就会出现这种情况:

I---------M----*----N   <-- master
 \       / \       /
  o--o--o   o--o--o

我们希望看到直接在master上进行的提交*,而不是通过合并某些功能分支进行的提交。

简而言之,流行答案适用于查看master时,当master只能用于合并时,但对于其他情况则不太适用。 接受的答案适用于这些其他情况。

origin/master这样的远程跟踪名称是否为分支名称?

Git的某些部分说它们不是:

git checkout master
...
git status

说:在主分支上,但是:
git checkout origin/master
...
git status

说:在 origin/master 处分离了 HEAD。我更倾向于同意 git checkout / git switch:origin/master 不是一个分支名称,因为你不能“进入”它。

被接受的答案 使用远程跟踪名称 origin/* 作为“分支名称”:

git log --no-merges origin/merge-only \
    --not $(git for-each-ref --format="%(refname)" refs/remotes/origin |
    grep -Fv refs/remotes/origin/merge-only)

这个中间行调用了git for-each-ref,遍历了名为origin的远程跟踪名称。

这个解决方案之所以对原始问题有效,是因为我们在这里关心的是别人的分支名称,而不是我们自己的分支名称。但这意味着我们已经把分支定义成了与我们的分支名称不同的东西。这很好:只要你在做的时候意识到这一点就可以了。

git log遍历提交历史图谱的某些部分

我们真正寻找的是我所谓的daglet系列:参见我们所说的"分支"到底是什么?也就是说,我们正在寻找整个提交图谱的某个子集内的片段

每当我们让Git查看类似于master的分支名称、类似于v2.1的标签名称或类似于origin/master的远程跟踪名称时,我们倾向于要求Git告诉我们有关该提交以及我们可以从该提交到达的每个提交的信息:从那里开始,并向后工作。
在数学中,这被称为遍历图形。Git的提交图是一个有向无环图DAG,这种图特别适合遍历。当遍历这样的图时,将访问通过使用的路径可到达的每个图形顶点。Git图中的顶点是提交,边缘是弧-单向链接-从每个子级到每个父级。(这就是像Git一样思考的原因。弧的单向性意味着Git必须反向工作,从子级到父级。)
两个主要的Git命令用于图形遍历是git loggit rev-list。这些命令非常相似 - 实际上它们大部分都是由相同的源文件构建的 - 但它们的输出不同:git log生成人类可读的输出,而git rev-list生成供其他Git程序阅读的输出。1这两个命令都执行这种类型的图形遍历。
他们执行的图形遍历具体来说是:给定一些起始点提交集(可能只有一个提交,也可能有一堆哈希ID,也可能有一堆解析为哈希ID的名称),遍历图形,访问提交。特定的指令,例如--not或前缀^,或--ancestry-path,或--first-parent,以某种方式修改了图形遍历
当他们进行图形遍历时,他们会访问每个提交。但是他们只会打印一些选择的走过的提交。例如 --no-merges--before <date> 指令告诉图形遍历代码要打印哪些提交。
为了一个接一个地访问这些提交,这两个命令使用 优先队列。你运行 git loggit rev-list 并给它一些起始提交点。然后将这些提交放入优先队列中。例如,一个简单的例子:
git log master

将名称master转换为原始哈希ID,并将该哈希ID放入队列中。 或者:
git log master develop

将这两个名称转换为哈希ID,并假定这是两个不同的哈希ID,然后将两者都放入队列中。

此队列中提交的优先级由更多参数确定。例如,参数--author-date-order告诉git loggit rev-list使用作者时间戳,而不是提交者时间戳。默认情况下,使用提交者时间戳并选择最新日期的提交:具有最高数字日期的提交。因此,在master develop的情况下,假设它们解析为两个不同的提交,则Git将首先显示稍后到达的提交,因为它将位于队列的前面。

无论如何,现在修订步进代码在循环中运行:

  • 当队列中有提交时:
    • 移除第一个队列条目。
    • 决定是否打印此提交。例如,--no-merges: 如果是合并提交,则不打印任何内容; --before: 如果其日期不早于指定时间,则不打印任何内容。如果没有被抑制打印,则打印提交:对于git log,显示其日志;对于git rev-list,打印其哈希ID。
    • 将此提交的一些或全部提交放入队列中(只要它现在不在那里,并且尚未被访问过2)。正常默认值是放入所有父提交。使用--first-parent抑制除每个合并的第一个父提交之外的所有父提交。

(现在,git loggit rev-list都可以进行历史简化,无论是否进行了父重写,但我们将跳过这部分内容。)

对于简单的链式结构,例如在没有合并提交时从HEAD开始向后工作,队列始终在循环顶部有一个提交。有一个提交,所以我们将其弹出并打印它,并将其(唯一的)父提交放入队列中再次循环,我们反向跟踪链直到达到第一个提交,或者用户厌倦了git log输出并退出程序。在这种情况下,任何排序选项都无关紧要:只有一个提交需要显示。
当存在合并且我们同时跟随两个父提交 - 合并的两个“分支”,或者当您给git loggit rev-list提供多个起始提交时,排序选项就很重要了。
最后,考虑在提交指定符前面加上--not^的影响。这些有几种编写方式:
git log master --not develop

或者:

git log ^develop master

或者:

git log develop..master

所有的意思都是相同的。 --not 就像前缀 ^,只不过它适用于多个名称:

git log ^branch1 ^branch2 branch3

表示 不是分支1,不是分支2,是分支3; 但是:

git log --not branch1 branch2 branch3

意思是不是分支1,不是分支2,不是分支3,你必须使用第二个--not来关闭它:

git log --not branch1 branch2 --not branch3

这有点棘手。两个“not”指令通过异或组合在一起,所以如果你真的想要,你可以写成:

git log --not branch1 branch2 ^branch3

如果你想要表示不是branch1,不是branch2,是branch3,可以使用这个语法,同时进行混淆

所有这些语法都通过影响图形遍历来实现。当git loggit rev-list遍历图形时,它确保不把任何可从任何否定引用到达的提交放入优先级队列中。(实际上,它们也影响起始设置:否定的提交不能直接从命令行进入优先级队列,因此git log master ^master不显示任何内容。)

所有gitrevisions文档中描述的花式语法都利用了这一点,你可以通过简单调用git rev-parse来暴露它。例如:

$ git rev-parse origin/pu...origin/master     # note: three dots
b34789c0b0d3b137f0bb516b417bd8d75e0cb306
fc307aa3771ece59e174157510c6db6f0d4b40ec
^b34789c0b0d3b137f0bb516b417bd8d75e0cb306

三个点的语法表示从左侧或右侧可达的提交,但不包括从两边都可达的提交。在这种情况下,origin/master提交本身可从origin/pufc307aa37...)到达,因此origin/master哈希值会出现两次,一次是否定形式,但实际上,Git通过放置两个非否定的哈希ID和一个负数来实现三个点的语法, represented by the ^ prefix.

类似地:

$ git rev-parse master^^@
2c42fb76531f4565b5434e46102e6d85a0861738
2f0a093dd640e0dad0b261dae2427f2541b5426c

“^@”语法表示“给定提交的所有父级”,而“master^”本身——由分支名称“master”选择的提交的第一个父级——是合并提交,因此它有两个父级。这些是两个父级。
$ git rev-parse master^^!
0b07eecf6ed9334f09d6624732a4af2da03e38eb
^2c42fb76531f4565b5434e46102e6d85a0861738
^2f0a093dd640e0dad0b261dae2427f2541b5426c
< p > ^! 后缀表示 提交本身,但不包括其父提交 。在这种情况下, master^ 0b07eecf6 ... 。我们已经通过 ^@ 后缀看到了两个父提交; 这里它们再次出现,但这次是被否定的。


1许多Git程序会使用各种选项运行git rev-list并读取其输出,以了解要使用哪些提交和/或其他Git对象。

2由于图形是无环的,如果我们将约束条件在显示所有子项之前不显示父项添加到优先级中,则可以保证没有被访问过。 --date-order--author-date-order--topo-order添加此约束条件。默认排序顺序-没有名称-不包括此约束条件。如果提交时间戳出现问题-例如,某些提交是由时间错误的计算机“未来”制作的-这可能会导致看起来很奇怪的输出。


如果您已经走到这一步,您现在已经了解了很多关于git log的知识

总结:

  • git log用于显示选择的提交记录,同时遍历部分或全部图形。
  • 接受和当前排名最高答案中都有的--no-merges参数,可以抑制显示一些被遍历的提交记录。
  • 当前排名最高答案中的--first-parent参数,在遍历图形的过程中,抑制了对某些部分的遍历。
  • 作为接受的答案中使用的命令行参数前缀--not,从一开始就完全抑制了访问图形的某些部分。

通过使用这些功能,我们得到了两个不同问题的答案。


git log feature/add-login-page --not master 对我的需求很有帮助。感谢关于 --not 前缀的信息。 - marckassay
1
@marckassay:请注意,对于您这里特别简单的需求,git log master..feature/add-login-page就足够了。表达式A..B通常意味着:^A B^A表示“非A”,其中“非”部分仅适用于A而不适用于B(而--not A B则将“非”部分应用于两者!)。我说“通常”主要是因为git diff很特殊:git diff A..B只是意味着git diff A B - torek

2

另一种接受的答案变体,用于与master一起使用

git log origin/master --not $(git branch -a | grep -Fv master)

过滤掉发生在除了主分支之外的任何分支中的所有提交。


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