【问题标题】:Using Git, show all commits that exist *only* on one specific branch, and not *any* others使用 Git,显示所有*仅*存在于一个特定分支上的提交,而不是*任何*其他分支
【发布时间】:2011-08-08 21:15:57
【问题描述】:

给定一个分支,我想查看仅在该分支上存在的提交列表。在this question 中,我们讨论了查看哪些提交在一个分支上而不是一个或多个指定的其他分支上的方法。

这略有不同。我想看看哪些提交在一个分支上,而不是在任何其他分支上。

该用例采用分支策略,其中一些分支只能合并到,而不能直接提交。这将用于检查是否直接在“仅合并”分支上进行了任何提交。

编辑:以下是设置虚拟 git repo 进行测试的步骤:

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

只有直接在仅合并分支上进行的带有“错误提交直接在仅合并上”消息的提交才会显示。

【问题讨论】:

  • 这个问题假定所有合并的分支当前都在 repo 上可用,一旦完全合并就永远不会删除,并且可能永远不会与快进合并。如果我遗漏了什么,请告诉我,但在我看来,这仅适用于相对较小的一组允许的合并分支,那么为什么不直接使用 git log ^branch1 ^branch2 merge-only-branch 语法呢?
  • git log ^branch1 ^branch2 merge-only-branch 要求列出每个分支。巧妙地使用 bash/grep 可以避免这种情况(请参阅下面的答案),但我希望 git 对此有一些内置支持。您是正确的,它假定所有合并分支都是远程的(仅本地与其他开发人员不存在一样好)。使用--no-merges 会省略任何已合并的提交,然后删除其原始的merge-from 分支,因此这假设merge-from 分支一直保留,直到它们被合并到一个非merge-only-branch(即master )。

标签: git


【解决方案1】:

这不是一个真正的答案,但我需要访问格式和大量空间。我将尝试描述我认为两个最佳答案背后的理论:the accepted one 和the (at least currently) top-ranked one。但事实上,他们回答了不同的问题。

Git 中的提交通常一次“开启”多个分支。事实上,这就是问题的大部分内容。给定:

...--F--G--H   <-- master
         \
          I--J   <-- develop

大写字母代表实际的 Git 哈希 ID,我们经常在 @987654334 中寻找 only commit H 或 only commits I-J @ 输出。通过G 向上提交在两个 分支上,所以我们想排除它们。

(请注意,在这样绘制的图表中,较新的提交位于右侧。names 选择该行上最右侧的单个提交。每个提交都有一个父提交,即他们左边的提交:H的父级是G,J的父级​​是I。I的父级是G。G的父级是@987654343 @ 和 F 有一个父级,这里根本没有显示:它是 ... 部分的一部分。)

对于这种特别简单的情况,我们可以使用:

git log master..develop    # note: two dots

查看I-J,或:

git log develop..master    # note: two dots

仅查看H。右侧的名称,在两个点之后,告诉 Git:是的,这些提交。两个点之前的左侧名称告诉 Git:不,不是这些提交。 Git 从 end 开始——在提交 H 或提交 J——并且工作向后。有关(更多)关于此的更多信息,请参阅Think Like (a) Git。

原始问题的表述方式是,希望找到可从一个特定名称访问的提交,但不能从同一通用类别中的任何其他名称访问.也就是说,如果我们有一个更复杂的图:

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

我们可以选择其中一个名称,例如name4 或name3,然后问:可以通过该名称找到哪些提交,但不能通过其他任何名称找到?如果我们选择name3,答案是提交L。如果我们选择name4,答案是根本没有提交:name4 命名的提交是提交 N 但提交 N 可以通过从 name5 开始并向后工作来找到。

接受的答案适用于远程跟踪名称,而不是分支名称,并允许您指定一个(拼写为 origin/merge-only)作为选定名称并查看该命名空间中的所有其他名称。它还避免了显示合并:如果我们选择name1 作为“有趣的名称”,并说显示可以从name1 访问但不能访问任何其他名称的提交,我们将看到合并提交M 以及定期提交 I。

最受欢迎的答案完全不同。这完全是关于遍历提交图不跟随合并的两条腿,并且不显示任何是的提交em> 合并。例如,如果我们以name1 开头,我们不会显示M(这是一个合并),但假设合并M 的first 父级是提交I,我们就赢了甚至不看提交J 和K。我们最终会显示提交I,并且还会提交H、G、F 等等——这些都不是合并提交,并且都可以通过从M 开始并向后工作来访问,仅访问每个合并提交的 first 父级。

最受欢迎的答案非常适合,例如,当 master 打算成为仅合并分支时,查看 master。如果所有“实际工作”都在随后合并到master 的分支上完成,我们将有这样的模式:

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

其中所有未命名的o 提交都是普通(非合并)提交,M 和N 是合并提交。提交I 是初始提交:有史以来第一次提交,也是唯一一个应该在master 上而不是合并提交的提交。如果git log --first-parent --no-merges master 显示任何提交除了 I,我们有这样的情况:

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

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

简而言之,当 master 仅用于合并时,流行的答案非常适合查看 master,但不适用于其他情况。接受的答案适用于这些其他情况。

远程跟踪名称是否类似于origin/master branch 名称?

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

git checkout master
...
git status

说on branch master,但是:

git checkout origin/master
...
git status

说HEAD detached at origin/master。我更愿意同意git checkout / git switch:origin/master 不是分支名称,因为你无法“使用”它。

The accepted answer 使用远程跟踪名称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 的远程的远程跟踪名称。

这是对原始问题的一个很好的解决方案的原因是我们在这里对其他人的分支名称感兴趣,而不是我们的分支名称。但这意味着我们将 branch 定义为 我们的分支名称 以外的东西。没关系:当你这样做时,请注意你正在这样做。

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

我们在这里真正要搜索的是我称之为 daglets 的系列: 参见 What exactly do we mean by "branch"? 也就是说,我们正在整个提交的某个子集中寻找 片段图表。

每当我们让 Git 查看像 master 这样的分支名称、像 v2.1 这样的标签名称或像 origin/master 这样的远程跟踪名称时,我们倾向于让 Git 告诉我们关于该提交的信息 和我们可以从该提交中获得的每个提交:从那里开始,然后向后工作。

在数学中,这被称为walking a graph。 Git 的提交图是有向无环图或DAG,这种图特别适合行走。在遍历这样的图时,将通过所使用的路径访问每个 可到达 的图顶点。 Git 图中的顶点是提交,边是弧线——单向链接——从每个子节点到每个父节点。 (这就是Think Like (a) Git 的用武之地。弧的单向性质意味着 Git 必须从子级到父级向后工作。)

图形遍历的两个主要 Git 命令是 git log 和 git rev-list。这些命令非常相似——实际上它们大多是从相同的源文件构建的——但它们的输出不同:git log 产生供人类阅读的输出,而git rev-list 产生供其他 Git 程序阅读的输出。1 两个命令都执行这种图形遍历。

他们所做的图遍历具体是:给定一些起点提交(可能只是一个提交,可能是一堆哈希 ID,可能是一堆解析为哈希 ID 的名称), 遍历图表,访问提交。特定指令(例如 --not 或前缀 ^、--ancestry-path 或 --first-parent)会以某种方式修改 graph walk。

当他们进行图遍历时,他们会访问每个提交。但他们只 print 一些选定的 subset 步行提交。诸如--no-merges 或--before &lt;date&gt; 之类的指令告诉图形遍历代码哪个提交到print。

为了进行这种访问,一次提交一个,这两个命令使用priority queue。你运行 git log 或 git rev-list 并给它一些起点提交。他们将这些提交放入优先级队列。例如,一个简单的:

git log master

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

git log master develop

将两个名称都转换为哈希 ID,并且假设它们是两个不同的哈希 ID,将两者都放入队列中。

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

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

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

(此时git log 和git rev-list 都可以在有或没有父级重写的情况下进行历史简化,但我们将在此处跳过。 )

对于一个简单的链,例如从HEAD 开始并向后工作,当没有合并提交时,队列中始终在循环顶部有一个提交。有一个提交,因此我们将其弹出并打印并将其(单个)父级放入队列并再次循环,然后我们沿着链向后直到我们到达第一个提交,或者用户厌倦了git log输出并退出程序。在这种情况下,任何排序选项都无关紧要:只显示一个提交。

当发生合并并且我们遵循两个父级(合并的两条“腿”)时,或者当您给予 git log 或 git rev-list 多个起始提交时,排序选项很重要。

最后,考虑--not 或^ 在提交说明符前的效果。这些有几种写法:

git log master --not develop

或:

git log ^develop master

或:

git log develop..master

所有的意思都是一样的。 --not 与前缀 ^ 类似,但它适用于多个名称:

git log ^branch1 ^branch2 branch3

表示不是branch1,不是branch2,是branch3;但是:

git log --not branch1 branch2 branch3

表示不是branch1,不是branch2,不是branch3,你必须使用第二个--not来关闭它:

git log --not branch1 branch2 --not branch3

这有点尴尬。两个“not”指令通过异或组合,所以如果你真的想要,你可以这样写:

git log --not branch1 branch2 ^branch3

表示不是branch1,不是branch2,是branch3,如果你想obfuscate。

所有这些都是通过影响图形遍历来起作用的。当git log 或git rev-list 遍历图表时,它确保不 将任何可从任何否定 引用访问的提交放入优先级队列。 (事实上​​,它们也会影响启动设置:否定的提交不能直接从命令行进入优先级队列,因此git log master ^master 什么也不会显示。)

所有在the gitrevisions documentation 中描述的花哨语法都利用了这一点,您可以通过对git rev-parse 的简单调用来公开这一点。例如:

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

三点语法意味着提交可从左侧或右侧访问,但不包括可从两侧访问的提交。在这种情况下,origin/master 提交 b34789c0b 本身可以从 origin/pu (fc307aa37...) 访问,因此 origin/master 哈希出现两次,一次带有否定,但实际上 Git 通过以下方式实现三点语法放入两个正引用(两个非否定哈希 ID)和一个负引用,由 ^ 前缀表示。

类似:

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

^@ 语法意味着给定提交的所有父级,而master^ 本身——由分支名称master 选择的提交的第一个父级——是一个合并提交,所以它有两个父母。这是两个父母。并且:

$ git rev-parse master^^!
0b07eecf6ed9334f09d6624732a4af2da03e38eb
^2c42fb76531f4565b5434e46102e6d85a0861738
^2f0a093dd640e0dad0b261dae2427f2541b5426c

^! 后缀表示提交本身,但没有其父项。在这种情况下,master^ 是 0b07eecf6...。我们已经看到父母双方都带有^@ 后缀;他们又来了,但这次被否定了。


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

2因为图是非循环的,如果我们添加约束never show an parent before shows all,可以保证没有任何访问过其孩子的优先级。 --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:请注意,对于您在这里特别简单的需求,git log master..feature/add-login-page 就足够了。表达式A..B 通常表示:^A B; ^A 表示“not-A”,“not”部分仅适用于 A 而不适用于 B(而 --not A B 的“not”部分适用于两者!)。我说通常主要是因为git diff很特别:git diff A..B只是意味着git diff A B。
【解决方案2】:

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

git log --first-parent --no-merges

当然,在您的示例中,初始提交仍然显示。

这个答案并没有完全回答这个问题,因为初始提交仍然出现。另一方面,许多来到这里的人似乎找到了他们正在寻找的答案。

【讨论】:

  • 由于 master 上的初始提交仍然显示,这并不能回答问题。
  • 仅此一项不满足 “仅存在于该分支上的提交” 条件——它显示了 initial valid commit,它是 merge-only 和 @ 的一部分987654324@分支机构。但是,如果努力将当前分支名称放在末尾,然后加上知道当前分支源自的 ^ 前缀的分支名称,它解决了一半的问题(不包括合并的东西)。例如:git log --first-parent --no-merges merge-only ^master
  • 我不知道为什么这么多赞成,这似乎与问题无关。它当然没有提供发布者正在寻找的信息。
  • 这个答案可能并不完美。但这很简单,并且在某种程度上肯定有效。我发现添加分支名称很有用 - 即过滤所有提交属于给定分支:git log --first-parent --no-merges | grep &lt;branch_name&gt;
  • 谢谢。最佳解决方案 imo。
【解决方案3】:

@Prakash 答案有效。只是为了清楚...

git checkout feature-branch
git log master..HEAD

列出功能分支上的提交,但不列出上游分支(通常是您的主分支)。

【讨论】:

    【解决方案4】:
    git log origin/dev..HEAD
    

    这将向您显示在您的分支中所做的所有提交。

    【讨论】:

    • @Prakash origin/branchName 将指向远程分支的负责人,HEAD 将指向该分支中最后一次本地提交的 commitid。因此,当您使用 git push 时,这将不起作用。
    • 您可以使用它来比较另一个本地分支。 --no-merges 标志也可以方便地解决 OP 的原始问题。
    【解决方案5】:

    感谢我亲爱的朋友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 是您的远程仅合并分支名称。如果在本地 git repo 上工作,请将 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)
    

    【讨论】:

    • 我希望其他人可以仅使用 git 提供无 grep 的解决方案,但如果没有,这感觉非常优雅。
    • 是的,优雅。使用 git for-each-ref 列出 origin 中的每个 ref 名称,使用 grep -v 省略仅合并分支。 git log 采用--not 选项,我们将所有参考列表传递给该选项(仅合并分支除外)。如果你对这个问题有更优雅的答案,让我们来听听。
    • 哦,我相信这是最优雅的答案。我只是认为真正的优雅有点“罗嗦/复杂”。 :-) 不是有意贬低您的做法,先生!
    • git for-each-refs 命令中的尾随/* 依赖于不匹配某些现有文件并且没有设置failglob 或nullglob(bash 选项,其他shell各不相同)。您应该引用/转义星号,或者将尾随的/* 关闭(git for-each-ref 模式可以匹配“从开头到斜线”)。也许使用grep -Fv refs/remotes/origin/foo (refs/heads/foo) 更严格地确定哪些 refs 被淘汰。
    • 如果您只想查看一个分支而不是另一个分支中的提交,可以简化:git log --no-merges B1 --not B2,其中 B1 是您感兴趣的分支,B2 是您想要的分支比较 B1。 B1和B2都可以是本地或远程分支,因此您可以指定git log --no-merges master --not origin/master,甚至可以指定两个远程分支。
    【解决方案6】:

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

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

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

    【讨论】:

      【解决方案7】:

      也许这会有所帮助:

      git show-branch

      【讨论】:

      【解决方案8】:

      试试这个:

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

      基本上git rev-list --all ^branch 获取所有不在分支中的修订,然后是你在 repo 中的所有修订并减去之前的列表,即分支中的修订仅。

      @Brian 的 cmets 之后:

      来自 git rev-list 的文档:

      List commits that are reachable by following the parent links from the given commit(s)

      因此,像 git rev-list A 这样的命令(其中 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 {commits only in other branches}

      所以我们要列出无法从all commits only in other branches 访问的all

      哪些是仅在分支中的提交集。举个简单的例子:

                   master
      
                   |
      
      A------------B
      
        \
      
         \
      
          C--------D--------E
      
                            |
      
                            branch
      

      这里的目标是获取 D 和 E,提交不在任何其他分支中。

      git rev-list --all ^branch只给B

      现在,git rev-list --all --not B 是我们的目标。这也是git rev-list -all ^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),您将获得您正在寻找的提交 - "bad commit directly on merge-only"。

      但是,一旦您在步骤git merge master 中执行了最后一步,该命令将不会给出预期的输出。因为到目前为止,没有任何提交不存在于仅合并中,因为 master 中的一个额外提交也已合并为仅合并。所以git rev-list --all ^branch 给出了空结果,因此git rev-list -all --not $(git rev-list --all ^branch) 将给出所有只合并的提交。

      【讨论】:

      • 嗯...不知道为什么,但这并不完全有效。将命令的输出传送到xargs -L 1 -t git branch -a --contains 会显示大量误报(实际上是在其他分支上的提交)。我试过有无--no-merges。感谢您的回答!
      • 就我在虚拟 git repo 中看到的而言,似乎对我来说工作正常。
      • 我添加了创建虚拟 git 存储库的步骤,以帮助证明您的答案存在问题。
      • 啊,哎呀。在我一直考虑之前就对此表示赞同。 git rev-list --all ^branch 会给你所有不在branch 中的提交。然后从branch 中的 are 列表中减去它;但根据定义,所有不在branch 中的提交都不在branch 中,所以你没有减去任何东西。 jimmyorr 正在寻找的是branch 但不是master 或任何其他分支中的提交。您不想减去不在branch 中的提交;您想减去任何其他分支中的提交。
      • @manojlds “(所有修订)-(所有不在分支中的修订)= 分支中的修订。”是的,这可以在branch 中获得所有修订,但git rev-list branch 也是如此。您只是以更复杂(且更慢)的方式编写git rev-list branch。无法回答这个问题,即如何在branch中找到所有提交不在任何其他分支中。
      猜你喜欢
      • 2010-12-15
      • 2014-05-29
      • 2015-04-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-12
      • 2012-05-02
      相关资源
      最近更新 更多