Demystifying Git: What Actually Happens When You Pull?
If you use Git daily, you have likely run into one of these errors when attempting to synchronize your code:
fatal: Need to specify how to reconcile divergent branches.fatal: The current branch has no upstream branch.
To a beginner, these warnings feel like Git is complaining about broken code. To an experienced developer, they are simple reminders of how Git’s internals work.
Let’s unpack what is actually happening behind the scenes when you run these commands, and look at the single file where Git stores all this information.
The git pull Misconception
The most common misunderstanding in Git revolves around this command:
git pull origin feature-branchIf you are currently working on your local main branch, you might run this command thinking: “I want to update my local feature-branch with whatever is on GitHub.”
Instead, Git throws a warning about divergent branches. Why?
Under the hood, git pull is actually a combination of two separate commands:
git fetch: Go to GitHub, findfeature-branch, and download its latest commits.git merge: Take those downloaded commits and merge them into the branch you are currently on.
Because you were on main, Git tried to merge the downloaded feature-branch commits directly into main. Since main and feature-branch have different commit histories (they have “diverged”), and because you haven’t configured a default merge strategy, Git aborts the action to prevent a messy conflict.
The rule of thumb: Git can only update the files and pointer of the branch you currently have active. If you want to update a branch, check it out first:
git checkout feature-branch
git pullHow Git Remembers: Under the Hood of .git/config
Where does Git keep track of which local branch goes to which remote branch?
If you open the hidden directory at the root of any Git project and look at the .git/config file, you will find a plain text configuration file. Here is a typical example:
[remote "origin"]
url = [email protected]:username/repository.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/mainWhen you create a brand new branch locally (e.g., git checkout -b new-feature), it exists only on your machine. The .git/config file has no entry for it.
If you try to run git push or git pull, Git reads this config, sees no [branch "new-feature"] block, and says: “I don’t know where to send/fetch this.”
Once you run:
git push -u origin new-featureThe -u (upstream) flag tells Git to write a new entry to your .git/config file:
[branch "new-feature"]
remote = origin
merge = refs/heads/new-featureFrom that point forward, a simple git push or git pull works seamlessly because Git has a lookup address.
Why Does git remote -v List the Same URL Twice?
If you run git remote -v to check your remote configuration, you will see output like this:
origin [email protected]:username/repository.git (fetch)
origin [email protected]:username/repository.git (push)This output is generated directly from the [remote "origin"] block in your .git/config file. By default, Git assumes you want to pull (fetch) changes from and push changes to the exact same repository.
However, you can configure them separately. For instance, if you want to pull updates from an upstream open-source project but push your changes to your own personal fork, you can configure a separate pushurl in your config:
[remote "origin"]
url = [email protected]:OriginalProject/repo.git
pushurl = [email protected]:YourUsername/repo.gitNow, git remote -v will show different URLs for fetching and pushing.
Wrap Up
Git isn’t magic; it is a collection of pointers, configurations, and commit histories stored locally in your .git folder.
The next time Git complains about divergent branches or missing upstream configuration, remember that it is just asking you two simple questions: “Which branch do you want to merge this into?” and “Where in my config file should I write this link?”