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:

  1. 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.
  2. 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:

  1. Rewrites the file .git/refs/heads/main to contain the hash of the remote commit: 8c9d0e1...
  2. Updates the staging area index file (.git/index).
  3. Updates the physical file document.txt in 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 to 8c9d0e1....
  • Meanwhile, someone else pushed a commit Remote Commit (C4) pointing to 8c9d0e1... 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:

  1. Git runs git fetch to download changes and move origin/main.
  2. Git temporarily “shelves” (saves in memory) your local commits (C3).
  3. Git moves your local branch pointer refs/heads/main forward to match origin/main (C4).
  4. 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 pull on 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 run git pull origin main intending to update your local main, but you notice your bugfix branch suddenly contains dozens of unrelated commits.
  • The Cause: git pull downloads remote history and integrates it directly into the currently checked out branch. Running git pull origin main fetched remote main and merged it into your active bugfix-auth branch.
  • 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 pull aborts 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 stash to clear your workspace, run the pull, and then restore your work:
    git stash
    git pull --rebase
    git stash pop

Command Quick Reference

CommandMerge StrategyConflict PotentialNotes
git pull3-way Merge (Default)HighCreates a merge commit if branches have diverged.
git pull --rebaseRebaseHighRewrites local commits on top of remote commits. Keeps history linear.
git pull --ff-onlyFast-Forward OnlyNoneAborts if branches have diverged. Safest option.
git pull --autostashMerge / RebaseHighAutomatically stashes and restores local changes during sync.
git pull --no-commitMergeHighPerforms 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.

  1. Run git reflog to view recent actions. Find the line before the pull occurred (e.g., HEAD@{1}: pull: Fast-forward).
  2. Run git reset --hard HEAD@{1} (replace 1 with 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…