Build the mental model
This project puts branching, remotes, pushing, pulling, and conflict resolution into one realistic scene: two developers editing the same file at the same time, and nothing here is staged or simplified.
- Both developers clone the same shared repository, starting from identical history
- Developer A commits and pushes first, and Git accepts it as a fast-forward
- Developer B's push is rejected because the remote moved without them
- git pull triggers a real merge conflict on the shared line
- Resolving it is a manual decision, not something Git can automate
This rejection is not a bug. It is Git protecting the team from silently overwriting a teammate's already-pushed work, forcing a pull before any further push can succeed.
A rejected push is not a bug
It means the remote has commits you don't have locally yet. The fix is always the same: pull (or fetch + merge/rebase), resolve anything that conflicts, then push again.
| Situation | What Git does |
|---|---|
| Clean push | Local history is ahead of remote with no divergence -- fast-forward, no conflict |
| Rejected push | Remote has commits your local branch doesn't -- push refused until you pull |
| Merge conflict | Both sides changed the same lines -- Git stops and asks a human to decide |
TWO DEVELOPERS, ONE FILE: PUSH, REJECT, CONFLICT, RESOLVE
---------------------------------------------------------
TEAM REPO (bare repo, stands in for GitHub)
^ ^
| clone | clone
| |
DEV A CLONE DEV B CLONE
edit config.py, line X edit SAME config.py, line X
commit (author: Aye) commit (author: Banyar)
| |
push main -> OK (fast-forward) |
| |
TEAM REPO now ahead ------------> push main -> REJECTED
"fetch first" (remote moved)
|
git pull (fetch + merge)
v
CONFLICT in config.py:
<<<<<<< HEAD (Banyar's line)
=======
>>>>>>> commit-hash (Aye's line)
|
edit file by hand, remove markers
git add config.py
git commit (merge commit)
|
push main -> OK
v
TEAM REPO now holds both developers' changesConnect it to a real scenario
Create the shared team repo
git init --bare a bare repository to stand in for GitHub, then clone it twice into two separate folders -- one per developer.
Developer A edits and pushes
Change one specific line in the shared file, commit with A's pinned identity, and push. Since A's history matches the remote, this push succeeds as a clean fast-forward.
Developer B edits the same line, unaware
Working from a clone that hasn't seen A's push, edit that same line differently, and commit with B's own pinned identity.
Developer B's push is rejected
Attempt to push, and Git refuses, telling you the remote has work you don't have locally -- fetch first.
Pull and hit the real conflict
git pull fetches A's commit and tries an automatic merge. Because both edits touch the same line, it fails, leaving the file mid-merge with real conflict markers.
Resolve by hand
Open the file, read both versions between <<<<<<<, =======, and >>>>>>>, decide what the line should say, and delete every marker.
Stage, commit, and push the merge
git add the resolved file, commit to complete the merge, then push -- this time it succeeds, carrying both developers' work forward together.
Try the working example
git init --bare /tmp/team-repo.git
git clone /tmp/team-repo.git /tmp/seed
cd /tmp/seed
export GIT_AUTHOR_NAME="Team Lead"
export GIT_AUTHOR_EMAIL="lead@example.com"
export GIT_COMMITTER_NAME="Team Lead"
export GIT_COMMITTER_EMAIL="lead@example.com"
export GIT_AUTHOR_DATE="2026-02-01T09:00:00"
export GIT_COMMITTER_DATE="2026-02-01T09:00:00"
cat > config.py <<'EOF'
# Application configuration
APP_NAME = "Thuta Widgets"
VERSION = "1.0.0"
DEBUG = False
EOF
git add config.py
git commit -m "Initial commit: add config.py"
git push origin main
cd ..
# Two developers each clone the same team repo
git clone /tmp/team-repo.git /tmp/dev-a
git clone /tmp/team-repo.git /tmp/dev-b
# --- Developer A ---
cd /tmp/dev-a
export GIT_AUTHOR_NAME="Developer Aye"
export GIT_AUTHOR_EMAIL="aye@example.com"
export GIT_COMMITTER_NAME="Developer Aye"
export GIT_COMMITTER_EMAIL="aye@example.com"
export GIT_AUTHOR_DATE="2026-02-01T10:00:00"
export GIT_COMMITTER_DATE="2026-02-01T10:00:00"
sed -i 's/DEBUG = False/DEBUG = True # enabled for local testing/' config.py
git add config.py
git commit -m "Enable DEBUG mode for local testing"
git push origin main # succeeds: fast-forward
# --- Developer B (has not seen A's push yet) ---
cd /tmp/dev-b
export GIT_AUTHOR_NAME="Developer Banyar"
export GIT_AUTHOR_EMAIL="banyar@example.com"
export GIT_COMMITTER_NAME="Developer Banyar"
export GIT_COMMITTER_EMAIL="banyar@example.com"
export GIT_AUTHOR_DATE="2026-02-01T10:30:00"
export GIT_COMMITTER_DATE="2026-02-01T10:30:00"
sed -i 's/DEBUG = False/DEBUG = os.environ.get("DEBUG", "false") == "true"/' config.py
git add config.py
git commit -m "Read DEBUG flag from environment variable"
git push origin main # REJECTED -- remote has A's commit
git pull origin main # fetch + merge -> real CONFLICT
cat config.py # shows <<<<<<< / ======= / >>>>>>> markers
# Resolve by hand: pick the final version, remove every marker
cat > config.py <<'EOF'
# Application configuration
APP_NAME = "Thuta Widgets"
VERSION = "1.0.0"
DEBUG = os.environ.get("DEBUG", "true") == "true" # defaults on for local dev
EOF
git add config.py
git commit -m "Merge branch 'main' of team-repo, resolve DEBUG conflict"
git push origin main # succeeds nowDeveloper A's push succeeds cleanly:
4a5335c..75007aa main -> main
Developer B's push is rejected outright:
! [rejected] main -> main (fetch first)
error: failed to push some refs to '...team-repo.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally...
Running git pull as Developer B fetches A's commit and attempts an automatic merge, which fails on the same line:
Auto-merging config.py
CONFLICT (content): Merge conflict in config.py
Automatic merge failed; fix conflicts and then commit the result.
git status confirms it:
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: config.py
Opening config.py shows Git's real conflict markers, both versions side by side:
<<<<<<< HEAD
DEBUG = os.environ.get("DEBUG", "false") == "true"
=======
DEBUG = True # enabled for local testing
>>>>>>> 75007aa66326065e94a973a8f81a152cd9144142
After editing the file by hand to combine both intents, staging it, and committing the merge, the final push succeeds:
75007aa..83b66b7 main -> main
git log --graph then shows both developers' commits preserved side by side under one merge commit.5-minute try-it
Repeat the scenario but this time have both developers create their edits on separate feature branches (dev-a/fix-debug and dev-b/fix-debug) instead of committing straight to main, open both as separate pull requests conceptually, merge Developer A's branch to main first, then try merging Developer B's branch and observe whether the conflict still happens, just at merge time instead of push time.
One important caution
Committing straight to main instead of a branch, so a conflict blocks the whole team's shared history instead of being isolated to one pull request.
Force-pushing (git push --force) to make the rejected push 'go through' instead of pulling and resolving the conflict -- this can silently overwrite a teammate's already-pushed commit.
GitHub Docs: Resolving a merge conflict using the command line — Git & GitHub