1. Introduction

As a complete versioning system, Git offers the ability to resolve conflicts between object versions. This includes branches, commits, and any other ref (reference). However, depending on the context, we might experience issues when attempting to merge or perform other operations that involve a given ref.

In this tutorial, we explore Git errors that involve ref problems. First, we check a basic decision tree for troubleshooting problems with seemingly missing refs (references). After that, we turn to the most basic reason. Next, we discuss refs, remotes, and how they get synchronized. Along the way, we create local and remote repositories to build an example. This leads to the topic of full and partial synchronization and one of the main reasons for missing refs.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15 and Git 2.39.2. Unless otherwise specified, it should work in most POSIX-compliant environments.

2. Common Reasons for Missing Refs (References)

There are two main reasons for ref problems:

  • ref doesn’t exist locally
  • ref name is incorrect (typo)

In the first case, a ref might not have been pulled from or pushed to the remote repository.

So, let’s create a basic chart for troubleshooting ref problems:

Missing Git references troubleshooting

Armed with this knowledge, we can first tackle the seemingly more complex but in reality fairly easier issue.

3. Incorrect Ref Names

Bash autocompletion, including that for Git, can suggest corrections for incomplete or similar-looking subcommands, options, and flags.

However, software can’t be clairvoyant. Especially when it comes to custom names, few implementations can recognize the input of the wrong branch, tag, or other object name.

For instance, let’s see the main causes for mistaken names:

  • basic typos in the form of missing or mistyped characters
  • extra characters, which can be regular, control, and other hidden
  • incorrect character set such as a Cyrillic с instead of a Latin C
  • wrong encoding, e.g., using Emoji characters in an ASCII-only setting

Beyond the obvious manual checks, one way to avoid such issues is to use Git autocomplete for branch names as well:

$ git branch br[Tab]anch[Tab]
branch1   branch2

As usual, another way is to copy and paste. Further, online or offline encoding checkers can display unexpected special characters.

Similarly, we can leverage the cat command with its –show-all (-A) flag:

$ printf 'CС' | cat --show-all
CM-PM-!

In this case, we can see that the two similar-looking characters aren’t the same.

4. Refs and Remotes

When it comes to Git refs (references), there are three main kinds:

These refs are mainly for convenience, so we don’t have to use commit identifiers or trace the last commit of a given branch manually.

Let’s look at a simple repository commit tree:

$ git log --all --decorate --oneline --graph
* fee1bab (branch1) branch feature
| * 349648c (tag: v0.1, branch2) secondary feature WIP
| * 5d3772e secondary branch modifications
|/
| * 536f16d (branch3) experimental
|/
* f30c039 branch modifications
| * d8c10fe (HEAD -> master) new structure
| * 19649bf wipeout
| * b0d6c31 major modifications
| * 3ce9677 minor modifications
|/
* f721f76 restructuring
* 6cb76f5 structuring
* bb75656 init commit

Now, we turn to its references:

$ tree .git/refs/
.git/refs/
├── heads
│   ├── branch1
│   ├── branch2
│   ├── branch3
│   └── master
└── tags
    └── v0.1

3 directories, 5 files

Notably, the repository contains four heads, i.e., branch-leading commits. Further, we have a single entry in the tags category.

Now, let’s add a remote and push to it:

$ git remote add origin ../remote/
$ git push --set-upstream origin master
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
Delta compression using up to 4 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (21/21), 16.66 KiB | 666.00 KiB/s, done.
Total 21 (delta 0), reused 0 (delta 0), pack-reused 0
To ../remote/
 * [new branch]      master -> master
branch 'master' set up to track 'origin/master'.

Notably, only the master primary branch got synchronized for the origin remote:

$ tree .git/refs
.git/refs
├── heads
│   ├── branch1
│   ├── branch2
│   ├── branch3
│   └── master
├── remotes
│   └── origin
│       └── master
└── tags
    └── v0.1

5 directories, 6 files

Let’s check the refs in the remote repository:

$ tree ../remote/.git/refs
../remote/refs
├── heads
│   └── master
└── tags

3 directories, 1 file

Critically, we only see the master ref.

5. Remote Ref Synchronization

At this point, let’s create a new repository in another subdirectory and add ../remote/ as its origin as well:

$ git init && git remote add origin ../remote/

Now, we perform a pull:

$ git pull
remote: Enumerating objects: 21, done.
remote: Counting objects: 100% (21/21), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 21 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (21/21), 16.66 KiB | 666.00 KiB/s, done.
From ../remote
 * [new branch]      master     -> origin/master
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/<branch> master

Thus, we only get the remote references:

$ tree .git/refs
.git/refs
├── heads
├── remotes
│   └── origin
│       └── master
└── tags

5 directories, 1 file

That’s why Git expects us to indicate the link between the remote master and the local repository in the initial pull output.

Since we don’t have a local branch, we perform a more explicit pull to replicate the remote branch:

$ git pull origin master
From ../remote
 * branch            master     -> FETCH_HEAD

Let’s verify the current refs:

$ tree .git/refs/
.git/refs/
├── heads
│   └── master
├── remotes
│   └── origin
│       └── master
└── tags

5 directories, 2 files

Although we now have the link between the local and remote master, the rest of the original reference structure, such as other branches, isn’t available.

Let’s see why this is critical.

6. Missing Ref Errors

At this point, we went through several steps:

  1. create a local repository with master, branch1, branch2, and branch3
  2. default-push from the local repository to a remote repository (only primary branch pushed)
  3. pull from the remote repository to an empty local repository (only primary branch pulled)

In summary, we have two local repositories, linked with one remote repository.

6.1. Branch Creation

Let’s create a new branch in the second local repository, which currently only contains master:

$ git branch branch1

Next, we check out branch1 and perform some changes:

$ git checkout branch1
Switched to branch 'branch1'
$ echo 'branch1 data' >> file
$ git commit --all --message='testing branch1'
[branch1 65b74d1] testing branch1
 1 file changed, 1 insertion(+)

Then, we can attempt some ref operations.

6.2. Merge Attempt

First, let’s attempt to merge the remote branch1 with the local one:

$ git merge origin/branch1
merge: origin/branch1 - not something we can merge

As expected, we get the not something we can merge error, since we haven’t pulled branch1 from origin.

6.3. Pull Attempt

So, let’s try to get branch1 from origin:

$ git pull origin branch1
fatal: couldn't find remote ref branch1

Of course, there’s currently no remote origin branch called branch1, so we see the error fatal: couldn’t find remote ref.

6.4. Branch Checkout Attempt

We can verify that a branch doesn’t exist locally as well by trying a checkout on it:

$ git checkout branch2
error: pathspec 'branch2' did not match any file(s) known to git

The did not match any file(s) known to git error is common when we attempt to use unknown object names.

6.5. Push From Initial Repository

To get branch1 and other branches, we need to push all of them from the original repository:

$ git push --all
Enumerating objects: 17, done.
Counting objects: 100% (17/17), done.
Delta compression using up to 4 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (15/15), 56.66 KiB | 666.00 KiB/s, done.
Total 15 (delta 5), reused 0 (delta 0), pack-reused 0
To ../remote/
 * [new branch]      branch1 -> branch1
 * [new branch]      branch2 -> branch2
 * [new branch]      branch3 -> branch3
$ git push --tags
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To ../remote/
 * [new tag]         v0.1 -> v0.1

As we can see, –all is the key to push all branch references, while –tags ensures we also send the tags.

Let’s verify the results in the remote repository:

$ tree ../remote/refs/
../remote/refs/
├── heads
│   ├── branch1
│   ├── branch2
│   ├── branch3
│   └── master
└── tags
    └── v0.1

3 directories, 5 files

So, the remote now has all the references and data.

6.6. Pull From Secondary Repository

Going back to the secondary local repository, let’s perform a complete pull of all data:

$ git pull --all
remote: Enumerating objects: 19, done.
remote: Counting objects: 100% (19/19), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 15 (delta 5), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (15/15), 1.01 KiB | 259.00 KiB/s, done.
From ../remote
 * [new branch]      branch1    -> origin/branch1
 * [new branch]      branch2    -> origin/branch2
 * [new branch]      branch3    -> origin/branch3
 * [new tag]         v0.1       -> v0.1
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/<branch> branch1

In this case, we also get the tag.

However, branch1 is now a problem, since we had it before the pull.

To remedy this, we can set the upstream link:

$ git branch --set-upstream-to=origin/branch1 branch1
branch 'branch1' set up to track 'origin/branch1'.

At this point, we can verify the secondary local repository references:

$ tree .git/refs/
.git/refs/
├── heads
│   ├── branch1
│   └── master
├── remotes
│   └── origin
│       ├── branch1
│       ├── branch2
│       ├── branch3
│       └── master
└── tags
    └── v0.1

5 directories, 7 files

To get a branch in the local heads, we can just perform a checkout:

$ git checkout branch2
branch 'branch2' set up to track 'origin/branch2'.
Switched to a new branch 'branch2'

Let’s see the result:

$ tree .git/refs/
.git/refs/
├── heads
│   ├── branch1
│   ├── branch2
│   └── master
├── remotes
│   └── origin
│       ├── branch1
│       ├── branch2
│       ├── branch3
│       └── master
└── tags
    └── v0.1

5 directories, 8 files

Finally, we can test that all objects are usable.

6.7. Verification

We should now be able to merge any branch:

$ git merge origin/branch3
Auto-merging file
[...]

Even branch3, which we haven’t checked out locally, can be used for a merge at this point.

7. Summary

In this article, we talked about problems with branch, tag, and other ref names in Git.

In conclusion, it’s relatively common to get the name of a Git object wrong, but the root cause can sometimes be more complex.