EGCA EGCA Engineering Handbook
Internal reference · v1
Home Onboarding Git Review Prototype ↔ Prod
Home 3. Git Workflow

Git Workflow

Git is the single tool you’ll use most. Get comfortable — the better you are at Git, the less anxious you’ll be about breaking things.

The core promise of Git: almost nothing is actually destructive. Commits stick around for 90 days in the reflog even after you “lose” them. Breathe. Then recover.

Mental model (the 30-second version)

Four places your code lives:

  1. Working directory — the files you see.
  2. Staging area (index) — changes added with git add.
  3. Local repo — committed history on your machine.
  4. Remote (GitHub) — the shared truth.

git add moves working → index. git commit moves index → local. git push moves local → remote. git pull is the reverse of push.

If you internalize those four arrows you’ll never be confused about where your changes are.

Daily commands

This is 95% of what you’ll use:

git status                     # what's changed
git diff                       # unstaged changes
git diff --staged              # staged changes
git add <file>                 # stage a specific file
git add -p                     # stage chunk-by-chunk (use this!)
git commit -m "feat: add X"    # commit staged changes
git push                       # send to remote
git pull --rebase              # fetch + replay your commits on top
git log --oneline -20          # recent history
git switch -c feat/my-branch   # create + switch branch
git switch main                # switch to main

git add -p (patch mode) is the single best habit you can build. It forces you to read every change before committing — huge for review quality and catches stray console.logs.

Branching strategy

We do trunk-based development.

  • main is always deployable.
  • Short-lived feature branches cut from main.
  • Typical lifespan: hours to a couple of days. If a branch is alive for a week, you’re doing something wrong — split it.
  • Merge back via Pull Request with squash merge.

Branch naming

type/short-slug — lowercase, hyphens:

  • feat/client-invoices
  • fix/auth-redirect-loop
  • refactor/extract-db-client
  • chore/bump-node-22
  • docs/update-readme

Add a ticket id if you have one: feat/ENG-142-client-invoices.

Commits

Conventional Commits

Every commit’s subject line starts with a type:

TypeWhenExample
feat:New user-facing capabilityfeat: add invoice export
fix:Bug fixfix: auth redirect loops on expired token
refactor:Internal change, no behavior changerefactor: extract DB client into lib/db
chore:Tooling, deps, opschore: bump Next.js to 15.4
docs:Docs onlydocs: update onboarding checklist
test:Tests onlytest: add integration test for /api/invoices
perf:Performance changeperf: cache expensive invoice query

Subject line

  • ≤ 72 characters.
  • Imperative mood: “add X”, not “added X” or “adds X”. Read it as “If applied, this commit will ___”.
  • No period at the end.

Body (optional)

Skip it for trivial changes. Write one when the why isn’t obvious:

fix: cache invoice list to unblock dashboard

The dashboard loads the invoice list on mount, which was
triggering a full-table scan on every page load. Dropped p95
from 2.3s to 80ms by caching for 60s — safe because invoices
are only created from the /new flow.

Don’t repeat what the diff already says. Explain context, tradeoffs, alternatives considered.

Before you commit

Always:

  1. git status — know what’s staged.
  2. git diff --staged — read what you’re about to ship.
  3. Did you stage something you didn’t mean to? git restore --staged <file>.

Rebasing vs merging

We prefer rebase for keeping your branch current:

git fetch origin
git rebase origin/main

This replays your commits on top of the latest main. Result: clean linear history on your branch.

Don’t git merge main repeatedly into your branch. It creates merge commits that pollute review.

Never rebase shared branches. Rebasing rewrites history. If someone else has pulled your branch, rebasing will wreck their clone. Rule of thumb: only rebase branches that only you have touched.

Merging conflicts

After a rebase you’ll sometimes hit conflicts. The flow:

git rebase origin/main
# CONFLICT in app/page.tsx
  1. Open the file. Find the <<<<<<<, =======, >>>>>>> markers.
  2. Decide what the resolved code should be. Delete all three markers.
  3. git add app/page.tsx
  4. git rebase --continue
  5. Repeat per conflict.

If you panic: git rebase --abort returns you to the state before you started. Nothing lost.

Squash merge

On GitHub, our merge button is set to squash and merge. Your whole branch becomes one commit on main.

  • Your messy wip, more wip, fix test commits don’t pollute history.
  • The squashed commit’s message = your PR title + description.
  • Write good PR titles. They become the commit log on main.

Pull requests

Before you open one

  • Rebased on latest main.
  • Tests pass locally.
  • git log looks reasonable (or you’ll squash anyway — fine).
  • You’ve self-reviewed the diff on GitHub (not just your editor).

Opening the PR

  • Title: conventional-commit style. feat: add invoice export.
  • Description: explain why. Link the ticket. Mention tradeoffs and what you skipped.
  • Screenshots / screen recordings for UI changes. Use CleanShot or Loom.
  • Draft PR if it’s not ready — opens conversation without requesting review.

PR template

If a project has a .github/PULL_REQUEST_TEMPLATE.md, GitHub fills it in automatically. Our default:

## Why
<1–3 sentences>

## What changed
<bullet list if non-obvious>

## How to verify
<steps a reviewer can actually run>

## Notes
<tradeoffs, follow-ups, things I skipped>

Closes #<ticket>

Size

Aim for < 400 changed lines. Bigger PRs get shallower reviews. If it must be big:

  • Split into multiple PRs that stack.
  • Feature-flag half-finished work.
  • Narrate what to look at first.

Reviewers

  • Default: 1 reviewer.
  • Touching auth, payments, migrations, or infra → request a senior explicitly.
  • Don’t ping the whole channel. Request 1–2 specific people.

Pulling down someone else’s PR

gh pr checkout 142       # via GitHub CLI

Browsing a PR locally is 10× better than reviewing in the browser for anything non-trivial. Run it. Poke at it.

Recovering from mistakes

Git almost never actually loses work. Your tools:

I committed to the wrong branch

git log                       # find your commit hash
git switch correct-branch
git cherry-pick <hash>
git switch wrong-branch
git reset --hard HEAD~1       # only if the commit was only on this branch

I committed something secret (API key, password)

  1. Rotate the secret immediately. Anything that touched git is compromised forever.
  2. Then clean the history: git rebase -i <parent>, drop the commit. Or use git filter-repo.
  3. Force-push the branch (only if it’s your own branch, never main).

I need my last commit back after a reset

git reflog                    # shows every HEAD move — your undo stack
git reset --hard HEAD@{2}     # jump back to where you were

git reflog is the nuclear undo. It remembers 90 days. Almost nothing is truly gone.

I force-pushed over someone else’s work

Apologize. Then:

git reflog                    # on the other person's machine
git reset --hard <pre-push-hash>
git push --force-with-lease origin <branch>

Don’t force-push to branches other people share.

Never do these

  • Never git push --force to main. Branch protection should block you — if it doesn’t, fix the protection.
  • Never git commit --amend on a commit already pushed to a shared branch.
  • Never git rebase a branch someone else has pulled.
  • Never commit .env files, even once. Rotate the secrets the moment you do.
  • Never git add . without reviewing. Use git add -p or stage specific files.

Branch protection (what’s on main)

Every repo has these settings enforced:

  • No direct push to main.
  • PR required.
  • At least 1 approval.
  • CI must pass (typecheck, lint, test, build).
  • Branch must be up to date with main before merge.
  • Stale approvals dismissed on new commits.
  • No force push to main, ever.

If any of these are off in a repo, open a PR to fix it. They exist for real reasons we’ve learned the hard way.

Quality-of-life

Global .gitignore

Set one for editor/OS junk so you don’t pollute project .gitignore files:

git config --global core.excludesFile ~/.gitignore_global

Content:

.DS_Store
.vscode/
.idea/
*.swp

Aliases that pay for themselves

git config --global alias.co checkout
git config --global alias.sw switch
git config --global alias.st status
git config --global alias.lg "log --oneline --graph --decorate -30"
git config --global alias.amend "commit --amend --no-edit"

GitHub CLI

gh makes a lot of this faster:

gh pr create --fill              # creates PR from your branch
gh pr checkout 142               # check out PR #142 locally
gh pr view --web                 # open current branch's PR in browser
gh pr status                     # PRs that need your attention
gh run watch                     # watch the CI run

Install: brew install gh or your package manager.

When in doubt

Copy the repo somewhere before doing anything scary:

cp -r my-project my-project.backup

Then experiment in the original. Git undo is powerful but your nerves are limited — a 30-second copy is cheaper than a panic spiral.