1

I know for a fact that a remote repo has a branch, let's call it tricky-branch. I can see it on gitlab. But I can't seem to find a way to pull it, or even see it, locally.

I've tried:

git fetch --all
git pull --all
git branch --all

$ git checkout origin/tricky-branch
error: pathspec 'origin/tricky-branch' did not match any file(s) known to git

I know I am pointing at the right repo and the connectivity is all right, because I can push to that repo. Just not pull.

torek
  • 448,244
  • 59
  • 642
  • 775
Irina Rapoport
  • 1,404
  • 1
  • 20
  • 37
  • Can you share the repository with us, or is it private? This would be much easier to solve as a concrete problem rather than an abstract problem. – larsks Aug 05 '22 at 19:23
  • Sorry, can't. Not my repo. – Irina Rapoport Aug 05 '22 at 19:29
  • Some troubleshooting questions/ideas... Does the branch name have special characters in it? When you say `git branch --all` does not work, I assume you mean it's not displaying `origin/tricky-branch` in its output? If you go to GitLab and find the sha1 from the head of tricky-branch, then try `git show `, what do you get? Are you allowed to paste the actual name of the branch here, in case it might help? – joanis Aug 05 '22 at 19:39
  • Wow, `git clone` fixed it! So the problem was in the local repo. TBC... – Irina Rapoport Aug 05 '22 at 19:55
  • Just curious, is RedHat officially involved in Gitlab? Good to see you here @joanis – Irina Rapoport Aug 05 '22 at 19:56
  • I have nothing to do with RedHat or GitLab (except as a user), you probably have me confused with someone else with a similar name... – joanis Aug 05 '22 at 20:03
  • Yeah, that was meant for @larsks – Irina Rapoport Aug 05 '22 at 20:18
  • Oh, OK, that makes a lot more sense. – joanis Aug 05 '22 at 20:22
  • 1
    So, if a fresh clone fixed it, I have the feeling this was a glitch and there might not be much value in this question in the future. If you agree, I'd recommended deleting it. – joanis Aug 05 '22 at 20:24
  • Except if I saw the answer this morning, it would have saved me the day, along with two coworkers who tried to help. Besides, if RedHat is a GitLab contributor, as I suspect they are (Sr. Principal types aren't usually in the habit of cloning other people's repos for no reason), then maybe they'll submit a fix. – Irina Rapoport Aug 05 '22 at 21:55
  • 1
    To the best of my knowledge, Red Hat is not involved in Gitlab. – larsks Aug 05 '22 at 23:05

1 Answers1

2

Based on the comments, I believe you had made a single branch clone. Your new clone, where things worked, was an ordinary clone. See How do I "undo" a --single-branch clone? (though these days git remote set-branches origin "*" is a "better" method for updating things, vs the accepted answer there).

(To find out, run:

git config --get-all remote.origin.fetch

and see what gets printed.)

Meanwhile, let's address these as well:

git fetch --all

This is sometimes useful. It probably wasn't in your case.

git pull --all

This is almost never useful.

git branch --all

This is quite useful and showing the output from this would potentially have helped here. The interesting question is, I think, "why is --all so inconsistent", or something along these lines. The answer to that is basically that Git is a collection of ad-hoc tools: each one does its own thing, and some of them have useful --all options and some just don't.

What --all means to git fetch is all remotes. In other words, if you have two or three remotes, such as origin + upstream + fred, a git fetch --all means that Git should run:

git fetch origin
git fetch upstream
git fetch fred

(in some order, or perhaps even in parallel).

If you have one remote named origin, git fetch --all means git fetch origin, which is what git fetch origin and git fetch also mean. So --all does absolutely nothing! Most setups have only one remote. (I say this without doing any statistical analysis first, but I'm pretty confident. ) For setups with multiple remotes, git remote update is probably more appropriate here.

Meanwhile, git pull means—at one point, it meant this literally as git pull was just a small shell script—that you'd like Git to run git fetch for you, and then, if the git fetch succeeds, run a second Git command for you. The --all option is passed to git fetch, where it usually has no effect for the reasons outlined above.

If it does have any effect at all, it has no effect on the second command, and the reason you're using git pull is to get Git to run the fetch and the second command and you probably care more about the results of the second command. So even if --all actually did something, you probably don't care. That's why it's almost never useful. (If you see someone advising the use of git pull --all, you should probably suspect them of cargo cult programming and/or xkcd #1597.)

More about single-branch clones

It's worth exploring what a "single branch clone" really is, why you might sometimes want one, why you probably don't want one, and—perhaps most important—why you might have gotten one without intending to. This gets into the mysteries of Git's refspecs, which are unfortunately somewhat complicated.

When we clone a repository in the ordinary way:

git clone [-b <branch>] <url>

we are telling our Git to:

  1. make a new empty directory and run the remaining commands in that directory;
  2. run git init to create a new, totally-empty repository;
  3. run git remote add origin url;
  4. run any additional git config or similar operations required;
  5. run git fetch origin; and
  6. run a final git switch or git checkout to create the branch we gave as our -b parameter, or the default branch if we didn't give -b.

Note step 5 in particular. This is what copies all the commits—technically, all reachable commits—from the repository at the given URL. It does not copy any branches though. Instead, it reads out their branch names and changes those names into remote-tracking names in our new clone. If they had branches main and feature/one and feature/two, we end up with remote-tracking names origin/main, origin/feature/one, and origin/feature/two.

A subsequent git fetch origin reaches back to the same URL and gets, from the Git that responds there, any new commits they have, that we lack, that we need in order to update our remote-tracking names. You can limit this fetch:

git fetch origin feature/two

tells your Git that, despite them maybe having new commits on their main for instance, you're not interested in those new-to-you commits unless those commits are also on their feature/two. You'd like to get all the new-to-you feature/two commits (using their feature/two, that is), and then when this is all done, update your own origin/feature/two.

The underlying mechanism that Git uses to do all this is the refspec, and there are some special cases here, because early versions of Git got this wrong. In Git 1.8.2, Git was changed to make this better. It's still downright weird, but it's better.

A refspec is a way of writing two names that will show up in two different Git repositories and relating them, or two patterns for names that will do the same thing. The complete form of a refspec is:

  • an optional leading plus sign +;
  • a left-side name or pattern;
  • a separating colon : character; and
  • a right-side name or pattern.

Both git fetch and git push use refspecs, because both git fetch and git push connect two repositories. The two repositories each have their own branch names (and other kinds of names) and, for git fetch in particular, we don't want their branch names to overwrite our branch names. We want their branch names to become our remote-tracking names.

There are a lot of finicky details with refspecs, including the fact that (for historical reasons including that Git 1.8.2 change) fetch and push treat them differently when you leave out the colon. But for the default operation, where you just run:

git fetch origin

for instance, Git has a default fetch refspec. The default value for the origin refspec is:

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

and now that we know that the format is + (optional) <name> : <name> it's suddenly obvious that the refs/heads/* on the left means their branches and the refs/remotes/origin/* on the right means our remote-tracking names.

This is how and why their main becomes our origin/main!

So, when you run git clone <url>, the new repository you get has origin as its one and only remote—this is why I'm so confident that most setups have one remote, because most people make a clone and then work with it and don't run git remote add on that to add a second remote—and its remote.origin.fetch is set to +refs/heads/*:refs/remotes/origin/*.

But if you use --single-branch during cloning, step 4 kicks in. Here it is again:

  1. run any additional git config or similar operations required;

What happens here is that we get:

git remote set-branch origin <branch>

or equivalently:

git config remote.origin.fetch +refs/heads/<branch>:refs/remotes/origin/<branch>

This changes the default fetch refspec, so that git fetch origin or git fetch only fetches the commits reachable from one branch name in their repository, and only creates or updates the one remote-tracking name.

A two-branch clone

If we make a one-branch clone and then run:

git remote set-branches --add origin branch2

we'll find that git config --get-all remote.origin.fetch now contains:

+refs/heads/branch:refs/remotes/origin/branch
+refs/heads/branch2:refs/remotes/origin/branch2

This makes use of what Git calls a multi-valued configuration variable. This fetch setting tells git fetch that by default, fetching from origin should check both branch and branch2 for new-to-us commits, bring those over, and update both remote-tracking names.

Using git remote set-branches without --add tells git remote to erase the old settings and install new ones.

A shallow clone is a single-branch clone

Besides single-branch (or two or three-branch) clones, Git offers the --depth option during cloning, and during git fetch as well. This makes an extra-limited clone in which Git deliberately doesn't get all the commits initially. Instead, Git fills in the branch graph to the specified depth, from each branch tip commit selected.

In some very large repositories, using --depth 1 speeds up cloning enormously. Most CI systems take advantage of this to make shallow clones for speed reasons, for instance. (Note that, due to a certain degree of silliness in the protocols, --depth 2 is much better if you intend to git push from this shallow clone. Also, not all implementations of Git protocols offer the ability to push from a shallow clone: e.g., GitHub Desktop apparently never intend to bother implementing it.)

But for no particularly good reason, using --depth with git clone turns on --single-branch automatically. You can use --no-single-branch to turn it back off. (Why they didn't just make you write --depth 1 --single-branch in the first place remains a mystery to me.)

The fact that shallow clones are single-branch clones, by default, has bitten many a casual Git user. It's easy to turn a shallow clone into a full clone using git fetch --unshallow, but if you're not aware that your shallow clone is also a single-branch clone, you don't realize that you must also run git remote set-branches origin "*" first. (Note the quotes around the asterisk: they are not always required, but are a good idea in general.)

torek
  • 448,244
  • 59
  • 642
  • 775