This is the first post in our GitHub and GitHub Actions series. Later posts cover Actions basics, reusable workflows, securing Actions, and multi-environment deployments.
Almost every confusing Git moment a team hits isn't actually a Git problem — it's a convention nobody agreed on. Two people using different commit message styles, different branch naming, or different rules about when to rebase versus merge will eventually produce a history that's technically correct and practically unreadable. This post covers the handful of conventions worth deciding deliberately, before a team is large enough for the lack of one to actually hurt.
Commits: atomic, and explaining why
A commit should represent one coherent change — not "everything I did today," and not a single line fix split across three commits for no reason.
git add src/auth/login.ts
git commit -m "Fix session token expiry check off-by-one error
The comparison used > instead of >=, causing tokens to be treated
as valid for one extra second past their actual expiry. Low
severity, but worth backporting to the 2.3 release branch."The first line is a short summary (conventionally under ~50 characters, imperative mood — "Fix," not "Fixed" or "Fixes"); the blank line and body explain why, not just what — a diff already shows what changed; a message that just restates the diff in prose adds nothing a reviewer, or you in six months, couldn't already see.
Branch naming: a convention, chosen once, followed consistently
git checkout -b feature/user-profile-avatar
git checkout -b fix/session-expiry-off-by-one
git checkout -b chore/upgrade-eslint-configThe exact prefixes matter far less than having one convention and using it consistently — feature/, fix/, chore/ is a common, reasonable choice, but a team that picks something different and applies it uniformly is in a genuinely better position than a team using three different, undocumented styles simultaneously. The actual payoff shows up in tooling: CI can trigger different behavior based on branch prefix, and anyone scanning a branch list instantly understands each one's purpose.
Merge vs. rebase: understanding the trade-off, not picking a side dogmatically
# Merge: preserves exact history, including a merge commit
git checkout main
git merge feature/user-profile-avatar
# Rebase: replays your commits on top of the latest main, linear history
git checkout feature/user-profile-avatar
git rebase mainmerge preserves exactly what happened, including a merge commit showing where two lines of work joined — an honest record, at the cost of a history that can look tangled with enough concurrent branches. rebase rewrites your branch's commits to appear as if they were made on top of the latest main, producing a clean, linear history — at the cost of literally rewriting commit hashes, which is fine on a branch only you're using, and actively dangerous on a branch anyone else has already pulled.
The one rule worth actually enforcing: never rebase a branch that other people have already pulled and might be working from — doing so rewrites history they've already based work on, and reconciling the two diverging versions afterward is a genuinely painful, error-prone process. Rebasing your own feature branch before opening a pull request is safe and common; rebasing main itself, or a shared long-lived branch, is not.
Pull requests: small, and reviewable
A pull request touching 40 files across unrelated concerns gets a shallow review, because no reviewer can hold that much context at once and give each part real attention. A pull request making one focused, explainable change gets an actual review — a reviewer can reasonably understand the whole thing, and disagreements surface on the specific change being made rather than getting lost across an enormous diff.
gh pr create --title "Fix session token expiry off-by-one" \
--body "Comparison used > instead of >=. Adds a regression test."The size discipline this requires is a genuine skill, not just a preference — it usually means resisting the urge to also fix an unrelated thing you noticed while you were in that file, and opening a separate, equally small PR for it instead.
.gitignore: getting it right from the very first commit
# .gitignore
node_modules/
.env
.env.local
*.log
.DS_Store
dist/
Getting this wrong the first time is the recurring, real-world mistake we've referenced multiple times already in this blog — committing node_modules, a .env file with real secrets, or OS-specific junk files like .DS_Store into a repository's permanent history. A blanket ignore rule can also be too aggressive: we hit this ourselves building this exact site, where a blanket .env* rule accidentally excluded .env.local.example — a harmless template file that was actually meant to be committed and shared. Review what a .gitignore pattern actually excludes, on a real git status, rather than assuming a broad pattern does only what you intended.
What to actually remember from this post
- Atomic commits with a message explaining why are worth more, to future readers, than a diff alone ever communicates on its own.
- Pick one branch-naming convention and use it consistently — the specific choice matters less than the consistency.
- Never rebase a branch other people have already pulled — that's the one rule worth genuinely enforcing, not a general "rebase is bad" stance.
- Small, focused pull requests get real reviews; large ones get rubber-stamped, because no reviewer can meaningfully hold that much context at once.
Next in the series: GitHub Actions 101: Your First CI Workflow, where these conventions become the foundation for actually automating a build and test pipeline.
