git 状态仍然显示“您的分支领先 <tag1> N 次提交。” git 变基后 <tag2>

git status still says “Your branch is ahead of <tag1> by N commits.” after git rebase <tag2>

假设我有一个 my-topic-branch b运行ch,它是 b运行ched off my local master b运行ch,并且 master b运行ch 绑定到遥控器 master b运行ch.

my-topic-branch b运行ch 最初是根据名为 tag1 的标签创建的。 tag1 是放置在远程主机 b运行ch 上的标签,我看到该标签是 git fetch.

的结果

一段时间过去了,允许其他人将他们的更改推送到远程主机 b运行ch。再后来,一个新的 tag2 被其他人放下(请参阅下面的 更新 6 了解这样做的原因)。

然后我再次使用 git fetch 以确保我将所有这些远程标签放入我的本地存储库以进行进一步操作。

我像这样 tag2 变基:

$ git rebase tag2
First, rewinding head to replay your work on top of it...
Applying: CENSORED_LOG_MESSAGE1
Applying: CENSORED_LOG_MESSAGE2
Auto packing the repository in background for optimum performance.
See "git help gc" for manual housekeeping.

但后来我 运行 git status 看到了这个:

$ git status
On branch my-topic-branch
Your branch is ahead of 'tag1' by 195 commits.
  (use "git push" to publish your local commits)

我的期望是上面的消息应该说一些关于 tag2 的事情,当然不是说我比旧标签领先 195 次提交。

为什么 git status 报告 my-topic-branch b运行 的提交,而不是我最近重新定位的新提交?

如果这是预期的行为,那么很好,我只需要忽略它,但奇怪的是我仍然落后 master 195 次提交,而这肯定不是真的 (如果 git rebase 实际上做了我认为应该做的事情)。

更新 1

如果 HEAD 还在那个 b运行ch 上,我仍然可以找到 my-topic-branch 的基点,来自:

$ git show -s $(git merge-base master @)
commit CENSORED_SHA1_TAG2 (tag: tag2)
Author: CENSORED_AUTHOR
Date:   CENSORED_DATE

    CENSORED_LOG_MESSAGE_TAG2
$

但这仍然引出了关于 git status 输出的问题。

更新 2:

更新以响应 Pesho_T's comment

我 运行 git branch -vv 得到了以下内容。这是经过审查的,但添加了“2”和“4”之类的数字以将它们与上面的其他数字区分开来:

$ git branch -vv | grep my-topic-branch
* my-topic-branch                                CENSORED_SHA1_TAG1 [tag1: ahead 195] MDFCOR-420 CENSORED_LOG_MESSAGE_TAG1

更新 3:

重播 中的一些命令,我​​看到:

$ git rev-parse --abbrev-ref @{u}
tag1
$ git rev-parse --symbolic-full-name @{u}
refs/tags/tag1

我目前从 torek 非常好的文章中得出结论,tag1 确实是一个标签,而不是 b运行ch。

更新 4:

我编辑了之前的 CENSORED_SHA1 以保持一致:

  1. CENSORED_SHA1_TAG1是tag1对应的commit SHA1
  2. CENSORED_SHA1_TAG2是tag2
  3. 对应的commit SHA1

更新 5:

响应 的另一个更新:

The upstream of a branch is always another branch name or remote-tracking name, as tag names are forbidden

由于这个实验,我不确定:

$ git rev-list --count --left-right my-topic-branch...my-topic-branch@{upstream}
195 0
$ git show -s $(git rev-parse my-topic-branch@{u})
commit CENSORED_SHA1_TAG1 (tag: tag1)
Author: CENSORED_AUTHOR
Date:   CENSORED_DATE

    CENSORED_LOG_MESSAGE_TAG1

以上表示上游指向一个标签

为了确认这些 tag1 和 tag2 标签都在主 b运行ch 上,我按照 :

中的提示进行了此操作
$ git branch -a --contains $(git rev-parse tag1^{commit}) | grep -E 'my-topic-branch|master'
* my-topic-branch
  master
$

更新 6:

我的计划实际上并不是要git push回到我运行git rebase上的那个标签(tag2)。我只是将该标记用作 master b运行ch 上的一个点,以将 my-topic-branch 变基为。 tag2 恰好是 master b运行ch 上的一个已知点,该应用程序正确地构建在该点上(出于保密原因,我无法进一步详细说明)。我希望继续 git rebase 到后续标签,tag3tag4 等等,直到 my-topic-branch 准备好生产,届时我会将其合并回我的local master b运行ch 并从那里执行 git push

更新 7

我发布了一个 答案来说明一旦在主题 b运行ch,从而支持其中显示的 torek 评论。我仍在考虑将 作为 的答案,因为如果没有他的意见,我不会想出来。

您似乎1不小心创建了一个分支名称和一个标签名称 缩短时两者打印相同。 [编辑:根据更新 3,这不是问题所在。相反,不知何故,您将标签名称设置为分支的上游。我不确定您是如何进入该状态的 - 请参阅下面我自己尝试这样做的结果。] 一旦您拥有两个这样的名称,它们就可以拥有不同的哈希 ID。 解析这样的名字时得到的哈希ID有点棘手(有规则,在the gitrevisions documentation中概述,但规则也有例外) .最好从这种情况中恢复过来,通常是通过重命名不合适的分支名称。

记住像master这样的分支名称才是真正的名称refs/heads/master;像 v2.25.0 这样的 标签名称 实际上是 refs/tags/v2.25.0。因此,即使标签存在,也可以创建名为 v2.25.0branch,因为分支的 full 名称是 refs/heads/v2.25.0,而不是 refs/tags/v2.25.0。这两个名称不同,但如果您查看每个名称的缩写版本,都将是 v2.25.0.

来自 git statusaheadbehind 计数消息是 运行:

的结果
git rev-list --count --left-right <name1>...<name2>

注意两个名字之间有三个点。2两个名字是:

  • name1 是您的当前分支;
  • name2 是您当前分支的上游

git rev-list 命令以这种形式(使用三个点)找到 可从 左侧名称而不是右侧名称到达的提交,并提交 可从 右侧名称而不是左侧名称到达的提交,然后是 counts (--count)它们,但分开 (--left-right) 而不是合并。

这意味着这些计数取决于您当前的分支(当然——这就是它说 "your branch ... is ahead of" 的原因) upstream 设置。您可以使用 git branch --set-upstream-to 控制上游设置,您可以使用以下方式读取当前分支的上游:

$ git rev-parse --abbrev-ref @{u}
origin/master
$ git rev-parse --symbolic-full-name @{u}
refs/remotes/origin/master

如果您不小心使 分支名称 标签名称 缩写时看起来相同,使用 --symbolic-full-name 变体。

一个分支的上游[编辑:或者应该是]总是另一个分支名称或远程跟踪名称,因为禁止使用标签名称:

git branch --set-upstream-to=v2.25.0
fatal: Cannot setup tracking information; starting point 'v2.25.0' is not a branch.

origin/master这样的远程跟踪名称更典型,但是您可以将任何分支的上游设置为任何其他分支名称。

如果 ahead 计数不为零,这通常就是您所看到的,也是您在这里看到的。但是,如果两个计数都不为零,git status 将使用单词 diverged。如果 ahead 计数为零且 behind 计数不为零,则 git status 打印 behind 计数。如果两个计数都为零——这样分支与其上游同步——git status 表示 Your branch is up to date with ....

有关三点语法的更多信息,请参阅 the gitrevisions documentation。要了解 可达性 ,请参阅 类似思考 (a) Git。对于简短的图形说明,请考虑这张图:

          I--J   <-- branch1
         /
...--G--H   <-- master
         \
          K--L   <-- branch2, origin/branch1

名称 branch1master 的 "ahead 2" 因为从提交 J 我们回到 I 然后 H,这意味着 I-J 提交可以从 branch1 访问,但不能从 master 访问。同样,branch2master 早 2 个,但它的两个是提交 K-L。这意味着 masterbranch1branch2 落后 ,这两个提交是 I-JK-L 分别。同时,branch1origin/branch1 分歧 因为它领先 2 (I-J) 2后面 (K-L).


1如果您 移动 标签,您可能会遇到类似的棘手情况,因为标签在所有 Git 存储库,但标签也意味着永远不会 移动 。一旦一个 Git 存储库有一个标签,它将倾向于假设 有的副本是 正确的,即使有人有强行移动了此 Git 存储库要匹配的其他 Git 存储库中的标签。但这会表现出不同的症状,因为您不能将分支的上游设置为标签名称。

2三点句法以集合论的形式产生 symmetric difference。因为这是对称的,所以您可以交换这两个名称,只要您记得 git rev-list --count --left-right 将打印的两个 counts 现在也交换了。

总结

我现在认为 是关键方面。下面我 通过在本地主机上使用 git push -u 来证明这一点 在此过程中早些时候在该分支上设置上游。

MCVE

我发现显示 git 状态的根本原因是:

Your branch is ahead of 'tag1' by N commits

是本地master分支没有upstream因为 -u 没有传递给最初的 git push。如下所示 与 MCVE。它仍然是最小的,因为我无法避免使用 工作树在这里(我在真实场景中确实有工作树)。我没有 在我的 OP 中包含有关使用工作树的详细信息,因为它是 不认为是相关的。我仍然不认为它是,但我不能 在不使用工作树的情况下重现问题。因此,或许 工作树是一个重要因素。也许有人可以启发我们 进一步说明为什么会这样,或者进一步最小化我的演示脚本 因此。

演示脚本

下面的Bash(Linux)脚本是执行一个函数两次 以两种方式重现我的场景:一种使用 git push -u 并再次使用 使用 git push(没有 -u):

#/!/bin/bash

change () {
  local file=""
  local change_text=""
  echo "$change_text" > $file
  git add $file
  git commit -m "$change_text"
}

log () {
  (
    set +x
    echo
    echo "Current branch:   $(git symbolic-ref --short HEAD)"
    echo "Current upstream: $(git rev-parse --symbolic-full-name @{u})"
    git log --oneline --graph --decorate --parents
    echo
  )
}

replay () {
  local scratch_dir=""
  local master_push_args=""
  test -z "" && { echo "USAGE: replay scratch_dir"; exit 1; }
  (
    echo
    echo "Generating Git scenario into ${scratch_dir} ..."

    set -e -x

    # Create a scratch area:
    rm -rf $scratch_dir
    mkdir $scratch_dir
    cd $scratch_dir

    # Create the bare repo for some scrubber product:
    mkdir scrubber
    cd scrubber
    git init --bare
    main_git_repo=$(pwd)
    cd ..

    git clone $main_git_repo bootstrap
    cd bootstrap

    # Add some changes to it:
    change file1 "change 1.1"
    change file1 "change 1.2"
    git tag tag1 -m "tag1"
    git push $master_push_args
    git push --tags
    log
    cd ..

    # Create a mirror:
    git clone --mirror $main_git_repo my_mirror
    mirror_dir=$(readlink -f my_mirror)

    # Create a worktree
    GIT_DIR=$mirror_dir git worktree add scrubber1
    cd scrubber1

    # Add some changes to a new branch:
    #
    #   Avoid conflicts during subsequent rebase by changing a different file.
    #
    git checkout -b my-topic-branch tag1
    change file2 "change 2.1"
    change file2 "change 2.2"
    change file2 "change 2.3"
    log

    # Add some changes to master:
    git checkout master
    change file1 "change 3.1"
    change file1 "change 3.2"
    change file1 "change 3.3"
    change file1 "change 3.4"
    git tag tag2 -m "tag2"
    change file1 "change 4.1"
    change file1 "change 4.2"
    git push $master_push_args
    log

    git checkout my-topic-branch
    git rebase tag2
    git status
    log
  ) 2>&1 # for subsequent filtration (e.g., ... | grep/sed/awk/etc.)
}

filter () {
  grep -A2 'git status'
}

# Show the version I'm using:
echo "Git Version: $(git --version)"

echo
echo "Using git push on master without -u ..."
replay /tmp/scenario1 | filter

echo
echo "Using git push on master with -u ..."
replay /tmp/scenario2 -u | filter

执行该脚本会得到来自 git 状态的两个结果:

Git Version: git version 2.20.1

Using git push on master without -u ...
+ git status
On branch my-topic-branch
Your branch is ahead of 'tag1' by 7 commits.

Using git push on master with -u ...
+ git status
On branch my-topic-branch
Your branch is up to date with 'my-topic-branch'.