ScottyC ScottyC - 21 days ago 8
Git Question

Git - How to update or remove "local out of date" copies of tracked remote branches?

When I run a

git remote show origin
I have a bunch of
local out of date
branches.

local out of date

I don't care about these branches, all I want is to not have
local out of date
versions, either by getting them
up to date
or removing my local copy (while retaining the tracking of origin/branchName).


I am not sure why I'm having such a difficult time figuring this one out. All my searches just turn up with people wanting to delete the remote branches or prune branches that no longer exist on remote. I thought
git pull --all
would be the solution, but that doesn't do anything for me.

Thanks all!

Answer

TL;DR: you probably just want to delete some of your branches

The full answer is a bit complex, in part because git remote does something no other Git command can do. If you're not using a local branch name, though, you can simply delete it as a distraction. The trick is knowing when it is safe to delete, although "when I'm not already using it and won't be using it" is always a good enough time.

Use git branch -d branchname to delete-with-checking, and git branch -D branchname to delete forcefully, i.e., without checking. The "checking" that Git does is different from the checking that git remote does, so do not be too surprised if they do not agree.

Since (and including) Git version 2.3, you can spell this git branch -d -f or git branch --delete --force, the way nearly all other Git commands do it. In older versions of Git you must use the uppercase -D to forcefully delete a branch name.

It might also be a good idea to stop using git remote show origin at all.

Git terminology, or, why you can't find documentation

The Git terminology surrounding this sucks, to put it bluntly. You will see the same words used in different combinations to mean different—sometimes very different, although usually at least a little bit related—things. Git has:

  • Remotes. A remote is just a one-word name like origin. A remote name supplies, at a minimum, a URL, so origin is short for ssh://example.com/path/to/repository or https://... or whatever. A remote name also provides a prefix for "remote-tracking branches", which we have not defined just yet.

    Note that the existence of a remote name implies that there is some other Git, usually on another machine entirely, found via the URL. That other Git repository has all of its own settings, quite independent from yours! This will matter when we talk about remote-tracking branches.

  • Branches. The word branch is ambiguous in Git (see What exactly do we mean by "branch"? for details), but here it means branch name. These may also be called local branches to distinguish them from the other kind. A branch name is just a short—well, usually short—name like master or develop or machines/hal9000 or users/dave or tasks/I_cant_do_that_dave. These names can have slashes, and the somewhat complicated rules about using slashes in them can mostly be ignored.

    A branch name points to one specific commit, which Git calls the tip commit of that branch. Each commit in any Git repository "points back" to earlier commits, too. More precisely, a commit points to its parent commit, or for merges, to its (multiple) parents. This forms a backwards chain, so that by starting from the tip of the branch, Git can find all the rest of the commits that are on that branch.

    (As an important side effect, this means many commits are on more than one branch. That does not matter here, but it is one of the keys to understanding Git's branches.)

    Beware of names that start with origin/ or any other remote-name, though, because those usually refer to...

  • Remote-tracking branches. These show up as names like origin/master. The function of a remote-tracking branch is to let your Git remember, for you, what your Git saw the last time it talked to that other Git, over on the remote. The points of contact here are git fetch, which brings stuff from the remote Git to your Git, and git push, which sends things from your Git to the remote Git along with a request that the remote Git accept them. The details matter somewhat but we'll leave that for later. Just note that git remote not only can, but usually does, contact the other Git—but then it immediately forgets everything it learned, unlike git fetch.

Git always keeps remote-tracking branches completely separate from local branch names, so that you can have a local branch named origin/master at the same time that you have a remote-tracking branch named origin/master.1 But don't do that: it's too confusing.

So far so good, except for the rather verbose name remote-tracking branch. Some people try to shorten it to "remote branch", but this is a bad idea, because ... well, there's that other Git, the one over on origin. It's the remote Git (not a great name but what else will we call it?). It is a Git repository, so it has branches. What are we going to call them? The obvious answer is "remote branches", or maybe "branches on the remote". (Git does not really have a proper name for these.)

The purpose of your remote-tracking branches is to keep track of these "on-the-remote branches". Your remote-tracking branches are, however, still yours: they live in your repository, and you can update them yourself if you really insist (but you should just let your Git do it automatically). This is also where git fetch --prune remotename and git remote prune remotename and git remote update --prune remotename come in. The prune options—which should all be the same, but have had slightly different bugs in different Git versions—are instructions to your Git: "When you fetch from that other Git, find out what branches it has, and for any of my remote-tracking branches that no longer match a branch-on-the-remote, delete it automatically."

While git fetch updates all your remote-tracking branches for you, git push does not. Instead, if you are pushing your master to origin and asking their Git to update their master, your Git will then take the opportunity to update your origin/master. Your Git does not, during this push transaction, bother to find and save information on any of their other branches. (There's no important idea behind this, it's just Git's historical behavior, with no strong reason to change it either.)


1Git has specific (and somewhat complicated) rules to pick which one you mean, in case of ambiguity. If you get into this situation, you can use the "full names" for each kind of branch to tell them apart: a local branch's full name always starts with refs/heads/, while a remote-tracking branch's full name always starts with refs/remotes/ followed by the name of the remote.

Thus, origin/master is short for refs/remotes/origin/master, i.e., a remote-tracking branch.

An accidental local branch with the same name is really refs/heads/origin/master.

If both exist, which one does origin/master actually use? The answer for most Git commands is in the gitrevisions documentation, but git checkout behaves differently. Just delete the bad local-branch-name. It's way too confusing.


Summary so far

A branch name is your own name, like master. This points to a tip commit.

A remote is just a name like origin.

A remote-tracking branch is your name, like origin/master, for your Git's memory of a name like master as last seen on the remote. Running git fetch updates these, and adding --prune cleans up leftovers of these. A successful git push updates your remote-tracking branch that corresponds to each successfully-pushed branch.

Now it gets bad. :-)

"Tracking" is not like remote-tracking branches, but usually uses them

In Git, you can set any branch—i.e., any ordinary local branch name—to "track" one, and only one, other branch. If local branch L is tracking remote-tracking branch R, we also say, at least in newer Git documentation, that R is L's upstream. For generality, though, and/or just to be more confusing and Gitty, Git lets you track another local branch instead. So L might track U, which is also local, and then U is L's upstream.

The only really critical thing is to realize that each of your local names, like master and develop, has either no upstream, or one upstream. Most often, local branches have an upstream that is a remote-tracking branch, with the remote-tracking branch having much the same name as the local branch, but with the remote's name added. Hence master has origin/master as its upstream.

Again, "has as its upstream" is the newfangled longer—but (we hope) less confusing—way to say "tracks". The problem with the phrase "master tracks origin/master" is that origin/master is already a remote-tracking branch. Remote-tracking branches don't have upstreams, so now we would have to claim that origin/master doesn't "track" anything, even though it's a "remote-tracking branch"! (Yuck! See why I say the terminology is terrible? Maybe it would help if origin/master were called a "memory name" or something, but it's not.)

What good is having an upstream anyway?

The full answer is a bit long and complicated, but the short version is: this is how Git can say:

Your branch is behind 'origin/master' by 286 commits, ...

and the like, and also how you can run git fetch, git merge, git rebase, and/or git push without spelling out where to fetch from, what to merge or rebase-upon, and where to push. That's the main reason to set an upstream: it automates a bunch of stuff for you and makes git status (and git branch -v, in modern Gits) much more informative.

Several things set upstreams automatically. The key one is that if you have a remote-tracking branch like origin/develop, and you don't already have a local branch named develop, you can just run:

git checkout develop

and your Git will discover that there is an origin/develop and create a new local name develop for you, with it set to "track"—i.e., have as its upstream—origin/develop.

Totally-new branches, that you have never pushed anywhere, generally won't have an upstream. The "why not" is simple enough: the correct upstream for newbranch would be origin/newbranch. But if you just created newbranch yourself, there is no newbranch on the Git at origin, so there can't be an origin/newbranch in your own Git repository either. There's no remote-tracking branch to set as the upstream yet! This is where git push -u comes in, as it means "push, then automatically set as upstream if the push works, since that also creates the remote-tracking branch if needed."

Pushing is not quite symmetric with fetching

When you git fetch, your Git calls up the remote's Git, probably over the Internet-phone. Your Git gets a list of their branches and such, and then brings things over as needed—and then it renames their branches, making them into your remote-tracking branches. Since your remote-tracking branches have, as a dedicated function, "remember what you saw on the remote", this is always a good thing to do, and modern Gits just always do it.2

When you git push, your Git calls up the remote's Git, sends them stuff, and asks them (politely, unless you use --force: then it asks a bit rudely) to set their branch to refer to the same commit as your branch. You can control the precise form of asking with a refspec, which I won't cover in detail here, but normally their branch name is the same as your branch name. That is, you git push origin master:master to ask them to set their master to match your master exactly.

There is no automatic renaming here. This is why, for your push to succeed, your branch must normally first be caught up with their branch. Generally, you do this by merging or rebasing. That's all your responsibility: the remote Git won't do any merging. It just either accepts your request, or rejects it. The default rule, if you do not use --force or equivalent, is to accept your push update if and only if their old tip commit is an ancestor of the new tip you are asking them to set.

Moving a branch name from an old tip to a new tip, such that the new tip commit "contains" the old tip commit, is called a fast-forward. This will be a key phrase later. The test: "is changing from hash X to hash Y a fast-forward?" is the same test as: "is X an ancestor of Y?"


2Since Git version 1.8.4, anyway; and this also glosses over any configured fetch refspecs. Older versions of Git are ... less helpful.


Summary the second

If your (local) branch L has (your) remote-tracking branch R set as its upstream, you get a bunch of nice automation. You do have to run git fetch to get R updated: your Git calls up the remote's Git and updates all your remote-tracking branches, and now your Git can compare your L to your R, which is now in sync with their branch (probably named L on their end, but that's in their repository, so it's their responsibility, not yours).

Likewise, when your L has (your) R as its upstream, you can run git push and your Git figures out what to ask their Git to do. For historical reasons, this is highly configurable, and versions of Git before Git 2.0 do something different by default, but since 2.0, the default action is simple (and is even called simple): your Git pushes your L to their branch that is also called L. Their branch is probably named L, but let's just call it B, to tell them apart. Your push will generally succeed only if your update to their B is a fast-forward.

git remote is very different

Running git remote show origin blows much of the above right out of the water.

So why did we discuss it all? Well, mainly because it's the much more normal way to work with Git. All the documentation out there mostly talks about using it, and the words in that documentation are this confusing mishmash of: "branch" "tracks" / "has as upstream" "remote-tracking branch" (on the "remote", where it's named "branch"). It's important to realize that all these words have specific technical definitions ... and that many of them can just get blown away when you use git remote. It's also useful because "local out of date", the message you are asking about, has a very specific meaning that ties in with git push, and we needed a lot of clarity about what git push does, vs what git fetch does, which means we needed a lot about remotes too.

What git remote show remotename normally does—you can tell it not to—is to call up the remote, just like git fetch or git push would. This means it gets an instantaneous, right-now picture, just like git fetch or git push would. It then tells you what git fetch right now, or git push right now, would do or try to do.

If their Git—the Git on the remote—has a branch named B, your Git would likely fetch it into your origin/B. So your git remote show origin says:

Remote branches:
  master tracked

which essentially means "I would fetch master and call it origin/master". (I would argue that this is poorly phrased; but it's like this for historical reasons.)

Meanwhile, if you have a local branch named L, that local branch may be set to "track" (i.e., may have as an upstream) one of the branches your git fetch would bring over from the remote and rename. For instance, your Git would bring over their master and call it origin/master, and your master might then be behind their master, or ahead of their master, or anyway need merging or rebasing. You would then merge or rebase using your origin/master, which they call master. So your git remote show origin says:

Local branches configured for 'git pull':
  master    merges with remote master

(There are some historical mistakes preserved here as well, which is why the phrasing is the way it is. This also refers to git pull, which I argue no beginner should use.)

Meanwhile, your master may not match their master, so that if you ran git push, your Git would ask their Git to change their master to match yours. Your git remote show origin tells you this by saying:

Local ref configured for 'git push':
  master pushes to master (local out of date)

or:

Local ref configured for 'git push':
  master pushes to master (fast-forwardable)

This last message is not as useful as it might seem, since by the time you actually run git push, their Git may have changed. Still, let's look at precisely what it means.

As we noted briefly above, when talking about git push, you can push your local branch—which we're calling L here—to a different name in their Git repository; so let's continue to call theirs B here. In any case, as we also noted, when you git push, you will send them commits if needed, then ask them to set their branch—their B—to point to one specific commit: the tip commit of your L.

Because your Git starts by asking their Git about all its branches, your Git can tell which commit hash-ID their B names. Any commits you will send their Git, ending with the tip of your L, will be all-new to them. But your Git can also tell whether their tip commit—their B—is "new to you", because you either have that commit now, or you don't.

If you do already have their B, your Git can tell whether that commit is an ancestor of your (tip-commit-of) L. If you don't have it, then by definition, it's not an ancestor of L,3 and the push will be rejected. You will need to fetch, then merge or rebase. Even if you do have it, it's possible that B is not an ancestor of L—and your push will be rejected, and you must merge or rebase. So that's what "local out of date" means: you may need to fetch—you might as well just do it since it's always safe—and then you definitely need to merge or rebase, to be able to push politely.

If you don't intend to push this particular branch, this information is useless. Even if you do intend to push, the information becomes stale, potentially very quickly: as soon as someone else pushes, what your git remote show origin showed is out of date.

In any case, though, that's what "fast-forwardable" means: you can try your push, and if it fails, you will need to fetch, then merge or rebase. "Local out of date" means you will need to fetch, then merge or rebase, and then try to push. Why not just fetch, then merge or rebase if needed, then try to push? What did you gain by running git remote show origin?


3The proof for this falls apart in the presence of a shallow repository. With shallow repositories, git remote show origin may simply lie to you.


Armed with 10k words, now back to the original question

I don't care about these branches, all I want is to not have local out of date versions, either by getting them up to date or removing my local copy (while retaining the tracking of origin/branchName).

The phrase "the tracking of origin/branchName" is kind of a stumbling block here for me. All the origin/branchNames will get updated on git fetch whether or not you have a local branch. The local branch will "track" (have as an upstream) origin/branchName if you create it with the automatic-upstream method, which is the easy way. So I think the answer here is "just delete the local branches".