Skip to content

Git & Version Control Standards

  • Main Branch: main (or master for legacy). Protected branch. All production code.
  • Development Branch: develop (optional). Integration branch for features.
  • Feature Branches: feature/description (e.g., feature/user-authentication)
  • Bug Fixes: fix/description (e.g., fix/email-validation)
  • Hotfixes: hotfix/description (e.g., hotfix/security-patch)
  • Releases: release/version (e.g., release/1.2.0)
  • Format: type/description (kebab-case)
  • Branch prefixes: feature, fix, hotfix, release, refactor, docs, test
  • Description: Concise, descriptive (3-5 words)

Branch prefixes are not commit types. The words above name branches (feature/user-auth, hotfix/security-patch). Commit messages use the Conventional Commits types below — note feat, not feature, and there is no hotfix/release commit type. The canonical commit-type set is defined once in shared/core-standards.md (§ Commit Messages); this document must stay aligned with it.

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

The canonical cross-cutting set (from shared/core-standards.md) is: feat, fix, refactor, test, docs, chore, perf, ci.

  • feat: New feature
  • fix: Bug fix
  • refactor: Code refactoring (no functional changes)
  • test: Adding or updating tests
  • docs: Documentation changes
  • chore: Maintenance tasks (dependencies, build config)
  • perf: Performance improvements
  • ci: CI/CD changes

Additional Conventional Commits types the linter also accepts (scripts/lint-checks/common/conventional-commits.sh) but which fall outside the canonical set: build (build-system changes), style (formatting-only changes), revert (reverting a prior commit). Prefer the canonical types.

  • Subject: Imperative mood, 50 characters or less, no period
  • Body: Explain “what” and “why”, wrap at 72 characters
  • Footer: Reference issues/PRs: Closes #123, Fixes #456
  • Scope: Optional, indicates area of change (e.g., domain, api, ui)
feat(domain): add user email validation
Implement RFC 5322 compliant email validation in Email value object.
Rejects invalid formats at domain boundary to fail fast.
Closes #123
fix(api): handle null response in user endpoint
Return 404 instead of 500 when user not found. Prevents server
errors from propagating to client.
Fixes #456
  • One Logical Change: Each commit should represent one complete, logical change
  • Small Commits: Prefer multiple small commits over one large commit
  • Testable: Each commit should leave the codebase in a working state
  • Regular Commits: Commit frequently (at least daily during active development)
  • Logical Units: Commit when a logical unit of work is complete
  • Before Break: Commit before leaving work for extended periods
  • Source Code: All source code, tests, and configuration
  • Documentation: README, docs, comments
  • Configuration: Build files, CI config, editor configs
  • Dependencies: Lock files (package-lock.json, Cargo.lock, etc.)
  • Secrets: API keys, passwords, tokens, credentials
  • Build Artifacts: Compiled binaries, dist/, build/, target/
  • Dependencies: node_modules/, venv/, .env files
  • IDE Files: .idea/, .vscode/ (unless project-specific settings)
  • OS Files: .DS_Store, Thumbs.db
  1. Create feature branch from main or develop
  2. Make atomic commits with descriptive messages
  3. Push branch regularly (at least daily)
  4. Open Pull Request when feature is complete
  5. Address review feedback with additional commits
  6. Squash commits if requested during review
  7. Merge via Pull Request (no direct pushes to main)
  • Title: Follow conventional commit format
  • Description: Explain what, why, and how. Include screenshots for UI changes
  • Size: Keep PRs focused and reviewable (< 400 lines when possible)
  • Tests: Include tests for new features and bug fixes
  • Documentation: Update documentation for user-facing changes
  • CI: All CI checks must pass before merge
  • Required: At least one approval before merge
  • Response Time: Review within 24-48 hours
  • Feedback: Be constructive and specific
  • Approval: Approve only when code meets standards
  • Squash and Merge: Preferred for feature branches (clean history)
  • Merge Commit: Use for important features (preserve branch context)
  • Rebase: Use for keeping feature branches up to date (avoid merge commits)

Format: MAJOR.MINOR.PATCH (e.g., 1.2.3)

  • MAJOR: Breaking changes
  • MINOR: New features (backward compatible)
  • PATCH: Bug fixes (backward compatible)
  • Format: v1.2.3 (prefixed with v)
  • Annotated Tags: Use annotated tags for releases: git tag -a v1.2.3 -m "Release 1.2.3"
  • Release Notes: Include release notes in tag message or GitHub release
  1. Update version in code and CHANGELOG.md
  2. Create release branch: release/v1.2.3
  3. Final testing and bug fixes
  4. Merge to main and tag: git tag -a v1.2.3
  5. Push tag: git push origin v1.2.3
  6. Create GitHub/GitLab release with notes
  7. Merge back to develop if applicable

For products that deploy continuously, distinguish two version numbers:

  • Release version — manual semantic version (MAJOR.MINOR.PATCH), bumped deliberately at a release. It is what users see and what gates a coordinated release (e.g. apps and marketing site move together on a release-version bump).
  • Build version — derived automatically and ticking on every commit to the mainline (e.g. git rev-list --count HEAD). Useful for “exact build” reporting and crash triage, never for user-facing semver decisions.

Surface both in an About/diagnostics view. A value only tree-shakes in if it is actually rendered — exporting it isn’t enough.

  • Separate staging/preview from production by channel. The mainline branch auto-deploys to the staging channel; production is gated on an explicit promotion. Never deploy production off arbitrary feature branches.
  • If the deploy platform’s branch control takes no wildcard, gate production on a single long-lived release branch (not release/*) and promote by fast-forwarding mainline into it (git push origin main:release).
  • Channel-gate not-yet-released content (e.g. an “unreleased” changelog section visible only on the staging channel) via a build-time channel variable. Verify the variable name matches what the deploy platform actually injects — a typo (PUBLIC_CHANNEL vs VITE_CHANNEL) silently ships the wrong content.
  • A green release does not prove production updated. End the release with an automated version assertion against the live deployment (fetch it, assert the new release/build number), not “the promote step succeeded”. The deploy platform’s credentials and branch protections live outside the repo and fail invisibly — a rolled or deleted build token, or a force-push-protected release branch that silently rejects the fast-forward promote — so a green in-repo pipeline is necessary but not sufficient. See arch-08_ci_cd_pipeline_standards.md §7 for the CI-side smoke check.

Automate the release cut as a script that produces a reviewable PR, not a manual sequence of edits:

  • A pure script performs the semver bump and promotes changelog entries (unreleased/ → the released version section), and emits the release-notes body.
  • The script opens (or prepares) a PR for human review before anything is tagged or promoted. Tagging and channel promotion happen only after that PR merges.
  • Mind CI-identity loop-prevention: a PR opened by a CI token often does not trigger the normal PR gate, so a fully hands-free release usually needs a dedicated app token or a human to open/merge the cut PR. Document which model the repo uses.

See arch-08_ci_cd_pipeline_standards.md §7 for the CI side of channel promotion.

Terminal window
# User identification
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
# Default branch name
git config --global init.defaultBranch main
# Push behavior
git config --global push.default simple
git config --global push.autoSetupRemote true
# Pull behavior
git config --global pull.rebase false # Use merge by default
Terminal window
# Color output
git config --global color.ui auto
# Editor
git config --global core.editor "code --wait" # VS Code

See Git Aliases Reference for the full alias catalog. Run scripts/setup-git-aliases.sh to install.

  • Linting: Run linters (ESLint, Pylint, Clippy)
  • Formatting: Auto-format code (Prettier, Black, rustfmt)
  • Tests: Run fast unit tests
  • Validation: Check commit message format
  • Tests: Run full test suite
  • Type Checking: Run type checkers (TypeScript, mypy)
  • Build: Verify project builds successfully
  • Format Validation: Ensure commit messages follow conventional format
  • Length Check: Validate subject line length

Use tools like:

  • Husky (Node.js)
  • pre-commit (Python)
  • git-hooks (Rust)
  • Custom shell scripts

Python:

__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
.venv

Node.js:

node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
dist/
build/

Rust:

target/
Cargo.lock # For libraries, not applications

Java:

*.class
*.jar
*.war
*.ear
.gradle/
build/

Ignore generated coverage reports so they are never committed:

coverage/

Common tools that write here: Vitest, Jest, pytest, Istanbul/c8, lcov. Projects using the standards setup or install scripts get this pattern added automatically.

# Environment
.env
.env.local
.env.*.local
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs/

Use Git LFS for:

  • Binary files > 100MB
  • Media files (images, videos)
  • Compiled binaries
  • Database dumps
Terminal window
git lfs install
git lfs track "*.psd"
git lfs track "*.zip"

Branches MUST be deleted — both local and remote — immediately after their PR is merged or closed. Do not accumulate stale branches.

  • PR merge: Enable “Automatically delete head branches” in GitHub repo settings. This handles the remote branch. Delete the local branch manually after merge.
  • Squash merges: Squash-merged branches are not detected by git branch --merged. Use git branch -d <branch> (lowercase -d) which checks if the branch is fully merged; if it refuses, verify the work landed via PR, then use git branch -D <branch>.
  • Worktrees: Remove the worktree before deleting its branch: git worktree remove <path> then git branch -D <branch>.

Include enough context in the branch name to identify the work without checking the log:

feat/preview-scroll-optimization ✓ clear purpose
fix/sidebar-drag-crash ✓ clear purpose
worktree-agent-a0939472 ✗ opaque, impossible to triage later
tmp ✗ no context
copilot/sub-pr-7 ✗ meaningless without the PR

Agent-generated branches MUST follow the same type/description convention as human branches. Opaque IDs or numeric suffixes alone are not acceptable names.

Run a branch audit at least every two weeks (or before starting a new feature):

Terminal window
# List local branches not on main, sorted by last commit date
git branch --no-merged main --format='%(committerdate:short) %(refname:short)' | sort
# List remote branches with no recent activity (>14 days)
git for-each-ref --sort=committerdate --format='%(committerdate:short) %(refname:short)' refs/remotes/origin | head -20
# Delete local branches whose remote is gone
git fetch --prune
git branch -vv | grep ': gone]' | awk '{print $1}' | xargs -r git branch -D

Projects using make SHOULD include a cleanup target:

branch-cleanup: ## Delete local branches whose remote tracking branch is gone
@git fetch --prune
@git branch -vv | grep ': gone]' | awk '{print $$1}' | xargs -r git branch -D
@echo "Remaining branches:" && git branch

As a guideline, a repository should have fewer than 10 active branches at any time. If you have more, audit and clean up before creating new ones. This applies to both local and remote branches (excluding main).

  • Run git gc periodically (usually automatic).
  • After large cleanups, run git gc --prune=now to reclaim space immediately.
  • Secrets Scanning: Use tools like git-secrets, truffleHog
  • History Rewriting: Avoid force-pushing to shared branches
  • Access Control: Use branch protection rules (GitHub/GitLab)
  • Remote Repository: Always push to remote (GitHub, GitLab, etc.)
  • Multiple Remotes: Consider backup remote for critical projects
  • Regular Pushes: Push at least daily during active development

11. Stacked & Dependent PRs and Platform Automation Traps

Section titled “11. Stacked & Dependent PRs and Platform Automation Traps”

When PRs depend on each other or are driven by automation, a handful of platform behaviors silently cause lost work or false signals. Encode them.

When PR B is based on PR A’s branch (B’s base = A’s feature branch):

  • Merging A with delete-branch (gh pr merge A --squash --delete-branch) closes B — the host does not auto-retarget B to mainline, and B cannot be reopened once its base branch is gone.
  • Avoid it: before merging A, either retarget B to mainline first (gh pr edit B --base main) while A’s branch still exists, or merge A without deleting its branch until B is retargeted.
  • Recover (if B was already closed by an A merge): in B’s worktree, git rebase --onto origin/main <A-tip-commit> to drop A’s now-redundant commits (their content is in mainline via the squash) and replay only B’s own commits, then open a fresh PR B′ → mainline referencing the closed one.

A PR with merge conflicts has no merge ref, so the host runs none of its merge-ref-triggered workflows — it looks like CI never fired (zero runs created, not even queued). If a PR’s checks are entirely absent, check its mergeable state and resolve the conflict; the checks then trigger normally. Distinguish this from a self-hosted runner being offline (which produces a queued run that waits).

  • One close-keyword per line. Closes #A, #B, #C only auto-closes the first issue. Write one Closes #X per line to fan out across issues.
  • Check polling can settle early. Right after a push, only fast external checks are registered; host-run jobs appear seconds later. A poll that exits as soon as “all known checks are non-pending” can exit before real CI starts — guard with a minimum-expected-check count.
  • gh pr create infers the branch from the current directory. Run it from the correct worktree, or pass --head <branch> explicitly, or it opens a PR for whatever branch the cwd is on. See proc-04_agent_workflow_standards.md § Worktree Hygiene.