git pull
TL;DR: The git pull command is a high-level composite command that combines two steps: git fetch (downloading the latest changes from a remote repository) and git merge (or git rebase, depending on configuration) to integrate those changes into your current local branch. It modifies both the local database and your active working directory files in one execution.
What Does git pull Actually Do?
In Git, git pull is categorized as a “porcelain” command—a user-facing tool designed for convenience. Under the hood, it invokes “plumbing” and low-level porcelain commands to synchronize your active work branch with a remote counterpart.
To understand its necessity, consider a team environment. Multiple developers push changes to a central repository (referred to as origin via /git-remote). Your local repository does not automatically sync. If you want your current files to reflect the team’s latest code, you must execute two operations:
- Download the new history database: Connect to the remote, scan for new commits, download the Git object packfiles, and update the remote tracking reference (e.g.,
origin/main). This is handled by /git-fetch. - Apply those commits locally: Update your current checked-out branch pointer, merge the differences, resolve conflicts if they exist, and write the modifications to your workspace files. This is handled by /git-merge (or /git-rebase).
git pull combines these steps into a single, automated command.
+------------------+
| git pull command |
+------------------+
|
+-----> 1. git fetch (Download commits to objects/, update origin/main ref)
|
+-----> 2. git merge (Integrate origin/main into local active branch)
The Developer’s Debate: Pull vs. Fetch-Merge
Many senior software developers avoid git pull in favor of running the two steps manually. The rationale is simple: control.
If you run git pull, Git will immediately attempt to integrate the incoming commits. If you have uncommitted changes or if your history has diverged, this can trigger automatic merge commits (cluttering your git log) or spawn unexpected merge conflicts.
By running git fetch first, you can safely inspect the incoming history using git log or git diff before deciding whether to merge, rebase, or stash your current work using /git-stash.
What Happens Inside .git/ (Hands-on exploration)
Let’s dissect what happens to your Git directory during a git pull operation under different scenarios.
Step 1: Baseline Setup
We will initialize a local repository, write an initial file, and configure our repository state.
$ mkdir pull-demo
$ cd pull-demo
$ git init
Initialized empty Git repository in /home/admin/Code/pull-demo/.git/
$ echo "Initial Line" > document.txt
$ git add document.txt
$ git commit -m "C1: Initial commit"
[main (root-commit) 5a1b2c3] C1: Initial commit
Let’s check the commit hash and references:
$ git show-ref --heads
5a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b refs/heads/main
Our local main branch points to commit 5a1b2c3....
Now, we set up a mock remote tracker:
$ git remote add origin https://github.com/developer/repo.git
Step 2: Scenario A - Fast-Forward Pull
Suppose the remote repository has advanced. A colleague pushed commit 8c9d0e1... (which updates document.txt). We have made no local commits since 5a1b2c3....
When we run git pull, Git executes git fetch origin first. This writes the new commit 8c9d0e1... into our object database, and creates the remote-tracking pointer:
# Simulating the fetch step:
$ mkdir -p .git/refs/remotes/origin
$ echo "8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d" > .git/refs/remotes/origin/main
At this moment, our local database contains the new commit hash, and the remote ref points to it:
refs/remotes/origin/main->8c9d0e1...refs/heads/main->5a1b2c3...
Now, Git executes the second phase: git merge origin/main. Because the commit history is a straight line, Git determines this is a fast-forward merge.
C1 [5a1b2c3] (local main) ---> C2 [8c9d0e1] (origin/main)
To complete the merge, Git:
- Rewrites the file
.git/refs/heads/mainto contain the hash of the remote commit:8c9d0e1... - Updates the staging area index file (
.git/index). - Updates the physical file
document.txtin the working directory.
Let’s simulate the ref update:
$ echo "8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d" > .git/refs/heads/main
If we run git log, the history is straight and clean, with no merge commit:
$ git log --oneline
8c9d0e1 C2: Remote updates to document.txt
5a1b2c3 C1: Initial commit
Step 3: Scenario B - Three-Way Merge (Diverged History)
Now let’s examine what happens when history has diverged.
- We make a local commit
Local Commit (C3)pointing to8c9d0e1.... - Meanwhile, someone else pushed a commit
Remote Commit (C4)pointing to8c9d0e1...to the remote repository.
/---> C3 [Local Commit] (refs/heads/main)
/
C1 ---> C2 [8c9d0e1]
\
\---> C4 [Remote Commit] (refs/remotes/origin/main)
Let’s write these commit objects to simulate the state before the merge step of the pull.
First, the remote commit C4:
$ C4_CONTENT=$(printf "tree 3a2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b\nparent 8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d\nauthor Colleague <colleague@dev.com> 1774345200 +0000\ncommitter Colleague <colleague@dev.com> 1774345200 +0000\n\nC4: Remote work")
$ C4_HASH=$(echo "$C4_CONTENT" | git hash-object -w --stdin -t commit)
$ echo "$C4_HASH" > .git/refs/remotes/origin/main
Next, our local commit C3:
$ C3_CONTENT=$(printf "tree 5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e\nparent 8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d\nauthor Developer <dev@company.com> 1774345300 +0000\ncommitter Developer <dev@company.com> 1774345300 +0000\n\nC3: Local work")
$ C3_HASH=$(echo "$C3_CONTENT" | git hash-object -w --stdin -t commit)
$ echo "$C3_HASH" > .git/refs/heads/main
Now we run git merge refs/remotes/origin/main (which is what git pull triggers). Since our histories have diverged, Git cannot fast-forward. It performs a 3-way merge.
It compares C3 (local main) and C4 (remote tracking branch) against their common ancestor C2 (8c9d0e1...).
Assuming there are no file conflicts, Git automatically creates a new Merge Commit (let’s call it C5). A merge commit has two parent pointers instead of one.
Let’s examine how Git structures the merge commit object:
# Git creates a merge commit object containing:
# parent 1: C3 (local)
# parent 2: C4 (remote)
$ MERGE_COMMIT_CONTENT=$(printf "tree 7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e\nparent $C3_HASH\nparent $C4_HASH\nauthor Developer <dev@company.com> 1774345400 +0000\ncommitter Developer <dev@company.com> 1774345400 +0000\n\nMerge branch 'main' of https://github.com/developer/repo")
$ MERGE_HASH=$(echo "$MERGE_COMMIT_CONTENT" | git hash-object -w --stdin -t commit)
# Point local main to the new merge commit
$ echo "$MERGE_HASH" > .git/refs/heads/main
Let’s use the plumbing tool git cat-file -p to inspect our new HEAD commit:
$ git cat-file -p HEAD
tree 7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e
parent d4a8f921bc823ef08341df90e38a29a009d123ea
parent a76b89c0de98543f09854721eb0c897f2122ab34
author Developer <dev@company.com> 1774345400 +0000
committer Developer <dev@company.com> 1774345400 +0000
Merge branch 'main' of https://github.com/developer/repo
Observe the double parent lines! That is the defining signature of a merge commit in Git. The local tracking branch refs/heads/main is updated to point to this new merge commit.
Detailed Command Options & Advanced Usage
1. git pull --rebase
By default, when history diverges, git pull creates a merge commit. This can lead to a messy, branched history log commonly called a “merge bubble”.
To prevent this, you can configure the pull to use git rebase:
git pull --rebase
When you pull with rebase:
- Git runs
git fetchto download changes and moveorigin/main. - Git temporarily “shelves” (saves in memory) your local commits (
C3). - Git moves your local branch pointer
refs/heads/mainforward to matchorigin/main(C4). - Git reapplies (re-commits) your shelved commits (
C3) one by one on top of the new parent commit.
Before pull:
/---> C3 [Local Commit] (main)
/
C1 ---> C2 ---> C2
\
\---> C4 [Remote Commit] (origin/main)
After pull --rebase:
C1 ---> C2 ---> C2 ---> C4 (origin/main) ---> C3' [Re-applied Commit] (main)
Your history remains perfectly linear. You can set this as your global default:
git config --global pull.rebase true
2. git pull --ff-only
If you want to protect your repository from accidental merge commits or rebases during a pull, use the fast-forward only flag:
git pull --ff-only
This tells Git: “Download the updates. If I can fast-forward my local branch pointer to match the server, do so. If history has diverged, do NOT perform a merge or rebase. Instead, abort the operation immediately.”
This gives you a chance to inspect the differences and handle them manually.
3. Automatically Stashing: git pull --autostash
If you have uncommitted changes in your working directory and try to pull, Git will abort if the incoming changes overlap with your uncommitted files:
error: Your local changes to the following files would be overwritten by merge:
document.txt
Please commit your changes or stash them before you merge.
Aborting
Instead of manually running git stash, pulling, and running git stash pop, you can tell Git to automate this:
git pull --rebase --autostash
This automatically stashes your uncommitted changes, performs the fetch and rebase, and pops your stash back onto the working directory.
Real-World Scenario: Handling a Diverged Sync
Imagine you are working on a team branch named feature-payments. You have been writing commits locally, while your teammate has pushed updates to the same branch on GitHub.
Here is the exact progression to bring your workspace up to date:
Step 1: Check Repository Status
Make sure your working directory is clean before syncing:
$ git status
On branch feature-payments
nothing to commit, working tree clean
Step 2: Attempt a Clean Fast-Forward Pull
Try running a fast-forward only pull to see if you can update without creating history loops:
$ git pull --ff-only
fatal: Not possible to fast-forward, aborting.
The error tells you that history has diverged (both you and your teammate have committed changes).
Step 3: Run Rebase Pull
To maintain a clean, linear project history, pull and rebase your local commits on top of your teammate’s commits:
$ git pull --rebase
Updating 5a1b2c3..8c9d0e1
First, rewinding head to replay your work on top of it...
Applying: Add checkout billing fields
Using index info to reconstruct a base tree...
M document.txt
Falling back to patching base and 3-way merge...
Auto-merging document.txt
CONFLICT (content): Merge conflict in document.txt
error: Failed to merge in the changes.
Patch failed at 0001 Add checkout billing fields
Step 4: Resolve the Conflict
The rebase has paused because both of you modified the same file. Open document.txt in your editor. You will see Git’s conflict markers:
<<<<<<< HEAD
Remote changes made by your colleague
=======
Local changes made by you
>>>>>>> Add checkout billing fields
Edit the file to merge the changes, remove the conflict markers, and save the file.
Step 5: Continue the Rebase
Stage the resolved files and instruct Git to resume the rebase process:
$ git add document.txt
$ git rebase --continue
Applying: Add checkout billing fields
Successfully rebased and updated refs/heads/feature-payments.
Your local history is now updated, clean, and ready to be pushed to the remote repository.
Common Pitfalls and Mistakes
1. The “Merge Bubble” Mess
- The Problem: Git logs are full of redundant, empty commits reading
"Merge branch 'main' of github.com:org/repo". - The Cause: Running default
git pullon a diverged branch. Git performs a 3-way merge and automatically writes a merge commit. - The Fix: Switch to rebase pulls by default:
git config --global pull.rebase true.
2. Pulling into the Wrong Local Branch
- The Problem: You checked out a branch named
bugfix-auth. You rungit pull origin mainintending to update your local main, but you notice your bugfix branch suddenly contains dozens of unrelated commits. - The Cause:
git pulldownloads remote history and integrates it directly into the currently checked out branch. Runninggit pull origin mainfetched remotemainand merged it into your activebugfix-authbranch. - The Fix: Undo the accidental merge immediately using the Git Reflog:
# Check the reflog to find the commit before the merge git reflog # Reset your branch to the pre-merge commit git reset --hard HEAD@{1}
3. Dirty Working Directory Block
- The Problem: Running
git pullaborts with a message stating that local changes would be overwritten. - The Cause: You have uncommitted modifications in files that the pull needs to update.
- The Fix: Use
git stashto clear your workspace, run the pull, and then restore your work:git stash git pull --rebase git stash pop
Command Quick Reference
| Command | Merge Strategy | Conflict Potential | Notes |
|---|---|---|---|
git pull | 3-way Merge (Default) | High | Creates a merge commit if branches have diverged. |
git pull --rebase | Rebase | High | Rewrites local commits on top of remote commits. Keeps history linear. |
git pull --ff-only | Fast-Forward Only | None | Aborts if branches have diverged. Safest option. |
git pull --autostash | Merge / Rebase | High | Automatically stashes and restores local changes during sync. |
git pull --no-commit | Merge | High | Performs the fetch and merge, but pauses before writing the merge commit. |
FAQ (People Also Ask)
What is the difference between git fetch and git pull?
git fetch is a network-only command that downloads commits and updates your remote tracking branches (like origin/main). It does not change your working files. git pull is a dual command: it runs git fetch to download the commits, and then immediately runs git merge or git rebase to integrate those changes into your active local working branch.
How do I undo a git pull that messed up my code?
If the pull resulted in an unwanted merge or rebase, you can undo it using the Git reflog, which keeps a history of your local branch pointer changes.
- Run
git reflogto view recent actions. Find the line before the pull occurred (e.g.,HEAD@{1}: pull: Fast-forward). - Run
git reset --hard HEAD@{1}(replace1with the index of the pre-pull state). This resets your files and commits back to that state.
How do I force git pull to overwrite my local files?
If you want to discard your local commits and local uncommitted work entirely, replacing them with the exact state of the remote branch, do not use git pull. Instead, fetch the remote and reset your branch pointer:
git fetch origin
git reset --hard origin/main
Warning: This permanently deletes all local changes and commits that have not been pushed to the remote.
Why does git pull say “fatal: refusing to merge unrelated histories”?
This occurs when you attempt to merge two repositories that do not share a common ancestor commit (for example, if you created a new local repository with commits, created a new GitHub repository with a README commit, and tried to pull). You can override this safety block by running:
git pull origin main --allow-unrelated-histories
Test Your Knowledge
Loading quiz…