本文介绍了祖先路径如何与git log一起使用?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经阅读了 git日志文档,但是我仍然很难理解--ancestry-path选项可以.我看到了不同的方式来调用git log:

$ git log origin/master..HEAD
$ git log --ancestry-path origin/master..HEAD

在第一个命令中,我得到了一个在HEAD上但不在origin/master上的提交列表,基本上,这向我显示了分支中未合并的内容.

在第二个命令中,我什么也没得到.如果我将其更改为3个点(...),则会显示某物,但是我不确定如何理解它.基本上,--ancestry-path的添加有何不同? 简化到底是什么?

解决方案

Matthieu Moy的答案是正确的,但可能无济于事非常感谢您,如果您还没有接触过必要的图论.

DAGs

首先,让我们快速查看由 D 取代的 A 循环的 G raps或DAG. DAG只是一个图(因此为g),即节点和它们之间的连接的集合(这些工作就像是铁路上的火车站,例如,车站是节点),它们是有向的"( d:火车只运行一种方式),并且其中没有循环(a).

线性链和树结构是有效的DAG:

o <- o <- o

或:

       o <- o
      /
o <- o
      \   o
       \ /
        o
         \
          o <- o

(想象对角线连接具有箭头,以便根据需要指向左上或左下).

但是,非树图可以具有合并回去的节点(这些是git的合并):

       o <- o
      /      \
o <- o        \
      \   o    \
       \ /      \
        o        o
         \      /
          o <- o

或:

     o--o
    /    \
o--o      o--o
    \    /
     o--o

(我只是在这里进一步压缩表示法,节点通常仍指向左).

接下来,git的..表示法并不代表大多数人通常首先认为的含义.特别是,让我们再次看一下该图,添加另一个节点,并使用一些单个字母标记特定的节点:

     o---o
    /     \
A--o       \
    \   B   \
     \ /     \
      o       C--D
       \     /
        o---o

然后,让我们做更多的事情,不要再将其视为git log了,而是将其视为选择具有祖先的修订版本"的更一般的情况.

根据祖先选择修订(提交)

如果选择版本A,我们将只得到版本A,因为它没有祖先(左边没有内容).

如果选择修订版B,则会得到以下图表:

A--o
    \   B
     \ /
      o

这是因为select-with-ancestry的意思是接受我确定的提交,并通过沿箭头的方向返回,我可以得到的所有提交."这里的结果有点有趣,但不是非常有趣,因为没有合并,并且跟随箭头的方向使我们从提交到B到返回到A的四个提交组成一个线性链. /p>

通过祖先选择C或D可以使我们走得更远.让我们看看D会得到什么:

     o---o
    /     \
A--o       \
    \       \
     \       \
      o       C--D
       \     /
        o---o

实际上,这是除 之外的所有所有内容B的提交.为什么我们没有得到B?因为箭头都指向左:我们得到D,它指向C,它指向两个未签名的提交.这两个点向左,依此类推,但是当我们在B的左下方单击节点时,不允许我们向右箭头方向前进,因此我们无法到达B. /p>

两点符号

现在,git中的两点符号实际上只是集合减法的简写语法.也就是说,例如,如果我们编写B..D,则意味着:祖先,然后选择具有祖先的B,然后在排除(减去)B选择中的所有提交之后,从D选择中提供一组提交."

先后选择D会得到整个图,除了对于B提交.减去B选择将删除A,我们之前绘制的两个o节点以及B. B不在集合中时,如何删除?容易:我们只是假装将其删除并说我们完成了!也就是说,集合减法只会打扰去除集合中实际的东西.

因此B..D的结果是此图:

     o---o
          \
           \
            \
             \
              C--D
             /
        o---o

三点符号

三点符号是不同的.在简单的分支y图(甚至是直树)中更有用.这次让我们从树状图开始,看看两点和三点符号.这是我们的树状图,其中有一些用于节点的单个字母名称:

     o--I
    /
G--H
    \   J
     \ /
      K
       \
        o--L

这次我添加了额外的字母,因为我们需要讨论提交联合"的某些地方,尤其是在节点H和K上.

使用两点符号,L..I会得到什么?要找到答案,请从节点I开始并向后工作.即使您也向上或向下移动,也必须始终向左移动.这些是选定的提交.然后,从节点L开始并向后工作,找到要进行 un -select的节点;如果您遇到任何较早选择的对象,请将其扔掉. (将最终列表留在练习中,尽管我将答案放在脚注中.)

现在让我们看看实际中的三点符号.它的操作有点复杂,因为它必须在图中的两个分支之间找到合并基础.合并库有一个正式的定义,,但就我们的目的而言,它只是:在向后跟随图形时,我们在某个提交时相遇的点."

例如,在这种情况下,如果我们要求提供L...I或I...L两者都产生相同的结果,则git会从 提交而不是.也就是说,它不包括合并基础和所有较早的提交,但将提交保持在该点之外.

L和I(或I和L)的合并基础是提交H,因此我们得到的内容是H之后的内容,而不是H本身,因此我们无法达到I或L中的节点J,因为它不在他们的祖先中.因此,I...L或L...I的结果为:

     o--I




      K
       \
        o--L

(请注意,由于我们放弃了节点H,因此这些历史不会合并在一起.)

--ancestry-path

现在,所有这些都是普通的选择操作.未使用--ancestry-path进行任何修改. git log和git rev-list的文档-这两个命令几乎相同,除了它们的输出格式外-以这种方式描述--ancestry-path:

我们在此处根据提交DAG定义祖先:如果第二次提交的箭头指向第一个提交,则第一次提交是秒的直接祖先.以及间接祖先(如果第二个通过第二个提交链指向第一个),则指向间接祖先. (出于选择目的,一次提交也被视为其自身的祖先.)

后代(有时也称为子代)的定义与此类似,但与图中的箭头相反.提交是另一个提交的子代(或子代),如果它们之间有路径.

请注意,--ancestry-path的描述谈论的是使用两点符号,而不是三点符号,这可能是因为三点符号的实现在内部有点怪异.如前所述,B...D排除(就像前导^一样)两次提交的合并基础(或多个(如果有多个)基础),因此合并基础为扮演必须是孩子"角色的角色.尽管我不确定--ancestry-path在现实世界"示例中的用处,但我将提及它是如何工作的.

实际示例

这实际上意味着什么?好吧,这取决于您提供的参数以及实际的提交DAG.让我们再次看看时髦的循环图:

     o---o
    /     \
A--o       \
    \   B   \
     \ /     \
      o       C--D
       \     /
        o---o

假设我们在这里没有 --ancestry-path要求B..D.这意味着我们将提交D及其祖先,但是将B及其祖先排除在外,就像我们之前看到的那样.现在,添加--ancestry-path.我们以前拥有的所有东西都是D的祖先,但这仍然是事实,但是这个新标记表示我们还必须 扔掉不是> 子代的提交.

节点B有几个孩子?好吧,没有!因此,我们必须放弃每次提交,为我们提供一个完全空列表.


如果我们要求B...D,而没有特殊的--ancestry-path标记怎么办?这使我们可以从D B到达所有内容,但不包括从D B都可以到达的所有内容:

     o---o
          \
           \
        B   \
             \
              C--D
             /
        o---o

这与B..D相同,除了我们也获得了节点B.

[注意:以下将--ancestry-path与B...D混合使用的部分在2016年4月至2017年2月之间错误了将近一年的时间.已经固定为注意,必须是孩子"部分始于合并基数,而不是从B...D标记的左侧开始.]

假设我们在此处添加--ancestry-path.我们以与B...D相同但没有--ancestry-path的图形开始 start ,然后放弃不是合并基础的子项的项目.合并基础是B左侧的o.顶行o提交不是此节点的子级,因此将其丢弃.再次,与祖先一样,我们将节点视为自己的子节点,因此我们保留该节点本身,从而得到部分结果:

        B
       /
      o       C--D
       \     /
        o---o

但是,当我们(或--ancestry-path正在)丢弃此合并基础节点的子级时,合并基础节点本身(位于B的左下方)不在B...D图中第一名.因此,最终结果(实际在Git 2.10.1中进行了测试)是:

        B

              C--D
             /
        o---o

(再次,我不太确定这在实践中是否有用.同样,起始图是B...D的图: 提交可访问的所有内容,减去< both commits:这是通过从每个合并基数(如果有两个或多个)开始丢弃来进行的,子检查代码也处理提交列表,它保留合并基础中 any 的子级,如果有多个合并基础,请参见 revision.c .)

因此,它取决于图形选择器

X..Y或X...Y的最终操作(带有或不带有--ancestry-path)取决于提交图.要对其进行预测,必须绘制图形. (可以使用git log --graph,也许可以使用--oneline --decorate --all,或者使用可以为您绘制图形的查看器.)


git diff中有一个例外,它对X..Y和X...Y做自己的特殊处理.不使用git diff时,应忽略其特殊处理.

我们从I和o左侧开始,还有H和G.然后,当我们从L开始工作时,我们会丢失H和G,因此结果就是o--I.

正式定义是合并基数是 L 最高 C 普遍 A 祖先,或者图中给定节点的LCA.在某些图表中,可能有多个LCA.对于Git,这些都是合并基础,X...Y将排除所有这些合并基础.

为我绘制的图形运行git rev-parse B...D是有趣/有启发性的.这些提交哈希在这里不仅取决于图形本身和提交,还取决于进行提交的时间戳,因此,如果您构建相同的图,则会得到不同的哈希,但是这是我在修改答案以修正--ancestry-path与B...D交互的描述时得到的:

$ git rev-parse B...D
3f0490d4996aecc6a17419f9cf5a4ab420c34cc2
7f0b666b4098282301a9f95e056a646483c2e5fc
^843eaf75d78520f9a569da35d4e561a036a7f107

,但是我们可以看到,它们是D,B和合并基础,并使用更多命令:

$ git rev-parse B     # this produces the middle hash
7f0b666b4098282301a9f95e056a646483c2e5fc

和:

$ git rev-parse D     # this produces the first hash
3f0490d4996aecc6a17419f9cf5a4ab420c34cc2

和:

$ git merge-base B D  # this produces the last, negated, hash
843eaf75d78520f9a569da35d4e561a036a7f107

具有多个合并基数的图形的确会出现,但是它们的构建有些困难-简单的方法是在运行git checkout br1; git merge br2; git checkout br2; git merge br1的十字交叉"合并中.如果遇到这种情况并运行git rev-list,您将看到多个取反的哈希,每个合并基数一个.运行git merge-base --all,您将看到相同的合并基础集.

I've read the git log documentation, but I still find it very difficult to understand what the --ancestry-path option does. I see different ways to invoke git log:

$ git log origin/master..HEAD
$ git log --ancestry-path origin/master..HEAD

In the first command, I get a list of commits that are on HEAD but not on origin/master, basically this shows me what is on my branch that isn't merged.

In the second command, I get nothing. If I change to 3 dots (...) it shows me something, but I'm not sure how to make sense of it. Basically, how is the addition of --ancestry-path any different? What exactly does it simplify?

解决方案

Matthieu Moy's answer is correct but may not help you very much, if you haven't been exposed to the necessary graph theory.

DAGs

First, let's take a quick look at Directed Acyclic Graphs or DAGs. A DAG is just a graph (hence the g), i.e., a collection of nodes and connections between them—these work like train stations on rail lines, for instance, where the stations are the nodes—that is "directed" (the d: trains only run one way) and have no loops in them (the a).

Linear chains and tree structures are valid DAGs:

o <- o <- o

or:

       o <- o
      /
o <- o
      \   o
       \ /
        o
         \
          o <- o

(imagine the diagonal connections having arrow heads so that they point up-and-left or down-and-left, as needed).

However, non-tree graphs can have nodes that merge back (these are git's merges):

       o <- o
      /      \
o <- o        \
      \   o    \
       \ /      \
        o        o
         \      /
          o <- o

or:

     o--o
    /    \
o--o      o--o
    \    /
     o--o

(I'm just compressing the notation further here, nodes still generally point leftward).

Next, git's .. notation does not mean what most people usually first think it means. In particular, let's take a look at this graph again, add another node, and use some single letters to mark particular nodes:

     o---o
    /     \
A--o       \
    \   B   \
     \ /     \
      o       C--D
       \     /
        o---o

And, let's do one more thing, and stop thinking about this as just git log but rather the more general case of "selecting revisions with ancestry".

Selecting revisions (commits), with ancestry

If we select revision A, we get just revision A, because it has no ancestors (nothing to the left of it).

If we select revision B we get this piece of the graph:

A--o
    \   B
     \ /
      o

This is because select-with-ancestry means "Take the commit I identify, and all the commits I can get to by following the arrows back out of it." Here the result is somewhat interesting, but not very interesting since there are no merges and following the arrows nets us a linear chain of four commits, starting from B and going back to A.

Selecting either C or D with ancestry, though, gets us much further. Let's see what we get with D:

     o---o
    /     \
A--o       \
    \       \
     \       \
      o       C--D
       \     /
        o---o

This is, in fact, everything except commit B. Why didn't we get B? Because the arrows all point leftward: we get D, which points to C, which points to two un-lettered commits; those two point left, and so on, but when we hit the node just left-and-down of B, we aren't allowed to go rightward, against the arrow, so we can't reach B.

Two-dot notation

Now, the two-dot notation in git is really just shorthand syntax for set subtraction. That is, if we write B..D for instance, it means: "Select D with ancestry, and then select B with ancestry, and then give me the set of commits from the D selection after excluding (subtracting away) all commits from the B selection."

Selecting D with ancestry gets the entire graph except for the B commit. Subtracting away the B selection removes A, the two o nodes we drew earlier, and B. How can we remove B when it's not in the set? Easy: we just pretend to remove it and say we're done! That is, set subtraction only bothers to remove things that are actually in the set.

The result for B..D is therefore this graph:

     o---o
          \
           \
            \
             \
              C--D
             /
        o---o

Three-dot notation

The three-dot notation is different. It's more useful in a simple branch-y graph, perhaps even a straight tree. Let's start with the tree-like graph this time and look at both two- and three-dot notation. Here's our tree-like graph, with some single letter names for nodes put in:

     o--I
    /
G--H
    \   J
     \ /
      K
       \
        o--L

This time I've added extra letters because we'll need to talk about some of the places the commits "join up", in particular at nodes H and K.

Using two-dot notation, what do we get for L..I? To find the answer, start at node I and work backwards. You must always move leftward, even if you also go up or down. These are the commits that are selected. Then, start at node L and work backwards, finding the nodes to un-select; if you come across any earlier selected ones, toss them out. (Making the final list is left as an exercise, though I'll put the answer in as a footnote.)

Now let's see the three-dot notation in action. What it does is a bit complicated, because it must find the merge base between two branches in the graph. The merge base has a formal definition, but for our purposes it's just: "The point where, when following the graph backwards, we meet up at some commit."

In this case, for instance, if we ask for L...I or I...L—both produce the same result—git finds all commits that are reachable from either commit, but not from both. That is, it excludes the merge base and all earlier commits, but keeps the commits beyond that point.

The merge base of L and I (or I and L) is commit H, so we get things after H, but not H itself, and we cannot reach node J from either I or L since it's not in their ancestry. Hence, the result for I...L or L...I is:

     o--I




      K
       \
        o--L

(Note that these histories do not join up, since we tossed out node H.)

--ancestry-path

Now, all these are ordinary selection operations. None have been modified with --ancestry-path. The documentation for git log and git rev-list—these two are almost the same command, except for their output format—describes --ancestry-path this way:

We define ancestors here in terms of the commit DAG: a first commit is a direct ancestor of a second if the second has an arrow pointing back at the first, and an indirect ancestor if the second points back at the first through some chain of commits. (For selection purposes a commit is also considered an ancestor of itself.)

Descendants (also sometimes called children) are defined similarly, but by going against the arrows in the graph. A commit is a child (or descendant) of another commit if there's a path between them.

Note that the description of the --ancestry-path talks about using the two-dot notation, not the three-dot notation, probably because the implementation of the three-dot notation is a little bit weird inside. As noted earlier, B...D excludes (as if with leading ^) the merge base (or bases, if there is/are more than one) of the two commits, so the merge base is the one that play the "must be child-of" role. I'll mention how --ancestry-path works with this, though I'm not sure how useful it is in "real world" examples.

Practical examples

What does this mean in practice? Well, it depends on the arguments you give, and the actual commit DAG. Let's look at the funky loopy graph again:

     o---o
    /     \
A--o       \
    \   B   \
     \ /     \
      o       C--D
       \     /
        o---o

Suppose we ask for B..D here without --ancestry-path. This means we take commit D and its ancestors, but exclude B and its ancestors, just as we saw before. Now let's add --ancestry-path. Everything we had earlier was an ancestor of D, and that's still true, but this new flag says we must also toss out commits that are not children of B.

How many children does node B have? Well, none! So we must toss out every commit, giving us a completely empty list.


What if we ask for B...D, without the special --ancestry-path notation? That gives us everything reachable from either D or B, but excludes everything reachable from both D and B:

     o---o
          \
           \
        B   \
             \
              C--D
             /
        o---o

This is the same as B..D except that we get node B as well.

[Note: the section below on mixing --ancestry-path with B...D was wrong for almost a year, between April 2016 and Feb 2017. It has been fixed to note that the "must be child" part starts from the merge base(s), not from the left side of the B...D notation.]

Suppose we add --ancestry-path here. We start with the same graph we just got for B...D without --ancestry-path, but then discard items that are not children of the merge base. The merge base is the o just to the left of B. The top row o commits are not children of this node, so they are discarded. Again, as with ancestors, we consider a node its own child, so we would keep this node itself—giving this partial result:

        B
       /
      o       C--D
       \     /
        o---o

But, while we are (or --ancestry-path is) discarding children of this merge base node, the merge base node itself, to the down-and-left of B, was not in the B...D graph in the first place. Hence, the final result (actually tested in Git 2.10.1) is:

        B

              C--D
             /
        o---o

(Again, I'm not really sure how useful this is in practice. The starting graph, again, is that of B...D: everything reachable from either commit, minus everything reachable from both commits: this works by discarding starting from every merge base, if there are two or more. The child-of checking code also handles a list of commits. It retains everything that is a child of any of the merge bases, if there are multiple merge bases. See the function limit_to_ancestry in revision.c.)

Thus, it depends on the graph and the selectors

The final action of X..Y or X...Y, with or without --ancestry-path, depends on the commit graph. To predict it, you must draw the graph. (Use git log --graph, perhaps with --oneline --decorate --all, or use a viewer that draws the graph for you.)


There's an exception in git diff, which does its own special handling for X..Y and X...Y. When you are not using git diff you should just ignore its special handling.

We start with I and the o to its left, and also H and G. Then we lose H and G when we work back from L, so the result is just o--I.

The formal definition is that the merge base is the Lowest Common Ancestor, or LCA, of the given nodes in the graph. In some graphs there may be multiple LCAs; for Git, these are all merge bases, and X...Y will exclude all of them.

It's interesting / instructive to run git rev-parse B...D for the graph I drew. These commit hashes here depend on not just the graph itself, and the commit, but also the time stamps at which one makes the commits, so if you build this same graph, you will get different hashes, but here are the ones I got while revising the answer to fix the description of --ancestry-path interacting with B...D:

$ git rev-parse B...D
3f0490d4996aecc6a17419f9cf5a4ab420c34cc2
7f0b666b4098282301a9f95e056a646483c2e5fc
^843eaf75d78520f9a569da35d4e561a036a7f107

but we can see that these are D, B, and the merge base, in that order, using several more commands:

$ git rev-parse B     # this produces the middle hash
7f0b666b4098282301a9f95e056a646483c2e5fc

and:

$ git rev-parse D     # this produces the first hash
3f0490d4996aecc6a17419f9cf5a4ab420c34cc2

and:

$ git merge-base B D  # this produces the last, negated, hash
843eaf75d78520f9a569da35d4e561a036a7f107

Graphs with multiple merge bases do occur, but they're somewhat harder to construct—the easy way is with "criss cross" merges, where you run git checkout br1; git merge br2; git checkout br2; git merge br1. If you get this situation and run git rev-list you will see several negated hashes, one per merge base. Run git merge-base --all and you will see the same set of merge bases.

这篇关于祖先路径如何与git log一起使用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

09-18 04:08