Project Overview
Architecture of this simulation:
- Origin repo (bare repository) — acts as the shared remote server (like GitHub)
- Dev A's clone — Developer A's local workspace
- Dev B's clone — Developer B's local workspace
Both developers work on the same project — a simple web app config file. They'll add different features, push, pull, and eventually hit a conflict they need to resolve.
Step 1 – Set Up the Shared Origin
# Create our workspace
mkdir ~/team-sim && cd ~/team-sim
# Create the "remote" — a bare repository (no working tree)
# This simulates a GitHub/GitLab remote
git init --bare origin.git
# Create the initial project and push to origin
mkdir seed-project && cd seed-project
git init
git commit --allow-empty -m "chore: initial commit"
# Create the app config file
cat > config.json << 'EOF'
{
"app": "TeamApp",
"version": "1.0.0",
"port": 3000,
"database": {
"host": "localhost",
"port": 5432
}
}
EOF
git add config.json
git commit -m "feat: add initial app config"
git remote add origin ~/team-sim/origin.git
git push origin main
cd ~/team-sim
Step 2 – Developer A: Add Auth Feature
# Developer A clones the origin
git clone ~/team-sim/origin.git dev-a
cd dev-a
# Verify the starting point
git log --oneline
# a1b2c3d (HEAD -> main, origin/main) feat: add initial app config
# Create a feature branch for auth configuration
git switch -c feature/auth-config
# Update config.json to add auth settings
cat > config.json << 'EOF'
{
"app": "TeamApp",
"version": "1.0.0",
"port": 3000,
"database": {
"host": "localhost",
"port": 5432
},
"auth": {
"jwtSecret": "change-in-production",
"tokenExpiry": "24h",
"bcryptRounds": 12
}
}
EOF
git add config.json
git commit -m "feat(auth): add JWT authentication configuration"
# Push the feature branch to origin
git push -u origin feature/auth-config
# Merge to main (simulating a PR merge)
git switch main
git merge --no-ff feature/auth-config -m "feat(auth): merge auth configuration"
git push origin main
cd ~/team-sim
Step 3 – Developer B: Add Dashboard Feature
# Developer B clones the origin (at the same time as Dev A was working)
git clone ~/team-sim/origin.git dev-b
cd dev-b
# Note: Dev B cloned before Dev A pushed!
# Dev B's main only has the initial commit.
git log --oneline
# a1b2c3d (HEAD -> main, origin/main) feat: add initial app config
# Dev B creates their feature branch
git switch -c feature/dashboard-config
# Dev B also modifies config.json (adding dashboard settings)
# This WILL create a conflict since Dev A also modified config.json
cat > config.json << 'EOF'
{
"app": "TeamApp",
"version": "1.0.0",
"port": 3000,
"database": {
"host": "localhost",
"port": 5432
},
"dashboard": {
"refreshInterval": 30,
"maxCharts": 10,
"theme": "light"
}
}
EOF
git add config.json
git commit -m "feat(dashboard): add dashboard configuration"
cd ~/team-sim
Step 4 – The Conflict Scenario
Developer B tries to push their work to main, but Developer A already pushed. This is the classic "diverged branch" scenario.
cd ~/team-sim/dev-b
# Developer B tries to merge to main and push
git switch main
git merge --no-ff feature/dashboard-config -m "feat(dashboard): merge dashboard config"
git push origin main
# ! [rejected] main -> main (fetch first)
# error: failed to push some refs to '~/team-sim/origin.git'
# hint: Updates were rejected because the remote contains work that you do
# hint: not have locally.
# Dev B needs to fetch and integrate Dev A's changes first
git fetch origin
git log --oneline --all --graph
# * b4c5d6e (HEAD -> main) feat(dashboard): merge dashboard config
# * c3d4e5f feat(dashboard): add dashboard configuration
# | * 9f8a7b6 (origin/main) feat(auth): merge auth configuration
# | * 8e7d6c5 feat(auth): add JWT authentication configuration
# |/
# * a1b2c3d (origin/main before fetch) feat: add initial app config
Step 5 – Resolve the Conflict
# Rebase Dev B's main on top of origin/main
git rebase origin/main
# CONFLICT (content): Merge conflict in config.json
# error: could not apply b4c5d6e... feat(dashboard): merge dashboard config
# Open config.json — it contains conflict markers:
# <<<<<<< HEAD (Dev A's version with auth)
# "auth": { "jwtSecret": "...", ... }
# =======
# "dashboard": { "refreshInterval": 30, ... }
# >>>>>>> feat(dashboard): merge dashboard config
{
"app": "TeamApp",
"version": "1.0.0",
"port": 3000,
"database": {
"host": "localhost",
"port": 5432
},
"auth": {
"jwtSecret": "change-in-production",
"tokenExpiry": "24h",
"bcryptRounds": 12
},
"dashboard": {
"refreshInterval": 30,
"maxCharts": 10,
"theme": "light"
}
}
# After editing the file to include BOTH auth and dashboard:
git add config.json
git rebase --continue
# Now push successfully
git push origin main
# View the final history
git log --oneline --graph --all
# * e5f6a7b (HEAD -> main, origin/main) feat(dashboard): merge dashboard config
# * d4e5f6a feat(dashboard): add dashboard configuration
# * 9f8a7b6 feat(auth): merge auth configuration
# * 8e7d6c5 feat(auth): add JWT authentication configuration
# * a1b2c3d feat: add initial app config
Best Practices Discovered
Lessons learned from this simulation:
| Practice | Why it matters |
|---|---|
Always git pull before starting new work | Start from the latest state; smaller conflicts |
| Use short-lived feature branches | Less divergence = smaller conflicts |
| Communicate when editing shared files | Coordinate with teammates before changing the same file |
| Fetch and rebase frequently | Catch conflicts early when they're small |
| Write clear commit messages | Helps teammates understand what changed and why |
| Use PRs for code review before merging | A second pair of eyes catches issues early |
📋 Summary
- A bare repository (
git init --bare) simulates a shared remote server — it has no working tree, only Git internals. - Developer A and B cloned the same origin and worked on separate feature branches simultaneously.
- The classic team problem: Developer B's push was rejected because Developer A had already pushed.
- Solution: git fetch origin → git rebase origin/main → resolve conflict → git push.
- The resolved config.json kept BOTH teams' changes — this is always the goal when resolving conflicts.
git log --oneline --graph --allis your go-to command for visualizing complex multi-branch history.
FAQ
Both work. git pull --rebase (or git fetch + git rebase origin/main) creates a linear history — your commits appear "on top of" the remote's commits as if you wrote them after. This avoids extra merge commits and makes the history cleaner and easier to read. git pull (merge) creates a merge commit, preserving the exact timeline of events. Many teams use rebase for their own feature branches but merge for integrating features to main via PRs. Neither approach is universally better — it depends on your team's preferences.
If only you have the branch (feature branch): you can force-push after resetting — git reset HEAD~1 then git push --force origin <branch>. If others might have pulled: don't force-push. Instead, create a revert commit: git revert HEAD and push that. For pushing to main by accident (when you should have used a PR), talk to your team immediately. If the commit hasn't been pulled by anyone yet, a force-push may be acceptable — but always coordinate first.
Complete prevention isn't possible, but frequency can be reduced: (1) Keep branches short-lived (less than a day or two). (2) Communicate ownership of files — if two people need to change the same file, coordinate. (3) Pull from main frequently during development. (4) Use feature flags to merge incomplete features early (reducing branch lifetime). (5) Modular architecture — design the codebase so different features touch different files. (6) Small, focused PRs — less chance of overlap with other work in progress.