Skip to content
My Guidelines
Guides
In this guide (13 items)

Git Workflow & Best Practices

Git Workflow & Best Practices

House standard for using Git: commit hygiene, conventional commits, branching, pull requests, history discipline, and secret safety. These rules are what github-actions.md enforces in CI (commit-lint, semver, branch/tag rulesets) — keep both in sync.

1. Commit philosophy

  • Atomic commits. One logical change per commit. A commit should build and pass tests on its own. Don’t mix a refactor with a feature, or formatting with logic.
  • Self-explanatory. The message says why, the diff says what. Future you reads the log, not your memory.
  • Commit early, commit often — locally. Squash into clean units before the branch is reviewed/merged.
  • Never commit secrets, generated artifacts, or large binaries. (§8, §9)

2. Conventional commits

Every commit follows Conventional Commits. This is mechanically enforced by commitlint in CI and drives automatic semantic versioning.

<type>(<scope>): <subject>
<body>
<footer>

Allowed types (lower-case, never empty):

Type Use SemVer
feat new user-facing capability minor
fix bug fix patch
refactor behavior-preserving restructuring
perf performance improvement patch
test add/adjust tests only
docs documentation only
style formatting, no logic (whitespace, lint)
build build system / dependencies
ci CI config / pipelines
chore tooling, housekeeping
revert reverts a previous commit

Rules (matching the enforced commitlint ruleset):

  • type required, lower-case, from the list above.
  • scope optional, lower-case (e.g. feat(auth):).
  • subject required, imperative mood (“add”, not “added”/“adds”), no trailing period, ≤ 100 chars; full header ≤ 120 chars.
  • Body wraps at ~100 cols, explains the why and trade-offs.
  • Breaking changes: add ! after type/scope and a BREAKING CHANGE: footer → triggers a major bump.
feat(payment)!: replace synchronous capture with async authorization
Capture now happens via the settlement worker, not inline on the request.
BREAKING CHANGE: POST /payments returns 202 + a status URL instead of 200.
Co-Authored-By: ...

Good vs bad:

✅ fix(auth): reject tokens whose exp equals now (use <=, not <)
✅ refactor(order): extract pricing into PricingService
❌ Fixed stuff # no type, vague, past tense
❌ feat: Added Feature. # capitalized subject, past tense, trailing period
❌ wip # not a change description

3. Branching model

Trunk-oriented flow with two long-lived protected branches and short-lived work branches.

  • main — production; always releasable; tagged on release.
  • develop — integration; what the next release is cut from.
  • Work branches — short-lived, branched from develop, one concern each:
Prefix For
feature/<slug> new capability
fix/<slug> bug fix
refactor/<slug> restructuring
chore/<slug> tooling/maintenance
hotfix/<slug> urgent prod fix, branched from main

Rules

  • Branch names are validated in CI (validate-source-branch) — use the prefixes.
  • Keep branches short-lived (hours/days, not weeks); rebase on develop often.
  • One branch = one PR = one concern. Split large work into stacked PRs.
  • Delete the branch after merge (CI delete-branch automates this).

4. Keeping a branch current

Prefer rebase for local/feature branches to keep history linear; never rebase shared/protected branches.

Terminal window
git fetch origin
git rebase origin/develop # replay your commits on top of latest develop
# resolve conflicts → git add -p → git rebase --continue
git push --force-with-lease # safe force-push; refuses if remote moved
  • Use --force-with-lease, never bare --force (it clobbers others’ pushes).
  • Don’t rebase a branch someone else is also committing to — merge instead.
  • Resolve conflicts per-hunk; re-run tests after a rebase.

5. Pull requests

PRs are the only path into main/develop (enforced by ruleset).

  • Small and focused. A reviewable PR is < ~400 changed lines. Stack PRs for big work.
  • Description: what + why + how to test; link the issue/ADR. If it’s an architecturally significant decision, attach an ADR.
  • Green before review: PR pipeline (lint, build, test, security) must pass.
  • Required: 2 approvals, all review threads resolved, approval after the last push, status checks green on the latest commit.
  • Merge = squash, producing one conventional commit on the target branch → linear history. The squash commit subject must itself be conventional (it’s what semver reads).
  • No force-push to the base; no merge commits (linear history rule).

6. Contributing to a repository (fork-based)

For repos where you lack push access (OSS, cross-team, third-party), contribute via the fork + pull-request model. §5 covers PRs from a branch you can push; this covers PRs from a fork you can’t.

Before you code

  • Read the repo’s CONTRIBUTING.md and Code of Conduct first — the project’s own process always wins over this guide. Generate both for your own projects from the templates in resources.md.
  • Discuss first. Open (or claim) an issue describing the change before writing code; get maintainer buy-in on anything non-trivial. An unsolicited large PR with no prior discussion usually gets closed.

Fork & keep it synced — add the canonical repo as upstream; rebase on it before branching and before every push so your PR stays conflict-free.

Terminal window
# fork on the host, then clone your fork
git clone git@github.com:<you>/<repo>.git && cd <repo>
git remote add upstream git@github.com:<ORG>/<repo>.git # canonical repo
git fetch upstream
git switch develop # or main, per the repo's base branch
git rebase upstream/develop # never commit directly on your fork's base
git push # fast-forward your fork's base

Propose the change — branch off upstream, push to your fork (origin), open the PR against the canonical repo.

Terminal window
git switch -c feature/<slug> upstream/develop
# ... atomic conventional commits (§1, §2) ...
git push -u origin feature/<slug> # origin = your fork
# open PR: base = <ORG>:develop, head = <you>:feature/<slug>, link the issue
  • Same bar as §5: focused diff, green pipeline, conventional squash subject; PR body says what + why + how to test and links the issue (Closes #123).
  • DCO / CLA: if the project requires a Developer Certificate of Origin, sign off every commit — git commit -s adds a Signed-off-by: trailer (§9). Some orgs require a signed CLA instead; check CONTRIBUTING.md.
  • Respond to review promptly; push fixups to the same branch (the PR updates itself) — don’t open a new PR per revision.
  • Follow the Code of Conduct in every interaction; assume good faith, credit sources, never paste proprietary or unlicensed code.

7. History discipline

Treat shared history as immutable — the same principle as liquibase.md for migrations.

  • Never rewrite published history on main/develop (no force-push, no amend of pushed commits). Fix forward with a revert or a new commit.
  • Local, unpushed history is yours to clean up: git rebase -i, squash, reorder, reword — before opening/updating the PR for review.
  • Revert a bad commit on a protected branch with git revert <sha> (creates an inverse commit; preserves history).
  • Tags are immutable: cut vX.Y.Z, never move or delete a released tag (tag ruleset blocks update/delete/force).

8. Secrets & sensitive data

  • Never commit API keys, tokens, passwords, connection strings, private keys, .env files. CI runs secret scanning (TruffleHog) and will fail the build.
  • Pre-commit grep before pushing: look for sk-, ghp_, AKIA, password=, secret=, BEGIN PRIVATE KEY.
  • If a secret is committed: rotate it immediately (assume it’s compromised), then purge from history (git filter-repo) and force-push with coordination — rotation first, scrubbing second.
  • Keep secrets in the platform’s secret store / CI secrets, referenced by env var (see security.md).

9. .gitignore & repo hygiene

  • Commit a .gitignore covering build output (build/, target/, dist/, node_modules/), IDE files (.idea/, .vscode/), OS cruft (.DS_Store), and local env (.env, *.local).
  • Keep gradle/wrapper/gradle-wrapper.jar and lockfiles (*.lock, pnpm-lock.yaml) — they’re reproducibility inputs, not artifacts.
  • Large binaries → Git LFS, not the main tree. Better: don’t version build outputs at all; rebuild them in CI.
  • One concern per repo; don’t vendor unrelated tooling into product repos.

10. Signing & identity

  • Configure a real user.name/user.email matching your account.
  • Sign commits (GPG/SSH/Sigstore gitsign) where the org requires verified commits; rulesets can mandate signed commits on protected branches.
  • Bot/CI commits use the Actions identity and are the only actors allowed to bypass tag protection.

11. Hooks & local enforcement

Catch problems before they reach CI.

  • commit-msg hook → run commitlint locally so bad messages fail at commit time, not in the pipeline.
  • pre-commit hook → formatter + linter (e.g. Spotless, ktlint, biome) and a secret-scan; keep it fast.
  • Manage hooks with a lightweight, zero-runtime-dep tool (e.g. simple-git-hooks / lefthook); commit the config so the whole team shares it.
  • Hooks are a convenience, not the source of truth — CI re-runs every check; never rely on local hooks alone.

12. Anti-patterns

git commit -m "fix" / “wip” / “stuff” conventional, descriptive subject
Mixing feature + refactor + formatting in one commit atomic commits, one concern
git push --force to a shared branch --force-with-lease, and never on protected branches
Amending/rebasing already-pushed shared history revert forward; rewrite only local commits
Long-lived feature branch diverging for weeks short-lived, rebased on develop often
Committing .env, keys, node_modules, build/ .gitignore + secret store + LFS
Merge commits into main squash merge, linear history
Moving/deleting a released tag tags immutable; cut a new version
Giant 2000-line PR split into small/stacked PRs (< ~400 lines)
Relying on local hooks only CI re-validates everything
Committing on your fork’s main/develop, or a stale fork track upstream; rebase before branching (§6)
Opening a fork PR with no prior issue/discussion issue-first; get maintainer buy-in (§6)

13. Quick reference

Terminal window
# start work
git switch develop && git pull --ff-only
git switch -c feature/order-pricing
# commit (conventional)
git add -p
git commit -m "feat(order): add tiered pricing calculator"
# stay current
git fetch origin && git rebase origin/develop
git push --force-with-lease
# clean local history before review
git rebase -i origin/develop # squash/reword unpushed commits
# undo safely on a shared branch
git revert <bad-sha>
# open PR → squash-merge → branch auto-deleted by CI
# contribute to a repo you can't push to (fork-based, §6)
git remote add upstream git@github.com:<ORG>/<repo>.git
git fetch upstream && git switch -c feature/<slug> upstream/develop
git push -u origin feature/<slug> # origin = your fork; PR base = <ORG>:develop

Git usage + conventional commits + branching that CI enforces. Pairs with github-actions.md (commit-lint, semver, rulesets), adr.md (record significant decisions in the PR), and liquibase.md (same immutability principle for shared history).