1

Suppose I have a my-topic-branch branch that is branched off my local master branch, and that master branch is tied to the remote master branch.

The my-topic-branch branch was originally created off of a tag called tag1. tag1 is a tag layed down on the remote master branch, and I see that tag as a result of git fetch.

Some time passes to allow others to pushed their changes into that remote master branch. And still later, a new tag2 is layed down by someone else (See Update 6 below for the reasons for doing this).

I then use git fetch again to ensure that I have all of those remote tags into my local repo for further operations.

I rebase to that tag2 like this:

$ 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.

But then I run git status and see this:

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

My expectation is that the above message should say something about tag2, and certainly not that I'm ahead of the old tag by 195 commits.

Why would git status report the commit that my-topic-branch branched from, and not the new commit that I mostly recently rebased to?

If this is expected behavior, then fine, I will just have to ignore it, but it is odd to see that I'm still behind master by 195 commits, when that is certainly not true (if the git rebase actually did what I think it should).

Update 1

I can still find the base point of my-topic-branch if HEAD is still on that branch, via:

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

    CENSORED_LOG_MESSAGE_TAG2
$

But this still begs the question about the git status output.

Update 2:

Update in response to Pesho_T's comment:

I ran git branch -vv and got the following. This is censored, but adding numbers like "2" and "4" to discriminate them from the others above:

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

Update 3:

Replaying some of the commands in torek's answer, I see:

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

I currently conclude, from torek's very nice writeup, that tag1 is indeed a tag, and not a branch.

Update 4:

I edited prior CENSORED_SHA1's to be consistent:

  1. CENSORED_SHA1_TAG1 is the commit SHA1 corresponding for tag1
  2. CENSORED_SHA1_TAG2 is the commit SHA1 corresponding for tag2

Update 5:

Another update in response to torek's answer:

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

I am not sure of that because of this experiment:

$ 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

The above shows that the upstream is pointing to a tag.

To confirm that both of these tag1 and tag2 tags are on the master branch I did this, per the tips found in answer to Git: How to find out on which branch a tag is?:

$ git branch -a --contains $(git rev-parse tag1^{commit}) | grep -E 'my-topic-branch|master'
* my-topic-branch
  master
$

Update 6:

My plans are not actually to git push back to that the tag (tag2) I ran git rebase upon. I am only using that tag as a point on the master branch to rebase the my-topic-branch to. The tag2 happens to be a known point, on the master branch, that the application builds properly upon (I cannot go into further detail on that for confidential reasons). I expect to continue to git rebase to subsequent tags, tag3, tag4 and so on, until my-topic-branch is ready for production, at which time I will merge it back to my local master branch and do the git push from there.

Update 7

I posted a MCVE answer to show how the upstream shown by git status changes once an upstream is set on the parent branch of the topic branch, thus bolstering torek's comments shown therein. I'm still considering torek's answer as the answer to this, because without his input, I would have not figured it out.

bgoodr
  • 2,744
  • 1
  • 30
  • 51
  • I was under the impression that the line in question from `git status` output shows how far ahead/behind you are from the tracking branch :/ what does `git branch --vv` show about `my-topic-branch`? – Pesho_T Feb 28 '20 at 17:14
  • @Pesho_T I added Update 2. This is prior to absorbing [torek's answer](https://stackoverflow.com/a/60459607/257924). – bgoodr Feb 29 '20 at 18:00
  • Interesting: the upstream is set to a tag. How did you manage this, when `git branch --set-upstream-to` refuses to set a tag as an upstream? (I know it's possible internally, by editing the various files directly, but the front end commands are supposed to prevent this!) – torek Feb 29 '20 at 21:50
  • Note that the purpose of a tag name is to identify a specific commit (by hash ID) and *stay there forever*, while the purpose of a branch name is to identify a specific commit (by hash ID) and *later identify a different, usually child-of-earlier, commit* (by hash ID again). So both branch and tag names always identify one specific commit. – torek Feb 29 '20 at 23:30
  • @torek I did not modify any files internally to the $GIT_DIR. All I did was the rebase command mentioned in my OP. – bgoodr Mar 01 '20 at 04:12
  • @torek According to [a comment on Git - rebasing to a particular tag](https://stackoverflow.com/questions/12469855/git-rebasing-to-a-particular-tag#comment16773780_12469855), this is possible. – bgoodr Mar 01 '20 at 04:14
  • @torek I added Update #6 to explain the behind-the-scenes reasons why I'm rebasing to tags. I probably should have mentioned that detail in the very beginning, but I did not until now think it was relevant (it is actually, as I'm sensing this practice is "strange"). – bgoodr Mar 01 '20 at 04:32
  • Added Update #7 to point to the answer I posted which contains the MCVE I came up with. – bgoodr Mar 09 '20 at 01:57

2 Answers2

2

It seems likely1 that you have accidentally created a branch name and a tag name that both print the same when shortened. [Edit: per update 3, this isn't the problem. Instead, somehow, you have the tag name set as the branch's upstream. I'm not sure how you got into that state—see the result of my own attempt to do that, below.] Once you have two such names, they can hold different hash IDs. The hash ID you get when parsing such a name is a little bit tricky (there are rules, which are outlined in the gitrevisions documentation, but there are exceptions to the rules as well). It's best to get back out of the situation, usually by renaming the inappropriate branch name.

Remember that a branch name like master is really the name refs/heads/master; a tag name like v2.25.0 is really refs/tags/v2.25.0. It is therefore possible to create a branch named v2.25.0 even though the tag exists, because the branch's full name is refs/heads/v2.25.0, not refs/tags/v2.25.0. These two names are different, but if you view the short versions of each, both will be v2.25.0.

The ahead or behind count message from git status is the result of running:

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

Note that there are three dots between the two names.2 The two names are:

  • name1 is your current branch;
  • name2 is the upstream of your current branch.

The git rev-list command, in this form (using three dots), finds commits that are reachable from the left-side name but not the right-side name, and commits that are reachable from the right-side name but not the left-side name, and then counts (--count) them, but separately (--left-right) rather than combined.

This means that these counts depend on your current branch (of course—that's why it says "your branch ... is ahead of") and the upstream setting. You control the upstream setting with git branch --set-upstream-to, and you can read the upstream of your current branch with:

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

To help with the case when you have accidentally made both a branch name and a tag name that look the same when abbreviated, use the --symbolic-full-name variant.

The upstream of a branch is [Edit: or should be] always another branch name or remote-tracking name, as tag names are forbidden:

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

Remote-tracking names like origin/master are more typical, but you can set the upstream of any branch to any other branch name.

If the ahead count is nonzero, that's usually what you see, and is what you are seeing here. However, if both counts are nonzero, git status will use the word diverged. If the ahead count is zero and the behind count is nonzero, git status prints the behind count. If both counts are zero—so that the branch is in sync with its upstream—git status says Your branch is up to date with ....

For more about the three-dot syntax, see the gitrevisions documentation. To understand reachability, see Think Like (a) Git. For a short graphic illustration, consider this drawing:

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

The name branch1 is "ahead 2" of master because from commit J we walk back to I and then H, which means the I-J commits are reachable from branch1 but not from master. Similarly, branch2 is 2 ahead of master, but its two are commits K-L. This means that master is 2 behind either of branch1 or branch2, with those two commits being I-J or K-L respectively. Meanwhile, branch1 has diverged from origin/branch1 because it is both 2 ahead (I-J) and 2 behind (K-L).


1You can get into similar tricky situations if you move a tag, because tags are meant to be universal across all Git repositories, but tags are also meant never to move. Once one Git repository has a tag, it will tend to assume that the copy that it has is correct, even if someone has forcibly moved a tag in some other Git repository that this Git repository is meant to match. But that would show different symptoms, because you cannot set the upstream of a branch to a tag name.

2The three-dot syntax produces a symmetric difference, in set-theoretical terms. Because this is symmetric, you can swap the two names, as long as you remember that the two counts that git rev-list --count --left-right will print are now swapped as well.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Very nice explanation; I'm still absorbing your question, and in doing so, I updated my OP with Update #3 now w.r.t. your `git rev-parse`. – bgoodr Feb 29 '20 at 18:25
  • I added Update #4 and #5 to [my OP](http://example.com) to address the `The upstream of a branch is always another branch name or remote-tracking name, as tag names are forbidden:` comment, but now I see your `[Edit: or should be]` which definitely makes sense here. I really can't `git push` back to a tag of course, and until now I've kept you all in the dark about how I am going to push back. – bgoodr Feb 29 '20 at 23:01
  • "kept you all in the dark" is now rectified by Update #6 to [my OP](http://example.com/). – bgoodr Mar 01 '20 at 04:33
  • I'm going to conjecture that I'm not violating the spirit of git by rebasing to a tag. That tag is _reachable_ by `master`, whereby `master` points to a subsequent point in its timeline. So, in a vague sense, I've not changed the _upstream branch_ at all: that tag is still on the `master` branch. But, darn it all, it is confusing as heck to have `@{u}` or `@{upstream}` indicate that tag, and not `master`, so I'm still not quite comprehending the situation. – bgoodr Mar 01 '20 at 04:43
  • 1
    You can use anything you like with `git rebase`: by default, its one argument—which it (confusingly) also calls *upstream* —determines both which commits to copy/not-copy, and where to put the copies. Meanwhile if you want the ahead/behind counts to make sense, you might want to use `git branch --set-upstream-to` to *change* the upstream of the branch so that it does not point to the tag. This won't affect your ability to rebase however you like in the future. – torek Mar 01 '20 at 05:33
0

Summary

I now believe that torek's comment is the key aspect. Below I demonstrate that, by instead using git push -u on the local master to set the upstream on that branch earlier on in the process.

MCVE

I discovered that the root cause of why git status was showing:

Your branch is ahead of 'tag1' by N commits

is that the local master branch did not have an upstream because the -u was not passed to the initial git push. This is shown below with a MCVE. It is still Minimal because I could not avoid using the worktrees here (I do have worktrees in my real scenario). I did not include the detail about the use of worktrees in my OP because it was not thought to be relevant. I still don't think it is, but I can't reproduce the issue without the use of worktrees. Thus, perhaps worktrees are an important factor. Perhaps someone can enlighten us further as to why it would be, or further minimize my demo script accordingly.

The demonstration script

The Bash (Linux) script below is executes a function twice to reproduce my scenario in two ways: one with git push -u and again with git push (no -u):

#/!/bin/bash

change () {
  local file="$1"
  local change_text="$2"
  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="$1"
  local master_push_args="$2"
  test -z "$1" && { 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

Executing that script gives two results from git status:

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'.
bgoodr
  • 2,744
  • 1
  • 30
  • 51