Home > Software design >  Warning renaming git remote
Warning renaming git remote

Time:07-07

I get the following warning when attempting to rename a git remote:

> git remote rename origin old-origin
warning: Not updating non-default fetch refspec
         refs/*:refs/*
        Please update the configuration manually if necessary.

I found a SO posting (How do I rename a git remote?) that talks about this and this is supposed to work. It does rename the remote, but why the warning? What does the warning mean and is it something that should be addressed?

CodePudding user response:

TL;DR

You have a mirror clone. Decide what you want to do about this.

Long

The warning means exactly what it says. The trick is to understand what a fetch refspec is in general and what this particular one is specifically. Let's start with refs and refspecs before we dive into fetch refspecs.

Refs and refspecs

A ref, in Git, is a name. It's more or less that simple: it's the generic word that encompasses branch names like main or master, tag names like v1.2, remote-tracking names like origin/develop, and so on. There are many kinds of refs and they all have a "full name" version; most have a "short name" version for convenience, like "Kirk" is short for James Tiberius Kirk and "Harry Mudd" is short for "Harcourt Fenton Mudd".

All branch names actually being with refs/heads/, so main is short for refs/heads/main. All tag names actually being with refs/tags/, so v1.2 is short for refs/tags/v1.2. Remote-tracking names begin with refs/remotes/ and go on to include the name of the remote, such as origin, and then the short form of the branch name as seen on that remote. So if some other Git repository has branch develop, and you "know" that repository as origin, you would typically choose to have your Git "remember" their branch develop—their refs/heads/develop—as your refs/remotes/origin/develop.

Each ref—each name—stores one hash ID. Some refs can store any kind of hash ID, and some (branch names in particular) are more constrained and must hold a commit hash ID (Git has four types of hash ID internally). We don't need to worry about these distinctions much here, but if you build a bad refspec, you can (at least attempt to) violate the constraints, so it's worth mentioning in passing. More importantly though, the point of storing a hash ID in a name is to allow us humans to use names to refer to particular commits. This way we can find the commits without having to remember hash IDs ourselves, even though Git requires that we supply the raw hash ID to Git. We can supply the name to Git, and Git will use the name we supplied to find the raw hash ID, and then use the raw hash ID to find the commit.

In other words, the name allows us to supply the raw hash ID to Git indirectly. We can just call for "the commit tagged v1.2" or "the latest develop-branch commit" and get the right one. Git itself does not, in an important sense, need the names at all (though it makes a whole lot of use of them, in furtherance of making things easy for humans, and this includes the idea of "garbage collecting" otherwise "dead" commits, where "dead" means "cannot be found using a name").

A refspec is, in its second-simplest form, simply a pair of refs separated by a colon. The simplest form is a single name, but such refspecs are not very sensible in our context here—they're mainly useful with git push—so we'll ignore them entirely for now.

When we write a refspec, especially for git fetch, we put a source name on the left side of the colon : character and a destination name on the right side. For instance, the refspec:

refs/heads/develop:refs/remotes/origin/develop

means that the source we'd like is the branch name develop and the destination we'd like is the remote-tracking name origin/develop.

Any refspec can be prefixed with a plus sign , which sets the force flag. We'll come back to this later. Both fetch and push refspecs can also deal, in a limited fashion, with shell-glob-like * characters:

refs/heads/*:refs/remotes/origin/*

is a refspec that says that the source is "every branch name", because * matches main, develop, feature/tall, and so on.1 The destination takes the matched string from the left and inserts it after refs/remotes/origin/, so this becomes a remote-tracking name whose remote is origin. Their main becomes our origin/main; their feature/short becomes our origin/feature/tall.

This is, in short, the usual way we use fetch refspecs: they cause our Git clone of some other Git repository to have remote-tracking names for each of their branch names. However, we also normally set the force flag.


1The fact that this kind of * can match across / makes it different from a shell glob.


The force flag, with a side of plumbing

Git can be told, at more or less any time using git update-ref for instance, to create, or destroy, or update any ref. This command falls into the group of commands that Git calls plumbing, as contrasted with the group that Git calls porcelain. A plumbing command is meant, in a sense, to be used as a building block to make something interesting happen as part of a larger program. A porcelain command is meant for a human to interact with. Porcelain commands tend to check for human error, and confirm things if something looks hinky. So we (humans) would more typically run git branch or git tag to create a new branch or tag, for instance, rather than running git update-ref; but update-ref will generally updated any ref exactly as we tell it, no questions asked, even if that's an obvious blunder.

When we work with branch names in particular, and we're creating new commits, we don't even have to invoke git branch at all, much less git update-ref. We simply check out some branch name (or git switch to it, since Git 2.23 introduced the less-error-prone git switch command) and then do work and run git commit. The git commit command:

  • saves a new snapshot of all of our files;
  • adds the appropriate metadata ("Torek made this commit, just now, here's his log message" and so on); and
  • updates the current branch name to record the hash ID of the new commit, which is now the latest commit.

This adds a commit on to the branch: the commit that was there before is still there,2 and now there's one more commit on the branch, namely the one I just made. This is how branches grow organically, as it were, one commit at a time.

We can also add more than one commit to a branch name, in an operation that Git refers to as a fast-forward, or sometimes a fast-forward merge.3 Insofar as Git has any thoughts or feelings,4 Git "likes" adding commits to a branch, thus moving a branch name "forward". Whether that's one commit at a time, via git commit, or a bunch all at once via a "fast-forward", Git is happy to add commits.

So, if we run git fetch and get some new commits from some other Git repository—new to us anyway—and one of their branch names has "moved forward" to encompass these new commits, our Git software will be happy to move our branch names or remote-tracking names forward too! But we can force Git to give up a commit, using git reset for instance. Git is "reluctant" to do that, and if we run git fetch and find that the other Git repository has retracted a branch, ejecting some commits off the end, our own Git will be unhappy about this. We may have to force this. With that in mind, let's look at git fetch itself a bit more closely.


2Unless, that is, I used git commit --amend, in which case the commit is still there but isn't "on" the branch any more because the stored parent hash ID in the new commit's metadata is not the hash ID of the commit I had checked out.

3The name fast-forward likely comes from cassette tapes and earlier recording technologies. We still have the concept with digital video: your DVR and/or BluRay player likely have a fast-forawrd button. The term fast-forward merge is a bit of technical nonsense: there's no actual merge happening at all. That's just a "Git thing": you just have to remember that in the context of git merge, a fast-forward isn't doing any actual merging. Perhaps that depends on how you define merge, but the way I define it, there's no merging involved.

4Don't anthropomorphize computers. They hate that! (Attributed to Andrew McAfee. I'm not sure where I first heard it, probably the old BSD "fortunes" database.)


How git fetch works

I mentioned earlier that Git doesn't need branch and tag names, or more generally "refs", to find commits. That's because every commit has a unique hash ID. If we were to memorize all our Git commit hash IDs, we could just give those directly to Git. And, whenever anyone adds any commit to any Git repository, that new commit gets a new, unique hash ID: one that has never been used before, in any Git repository anywhere, and can never be used again, in any other Git repository, unless it is to be used for that very commit.5

This in turn means that any two Git implementations, working with any two repositories, can simply exchange raw hash IDs to see whether one has the same commits as the other. This idea is at the heart of git fetch: we connect our Git software, looking at our repository, to some other Git software looking at some other Git repository. Let's call these "our Git" and "their Git" respectively.

We now have their Git list out some or all of their commit hash IDs. When they list out a hash ID that we don't recognize, that means they have a commit that we lack. We can choose to ask them to send us that commit (its metadata and snapshot, preferably in a compressed form).6 Git uses its backwards-looking commit graph to do this efficiently. It also typically uses names to get the process started.7 This is precisely where the refspecs come in.

A refspec of the form:

refs/heads/*:refs/remotes/origin/*

tells our Git to have their Git list out all their branch names and the corresponding commit hash IDs. We then have our Git ask their Git for every commit that they have on their branches that we don't have at all in our repository.

At the end of this process, our Git software knows all their branch names and hash IDs. We can now have our software save this information into our repository. To do that, we just have to tell it: for each branch name they have, create or update the corresponding remote-tracking name.

That's an example of a refspec, and that's what a fetch refspec is and does. It takes some name(s) from their repository and creates and/or updates corresponding names in our repository.

Whenever those names are certain kinds of names—branch or tag names, for instance—our Git may be reluctant to make "suspicious-looking" updates, like rolling a branch name "backwards". To force our software to do that, we simply add the force flag to the refspec. So:

 refs/heads/*:refs/remotes/origin/*

tells our Git software to take all their branch names and create or update remote-tracking names in our repository, even if that requires a "forced update" to eject commits off the end of one of our remote-tracking names.

This particular refspec is the bog-standard git fetch refspec for the remote named origin. Indeed, whatever the remote's name is, refs/heads/:refs/remotes/remote/ is the expected default refspec, which will cause all their branches to become all our remote-tracking names.8

Because this is standard, git remote rename knows how to change it in place. Not only will git remote rename replace the standard fetch = refspec setting, it will also rename all the remote-tracking names that would have been created by that setting. That is:

git remote rename origin xyzzy

will replace:

remote.origin.fetch =  refs/heads/*:refs/remotes/origin/*

with:

remote.xyzzy.fetch =  refs/heads/*:refs/remotes/xyzzy/*

it will also rename all existing origin/* remote-tracking names to xyzzy/* names.


5The pigeonhole principle tells us that this cannot work forever, and it won't: someday Git will fail. The sheer size of hash IDs puts that day far into the future (we hope, anyway). The conditions for failure, and the precise way Git will fail, are tricky: see How does the newly found SHA-1 collision affect Git?

6A lot of fancy graph theory goes into this compression, and it works really well, unless you're using shallow repositories. How well or poorly it works for shallow repositories, and what can be done about this, is a topic for at least a paper or two.

7Since a branch name, by definition, names the latest commit, this seems pretty much ideal. Git already has the names—the refs—that it's keeping for us humans; why not use them to make Git efficient for itself too? Note that in the old days, Git had some optional restrictions to prevent use of raw hash IDs here, but the newfangled "promisor remotes" feature have more or less thrown this particular baby out with the bathwater.

8As a sort of defect, this fails to delete any branch names they once had, that we converted to a remote-tracking name and therefore created in our repository. That is, they had a refs/heads/foo so we created a refs/remotes/origin/foo. Then they deleted their foo branch and so we ... simply stop updating our existing refs/remotes/origin/foo. It's a left-over; it has gone stale. To get rid of it we have to have our Git software ask about all their branch names, notice that they don't have a foo any more, and delete our origin/foo name. Git calls this pruning a remote and you can use git remote prune or git fetch --prune to make it happen.

You can even set Git's configuration variable fetch.prune to true to make git fetch do it by default. It turns out there are some (minor) sharp edges around this, though, so it's wise to know what you're doing, if you set this. Personally, I think Git should always have just done this, and smoothed out the rough edges long ago, but Git is what it is.


Your fetch refspec is a mirror refspec

All of the above talks about the normal everyday fetch refspec, but your clone has:

 refs/*:refs/*

Since all refs start with refs/, this refspec says that, whenever you run git fetch to this remote, your Git software should take every ref they give you and create or update the same ref in your repository.

That is, if they have branches main and develop and feature/tall, you'll completely wipe out your existing branch names main and develop and feature/tall and replace them with their branch name hash IDs. (See footnote 8 about pruning: a mirror should generally also have pruning turned on to avoid accumulating stale names, but I don't know if yours does.)

Instead of remote-tracking names, then, your Git slavishly copies all of their refs. That includes refs/replace/* names made by git replace. It includes their remote-tracking names, if they have any.

You get this kind of clone by running git clone --mirror, which sets up a bare mirror clone in which no one will ever do any work. The purpose of such a clone is to act as a local cache. This sort of thing is common in some larger companies, for instance: there's a "single source of truth" (SSoT) Git repository in the SF headquarters, but the data line going to Peoria or Auckland or wherever is kind of slow and expensive, so instead of having a dozen or so employees in the satellite office all clone the SSoT repository directly, you set up one mirror in the satellite office and update it frequently (say, every 10 or 15 minutes) and have the employees there clone the local clone. This takes seconds per clone instead of hours.9

It's not typical to rename the remote in a mirror clone, so we'd need to know a lot more about what you're really doing and why, to make any kind of solid recommendation here.


9There are other useful tricks here too. I literally reduced the clone time for a multi-repository build from about an hour to about 30 seconds using reference clones at one place, for instance.

CodePudding user response:

The fetch line in .git/config specifies which branches are fetched from a particular remote and under which local ref path they will be mirrored.

Here an example of the default origin

[remote "origin"]      
        url = git@mygitserver:mygit.git
        fetch =  refs/heads/*:refs/remotes/origin/*

The fetch line instructs git to fetch all heads (branches) from the remote (refs/heads/*) and stores them locally as origin/* remote branches.

Apparently your fetch line differs from the default and mirrors the remote branches onto regular local branches with the same names; instead of remote branches
That's why you get that warning.

Whether you want to change the line back to its default is up to you.

  •  Tags:  
  • git
  • Related