Git and Commits
A change in this repo flows the same way every time: branch → commits → PR → review → squash-merge. The branch name, the commits on it, and the PR title all share one shape — <type>(<scope>): <description> — so the final merge commit on master reads cleanly without anyone having to rewrite history.
We follow this discipline because the merge commit isn't just for you. It feeds the Linear issue, the squash on master, the GitHub PR list, and the git log --oneline history that someone will grep through six months from now. If everyone names branches and commits the same way, releases summarise themselves and bisecting a regression is fast.
This chapter walks through the workflow, the naming format, the scopes you can pick from, and the pre-push hook that catches missing translations before they hit CI.
The shape — one example end to end
A typical change looks like this:
# Branch off master
git checkout -b feat/admincp-controllers-message-show
# Commit as you go (Conventional Commits)
git commit -m "feat(admincp-controllers): add show page for MessageController"
git commit -m "test(admincp-controllers): cover MessageController@show"
# Push (pre-push hook runs translations:check --changed)
git push -u origin feat/admincp-controllers-message-show
# Open a PR with title matching the same format,
# and body referencing the Linear issue (e.g. "Closes WIN-123")
The branch name, each commit, and the PR title all use <type>(<scope>): <description>. When the PR is squash-merged, the title becomes the single Conventional Commit on master and the linked Linear issue auto-closes.
The five-step workflow
Every change goes through these steps:
- Create a branch for the feature or issue.
- Open a pull request. The body MUST contain the associated Linear identifier (e.g.
Closes WIN-123). - Request a review when the PR is ready — Linear moves to "In Review" automatically.
- Iterate on review feedback if needed.
- Once approved, squash-merge. The squash collapses the branch into a single Conventional Commit on
master, and the linked Linear issue auto-closes.
master.Branch naming
Format: <type>/<scope>-<short-description>. Lowercase, kebab-cased. Think of the branch name as a preview of the merge commit.
✅ Good:
feat/admincp-web-routes-login-pages
fix/businesscp-resources-user-data
refactor/domain-services-provider-contract
fix/domain-actions-correct-order-total
❌ Bad:
my-fix
Haytham/login
WIN-123
update_stuff
Branches like my-fix or update_stuff give nobody useful information — neither you nor the reviewer can tell what's on it. Use the type and scope so the branch list itself is searchable.
Conventional Commits
Format: <type>(<scope>): <description>. Lowercase imperative mood in the description ("add", "fix", "remove" — not "added" / "adds" / "fixes").
Types
feat— new featurefix— bug fixrefactor— code change that is neither a feature nor a bug fixperf— refactor that improves performancetest— adding or updating testsstyle— code style only (formatting; not CSS)ci— continuous integration pipeline changesbuild— build pipeline changesdocs— README or documentation-only changeschore— anything else (deps, maintenance)
Examples
feat(domain-actions): add CreateOrder action with transaction support
fix(admincp-controllers): correct validation in UserController
refactor(domain-services): migrate ProviderService to use GetProvider action
perf(domain-entities): add eager loading for order relationships
docs(readme): update installation instructions
Scopes — domain or presentation panel
The scope identifies what part of the codebase changed. Pick the most specific scope that fits — usually a domain layer concept or a control panel.
Domain layer
domain-actions— Actionsdomain-contracts— Contractsdomain-services— Servicesdomain-dtos— DataTransferObjectsdomain-enums— Enumsdomain-exceptions— Exceptionsdomain-entities— Models / Entitiesdomain-scopes— Query Scopesdomain-abilities— Abilitiesdomain-model-traits— Model Traitsdomain-providers— Service Providersdomain-observers— Observersdomain-traits— Domain Traits
Presentation layer
Replace name with the control panel (admincp, businesscp, customercp, ownercp, providercp):
{name}cp-controllers{name}cp-requests{name}cp-providers{name}cp-resources{name}cp-api-routes{name}cp-web-routes
Common components
migrations— database migrationsmiddleware— HTTP middlewareroutes— route definitionsconfig— configuration filesevents— event classes and listenersjobs— queued jobsnotifications— notificationsassets— CSS / JS / imagestests— test filesexceptions— exception handlershelpers— helper functionslang— translations
PR titles
Same format as commits: <type>(<scope>): <description>. Keep titles concise; put detail in the body.
✅ Good:
feat(security-contracts): implement login functionality
fix(accounting-actions): correct user data retrieval
❌ Bad:
Login work
WIP - fixes
Updated some files
The PR body MUST reference the Linear issue — typically as Closes WIN-123 or Refs WIN-123. Without it, Linear has no idea this PR exists and the issue won't auto-close on merge.
PR body skeleton
Copy this every time:
## Summary
- Short bullet list of what changed and why.
## Test plan
- [ ] Manual steps the reviewer can follow.
- [ ] Any automated tests added.
Closes WIN-123
Writing commit messages
The subject (<type>(<scope>): <description>) must stand alone — it is what appears in git log --oneline, the squash commit on master, and the GitHub PR list. Aim for under 72 characters.
If the change needs context, add a blank line and then a body explaining the why. Do not restate what the diff already shows.
❌ Bad — restates the diff:
refactor(domain-services): rename foo to bar
Renamed foo to bar in OrderService.
✅ Good — explains the why:
refactor(domain-services): rename foo to bar
`foo` collided with the new `Foo` enum introduced in WIN-119. Renaming
unblocks the enum migration without changing public API behavior.
The first version tells the reader nothing they couldn't get from git show. The second answers the question every future reader asks: "why was this changed?"
Examples by scope
feat(domain-actions): add CreateOrder action with transaction support
feat(admincp-controllers): add show page for AddDataRequest
fix(domain-entities): correct soft-delete cascade on Quotation
fix(migrations): drop duplicate index on orders.user_id
refactor(domain-services): migrate OrderService to delegate via Actions
perf(domain-scopes): use whereByType macro instead of repeated where()
test(domain-actions): cover CreateOrder transaction rollback paths
ci(build): bump PHP image to 8.3 in deploy workflow
docs(localization): document translations:check pre-push hook
chore(deps): bump laravel/framework to 12.4
Squash merging
PRs are squashed before merging to master:
- Multiple iteration commits on the branch collapse into a single Conventional Commit.
- That commit's subject is the PR title — ensure the title is well-formed before clicking merge.
- The linked Linear issue auto-closes once the squash lands.
- Force-pushing to the feature branch during review is fine; the squash erases it anyway.
master. Iteration noise belongs on the feature branch and gets erased by the squash. master history is permanent.What not to commit
Never stage these:
.env,.env.*— environment files.*-firebase-credentials.jsonand any other secret JSON.- Cloud provider keys, API tokens, private SSH keys.
- Local debug output,
storage/logs/*.log, generated PDFs, etc.
When staging, prefer naming specific files over git add -A or git add . — those can sweep in untracked secrets or large binaries by accident.
✅ Good:
git add src/Domain/Order/Actions/CreateOrder.php tests/Feature/CreateOrderTest.php
❌ Bad:
git add -A
The blanket form will happily stage the .env file you forgot to gitignore on a new machine.
The pre-push hook
The repo ships a pre-push hook (install via ./install-git-hooks.sh) that runs:
php artisan translations:check --changed
It validates translation integrity across all supported languages for the files in the push. If keys are missing in lang/en/ or lang/ar/, the push fails — add the missing translations and try again.
If the hook reports a missing key:
Missing translation: lang/ar/pages.php → your_resource
Add the key to the named file (and any other locale files reported), re-stage, commit, then push again. The hook only checks files in the current push, so isolated additions resolve quickly.
--no-verify. The missing keys will fail in CI anyway, and the next person to deploy gets stuck with a half-translated UI in production.What to read next
- Code review — what the reviewer is looking for.
- Admin resource recipe — the canonical workflow for the most common kind of feature.
- Refactoring legacy — how to land a refactor without breaking unmigrated callers.
- DDD overview — the layout names that show up in your branch and commit scopes.