Course 5 · Tooling

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.

Setup — do this first

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:

terminal
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.

The mental shift

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:

●───●───● feature/add-eu-region ← your work-in-progress lives here / \ main ●───●───●─────────● ← stays clean & deployable; your branch MERGES back in later past │ you branched here

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:

terminal
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)
✓ You should see

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 branchesTrunk-based development
IdeaOne branch per feature/fix, merged when done.Tiny branches merged into main daily or faster.
Branch lifeHours to a few days.Minutes to hours.
RiskLong-lived branches drift from main and conflict.Requires good tests; less drift, fewer conflicts.
Common inMost 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:

branch off main → commit work → push → OPEN PULL REQUEST │ │ │ ┌──────┴───────┐ │ review CI runs │ (humans) (tests/lint) │ └──────┬───────┘ │ ▼ └─────────────────────────────────────▶ MERGE to main → delete branch

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:

terminal
# 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.
Why not just merge it yourself?

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
CorrectnessDoes it do what it claims? Any edge cases, off-by-ones, wrong joins?
ClarityWill someone (including future-you) understand this in six months?
TestsIs the new behavior covered? Would a regression be caught?
SafetyAny secrets, large data files, or destructive operations sneaking in?
ScopeIs 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.

Review your own PR first

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:

<<<<<<< HEAD ← everything below is YOUR current branch's version revenue = price * qty ======= ← the divider between the two versions revenue = price * quantity >>>>>>> main ← everything above is the OTHER branch's version (main)

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:

terminal
# 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-a
✓ You should see

Git 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:

terminal
# 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 top
✓ You should see

git 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."

When in doubt, abort

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:

① git switch main && git pull update your local main to the latest │ ② git switch -c feature/x branch off for one focused change │ ③ edit · git add · git commit do the work in small commits │ ④ git push -u origin feature/x publish the branch │ ⑤ open a Pull Request propose it; CI runs, teammates review │ ⑥ address review · merge apply feedback, then merge to main │ ⑦ git switch main · delete branch clean up; the branch has served its purpose │ └──────────────── back to ① for the next change ───────────────┘

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 log will be grateful.
  • Never commit secrets or data. Passwords, API keys, .env files, 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 .gitignore is for — a file listing patterns git should never track:
.gitignore
# .gitignore — recap from Course 2
.env
*.csv
data/
__pycache__/
.DS_Store
⚠ git remembers everything

If 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 main first) 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.

solution
# --- 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 landed

If 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