Professional Git & Collaboration
In Course 2 you learned to commit and push — git as a personal save button. On a team, git becomes something bigger: the shared discipline that lets several people change the same codebase every day without overwriting each other, losing work, or shipping unreviewed bugs. This lab is about that discipline — branches, pull requests, code review, and the inevitable merge conflict — and it's the workflow you'll use more than any other tool in your career.
You need two things, both from Course 2: git installed (check with git --version) and a GitHub account you can push to. We'll work in a throwaway practice repo so nothing here can hurt your real projects. Keep this page beside your terminal and run each command as you read — the muscle memory is the point. If git switch isn't found, your git is old; update it (modern git is 2.23+).
Beyond commit & push
Quick recap of the Course 2 basics, because everything here builds on them. A commit is a saved snapshot of your files with a message; push sends your commits to a remote like GitHub so others can see them:
git status # what's changed, what's staged
git add file.sql # stage a change for the next commit
git commit -m "Add daily revenue model" # snapshot the staged changes
git push # send commits to the remote (GitHub)That loop is enough when you're alone. The moment a second person shares the repo, a new question appears: how do two people change the same code at the same time without chaos? If you both push to the same line of history, you trip over each other — one person's push gets rejected, or worse, half-finished work lands on top of working code. The answer the whole industry has settled on is: nobody works directly on the main line. Each change gets its own isolated branch, is proposed as a pull request, gets reviewed, and only then merges into main. The rest of this chapter unpacks each of those words.
Solo git is about saving your own history. Team git is about proposing changes to a shared history that others approve before it becomes official. Same tool, completely different posture — and learning the team posture is what makes you employable as an engineer rather than a script-writer.
Branches — your private workspace on shared code
A branch is a movable label pointing at a line of commits. main is just the branch everyone agrees is the official, working version. When you start a piece of work, you create a new branch off main — now you have a private copy of history where you can commit freely without touching anyone else's work. The flow looks like this:
Because your commits live on the branch, main stays clean and deployable the whole time you're working. You could break everything on your branch and main wouldn't notice. Creating and switching to a branch is one command:
git switch main # make sure you're starting from main
git pull # get the latest main from the remote first
git switch -c feature/add-eu-region # create a NEW branch and switch to it
# ...edit files, then...
git add . && git commit -m "Add EU region to revenue model"
git push -u origin feature/add-eu-region # push the branch to GitHub (-u links it)After git switch -c, git prints Switched to a new branch 'feature/add-eu-region', and git status shows On branch feature/add-eu-region. After the push, GitHub usually prints a URL you can click to open a pull request — that's your next step.
How big should a branch be? There are two schools, and you'll meet both:
| Feature branches | Trunk-based development | |
|---|---|---|
| Idea | One branch per feature/fix, merged when done. | Tiny branches merged into main daily or faster. |
| Branch life | Hours to a few days. | Minutes to hours. |
| Risk | Long-lived branches drift from main and conflict. | Requires good tests; less drift, fewer conflicts. |
| Common in | Most teams, most data work. | High-velocity teams with strong CI. |
The shared lesson of both: keep branches short-lived. A branch open for three weeks is a branch that's drifted far from main and will fight you when it merges. Branch, do one focused thing, merge, delete, repeat.
Pull requests — the unit of collaboration
A pull request (PR) is you saying: "here's a branch with a change I'd like to merge into main — please look at it." It's not a git command; it's a GitHub feature that wraps your branch in a page where the change can be seen as a diff, discussed line-by-line, checked by automated tests, and finally merged with one click. The PR is where collaboration actually happens — it's the unit of work a team reviews, approves, and records.
The standard rhythm, called the GitHub flow, is deliberately simple:
A good PR does three things for a reviewer. It has a title that says what changed, a description explaining why (and how to test it), and it's small enough to actually read. A 1,000-line PR gets a rubber-stamp "looks good"; a 60-line PR gets a real review. You open a PR from the URL GitHub printed after your push, or with the gh CLI:
# If you have the GitHub CLI installed:
gh pr create --title "Add EU region to revenue model" \
--body "Adds eu to the region dimension and backfills the rollup. Tested with dbt test."
# No gh? Just open the URL git printed after `git push`, or go to the repo on github.com
# and click the "Compare & pull request" button that appears.Because the PR is a checkpoint where other eyes and automated tests see your change before it becomes official. Most teams configure main so that direct pushes are blocked and a PR with at least one approval is required. That single rule prevents the majority of "who broke main?" incidents.
Code review — the human checkpoint
Once your PR is open, a teammate reviews it: they read the diff, leave comments, and either approve it or request changes. Review is the single highest-leverage habit on a team, and it does two jobs at once — it catches bugs before they reach production, and it spreads knowledge, so no part of the codebase is understood by only one person (the dreaded "bus factor of one").
What does a reviewer actually look for? Roughly in priority order:
| Reviewers check for… | Asking |
|---|---|
| Correctness | Does it do what it claims? Any edge cases, off-by-ones, wrong joins? |
| Clarity | Will someone (including future-you) understand this in six months? |
| Tests | Is the new behavior covered? Would a regression be caught? |
| Safety | Any secrets, large data files, or destructive operations sneaking in? |
| Scope | Is the PR doing one thing, or three unrelated things tangled together? |
Giving good feedback is a skill. The rule of thumb: be kind, be specific, and critique the code, not the person. "Nit: could rename x to region_id for clarity" is useful; "this is messy" is not. Distinguish blocking issues from optional suggestions — many teams prefix the latter with nit: so the author knows it's not a hard requirement. And receiving review is a skill too: assume good intent, ask questions when feedback is unclear, and don't take it personally — every senior engineer's code gets reviewed too.
Before requesting a human, read your own diff on the PR page top to bottom. You'll catch a stray debug print, a committed secret, or an unrelated change you forgot to split out. It's the cheapest review you'll ever get, and it shows your reviewer respect for their time.
Merge conflicts — reading and resolving them
A merge conflict happens when two branches change the same lines of the same file in different ways. Git can merge most changes automatically — different files, different regions of a file — but when it can't decide which version wins, it stops and asks you. Conflicts aren't errors or signs you did something wrong; they're a normal, routine part of teamwork. The only real skill is reading the markers calmly. Here's the anatomy:
To resolve, you edit the file so it contains the one correct final version — keeping yours, keeping theirs, or blending them — and delete all three marker lines (<<<<<<<, =======, >>>>>>>). Then stage and commit. Let's create a real conflict on purpose and fix it:
# Set up a tiny practice repo
mkdir conflict-demo && cd conflict-demo
git init -b main
printf 'revenue = price * qty\n' > calc.py
git add calc.py && git commit -m "Initial calc"
# Branch A changes the line one way
git switch -c branch-a
printf 'revenue = price * quantity\n' > calc.py
git commit -am "Rename qty -> quantity"
# Meanwhile main changes the SAME line another way
git switch main
printf 'revenue = unit_price * qty\n' > calc.py
git commit -am "Rename price -> unit_price"
# Now try to merge branch-a into main — boom, conflict
git merge branch-aGit prints CONFLICT (content): Merge conflict in calc.py and Automatic merge failed; fix conflicts and then commit the result. Open calc.py and you'll find both versions wrapped in the <<<<<<< / ======= / >>>>>>> markers. git status lists calc.py under "Unmerged paths."
Now resolve it. Edit calc.py so it reads exactly the final line you want — combining both renames — and remove every marker:
# Replace the conflicted file with the intended final version (both renames kept)
printf 'revenue = unit_price * quantity\n' > calc.py
git add calc.py # mark the conflict as resolved
git commit # finish the merge (git pre-fills a merge message)
git log --oneline # see your merge commit on topgit status now reports a clean tree, and cat calc.py shows the single clean line revenue = unit_price * quantity with no markers. You just resolved a merge conflict — the thing beginners fear most turns out to be "pick the right lines and delete the markers."
Mid-merge and confused? git merge --abort rewinds you to before the merge started, as if nothing happened. There's almost no way to permanently lose work to a conflict — you can always back out and try again calmly.
The everyday loop
Put it all together and you get the rhythm a professional repeats many times a day. Internalize this loop and you'll fit into almost any engineering team on day one:
Step ① matters more than it looks: always start from a fresh main. Branching off a stale main is the number-one cause of avoidable conflicts. After a merge, delete the branch (GitHub offers a button; locally git branch -d feature/x) — branches are cheap and meant to be disposable.
Hygiene & habits
The difference between a repo that's a pleasure to work in and one that's a minefield is a handful of habits:
- Small, focused PRs. One PR = one logical change. Easier to review, safer to merge, easier to revert if something breaks.
- Good commit messages. A short imperative summary ("Add EU region to revenue model"), not "fix" or "stuff." Future-you reading
git logwill be grateful. - Never commit secrets or data. Passwords, API keys,
.envfiles, and big data files do not belong in git. A leaked key in history is hard to fully remove and may be exploited within minutes. This is what.gitignoreis for — a file listing patterns git should never track:
# .gitignore — recap from Course 2
.env
*.csv
data/
__pycache__/
.DS_StoreIf you commit a secret and then delete it in a later commit, it's still in the history — anyone can check out the old commit and read it. The fix is to rotate (invalidate) the leaked credential immediately, then clean history. Far easier to never commit it: add it to .gitignore before the first git add.
Rebasing vs merging, in one line: merge joins two branches and records a merge commit (preserves exactly what happened); rebase replays your commits on top of the latest main for a cleaner straight-line history (rewrites your commits — never rebase shared branches others are using). Beginners can safely default to merge; you'll learn when rebase pays off.
✓ Check yourself
- Can you explain why a team works on branches instead of pushing straight to
main? - Could you create a branch, commit, push, and open a PR without looking it up?
- Can you read the three conflict markers and say which side is yours?
- Do you know the seven steps of the everyday loop, and why step ① (
pull mainfirst) matters?
Exercise — Branch, PR, and resolve a conflict on your own repo
Using your mini-griddp repo (or any repo you own on GitHub): (1) create a feature branch, make a small change to the README, push it, and open a PR against your own repo. (2) Then intentionally create a small merge conflict by changing the same README line on main, and resolve it.
# --- Part 1: branch, change, push, open PR ---
git switch main && git pull
git switch -c docs/update-readme
printf '\nUpdated by the git lab.\n' >> README.md
git add README.md && git commit -m "Note the git lab in the README"
git push -u origin docs/update-readme
gh pr create --fill # or open the URL git printed and click "Create pull request"
# --- Part 2: create and resolve a conflict ---
# Change the SAME line on main so the two diverge
git switch main
printf '\nUpdated directly on main.\n' >> README.md
git commit -am "Tweak README on main"
# Merge the branch in -> conflict on the last lines of README.md
git merge docs/update-readme # CONFLICT (content): Merge conflict in README.md
# Open README.md, delete the <<<<<<< ======= >>>>>>> markers,
# and keep the single final version of those lines you want. Then:
git add README.md
git commit # completes the merge
git log --oneline # confirm the merge commit landedIf you opened a PR and then resolved a conflict without panicking, you've done the two things that intimidate beginners most — and they're now just steps in a loop you'll repeat thousands of times.
Next
Your collaboration on shared code is now professional. Next we make sure the code runs the same everywhere it lands — by packaging it in a container. → Docker Deeper