Ad – 728×90
🛠️ Projects

Simulate Team Collaboration with Git – Two-Developer Workflow

The best way to understand team Git workflows is to simulate them yourself. In this project, you'll play both "Developer A" and "Developer B" using two separate local clones of the same repository. You'll experience pushing and pulling, dealing with diverged history, creating and resolving a real merge conflict — all the challenges that happen every day on real development teams. No second computer needed.

⏱️ 60 min 🎯 Intermediate 📅 Updated 2026

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

Bash
# 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

Bash (Developer A's terminal)
# 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
Ad – 336×280

Step 3 – Developer B: Add Dashboard Feature

Bash (Developer B's terminal)
# 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.

Bash (Developer B continues)
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

Bash (Developer B resolves)
# 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
JSON (resolved config.json)
{
  "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"
  }
}
Bash
# 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:

PracticeWhy it matters
Always git pull before starting new workStart from the latest state; smaller conflicts
Use short-lived feature branchesLess divergence = smaller conflicts
Communicate when editing shared filesCoordinate with teammates before changing the same file
Fetch and rebase frequentlyCatch conflicts early when they're small
Write clear commit messagesHelps teammates understand what changed and why
Use PRs for code review before mergingA 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 origingit 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 --all is your go-to command for visualizing complex multi-branch history.

FAQ

Why use rebase instead of merge when integrating remote changes? +

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.

What if I push to the wrong branch by accident? +

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.

How do teams prevent merge conflicts in the first place? +

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.