<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Laravel Git Blog</title><description>Notes on Git, code review and shipping Laravel projects.</description><link>https://laravelgit.com/</link><language>en-gb</language><lastBuildDate>Fri, 30 Oct 2026 12:26:09 GMT</lastBuildDate><ttl>60</ttl><atom:link href="https://laravelgit.com/rss.xml" rel="self" type="application/rss+xml"/><item><title>A First CI Pipeline for a Laravel Project</title><link>https://laravelgit.com/blog/a-first-ci-pipeline-for-laravel/</link><guid isPermaLink="true">https://laravelgit.com/blog/a-first-ci-pipeline-for-laravel/</guid><description>Install, lint, test, build - four jobs that run on every pull request and turn &apos;works on my machine&apos; into a check everyone can see.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;CI does not need to be elaborate to be valuable. For a small Laravel team, a first pipeline is four jobs that run on every pull request, and its real product is a green checkmark everyone trusts.&lt;/p&gt;
&lt;h2&gt;The four jobs&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Install&lt;/strong&gt;: &lt;code&gt;composer install&lt;/code&gt; and &lt;code&gt;npm ci&lt;/code&gt;. If dependencies cannot install cleanly from the lockfiles, nothing else matters - this is why the lockfiles are committed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lint&lt;/strong&gt;: &lt;code&gt;./vendor/bin/pint --test&lt;/code&gt; fails the build on style drift, so style stops being a code review topic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test&lt;/strong&gt;: &lt;code&gt;php artisan test&lt;/code&gt; against a throwaway database (SQLite in memory is enough for a first pipeline).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build&lt;/strong&gt;: &lt;code&gt;npm run build&lt;/code&gt; proves the frontend compiles - the failure you do not want to meet at deploy time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Any pipeline runner can express this: GitHub Actions, GitLab CI, or the CI your self-hosted setup provides. The shape is the same everywhere: checkout, cache the composer/npm directories, run the four jobs.&lt;/p&gt;
&lt;h2&gt;Make the check required&lt;/h2&gt;
&lt;p&gt;A pipeline nobody is forced to wait for becomes decoration within a month. Wire it into &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;branch protection&lt;/a&gt;: pull requests cannot merge until CI is green. That single setting is the difference between &amp;quot;we have CI&amp;quot; and &amp;quot;CI has us&amp;quot; - and it is what lets &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small PRs&lt;/a&gt; move quickly, because reviewers see the robot&amp;#39;s verdict before they spend their own attention.&lt;/p&gt;
&lt;h2&gt;Keep it boring and fast&lt;/h2&gt;
&lt;p&gt;Two traps make teams abandon CI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slow runs.&lt;/strong&gt; Cache dependencies, and resist running the full browser-test suite on every push - that belongs in a nightly job. A PR pipeline that answers in under five minutes gets respected; one that takes forty gets bypassed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flaky tests.&lt;/strong&gt; A test that fails randomly teaches everyone to click &amp;quot;re-run&amp;quot; without looking, which is worse than no test. Quarantine flakes the week they appear.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What not to automate yet&lt;/h2&gt;
&lt;p&gt;Deploys. The first pipeline stops at the green checkmark on the PR; deployment stays a deliberate human act until the test suite earns more trust. When that day comes, the pipeline&amp;#39;s final job is a tag, and &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;Deploy With Tags, Not Branches&lt;/a&gt; takes it from there.&lt;/p&gt;
&lt;p&gt;Four jobs, one required check, five minutes. That is the whole first pipeline - everything after is refinement.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>A Staging Environment Worth Having</title><link>https://laravelgit.com/blog/a-staging-environment-worth-having/</link><guid isPermaLink="true">https://laravelgit.com/blog/a-staging-environment-worth-having/</guid><description>Staging only catches what it shares with production. Same deploy path, same services, sanitized data - and a clear rule for what it is for.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most staging environments fail quietly: they run different code, on different data, deployed by a different process, so a green staging test proves almost nothing and everyone learns to ignore it. A staging environment earns its cost by being production&amp;#39;s rehearsal room, and that property comes from three decisions.&lt;/p&gt;
&lt;h2&gt;Same deploy path&lt;/h2&gt;
&lt;p&gt;Deploy to staging exactly the way you deploy to production - same script, same &lt;a href=&quot;/blog/zero-downtime-deploys-for-laravel/&quot;&gt;releases and symlink structure&lt;/a&gt;, same &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;tag-based flow&lt;/a&gt;, differing only in target host. This is the highest-value property: most deploy failures are process failures, and you want to meet them on staging. If production deploys a tag and staging deploys &amp;quot;whatever branch someone pushed,&amp;quot; staging is testing a process you do not use.&lt;/p&gt;
&lt;h2&gt;Same shape of services&lt;/h2&gt;
&lt;p&gt;Match the services that change behavior: same PHP version, same database engine and version, a real queue worker, a real cache. SQLite on staging against MySQL in production hides the query that only MySQL rejects. The hardware can be smaller - the interfaces cannot.&lt;/p&gt;
&lt;h2&gt;Sanitized, realistic data&lt;/h2&gt;
&lt;p&gt;Copy production data down and scrub it: anonymize emails, strip tokens and personal fields, keep the volumes and shapes. &lt;code&gt;User::factory()&lt;/code&gt; data has perfect distributions and no history; real data has a user from 2019 with a null middle name that breaks your new export. Laravel&amp;#39;s model factories can help fill gaps, but the scrubbed copy is what finds migration surprises.&lt;/p&gt;
&lt;h2&gt;What staging is for&lt;/h2&gt;
&lt;p&gt;Decide, write it down, and the environment stays healthy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rehearse deploys and migrations before production.&lt;/li&gt;
&lt;li&gt;Let someone click through a feature before release.&lt;/li&gt;
&lt;li&gt;Reproduce a production bug without touching production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What it is not: a long-lived fork of production with its own data edits, or a second production for selected users. Both drift, and drifted staging is worse than none - it provides false confidence.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;/blog/managing-env-files-across-environments/&quot;&gt;env file&lt;/a&gt; for staging points at its own services but mirrors production&amp;#39;s key list, and the quarterly audit keeps it that way. Done right, &amp;quot;it worked on staging&amp;quot; is a sentence worth saying in an incident review.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Annotating Commits with git notes: Metadata Without Rewriting History</title><link>https://laravelgit.com/blog/annotating-commits-with-git-notes/</link><guid isPermaLink="true">https://laravelgit.com/blog/annotating-commits-with-git-notes/</guid><description>Some things belong on a commit but not in it: review links, benchmark results, the ticket nobody remembered to reference. git notes adds them after the fact, no rebase required.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every so often a commit needs a footnote. The reviewer found something worth remembering. The ticket reference went missing. A benchmark run proved the change was worth it. The message is already written and pushed, though - and &lt;a href=&quot;/blog/why-you-should-force-push-with-lease/&quot;&gt;rewriting pushed history&lt;/a&gt; is a bad trade for a footnote.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git notes&lt;/code&gt; is the feature for exactly this: annotations that attach to a commit without changing the commit.&lt;/p&gt;
&lt;h2&gt;The basics&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git notes add -m &amp;quot;Reviewed in PR #412, follow-up filed as BILL-88&amp;quot; HEAD
git log --show-notes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The note shows up in &lt;code&gt;git log&lt;/code&gt; output under the commit message. The commit hash does not change - the note lives in a separate ref (&lt;code&gt;refs/notes/commits&lt;/code&gt;), so nothing is rewritten and nobody else&amp;#39;s clone breaks.&lt;/p&gt;
&lt;p&gt;Notes do not push by default, which surprises everyone once:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push origin refs/notes/commits
git fetch origin refs/notes/commits:refs/notes/commits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Share them by pushing the notes ref; teammates fetch it explicitly. It is one extra step, once, per clone.&lt;/p&gt;
&lt;h2&gt;What Laravel teams actually use them for&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linking commits to review.&lt;/strong&gt; A note with the pull request URL makes &lt;code&gt;git blame&lt;/code&gt; and &lt;a href=&quot;/blog/reading-git-history/&quot;&gt;history archaeology&lt;/a&gt; richer months later, without touching the original message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recording verification.&lt;/strong&gt; &amp;quot;Deployed to staging 2026-10-20, smoke tests green&amp;quot; - evidence attached to the exact commit it concerns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explaining the weird one.&lt;/strong&gt; The commit that looks wrong but is deliberate gets a note saying why, so the next person does not &amp;quot;fix&amp;quot; it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The honest limits&lt;/h2&gt;
&lt;p&gt;Notes are the right size for footnotes, not documentation. They are easy to forget (out of &lt;code&gt;git log&lt;/code&gt; unless you ask), awkward to merge when two people annotate the same commit, and invisible on most web UIs - GitHub shows them in the API but not prominently in the interface. If the information matters to everyone every day, put it in the commit message or the changelog instead.&lt;/p&gt;
&lt;p&gt;Reach for notes when the annotation is valuable to the future archaeologist and not worth a history rewrite: that is a real niche, and nothing else in Git fills it as cleanly.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Backing Up Your Git Server So It Actually Restores</title><link>https://laravelgit.com/blog/backing-up-your-git-server/</link><guid isPermaLink="true">https://laravelgit.com/blog/backing-up-your-git-server/</guid><description>Every clone is a partial backup - but hooks, config and server state live outside the repo. Mirror, bundle, copy off-site, and rehearse the restore.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Self-hosting your Git means the server is the canonical copy. Developers&amp;#39; clones soften the blow of losing it - every clone has the history - but a clone is not a backup of the server: hooks, server config, access rules and anything outside the refs live nowhere else. A backup plan has to cover both, and it only counts if a restore has been rehearsed.&lt;/p&gt;
&lt;h2&gt;Mirror the repositories&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone --mirror git@yourserver:app.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A mirror clone copies every ref - branches, tags, notes - and none of the working-tree state. To refresh it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git -C app.git remote update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two lines in a nightly cron and every repository on the server has a current, complete copy somewhere else. On a small server, loop over the bare repos directory and mirror each one.&lt;/p&gt;
&lt;h2&gt;Bundle for portability&lt;/h2&gt;
&lt;p&gt;For a single-file, copy-anywhere backup of one repo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bundle create app-2026-10-21.bundle --all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A bundle is a real Git pack: &lt;code&gt;git clone app-2026-10-21.bundle app&lt;/code&gt; reconstructs the repository on any machine with Git and nothing else. Bundles are the right shape for weekly off-site copies - one dated file per repo, uploaded to object storage or another box.&lt;/p&gt;
&lt;h2&gt;Copy what Git does not track&lt;/h2&gt;
&lt;p&gt;The repositories are the obvious half. The other half lives next to them: server hooks (your deploy and protection scripts), the Git host&amp;#39;s own configuration, user and access data. A plain &lt;code&gt;rsync -a&lt;/code&gt; of those directories into the same off-site destination covers it. Losing the repos is painful; losing the hooks and config is what turns a restore into a rebuild.&lt;/p&gt;
&lt;h2&gt;Rehearse the restore&lt;/h2&gt;
&lt;p&gt;A backup you have never restored is a hope, not a plan. Once a month, pick one bundle or mirror and restore it to scratch space: clone from it, run the test suite, check the latest commits are there. Ten minutes, and the night the server actually dies becomes an inconvenience instead of an incident.&lt;/p&gt;
&lt;p&gt;The honest summary: mirror nightly, bundle weekly off-site, rsync the config, restore monthly. Four small habits, and the machine your whole team depends on stops being a single point of failure.&lt;/p&gt;
</content:encoded><category>Self-hosting</category><category>Tooling</category></item><item><title>Branch Naming Conventions That Scale Past Three People</title><link>https://laravelgit.com/blog/branch-naming-conventions-for-small-teams/</link><guid isPermaLink="true">https://laravelgit.com/blog/branch-naming-conventions-for-small-teams/</guid><description>Branch names are a user interface for your team&apos;s work. The conventions that survive growth, the ones that collapse, and why boring always wins.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With two developers, branch names do not matter - you both know what everything is. Add a third, then a fifth, and suddenly &lt;code&gt;fix-stuff&lt;/code&gt; and &lt;code&gt;stefan-wip-2&lt;/code&gt; are a shared inbox full of unmarked letters. A naming convention is not bureaucracy; it is how a branch list becomes readable at a glance.&lt;/p&gt;
&lt;h2&gt;The shape that works&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;BILL-88-retry-failed-invoices
feature/team-invitations
hotfix/checkout-double-charge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three ingredients, in rough order of value:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A ticket reference when one exists.&lt;/strong&gt; &lt;code&gt;BILL-88-retry-failed-invoices&lt;/code&gt; links the branch to the tracker without opening anything. Tools pick this up automatically - GitHub and GitLab cross-reference, and the PR inherits the link. When there is no ticket, skip it; inventing ceremony for a typo fix is worse than omitting it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lowercase words joined by dashes.&lt;/strong&gt; No underscores, no camelCase, no spaces escaped into &lt;code&gt;%20&lt;/code&gt;. Branch names appear in URLs, CI logs and shell commands; the boring format survives all three.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A type prefix for the exceptions.&lt;/strong&gt; &lt;code&gt;hotfix/&lt;/code&gt; and &lt;code&gt;release/&lt;/code&gt; prefixes earn their keep because they signal urgency and different rules - a hotfix skips the queue (see &lt;a href=&quot;/blog/shipping-a-hotfix-without-the-drama/&quot;&gt;shipping a hotfix without the drama&lt;/a&gt;). Feature branches need no prefix; they are the default case.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The conventions that collapse&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Names as ownership.&lt;/strong&gt; &lt;code&gt;stefan-billing&lt;/code&gt; tells you who, not what - and the moment two people touch it, the name is a lie. Ownership lives in the PR assignee, not the branch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Version branches that never die.&lt;/strong&gt; &lt;code&gt;v2-redesign&lt;/code&gt; living for four months is not a branch, it is a fork with anxiety. If work takes that long, the answer is &lt;a href=&quot;/blog/feature-flags-and-trunk-based-development/&quot;&gt;feature flags&lt;/a&gt; and incremental merges, not a longer-lived branch name.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Over-specified formats.&lt;/strong&gt; &lt;code&gt;type/TEAM-1234/short-description/v1&lt;/code&gt; looks great in a wiki and collapses on contact with reality. Every required element is a place to be wrong.&lt;/p&gt;
&lt;h2&gt;Pair it with cleanup&lt;/h2&gt;
&lt;p&gt;Names decay faster than branches. A convention only stays legible if merged branches get deleted - most hosts offer this as a button or an automatic setting - and stale ones get pruned locally (&lt;code&gt;git fetch --prune&lt;/code&gt;, or the config version in &lt;a href=&quot;/blog/settings-worth-changing-in-your-git-config/&quot;&gt;five git config settings worth changing&lt;/a&gt;). A branch list of twelve current names beats a list of two hundred historical ones, whatever convention they follow.&lt;/p&gt;
&lt;p&gt;Pick the boring format, write it in CONTRIBUTING.md, and spend the saved energy on the work the branches exist for.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Branch Protection Rules That Stick</title><link>https://laravelgit.com/blog/branch-protection-rules-that-stick/</link><guid isPermaLink="true">https://laravelgit.com/blog/branch-protection-rules-that-stick/</guid><description>Three rules cover most of the risk on main: review required, tests green, no force-push. More than that tends to get routed around.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Branch protection fails in two ways: too little of it, and so much of it that the team routes around it. For a small team, three rules do most of the work.&lt;/p&gt;
&lt;h2&gt;1. Require a pull request and one review&lt;/h2&gt;
&lt;p&gt;Nobody pushes straight to main, including the senior who &amp;quot;just fixes a typo&amp;quot;. One reviewer is enough at this size — the point is a second pair of eyes, not a committee.&lt;/p&gt;
&lt;h2&gt;2. Require checks to pass&lt;/h2&gt;
&lt;p&gt;The test suite runs on the PR and must be green before merge. This is what keeps &amp;quot;main always runs&amp;quot; true in practice rather than in principle. If the suite is slow, fix the suite — do not downgrade the rule to &amp;quot;tests optional&amp;quot;.&lt;/p&gt;
&lt;h2&gt;3. No force-pushes to main&lt;/h2&gt;
&lt;p&gt;Shared history is append-only. Rewriting main breaks everyone&amp;#39;s clones and makes the audit trail meaningless. Rebasing stays on feature branches, where it belongs.&lt;/p&gt;
&lt;h2&gt;What to leave out until it hurts&lt;/h2&gt;
&lt;p&gt;Code-owner reviews, signed-commit requirements, linear-history-only, status checks from five tools — each adds friction that makes sense at a specific size or risk level. Add them when a real incident names the need, not because the settings page offers them.&lt;/p&gt;
&lt;p&gt;The test of a ruleset is whether the team still respects it in month six. Small and enforced beats thorough and bypassed.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Reviews</category></item><item><title>Cherry-Picking: One Commit Without the Branch</title><link>https://laravelgit.com/blog/cherry-picking-one-commit-in-git/</link><guid isPermaLink="true">https://laravelgit.com/blog/cherry-picking-one-commit-in-git/</guid><description>Cherry-pick copies a single commit onto your branch and leaves the rest behind. Right for hotfixes across release branches, wrong as a sync strategy.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A branch has ten commits and you need one of them - not the merge, not the history, just that change, on your branch. That is cherry-pick&amp;#39;s whole job.&lt;/p&gt;
&lt;h2&gt;The basic move&lt;/h2&gt;
&lt;p&gt;Find the hash on the source branch, then apply it where you stand:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --oneline release/1.4
git switch main
git cherry-pick 332a818
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git replays that commit&amp;#39;s diff on your current branch and makes a new commit with the same message. Note the new commit gets a new hash: the parent is different, so it is a copy, not a move - the original still lives on its branch.&lt;/p&gt;
&lt;h2&gt;Record where it came from&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git cherry-pick -x 332a818
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-x&lt;/code&gt; appends &amp;quot;(cherry picked from commit ...)&amp;quot; to the message. Six weeks later, when the release branch and main both contain what looks like the same fix, that line is the difference between &amp;quot;already backported&amp;quot; and an hour of comparing diffs. Use it every time the source branch will keep existing.&lt;/p&gt;
&lt;h2&gt;When it fights back&lt;/h2&gt;
&lt;p&gt;A cherry-pick can conflict exactly like a merge, usually because the surrounding code drifted between the branches. Resolve the files, then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git cherry-pick --continue   # or: git cherry-pick --abort
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--abort&lt;/code&gt; puts the branch back as if you never started, so a pick that is turning into a rewrite costs nothing to abandon.&lt;/p&gt;
&lt;h2&gt;The Laravel-shaped case&lt;/h2&gt;
&lt;p&gt;A hotfix lands on main during the week; the release branch for 1.4 tags on Friday and needs that fix but none of the week&amp;#39;s other work. Cherry-pick it with &lt;code&gt;-x&lt;/code&gt;, run the suite, tag. The release notes stay honest and the histories traceable.&lt;/p&gt;
&lt;h2&gt;Where it goes wrong&lt;/h2&gt;
&lt;p&gt;Cherry-pick is for exceptions, and it turns sour as routine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The same fix picked onto four branches is four divergent commits to reconcile later. If every active branch needs a change, land it on the oldest branch that needs it and merge forward instead.&lt;/li&gt;
&lt;li&gt;Picking whole features commit-by-commit rewrites review history into a queue of conflicts. That is what rebases and merges are for.&lt;/li&gt;
&lt;li&gt;Because the copy has a new hash, tooling that matches commits by hash - and teammates comparing branches - will see two commits. &lt;code&gt;-x&lt;/code&gt; keeps that readable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Used sparingly and always with &lt;code&gt;-x&lt;/code&gt;, cherry-pick is the scalpel of the toolbox: not how you move work, exactly how you move one piece of it.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Cleaning Untracked Files Without Regret</title><link>https://laravelgit.com/blog/cleaning-untracked-files-safely/</link><guid isPermaLink="true">https://laravelgit.com/blog/cleaning-untracked-files-safely/</guid><description>git clean deletes files no commit has ever held - there is no reflog for them. One dry-run flag and one .env warning make it safe.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Untracked files accumulate: scratch scripts, a dump from debugging, build leftovers from a branch with different tooling, the config copy you made &amp;quot;just for a minute&amp;quot;. &lt;code&gt;git clean&lt;/code&gt; is how you clear them - and it is one of the few Git commands whose victims are genuinely unrecoverable, because untracked files exist in no commit, no stash, no reflog.&lt;/p&gt;
&lt;h2&gt;Always dry-run first&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clean -nd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-n&lt;/code&gt; lists what would go, without touching anything. &lt;code&gt;-d&lt;/code&gt; includes directories, which is where most of the mess lives. Read the list; if it is all junk, run it for real:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clean -fd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole safe workflow: &lt;code&gt;-n&lt;/code&gt;, read, then &lt;code&gt;-f&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The -x warning, especially on Laravel apps&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clean -fdx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-x&lt;/code&gt; also removes &lt;em&gt;ignored&lt;/em&gt; files - everything in your .gitignore. On a Laravel project that includes &lt;code&gt;.env&lt;/code&gt;, and &lt;code&gt;git clean -fdx&lt;/code&gt; deletes your .env with no backup and no recovery. It also deletes &lt;code&gt;vendor/&lt;/code&gt; and &lt;code&gt;node_modules/&lt;/code&gt;, which at least reinstall.&lt;/p&gt;
&lt;p&gt;Reach for &lt;code&gt;-x&lt;/code&gt; only when you truly mean &amp;quot;make this clone pristine&amp;quot; - and even then, copy &lt;code&gt;.env&lt;/code&gt; aside first. The everyday clean is &lt;code&gt;-fd&lt;/code&gt; without the x.&lt;/p&gt;
&lt;h2&gt;Pick instead of sweeping&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clean -i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Interactive mode lists candidates and lets you choose one by one, by pattern, or toggle each. Right answer when the directory mixes junk with files you are not sure about.&lt;/p&gt;
&lt;h2&gt;What not to clean&lt;/h2&gt;
&lt;p&gt;Framework caches are not &lt;code&gt;git clean&lt;/code&gt; territory. Stale config, route and view caches on a Laravel app belong to &lt;code&gt;php artisan optimize:clear&lt;/code&gt; - it knows what to rebuild and touches nothing else. Save &lt;code&gt;git clean&lt;/code&gt; for what the framework does not own: experiment leftovers, wrong-branch artifacts, scratch files.&lt;/p&gt;
&lt;p&gt;And when you are unsure whether something is junk, that is what &lt;code&gt;git stash -u&lt;/code&gt; is for. A stash can be popped next week; a cleaned file cannot come back at all.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>CODEOWNERS Files: Automatic Reviewers for the Risky Parts of Your Repo</title><link>https://laravelgit.com/blog/codeowners-files-for-laravel-teams/</link><guid isPermaLink="true">https://laravelgit.com/blog/codeowners-files-for-laravel-teams/</guid><description>A CODEOWNERS file routes reviews to the people who know each part of the codebase. How the syntax works, where Laravel teams get the most value, and the failure modes to avoid.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some pull requests are fine with any reviewer. Some are not: a migration touching a billing table, a change to the deploy script, a tweak to authentication middleware. For those, you want a specific pair of eyes - and you want that to happen by default, not because someone remembered to ask.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;CODEOWNERS&lt;/code&gt; file encodes that routing. When a pull request touches a path with an owner, the platform automatically requests the owner&amp;#39;s review, and &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;branch protection&lt;/a&gt; can require their approval before merge.&lt;/p&gt;
&lt;h2&gt;The syntax&lt;/h2&gt;
&lt;p&gt;The file lives at &lt;code&gt;.github/CODEOWNERS&lt;/code&gt; (GitHub) or &lt;code&gt;.gitlab/CODEOWNERS&lt;/code&gt; (GitLab), and each line is a pattern followed by one or more owners:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Default owner for everything
* @stefan

# Database changes need the data team
database/migrations/ @stefan @amira

# Deploy and CI config
deploy.sh @ops-team
.github/workflows/ @ops-team

# One specific file
config/services.php @stefan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Patterns follow gitignore-style matching. Later lines override earlier ones for the paths they match, so put the general rules first and the specific ones last.&lt;/p&gt;
&lt;h2&gt;Where Laravel teams get the most value&lt;/h2&gt;
&lt;p&gt;You do not need to own every path. Two or three rules cover most of the risk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;database/migrations/&lt;/code&gt;&lt;/strong&gt; - migrations are hard to roll back and easy to get subtly wrong. Our post on &lt;a href=&quot;/blog/migrations-in-code-review/&quot;&gt;reviewing migrations&lt;/a&gt; is the checklist your owner will run through.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy and CI configuration&lt;/strong&gt; - &lt;code&gt;deploy.sh&lt;/code&gt;, workflow files, &lt;code&gt;docker-compose.yml&lt;/code&gt;. A broken deploy script costs the whole team&amp;#39;s afternoon.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth and payment code&lt;/strong&gt; - the directories where a mistake is a security incident or a billing bug, not just a UI glitch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything else can stay with the team-wide default: anyone reviews, someone approves.&lt;/p&gt;
&lt;h2&gt;The failure modes&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Owner becomes a bottleneck.&lt;/strong&gt; If every migration needs one person&amp;#39;s approval and they are on holiday, work stops. Name at least two owners for critical paths, and keep the required-approvals count at one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File goes stale.&lt;/strong&gt; People change roles, teams change shape. A CODEOWNERS file pointing at someone who left is worse than none - it creates the illusion of review. Re-read it when the team changes, the same way you re-read branch protection rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ownership as territory.&lt;/strong&gt; The file routes review; it does not grant veto over ideas. If &amp;quot;owner approval required&amp;quot; starts meaning &amp;quot;nobody else may touch this,&amp;quot; that is a team problem the file merely made visible.&lt;/p&gt;
&lt;p&gt;Start small: one file, three rules, two owners per critical path. The first time an automatic review request catches a risky migration before it merges, the file has paid for itself.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Conventional Commits for a Small Team</title><link>https://laravelgit.com/blog/conventional-commits-for-a-small-team/</link><guid isPermaLink="true">https://laravelgit.com/blog/conventional-commits-for-a-small-team/</guid><description>A prefix on every commit message - feat, fix, chore - makes the log scannable and the changelog half-written. Keep the rules short or the team routes around them.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Six months of commit messages that say &amp;quot;fix&amp;quot;, &amp;quot;wip&amp;quot; and &amp;quot;changes&amp;quot; tell the next developer nothing - and on a small team the next developer is usually you. Conventional Commits add one prefix to every message and turn the log into something you can actually read.&lt;/p&gt;
&lt;h2&gt;The whole format&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type(scope): short summary
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The types that earn their keep on a Laravel app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat&lt;/code&gt; - new behaviour (&lt;code&gt;feat(billing): add invoice PDF download&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix&lt;/code&gt; - a bug (&lt;code&gt;fix(queue): stop retrying permanently failed jobs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refactor&lt;/code&gt; - same behaviour, different code&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt; - tests only&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chore&lt;/code&gt; - dependencies, config, tooling&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs&lt;/code&gt; - the README and friends&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The scope is optional but cheap: one word naming the part of the app (&lt;code&gt;billing&lt;/code&gt;, &lt;code&gt;queue&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt;). The summary stays imperative and under about 70 characters.&lt;/p&gt;
&lt;h2&gt;What it buys&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git log --oneline&lt;/code&gt; becomes a changelog draft: everything user-visible is a &lt;code&gt;feat&lt;/code&gt; or &lt;code&gt;fix&lt;/code&gt;, everything else filters out. That is exactly the input the tags-to-changelog habit needs, and it makes releases a paste-and-edit job instead of an archaeology dig. Review gets faster too - a &lt;code&gt;refactor&lt;/code&gt; prefix sets expectations before the diff loads, and nobody hunts for behaviour changes in a commit that announced there are none.&lt;/p&gt;
&lt;p&gt;Breaking changes get a &lt;code&gt;!&lt;/code&gt; (&lt;code&gt;feat(api)!: drop v1 endpoints&lt;/code&gt;) or a &lt;code&gt;BREAKING CHANGE:&lt;/code&gt; footer, so they are impossible to miss at release time.&lt;/p&gt;
&lt;h2&gt;Adopt it without a fight&lt;/h2&gt;
&lt;p&gt;The failure mode is process, not spelling: a team that gets a 12-type spec and a CI gate on day one starts writing &lt;code&gt;chore: stuff&lt;/code&gt; to make the check pass. Start smaller:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Five or six types, written in the README or pull request template.&lt;/li&gt;
&lt;li&gt;Scope optional, body only when the why is not obvious from the diff.&lt;/li&gt;
&lt;li&gt;No CI enforcement at first. Squash-merge pull requests and write the conventional message on the squash commit - main stays clean even when the branch&amp;#39;s own commits are messy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If it still feels like overhead after a month, drop it. A convention the team half-follows is worse than none, because it teaches people the log cannot be trusted. The bar is not a perfect history; it is a &lt;code&gt;git log&lt;/code&gt; you can skim before a release and believe.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Releases</category></item><item><title>Deploy Keys and Machine Users: Giving CI Access Without Handing Over Yours</title><link>https://laravelgit.com/blog/deploy-keys-and-machine-users-for-ci/</link><guid isPermaLink="true">https://laravelgit.com/blog/deploy-keys-and-machine-users-for-ci/</guid><description>CI needs to read your repo; your personal SSH key should not be how. Deploy keys, machine users and fine-grained tokens compared, with the scoping rules that matter.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Somewhere in the early days of many teams, someone generated a key on the CI server and added it to a developer&amp;#39;s GitHub account &amp;quot;just to get the build working.&amp;quot; That key now has that person&amp;#39;s full access to every repo they can touch - readable by anyone who can read the CI config. This is the access equivalent of the committed-secret problem in &lt;a href=&quot;/blog/a-secret-got-committed-now-what/&quot;&gt;a secret got committed, now what&lt;/a&gt;, and it deserves the same urgency.&lt;/p&gt;
&lt;h2&gt;Deploy keys: read-only, one repo&lt;/h2&gt;
&lt;p&gt;A deploy key is an SSH key attached to a single repository, not to a person. GitHub and GitLab both support them, and both let you make the key read-only.&lt;/p&gt;
&lt;p&gt;Use one when CI needs to clone one repo: your app repo for the test pipeline, the deployment target pulling code on the server. Generate a dedicated keypair on the machine, add the public half as a deploy key with read-only access, and revoke it by deleting the key - no person&amp;#39;s account involved. If you followed our &lt;a href=&quot;/blog/ssh-keys-for-git-access/&quot;&gt;SSH keys guide&lt;/a&gt;, this is the same mechanics, pointed at a repo instead of a user.&lt;/p&gt;
&lt;p&gt;The limitation is real: one deploy key cannot span repos. If CI must pull private Composer or npm packages from sibling repositories, a deploy key per repo works but multiplies keys fast.&lt;/p&gt;
&lt;h2&gt;Machine users and fine-grained tokens&lt;/h2&gt;
&lt;p&gt;When CI genuinely needs several private repos - a Laravel app plus two private packages - the options widen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A machine user&lt;/strong&gt;: a separate GitHub account that exists only for automation, granted read access to exactly the repos CI needs. Simple to reason about, and access is visible in the repo&amp;#39;s collaborator list. Costs a seat on some plans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A fine-grained personal access token&lt;/strong&gt;: scoped to specific repositories, read-only contents permission, with an expiry date. Lighter than a machine user, and the expiry forces regular rotation. Use the shortest expiry your rotation process can keep up with.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What both avoid is the original sin: a credential tied to a human&amp;#39;s full access, valid forever, sitting in a CI variable.&lt;/p&gt;
&lt;h2&gt;The rules that matter&lt;/h2&gt;
&lt;p&gt;Whatever mechanism you pick, three rules keep it healthy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read-only by default.&lt;/strong&gt; CI that only clones needs no write access. Grant push only to jobs that genuinely push (a release workflow tagging a version), and only on that job.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One credential per consumer.&lt;/strong&gt; The staging server&amp;#39;s key and the CI key are different keys, so revoking one never breaks the other and a leak has a smaller blast radius.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotation you can actually do.&lt;/strong&gt; A credential you cannot rotate without a meeting will never be rotated. Keep a short note of where each key lives; the day one leaks, you want a five-minute fix, not archaeology.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Access for automation is a solved problem - the only wrong answer left is your personal key in a CI variable.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Deploy With Tags, Not Branches</title><link>https://laravelgit.com/blog/deploy-with-tags-not-branches/</link><guid isPermaLink="true">https://laravelgit.com/blog/deploy-with-tags-not-branches/</guid><description>Environment branches drift, get force-pushed, and lie about what is live. A deploy that checks out a tag is boring, reproducible, and honest.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A common small-team setup has a &lt;code&gt;production&lt;/code&gt; branch: merge into it, push, and the server pulls it. It works until the day someone commits directly to it, force-pushes it, or merges the wrong thing and the branch history no longer matches what anyone intended. The branch stops being a record and becomes a rumor.&lt;/p&gt;
&lt;p&gt;A tag does not have these problems.&lt;/p&gt;
&lt;h2&gt;What deploying a tag looks like&lt;/h2&gt;
&lt;p&gt;The release flow is: finish work on main, tag the commit, deploy the tag.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git tag -a v1.14.0 -m &amp;quot;v1.14.0&amp;quot;
git push origin v1.14.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the server, instead of &lt;code&gt;git pull&lt;/code&gt;, the deploy script fetches and checks out the exact tag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch --tags
git checkout v1.14.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a detached HEAD on purpose - the server is not a place where commits happen, so there is nothing to lose. The result is that &amp;quot;what is running in production&amp;quot; has one unambiguous answer, and it is the same answer &lt;code&gt;git tag&lt;/code&gt; gives everyone on the team.&lt;/p&gt;
&lt;h2&gt;Why this beats an environment branch&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tags do not drift. A branch can pick up a stray commit; a tag points at one commit forever.&lt;/li&gt;
&lt;li&gt;Rollback is a checkout. Deploy the previous tag and you are back, with no revert commit needed - &lt;a href=&quot;/blog/when-to-reset-restore-or-revert/&quot;&gt;When to Reset, Restore, or Revert&lt;/a&gt; stays a developer-machine topic, not a 2 AM production topic.&lt;/li&gt;
&lt;li&gt;The release log writes itself. Tag messages and annotated-tag history double as the skeleton of a changelog, which is the whole trick in &lt;a href=&quot;/blog/keep-a-changelog-from-git-tags/&quot;&gt;Keep a Changelog Straight From Git Tags&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&quot;/blog/tagging-releases-properly/&quot;&gt;Tagging Releases Properly&lt;/a&gt; post covers annotation and naming; this one is about making the tag the unit of deployment.&lt;/p&gt;
&lt;h2&gt;Keeping staging honest&lt;/h2&gt;
&lt;p&gt;If you want a staging environment, deploy the same way: tag a release candidate (&lt;code&gt;v1.14.0-rc.1&lt;/code&gt;), deploy that tag to staging, and promote by re-tagging or tagging the final release on the same commit. &amp;quot;Staging runs something production has never seen&amp;quot; becomes structurally impossible.&lt;/p&gt;
&lt;p&gt;For the full pipeline around this - build steps, env handling, the push-to-server mechanics - &lt;a href=&quot;/blog/deploy-laravel-with-git-push/&quot;&gt;Deploy Laravel With Git Push&lt;/a&gt; walks through it. The short version of both posts: branches are for work in progress, tags are for facts. Deploy facts.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Does Your Team Need a Merge Queue? Probably Not Yet</title><link>https://laravelgit.com/blog/does-your-team-need-a-merge-queue/</link><guid isPermaLink="true">https://laravelgit.com/blog/does-your-team-need-a-merge-queue/</guid><description>Merge queues solve a real problem - broken main from semantically conflicting PRs - but the cure is heavier than the disease until a specific threshold. How to tell when you have crossed it.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A merge queue is CI&amp;#39;s answer to a sneaky failure: two pull requests, each green against main, that break when combined. One renames a function; the other adds a new call to the old name. Both pass, both merge, main is red. The queue fixes this by testing each PR against main plus everything already queued, so &amp;quot;green&amp;quot; means &amp;quot;green against what main will actually be.&amp;quot;&lt;/p&gt;
&lt;p&gt;GitHub, GitLab and Bors all offer one. The question is not whether they work - they do - but whether your team has the problem they solve.&lt;/p&gt;
&lt;h2&gt;The threshold&lt;/h2&gt;
&lt;p&gt;The broken-main-from-colliding-PRs problem needs two ingredients: enough parallel merges that collisions are likely, and a test suite slow enough that &amp;quot;just rebase and rerun CI&amp;quot; is painful. A team merging three pull requests a day with a five-minute suite almost never sees it. A team merging thirty a day with a forty-minute suite sees it weekly.&lt;/p&gt;
&lt;p&gt;Some honest signals you have crossed the line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Main breaks from semantic conflicts more than once a month, and each break costs real debugging time.&lt;/li&gt;
&lt;li&gt;Reviewers ask authors to &amp;quot;rebase and make sure it is still green&amp;quot; so often that the rebase-rerun loop is the bottleneck.&lt;/li&gt;
&lt;li&gt;Releases get delayed because main went red at the wrong moment.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What to do before you get there&lt;/h2&gt;
&lt;p&gt;Most small teams get the same protection cheaper:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Keep CI fast.&lt;/strong&gt; A five-minute suite makes &amp;quot;rebase and rerun&amp;quot; a shrug, not an afternoon. This is the same investment as always - &lt;a href=&quot;/blog/shallow-and-partial-clones-for-faster-ci/&quot;&gt;shallow clones&lt;/a&gt; and dependency caching - and it pays whether or not you ever need a queue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Require branches to be up to date before merging&lt;/strong&gt; on the riskiest repo. GitHub&amp;#39;s branch protection has this checkbox; it forces the rebase-rerun loop explicitly, which is exactly a manual merge queue. Fine at low volume, painful at high volume - which is your signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Merge promptly after approval.&lt;/strong&gt; Most collisions happen because approved PRs sit for days while main moves on. A queue will not fix a review-and-forget habit; &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small pull requests&lt;/a&gt; will.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When the manual loop becomes the thing slowing everyone down, the queue stops being infrastructure cosplay and starts paying rent. Until then, the honest answer is: not yet - and here is how you will know.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Use Draft Pull Requests to Get Feedback Early</title><link>https://laravelgit.com/blog/draft-pull-requests-for-early-feedback/</link><guid isPermaLink="true">https://laravelgit.com/blog/draft-pull-requests-for-early-feedback/</guid><description>Open the PR before the work is done. A draft invites direction-level feedback while changes are still cheap, and it shows the team what is in flight.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The traditional pull request arrives finished: code done, tests written, author braced for judgment. By that point the expensive decisions are locked in, and &amp;quot;have you considered a different approach?&amp;quot; means redoing two days of work. A draft PR flips the order - feedback lands while changing course is still cheap.&lt;/p&gt;
&lt;h2&gt;What a draft is for&lt;/h2&gt;
&lt;p&gt;Open the PR as a draft the moment the shape of the change exists: the migration, a sketched controller, maybe a failing test that names the behavior. The draft status says plainly: do not review the details yet, do look at the direction.&lt;/p&gt;
&lt;p&gt;That early look catches the reviews that matter most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;This duplicates what the billing package already does&amp;quot; - before you polish your version.&lt;/li&gt;
&lt;li&gt;&amp;quot;The column should live on the invoices table, not the users table&amp;quot; - before the migration ships.&lt;/li&gt;
&lt;li&gt;&amp;quot;We tried this approach in March, here is why it hurt&amp;quot; - before you rediscover March.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The working agreement&lt;/h2&gt;
&lt;p&gt;Drafts only work if the team reads them in the right spirit. Two conventions keep it healthy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The author says what they want. &amp;quot;Does this structure make sense?&amp;quot; gets a useful answer; silence about intent gets either no review or a full one you did not want yet. The &lt;a href=&quot;/blog/writing-a-good-pull-request-description/&quot;&gt;PR description&lt;/a&gt; matters more, not less, in a draft.&lt;/li&gt;
&lt;li&gt;Nobody treats a draft as a promise. Scope can shrink, approach can change, the branch can be rebased - drafts are &lt;a href=&quot;/blog/why-you-should-force-push-with-lease/&quot;&gt;force-push-with-lease&lt;/a&gt; territory by default.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Marking the PR ready for review is then a real signal: the direction is settled, now judge the details.&lt;/p&gt;
&lt;h2&gt;The quiet side benefit&lt;/h2&gt;
&lt;p&gt;Drafts double as a status board. A team that opens drafts early can see what is in flight without asking - which prevents the awkward moment where two people discover at merge time that they both spent the week on the same invoice controller. It also feeds the habit that keeps everything else moving: work arrives in &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small, reviewable pieces&lt;/a&gt;, because a draft that has been open for three weeks is visibly not that.&lt;/p&gt;
&lt;p&gt;Open it early, say what you want, mark it ready when it is. The PR stops being a verdict and becomes a conversation - which was the point of having a team.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Feature Flags: How to Merge Daily Without Shipping Half-Built Work</title><link>https://laravelgit.com/blog/feature-flags-and-trunk-based-development/</link><guid isPermaLink="true">https://laravelgit.com/blog/feature-flags-and-trunk-based-development/</guid><description>Long-lived feature branches exist because unfinished code cannot go to production. Feature flags remove that premise - and pair naturally with trunk-based development.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The argument for a long-lived feature branch goes: the work takes three weeks, it cannot ship half-done, so it lives on a branch until it is ready. The cost shows up at the end - a three-week merge, conflicts with everything that moved underneath, and a review nobody can hold in their head.&lt;/p&gt;
&lt;p&gt;Feature flags attack the premise instead of the branch. If unfinished code can sit in production safely, invisible behind a flag, there is no reason to keep it out of main.&lt;/p&gt;
&lt;h2&gt;What a feature flag is&lt;/h2&gt;
&lt;p&gt;A flag is a runtime check around new behaviour:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (feature(&amp;#39;new-billing-dashboard&amp;#39;)) {
    return view(&amp;#39;billing.dashboard-new&amp;#39;);
}
return view(&amp;#39;billing.dashboard&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel teams have good options for the &lt;code&gt;feature()&lt;/code&gt; part: Laravel Pennant (first-party, ships with recent Laravel versions), a config value, or an external service. The mechanism matters less than the habit: new behaviour ships dark, off by default, and turns on when it is ready - per user, per team, or for everyone.&lt;/p&gt;
&lt;h2&gt;Why it pairs with trunk-based development&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/blog/trunk-based-development-for-small-laravel-teams/&quot;&gt;Trunk-based development&lt;/a&gt; asks for small, frequent merges into main. The objection is always &amp;quot;but my feature is not done.&amp;quot; Flags answer it: merge the first slice behind a flag on Monday, the second on Wednesday, the third next week. Each merge is small, reviewed quickly, and deployed continuously - while users see nothing until you flip the flag.&lt;/p&gt;
&lt;p&gt;The bonus nobody expects: rolling out to 5% of users, then 50%, then everyone is free. And if something breaks, &amp;quot;roll back&amp;quot; is turning the flag off - no revert deploy, no drama, exactly the cheap-exit thinking in &lt;a href=&quot;/blog/rollback-or-roll-forward/&quot;&gt;rollback or roll forward&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The disciplines that keep flags healthy&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Name flags for the feature, not the date.&lt;/strong&gt; &lt;code&gt;new-billing-dashboard&lt;/code&gt; is clear in six months; &lt;code&gt;experiment-2026-10&lt;/code&gt; is not.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flag the smallest unit of behaviour.&lt;/strong&gt; One flag around one feature. A flag that controls three unrelated things cannot be turned off cleanly when one of them misbehaves.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Delete flags when the rollout finishes.&lt;/strong&gt; A flag that stays at 100% forever is an &lt;code&gt;if&lt;/code&gt; statement with extra steps. Make &amp;quot;remove the flag&amp;quot; the last task on the feature ticket, and grep for stale flags every quarter. This is the unglamorous part, and the part that decides whether flags stay a tool or become sediment.&lt;/p&gt;
&lt;p&gt;Start with one flag on your next multi-day feature. The first time you merge on day one instead of week three - and turn the thing off from your phone when it misbehaves - the branch-everything habit loses its grip.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Git Fetch vs Pull: What Your Pull Actually Does</title><link>https://laravelgit.com/blog/fetch-vs-pull-what-your-pull-actually-does/</link><guid isPermaLink="true">https://laravelgit.com/blog/fetch-vs-pull-what-your-pull-actually-does/</guid><description>git pull is git fetch plus a merge (or rebase). Knowing the two halves separately turns surprise merges into a choice you make on purpose.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;git pull&lt;/code&gt; is the command everyone learns first and understands last. It is not one operation - it is &lt;code&gt;git fetch&lt;/code&gt; followed immediately by &lt;code&gt;git merge&lt;/code&gt; (or &lt;code&gt;git rebase&lt;/code&gt;, depending on your config). Splitting them apart is worth the ten seconds it takes, because the second half is where surprises live.&lt;/p&gt;
&lt;h2&gt;Fetch: look before you leap&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch origin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fetch downloads new commits from the remote and updates your remote-tracking branches - &lt;code&gt;origin/main&lt;/code&gt; and friends - without touching your working tree or your local branches at all. Nothing can conflict, nothing can be overwritten. It is pure information.&lt;/p&gt;
&lt;p&gt;That makes fetch the safe first step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch origin
git log --oneline HEAD..origin/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The log shows exactly what arrived while you were working. If the answer is three commits you already know about, merging is a formality. If it is a force-push or a rewritten history, you found out before it landed in your branch - which is also why teams that force-push feature branches should &lt;a href=&quot;/blog/why-you-should-force-push-with-lease/&quot;&gt;force-push with lease&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Merge or rebase: the second half&lt;/h2&gt;
&lt;p&gt;Once fetched, you choose how to combine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git merge origin/main&lt;/code&gt; - creates a merge commit when both sides moved. Honest history, extra bubbles.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git rebase origin/main&lt;/code&gt; - replays your unpushed commits on top, keeping history linear. Cleaner, but it rewrites your local commits, so only for work nobody else has.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;git pull&lt;/code&gt; picks one of these for you based on config (&lt;code&gt;pull.rebase&lt;/code&gt;, or merge by default). Teams that care should pick deliberately - &lt;a href=&quot;/blog/merging-vs-rebasing-for-small-teams/&quot;&gt;Merging vs Rebasing for Small Teams&lt;/a&gt; is the full argument, and a one-line &lt;code&gt;git config pull.rebase true&lt;/code&gt; (or &lt;code&gt;pull.ff only&lt;/code&gt;) makes the default explicit instead of accidental.&lt;/p&gt;
&lt;h2&gt;A small daily upgrade&lt;/h2&gt;
&lt;p&gt;Replacing the reflexive &lt;code&gt;git pull&lt;/code&gt; with fetch, look, then merge-or-rebase costs seconds and buys two things: no surprise merge commits in the middle of your work, and no discovering a conflict after you have mentally committed to it. When conflicts do show up, &lt;a href=&quot;/blog/resolving-merge-conflicts-without-panic/&quot;&gt;Resolving Merge Conflicts Without Panic&lt;/a&gt; has the rest of the drill.&lt;/p&gt;
&lt;p&gt;The one-line summary: fetch is reading, pull is reading plus writing. Read first.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Git Bisect: Find the Commit That Broke It</title><link>https://laravelgit.com/blog/git-bisect-find-the-commit-that-broke-it/</link><guid isPermaLink="true">https://laravelgit.com/blog/git-bisect-find-the-commit-that-broke-it/</guid><description>The tests passed last week and fail now. Bisect turns &apos;somewhere in these 40 commits&apos; into six test runs.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Something that worked last release is broken now, and forty commits sit between then and now. Reading every diff is the slow way. &lt;code&gt;git bisect&lt;/code&gt; does a binary search over the range, so six checks find the culprit instead of forty.&lt;/p&gt;
&lt;h2&gt;Mark the ends&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect start
git bisect bad            # the commit you are on is broken
git bisect good v1.4.0    # the last tag you know was fine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git checks out a commit in the middle. Run whatever reproduces the bug — the feature test, the artisan command, the page that 500s — then tell Git the answer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect good   # or: git bisect bad
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Repeat. Each answer halves the range, and after a few rounds Git names the first bad commit with its full diff and message.&lt;/p&gt;
&lt;h2&gt;Let the test suite drive&lt;/h2&gt;
&lt;p&gt;If a single command answers &amp;quot;is this commit broken&amp;quot;, hand the whole search to it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect run php artisan test --filter=CheckoutTest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bisect now walks the range alone, marks each commit from the exit code, and stops on the first failure. This is the version worth using: it is fast, and it removes the temptation to eyeball a step and guess.&lt;/p&gt;
&lt;h2&gt;Two practical bumps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A commit that cannot run at all — a dependency changed and the test cannot boot — stalls the search. &lt;code&gt;git bisect skip&lt;/code&gt; tells Git to step over it, and &lt;code&gt;git bisect run&lt;/code&gt; can skip automatically when the command exits with 125.&lt;/li&gt;
&lt;li&gt;Vendored directories move between commits. Between steps you may need &lt;code&gt;composer install&lt;/code&gt;; inside &lt;code&gt;bisect run&lt;/code&gt;, a tiny shell script that runs it before the test keeps every commit bootable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When it names a commit&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That returns you to where you started. The bisect log disappears, so note the hash first. The usual next step is not the fix — it is a failing regression test on current main that proves the bug, so the fix that follows can never quietly regress.&lt;/p&gt;
&lt;p&gt;Bisect rewards the boring habits: small commits that each boot, and tags you can trust as &amp;quot;known good&amp;quot;. Those habits are cheap right up until the day they are the fastest way back to a working app.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Git LFS: When a Laravel Repo Needs It</title><link>https://laravelgit.com/blog/git-lfs-when-a-laravel-repo-needs-it/</link><guid isPermaLink="true">https://laravelgit.com/blog/git-lfs-when-a-laravel-repo-needs-it/</guid><description>Git holds text well and binaries badly. LFS moves the big files out of history — for the few assets that truly belong in the repo.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every clone downloads the whole history. A 50 MB design file committed once is 50 MB in every clone forever, even after you delete it. Git LFS exists for that problem: the repo keeps a pointer, the bytes live in separate storage.&lt;/p&gt;
&lt;h2&gt;The setup&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git lfs install
git lfs track &amp;quot;*.psd&amp;quot; &amp;quot;*.zip&amp;quot; &amp;quot;*.mp4&amp;quot;
git add .gitattributes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From then on, matching files are stored via LFS automatically. Existing history is untouched — only new commits use the pointer.&lt;/p&gt;
&lt;h2&gt;What belongs in LFS on a Laravel project&lt;/h2&gt;
&lt;p&gt;Genuinely version-worthy binaries: design source files, brand assets, documentation PDFs, maybe small fixture videos for tests. If the file is source material the team edits and needs history for, LFS fits.&lt;/p&gt;
&lt;h2&gt;What does not&lt;/h2&gt;
&lt;p&gt;User uploads, media libraries, anything served to visitors. Those belong in object storage with a CDN in front — Laravel&amp;#39;s filesystem disks are built for exactly that, and your repo stays lean. The test is &amp;quot;does the next developer need this on a fresh clone?&amp;quot; If not, it is not repo content.&lt;/p&gt;
&lt;h2&gt;If the repo is already heavy&lt;/h2&gt;
&lt;p&gt;Adding LFS now does not shrink old history; that takes the same history-rewriting treatment as a leaked secret, with the same team-wide re-clone cost. Worth it when clones hurt, not before.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Keeping a Changelog From Git Tags</title><link>https://laravelgit.com/blog/keep-a-changelog-from-git-tags/</link><guid isPermaLink="true">https://laravelgit.com/blog/keep-a-changelog-from-git-tags/</guid><description>Tags mark what shipped; the log between them writes the changelog. A small habit that pays off every release.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A changelog is only painful when you write it three months late. If you tag releases, most of the work is already done.&lt;/p&gt;
&lt;h2&gt;Tag what you ship&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git tag -a v1.3.0 -m &amp;quot;v1.3.0&amp;quot;
git push origin v1.3.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Annotated tags carry a message and a date, and they give you a name for &amp;quot;what is in production right now&amp;quot;.&lt;/p&gt;
&lt;h2&gt;The log between tags is your draft&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log v1.2.0..v1.3.0 --oneline --no-merges
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That list, cleaned up, is the release note. Short, descriptive commit subjects pay off here — another reason the commit-msg habit matters.&lt;/p&gt;
&lt;h2&gt;Keep a CHANGELOG.md in the repo&lt;/h2&gt;
&lt;p&gt;Add a section per tag, newest at the top, dated. Users of your package or app read it before upgrading; future-you reads it when something breaks and the question is &amp;quot;since when&amp;quot;. For a Laravel app, note the migrations and any &lt;code&gt;.env&lt;/code&gt; keys a release adds, because those are the steps a deploy can trip on.&lt;/p&gt;
&lt;h2&gt;The habit that makes it work&lt;/h2&gt;
&lt;p&gt;Write the entry at tag time, while the diff is fresh. A changelog maintained per release is ten lines of work; reconstructed quarterly it is an afternoon of archaeology.&lt;/p&gt;
</content:encoded><category>Releases</category></item><item><title>Keeping a Fork in Sync With Upstream</title><link>https://laravelgit.com/blog/keeping-a-fork-in-sync-with-upstream/</link><guid isPermaLink="true">https://laravelgit.com/blog/keeping-a-fork-in-sync-with-upstream/</guid><description>A fork drifts the moment you create it. The two-remote setup - origin for you, upstream for the source - keeps it a weekend task, not a rescue job.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Fork a repository and the clock starts: the original keeps moving, and your copy quietly becomes a snapshot of the past. Whether it is a package you patched or an internal template your team forked, syncing is the same two-remote routine.&lt;/p&gt;
&lt;h2&gt;Two remotes, two jobs&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git remote add upstream git@yourgit.example.com:original/project.git
git remote -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;origin&lt;/code&gt; points at your fork - where you push. &lt;code&gt;upstream&lt;/code&gt; points at the source - where you only read. Naming them this way is convention, and the convention matters: when every guide and every teammate says &amp;quot;upstream&amp;quot;, everyone means the same thing.&lt;/p&gt;
&lt;h2&gt;The sync routine&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch upstream
git switch main
git merge upstream/main
git push origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fetch first, look at what arrived (&lt;code&gt;git log --oneline HEAD..upstream/main&lt;/code&gt;), then merge and push. This is the fetch-then-decide rhythm from &lt;a href=&quot;/blog/fetch-vs-pull-what-your-pull-actually-does/&quot;&gt;Fetch vs Pull&lt;/a&gt; - the only difference is the remote&amp;#39;s name. If your team prefers linear history, &lt;code&gt;git rebase upstream/main&lt;/code&gt; works too, but only on commits that exist solely in your fork.&lt;/p&gt;
&lt;h2&gt;Keep your changes out of main&lt;/h2&gt;
&lt;p&gt;The fork stays easy to sync when your own work never touches its &lt;code&gt;main&lt;/code&gt;. Do everything on feature branches - &lt;code&gt;git switch -c our-patch&lt;/code&gt; - and let main mirror upstream exactly. The day you commit directly to the fork&amp;#39;s main, every future sync becomes a merge of two real histories instead of a fast-forward, and conflicts move in permanently.&lt;/p&gt;
&lt;p&gt;If that has already happened, the repair is usually: branch your changes off (&lt;code&gt;git branch our-work&lt;/code&gt;), reset main back to upstream (&lt;code&gt;git reset --hard upstream/main&lt;/code&gt; after a fetch), and push. That is a force-push to your own fork, so do it &lt;a href=&quot;/blog/why-you-should-force-push-with-lease/&quot;&gt;with lease&lt;/a&gt; and warn anyone else using the fork.&lt;/p&gt;
&lt;h2&gt;How often&lt;/h2&gt;
&lt;p&gt;Often enough that each sync is boring. Weekly keeps the diff readable; monthly is survivable; yearly turns syncing into an archaeology project. Teams running a self-hosted server can nudge this with a scheduled pipeline that fetches upstream and opens a pull request when main falls behind - boring automation for a task that should never be exciting.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Keeping Diffs Clean With .gitattributes</title><link>https://laravelgit.com/blog/keeping-diffs-clean-with-gitattributes/</link><guid isPermaLink="true">https://laravelgit.com/blog/keeping-diffs-clean-with-gitattributes/</guid><description>One config file tells Git how to treat each path: line endings, which diffs to hide, better hunk headers, and what stays out of downloads.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt; decides what enters the repo; &lt;code&gt;.gitattributes&lt;/code&gt; decides how Git treats what is in it. One small file at the root, and four annoyances quietly disappear.&lt;/p&gt;
&lt;h2&gt;Line endings that stop moving&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitattributes&quot;&gt;* text=auto eol=lf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every text file normalizes to LF in the repo, whatever editor or OS committed it. The &amp;quot;whole file changed&amp;quot; diffs that come from a Windows checkout meeting an LF repo stop happening, and this line costs you nothing on day one of a repo.&lt;/p&gt;
&lt;h2&gt;Diffs that show what matters&lt;/h2&gt;
&lt;p&gt;Lock files are for machines. Hiding their diffs keeps reviews on your code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitattributes&quot;&gt;composer.lock -diff
package-lock.json -diff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The files still commit, still version, still deploy - they just stop producing thousand-line review noise. On GitHub, mark generated directories so they collapse by default and stay out of the language stats:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitattributes&quot;&gt;public/build/** linguist-generated=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hunk headers that name the function&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitattributes&quot;&gt;*.php diff=php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the PHP diff driver, every hunk header in a diff names the method or function it sits in, not whatever line happened to be nearby. Reviewing &lt;code&gt;git log -p&lt;/code&gt; on a large model gets noticeably easier.&lt;/p&gt;
&lt;h2&gt;Cleaner package downloads&lt;/h2&gt;
&lt;p&gt;If any repo is ever installed as a Composer package, export-ignore keeps its development baggage out of other people&amp;#39;s &lt;code&gt;vendor/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitattributes&quot;&gt;/tests export-ignore
/.github export-ignore
/phpunit.xml export-ignore
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;End users download the code, not your CI.&lt;/p&gt;
&lt;p&gt;None of this is mandatory and none of it changes what runs. It is housekeeping: the diffs get shorter, the reviews get faster, and the repo stops leaking its working files into places they never belonged.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Keeping Files Out of Git: ignore, exclude, and the Global File</title><link>https://laravelgit.com/blog/keeping-files-out-of-git/</link><guid isPermaLink="true">https://laravelgit.com/blog/keeping-files-out-of-git/</guid><description>Three places to ignore files - .gitignore, .git/info/exclude, and a global excludes file - plus how to untrack something that should never have been committed.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;/blog/what-not-to-commit-in-a-laravel-project/&quot;&gt;What Not to Commit in a Laravel Project&lt;/a&gt; covers which files stay out of the repo. This is the mechanics post: Git gives you three different places to say &amp;quot;ignore this&amp;quot;, and they are not interchangeable.&lt;/p&gt;
&lt;h2&gt;.gitignore: the shared, committed one&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;.gitignore&lt;/code&gt; at the root of the repo is committed, so every clone gets it. This is where project-level rules live - Laravel&amp;#39;s default file already covers the essentials:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/vendor
/node_modules
.env
/storage/logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a file would be junk for anyone who clones the project, its rule belongs here.&lt;/p&gt;
&lt;h2&gt;.git/info/exclude: private to your clone&lt;/h2&gt;
&lt;p&gt;This file works exactly like &lt;code&gt;.gitignore&lt;/code&gt; but is not committed or pushed. Use it for things only your checkout produces - a scratch script you keep in the project root, a local &lt;code&gt;docker-compose.override.yml&lt;/code&gt; nobody else has. Nobody else&amp;#39;s repo should carry rules for your personal files.&lt;/p&gt;
&lt;h2&gt;The global file: private to your machine&lt;/h2&gt;
&lt;p&gt;Editor and OS noise follows you across every project, so Git lets you ignore it once, everywhere:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global core.excludesFile ~/.gitignore_global
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Put &lt;code&gt;.idea/&lt;/code&gt;, &lt;code&gt;.vscode/&lt;/code&gt;, &lt;code&gt;.DS_Store&lt;/code&gt; and friends in there. If every project&amp;#39;s committed &lt;code&gt;.gitignore&lt;/code&gt; carries editor rules, that is a sign this file is missing on someone&amp;#39;s machine.&lt;/p&gt;
&lt;h2&gt;When the file is already tracked&lt;/h2&gt;
&lt;p&gt;An ignore rule does nothing for a file Git is already tracking - the classic case is a &lt;code&gt;.env&lt;/code&gt; committed before the rule existed. Untrack it without deleting your local copy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git rm --cached .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Commit that removal and the ignore rule takes over from there. Note the file still exists in history; if what slipped in was a secret, untracking is not enough - see &lt;a href=&quot;/blog/a-secret-got-committed-now-what/&quot;&gt;A Secret Got Committed. Now What?&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One last habit: run &lt;code&gt;git status&lt;/code&gt; before committing and actually read the list. Ignoring rules prevent accidents, but the status list is where you catch the ones that got through.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Commit Your Lockfiles (and How to Survive Their Conflicts)</title><link>https://laravelgit.com/blog/lockfiles-in-git-commit-them-and-survive-the-conflicts/</link><guid isPermaLink="true">https://laravelgit.com/blog/lockfiles-in-git-commit-them-and-survive-the-conflicts/</guid><description>composer.lock and package-lock.json belong in the repo - they are the difference between one dependency set and forty. Conflicts in them are normal, and mechanical to fix.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every so often someone proposes gitignoring &lt;code&gt;composer.lock&lt;/code&gt; or &lt;code&gt;package-lock.json&lt;/code&gt; &amp;quot;because they cause conflicts.&amp;quot; That trades a small recurring cost for a large hidden one: without the lockfile, every machine resolves dependencies independently, and &amp;quot;works on my machine&amp;quot; becomes structural.&lt;/p&gt;
&lt;h2&gt;Why the lockfile is committed&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;composer.json&lt;/code&gt; says what you allow; &lt;code&gt;composer.lock&lt;/code&gt; says what you got - exact versions, exact hashes. Committing it means CI, staging, production, and every laptop install the same dependency set. Your &lt;a href=&quot;/blog/a-first-ci-pipeline-for-laravel/&quot;&gt;CI pipeline&lt;/a&gt; relies on this: &lt;code&gt;composer install&lt;/code&gt; and &lt;code&gt;npm ci&lt;/code&gt; read the lockfiles, which is why a green PR means something.&lt;/p&gt;
&lt;p&gt;Without them, a package author publishing a patch release between your test and your deploy is an unreviewed code change in production.&lt;/p&gt;
&lt;h2&gt;When they conflict&lt;/h2&gt;
&lt;p&gt;Two branches both ran &lt;code&gt;composer update&lt;/code&gt; or &lt;code&gt;npm install&lt;/code&gt;, and now the lockfiles conflict on merge. The rule is: never resolve a lockfile conflict by hand-editing. Regenerate instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git merge main
# conflicts in composer.lock
git checkout --theirs composer.lock   # or ours; the content hardly matters
composer update --lock                 # regenerate a valid lockfile
git add composer.lock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The regenerated file is consistent by construction; a hand-merged one is two inconsistent halves glued together. Then run the test suite, because the merge may have combined two dependency sets that each passed alone.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;package-lock.json&lt;/code&gt; the same shape: take one side, &lt;code&gt;npm install&lt;/code&gt;, commit the result.&lt;/p&gt;
&lt;h2&gt;The habits that keep it rare&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;One person upgrades dependencies per branch; upgrades go in their own PR, never smuggled inside a feature.&lt;/li&gt;
&lt;li&gt;Merge or rebase frequently - &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;short-lived branches&lt;/a&gt; shrink the window where two lockfiles diverge.&lt;/li&gt;
&lt;li&gt;When you do upgrade, read the lockfile diff in the PR. It is the one diff that lists exactly what new code will run on your server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The lockfile is the receipt for your dependencies. Keep it in Git, regenerate it instead of hand-merging, and dependency conflicts drop to a minor chore.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Managing .env Files Across Environments</title><link>https://laravelgit.com/blog/managing-env-files-across-environments/</link><guid isPermaLink="true">https://laravelgit.com/blog/managing-env-files-across-environments/</guid><description>Local, staging, production - each needs its own .env, none of them belongs in Git. The setup that keeps secrets out and onboarding fast.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every Laravel app has the same three-way split: your laptop, a staging server, production. Each needs its own &lt;code&gt;.env&lt;/code&gt;, and the &lt;code&gt;.env&lt;/code&gt; itself must never be committed - &lt;a href=&quot;/blog/what-not-to-commit-in-a-laravel-project/&quot;&gt;What Not to Commit&lt;/a&gt; covers why. What is worth deciding deliberately is how the values get to each environment and how the team learns about new keys.&lt;/p&gt;
&lt;h2&gt;.env.example is the contract&lt;/h2&gt;
&lt;p&gt;Commit &lt;code&gt;.env.example&lt;/code&gt; with every key and a safe placeholder value, and treat it as part of every pull request that adds configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MAIL_MAILER=
QUEUE_CONNECTION=
INVOICE_SIGNING_KEY=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new hire runs &lt;code&gt;cp .env.example .env&lt;/code&gt;, fills in the blanks, and the app boots. A reviewer sees the new key in the PR diff without seeing its value. When someone adds a key to their local &lt;code&gt;.env&lt;/code&gt; without updating the example, the next person&amp;#39;s setup breaks quietly - so make &amp;quot;PR adds a key to .env.example&amp;quot; as automatic as &amp;quot;PR includes a migration&amp;quot;.&lt;/p&gt;
&lt;h2&gt;Getting values to servers&lt;/h2&gt;
&lt;p&gt;Servers have no checkout step where you can edit a file by hand, so pick a delivery mechanism:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The deploy tool&amp;#39;s secrets store (most managed deploy platforms have one).&lt;/li&gt;
&lt;li&gt;A file written by your deploy script from your password manager or vault - never from the repo.&lt;/li&gt;
&lt;li&gt;On a self-hosted box you administer: a plain &lt;code&gt;.env&lt;/code&gt; in the shared directory of your &lt;a href=&quot;/blog/zero-downtime-deploys-for-laravel/&quot;&gt;releases structure&lt;/a&gt;, permissions &lt;code&gt;600&lt;/code&gt;, owned by the deploy user.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three share one property: the value&amp;#39;s home is outside Git, and changing it does not require a commit.&lt;/p&gt;
&lt;h2&gt;After changing values&lt;/h2&gt;
&lt;p&gt;Laravel caches config aggressively in production, so a changed &lt;code&gt;.env&lt;/code&gt; does nothing until you rebuild the cache:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan config:cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fold that into the deploy script and &amp;quot;I changed it but nothing happened&amp;quot; stops being a genre of bug report. Also restart the queue workers - they hold the old config in memory, same lesson as the deploy post&amp;#39;s &lt;code&gt;queue:restart&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The audit habit&lt;/h2&gt;
&lt;p&gt;Once a quarter, diff each environment&amp;#39;s keys against &lt;code&gt;.env.example&lt;/code&gt;: keys present on servers but missing from the example are undocumented config, and keys in the example that no environment sets are dead weight. Two minutes with &lt;code&gt;comm&lt;/code&gt; on sorted key lists keeps the contract honest.&lt;/p&gt;
&lt;p&gt;Secrets stay out of history - and if one ever lands there, &lt;a href=&quot;/blog/a-secret-got-committed-now-what/&quot;&gt;A Secret Got Committed. Now What?&lt;/a&gt; is the cleanup drill.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Merging vs Rebasing on a Small Team</title><link>https://laravelgit.com/blog/merging-vs-rebasing-for-small-teams/</link><guid isPermaLink="true">https://laravelgit.com/blog/merging-vs-rebasing-for-small-teams/</guid><description>Rebase to tidy your own branch, merge to combine into main. One sentence of policy saves a lot of history arguments.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Few git arguments cost more than they settle. The working rule for a small team fits in one sentence: rebase your own branch, merge into main.&lt;/p&gt;
&lt;h2&gt;Rebase is for your branch&lt;/h2&gt;
&lt;p&gt;While your branch is still yours, keep it current and readable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch origin
git rebase origin/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your commits replay on top of main, conflicts show up in small pieces while you still remember the change, and the branch reads like a story a reviewer can follow.&lt;/p&gt;
&lt;h2&gt;Merge is for main&lt;/h2&gt;
&lt;p&gt;When the branch is done, merge it. The merge commit marks where the work landed and keeps the group&amp;#39;s shared history untouched:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout main
git merge --no-ff pr-42-branch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--no-ff&lt;/code&gt; keeps the feature visible as one unit, which makes the changelog draft and any later revert much easier.&lt;/p&gt;
&lt;h2&gt;What not to do&lt;/h2&gt;
&lt;p&gt;Never rebase a branch someone else has pulled. Rewriting shared history is how teammates lose work and trust the repo a little less. If it is pushed and shared, it merges.&lt;/p&gt;
&lt;p&gt;That is the whole policy. Write it in the README and spend the argument on the code instead.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Reviews</category></item><item><title>Migrations in Code Review: What to Look For</title><link>https://laravelgit.com/blog/migrations-in-code-review/</link><guid isPermaLink="true">https://laravelgit.com/blog/migrations-in-code-review/</guid><description>A merged migration is a fact you cannot edit. The review checklist: reversible down(), safe defaults, indexes, and no data surgery in schema changes.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most code review feedback is a request for changes, and the author edits the same file. Migrations break that rule: once a migration is merged and run anywhere - staging, a teammate&amp;#39;s machine, production - the file is a fact. Editing it changes nothing that already ran and silently forks the databases that have not. Reviewing migrations well matters more than reviewing almost any other diff, because it is the last time the change is cheap.&lt;/p&gt;
&lt;h2&gt;The review checklist&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;down()&lt;/code&gt; actually reverse the &lt;code&gt;up()&lt;/code&gt;?&lt;/strong&gt; A &lt;code&gt;down()&lt;/code&gt; that drops the table when &lt;code&gt;up()&lt;/code&gt; added a column is a trap for the next rollback. If the change is genuinely irreversible - a dropped column - that should be a shouted comment, not a silent absence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Are defaults and nullability safe for existing rows?&lt;/strong&gt; Adding a &lt;code&gt;NOT NULL&lt;/code&gt; column without a default to a table with a million rows either fails or rewrites the table while holding a lock. The safe shape is nullable first, backfill, constrain later - the same three-step dance as &lt;a href=&quot;/blog/zero-downtime-deploys-for-laravel/&quot;&gt;zero-downtime deploys&lt;/a&gt; require.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Did new query paths get indexes?&lt;/strong&gt; A column added for a lookup (&lt;code&gt;WHERE stripe_customer_id = ...&lt;/code&gt;) without an index is a slow query waiting for traffic. The migration is the cheapest place the index will ever be added.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is data surgery mixed into schema changes?&lt;/strong&gt; A migration that both alters a table and rewrites its rows couples &amp;quot;change the shape&amp;quot; with &amp;quot;change the content&amp;quot; in one irreversible step. Schema in migrations, data in a one-off artisan command you can rerun and monitor.&lt;/p&gt;
&lt;h2&gt;After merge: hands off&lt;/h2&gt;
&lt;p&gt;The corollary rule, worth stating in the team&amp;#39;s &lt;a href=&quot;/blog/onboarding-a-developer-to-your-git-workflow/&quot;&gt;written workflow&lt;/a&gt;: a merged migration is never edited. Found a typo in a column name after merge? A new migration renames it. This feels heavy the first time and is obviously correct the first time someone hits a database that ran the original.&lt;/p&gt;
&lt;p&gt;Review migrations slowly, then let them harden into facts. That asymmetry - expensive to change, cheap to get right beforehand - is exactly what the habits in &lt;a href=&quot;/blog/code-review-habits/&quot;&gt;Code Review Habits That Actually Catch Bugs&lt;/a&gt; are for.&lt;/p&gt;
</content:encoded><category>Reviews</category></item><item><title>Monitoring Your Self-Hosted Git Server: The Four Things That Matter</title><link>https://laravelgit.com/blog/monitoring-your-self-hosted-git-server/</link><guid isPermaLink="true">https://laravelgit.com/blog/monitoring-your-self-hosted-git-server/</guid><description>A Git server fails quietly: disks fill, backups rot, SSH dies at 2am. The four checks that catch every realistic failure, and how to keep them boring.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hosted Git comes with someone else&amp;#39;s pager. Self-hosted means the pager is yours - which is fine, as long as it only rings for real problems. Four checks cover every failure mode a small team&amp;#39;s Git server actually has, and none of them requires a monitoring platform you will spend a weekend configuring.&lt;/p&gt;
&lt;h2&gt;1. Is it answering?&lt;/h2&gt;
&lt;p&gt;An external uptime check hitting the web interface every minute, from outside your network. This catches the server being down, the reverse proxy dying, and the certificate expiring. Any ping service does this for free or close to it. Alert on two consecutive failures, not one - single blips are network weather.&lt;/p&gt;
&lt;h2&gt;2. Can someone actually clone?&lt;/h2&gt;
&lt;p&gt;The web UI can be green while SSH is dead, and your team notices at 9am Monday. A synthetic check that runs &lt;code&gt;git ls-remote&lt;/code&gt; against a small repo every few minutes exercises the path that matters - SSH auth, Git daemon, disk read. A cron job on another machine with a dedicated read-only key (the &lt;a href=&quot;/blog/deploy-keys-and-machine-users-for-ci/&quot;&gt;deploy keys guide&lt;/a&gt; covers scoping it) is thirty lines of setup.&lt;/p&gt;
&lt;h2&gt;3. Is the disk filling up?&lt;/h2&gt;
&lt;p&gt;Repos only grow, and a full disk corrupts things in the order you least want. Alert at 80%, not 95% - the gap between &amp;quot;we should prune something this week&amp;quot; and &amp;quot;pushing is failing right now.&amp;quot; Watch the backup destination too; it fills up quieter.&lt;/p&gt;
&lt;h2&gt;4. Did the backup actually run - and restore?&lt;/h2&gt;
&lt;p&gt;The backup cron succeeding means a file was written, not that it restores. &lt;a href=&quot;/blog/backing-up-your-git-server/&quot;&gt;Backing up your Git server&lt;/a&gt; covers the backup half; the monitoring half is two alerts: one when the backup job misses its window, and a monthly drill where you restore to a scratch machine and clone from it. The drill is the only check on this list that cannot be automated away, and the only one that proves the rest mean anything.&lt;/p&gt;
&lt;p&gt;Four checks, one drill, and the pager stays silent for months at a time - which is exactly what &amp;quot;the server is fine&amp;quot; should feel like.&lt;/p&gt;
</content:encoded><category>Self-hosting</category></item><item><title>Monorepo or Many Repos for a Small Laravel Team</title><link>https://laravelgit.com/blog/monorepo-or-many-repos/</link><guid isPermaLink="true">https://laravelgit.com/blog/monorepo-or-many-repos/</guid><description>One repo for the app and its packages, or a repo per package? For small teams the boring default wins: start with one repo and split only when it hurts.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Laravel projects grow extra code over time: a shared package, an internal admin tool, maybe a second app that reuses the first one&amp;#39;s domain logic. At some point you have to decide whether everything lives in one repository or gets split into several.&lt;/p&gt;
&lt;h2&gt;Start with one repo&lt;/h2&gt;
&lt;p&gt;For a small team, one repository is the right default for a simple reason: every change is atomic. When the app and its shared package live together, a rename lands in a single commit, CI runs once, and there is no version matrix to keep in your head. Composer makes this comfortable with path repositories:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;repositories&amp;quot;: [
  { &amp;quot;type&amp;quot;: &amp;quot;path&amp;quot;, &amp;quot;url&amp;quot;: &amp;quot;packages/billing&amp;quot; }
],
&amp;quot;require&amp;quot;: {
  &amp;quot;acme/billing&amp;quot;: &amp;quot;*&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The package sits in &lt;code&gt;packages/billing&lt;/code&gt; inside the same repo, and Composer symlinks it in. You get the directory structure of a split without any of the coordination cost.&lt;/p&gt;
&lt;h2&gt;When splitting starts to make sense&lt;/h2&gt;
&lt;p&gt;Split a repo only when one of these becomes true:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A package is genuinely reused across projects with different release cadences, and tagging it independently matters.&lt;/li&gt;
&lt;li&gt;Different people should have different access rights - a contractor on the marketing site should not see billing code.&lt;/li&gt;
&lt;li&gt;CI time is dominated by unrelated code, and path-scoped pipelines cannot fix it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;quot;One day this might be an open-source package&amp;quot; is not on the list. That day can be handled when it arrives.&lt;/p&gt;
&lt;h2&gt;Splitting later, keeping history&lt;/h2&gt;
&lt;p&gt;When the day comes, you do not have to lose history. &lt;code&gt;git subtree split&lt;/code&gt; extracts a directory into its own branch with every commit that touched it, ready to push into a fresh repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git subtree split --prefix=packages/billing -b billing-history
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a much better story than the usual alternative of planning repository topology up front for a future that may never happen. It is also worth contrasting with &lt;a href=&quot;/blog/submodules-when-a-laravel-team-needs-one/&quot;&gt;submodules&lt;/a&gt;, which solve a different problem - vendoring an outside dependency with its own history - and bring real day-to-day friction.&lt;/p&gt;
&lt;p&gt;The short version: one repo until something concrete hurts, path repositories for internal packages, subtree split when a package earns its independence. Repository topology is a scaling decision, and small teams scale by not making it early.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Branched Off the Wrong Branch? rebase --onto Moves It Cleanly</title><link>https://laravelgit.com/blog/moving-a-branch-with-rebase-onto/</link><guid isPermaLink="true">https://laravelgit.com/blog/moving-a-branch-with-rebase-onto/</guid><description>You built a feature on top of another feature, and now the first one merged - or will not. rebase --onto replants your commits where they belong, without the duplicate-commit mess.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It happens to everyone: you start &lt;code&gt;feature-b&lt;/code&gt; from &lt;code&gt;feature-a&lt;/code&gt; because you needed its changes, and then &lt;code&gt;feature-a&lt;/code&gt; merges - squash-merged, naturally, so its commits on main have different hashes. Now your pull request for &lt;code&gt;feature-b&lt;/code&gt; shows ten commits instead of three, half of them already on main. Or worse, &lt;code&gt;feature-a&lt;/code&gt; gets abandoned, and your work is sitting on a branch going nowhere.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git rebase --onto&lt;/code&gt; is the precise tool for both. It says: take my commits from here to here, and replay them over there.&lt;/p&gt;
&lt;h2&gt;The recipe&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git rebase --onto main feature-a feature-b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read it as: take everything on &lt;code&gt;feature-b&lt;/code&gt; that is not on &lt;code&gt;feature-a&lt;/code&gt;, and replay it onto &lt;code&gt;main&lt;/code&gt;. Your three commits land on top of main, the squash-merged duplicates vanish from your branch, and the pull request shrinks to what you actually wrote.&lt;/p&gt;
&lt;p&gt;The abandoned-branch case is identical: if &lt;code&gt;feature-a&lt;/code&gt; is dead, the same command moves &lt;code&gt;feature-b&lt;/code&gt; onto main without dragging the dead work along.&lt;/p&gt;
&lt;h2&gt;After the move&lt;/h2&gt;
&lt;p&gt;Your branch was rewritten, so the push needs the safe force:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push --force-with-lease
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole workflow - the &lt;a href=&quot;/blog/why-you-should-force-push-with-lease/&quot;&gt;force-with-lease guide&lt;/a&gt; covers why the flag matters. If the rebase hits conflicts, resolve them the same way as any &lt;a href=&quot;/blog/resolving-merge-conflicts-without-panic/&quot;&gt;merge conflict&lt;/a&gt;; Git stops at each conflicting commit and lets you continue with &lt;code&gt;git rebase --continue&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Preventing the tangle&lt;/h2&gt;
&lt;p&gt;Stacking branches is sometimes unavoidable, but two habits make it rare:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Branch from main by default.&lt;/strong&gt; Before &lt;code&gt;git switch -c&lt;/code&gt;, glance at where you are. Half of all wrong-base branches are born in a hurry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small pull requests&lt;/a&gt;.&lt;/strong&gt; A PR that merges in a day is rarely something you need to build on top of; a PR that sits for two weeks practically forces stacking.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And when stacking is genuinely needed - a big feature split into dependent slices - note the dependency in the pull request description, so nobody merges the second slice first.&lt;/p&gt;
&lt;p&gt;rebase --onto looks intimidating in the manual and is three words in practice. Learn it once and the wrong-branch mistake stops costing an afternoon.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Moving Your Repositories to a New Git Server Without Losing Anything</title><link>https://laravelgit.com/blog/moving-your-repositories-to-a-new-git-server/</link><guid isPermaLink="true">https://laravelgit.com/blog/moving-your-repositories-to-a-new-git-server/</guid><description>Git makes migration nearly lossless - every clone carries the full history. The mirror-push recipe, what does not travel with the repo, and the order to do things in.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Moving hosts feels risky because the repo is the crown jewels. The good news: Git is a distributed system, and every clone already contains the complete history. Migrating is mostly a matter of pushing that history somewhere new - plus remembering the things that are not in the repo.&lt;/p&gt;
&lt;h2&gt;The mirror push&lt;/h2&gt;
&lt;p&gt;The recipe for a lossless move is three commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone --mirror git@old-host:your-org/your-app.git
cd your-app.git
git push --mirror git@new-host:your-org/your-app.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--mirror&lt;/code&gt; on the clone fetches every branch, every tag, every note - not just the default branch checkout. &lt;code&gt;--mirror&lt;/code&gt; on the push replicates all of it, exactly, onto the new server. When the push finishes, the new repo is a bit-for-bit copy of the old one&amp;#39;s refs. History, tags, branches: done.&lt;/p&gt;
&lt;h2&gt;What does not travel&lt;/h2&gt;
&lt;p&gt;The repository is only part of what makes a project work. Plan separately for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pull requests and issues.&lt;/strong&gt; These live in the host&amp;#39;s database, not in Git. GitHub&amp;#39;s API can export them; some teams archive them to a document and start fresh, some use migration tooling. Decide consciously - do not discover the gap after the cutover.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI secrets and variables.&lt;/strong&gt; Re-enter them on the new side. This is also a natural moment to apply the scoping rules from &lt;a href=&quot;/blog/deploy-keys-and-machine-users-for-ci/&quot;&gt;deploy keys and machine users&lt;/a&gt; instead of copying an old personal key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Webhooks and integrations.&lt;/strong&gt; Deploy hooks, Slack notifications, status checks - make a list before you start, and tick each one off on the new server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Branch protection rules.&lt;/strong&gt; Recreate them; &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;branch protection rules that stick&lt;/a&gt; is the checklist.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The order that avoids pain&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Set up the new server and recreate protections&lt;/strong&gt; while the old one still serves everyone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mirror-push every repo&lt;/strong&gt;, and spot-check: clone from the new host, run the test suite, compare &lt;code&gt;git log&lt;/code&gt; on a few branches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Move one non-critical repo first&lt;/strong&gt; as a dress rehearsal for the team&amp;#39;s workflow changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Announce a short freeze&lt;/strong&gt; on the old host, do a final mirror push to catch the last commits, then update everyone&amp;#39;s remotes: &lt;code&gt;git remote set-url origin git@new-host:your-org/your-app.git&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the old host read-only for a month.&lt;/strong&gt; Deleting it on day one is how you find out what you forgot.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you are moving to self-hosted, the prep work is the same - plus the operational habits in &lt;a href=&quot;/blog/backing-up-your-git-server/&quot;&gt;backing up your Git server&lt;/a&gt; from day one, because the safety net is now yours to run.&lt;/p&gt;
&lt;p&gt;The history is the easy part; Git was built for this. The checklist above is the difference between a quiet migration and a week of &amp;quot;did anyone move the webhook?&amp;quot;&lt;/p&gt;
</content:encoded><category>Self-hosting</category></item><item><title>Onboarding a New Developer to Your Git Workflow</title><link>https://laravelgit.com/blog/onboarding-a-developer-to-your-git-workflow/</link><guid isPermaLink="true">https://laravelgit.com/blog/onboarding-a-developer-to-your-git-workflow/</guid><description>A new hire&apos;s first week sets their Git habits for years. Write down the five decisions, automate the enforceable ones, and pair on the first PR.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every team has a Git workflow, but most teams keep it in their heads. A new developer then learns it by breaking it: committing to the wrong branch, force-pushing the wrong thing, opening a 2,000-line PR on day three. A one-page written workflow and a little automation fix most of this before it happens.&lt;/p&gt;
&lt;h2&gt;Write down the five decisions&lt;/h2&gt;
&lt;p&gt;However your team works, these five choices define it. Put them in the README or CONTRIBUTING.md:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Where work happens: feature branches off &lt;code&gt;main&lt;/code&gt;, or straight to main for tiny teams.&lt;/li&gt;
&lt;li&gt;How branches are named: &lt;code&gt;feature/short-name&lt;/code&gt;, &lt;code&gt;fix/thing&lt;/code&gt;, whatever - consistency matters more than the scheme.&lt;/li&gt;
&lt;li&gt;What a pull request needs: description, linked ticket, one approval, green CI.&lt;/li&gt;
&lt;li&gt;How merges land: merge commit, squash, or rebase - &lt;a href=&quot;/blog/merging-vs-rebasing-for-small-teams/&quot;&gt;Merging vs Rebasing for Small Teams&lt;/a&gt; if this is still unsettled.&lt;/li&gt;
&lt;li&gt;How releases happen: tags, changelogs, deploy steps. &lt;a href=&quot;/blog/tagging-releases-properly/&quot;&gt;Tagging Releases Properly&lt;/a&gt; covers the shape.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A new hire who can read those five lines commits correctly on day one.&lt;/p&gt;
&lt;h2&gt;Automate what is enforceable&lt;/h2&gt;
&lt;p&gt;Rules that live only in the document decay. The ones that survive are enforced by the repo itself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;Branch protection&lt;/a&gt; on main, so &amp;quot;don&amp;#39;t push to main&amp;quot; is a fact, not a preference.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/git-hooks-for-laravel-projects/&quot;&gt;Git hooks&lt;/a&gt; for the local checks - Pint, tests, no debugging leftovers.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/conventional-commits-for-a-small-team/&quot;&gt;Conventional commits&lt;/a&gt; if the team wants parseable history, with a commit-msg hook to back it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automation also shortens the document: you stop writing rules down once the tooling states them for you.&lt;/p&gt;
&lt;h2&gt;Pair on the first pull request&lt;/h2&gt;
&lt;p&gt;The highest-leverage onboarding hour is walking a new developer through their first PR the team&amp;#39;s way: small branch, tidy commits, a description a stranger could review from, responding to comments without defensiveness. Point them at the team&amp;#39;s own posts on &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small pull requests&lt;/a&gt; and &lt;a href=&quot;/blog/writing-a-good-pull-request-description/&quot;&gt;PR descriptions&lt;/a&gt; if you have them - or write yours down once and link it forever.&lt;/p&gt;
&lt;p&gt;The measure of a good onboarding is how boring the second week is: the new hire ships a small PR, gets one round of comments, merges, and nobody had to correct a Git habit. That outcome is designed, not lucky.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Private Composer Packages Straight From Your Own Git Server</title><link>https://laravelgit.com/blog/private-composer-packages-from-your-own-git/</link><guid isPermaLink="true">https://laravelgit.com/blog/private-composer-packages-from-your-own-git/</guid><description>You do not need Satis or Packagist for internal packages. Point Composer at a Git repo, tag properly, and your self-hosted server becomes the package registry.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every growing Laravel team eventually extracts a package: the shared billing client, the internal logging helper, the theme. Then comes the assumption that serving it needs infrastructure - Satis, Private Packagist, a registry. For most teams it does not: Composer reads plain Git repositories directly, and if you already self-host your Git, the registry is running.&lt;/p&gt;
&lt;h2&gt;The VCS repository&lt;/h2&gt;
&lt;p&gt;In the consuming app, tell Composer where the package lives:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;repositories&amp;quot;: [
    { &amp;quot;type&amp;quot;: &amp;quot;vcs&amp;quot;, &amp;quot;url&amp;quot;: &amp;quot;git@git.yourcompany.com:shared/billing-client.git&amp;quot; }
],
&amp;quot;require&amp;quot;: {
    &amp;quot;yourcompany/billing-client&amp;quot;: &amp;quot;^1.2&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The package repo itself just needs a &lt;code&gt;composer.json&lt;/code&gt; with its name and autoload config - no extra hosting. On install, Composer clones the repo over SSH and checks out the requested version. That is the whole registry.&lt;/p&gt;
&lt;h2&gt;Versions come from tags&lt;/h2&gt;
&lt;p&gt;Composer reads versions from Git tags, which makes disciplined tagging the whole game: &lt;a href=&quot;/blog/tagging-releases-properly/&quot;&gt;tagging releases properly&lt;/a&gt; covers annotated semver tags, and the contract here is simple - tag &lt;code&gt;1.2.0&lt;/code&gt; in the package repo and &lt;code&gt;^1.2&lt;/code&gt; resolves to it. No tag, no release; a branch name works for dev versions (&lt;code&gt;dev-main&lt;/code&gt;) but treat that as a development convenience, never a production dependency.&lt;/p&gt;
&lt;h2&gt;Access is the only real setup&lt;/h2&gt;
&lt;p&gt;CI and every developer need read access to the package repo. This is where teams reach for a shared key in a wiki - do not. The patterns in &lt;a href=&quot;/blog/deploy-keys-and-machine-users-for-ci/&quot;&gt;deploy keys and machine users&lt;/a&gt; apply exactly: a read-only deploy key per consumer, or a machine user with access to the package repos. Developers authenticate with their own SSH keys, which they already have.&lt;/p&gt;
&lt;h2&gt;When you outgrow it&lt;/h2&gt;
&lt;p&gt;The direct-VCS approach slows down at scale: Composer clones every package repo on every install, and metadata lookups multiply. The honest thresholds are dozens of packages or very slow CI installs, and the upgrade path is Satis (static, self-hosted, stays on-brand) generating metadata over the same repos. Until then, VCS entries plus tags cover the need with zero new infrastructure.&lt;/p&gt;
&lt;p&gt;Your Git server already hosts the code. Let it host the packages too.&lt;/p&gt;
</content:encoded><category>Self-hosting</category></item><item><title>Pull Request Templates That People Actually Fill In</title><link>https://laravelgit.com/blog/pull-request-templates-that-get-filled-in/</link><guid isPermaLink="true">https://laravelgit.com/blog/pull-request-templates-that-get-filled-in/</guid><description>A PR template should make good descriptions the path of least resistance. What to put in one, what to leave out, and why shorter always wins.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A pull request with an empty description is a small act of unkindness to its reviewer. They have to reconstruct the intent from the diff, guess at the testing, and ask questions the author could have answered in thirty seconds. Teams fix this with a template - then often ruin it by making the template a questionnaire nobody reads.&lt;/p&gt;
&lt;h2&gt;What a template is for&lt;/h2&gt;
&lt;p&gt;Drop a file at &lt;code&gt;.github/pull_request_template.md&lt;/code&gt; (or &lt;code&gt;.gitlab/merge_request_templates/Default.md&lt;/code&gt;) and every new pull request starts with its contents pre-filled. The author&amp;#39;s job shifts from &amp;quot;write something from nothing&amp;quot; to &amp;quot;fill in the blanks&amp;quot; - which is a much lower bar, and that is the whole point.&lt;/p&gt;
&lt;h2&gt;The sections that earn their place&lt;/h2&gt;
&lt;p&gt;Three prompts cover almost everything a reviewer needs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## What
One or two sentences: what does this change do?

## Why
What problem does it solve? Link the ticket if there is one.

## How tested
What did you run, click, or deploy to staging to convince yourself?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;quot;What&amp;quot; and &amp;quot;why&amp;quot; are the difference between a review and an interrogation. &amp;quot;How tested&amp;quot; is the one authors skip when there is no prompt, and the one that catches &amp;quot;I did not actually run it&amp;quot; before the reviewer does.&lt;/p&gt;
&lt;p&gt;For risky changes, one optional line helps: &lt;code&gt;**Risk:** what breaks if this is wrong?&lt;/code&gt; Leave it in the template as a comment or optional heading, and the answer doubles as your rollback plan - which pairs well with the habits in &lt;a href=&quot;/blog/rollback-or-roll-forward/&quot;&gt;rollback or roll forward&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What to leave out&lt;/h2&gt;
&lt;p&gt;Every field you add is a tax on every pull request. Skip:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Long checklists of things CI already checks.&lt;/strong&gt; &amp;quot;Tests pass&amp;quot; is a status badge, not a checkbox.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legalistic boilerplate.&lt;/strong&gt; Nobody has ever been protected by &amp;quot;I confirm I have read the contributing guide.&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sections that only apply to one pull request in fifty.&lt;/strong&gt; Database migration notes, screenshots, API changes - authors will add these when relevant. A heading they must delete is friction, not help.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a section is empty on most merged pull requests, cut it.&lt;/p&gt;
&lt;h2&gt;Keep it alive&lt;/h2&gt;
&lt;p&gt;A template is a living document with exactly one success metric: do merged pull requests have useful descriptions? Skim the last ten. If &amp;quot;How tested&amp;quot; is always &amp;quot;yes&amp;quot;, sharpen the prompt (&amp;quot;what command did you run?&amp;quot;). If a section is always deleted, remove it.&lt;/p&gt;
&lt;p&gt;The template is also a forcing function for smaller work: a pull request too big to describe in two sentences is a pull request that should be split, which is exactly the argument in &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small pull requests get reviewed faster&lt;/a&gt;. And when the description is written, the advice in &lt;a href=&quot;/blog/writing-a-good-pull-request-description/&quot;&gt;writing a good pull request description&lt;/a&gt; makes it land.&lt;/p&gt;
&lt;p&gt;Start with three headings and resist every addition. The best template is the one nobody notices.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Reading Git History: The log Flags Worth Knowing</title><link>https://laravelgit.com/blog/reading-git-history/</link><guid isPermaLink="true">https://laravelgit.com/blog/reading-git-history/</guid><description>git log with no flags is a wall of text. Five flags turn it into a map you can actually use before a release or a review.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Plain &lt;code&gt;git log&lt;/code&gt; prints every commit at full width, oldest context buried pages deep. Nobody reads it. The flags below are the difference between archaeology and a quick look at the map.&lt;/p&gt;
&lt;h2&gt;The map&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --oneline --graph --decorate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One line per commit, branch structure drawn, branches and tags labelled. Add &lt;code&gt;--all&lt;/code&gt; to see every branch at once instead of just the one you are on. This combination answers &amp;quot;where is everything right now&amp;quot; faster than any GUI, and it is the natural first step before a rebase or a release.&lt;/p&gt;
&lt;h2&gt;The filter&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --oneline --since=&amp;quot;2 weeks ago&amp;quot; --author=sara
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Time and author filters narrow the noise. The one that earns its keep before a release:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --oneline v1.4.0..main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything on main since the tag - exactly the input a changelog needs.&lt;/p&gt;
&lt;h2&gt;The detail&lt;/h2&gt;
&lt;p&gt;When a commit list shows something interesting, two flags show what it did without leaving the log:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --stat     # files changed and how much
git log -p         # the full diff per commit
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The tracers&lt;/h2&gt;
&lt;p&gt;Two more for tracking specific things through time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --follow -p -- app/Models/Invoice.php   # history across renames
git log -S &amp;#39;QUEUE_CONNECTION&amp;#39; --oneline          # when did this string appear or vanish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--follow&lt;/code&gt; matters on Laravel apps, where files move as the structure settles; the pickaxe (&lt;code&gt;-S&lt;/code&gt;) finds the commit that introduced or removed a setting, a call, or a constant.&lt;/p&gt;
&lt;h2&gt;What a branch actually adds&lt;/h2&gt;
&lt;p&gt;Before reviewing or merging:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --oneline main..feature/invoice-pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just the commits the branch contributes, nothing main already has. Pair with &lt;code&gt;git diff main...feature/invoice-pdf --stat&lt;/code&gt; and you know the shape of a PR before opening it.&lt;/p&gt;
&lt;p&gt;None of this needs memorising as a wall of flags. Keep the map (&lt;code&gt;--oneline --graph --decorate&lt;/code&gt;) as a shell alias, learn the range form (&lt;code&gt;a..b&lt;/code&gt;), and look the rest up when you need it - the habit that matters is reading history before you change it.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Release Notes People Actually Read</title><link>https://laravelgit.com/blog/release-notes-people-actually-read/</link><guid isPermaLink="true">https://laravelgit.com/blog/release-notes-people-actually-read/</guid><description>A changelog generated from commits is a ledger; release notes are a letter to your users. Who you are writing for changes what you write - and what you leave out.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Two artifacts get confused constantly. The changelog answers &amp;quot;what changed, exactly&amp;quot; for developers - &lt;a href=&quot;/blog/keep-a-changelog-from-git-tags/&quot;&gt;keep a changelog from git tags&lt;/a&gt; covers generating one honestly. Release notes answer a different question for a different reader: &amp;quot;what can I do now that I could not before, and did anything I rely on change?&amp;quot; Users, not maintainers. The mistake is shipping the ledger to the letter&amp;#39;s audience.&lt;/p&gt;
&lt;h2&gt;Lead with what the reader gets&lt;/h2&gt;
&lt;p&gt;Not &amp;quot;Fixed bug in invoice retry logic&amp;quot; but &amp;quot;Failed invoice payments now retry automatically - no more manual re-runs.&amp;quot; Same change, different subject. The test for each line: can a user tell whether to care in five seconds? If the change is invisible to them - refactors, dependency bumps, internal cleanup - it does not belong in release notes at all. It belongs in the changelog, which is exactly why having both frees you.&lt;/p&gt;
&lt;h2&gt;The shape that works&lt;/h2&gt;
&lt;p&gt;Three groups, in this order, skipping any that are empty:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;New&lt;/strong&gt; - capabilities, with a sentence on when you would reach for each.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improved and fixed&lt;/strong&gt; - changes to things people already use, phrased from their side (&amp;quot;exports are faster&amp;quot;, not &amp;quot;optimized query&amp;quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Breaking and action needed&lt;/strong&gt; - the section everyone reads first even though it is last. Anything requiring a config change, a migration, or a new minimum version goes here, with the exact step to take. Hiding a breaking change in the fixed list is how release notes lose trust permanently.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The habits behind good notes&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Write the note with the PR, not at release time.&lt;/strong&gt; One line in the pull request description - &amp;quot;release note: ...&amp;quot; - while the context is fresh beats reconstructing twenty changes from commit messages on release day. Your &lt;a href=&quot;/blog/pull-request-templates-that-get-filled-in/&quot;&gt;PR template&lt;/a&gt; can carry the prompt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tie notes to the tag.&lt;/strong&gt; Notes ship when the version ships - see &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;deploy with tags, not branches&lt;/a&gt; - so &amp;quot;what is in production&amp;quot; and &amp;quot;what do the notes say&amp;quot; can never drift.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep a human pass.&lt;/strong&gt; Generated drafts are a fine start; a generated final is a ledger wearing the letter&amp;#39;s clothes. Ten minutes of editing per release is the whole cost.&lt;/p&gt;
&lt;p&gt;Users who trust your release notes read them. That trust is built line by line, and spent in one &amp;quot;everything changed, nothing says why.&amp;quot;&lt;/p&gt;
</content:encoded><category>Releases</category></item><item><title>Rerere: Stop Solving the Same Merge Conflict Twice</title><link>https://laravelgit.com/blog/rerere-reusing-conflict-resolutions/</link><guid isPermaLink="true">https://laravelgit.com/blog/rerere-reusing-conflict-resolutions/</guid><description>git rerere records how you resolve a conflict and replays it next time. One config flag turns repeated long-lived-branch conflicts into a non-event.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you keep a long-lived branch - a big feature, a fork of a config file, a redesign - you know the ritual: merge main in, resolve the same conflict in the same file, again, every week. Git has a built-in answer almost nobody turns on: &lt;code&gt;rerere&lt;/code&gt;, short for &amp;quot;reuse recorded resolution&amp;quot;.&lt;/p&gt;
&lt;h2&gt;One flag&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global rerere.enabled true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From then on, whenever you resolve a conflict, Git quietly records what the conflict looked like and what you turned it into. The next time the same conflict appears - same file, same conflicting hunks - Git applies your recorded resolution automatically and tells you it did:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Resolved &amp;#39;routes/web.php&amp;#39; using previous resolution.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You still review the result and commit it yourself; rerere never commits for you. It just skips the part where you re-derive a decision you already made.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Long-lived feature branches that merge main regularly: the recurring conflicts stop recurring.&lt;/li&gt;
&lt;li&gt;Rebase-heavy workflows: a rebase replays commits one by one, so the same conflict can surface at several steps. With rerere, you solve it at the first step and the rest resolve themselves. This pairs well with the linear-history style from &lt;a href=&quot;/blog/merging-vs-rebasing-for-small-teams/&quot;&gt;Merging vs Rebasing for Small Teams&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Release branches that cherry-pick: &lt;a href=&quot;/blog/cherry-picking-one-commit-in-git/&quot;&gt;cherry-picking&lt;/a&gt; a set of commits onto an older branch often hits conflicts similar to ones you resolved on main.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Trust, but verify&lt;/h2&gt;
&lt;p&gt;Rerere matches conflicts by content, so it only replays when the conflict is genuinely the same - a different hunk still stops for you. Two commands keep it honest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git rerere status    # files with recorded resolutions in play
git rerere forget    # drop a recorded resolution you no longer trust
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the normal safety nets still apply: run the tests after any auto-resolved merge, exactly as in &lt;a href=&quot;/blog/resolving-merge-conflicts-without-panic/&quot;&gt;Resolving Merge Conflicts Without Panic&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is a one-line config change with no downside for solo or team use - each clone keeps its own recordings, and nothing is shared or pushed. Turn it on, and the second time a conflict shows up is the last time you solve it.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Resolving Merge Conflicts Without Panic</title><link>https://laravelgit.com/blog/resolving-merge-conflicts-without-panic/</link><guid isPermaLink="true">https://laravelgit.com/blog/resolving-merge-conflicts-without-panic/</guid><description>A conflict is not an error, it is a question: two histories changed the same lines. Read both sides, decide, and finish the merge.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The first time Git says CONFLICT, it feels like something broke. Nothing broke. A conflict just means two branches changed the same lines and Git refuses to guess which change you want. You are the missing piece of the algorithm.&lt;/p&gt;
&lt;h2&gt;Slow down and look at the state&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lists the conflicted files under &amp;quot;Unmerged paths&amp;quot;. Everything else already merged cleanly - the conflict is usually much smaller than the panic suggests.&lt;/p&gt;
&lt;h2&gt;Read both sides&lt;/h2&gt;
&lt;p&gt;Open a conflicted file and you find the markers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
Route::get(&amp;#39;/invoices&amp;#39;, [InvoiceController::class, &amp;#39;index&amp;#39;]);
=======
Route::resource(&amp;#39;invoices&amp;#39;, InvoiceController::class);
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature/invoice-crud
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HEAD is your side - the branch you are merging into. The other side is the incoming branch. The decision is rarely &amp;quot;pick one&amp;quot;: most of the time the right answer is a combination, and in this example it is deleting both lines and writing the single route the app actually needs. Edit the file until it reads correctly and every marker is gone.&lt;/p&gt;
&lt;h2&gt;Then finish properly&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git add routes/web.php
git merge --continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two habits make this safe:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Search for leftover markers before committing: &lt;code&gt;git diff --check&lt;/code&gt; flags them, and a quick &lt;code&gt;grep -r &amp;#39;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;#39; app routes&lt;/code&gt; confirms none survived.&lt;/li&gt;
&lt;li&gt;Run the test suite after resolving, not before. The resolved state is the code that has to pass.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;If it goes wrong&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git merge --abort&lt;/code&gt; puts everything back to before the merge, any time before you commit the result. Knowing the exit exists makes the whole thing less tense. And if a conflict was resolved badly and committed, the tools from &lt;a href=&quot;/blog/when-to-reset-restore-or-revert/&quot;&gt;When to Reset, Restore, or Revert&lt;/a&gt; and &lt;a href=&quot;/blog/the-reflog-undoing-almost-anything/&quot;&gt;the reflog&lt;/a&gt; get you out.&lt;/p&gt;
&lt;h2&gt;Fewer conflicts, not zero&lt;/h2&gt;
&lt;p&gt;Conflicts scale with how long branches live and how much they overlap. Small, short-lived branches - the same habit that keeps &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;pull requests reviewable&lt;/a&gt; - turn most conflicts into thirty-second events. The ones that remain are usually telling you something useful: two people edited the same code because the code needed one decision, not two.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Restoring a Deleted File From Git History</title><link>https://laravelgit.com/blog/restoring-a-deleted-file-from-history/</link><guid isPermaLink="true">https://laravelgit.com/blog/restoring-a-deleted-file-from-history/</guid><description>If the file was ever committed, it is recoverable - even after the commit that deleted it. Find the deletion, restore from the commit before.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Someone deleted a file three commits ago - or three months ago - and now it turns out it was load-bearing. If the file was ever committed, getting it back is a two-command job.&lt;/p&gt;
&lt;h2&gt;Find where it disappeared&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --oneline --follow -- database/migrations/2026_09_14_100000_add_invoice_notes.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lists every commit that touched the file, ending with the one that deleted it. When you are not sure of the exact path, widen the search:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --all --oneline --diff-filter=D -- &amp;#39;*invoice*&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--diff-filter=D&lt;/code&gt; shows only commits where matching files were deleted.&lt;/p&gt;
&lt;h2&gt;Restore from before the deletion&lt;/h2&gt;
&lt;p&gt;Bring the file back from the commit just before the one that removed it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git restore --source=HEAD~3 -- database/migrations/2026_09_14_100000_add_invoice_notes.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file lands in your working tree as an untracked-by-that-name restoration - review it, then commit it like any new change. Restoring a deleted migration this way, before anyone runs it in production, is exactly the Laravel-shaped case: the file matters, the history of how it vanished does not.&lt;/p&gt;
&lt;p&gt;For a file deleted long ago, grab the hash of the last commit that had it and use that as the source instead of counting back from HEAD.&lt;/p&gt;
&lt;h2&gt;The honest limits&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The file must have been committed at least once. Git only versions what it was told about.&lt;/li&gt;
&lt;li&gt;Staged-but-never-committed work sometimes survives as a dangling blob: &lt;code&gt;git fsck --lost-found&lt;/code&gt; can surface it. It is a long shot, but a real one.&lt;/li&gt;
&lt;li&gt;Unstaged, uncommitted work is gone for good - the same rule as &lt;code&gt;git clean&lt;/code&gt; and &lt;code&gt;git restore&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern worth keeping: deletions in Git are just commits too, and everything committed is recoverable until the day it is not - which is the same reason the server itself still needs real backups, not just a long history.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Reverting a Merge Commit (and Getting the Feature Back)</title><link>https://laravelgit.com/blog/reverting-a-merge-commit/</link><guid isPermaLink="true">https://laravelgit.com/blog/reverting-a-merge-commit/</guid><description>git revert -m 1 takes a whole feature out of main in one commit. The trap comes later, when re-merging the branch brings nothing back.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Reverting a normal commit is a one-liner. Reverting a merge commit - the one that brought a whole feature branch into main - needs an extra decision, and it plants a trap for later. Both are manageable once you know they exist.&lt;/p&gt;
&lt;h2&gt;The -m flag&lt;/h2&gt;
&lt;p&gt;A merge commit has two parents, so Git cannot know which side&amp;#39;s changes to undo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git revert -m 1 6d12f5a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-m 1&lt;/code&gt; says &amp;quot;treat parent 1 as the main line&amp;quot; - the branch you merged &lt;em&gt;into&lt;/em&gt;, which is main for an ordinary PR merge. The revert undoes everything the feature branch added, in a single new commit. The whole feature is out of main, history untouched, nobody re-pulls a rewritten branch.&lt;/p&gt;
&lt;h2&gt;The trap: the feature will not come back on its own&lt;/h2&gt;
&lt;p&gt;Weeks later the feature is fixed and you merge the branch again - and the merge brings almost nothing. From Git&amp;#39;s point of view those commits are &lt;em&gt;already merged&lt;/em&gt;: their ancestry is in main, so a new merge sees no new commits. The fact that a later revert undid their changes is content, not ancestry, and merges do not look at content.&lt;/p&gt;
&lt;p&gt;This is the confusing part: the branch is &amp;quot;merged&amp;quot;, main lacks the code, and nothing looks wrong until the deploy.&lt;/p&gt;
&lt;h2&gt;The two ways back&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Revert the revert.&lt;/strong&gt; The clean route:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git revert 52ed808   # the commit that reverted the merge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now main has the feature&amp;#39;s changes again, and merging the branch brings only what is genuinely new. The history reads oddly - merge, revert, revert-of-revert - but it is accurate, and accuracy beats tidy here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rebase the branch.&lt;/strong&gt; If the branch is still yours and unpushed elsewhere, rebase it onto current main. The rebased commits get new hashes, so the next merge sees them as new work. This hides the oddity but rewrites the branch, with the usual force-push-with-lease implications if it was shared.&lt;/p&gt;
&lt;p&gt;For a small team, revert-the-revert is usually right: no rewriting, and the paper trail explains the whole episode to the next person who reads &lt;code&gt;git log&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;When to reach for it&lt;/h2&gt;
&lt;p&gt;Reverting the merge is the right move when the &lt;em&gt;whole feature&lt;/em&gt; has to leave main quickly - a bad release, a pulled launch - and you want it out in one reviewable commit. If only one change inside the feature is bad, revert that specific commit instead and leave the merge alone: surgical beats sweeping whenever it is an option.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>How to Review a Large Pull Request Without Losing a Day</title><link>https://laravelgit.com/blog/reviewing-a-large-pull-request/</link><guid isPermaLink="true">https://laravelgit.com/blog/reviewing-a-large-pull-request/</guid><description>Sometimes the big PR lands in your queue anyway. A reading order that surfaces the risk first, and how to give useful feedback instead of a rubber stamp or a rewrite request.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The right answer to &amp;quot;how do I review a 2,000-line pull request&amp;quot; is usually &amp;quot;ask for three smaller ones&amp;quot; - and &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small pull requests get reviewed faster&lt;/a&gt; makes that case. But sometimes the split is not happening: it is a generated migration plus its fallout, a vendored update, a refactor that only makes sense whole. Here is how to review it honestly anyway.&lt;/p&gt;
&lt;h2&gt;Read in this order&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. The description and the tests, before any source.&lt;/strong&gt; The description tells you what the author thinks they did; the tests tell you what the code claims to guarantee. If either is missing, that is your first comment - reviewing code whose intent you are guessing is how subtle bugs get waved through.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The risky files, not the first files.&lt;/strong&gt; Sort mentally by blast radius: migrations (see &lt;a href=&quot;/blog/migrations-in-code-review/&quot;&gt;reviewing migrations&lt;/a&gt;), anything touching auth, money or deletion, config and deploy changes. These get your freshest attention and line-by-line reading. Controllers and views get pattern-level reading. Generated files, snapshots and lockfiles get a skim - nobody reviews &lt;code&gt;composer.lock&lt;/code&gt; line by line, they verify it was regenerated (the &lt;a href=&quot;/blog/lockfiles-in-git-commit-them-and-survive-the-conflicts/&quot;&gt;lockfiles guide&lt;/a&gt; has the drill).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Commit by commit, if the commits are clean.&lt;/strong&gt; A well-sequenced history is the author&amp;#39;s own explanation of the change - this is what &lt;a href=&quot;/blog/tidying-commits-before-review/&quot;&gt;tidying commits before review&lt;/a&gt; is for. If the commits are &amp;quot;wip&amp;quot;, &amp;quot;fix&amp;quot;, &amp;quot;fix fix&amp;quot;, abandon this step and read the full diff.&lt;/p&gt;
&lt;h2&gt;Give feedback that fits the size&lt;/h2&gt;
&lt;p&gt;The failure mode of big reviews is binary: a defeated &amp;quot;LGTM&amp;quot; or a comment list so long it reads as hostility. The useful middle is sorting your comments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blocking:&lt;/strong&gt; correctness, security, data loss. Few of these, clearly marked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worth discussing:&lt;/strong&gt; design questions where you could be wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not blocking:&lt;/strong&gt; style and preference - say once, in one comment, not forty.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And say what you did: &amp;quot;read migrations and billing code line by line, skimmed the rest&amp;quot; is honest and lets the author ask a second reviewer for the parts you skimmed. Two honest partial reviews beat one pretend complete one.&lt;/p&gt;
&lt;p&gt;The last word stays with prevention: the best large-PR review is the one that never has to happen because the next one got split.&lt;/p&gt;
</content:encoded><category>Reviews</category></item><item><title>Review Your Own Pull Request Before Anyone Else Does</title><link>https://laravelgit.com/blog/reviewing-your-own-pull-request-first/</link><guid isPermaLink="true">https://laravelgit.com/blog/reviewing-your-own-pull-request-first/</guid><description>The cheapest review round is the one you do yourself. Read the diff as a stranger, hunt the usual suspects, and annotate the non-obvious parts.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;By the time you open a pull request, you have read your own code a dozen times - as its author. That is exactly the problem. You see what you meant, not what you wrote. A self-review pass, done in the diff view rather than your editor, catches the gap between the two while it is still free to fix.&lt;/p&gt;
&lt;h2&gt;Read the diff, not the code&lt;/h2&gt;
&lt;p&gt;Your editor shows the final state with all the context in your head. The diff shows only what changed, which is what your reviewer gets. Open the &amp;quot;files changed&amp;quot; view - or run it locally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git diff main...HEAD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read it top to bottom as if a colleague sent it. The first things that jump out are usually embarrassing in a useful way: a leftover &lt;code&gt;dump()&lt;/code&gt; in a Laravel controller, a commented-out block, a migration that adds a column the model no longer uses.&lt;/p&gt;
&lt;h2&gt;The usual suspects&lt;/h2&gt;
&lt;p&gt;A short checklist beats good intentions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debug leftovers: &lt;code&gt;dump()&lt;/code&gt;, &lt;code&gt;dd()&lt;/code&gt;, &lt;code&gt;console.log&lt;/code&gt;, stray log channels.&lt;/li&gt;
&lt;li&gt;Dead code: commented blocks, unused imports, variables set and never read.&lt;/li&gt;
&lt;li&gt;Names that made sense mid-thought: &lt;code&gt;$data2&lt;/code&gt;, &lt;code&gt;$temp&lt;/code&gt;, &lt;code&gt;$result&lt;/code&gt; where the domain has a real word.&lt;/li&gt;
&lt;li&gt;Tests that prove the happy path only - the &lt;a href=&quot;/blog/code-review-habits/&quot;&gt;code review habits&lt;/a&gt; post has the reviewer-side version of this list.&lt;/li&gt;
&lt;li&gt;Anything you cannot explain in one sentence. If you struggle, the reviewer will too.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Annotate the non-obvious parts&lt;/h2&gt;
&lt;p&gt;Some lines are strange on purpose: a workaround for a queue driver quirk, an index added for one specific report. Leave a comment on your own PR explaining them before anyone asks. It saves a review round, and it pairs naturally with a good &lt;a href=&quot;/blog/writing-a-good-pull-request-description/&quot;&gt;pull request description&lt;/a&gt; - the description says what and why, the inline comments say &amp;quot;yes, this odd line is deliberate.&amp;quot;&lt;/p&gt;
&lt;p&gt;Self-review also keeps the whole pipeline honest: PRs stay &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small and reviewable&lt;/a&gt;, commits stay &lt;a href=&quot;/blog/tidying-commits-before-review/&quot;&gt;tidy&lt;/a&gt;, and your reviewer&amp;#39;s attention goes to the decisions that actually need a second brain instead of the typos that needed a second pair of eyes - yours, an hour later.&lt;/p&gt;
</content:encoded><category>Reviews</category></item><item><title>Rollback or Roll Forward? Decide Before You Need To</title><link>https://laravelgit.com/blog/rollback-or-roll-forward/</link><guid isPermaLink="true">https://laravelgit.com/blog/rollback-or-roll-forward/</guid><description>When a deploy breaks production, you either go back or fix ahead. The right answer depends on your migrations, your data, and how fast each path is.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Something is wrong in production. The temptation is to start fixing immediately - but the first decision is which direction to move: back to the last good release, or forward to a fix. Teams that decide the criteria in advance make this call in seconds; teams that improvise it make it under pressure, badly.&lt;/p&gt;
&lt;h2&gt;Roll back when you can&lt;/h2&gt;
&lt;p&gt;Going back is the better default whenever it is actually available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The bug is in the new code, not in data the new code wrote.&lt;/li&gt;
&lt;li&gt;No migration in this release changed the schema in a way old code cannot live with.&lt;/li&gt;
&lt;li&gt;Your deploys are &lt;a href=&quot;/blog/zero-downtime-deploys-for-laravel/&quot;&gt;structured for it&lt;/a&gt; - a symlink swap back, or deploying the previous &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;tag&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A rollback is one known-good state replacing one known-bad state. There is nothing to debug; you are simply not on fire anymore.&lt;/p&gt;
&lt;h2&gt;Roll forward when you must&lt;/h2&gt;
&lt;p&gt;Sometimes back is closed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A destructive migration already ran - dropped or renamed columns the old code expects.&lt;/li&gt;
&lt;li&gt;Users have created data under the new behavior, and reverting would strand or corrupt it.&lt;/li&gt;
&lt;li&gt;The &amp;quot;old&amp;quot; state has its own known bug that this release was fixing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then the fix is a new commit on main, reviewed as fast as a review can responsibly go, deployed as a new release. Note what rolling forward is not: committing directly to the server&amp;#39;s working tree. Even mid-incident, the fix goes through Git - a branch, a very short PR, a deploy - because the history of what happened during the incident is exactly what you will want afterwards. &lt;a href=&quot;/blog/when-to-reset-restore-or-revert/&quot;&gt;When to Reset, Restore, or Revert&lt;/a&gt; covers the mechanics; the revert of the bad commit belongs in history either way.&lt;/p&gt;
&lt;h2&gt;The deciding question&lt;/h2&gt;
&lt;p&gt;Ask one thing: can the old code run against the current database? If yes, roll back - it is faster and safer than any fix you will write under stress. If no, you are rolling forward, and the time to accept that is immediately, not after forty minutes of trying to make rollback possible.&lt;/p&gt;
&lt;p&gt;The two habits that keep the rollback door open are boring ones: backward-compatible migrations, and releases structured so going back is one command. Both are decided on quiet days, which is the only time incident decisions ever get made well.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Server-Side Git Hooks Worth Running on Your Own Server</title><link>https://laravelgit.com/blog/server-side-git-hooks-worth-running/</link><guid isPermaLink="true">https://laravelgit.com/blog/server-side-git-hooks-worth-running/</guid><description>Client hooks are a favor developers do themselves; server hooks are the rule. Three pre-receive checks that earn their place on a self-hosted Git server, and the restraint that keeps them tolerable.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Local hooks (the kind in &lt;a href=&quot;/blog/git-hooks-for-laravel-projects/&quot;&gt;git hooks for Laravel projects&lt;/a&gt;) run on the developer&amp;#39;s machine, which means they run only when the developer kept them installed. Server-side hooks run on every push, for everyone, no exceptions. When you self-host, they are one of the best reasons to - and one of the easiest ways to annoy your team if you overreach.&lt;/p&gt;
&lt;h2&gt;Three checks that earn their place&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Block the secrets.&lt;/strong&gt; A pre-receive hook scanning pushed commits for private keys, &lt;code&gt;.env&lt;/code&gt; contents and common token shapes turns &lt;a href=&quot;/blog/a-secret-got-committed-now-what/&quot;&gt;a secret got committed&lt;/a&gt; from an incident into a rejected push. Gitleaks or a few good regexes; the point is that the secret never lands in the repo&amp;#39;s history at all, which is when cleanup is free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Protect the default branch.&lt;/strong&gt; Even with &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;branch protection rules&lt;/a&gt; configured in the UI, a pre-receive or update hook rejecting direct pushes to main is the belt to the platform&amp;#39;s suspenders - it still fires if someone widens a permission by mistake.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Refuse giant blobs.&lt;/strong&gt; A 200MB binary committed once lives in every clone forever. Rejecting pushes with files over a sane limit (100MB is GitHub&amp;#39;s line; yours can be lower) pushes large assets to &lt;a href=&quot;/blog/git-lfs-when-a-laravel-repo-needs-it/&quot;&gt;Git LFS&lt;/a&gt; before they become permanent.&lt;/p&gt;
&lt;h2&gt;The restraint that keeps them tolerable&lt;/h2&gt;
&lt;p&gt;A rejected push interrupts someone mid-flow, so every hook owes them three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A fast answer.&lt;/strong&gt; A hook that scans ten thousand commits on every push will be bypassed, not loved. Check the pushed range, not all of history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A message that says how to fix it.&lt;/strong&gt; &amp;quot;Push rejected: possible secret in src/config/legacy.php (line 41). Remove it, rewrite the commit, see &lt;link to your secret-removal doc&gt;.&amp;quot; A bare &amp;quot;rejected&amp;quot; teaches people to fear the server; a clear one teaches them the rule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A real escape hatch.&lt;/strong&gt; False positives happen - a test fixture that looks like a key, a legitimate big file. Someone on the team should be able to bypass with a reason, and that bypass should be logged, not forbidden.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start with the secret scanner; it has the best ratio of one-time setup to incidents prevented. Add rules the way you add tests: when the failure actually happens, not in anticipation of every imaginable one.&lt;/p&gt;
</content:encoded><category>Self-hosting</category></item><item><title>Five git config Settings Worth Changing Today</title><link>https://laravelgit.com/blog/settings-worth-changing-in-your-git-config/</link><guid isPermaLink="true">https://laravelgit.com/blog/settings-worth-changing-in-your-git-config/</guid><description>Git&apos;s defaults are from another decade. Five one-line settings that prevent real mistakes: safer pulls, pruned remotes, remembered conflict resolutions and more.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Git ships with conservative defaults that made sense in 2005 and mostly survive on inertia. Five one-line changes make day-to-day mistakes structurally harder. Each takes ten seconds; none changes anything you have already committed.&lt;/p&gt;
&lt;h2&gt;1. pull.ff=only - pulls that cannot create surprise merges&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git config --global pull.ff only
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A plain &lt;code&gt;git pull&lt;/code&gt; on a diverged branch creates a merge commit you did not ask for. With this setting, a pull either fast-forwards cleanly or stops and tells you to decide - rebase or merge - consciously. It converts a common accident into a deliberate choice.&lt;/p&gt;
&lt;h2&gt;2. fetch.prune=true - remotes that tidy themselves&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git config --global fetch.prune true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deleted remote branches normally haunt your &lt;code&gt;git branch -a&lt;/code&gt; forever. Prune makes every fetch drop the local copies of branches that no longer exist on the remote, so the list you see is the list that is real.&lt;/p&gt;
&lt;h2&gt;3. rerere.enabled=true - resolve each conflict once&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git config --global rerere.enabled true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reuse recorded resolution: Git remembers how you resolved a conflict and replays it the next time the same conflict appears - a gift for anyone who rebases long branches or keeps a fork in sync. Our &lt;a href=&quot;/blog/rerere-reusing-conflict-resolutions/&quot;&gt;rerere guide&lt;/a&gt; shows the workflow in full.&lt;/p&gt;
&lt;h2&gt;4. push.autoSetupRemote=true - no more --set-upstream&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git config --global push.autoSetupRemote true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pushing a new branch stops failing with the &amp;quot;no upstream&amp;quot; lecture and just sets the upstream. Modern Git (2.37+) has this built in; it removes the most copy-pasted error message in the tool.&lt;/p&gt;
&lt;h2&gt;5. init.defaultBranch=main - the name you expect&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git config --global init.defaultBranch main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;New local repositories start on &lt;code&gt;main&lt;/code&gt;, matching every modern host, instead of falling back to the legacy default and forcing a rename later.&lt;/p&gt;
&lt;p&gt;One caveat: these are personal settings, not team policy. Anything the team must share belongs in documentation, hooks, or CI - a config value only travels as far as your laptop. For the shared layer, &lt;a href=&quot;/blog/git-hooks-for-laravel-projects/&quot;&gt;git hooks for Laravel projects&lt;/a&gt; is the right tool.&lt;/p&gt;
&lt;p&gt;Set all five now; your future self, mid-conflict, will be quietly grateful.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Shallow and Partial Clones: Faster CI Without Losing Your Mind</title><link>https://laravelgit.com/blog/shallow-and-partial-clones-for-faster-ci/</link><guid isPermaLink="true">https://laravelgit.com/blog/shallow-and-partial-clones-for-faster-ci/</guid><description>CI does not need ten years of history to run a test suite. How --depth, --filter and fetch-depth cut clone time, and the two gotchas that bite Laravel teams.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every CI run starts the same way: clone the repo. As the repo grows - years of history, old assets, deleted binaries that still live in the past - that clone quietly becomes the slowest part of the pipeline. The fix is usually two flags, but they come with sharp edges worth knowing about.&lt;/p&gt;
&lt;h2&gt;Shallow clone: --depth&lt;/h2&gt;
&lt;p&gt;A shallow clone downloads only the most recent commits:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone --depth 1 https://github.com/your-org/your-app.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No history, no old blobs, a fraction of the download. For a pipeline that installs dependencies and runs PHPUnit, history is dead weight. GitHub Actions spells the same idea &lt;code&gt;fetch-depth: 1&lt;/code&gt; in &lt;code&gt;actions/checkout&lt;/code&gt;; GitLab CI calls it &lt;code&gt;GIT_DEPTH: &amp;quot;1&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Partial clone: --filter&lt;/h2&gt;
&lt;p&gt;A partial clone keeps all commits but skips blobs until they are actually needed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone --filter=blob:none https://github.com/your-org/your-app.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get the full commit graph instantly; file contents are fetched on demand from the remote. This suits developer machines working on a huge repo more than CI - CI usually wants no history at all, which is the shallow clone&amp;#39;s job.&lt;/p&gt;
&lt;h2&gt;The two gotchas&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Tags and versions.&lt;/strong&gt; A shallow clone may not include tags, which breaks tooling that derives a version from &lt;code&gt;git describe&lt;/code&gt; or the nearest tag. If your deploy step stamps a version this way, either fetch tags explicitly (&lt;code&gt;git fetch --tags --depth 1&lt;/code&gt; or &lt;code&gt;fetch-depth: 0&lt;/code&gt; in Actions) or pass the version in from the pipeline. Our post on &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;deploying with tags&lt;/a&gt; covers the tagging side.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anything that diffs against the past.&lt;/strong&gt; A changelog generated from history, &lt;code&gt;git blame&lt;/code&gt;, an incremental static-analysis baseline, &lt;code&gt;git log&lt;/code&gt; for a &amp;quot;what changed since last deploy&amp;quot; note - all of these silently degrade or fail on a depth-1 clone. The rule of thumb: if a step reads history, give it history; if a step only builds and tests, shallow is safe.&lt;/p&gt;
&lt;p&gt;Measure first: time the checkout step of your slowest pipeline. If it is more than thirty seconds, a shallow clone is likely the cheapest speedup available to you - one line in the CI config, no code changes, every run a little faster.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Shipping a Hotfix Without the Drama</title><link>https://laravelgit.com/blog/shipping-a-hotfix-without-the-drama/</link><guid isPermaLink="true">https://laravelgit.com/blog/shipping-a-hotfix-without-the-drama/</guid><description>Branch from the live tag, fix one thing, tag the release, merge back. A hotfix is a small normal deploy, not a breach of process.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Production is broken and main has moved on - it contains three half-finished features that cannot ship today. The hotfix question is how to patch what is live without dragging main along. The answer is short: branch from the tag that is running, fix there, tag, deploy, merge back.&lt;/p&gt;
&lt;h2&gt;The walkthrough&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch -c hotfix/invoice-total-rounding v1.14.0
# fix the one thing, with a test
git tag -a v1.14.1 -m &amp;quot;v1.14.1&amp;quot;
git push origin hotfix/invoice-total-rounding v1.14.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deploy &lt;code&gt;v1.14.1&lt;/code&gt; exactly the way you deploy everything else - &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;Deploy With Tags, Not Branches&lt;/a&gt; applies even more under pressure, because &amp;quot;what is running&amp;quot; must have one unambiguous answer during an incident.&lt;/p&gt;
&lt;p&gt;Then bring the fix home so the next regular release does not reintroduce the bug:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch main
git merge hotfix/invoice-total-rounding
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Discipline under pressure&lt;/h2&gt;
&lt;p&gt;The rules that keep a hotfix small:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One fix.&lt;/strong&gt; A hotfix branch is not a vehicle for the cleanup you noticed on the way in. Everything else goes into a normal PR tomorrow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The test comes too.&lt;/strong&gt; If the bug was reachable once, it is reachable again; the test is what makes &amp;quot;fixed&amp;quot; verifiable rather than hoped. This is the moment the &lt;a href=&quot;/blog/a-first-ci-pipeline-for-laravel/&quot;&gt;first CI pipeline&lt;/a&gt; earns its keep - the hotfix PR gets the same green check as everything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No shortcuts around review.&lt;/strong&gt; One approval, even a two-minute one, catches the mistake made at speed. &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;Branch protection&lt;/a&gt; should have no &amp;quot;except hotfixes&amp;quot; hole; it should have a fast path through the normal gate.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;If the fix is bigger than a hotfix&lt;/h2&gt;
&lt;p&gt;When the honest assessment is &amp;quot;this needs a day of work,&amp;quot; stop calling it a hotfix and decide &lt;a href=&quot;/blog/rollback-or-roll-forward/&quot;&gt;rollback or roll-forward&lt;/a&gt; instead. A rollback buys the day cleanly. The worst outcome is the middle path: an ever-growing hotfix branch that is secretly a release, deployed with none of the care a release gets.&lt;/p&gt;
&lt;p&gt;Branch from the tag, fix one thing, tag, merge back. Written down and rehearsed once, the next production bug is a procedure, not an event.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Shortening Git With Aliases</title><link>https://laravelgit.com/blog/shortening-git-with-aliases/</link><guid isPermaLink="true">https://laravelgit.com/blog/shortening-git-with-aliases/</guid><description>git config --global alias turns the commands you type fifty times a day into two letters. A handful is useful; a hundred is a hobby.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You will type &lt;code&gt;git status&lt;/code&gt; ten thousand times this year. Aliases turn the frequent commands into a few keystrokes, and the long flag combinations into something you no longer have to remember.&lt;/p&gt;
&lt;h2&gt;Setting them&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global alias.st status
git config --global alias.sw switch
git config --global alias.br branch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git st&lt;/code&gt; now runs &lt;code&gt;git status&lt;/code&gt;. Aliases live in your &lt;code&gt;~/.gitconfig&lt;/code&gt; under &lt;code&gt;[alias]&lt;/code&gt; - plain text, editable directly when you outgrow the command line form.&lt;/p&gt;
&lt;h2&gt;The ones that earn their keys&lt;/h2&gt;
&lt;p&gt;The graph view from the history guide, as two letters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global alias.lg &amp;quot;log --oneline --graph --decorate --all&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A safe undo for the commit you just made:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global alias.undo &amp;quot;reset --soft HEAD~1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the fixup workflow paired with autosquash:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global alias.fixup &amp;quot;commit --fixup&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git fixup abc1234&lt;/code&gt; marks a commit as belonging to an earlier one; &lt;code&gt;git rebase -i --autosquash main&lt;/code&gt; then folds it in automatically. That pair is the fastest way to keep a branch tidy as you go instead of in one pass before review.&lt;/p&gt;
&lt;p&gt;Anything beyond one git command gets a &lt;code&gt;!&lt;/code&gt; prefix to run through the shell - &lt;code&gt;alias.today = &amp;quot;!git log --since=midnight --oneline&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The honest limits&lt;/h2&gt;
&lt;p&gt;Aliases are yours, not the repo&amp;#39;s. They do not exist on a teammate&amp;#39;s machine, in CI, or on a fresh laptop until you recreate them - so scripts, hooks and documentation must always use the full commands, and the aliases belong in your dotfiles backup, not in project tooling. And keep the list short: an alias you have to look up defeats the point. Five you use hourly beat fifty you typed once.&lt;/p&gt;
&lt;p&gt;If a command pair keeps recurring across the whole team, that is a sign it wants to be documentation or a shared script - not forty personal aliases converging on the same idea.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Signed Commits: When Your Team Should Bother</title><link>https://laravelgit.com/blog/signed-commits-when-to-bother/</link><guid isPermaLink="true">https://laravelgit.com/blog/signed-commits-when-to-bother/</guid><description>Signing proves a commit came from the key holder. Worth it for some repos, overhead for others. SSH keys make it cheap.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A commit&amp;#39;s author line is a text field anyone can set. Signing replaces &amp;quot;trust the name&amp;quot; with &amp;quot;trust the key&amp;quot;. Whether your team needs that depends on what the repo protects.&lt;/p&gt;
&lt;h2&gt;The cheap version: sign with your SSH key&lt;/h2&gt;
&lt;p&gt;No GPG setup anymore — git signs with the SSH key you already push with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the same public key to your git host as a signing key and your commits show as verified.&lt;/p&gt;
&lt;h2&gt;When it earns the setup&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Repos whose history feeds audits or compliance, where &amp;quot;who actually wrote this&amp;quot; needs an answer.&lt;/li&gt;
&lt;li&gt;Open source or vendor code others build on, where a forged author line is a real supply-chain risk.&lt;/li&gt;
&lt;li&gt;Teams with contractors coming and going, where key rotation is easier to reason about than usernames.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When to skip it&lt;/h2&gt;
&lt;p&gt;A private repo for a two-person team that reviews everything anyway gains little. Signing proves authorship of the commit, not quality of the code — review is still the control that matters.&lt;/p&gt;
&lt;h2&gt;If you adopt it&lt;/h2&gt;
&lt;p&gt;Turn on the host&amp;#39;s &amp;quot;require signed commits&amp;quot; only after everyone signs comfortably. Flipping it early just teaches people to bypass the rule.&lt;/p&gt;
</content:encoded><category>Security</category><category>Tooling</category></item><item><title>Small Pull Requests Get Reviewed Faster</title><link>https://laravelgit.com/blog/small-pull-requests-get-reviewed-faster/</link><guid isPermaLink="true">https://laravelgit.com/blog/small-pull-requests-get-reviewed-faster/</guid><description>Review time grows faster than diff size. Splitting work into small, single-idea pull requests gets you same-day reviews and better feedback.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A 1,500-line pull request does not take ten times longer to review than a 150-line one. It takes longer than that, because nobody starts it. Reviewers see the line count, decide they need a free afternoon they do not have, and the PR sits. Small PRs break that cycle.&lt;/p&gt;
&lt;h2&gt;Why big PRs stall&lt;/h2&gt;
&lt;p&gt;Three things happen with a large diff:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reviewers postpone it, so feedback arrives days late, after you have mentally moved on.&lt;/li&gt;
&lt;li&gt;When the review finally happens, it is shallow - nobody can hold 40 files in their head, so real bugs hide in the middle of the diff.&lt;/li&gt;
&lt;li&gt;Merges get riskier, because a PR that touches everything conflicts with everything.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A PR under a few hundred lines gets read the day it opens, and it gets read properly.&lt;/p&gt;
&lt;h2&gt;Splitting a Laravel feature&lt;/h2&gt;
&lt;p&gt;Take a typical feature: a migration, a model, a controller, routes, a form request, views, tests. That feels atomic, but it usually splits cleanly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Migration and model first - the schema is reviewable on its own.&lt;/li&gt;
&lt;li&gt;The read path next - list and show endpoints with their tests.&lt;/li&gt;
&lt;li&gt;The write path last - create, update, delete, and validation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each PR is deployable, each review is quick, and if the write path turns out to be the wrong design, you have not merged it yet. If later PRs depend on earlier ones, use stacked branches: branch the second PR off the first, and retarget it to main once the first merges.&lt;/p&gt;
&lt;h2&gt;Keeping PRs small day to day&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;One idea per PR. If the description needs an &amp;quot;and also&amp;quot;, it is two PRs.&lt;/li&gt;
&lt;li&gt;Split mechanical changes (a rename across 30 files, a Pint run) from behavioral ones, so the behavioral diff stays readable.&lt;/li&gt;
&lt;li&gt;Tidy commits before opening the PR so the reviewer reads your intent, not your process - see &lt;a href=&quot;/blog/tidying-commits-before-review/&quot;&gt;Tidying Commits Before You Open a Pull Request&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Write a description that says what to look at first - &lt;a href=&quot;/blog/writing-a-good-pull-request-description/&quot;&gt;Writing a Good Pull Request Description&lt;/a&gt; covers that.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The habit compounds: reviewers who trust your PRs to be small stop dreading them, and the whole team&amp;#39;s queue moves. Pair it with the review-side habits in &lt;a href=&quot;/blog/code-review-habits/&quot;&gt;Code Review Habits That Actually Catch Bugs&lt;/a&gt; and the review round trip gets short enough to feel like pair programming at a distance.&lt;/p&gt;
</content:encoded><category>Reviews</category></item><item><title>Squash Merge or Merge Commit: Choosing at the PR Button</title><link>https://laravelgit.com/blog/squash-merge-or-merge-commit/</link><guid isPermaLink="true">https://laravelgit.com/blog/squash-merge-or-merge-commit/</guid><description>The merge button offers three histories for the same code. Pick by what the branch&apos;s commits are worth, not by habit.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The pull request is approved and the button offers a choice: merge commit, squash, or rebase. The code landing on main is identical either way; what changes is the story main tells afterwards.&lt;/p&gt;
&lt;h2&gt;Merge commit: keep the branch as it was&lt;/h2&gt;
&lt;p&gt;A merge commit preserves every commit on the branch plus a marker joining it to main. Choose it when the branch&amp;#39;s commits are already clean - tidied with an interactive rebase, each one a real step. Main keeps the full context, and &lt;code&gt;git log --graph&lt;/code&gt; shows the branch as a readable bubble.&lt;/p&gt;
&lt;p&gt;The cost: if the branch was full of &amp;quot;wip&amp;quot; and &amp;quot;fix&amp;quot;, all of that lands on main forever.&lt;/p&gt;
&lt;h2&gt;Squash: the branch becomes one commit&lt;/h2&gt;
&lt;p&gt;Squashing collapses the whole PR into a single commit on main. Choose it for the common case: a feature branch with exploratory, fix-as-you-go commits whose value was the journey, not the record. Main gains one commit per piece of work, which is exactly the granularity a changelog and a &lt;code&gt;git log --oneline&lt;/code&gt; scan want.&lt;/p&gt;
&lt;p&gt;Two things make squashing work well. Write the squash commit message properly - it is the only message main will keep, so give it the conventional prefix and a real summary. And delete the branch after merging: pushing more commits to a branch that was already squash-merged produces a follow-up PR full of commits main already has, because the squashed commit shares no history with them.&lt;/p&gt;
&lt;h2&gt;Rebase merge: linear, commit by commit&lt;/h2&gt;
&lt;p&gt;Rebasing at merge replays the branch&amp;#39;s commits onto main one by one, no merge bubble. Choose it for small PRs that are already clean and where the team wants a strictly linear main. It asks the most of the branch - every commit lands verbatim, &amp;quot;fix typo&amp;quot; included - so it only suits branches that were tidied first.&lt;/p&gt;
&lt;h2&gt;A rule a small team can keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Branch is messy (the usual case): &lt;strong&gt;squash&lt;/strong&gt;, write a real message, delete the branch.&lt;/li&gt;
&lt;li&gt;Branch is clean and the steps matter: &lt;strong&gt;merge commit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Branch is clean and tiny: &lt;strong&gt;rebase&lt;/strong&gt; is fine, but squashing it is fine too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Agree on it once, write it in the README, and the choice stops costing a thought per PR.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>SSH Keys for Git Access: Set Up Once, Push Forever</title><link>https://laravelgit.com/blog/ssh-keys-for-git-access/</link><guid isPermaLink="true">https://laravelgit.com/blog/ssh-keys-for-git-access/</guid><description>One ed25519 key, a config entry, and a quick test - how SSH access to your Git server works and how to fix it when it doesn&apos;t.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every push to a self-hosted Git server starts with the same question: who are you? SSH keys answer it without passwords - but only if they are set up right, and &amp;quot;set up right&amp;quot; takes about five minutes once.&lt;/p&gt;
&lt;h2&gt;One modern key&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t ed25519 -C &amp;quot;stefan@work-laptop&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ed25519 is the current default: short keys, fast verification, no size decisions. The comment (&lt;code&gt;-C&lt;/code&gt;) is a label, not a login - use the machine or purpose, so that when you revoke a lost laptop&amp;#39;s key next year you can tell which one it was. A passphrase on the key is worth it; the SSH agent means you type it once per session, not once per push.&lt;/p&gt;
&lt;h2&gt;Hand the public half to the server&lt;/h2&gt;
&lt;p&gt;The file ending in &lt;code&gt;.pub&lt;/code&gt; is the one you share. On a self-hosted server it goes into the user&amp;#39;s account in the web UI (or &lt;code&gt;authorized_keys&lt;/code&gt; on a bare setup). The private half never leaves your machine - a server asking for it is a server you should stop using.&lt;/p&gt;
&lt;p&gt;Then test:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -T git@yourgit.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A greeting means you are done; &amp;quot;Permission denied (publickey)&amp;quot; means read on.&lt;/p&gt;
&lt;h2&gt;When it does not work&lt;/h2&gt;
&lt;p&gt;Three causes cover almost every failure:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The agent is not offering the key: &lt;code&gt;ssh-add -l&lt;/code&gt; lists what the agent holds; &lt;code&gt;ssh-add ~/.ssh/id_ed25519&lt;/code&gt; adds it.&lt;/li&gt;
&lt;li&gt;SSH is offering the wrong key first. Some servers drop the connection after a few failed offers. Pin the right one in &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Host yourgit.example.com
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;The remote URL is HTTPS while your account expects SSH (or the reverse). Check with &lt;code&gt;git remote -v&lt;/code&gt; - the fix is changing the URL, and if a fork or second server is involved, the two-remote layout from &lt;a href=&quot;/blog/keeping-a-fork-in-sync-with-upstream/&quot;&gt;Keeping a Fork in Sync&lt;/a&gt; keeps them straight.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Per-machine, not per-person&lt;/h2&gt;
&lt;p&gt;Treat keys as belonging to machines, not to you: laptop key, desktop key, CI deploy key. Revoking a device is then a one-line delete on the server, and the audit log of who pushed what stays meaningful - which matters more once &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;branch protection&lt;/a&gt; makes pushes consequential.&lt;/p&gt;
&lt;p&gt;Set it up once per machine, and the question &amp;quot;who are you?&amp;quot; stops appearing in your day entirely.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Staging Changes in Hunks With git add -p</title><link>https://laravelgit.com/blog/staging-changes-in-hunks-with-git-add-p/</link><guid isPermaLink="true">https://laravelgit.com/blog/staging-changes-in-hunks-with-git-add-p/</guid><description>git add -p lets you stage part of a file instead of all of it - the difference between one messy commit and two clean ones.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You sat down to fix a validation bug, and forty minutes later the file also contains a renamed method, a corrected comment, and an unrelated typo fix. &lt;code&gt;git add app/&lt;/code&gt; would bake all of that into one commit. &lt;code&gt;git add -p&lt;/code&gt; is how you split it instead.&lt;/p&gt;
&lt;h2&gt;Stage by hunk, not by file&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git add -p app/Http/Requests/StoreInvoiceRequest.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git walks through the file&amp;#39;s changes hunk by hunk and asks what to do with each:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stage this hunk [y,n,q,a,d,s,e,?]?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The keys worth memorizing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; - stage this hunk&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; - skip it (stays in your working tree)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; - split the hunk into smaller pieces, when Git&amp;#39;s chunk is too coarse&lt;/li&gt;
&lt;li&gt;&lt;code&gt;e&lt;/code&gt; - edit the hunk manually, for the cases splitting cannot reach&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt; - stop; everything already chosen stays staged&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two passes and you have the validation fix staged as one commit while the rename waits in the working tree for its own. This is the mechanical skill underneath &lt;a href=&quot;/blog/tidying-commits-before-review/&quot;&gt;Tidying Commits Before You Open a Pull Request&lt;/a&gt; - clean commits are much easier to build at staging time than to reconstruct afterwards.&lt;/p&gt;
&lt;h2&gt;Why it matters beyond tidiness&lt;/h2&gt;
&lt;p&gt;Partial staging is also a review tool. Going hunk by hunk forces you to look at every change once more before it is committed - the same effect as &lt;a href=&quot;/blog/reviewing-your-own-pull-request-first/&quot;&gt;reviewing your own pull request&lt;/a&gt;, but earlier and cheaper. The leftover &lt;code&gt;dump()&lt;/code&gt; rarely survives a &lt;code&gt;git add -p&lt;/code&gt; pass.&lt;/p&gt;
&lt;p&gt;And it keeps each commit revertable. When the typo fix and the validation fix are separate commits, rolling back a bad one with &lt;code&gt;git revert&lt;/code&gt; does not take the good one with it - the payoff promised in &lt;a href=&quot;/blog/when-to-reset-restore-or-revert/&quot;&gt;When to Reset, Restore, or Revert&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The same idea elsewhere&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git checkout -p&lt;/code&gt;, &lt;code&gt;git restore -p&lt;/code&gt;, and &lt;code&gt;git stash -p&lt;/code&gt; all speak hunk-level too: discard part of a file, or stash only the experiment while keeping the fix. Once the hunk way of thinking clicks, half of Git&amp;#39;s plumbing turns out to support it.&lt;/p&gt;
&lt;p&gt;One honest caveat: staging part of a file means the committed state is one you never ran. For risky splits, &lt;code&gt;git stash --keep-index&lt;/code&gt; before testing stashes the unstaged half so you can run the test suite against exactly what you are about to commit.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Stashing Changes in Git Without Losing Them</title><link>https://laravelgit.com/blog/stashing-changes-in-git/</link><guid isPermaLink="true">https://laravelgit.com/blog/stashing-changes-in-git/</guid><description>Stash is the drawer for work in progress: shelve it, switch context, bring it back. A few flags keep the drawer from becoming a junk pile.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You are halfway through a change when something more urgent lands: a failing deploy, a review to run, a bug on main. Committing half-work pollutes history; stashing shelves it exactly as it is.&lt;/p&gt;
&lt;h2&gt;Shelve and return&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git stash            # shelve tracked changes, clean working tree
git stash pop        # bring the latest stash back and drop it from the list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two additions cover most real cases. Untracked files are left behind by default, and a new factory or migration is usually untracked:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git stash -u
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a message, because &amp;quot;WIP on main&amp;quot; tells you nothing next week:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git stash push -u -m &amp;quot;invoice pdf: half the template&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The pile&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git stash list
git stash show -p stash@{1}
git stash apply stash@{1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pop&lt;/code&gt; applies and drops; &lt;code&gt;apply&lt;/code&gt; applies and keeps. Reach for &lt;code&gt;apply&lt;/code&gt; when the same shelved work might go on more than one branch, or whenever you are not sure - a stash you keep costs nothing, a stash you dropped and needed is gone.&lt;/p&gt;
&lt;h2&gt;Stash part of the mess&lt;/h2&gt;
&lt;p&gt;Sometimes half the working tree belongs to the urgent thing and half to your paused work. Pathspecs split them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git stash push -m &amp;quot;just the config tweak&amp;quot; config/queue.php .env.example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only those paths are shelved; the rest of the mess stays where it was.&lt;/p&gt;
&lt;h2&gt;When the stash grew into real work&lt;/h2&gt;
&lt;p&gt;A shelved experiment that turned out to be the fix does not belong on top of whatever your working tree has become. Give it its own branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git stash branch fix/invoice-pdf stash@{0}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That creates the branch from the commit you stashed on, applies the stash there, and drops it.&lt;/p&gt;
&lt;h2&gt;The honest warnings&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Stashes are local. They are never pushed, so they are not a backup and your self-hosted remote never sees them. Anything you would miss should be a commit on a branch.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop&lt;/code&gt; can conflict if the tree moved on. It works like a merge: resolve, and the stash stays in the list if the pop fails.&lt;/li&gt;
&lt;li&gt;Stashes fossilize. If &lt;code&gt;git stash list&lt;/code&gt; shows entries from last month, they are todos in disguise - apply them to a branch or drop them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the context switch is a review rather than an emergency, a worktree beats a stash entirely: the working tree stays untouched and the PR runs next door. But for the five-minute interruption, stash is the right size of tool.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Submodules: When a Laravel Team Actually Needs One</title><link>https://laravelgit.com/blog/submodules-when-a-laravel-team-needs-one/</link><guid isPermaLink="true">https://laravelgit.com/blog/submodules-when-a-laravel-team-needs-one/</guid><description>A submodule pins another repo inside yours. For shared PHP code a Composer package is almost always the better answer - but not always.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Someone suggests a submodule the first time two apps need the same code. It sounds like sharing; what it actually does is pin a commit of one repository inside another. Sometimes that is exactly what you want. For PHP code, it usually is not.&lt;/p&gt;
&lt;h2&gt;Why they have a reputation&lt;/h2&gt;
&lt;p&gt;A submodule is a second repository with its own history, and Git only records which commit it sits on. That design creates the daily friction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A plain &lt;code&gt;git clone&lt;/code&gt; or &lt;code&gt;git pull&lt;/code&gt; does not fetch submodule contents - the directory sits empty until &lt;code&gt;git submodule update --init --recursive&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The submodule&amp;#39;s HEAD is detached by default, so committing inside it needs the same care as any detached state.&lt;/li&gt;
&lt;li&gt;Updating means committing in the submodule, pushing it, then committing the new pin in the parent - two commits, two pushes, and a parent repo that is wrong if either is forgotten.&lt;/li&gt;
&lt;li&gt;CI, deploy hooks and new teammates each get to discover all of this once.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Laravel answer for shared code&lt;/h2&gt;
&lt;p&gt;If the shared thing is PHP - a domain package, SDK wrappers, shared DTOs - make it a Composer package instead. A private package on Satis or Private Packagist, or even a &lt;code&gt;path&lt;/code&gt; repository while it is small, gives you real versioning: &lt;code&gt;composer update&lt;/code&gt;, semver constraints, and a lock file that says exactly what runs where. That is strictly better than a pinned commit with no version number, and every Laravel developer already knows the workflow.&lt;/p&gt;
&lt;h2&gt;When the submodule is right&lt;/h2&gt;
&lt;p&gt;The honest cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shared non-PHP content - infrastructure definitions, deployment config, a docs site - where Composer cannot carry it anyway.&lt;/li&gt;
&lt;li&gt;Vendoring a third-party project you patch and need to track against upstream, where forking plus subtree is heavier than the pin.&lt;/li&gt;
&lt;li&gt;You genuinely want an exact, reviewable pin with no registry involved: the parent repo&amp;#39;s history shows every bump as a normal commit.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;If you take one on&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone --recurse-submodules git@yourserver:app.git
git config submodule.recurse true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The clone flag populates the submodule from day one; the config makes ordinary pulls update it too. Both remove the class of mistake that gives submodules their name, because the empty-directory surprise is the one people hit first.&lt;/p&gt;
&lt;p&gt;The rule of thumb: PHP code becomes a package; everything else is allowed to be a submodule - with the flags set before the first teammate clones it.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Switching Branches With git switch (and Why checkout Still Works)</title><link>https://laravelgit.com/blog/switching-branches-with-git-switch/</link><guid isPermaLink="true">https://laravelgit.com/blog/switching-branches-with-git-switch/</guid><description>Git 2.23 split checkout&apos;s many jobs into two clearer commands: switch for branches, restore for files. What changed, and what you can keep doing.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For years &lt;code&gt;git checkout&lt;/code&gt; did at least four different jobs: switching branches, creating branches, restoring files, and detaching HEAD. One command, four meanings, and a classic footgun - &lt;code&gt;git checkout .&lt;/code&gt; throws away uncommitted work, which surprises everyone exactly once.&lt;/p&gt;
&lt;p&gt;Since Git 2.23 the jobs are split into two commands with names that say what they do.&lt;/p&gt;
&lt;h2&gt;git switch is for branches&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch main                 # go to an existing branch
git switch -c feature/invoices  # create a branch and move to it
git switch -                    # back to the branch you were just on
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-c&lt;/code&gt; flag replaces &lt;code&gt;git checkout -b&lt;/code&gt;, and &lt;code&gt;git switch -&lt;/code&gt; is the handiest of the lot when you bounce between a feature branch and main to re-run a test suite. If you have uncommitted changes that would be overwritten, switch refuses instead of silently discarding them - the same safety checkout had, but without the file-restoring ambiguity.&lt;/p&gt;
&lt;h2&gt;git restore is for files&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git restore app/Models/Invoice.php   # discard uncommitted changes to a file
git restore --staged config/mail.php # unstage, keeping the edits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both remain destructive-or-not in exactly the ways covered in &lt;a href=&quot;/blog/when-to-reset-restore-or-revert/&quot;&gt;When to Reset, Restore, or Revert&lt;/a&gt; - the split just makes the intent obvious when you read a command back in your shell history.&lt;/p&gt;
&lt;h2&gt;What you can keep doing&lt;/h2&gt;
&lt;p&gt;Nothing was removed. &lt;code&gt;git checkout&lt;/code&gt; still works everywhere it used to, and plenty of teams never bothered switching habits. The reason to adopt the newer pair is readability: &lt;code&gt;git switch -c feature/x&lt;/code&gt; tells the next person (and future you) exactly what happened, while &lt;code&gt;git checkout&lt;/code&gt; always needs a second look to see which of its four jobs it was doing.&lt;/p&gt;
&lt;p&gt;Worth knowing alongside this: &lt;a href=&quot;/blog/stashing-changes-in-git/&quot;&gt;Stashing Changes Without Losing Your Place&lt;/a&gt; for the case where you want to switch branches but are not ready to commit, and &lt;code&gt;git switch --detach &amp;lt;commit&amp;gt;&lt;/code&gt; for deliberately inspecting an old commit - the tamer cousin of accidentally landing in a &lt;a href=&quot;/blog/what-to-do-in-a-detached-head/&quot;&gt;detached HEAD&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Tagging Releases Properly in Git</title><link>https://laravelgit.com/blog/tagging-releases-properly/</link><guid isPermaLink="true">https://laravelgit.com/blog/tagging-releases-properly/</guid><description>Lightweight tags are bookmarks; annotated tags are records. Releases deserve the record: a message, a date, and a tagger.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;git tag v1.5.0&lt;/code&gt; takes three seconds and produces the weaker of Git&amp;#39;s two tag types. For anything you will deploy, reference in a changelog, or bisect against later, take the extra flag.&lt;/p&gt;
&lt;h2&gt;Lightweight vs annotated&lt;/h2&gt;
&lt;p&gt;A lightweight tag is just a name pointing at a commit - a branch that never moves. An annotated tag is a real Git object: it carries its own message, the tagger&amp;#39;s identity and the date:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git tag -a v1.5.0 -m &amp;quot;v1.5.0: invoice PDFs, queue retry fix&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That message is the difference. Six months from now, &lt;code&gt;git show v1.5.0&lt;/code&gt; on an annotated tag tells you what the release was; on a lightweight one it shows whatever commit happened to be tagged, with no context.&lt;/p&gt;
&lt;h2&gt;Tag what you deploy&lt;/h2&gt;
&lt;p&gt;The tag should mark the commit that actually went out. On a merge-based flow that is the merge commit on main - not the last feature commit on the branch, and not &amp;quot;roughly that week&amp;quot;. Tagging anything else makes &lt;code&gt;git bisect&lt;/code&gt; between releases and &lt;code&gt;git log v1.4.0..v1.5.0&lt;/code&gt; quietly wrong, and those two commands are the whole point of having tags.&lt;/p&gt;
&lt;h2&gt;Push them deliberately&lt;/h2&gt;
&lt;p&gt;Tags do not travel with ordinary pushes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git push origin v1.5.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Push the one tag by name. &lt;code&gt;git push --tags&lt;/code&gt; sends every local tag, including the throwaway markers you made for yourself - keep that one for the rare day you actually mean it.&lt;/p&gt;
&lt;h2&gt;Optional: signed tags&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git tag -s&lt;/code&gt; signs the tag with your key, so the release record is verifiable as yours. For a small team shipping to their own server it is usually more ceremony than threat model; for anything customers or auditors pull, it is the same calculation as signing commits - cheap with SSH keys, worth it when provenance matters.&lt;/p&gt;
&lt;p&gt;Annotated tags plus the changelog habit are the whole release ritual: the tag marks what shipped and says it in a sentence, the log between tags writes the notes, and &lt;code&gt;git bisect&lt;/code&gt; between two tags finds the commit that broke the build.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>The Reflog: Undoing Almost Anything in Git</title><link>https://laravelgit.com/blog/the-reflog-undoing-almost-anything/</link><guid isPermaLink="true">https://laravelgit.com/blog/the-reflog-undoing-almost-anything/</guid><description>Reset too far, deleted the wrong branch, botched a rebase? The reflog remembers where HEAD has been, and that is usually enough.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Git rarely loses commits; it loses your pointer to them. The reflog is the record of everywhere HEAD has been, and it turns most &amp;quot;I destroyed everything&amp;quot; moments into a two-minute fix.&lt;/p&gt;
&lt;h2&gt;Find where you were&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reflog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every line is a position HEAD held, newest first: checkouts, commits, resets, rebases. Find the entry just before the mistake — its message usually names the operation that went wrong.&lt;/p&gt;
&lt;h2&gt;Go back there&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --hard HEAD@{2}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, safer, put a branch on the good position first and look at it before resetting anything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch rescue HEAD@{2}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The classic recoveries&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git reset --hard&lt;/code&gt; one commit too far: the &amp;quot;lost&amp;quot; commit is in the reflog, reset back to it.&lt;/li&gt;
&lt;li&gt;Deleted branch you still needed: &lt;code&gt;git reflog&lt;/code&gt;, find the last commit the branch pointed at, &lt;code&gt;git branch the-name &amp;lt;hash&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rebase gone wrong: the reflog holds the branch&amp;#39;s pre-rebase position as &lt;code&gt;HEAD@{n}&lt;/code&gt; from before the rebase started.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The limits&lt;/h2&gt;
&lt;p&gt;The reflog is local and expires (90 days by default for reachable entries). It saves you from your own commands, not from a disk dying — that is what the remote is for. And uncommitted work that was never staged is genuinely gone; the reflog only tracks commits.&lt;/p&gt;
&lt;p&gt;When in doubt, &lt;code&gt;git branch rescue&lt;/code&gt; first. A branch you delete after inspecting costs nothing.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Interactive Rebase: Tidying Commits Before Review</title><link>https://laravelgit.com/blog/tidying-commits-before-review/</link><guid isPermaLink="true">https://laravelgit.com/blog/tidying-commits-before-review/</guid><description>Fixup the typo commits, reword the vague ones, drop the debug one. Your branch tells a clean story before anyone reads it.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A working branch grows honest mess: &amp;quot;wip&amp;quot;, &amp;quot;fix&amp;quot;, &amp;quot;actually fix the fix&amp;quot;, a debug commit with a dump in it. Reviewers deserve better, and interactive rebase is how the branch tells a clean story before they see it.&lt;/p&gt;
&lt;h2&gt;Open the editor on your own commits&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git rebase -i main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git lists the commits your branch adds, oldest first, each marked &lt;code&gt;pick&lt;/code&gt;. Reorder the lines to reorder the commits; change the verb to change what happens to each.&lt;/p&gt;
&lt;h2&gt;The four verbs that matter&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reword&lt;/code&gt; - keep the change, rewrite the message. For every commit whose message you would not say out loud.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;squash&lt;/code&gt; - fold a commit into the one above it and combine the messages. For &amp;quot;and the test for it&amp;quot; commits.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fixup&lt;/code&gt; - fold in and discard the message. The typo fixes and &amp;quot;forgot a semicolon&amp;quot; commits disappear into the commit they belong to, which is exactly where they belonged all along.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;drop&lt;/code&gt; - delete the commit. The debug dump goes here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save, and Git replays the branch with your edits. If a squash or reorder conflicts, resolve as usual and &lt;code&gt;git rebase --continue&lt;/code&gt;; &lt;code&gt;git rebase --abort&lt;/code&gt; returns everything to how it was.&lt;/p&gt;
&lt;h2&gt;The two rules&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Only rewrite commits nobody else has. Interactive rebase rewrites history, so it belongs to your own unpushed branch - the same boundary as plain rebase and reset.&lt;/li&gt;
&lt;li&gt;If the branch was already pushed, the tidy version goes up with &lt;code&gt;git push --force-with-lease&lt;/code&gt;, never bare &lt;code&gt;--force&lt;/code&gt; - the lease refuses if a reviewer pushed anything meanwhile.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;When it goes wrong&lt;/h2&gt;
&lt;p&gt;A rebase that produced nonsense is not lost. The reflog remembers the branch&amp;#39;s pre-rebase position; &lt;code&gt;git branch rescue HEAD@{n}&lt;/code&gt; on that entry, inspect, and try again. Tidying is safe precisely because the escape hatch is reliable.&lt;/p&gt;
&lt;p&gt;The goal is not a fake-perfect history. It is a branch where every commit is one idea, described once - the version of the work you would have written if you had known the ending from the start.&lt;/p&gt;
</content:encoded><category>Tooling</category></item><item><title>Trunk-Based Development on a Small Laravel Team</title><link>https://laravelgit.com/blog/trunk-based-development-for-small-laravel-teams/</link><guid isPermaLink="true">https://laravelgit.com/blog/trunk-based-development-for-small-laravel-teams/</guid><description>Short-lived branches into main beat long feature branches: smaller merges, a main that always runs, and deploys that stay boring.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Long-lived feature branches feel safe and bill you later: the two-week branch that takes a day to merge, and the merge nobody wants to re-run. Trunk-based development is the alternative that works well for small teams.&lt;/p&gt;
&lt;h2&gt;The rules, sized for a small team&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Everything targets &lt;code&gt;main&lt;/code&gt;. Branches live hours to a couple of days, never weeks.&lt;/li&gt;
&lt;li&gt;Main always runs: tests pass, migrations apply, the app boots.&lt;/li&gt;
&lt;li&gt;Review is quick and small, because the changes are small.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Half-done work still merges&lt;/h2&gt;
&lt;p&gt;The objection is always &amp;quot;the feature is not finished&amp;quot;. Merge it anyway, behind a flag. In Laravel that is a config entry or a &lt;code&gt;Feature::active()&lt;/code&gt; check, not a branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;if (config(&amp;#39;features.new_billing&amp;#39;)) {
    // new flow
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Delete the flag when the feature ships. A flag you forget is cheaper than a branch you rebase for a month.&lt;/p&gt;
&lt;h2&gt;What it buys&lt;/h2&gt;
&lt;p&gt;Conflicts stay small because diffs stay small. Main is deployable any day, so the git-push deploy from a tag or from main stops being an event. And code review gets read, because there is finally little enough of it.&lt;/p&gt;
&lt;p&gt;Start with the rule that matters most: nothing lives on a branch longer than two days.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Reviews</category></item><item><title>What Not to Commit in a Laravel Project</title><link>https://laravelgit.com/blog/what-not-to-commit-in-a-laravel-project/</link><guid isPermaLink="true">https://laravelgit.com/blog/what-not-to-commit-in-a-laravel-project/</guid><description>vendor, .env, caches and build output: the short list of what stays out of the repo, and the .gitignore that keeps it out.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Laravel repo should hold the recipe, not the kitchen. The generated and environment-specific parts belong elsewhere.&lt;/p&gt;
&lt;h2&gt;The .gitignore that covers it&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitignore&quot;&gt;/vendor
/node_modules
.env
.env.backup
.phpunit.result.cache
/public/build
/public/hot
/storage/*.key
/storage/logs/*.log
Homestead.json
Homestead.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel&amp;#39;s default &lt;code&gt;.gitignore&lt;/code&gt; ships most of this. The ones people add by hand are the environment files and the editor/OS noise (&lt;code&gt;.idea&lt;/code&gt;, &lt;code&gt;.DS_Store&lt;/code&gt; — those belong in your global gitignore, not the project&amp;#39;s).&lt;/p&gt;
&lt;h2&gt;Why each one stays out&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vendor/&lt;/code&gt; and &lt;code&gt;node_modules/&lt;/code&gt; are rebuilt by &lt;code&gt;composer install&lt;/code&gt; and &lt;code&gt;npm ci&lt;/code&gt; from the lock files — which you do commit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; is per-machine by definition and carries secrets. Commit &lt;code&gt;.env.example&lt;/code&gt; with the keys and empty values instead.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public/build&lt;/code&gt; is Vite output; it gets rebuilt on every deploy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;storage/*.key&lt;/code&gt; and logs are runtime state, different on every server.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Already committed something?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git rm --cached -r vendor&lt;/code&gt; stops tracking it without deleting your local copy, then commit the removal. If the file carried a secret, that is not enough — the history still has it, and the rotate-first routine applies.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>What to Do in a Detached HEAD</title><link>https://laravelgit.com/blog/what-to-do-in-a-detached-head/</link><guid isPermaLink="true">https://laravelgit.com/blog/what-to-do-in-a-detached-head/</guid><description>Detached HEAD sounds like an injury. It is just HEAD pointing at a commit instead of a branch - safe to look around, one command to keep any work.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;quot;You are in &amp;#39;detached HEAD&amp;#39; state&amp;quot; reads like a warning that something broke. Nothing broke. Git is saying your HEAD - the pointer that normally sits on a branch - is pointing directly at a commit. Everything works; the only real question is what happens to commits you make here.&lt;/p&gt;
&lt;h2&gt;How you got here&lt;/h2&gt;
&lt;p&gt;The usual doors in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout 3357a04      # inspect an old commit
git checkout v1.4.0       # look at exactly what a tag shipped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bisect and rebase also park you here on purpose. Checking out a tag to reproduce a bug against the exact deployed code is a completely ordinary reason to be detached.&lt;/p&gt;
&lt;h2&gt;Looking is free, committing is fine, leaving is the catch&lt;/h2&gt;
&lt;p&gt;Read files, run the test suite, boot the app - the working tree is real. You can even commit. The catch: no branch points at those new commits, so when you switch away they become unreferenced, and the only thing that will ever find them again is the reflog.&lt;/p&gt;
&lt;p&gt;So the rule is simple. The moment you make a commit you want to keep, give it a branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch -c fix/reproduce-invoice-bug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole fix. HEAD now points at a normal branch holding your work, and the detached state is over.&lt;/p&gt;
&lt;h2&gt;If you already switched away&lt;/h2&gt;
&lt;p&gt;The commits are not gone; they are unreferenced. Find the hash you were on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reflog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recent checkouts are right at the top. Put a branch on the hash:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch rescue 3357a04
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reflog keeps these entries for 90 days by default, so &amp;quot;I detached, committed, and switched away an hour ago&amp;quot; is a two-command recovery, not a lost afternoon.&lt;/p&gt;
&lt;h2&gt;The habit&lt;/h2&gt;
&lt;p&gt;Treat detached HEAD as a reading room, not a workspace. Look all you want; the instant you write something worth keeping, &lt;code&gt;git switch -c&lt;/code&gt; before you do anything else - especially before switching branches, which is the moment the work becomes findable only through the reflog.&lt;/p&gt;
&lt;p&gt;If you find yourself detaching often just to run someone else&amp;#39;s PR without disturbing your working tree, that is the job worktrees do without the detachment at all.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>When to Reset, Restore or Revert</title><link>https://laravelgit.com/blog/when-to-reset-restore-or-revert/</link><guid isPermaLink="true">https://laravelgit.com/blog/when-to-reset-restore-or-revert/</guid><description>Three commands that all undo something in Git. The right one depends on what state you are undoing and whether anyone else has seen it.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Git has three undo commands and they are not interchangeable. &lt;code&gt;restore&lt;/code&gt; touches uncommitted work, &lt;code&gt;reset&lt;/code&gt; moves your own branch, and &lt;code&gt;revert&lt;/code&gt; undoes a commit that other people may already have. Picking by what you are undoing - and who has seen it - keeps undo boring.&lt;/p&gt;
&lt;h2&gt;Restore: uncommitted work&lt;/h2&gt;
&lt;p&gt;You edited a file and the change is wrong. It is not staged, not committed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git restore app/Models/Invoice.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Staged it by accident but still want to edit? Unstage without losing the edit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git restore --staged app/Models/Invoice.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restore only reaches work that exists in some committed form. An edit to a file that was never committed anywhere is gone for good once restored away.&lt;/p&gt;
&lt;h2&gt;Reset: commits nobody else has&lt;/h2&gt;
&lt;p&gt;You committed on your branch, have not pushed, and want those commits back as working changes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --soft HEAD~1    # undo the commit, keep everything staged
git reset HEAD~1           # undo the commit, keep the changes unstaged
git reset --hard HEAD~1    # undo the commit and throw the changes away
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reset rewrites the branch, so it belongs to history only you have seen. If a commit is already on the shared remote, reset is how you end up force-pushing over a teammate&amp;#39;s work.&lt;/p&gt;
&lt;h2&gt;Revert: commits everyone has&lt;/h2&gt;
&lt;p&gt;A commit is on main, the deploy from it broke invoices, and the team has pulled it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git revert 3e39e4f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Revert adds a new commit that applies the inverse diff. History keeps growing forward, nobody re-pulls a rewritten branch, and &lt;code&gt;git revert&lt;/code&gt; of the revert brings the change back when the fix is ready. On a Laravel app this pairs cleanly with rollback: revert the commit, run &lt;code&gt;php artisan migrate:rollback&lt;/code&gt; if it carried a migration, deploy again.&lt;/p&gt;
&lt;h2&gt;The rule of thumb&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Not committed: &lt;strong&gt;restore&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Committed, not pushed: &lt;strong&gt;reset&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pushed: &lt;strong&gt;revert&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the mistake is bigger than one commit - a wrong rebase, a reset too far - the reflog remembers where the branch was before any of it. And when you are about to undo something you are not sure about, &lt;code&gt;git branch rescue&lt;/code&gt; first: a branch you delete after inspecting costs nothing.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Why Git Blame Is Not About Blame</title><link>https://laravelgit.com/blog/why-git-blame-is-not-about-blame/</link><guid isPermaLink="true">https://laravelgit.com/blog/why-git-blame-is-not-about-blame/</guid><description>Blame answers &apos;why does this line exist&apos;, not &apos;who do I talk to about breaking it&apos;. Used well it is the fastest route from a strange line to the decision behind it.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The name is the worst thing about &lt;code&gt;git blame&lt;/code&gt;. The command does not exist to find someone to fault; it exists to answer the question every developer asks twenty times a week: why is this line here?&lt;/p&gt;
&lt;h2&gt;From line to decision&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git blame app/Models/Invoice.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every line comes back annotated with the commit that last touched it, the author and the date. The commit hash is the valuable part - follow it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git show 8c6e7ec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you have the full diff, the commit message, and usually the pull request it came from. That chain - line, commit, message, PR - is the difference between staring at a strange condition and understanding the edge case it guards.&lt;/p&gt;
&lt;p&gt;Two options earn their keep:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git blame -L 40,60 app/Models/Invoice.php   # just these lines
git blame -w config/queue.php               # ignore whitespace-only changes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-w&lt;/code&gt; matters more than it looks: without it, the last person to reindent a block &amp;quot;owns&amp;quot; every line in it.&lt;/p&gt;
&lt;h2&gt;Hide the reformat commit&lt;/h2&gt;
&lt;p&gt;Every repo has one: the day the whole codebase was reformatted, and blame started pointing every line at it. Git has a built-in answer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config blame.ignoreRevsFile .git-blame-ignore-revs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Put the reformat commit&amp;#39;s hash in &lt;code&gt;.git-blame-ignore-revs&lt;/code&gt;, commit the file, and blame skips those revisions on everyone&amp;#39;s machine. GitHub honours the same file in its blame view, so the fix works locally and in the browser.&lt;/p&gt;
&lt;h2&gt;When blame is the wrong tool&lt;/h2&gt;
&lt;p&gt;Blame shows the last commit to touch a line. To find when something appeared or disappeared entirely - a setting, a constant, a call - the pickaxe is better:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log -S &amp;#39;QUEUE_CONNECTION&amp;#39; --oneline -- config/queue.php .env.example
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The culture part&lt;/h2&gt;
&lt;p&gt;Used as a name-and-shame tool, blame makes people defensive and teaches them to avoid touching old code. Used as a documentation tool, it does the opposite: the author of a line is the person most likely to remember the constraints behind it, and &amp;quot;what were we solving here?&amp;quot; gets a useful answer where &amp;quot;who broke this?&amp;quot; gets silence.&lt;/p&gt;
&lt;p&gt;The habit to build is simple. When a line surprises you, blame it, read the commit, and ask about the decision - not the person.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Why You Should Force Push With --force-with-lease</title><link>https://laravelgit.com/blog/why-you-should-force-push-with-lease/</link><guid isPermaLink="true">https://laravelgit.com/blog/why-you-should-force-push-with-lease/</guid><description>Rewriting your own branch is normal after a rebase. --force-with-lease makes the push refuse to run if someone else touched the branch since your fetch.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you rebase, squash or amend, you will eventually need to force-push. The question is never whether force-pushing is evil - it is whether the push can silently delete someone else&amp;#39;s work. Plain &lt;code&gt;--force&lt;/code&gt; can. &lt;code&gt;--force-with-lease&lt;/code&gt; cannot.&lt;/p&gt;
&lt;h2&gt;The legitimate case&lt;/h2&gt;
&lt;p&gt;Your PR branch, reviewed once, now rebased onto current main with the fixups squashed in. The remote still holds the pre-rebase version, and the only way to update it is a force-push. That is fine: the branch is yours, and nobody should be building on it.&lt;/p&gt;
&lt;h2&gt;What --force actually says&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git push --force
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;quot;Make the remote branch point here, whatever it points at now.&amp;quot; If a teammate pushed a commit to your branch since your last fetch - a reviewer fixing a typo through the web UI is the classic way - that commit is gone from the branch, and nobody was asked.&lt;/p&gt;
&lt;h2&gt;What --force-with-lease says&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git push --force-with-lease
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;quot;Make the remote branch point here, but only if it still points where my last fetch left it.&amp;quot; The lease is your local record of the remote ref. If the remote moved - anyone pushed anything - the push is refused and nothing is overwritten. You fetch, look at what arrived, rebase onto it, and push again.&lt;/p&gt;
&lt;p&gt;For another layer, &lt;code&gt;--force-if-includes&lt;/code&gt; additionally checks that the remote&amp;#39;s current commits are already part of what you are pushing, which catches some stale-reflog corner cases the lease alone misses.&lt;/p&gt;
&lt;h2&gt;The two rules that make it boring&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Never force-push a branch other people build on. Main, release branches, long-lived feature branches: these get merges and reverts, not rewrites. Branch protection should simply forbid it there.&lt;/li&gt;
&lt;li&gt;On your own short-lived branches, always &lt;code&gt;--force-with-lease&lt;/code&gt;, never bare &lt;code&gt;--force&lt;/code&gt;. Put it in muscle memory or in an alias, because the one time you reach for plain &lt;code&gt;--force&lt;/code&gt; is the one time the remote had a surprise waiting.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the worst already happened and a force-push ate a commit, it is recoverable: the commit still exists on the machine that made it, and that machine&amp;#39;s reflog knows the hash. A &lt;code&gt;git push &amp;lt;hash&amp;gt;:refs/heads/branch-name&lt;/code&gt; from there puts it back.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Writing a CONTRIBUTING.md People Actually Read</title><link>https://laravelgit.com/blog/writing-a-contributing-guide/</link><guid isPermaLink="true">https://laravelgit.com/blog/writing-a-contributing-guide/</guid><description>A contributing guide is not a legal document - it is the answer to &apos;how do I get this running and get a PR in.&apos; The sections that earn their place, in the order a newcomer needs them.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most CONTRIBUTING.md files fail the same way: they are written for the maintainer&amp;#39;s peace of mind, not the reader&amp;#39;s goal. A new contributor - or a teammate joining the project, which is the same reader - wants three answers: how do I run this, how do I change it, and how do I get the change accepted. Write those, in that order, and stop.&lt;/p&gt;
&lt;h2&gt;1. Setup that actually works&lt;/h2&gt;
&lt;p&gt;The first section is the one that gets read: clone, install, run. For a Laravel app that is the composer install, &lt;code&gt;.env&lt;/code&gt; setup (which our &lt;a href=&quot;/blog/managing-env-files-across-environments/&quot;&gt;env files guide&lt;/a&gt; covers properly), migrations, and the one command that proves it works - &lt;code&gt;php artisan serve&lt;/code&gt;, &lt;code&gt;npm run dev&lt;/code&gt;, the test suite.&lt;/p&gt;
&lt;p&gt;The test is brutal and simple: hand the repo to someone who has never seen it and watch. Every place they stop is a line your guide is missing. Do this once a year, because setup rots.&lt;/p&gt;
&lt;h2&gt;2. The workflow, concretely&lt;/h2&gt;
&lt;p&gt;Branch naming if you have a convention, what a pull request should look like (link your &lt;a href=&quot;/blog/pull-request-templates-that-get-filled-in/&quot;&gt;PR template&lt;/a&gt; if you have one), who reviews, and how long a review usually takes. That last one matters more than anything else in the file: &amp;quot;expect a first review within two working days&amp;quot; turns waiting from neglect into process.&lt;/p&gt;
&lt;p&gt;Keep it to what a contributor does, not how the team philosophizes. Three paragraphs is plenty.&lt;/p&gt;
&lt;h2&gt;3. The bar for merging&lt;/h2&gt;
&lt;p&gt;What CI must pass, whether commits get squashed (see &lt;a href=&quot;/blog/squash-merge-or-merge-commit/&quot;&gt;squash merge or merge commit&lt;/a&gt;), any style tooling that will reject the PR - Pint, ESLint, whatever runs. Contributors forgive strict rules; they do not forgive discovering them in a red CI run after pushing.&lt;/p&gt;
&lt;h2&gt;What to leave out&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code of conduct boilerplate in the same file.&lt;/strong&gt; Worth having, worth its own file - GitHub surfaces CODE_OF_CONDUCT.md separately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architecture essays.&lt;/strong&gt; Link the README or docs instead; a guide that takes twenty minutes to read gets zero minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rules nobody enforces.&lt;/strong&gt; Each one teaches the reader that the file is decorative, and then they skip the real rules too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A contributing guide is a promise: follow this and your work lands. Every line either keeps that promise shorter or makes it longer. Keep the short ones.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Writing a Pull Request Description People Actually Read</title><link>https://laravelgit.com/blog/writing-a-good-pull-request-description/</link><guid isPermaLink="true">https://laravelgit.com/blog/writing-a-good-pull-request-description/</guid><description>What, why, and how to see it working. Three short sections beat a wall of text and speed up every review.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Reviewers decide how carefully to read from the description. A blank one says &amp;quot;good luck&amp;quot;; a wall of text says the same thing louder. Three short sections cover almost every PR.&lt;/p&gt;
&lt;h2&gt;What changed&lt;/h2&gt;
&lt;p&gt;One or two sentences, no file list — the diff already shows files. &amp;quot;Adds a queued job that re-sends failed webhook deliveries with backoff&amp;quot; is enough.&lt;/p&gt;
&lt;h2&gt;Why&lt;/h2&gt;
&lt;p&gt;The part that saves the reviewer a Slack message. Link the ticket if there is one, otherwise say what was broken or missing. &amp;quot;Deliveries failed silently when the endpoint returned 5xx, and clients had no way to replay them&amp;quot; is a reason; &amp;quot;requested changes&amp;quot; is not.&lt;/p&gt;
&lt;h2&gt;How to see it working&lt;/h2&gt;
&lt;p&gt;The step everyone skips and the one reviewers want most: the command to run, the page to open, the seed data to create. On a Laravel app that is usually a route to hit, an artisan command, or a test to run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan test --filter=WebhookRetryTest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Keep it short on purpose&lt;/h2&gt;
&lt;p&gt;If the description needs paragraphs, the PR probably needs splitting. Small PRs with three honest sections get read, get real comments, and merge while the context is still warm.&lt;/p&gt;
</content:encoded><category>Reviews</category></item><item><title>Writing a Postmortem That Actually Changes Something</title><link>https://laravelgit.com/blog/writing-a-postmortem-that-changes-things/</link><guid isPermaLink="true">https://laravelgit.com/blog/writing-a-postmortem-that-changes-things/</guid><description>Most postmortems are written, nodded at, and never read again. The difference between theater and change is three sections written honestly: timeline, contributing factors, and actions with owners.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The deploy broke billing for forty minutes. Everyone is relieved it is over, everyone is tired, and now someone has to write the postmortem. The temptation is a document that says &amp;quot;a mistake happened, we fixed it, we will be more careful.&amp;quot; That document is worse than none: it performs learning without doing any.&lt;/p&gt;
&lt;p&gt;A postmortem that changes things has three sections, and each has a rule.&lt;/p&gt;
&lt;h2&gt;Timeline: facts, in order, with timestamps&lt;/h2&gt;
&lt;p&gt;When did monitoring fire, who saw it, what did they try, when did recovery start. Written from logs and chat history, not memory - memory edits itself within hours. The timeline is where you discover the real story: the alert that fired twenty minutes before anyone looked, the rollback that took fifteen minutes because nobody remembered the command (which is why &lt;a href=&quot;/blog/rollback-or-roll-forward/&quot;&gt;rollback or roll forward&lt;/a&gt; exists as a pre-made decision, not an improvised one).&lt;/p&gt;
&lt;p&gt;Rule: no judgments in the timeline. &amp;quot;Deployed v2.3.1 at 14:02&amp;quot; is a fact. &amp;quot;Deployed carelessly&amp;quot; is a verdict, and verdicts here make people defensive before you reach the part that matters.&lt;/p&gt;
&lt;h2&gt;Contributing factors: plural, and never a person&amp;#39;s name&lt;/h2&gt;
&lt;p&gt;&amp;quot;Stefan merged the bad migration&amp;quot; is not a cause; it is a scapegoat. The contributing factors are the things that made the mistake easy and its blast radius large: migrations deploy without a dry run, the billing smoke test only runs hourly, one person holds the rollback knowledge. Any of these could bite a different person next week, which is exactly why they are the fixable layer.&lt;/p&gt;
&lt;p&gt;Rule: if a factor cannot be assigned to a system, a process or a missing safeguard, keep digging.&lt;/p&gt;
&lt;h2&gt;Actions: owned, dated, small&lt;/h2&gt;
&lt;p&gt;Every action has one name and one date. &amp;quot;Improve testing&amp;quot; is a wish; &amp;quot;Amira adds a billing smoke test to the deploy pipeline by Nov 7&amp;quot; is an action. Three real actions beat ten aspirational ones - and they go in the tracker, not just the document, because a postmortem action without a ticket is a hope.&lt;/p&gt;
&lt;p&gt;Rule: review the actions at the next incident review or team meeting. An action that slips twice gets escalated or honestly dropped; silently forgotten is the one outcome that teaches the team postmortems are theater.&lt;/p&gt;
&lt;p&gt;Write it within two days, share it where everyone can read it, and thank the people named in the timeline - they are the ones who told you the truth.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Zero-Downtime Deploys for Laravel, Explained Simply</title><link>https://laravelgit.com/blog/zero-downtime-deploys-for-laravel/</link><guid isPermaLink="true">https://laravelgit.com/blog/zero-downtime-deploys-for-laravel/</guid><description>Releases in directories, a symlink swap, and migrations that run before code. The boring structure behind deploys nobody notices.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The naive Laravel deploy is &lt;code&gt;git pull&lt;/code&gt; on the live directory, and it has a window where the code is half-old, half-new - composer autoloads one version, the templates reference another, and a visitor in that second gets a broken page. Zero-downtime deploys remove the window with one idea: never change the directory the web server is looking at.&lt;/p&gt;
&lt;h2&gt;Releases and one symlink&lt;/h2&gt;
&lt;p&gt;The structure, used by every deploy tool worth using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/var/www/app/
  releases/
    20261020-141122/
    20261021-093015/
  current -&amp;gt; releases/20261021-093015
  shared/
    .env
    storage/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each deploy is a full checkout into a new timestamped directory. Composer install, &lt;code&gt;artisan config:cache&lt;/code&gt;, asset builds - all happen there, while &lt;code&gt;current&lt;/code&gt; still points at the previous release and the site keeps serving it. The switch is a single atomic operation: repoint the &lt;code&gt;current&lt;/code&gt; symlink. If anything is wrong, point it back - rollback takes one second because the old release directory is still intact.&lt;/p&gt;
&lt;p&gt;Two things must live outside releases: &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;storage/&lt;/code&gt;. Symlink them in from &lt;code&gt;shared/&lt;/code&gt; so every release uses the same environment and the same uploaded files.&lt;/p&gt;
&lt;h2&gt;Migrations are the hard part&lt;/h2&gt;
&lt;p&gt;Code swaps are easy; databases cannot be symlinked back. The rule that keeps deploys reversible: every migration must work with both the old and the new code. Adding a column is fine. Renaming one is not - do it as three deploys (add new, backfill and dual-write, drop old). Run &lt;code&gt;php artisan migrate --force&lt;/code&gt; before the symlink swap, so the schema is ready for the release that is about to go live.&lt;/p&gt;
&lt;h2&gt;The queue and scheduler&lt;/h2&gt;
&lt;p&gt;Laravel queues hold serialized jobs, so after the swap, restart the workers (&lt;code&gt;artisan queue:restart&lt;/code&gt;) or they keep running the old code from memory. The scheduler needs no restart - cron resolves through &lt;code&gt;current&lt;/code&gt; on each run.&lt;/p&gt;
&lt;p&gt;That is the whole shape: build beside, switch atomically, migrate compatibly, restart workers. Whether you script it yourself or adopt a tool, &lt;a href=&quot;/blog/deploy-laravel-with-git-push/&quot;&gt;Deploy Laravel With Git Push&lt;/a&gt; covers the pipeline around it, and &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;Deploy With Tags, Not Branches&lt;/a&gt; decides what each release directory should contain. A deploy your users never notice is not luxury - it is just structure.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Reviewing Pull Requests With Git Worktrees</title><link>https://laravelgit.com/blog/git-worktrees-for-laravel-reviews/</link><guid isPermaLink="true">https://laravelgit.com/blog/git-worktrees-for-laravel-reviews/</guid><description>Run the PR next to your working copy instead of stashing and switching: git worktree, a second .env, and a throwaway database.</description><pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Reviewing a pull request properly means running it, not just reading the diff. Switching your working copy back and forth costs your uncommitted state every time. Worktrees sidestep that.&lt;/p&gt;
&lt;h2&gt;Check out the PR next to your work&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch origin
git worktree add ../myapp-pr-42 pr-42-branch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get a second directory on the PR branch while your main checkout stays exactly as it was.&lt;/p&gt;
&lt;h2&gt;Give it its own environment&lt;/h2&gt;
&lt;p&gt;A Laravel app needs more than the code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ../myapp-pr-42
composer install
cp ../myapp/.env .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then change what must not clash: &lt;code&gt;APP_URL&lt;/code&gt; and port if you serve both at once, and &lt;code&gt;DB_DATABASE&lt;/code&gt; to a throwaway database so the PR&amp;#39;s migrations do not touch your dev data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan migrate --seed
php artisan serve --port=8001
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tear it down cleanly&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git worktree remove ../myapp-pr-42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The branch stays until you delete it; the directory and database go.&lt;/p&gt;
&lt;h2&gt;When it pays&lt;/h2&gt;
&lt;p&gt;One PR a month is fine to stash and switch. A team merging daily needs the review to be cheap, and cheap means the PR runs in its own directory while your own work never moves.&lt;/p&gt;
</content:encoded><category>Reviews</category><category>Tooling</category></item><item><title>Deploying a Laravel App With Git Push</title><link>https://laravelgit.com/blog/deploy-laravel-with-git-push/</link><guid isPermaLink="true">https://laravelgit.com/blog/deploy-laravel-with-git-push/</guid><description>A bare repo and a post-receive hook are enough for a honest small-team deploy: checkout, composer, migrate, cache.</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Self-hosting your git and your app on the same box has a nice property: a push can be the deploy. You need a bare repository and one hook.&lt;/p&gt;
&lt;h2&gt;The bare repo&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git init --bare /srv/git/myapp.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add it as a remote on your laptop and push a branch you deploy from, for example &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The post-receive hook&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/srv/git/myapp.git/hooks/post-receive&lt;/code&gt;, made executable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
set -e
APP=/var/www/myapp
git --work-tree=$APP --git-dir=/srv/git/myapp.git checkout -f main
cd $APP
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;checkout -f&lt;/code&gt; syncs the working tree to the pushed commit. The artisan steps are the boring, repeatable part of every Laravel release.&lt;/p&gt;
&lt;h2&gt;Keep it honest&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Run the hook as a user that owns &lt;code&gt;/var/www/myapp&lt;/code&gt; but cannot sudo.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;migrate --force&lt;/code&gt; runs unattended, so only push migrations you have tested against a copy of production data.&lt;/li&gt;
&lt;li&gt;If the hook fails, the push still lands in the repo; redeploy by pushing an empty commit or rerunning the hook on the server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is one step above &amp;quot;SSH in and pull&amp;quot;. When you outgrow it — zero-downtime releases, rollbacks, multiple servers — that is the point where a real pipeline earns its keep, not before.&lt;/p&gt;
</content:encoded><category>Releases</category><category>Self-hosting</category></item><item><title>A Secret Got Committed. Now What?</title><link>https://laravelgit.com/blog/a-secret-got-committed-now-what/</link><guid isPermaLink="true">https://laravelgit.com/blog/a-secret-got-committed-now-what/</guid><description>Rotate first, scrub second, prevent third. The order matters more than the tooling.</description><pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It happens: &lt;code&gt;.env&lt;/code&gt;, an API key, a database password ends up in a commit. The fix is the same every time, and the order matters.&lt;/p&gt;
&lt;h2&gt;1. Rotate the secret first&lt;/h2&gt;
&lt;p&gt;Deleting the file from git does nothing while the key still works. Assume anything committed is public, even in a private repo: clones, forks and CI caches all copy it. Revoke or rotate the credential at the provider before touching the repository.&lt;/p&gt;
&lt;h2&gt;2. Scrub the history&lt;/h2&gt;
&lt;p&gt;Once the old key is dead, remove it from history so the repo stops carrying it. &lt;code&gt;git filter-repo&lt;/code&gt; is the tool that works:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install git-filter-repo
git filter-repo --path .env --invert-paths
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This rewrites every commit, so force-push and have the team re-clone. A commit hash they still hold can resurrect the file.&lt;/p&gt;
&lt;h2&gt;3. Stop it happening again&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.gitignore&lt;/code&gt; the file and commit a &lt;code&gt;.env.example&lt;/code&gt; with the keys but no values.&lt;/li&gt;
&lt;li&gt;Laravel reads real values from the environment, so nothing secret needs to be in the repo at all.&lt;/li&gt;
&lt;li&gt;Turn on your host&amp;#39;s push protection or run a pre-commit scan so a key gets caught before it lands.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The short version&lt;/h2&gt;
&lt;p&gt;A scrubbed repo with a live key is still a breach. A rotated key with an ugly history is just housekeeping. Rotate first.&lt;/p&gt;
</content:encoded><category>Security</category><category>Workflows</category></item><item><title>Git Hooks That Earn Their Place on a Laravel Project</title><link>https://laravelgit.com/blog/git-hooks-for-laravel-projects/</link><guid isPermaLink="true">https://laravelgit.com/blog/git-hooks-for-laravel-projects/</guid><description>Three hooks that catch real problems: Pint before commit, tests before push, and a sane commit message check.</description><pubDate>Sat, 17 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hooks live in &lt;code&gt;.git/hooks&lt;/code&gt;, which Git does not version. Point &lt;code&gt;core.hooksPath&lt;/code&gt; at a folder in the repo so the whole team runs the same ones:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config core.hooksPath bin/git-hooks
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pre-commit: fix style before it lands&lt;/h2&gt;
&lt;p&gt;Run Laravel Pint against the staged PHP files only, so the check stays fast:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/sh
files=$(git diff --cached --name-only --diff-filter=ACM -- &amp;#39;*.php&amp;#39;)
[ -z &amp;quot;$files&amp;quot; ] &amp;amp;&amp;amp; exit 0
echo &amp;quot;$files&amp;quot; | xargs ./vendor/bin/pint
echo &amp;quot;$files&amp;quot; | xargs git add
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pre-push: run the tests that matter&lt;/h2&gt;
&lt;p&gt;A full suite on every commit is too slow to keep. Run it on push instead, where a failure is still cheap:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/sh
php artisan test --stop-on-failure
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;commit-msg: keep the log readable&lt;/h2&gt;
&lt;p&gt;Reject empty subjects and very short ones. Everything stricter tends to get bypassed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/sh
subject=$(head -1 &amp;quot;$1&amp;quot;)
if [ ${#subject} -lt 10 ]; then
  echo &amp;quot;Commit subject too short - say what changed and why.&amp;quot; &amp;gt;&amp;amp;2
  exit 1
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Keep them honest&lt;/h2&gt;
&lt;p&gt;A hook the team bypasses with &lt;code&gt;--no-verify&lt;/code&gt; every day is worse than none. Start with these three, keep them under a few seconds, and only add more when a real mistake slips through twice.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Tooling</category></item><item><title>Code Review Habits That Keep Main Clean</title><link>https://laravelgit.com/blog/code-review-habits/</link><guid isPermaLink="true">https://laravelgit.com/blog/code-review-habits/</guid><description>Small changes, clear descriptions and fast feedback.</description><pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Review works best when it is cheap.&lt;/p&gt;
&lt;h2&gt;Keep changes small&lt;/h2&gt;
&lt;p&gt;Small changes get read. Large ones get skimmed.&lt;/p&gt;
&lt;h2&gt;Say what and why&lt;/h2&gt;
&lt;p&gt;A short description of the change and the reason saves the reviewer a guess.&lt;/p&gt;
&lt;h2&gt;Review quickly&lt;/h2&gt;
&lt;p&gt;A review that waits a week blocks the next change too.&lt;/p&gt;
</content:encoded><category>Reviews</category><category>Workflows</category></item><item><title>Shipping a Laravel App From Git</title><link>https://laravelgit.com/blog/shipping-laravel-from-git/</link><guid isPermaLink="true">https://laravelgit.com/blog/shipping-laravel-from-git/</guid><description>Keep deploys boring: build from a tagged commit, migrate, cache.</description><pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Laravel deploy is mostly the same few steps each time.&lt;/p&gt;
&lt;h2&gt;Build from a known commit&lt;/h2&gt;
&lt;p&gt;Deploy a tag or a commit hash, not whatever is on a branch right now.&lt;/p&gt;
&lt;h2&gt;Run migrations deliberately&lt;/h2&gt;
&lt;p&gt;Run them as a step you can see, and know how to roll back.&lt;/p&gt;
&lt;h2&gt;Warm the caches&lt;/h2&gt;
&lt;p&gt;Config, route and view caches are part of the release, not an afterthought.&lt;/p&gt;
</content:encoded><category>Releases</category><category>Workflows</category></item><item><title>Why Teams Self-Host Their Git</title><link>https://laravelgit.com/blog/why-self-host-your-git/</link><guid isPermaLink="true">https://laravelgit.com/blog/why-self-host-your-git/</guid><description>Control, access and where your code lives.</description><pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hosted Git is convenient. Self-hosting is about control.&lt;/p&gt;
&lt;h2&gt;Know where the code lives&lt;/h2&gt;
&lt;p&gt;Repositories sit on servers you run. You decide on backups, retention and who gets in.&lt;/p&gt;
&lt;h2&gt;Set your own access rules&lt;/h2&gt;
&lt;p&gt;Give people the access their role needs and remove it when they leave.&lt;/p&gt;
&lt;h2&gt;Keep it simple&lt;/h2&gt;
&lt;p&gt;Self-hosting only pays off if it stays easy to run. Pick tools you can update without a project.&lt;/p&gt;
</content:encoded><category>Self-hosting</category></item></channel></rss>