Collaborating with Git
Learn to work with branches, remotes, and pull requests — the everyday workflow of every professional software team.
Why branches?
Everything committed so far has been on a single line of history called main. This works for solo projects, but consider a team scenario:
- Alice is building the login system
- Bob is fixing a security bug in the API
- Charlie is experimenting with a new caching layer
If they all commit to main simultaneously, their changes mix together. Alice cannot test her login system without also getting Bob's half-finished security fix and Charlie's experimental cache. When something breaks, it is impossible to tell whose change caused it.
Branches solve this by giving each person (or each feature) a separate line of history. Alice's login work exists independently from Bob's bug fix. When each is finished and tested, it can be merged into main.
Creating and switching branches
# List all local branches (* = current)
git branch
* main
# Create a new branch
git branch feature/login
# Switch to it
git switch feature/login
Switched to branch 'feature/login'
# Create and switch in one step (preferred)
git switch -c feature/login
# Rename the current branch
git branch -m feature/user-login
# Delete a branch (must not be current branch)
git branch -d feature/login
# safe delete (only if merged)
git branch -D feature/login
# force delete
After creating a branch, any commits you make are on that branch. The main branch is unaffected until you merge.
main: A --- B --- C
\
feature/login: D --- E
# HEAD points to the current commit on the current branch
git log --oneline --graph --all
* f4e5d6c (HEAD -> feature/login) Add login form
* a1b2c3d (main) Initial commit
Merging strategies
When a feature branch is ready, you merge it back into the target branch (usually main). Git uses different strategies depending on the history:
Fast-forward merge
If main has not moved since the branch was created, Git can simply move the main pointer forward. The history stays linear.
git switch main
git merge feature/login
Updating a1b2c3d..f4e5d6c
Fast-forward
login.py | 42 ++++
Three-way merge
If both branches have new commits, Git creates a merge commit that has two parents. The history records that both lines were combined.
git merge feature/login
Merge made by the 'ort' strategy.
login.py | 42 ++++
Remote repositories in depth
A remote repository is a copy of your repository hosted elsewhere — usually GitHub. The default remote is named origin by convention.
# Clone an existing repository
git clone https://github.com/user/repo.git
# See configured remotes
git remote -v
origin https://github.com/user/repo.git (fetch)
origin https://github.com/user/repo.git (push)
# Download changes without merging (safe)
git fetch origin
# Download and merge in one step
git pull
# Push your current branch to origin
git push origin feature/login
# Push and set tracking (first push of a new branch)
git push -u origin feature/login
git fetch downloads changes from the remote but does NOT modify your local branches. It is always safe. git pull fetches and then immediately merges. Prefer fetch followed by git merge origin/main so you can inspect what is coming before merging.
The pull request workflow
A pull request (PR) is a GitHub feature (not a Git concept) that wraps a branch merge in a review process. It is the standard collaborative workflow in most professional teams.
- Create a branch for your work:
git switch -c feature/add-search - Make commits on the branch as you work.
- Push the branch to GitHub:
git push -u origin feature/add-search - Open a PR on GitHub. Write a clear description: what does this change do? Why? Any testing notes?
- Review happens — teammates comment on the code, suggest changes, ask questions. You can push more commits to the branch in response.
- CI checks run automatically — the pipeline you will build in Module 6 triggers and reports pass/fail.
- Approval and merge — once approved and all checks pass, the PR is merged. The branch can then be deleted.
A good pull request description answers three questions:
- What — what exactly does this change?
- Why — what problem does it solve? Link to any relevant issue or ticket.
- How — any notes on implementation decisions, trade-offs, or things reviewers should pay attention to.
A bad description is: 'fix bug'. A good description is: 'Fix race condition in cache invalidation that caused stale data to appear for up to 30 seconds after profile updates. Reproduced locally, added regression test in test_cache.py'.
Merge conflicts step by step
A merge conflict occurs when two branches have changed the same part of the same file in incompatible ways. Git cannot decide which version to keep, so it puts both options into the file and asks you to choose.
# On main: someone changes a function
# main: app.py line 15: return user.name.upper()
# On feature/login: you change the same line
# feature: app.py line 15: return user.name.strip()
# Merging triggers a conflict
git merge feature/login
CONFLICT (content): Merge conflict in app.py
Automatic merge failed; fix conflicts and then commit.
<<<<<<< HEAD
return user.name.upper()
=======
return user.name.strip()
>>>>>>> feature/login
- Open the conflicted file in your editor.
- Understand both sides —
HEADis the current branch (main), the other marker is the incoming branch. - Edit the file to the correct final state — delete all four marker lines and keep what you want.
- Stage the file:
git add app.py - Complete the merge:
git commit(Git pre-fills a merge commit message)
Do not just pick one side or the other automatically. Sometimes the correct resolution combines both changes:
return user.name.strip().upper() # both: strip whitespace AND uppercase
Read the context carefully. If you are unsure what the correct result should be, talk to the author of the conflicting change.
Branch strategies in teams
Teams adopt conventions for how branches are named and how work flows. Two common models:
Feature branch workflow
Every piece of work gets its own branch. All work flows through PRs into main. Main is always deployable. This is the most common model for teams of any size.
feature/user-login
feature/JIRA-1234-search-api
fix/login-null-check
hotfix/critical-security-patch
chore/update-dependencies
Trunk-based development
Developers commit directly to main (the trunk) very frequently — multiple times per day. Features are hidden behind feature flags until ready. This requires strong CI and a culture of very small commits. Used by high-frequency deployment organisations like Google.
git stash for context switching
You are halfway through a feature when an urgent bug fix arrives. You cannot commit half-finished work. git stash saves your changes temporarily so you can switch context.
# Save all uncommitted changes
git stash
# Save with a descriptive name
git stash push -m "half-built login form"
# List stashed changes
git stash list
stash@{0}: On feature/login: half-built login form
stash@{1}: WIP on main: ...
# Restore the most recent stash
git stash pop
# Apply without removing from the list
git stash apply
# Apply a specific stash
git stash apply stash@{1}
# Discard a stash
git stash drop
Key terms
Exercises
Part A: Branching
- Fork the provided starter repository on GitHub and clone it locally.
- Create a branch called
feature/your-nameand switch to it. - Make two commits on this branch — add a new function and a docstring.
- Use git log --oneline --graph --all to visualise the branch structure.
- Switch back to main. Notice that your changes are not there.
Part B: Remotes and pull requests
- Push your branch to GitHub with
git push -u origin feature/your-name. - Go to GitHub and open a pull request with a clear description.
- Ask a classmate to leave a code review comment.
- Make one more commit in response to the comment and push it.
- The reviewer approves. Merge the PR on GitHub.
- Back locally, switch to main and run
git pullto get the merged changes. - Delete your feature branch:
git branch -d feature/your-name.
Part C: Conflict resolution
- You and your tutor will both be given the same file to edit. Both of you change line 5 to different things.
- Your tutor pushes first. When you try to push, Git will reject it.
- Fetch the changes, then merge. Resolve the conflict thoughtfully (read both sides).
- Stage and complete the merge commit.
- Push the resolved version.
Part D: git stash
- Start editing a file but do not commit.
- Use
git stashto save your changes. Verify withgit statusthat the working directory is clean. - Make and commit a different change. Then use
git stash popto restore your original changes. - Verify both changes are now present.