Mike Robinson
Mike Robinson

Reputation: 8965

How can I fix a "stale" reference to a remote branch?

Git says this:

On branch f-dbmailer
Your branch and 'origin/DBmailer' have diverged,
and have 7 and 43 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)

But that information is now out-of-date: I deleted the remote branch altogether, and have confirmed that it is really gone. (So, it doesn't have "43 commits.") The actual remote branch now has the name f-dbmailer ... but Git hasn't forgotten about the old branch, nor apparently the commit count that it used to have.

How do I clear this up?

Upvotes: 1

Views: 1855

Answers (2)

ErikMD
ErikMD

Reputation: 14743

The issue you describe certainly comes from the fact a branch has been removed in the remote, while the corresponding remote-tracking branch is still part of your local repository.

As I mentioned in my comment, this situation can be spotted by running the command git fetch git fetch -p then git branch -vv (with two v, see the example below), but instead of needing to run git branch -d … manually for each similar branch, the procedure to "cleanup" your local repo can just as well be automated using a Git alias.

Definition of a new command git deldone (relying on Perl)

$ git config --global alias.deldone '!f() { git fetch -p && git branch -vv | \
  perl -wne '\''print "$1\n" if m/^\s*(\S+)\s+[0-9a-f]+\s+\[\S+: gone\]/;'\'' | \
  xargs git branch -d; }; f'

Note that the git fetch option -p (for --prune) is necessary in this alias; see also @torek's answer for more details on this option.

Complete example of use

$ git branch -vv
    feature 12d22c1 [origin/feature] README.md
    fix     275e548 [origin/fix] Fix .gitignore
  * master  49b6c4a [origin/master: ahead 2] Merge branch 'fix'

  # let's remove branches feature and fix in the remote:

$ git push origin :feature
  To github.com:erikmd/demo-branch.git
   - [deleted]         feature
$ git push origin :fix
  To github.com:erikmd/demo-branch.git
   - [deleted]         fix

  # due to the commands above a `git fetch -p` was implied,
  # and you then get:

$ git branch -vv
    feature 12d22c1 [origin/feature: gone] README.md
    fix     275e548 [origin/fix: gone] Fix .gitignore
  * master  49b6c4a [origin/master: ahead 2] Merge branch 'fix'

$ git deldone
  error: The branch 'feature' is not fully merged.
  If you are sure you want to delete it, run 'git branch -D feature'.
  Deleted branch fix (was 275e548).

$ git branch -vv
    feature 12d22c1 [origin/feature: gone] README.md
  * master  49b6c4a [origin/master: ahead 2] Merge branch 'fix'

$ git branch -D feature
  Deleted branch feature (was 12d22c1).

Note that unlike the fix branch, the feature branch was properly detected to be gone upstream, but not removed automatically as Git knew it could lead to data loss, given the branch at stake had not been integrated. (see doc)

Upvotes: 2

torek
torek

Reputation: 489113

Use git fetch --prune (or set fetch.prune in a Git configuration file: I set mine globally some years ago and never have to think about it now).

What's going on

When you run git fetch or git fetch origin, your Git calls up another Git, using a remote name such as origin. That other Git lists out its branch names and their corresponding hash IDs. (It also lists tag names and other names, but the ones we're interested in here are branch names.)

Your Git takes those branch names and renames them, to make your remote-tracking names. This renaming is actually under the control of the remote.origin.fetch setting in your Git configuration:

$ git config --get remote.origin.fetch
+refs/heads/*:refs/remotes/origin/*

The leading plus sign is a force flag: these refs are updated as if you had used git fetch --force (but other updates aren't, unless you do use git fetch --force or they too are flagged). The left side of the colon, refs/heads/*, matches all of the other Git's branch names. The right side of the colon, refs/remotes/origin/*, provides the renaming: their refs/heads/master becomes your refs/remotes/origin/master, for instance.

Now, suppose that you run git fetch twice, with some time interval in between during which branch names get created and/or deleted in that other Git. The first time you have your Git call their Git, the listing includes:

refs/heads/master
refs/heads/br1
refs/heads/br2

The second time you have your Git call them, the listing doesn't have br2 in it but does have br3 in it:

refs/heads/master
refs/heads/br1
refs/heads/br3

If the commit hash ID for any of these is updated, your Git updates your corresponding remote-tracking name. But br2 just doesn't exist any more. So your refs/remotes/origin/br2 has no new value.

By default, your refs/remotes/origin/br2 keeps its old value. With --prune in effect, your Git says: Aha, the thing that would create or update origin/br2 is gone, so I should delete origin/br2 entirely ... and it does.

The --prune option probably should be the default, but it isn't. You can make it your default with:

git config --global fetch.prune true

and now your Git will automatically delete any remote-tracking name that no longer has a corresponding branch name on the remote.

Upvotes: 1

Related Questions