Home > Software design >  How can I pull after renaming local branch and deleting duplicated remote branch via GitHub?
How can I pull after renaming local branch and deleting duplicated remote branch via GitHub?

Time:10-15

Everything was in sync with the Remote repository on GitHub.

Then I renamed a local branch in Sublime Merge (right-click → rename branch).

Then I pushed – This did not sync the remote repo, instead I now had two identical branches there, one with the old and one with the new name. So I deleted the old branch via GitHub, because I have no idea how to do it from Sublime Merge.

Now, when I try to pull in Sublime Merge, it says:

Ihre Konfiguration gibt an, den Merge mit Referenz 'refs/heads/<old-branch-name>' des Remote-Repositories durchzuführen, aber diese Referenz wurde nicht angefordert.

English translation via Google Translate:

Your configuration says to merge with reference 'refs/heads/<old-branch-name>' of the remote repository, but this reference was not requested.

What can I do to fix this problem? I guess I have to remove the old branch entry from my configuration, but how and where?

CodePudding user response:

What I did:

# Read eftshift0´s and torek´s comments
# Duckduckify "upstream" knowledge, find shorturl.at/tEXY3.
git help                           # too broad
git branch help                    # better

git branch --list                  # OK, renamed branch is active, but too terse
git branch --list --all            # too terse
git branch --list --all --verbose  # too terse
# make console full screen
git branch --list --all -vv        # better (-vv = "very verbose")
                                   # lists deleted remote
                                   # blue shows deleted remote
                                   # thats wrong

git fetch --prune                  # eftshift0´s comment
git branch --list --all -vv        # SUCCESS, deleted remote branch not listed

git branch --set-upstream-to=origin/new-correct-remote
                                   # torek´s comment
git branch --list --all -vv        # SUCCESS, correct blue remote branch

Added to ~/.bash_aliases for future reference:

gitlistbranches() {
    git branch --list --all -vv
}

gitsetremote() {
    git fetch --prune
    git branch "--set-upstream-to=origin/$1"
}

CodePudding user response:

Short version: use git branch --set-upstream-to to fix the upstream setting, e.g.:

git branch --set-upstream-to=origin/bract bract

(note that the origin/ version of the name goes after the equal sign, with no space, in this format; the branch name itself is optional and if you leave it out Git will assume "the current branch name").

Longer: how to understand what's going on here

Branch names, in Git, don't actually matter very much. This means you can change them whenever you want. But there are some issues here, because each Git repository has its own (separate) branch names. Changing the name in one repository doesn't affect the name in another.

Besides branch names, Git has other kinds of names, such as tag names (often written with a v, e.g., v1.7 or v2.12.1) and remote-tracking names.1 You'll likely be at least acquainted with, and perhaps quite familiar with, names like origin/main or origin/master, and these are examples of these remote-tracking names.

Whenever you run git fetch—including the git fetch that git pull runs—you are instructing your Git software to, using data from your repository, call up some other Git software at some URL. The URL itself is typically the one that Git saved at the time you ran git clone to create your Git repository. Git will have saved that URL under the name origin. This name—origin—is actually change-able, but people don't normally change origin much if at all.

This short name, usually origin here, is just a way of helping you keep track of the fact that there's another Git repository "out there" on the Internet somewhere. The short name both stores the URL, so that you can run git fetch later without having to remember it yourself, and forms the basis for remote-tracking names.

So, when you do run git fetch—including the git fetch that git pull runs, and the git fetch that git clone ran when you made the repository in the first place—your Git software calls up this other Git software, which reads from some existing Git repository. That Git repository has its own branch and tag names. It may even have remote-tracking and/or other kinds of names. Your Git software can see all of these names—or at least, all the ones they're willing to show you—and the Git hash IDs that go with them. In fact, there's a command you can use to dump all this out:

git ls-remote origin

will call up the other Git over at origin, have it list out the names it will let you see, and will then just print them all out without actually doing anything useful with them.

But when you run git fetch, Git uses this same mechanism to see all their branch and tag names, and uses that to get commits from them (and to get tags if that's enabled, as it normally is by default). Then—once your Git has any new commits from their set of commits added to your repository—your Git normally updates all your remote-tracking names. This involves creating new names, if they show a name that you don't have yet as a remote-tracking name, or updating the existing names, if they show a name that you do have.

What all this means is that if you clone a repository that, at the time you clone it, has branches named main, br1, and br2, you'll get, in your own clone:

origin/main        (corresponds to their main)
origin/br1         (corresponds to their br1)
origin/br2         (corresponds to their br2)

At this point you have no branches, which is a really miserable way to work in Git (although it can be done). So git clone now creates one of these three branch names in your repository, based on the -b argument you gave to git clone. If you didn't give a -b argument, and most people mostly don't, you get the name that their software recommends, which is typically main or master.2

But now suppose that you use git push to delete the name br1 on the hosting site and replace it instead with the name anch. If you now run git ls-remote origin, you'll see the three names main, br2, and anch. That's what your Git software will see too, so git fetch origin will create an anch, spelling it origin/anch. That's your new remote-tracking name for their anch.

What happens to br1? Nothing. It's still there, as a stale left-over. Your Git software creates or updates appropriate remote-tracking names for the branch names they have now, but doesn't clean out the dead ones for names your Git software created earlier, that no longer have any obvious purpose.


1Git now fairly consistently calls these "remote-tracking branch names", but the word branch here is just cluttering things up, in my opinion. These names are "remote-tracking", so that part makes sense. What they track is some other repository's branch names, hence the rest of the name—but since they are in your repository, and are not actually branch names, I think it makes more sense to leave out the word branch entirely. They are names, and they track—as in "follow along with"—a remote's branch names, but they're not branch names at all.

2To change the recommendation on GitHub, use GitHub's web interface to set the "default branch". To change it on Bitbucket, use Bitbucket's web interface. To change it on Google Hosting ... well, they forgot to give you a way to change it! Oops. But this is the general idea: you can't do it from within Git, you have to use some add-on. That's probably a mistake in Git, but given that it's been this way for almost two decades now, it will probably stay this way for a while yet.


The special properties of a branch name

All the different varieties of names, in Git, hold exactly one (1) hash ID or—more formally—object ID, a big ugly string of letters and digits like c3ff4cec66ec004d884507f5296ca2a323adbbc5. This, which is really just a hexadecimal encoding of a very large number, serves to locate an object in Git's "object database", which is one of the two databases that make up the core of a Git repository (the other database being the names-to-IDs database). It's the combination of these two databases that makes a Git repository work; these two databases, plus a minimal set of ancillary files, are the necessary basic parts of any Git repository.

What makes a branch name particularly magic in Git comes in three parts:

  1. It's forced to hold only a commit hash ID. No other kind of object is allowed. (There are three other kinds of objects, and tag names, for instance, can hold any of the four kinds.)

  2. You can get "on" a branch, using git checkout or (since Git 2.23) git switch.

  3. Branch names support particular settings that other names lack.

It's actually item 2 that's the most crucial here. Being "on" a branch is how Git keeps track of the latest commit. Whenever you are "on" some branch and make a new commit, Git updates the stored hash ID to use the new, unique, apparently-random hash ID of the new commit. Git needs the hash ID of a commit, in order to find that commit. Branch names store the hash ID of the latest commit. Commits themselves store the hash IDs of earlier commits, so by finding the latest, Git can find earlier ones.

Given that a branch name will auto-update when you make a new commit, this makes branch names work to find all the commits. But since that's not the purpose of this answer / article, we'll stop here without further explanation. Instead, we'll move on to item 3, the settings.

The settings for any one particular branch name include:

  • whether you want this branch name to "merge" or "rebase" with git pull, if you choose to use git pull; and
  • the name of another branch in your repository, or the information needed to locate a remote-tracking name in your repository.3

The first item is strictly for git pull. The second item is also used by git pull, and by git rebase and git merge as well.

The git pull command means:

  1. run git fetch, then
  2. run a second command of my choice, usually git merge but sometimes git rebase.

Because it runs these back-to-back, it can actually tell a little bit more about what's going on than each command alone would know (which means that you get a vaguely-informative error message, as we'll see).


3The reason for this funny phrasing is that, for historical reasons, the actual encoding of this "upstream" setting is rather peculiar. To refer to a branch in your repository, branch.B1.remote is set to the literal string . (a period) and branch.B1.merge is set to refs/heads/B2, where B1 and B2 are the two branch names as they appear in your repository. To refer to a remote-tracking name in your repository, however, these two are set to R and refs/heads/B2 where R is the remote and B2 is the branch name as seen on the remote. These are then run through a mapping determined by the refspec(s) in the default fetch settings, which I'm not going to go into for space reasons. It's crazy-complicated, even though the normal effect is just that, e.g., branch main has origin/main as its upstream.


The git pull operation

The git fetch command needs one or two pieces of information, and the git merge or git rebase operation also needs one more pieces of information. In particular, git fetch needs to know which remote to git fetch from. Are we doing git fetch origin, or maybe git fetch rumpelstiltskin or git fetch bruce or...? Of course it almost always turns out to be origin anyway (and that's a fallback default built in to git fetch), but it would like to know.

The second command, whether it's merge or rebase, needs to know at least one thing. What it really needs is a hash ID. It can take a name:

git merge origin/br2

or:

git rebase origin/br2

for instance both work just fine, providing you have an origin/br2 of course, but in both cases Git is going to turn that name into a raw hash ID to get its job done. Git may then use the name, e.g., git merge will generate a default merge message from the name. However, if git pull is running git merge, git pull generates its own merge message and overrides this default anyway.

In any case, both of these bits of information are encoded into a branch's upstream setting. The upstream of a branch named B is typically origin/B. That is, if you're on your branch main, you made that based on origin's main which you call origin/main, and you intend your new commits to go to their main and so on.

That's all fine, and when you create a branch br1 based on their branch br1 which you call origin/br1, you get a branch br1 with an upstream setting of origin/br1. You'll see this if you run:

git branch -vv

(or git branch --verbose --verbose if you like the long spellings). But if you then use git push to change the name, in the other repository, from br1 to anch, and then run:

git fetch

your Git will bring over a new name, anch, and turn that into origin/anch. If you're thinking ahead, you will also rename your local br1 to anch:

git switch br1
git branch -m anch

(just before or after renaming the branch over in GitHub or wherever). But: your branch, now known as anch, still says that its upstream is origin/b1. You've renamed b1 locally, and—whether using a web interface or git push—you've renamed b1 over on the hosting site, but the upstream setting in your repository is just a specially-formatted string, and it has not changed.

Running:

git fetch --prune origin

tells your Git: look up origin, use that to get the URL, call up that other Git software, inspect the set of branch names, and clean out any old stale names I have. That will toss out your stale origin/b1. But your anch branch still has the old origin/br1 as its upstream setting. So now you need:

git branch --set-upstream-to=origin/anch

to set the upstream of the current branch (which you now call anch) to be your origin/anch, which is what you call their anch.

Without this, git pull thinks that it should run git fetch origin and then use whatever update got made to origin/br1. Since no update did get made, git pull complains:

Your configuration says to merge with reference
'refs/heads/br1' of the remote repository, but this
reference was not requested.

(though actually the English version is a bit different than the reverse translation from the German translation).

Were you to run git merge or git rebase without supplying additional arguments, you'd get a similar (but somewhat different) complaint, e.g.:

fatal: No remote-tracking branch for refs/heads/br1 from origin

Note that this requires first cleaning out the "stale" name to get the error. If we don't run git fetch --prune first, there's a "stale" origin/br1 lying around. The rebase or merge command will use the hash ID stored in that stale name! The resulting rebase or merge will probably do nothing at all, but in general this isn't a great idea.

Some slight flaws with git fetch

Ideally, it would be nice if git fetch would always do --prune. You can make it do that:

git config --global fetch.prune true

This makes git fetch, including the one run by git pull, automatically do a prune operation whenever it can. There are some small bug-ettes here, though they are generally pretty safe: if you ever hit a weird corner case, just run git fetch again to fix things up, leaving out any extra arguments that caused you to hit the mini-bug.

There are two other things I must mention here:

  1. Very old version of Git (pre-1.8.4) often fail to update some remote-tracking name.
  2. There are ways to run git fetch so that it can't update most remote-tracking names.

In particular, if you run:

git fetch origin main

you're telling your own Git that, whatever the list of branch names the other Git spills out for us, we should only update our origin/main. (The slash here is implied; this goes back to footnote 3 and the hysterical raisins mentioned therein.) When you use this form of git fetch, fetch cannot prune (and will not do so even with fetch.prune set to true). If your Git version is really old, it won't even update origin/main: it will just leave, in .git/FETCH_HEAD, just enough information for this old version of Git to finish a git pull operation.

Note that when you run git pull like this;

git pull

this runs git fetch, not git fetch origin main, so these two odd cases don't happen. But when you run:

git pull origin main

the git pull command passes this entire set of arguments on to git fetch, resulting in git fetch origin main, so in ancient Git, these two special cases (no pruning and no remote-tracking update) do happen, and in modern Git, the one case (no pruning) happens.

The fact that it's this complicated, and that I need to mention this, is a sort of indictment of Git. I find Git to be very useful and mostly pretty good, but it does have a lot of instances of meaning 3 here.

  • Related