NovuSpark
All articles

September 12, 2025 · NovuSpark Team

Git Fundamentals Every Team Should Standardize On

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-config

The exact prefixes matter far less than having one convention and using it consistentlyfeature/, 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 main

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

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.