Push and Pull Safely
Learn how to push and pull changes safely in Git — a hands-on Git Tutorial lesson.
Focus: push and pull changes safely
You've committed your work locally, but your teammate just pushed a breaking change, and now your git push is rejected with a scary error. Or worse, you ran git pull and overwrote your own uncommitted edits. Pushing and pulling changes safely is the difference between a smooth collaboration and a frantic search through the reflog. In this lesson, you'll learn a repeatable workflow that lets you share your work and integrate others' changes without losing a single line of code.
The Problem This Lesson Solves
When you work alone, Git feels like a personal time machine. But the moment you share a repository, remote synchronization becomes a two-way street. The core pain points are:
- Rejected pushes because your local branch has fallen behind the remote.
- Merge conflicts that appear out of nowhere when you pull.
- Lost work when a pull or push overwrites uncommitted changes.
- Fear of the unknown — not knowing what
git pullwill actually do to your working tree.
The default git pull is a shortcut that hides important details. It fetches remote changes and then immediately merges them into your current branch. If you're not careful, that merge can create conflicts or even wipe out local edits. Similarly, git push can be rejected if your history has diverged. Understanding the mechanics behind these commands—and how to control them—turns a risky operation into a predictable routine.
Core Concept / Mental Model
Think of your repository as a shared notebook that everyone on your team writes in. Your local copy is a snapshot, and the remote (like GitHub or GitLab) is the official version that everyone pulls from.
git fetchis like reading the latest entries in the notebook without writing anything yourself. It updates your remote-tracking branches (e.g.,origin/main) but leaves your working tree and local branches untouched.git pullis like reading the new entries and then copying them into your own section — it combinesgit fetchwith a merge (or rebase) of the fetched changes into your current branch.git pushis like submitting your own new pages to the official notebook. If someone else has already added pages, your submission might be rejected until you integrate their changes.
The key to safety is to separate the two steps: fetch first, inspect what's new, then decide how to integrate. This is exactly what git pull --rebase and the --ff-only flag help you do.
How It Works Step by Step
Here's a safe, repeatable workflow for pushing and pulling changes:
Step 1: Fetch and Inspect Before You Pull
Instead of running git pull blindly, run:
git fetch origin
This downloads all new commits from the remote but doesn't touch your working files. You can then inspect what's changed with:
git log HEAD..origin/main --oneline
This shows the commits that exist on origin/main but not on your local branch. If the list is empty, you're up to date. If it's not empty, you decide how to integrate.
Step 2: Choose Your Integration Strategy
- Fast-forward only — If your local branch hasn't diverged (no new commits of your own), you can safely move your branch pointer forward with
git pull --ff-only. This never creates a merge commit and never causes conflicts. - Rebase — If you have local commits that are not on the remote, rebasing replays your commits on top of the remote's history. This keeps a linear history, which is often preferred for clarity.
- Merge — A merge creates a new commit that brings both histories together. This is the default behavior of
git pulland can be used when you want to preserve an explicit merge record.
For most workflows, I recommend git pull --rebase for feature branches and git pull --ff-only for branches you only pull from (like main). Rebase avoids unnecessary merge commits, and --ff-only protects against accidental merges.
Step 3: Push with Safety Checks
Before you push, run a quick check that your local branch is up to date:
git status
git log origin/main..HEAD --oneline
The first command shows if you have uncommitted changes (you shouldn't push those). The second shows the commits that you are about to push. If you see a list of commits, you're ready.
Then push:
git push origin your-branch
If the push is rejected because the remote has new commits, do not force push. Instead, pull with rebase and then push again.
Hands-On Walkthrough
Let's put this into practice with a simple exercise.
Setup
Create a temporary repository and a remote (we'll use a bare repo as a fake remote):
mkdir git-safe-demo && cd git-safe-demo
git init
git config user.email "you@example.com"
git config user.name "Your Name"
echo "Initial content" > file.txt
git add file.txt && git commit -m "Initial commit"
git branch -m main
# Create a bare remote and push the main branch
mkdir ../remote-repo.git
git init --bare ../remote-repo.git
git remote add origin ../remote-repo.git
git push -u origin main
Scenario: Pull with Rebase
Now, pretend a teammate adds a commit to the remote. (In real life, they'd push from their own clone.) We'll simulate it by cloning the bare repo and making a change:
cd ..
git clone remote-repo.git teammate-clone
cd teammate-clone
echo "Teammate line" >> file.txt
git add file.txt && git commit -m "Add teammate line"
git push origin main
Back in your original repo, make a local commit, then pull with rebase:
cd ../git-safe-demo
echo "My local line" >> file.txt
git add file.txt && git commit -m "Add my line"
# Now pull with rebase
git pull --rebase origin main
Git will fetch the remote's new commit, then replay your local commit on top of it. Check the log to see a linear history:
git log --oneline --graph
You should see both commits in a straight line, with your local commit after the teammate's. Push now:
git push origin main
Expected output (abbreviated):
To ../remote-repo.git
a1b2c3d..e4f5g6h main -> main
Scenario: Pull with --ff-only
Now let's see what happens when you try to pull with --ff-only and you have local commits. First, make another local commit:
echo "Another local line" >> file.txt
git add file.txt && git commit -m "Add another local line"
Now simulate another push from the teammate:
cd ../teammate-clone
echo "Teammate second line" >> file.txt
git add file.txt && git commit -m "Add teammate's second line"
git push origin main
Back in your repo, try to pull with --ff-only:
cd ../git-safe-demo
git pull --ff-only origin main
You'll get an error like:
fatal: Not possible to fast-forward, aborting.
This is a safety feature — it stops you from creating a mess. In this case, you should either rebase (to keep a linear history) or merge (to create a merge commit).
# Choose one:
git pull --rebase origin main
# or
git pull --no-rebase origin main
After resolving any conflicts, you can push again.
Compare Options / When to Choose What
| Command / Strategy | Result | Best used when | Conflict risk |
|---|---|---|---|
git pull --ff-only |
Fast-forward, no merge commit | You have no local commits on that branch | None |
git pull --rebase |
Linear history, replays local commits | You have local commits and want clean history | Low if no overlapping changes |
git pull (default merge) |
Creates a merge commit | You want a record of the merge, or you're on a shared branch like main |
Medium |
git push (plain) |
Pushes your commits if possible | Any time you have commits to share | None if up to date; rejection if remote has new commits |
git push --force-with-lease |
Overwrites remote, but only if no one else pushed | Recovering from a bad rebase, on a personal feature branch | High — use with extreme care |
Key takeaways from the table:
- Prefer --ff-only for branches you don't commit to (like main).
- Prefer --rebase for feature branches to keep history clean.
- Avoid --force at all costs; use --force-with-lease only when absolutely necessary and only on your own branches.
Troubleshooting & Edge Cases
"Push rejected: Fetch first"
This happens when the remote has commits you don't have. Fix: run git pull --rebase origin branch-name and then push again. Never force-push to fix this.
"Merge conflict during pull"
If your changes and the remote's changes affect the same lines, Git can't auto-merge. Fix: open the conflicted files, look for <<<<<<<, =======, >>>>>>> markers, edit to keep the correct content, then git add the file and git commit (or git rebase --continue if you were rebasing).
"Uncommitted changes would be overwritten by pull"
Git refuses to pull if it would clobber your uncommitted work. Fix: either commit your changes (git commit -am "WIP"), stash them (git stash), or discard them (git checkout -- file). The safest is to stash and then pop after the pull:
git stash
git pull --rebase origin main
git stash pop
"I force-pushed by accident"
If you used git push --force and lost commits, immediately run git reflog to find the lost commit hash, then git reset --hard <hash> to restore. This is the reflog escape hatch referenced in the track description.
What You Learned & What's Next
You now know how to push and pull changes safely by:
- Fetching before you pull to inspect what's new.
- Choosing
--ff-onlyor--rebaseto avoid messy merges. - Avoiding
--forceand using--force-with-leaseonly as a last resort. - Handling merge conflicts and uncommitted changes proactively.
These skills directly support your ability to collaborate in a distributed environment. Next in this track, you'll dive into branching models — how to structure feature branches, release branches, and hotfix branches to make pushing and pulling even more predictable. You'll also explore merge strategies to resolve conflicts efficiently when they do happen.
Keep practicing: try the rebase workflow on your next feature branch, and get comfortable with git fetch as your first move when you sit down to work.
Practice recap
Create a second clone of a test repository, make conflicting changes in both, and practice pulling with --rebase until you can resolve conflicts comfortably. Then try git pull --ff-only and see how it fails when you have local commits — that's your safety net.
Common mistakes
- Running
git pullwithout checking your local status first — you might overwrite uncommitted changes or create an unexpected merge commit. - Using
git push --forceto get past a rejected push — this can erase teammates' commits. Instead, pull with--rebaseand then push. - Forgetting to fetch before pulling, so you assume your local branch is up to date when it isn't.
- Panicking when a merge conflict appears — just resolve the markers, add the file, and finish the merge or rebase.
Variations
- Some teams prefer
git pull --rebaseby default — you can set it in your Git config withgit config --global pull.rebase true. - Instead of
git pull, you can always rungit fetchand then manuallygit rebase origin/mainorgit merge origin/mainfor more control. - If you work on a branch that only you touch, you can use
git push --force-with-leaseafter a rebase to safely update the remote.
Real-world use cases
- A developer working on a feature branch pulls from
maindaily with--rebaseto keep the branch up to date and avoid merge conflicts at the end. - A release manager uses
git pull --ff-onlyon themainbranch to guarantee that the local copy is exactly in sync with the remote before tagging a release. - A team member recovers from an accidental force push by using
git reflogandgit reset --hardto restore lost commits, then pushes again with--force-with-lease.
Key takeaways
- Always
git fetchbefore you pull to see what's coming. - Use
git pull --ff-onlyon branches you only pull from, andgit pull --rebaseon branches you commit to. - A rejected push is not a crisis — pull with rebase, resolve conflicts, then push.
- Never force-push to branches others use; use
--force-with-leaseonly on your own branches and as a last resort. - Handle uncommitted changes with
git stashbefore pulling, then pop them after. - Inspect what you're about to push with
git log origin/main..HEADto avoid pushing the wrong commits.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.