Git branch is not displaying all branches

When you clone an existing repository, your Git makes a new and different repository, and copies into this new repository all1 of the commits and none of the branches from the original repository. The last step of git clone is to create one branch. This branch name is yours, not theirs; it's just spelled the same as one of their names.

As you work with your clone—a different repository—you can add more and more branches to it. If you add to it all the same branches that are in the original repository, you now have all of their commits and all of their branch names (as your own branches, mind you). But until then, you just have all of their commits. That's fine, because Git isn't about branches. Git is about commits.


1The precise description is much more complicated than this, but thinking of it as "copy all of their commits and none of their branches" will get you started.


I tried to solve the issue by cloning the branch inside a new folder (typed git clone -b) and now only the branch that I cloned is appearing..

When you make a new clone—which, again, is a new repository, where you get all of the previous repository's commits but none of its branches yet—the last step of the git clone command is to run a git checkout or git switch command2 that makes one branch. The -b flag exists so that you can tell your Git which of their branch names to copy, as the last step. If you omit the -b flag, your Git asks their Git repository—the one you're cloning—which branch they recommend. But either way you get only one branch.

You don't actually need any branch names to do work in Git. You do need some kind of name, though, and branch names are the best kind of name here. That's why your Git makes one name at the end of the git clone process. Each name you make gives you one more thing to work with.

To understand what's going on, read on. If you're satisfied that your immediate question has been answered, you can stop here.


2The git switch command was first added in Git version 2.23, to split up the overly-complicated git checkout command into two separate commands, git switch and git restore. The existing git checkout remains; you can use it instead of the two new, simpler commands. The new simplified commands are in a sense safer, though: the git switch command tries to be very safe, as does the half of git checkout that it copied. The git restore command, however, is deliberately unsafe in that it will irrevocably destroy work; it copies the other half of git checkout. So if you use git checkout, you can accidentally invoke the "destroy my work" half when you think you're invoking the "safely do stuff" half.


Git is all about commits

To understand what Git is doing here and why it does this like this, start with the fact that Git itself is really all about commits. It's not about branches, though branch names help you (and Git) find commits. It's not about files, though commits contain files. It's really about the commits: everything else Git does is in service of retaining and adding commits. The commits are where things start, and are the purpose of everything else.

This means it's crucial to understand what a commit is, how you name a particular commit, and how you make a new commit. Let's start with the name.

The true name of a commit is its hash ID

You might think that a branch name would name a commit—and it sort of does, but indirectly. In fact, every commit is named by its number. Each commit has a unique number. No other commit can ever have that number: once that commit is made, that number is allocated to that commit. Because that commit takes up that number forever, the number has to be really big, and it is. Currently, each Git commit gets one out of 2160 possible numbers.3 This number gets expressed in hexadecimal as a big ugly string like e31aba42fb12bdeb0f850829e008e1e3f43af500 (this is an actual commit in a Git repository for Git itself).

This number always works: if you have this commit, that is its number, and git show e31aba42fb12bdeb0f850829e008e1e3f43af500 will show it, for instance. You can usually abbreviate the number, to as little as the first four characters if that's unambiguous, so if you have a clone of the Git repository for Git, git show e31aba42fb12bdeb0f850829e008 is almost guaranteed to work. But git show e31a doesn't because it could be short for this commit, or for commit e31a17f741..., for instance. While e31ab works today, as more commits get added, it might stop working.

These numbers look random, but aren't. In fact, each one is a cryptographic checksum of the complete contents of the commit.4 Git does a double-check when extracting any of its internal objects, including commits, that the checksum still matches, so as to detect storage failures: you tell Git to find a commit (or other object) by hash ID and it checks that the hash ID still matches. So this in turn means that no part of any commit—or any of Git's other internal objects—can ever change, either. You can make new ones, each of which gets a new and different ID, but this does not affect the existing ones, which remain in the repository.


3There are plans to redo the numbering system to use 2256 numbers, with some kind of ugly transition.

4In fact, all of Git's internal objects use this scheme. This means all saved objects are frozen for all time. This is how Git freezes and de-duplicates file contents, for instance.


What's in a commit

Now that we know one—and the deepest, as it were—way to look up a commit, by its hash ID, it's time to look at what's inside each commit. Each commit has two parts:

  • A commit holds a full snapshot of all your files. This is the main data of most commits (and usually also the bulk of the repository). Each file is stored as an internal blob object, using this same hash-name-encoding trick. This automatically de-duplicates files, so that if you make a hundred commits in a row that mostly re-use most of their files, they don't really take any extra space.

  • Each commit also holds some metadata, or information about the commit itself: who made it, when, and why, for instance. The "why" part is your log message: your own explanation to yourself and/or others later. Why is this commit better than the last one? Or at least, why it is any different, if it's not necessarily better. The goal of this particular commit might be to fix some bug, or add some new feature, or make something ready to add a new feature, or whatever. The commit itself has the updated source code, but not necessarily anything about the bug that the update is supposed to fix. This is your chance to explain that.

There's a piece of the metadata that Git generates for you, and then uses later, that you rarely see directly, and that is this: Each commit holds the raw hash ID of its immediate predecessor commit. This strings commits together, backwards, into a chain of commits that ends with the latest commit.

We can draw this. Imagine we have a repository with just three commits in it. Instead of real hash IDs, we'll use single uppercase letters to stand in for the commits. The very first commit will be A, the next will be B, and the third commit is commit C:

A <-B <-C

Since commit C is the last one, it has earlier commit B's hash ID in its metadata. We say that C points to B. By the same token, commit B points to A. Since A is the very first commit ever made, it lacks this backwards-pointing arrow: it does not point anywhere. Git calls this a (or the) root commit. It's where we get to stop working backwards.

I mentioned just a moment ago that each commit has a full snapshot of every file. But if you have Git show a commit, Git shows you what changed. How and why does Git do this?

The why is perhaps the easiest to explain. If you want to see all the files that are in the commit, you can just check out the commit. Git will copy all those files out of the commit—where, remember, they're stored in a special frozen Git format, de-duplicated (and compressed too)—to regular ordinary computer files. You probably have a bunch of file viewers that are more competent than Git could ever be: they can show you images as images, open textual documents in text editors, open PDFs with PDF viewers, and so on. But your file-viewer probably can't compare the entire snapshot with the previous entire snapshot. Git can.

Git can compare snapshot C against snapshot B pretty easily, because commit C holds commit B's hash ID. So Git can just extract both commits. Moreover, because of the way Git de-duplicates files, Git can immediately know—and not even bother extracting—the duplicated files. Git needs only to extract and compare the different files. Git will do that, and will construct a set of changes that will turn the old files into the new ones. That's what Git will show you: this set of instructions.

(Note that Git creates the set of instructions on demand. Until you ask Git to compare any two commits, all Git has are the two snapshots. You can get different sets of instructions based on options you pass to the comparison command. For instance, Git can do the difference-checking based on words, or otherwise ignore certain kinds of white-space changes. Git's abilities here are not always as good as we might like, but there are some tricks we can use. They're out of scope for this particular answer, though.)

Finding commits by branch names

We already know that if we memorize the big ugly hash IDs (or write them down), we can use those to find commits. But this is ridiculous. We have a computer. Why don't we have the computer write down the hash IDs for us?

This is what a branch name does. But it's a little sneaky. What a branch name really does is store just the last commit's hash ID. Let's draw that three-commit repository again, and add a name, main, that identifies the last commit:

A--B--C   <-- main

Here, instead of trying to remember C's hash ID, we just know that the name main does that for us. So git checkout main (pre-2.23 Git) or git switch main (2.23 and later) gets us the latest commit—currently C—no matter what hash ID it has.

We can now add a new name that also points to commit C:

A--B--C   <-- main, dev

Now we need one more thing: which of these names are we using? Right now, it doesn't matter much, because both names select commit C. But let's attach the special name HEAD to one of the two branch names, like this:

A--B--C   <-- main (HEAD), dev

If we git switch dev, we're re-attaching the special name HEAD to the name dev, like this:

A--B--C   <-- main, dev (HEAD)

Now let's make a new commit. Without worrying about how we make a new commit, let's just assume that it's all done. This new commit D will, necessarily, point back to existing commit C, because we made D from C. So that looks like this:

A--B--C
       \
        D

But D is now the latest commit, so Git has to update a name. Which name should it update? The answer is clear: it should update the one that HEAD is attached-to:

A--B--C   <-- main
       \
        D   <-- dev (HEAD)

We now have two branch names, and the two names specify two different "latest" commits. The latest commit on main is C, and the latest commit on dev is D. Commit D points back to commit C, which points back to B, which points back to A; so all four commits are on branch dev, while three of them are on main.

If we switch back to the name main and make new a new commit there, we get:

        E   <-- main (HEAD)
       /
A--B--C
       \
        D   <-- dev

which means we now have three commits that are shared on both branches, and one commit that is only on main and one commit that is only on dev. Now we need both names to find all five commits; one name will find one commit, which will find the three shared commits, but we need the other name to find the last remaining commit.

Note that the branch names move. In fact, they move automatically, as we make new commits: whichever branch name has HEAD attached to it automatically moves to encompass the new commit. All the other branch names stay in place at that point, but because they are our branch names, we are in control. We can have our Git move those names any time we like. The only constraint is that we have to have a commit to move the name to.

Cloning creates remote-tracking names

When we clone someone else's repository, we get all their commits and none of their branches. How does this work? Well, suppose we have the above, with two actual branch names main and dev selecting commits E and D respectively. We now make a new repository where we copy all five commits, giving us:

        E
       /
A--B--C
       \
        D

We do actually need two names to find all the commits. But we don't need branch names. The other Git, working with the other repository, has branch names, because those are his branches that he'll move around as he makes new commits. So what our Git does is copy their names but change them. We have our Git take their branch names and create our remote-tracking names, by adding something—usually origin/—to the names.5 So we get:

        E   <-- origin/main
       /
A--B--C
       \
        D   <-- origin/dev

Git will refuse to attach the special name HEAD to one of these remote-tracking names. HEAD is only allowed to attach to a branch name. So the last step of our git clone is to use the -b option, or their recommendation, to pick one of these two names, and create a branch name from it, like this:

        E   <-- main (HEAD), origin/main
       /
A--B--C
       \
        D   <-- origin/dev

Note that our branch name selects the same commit as the remote-tracking name that our git clone made from their branch name. But we now have only one branch name, not two. If we run:

git switch dev

this uses a special feature that Git provides, that finds their origin/dev and creates our own new name dev:

        E   <-- main, origin/main
       /
A--B--C
       \
        D   <-- dev (HEAD), origin/dev

and now we have two branch names. But we didn't initially. Note that we also now have commit D checked out, rather than commit E, because git switch (or git checkout, if we use that) not only switches branches, but also selects the commit that the branch name identifies, as the commit that is to be checked-out, and therefore available to us to work with.


5Technically, a remote-tracking name is in a separate namespace. Our Git doesn't just tack origin/ in front, it replaces refs/heads/ with refs/remotes/origin/. The name origin is actually a remote and we can have as many remotes as we like in our Git repository. But this is a topic for another question.


Notes:

  • you can clone a specific branch with git clone --branch <branch> --single-branch
  • you can have multiple worktrees (without having to copy the whole folder) with git worktree

For branches, use git branch -avv to get a list of all local and remote branches.
Then try again your copy, and compare git branch -avv when done in the new copied folder: if a remote branch is missing, a simple git fetch will be enough.


In order to make sure you have all the up to date information on branches from Github (your remote), you can do a git fetch:

git fetch --all

Where the --all flag fetches branches from all remotes. If you would just like to see all the branches (on your machine and on your GitHub), you can do a git branch:

git branch -av

Where -a shows branches from local and remotes, and -v gives more verbose output.