spiriitlabs/commit-history-bundle
SpiriitLabs Commit History Bundle adds a simple Symfony interface for browsing your project's Git commit history. View commits, authors, dates, and messages directly in your app, making release notes and change tracking easier without leaving the admin.
Read the label before you install. spiriitlabs/commit-history-bundle is a Symfony bundle, not a Laravel package. It ships a Bundle class, a Doctrine entity, a console command, and Twig templates. There is no Illuminate\Support\ServiceProvider, no php artisan vendor:publish, no Blade views, and Laravel's package discovery won't touch it — it looks for a Laravel provider, not a Symfony\Component\HttpKernel\Bundle\Bundle. composer require-ing it into a Laravel app gets you a dependency sitting in vendor/ with nothing wired up.
A Laravel developer has exactly three realistic paths:
git log format string and the entity schema are the only genuinely interesting parts — and reimplement as an Artisan command + Eloquent model + Blade view. Best when the changelog page is small and you want one stack. The package is MIT, so keep the license header if you lift code.git log, a 40-line Artisan command will get you there faster than either option above. This is a 2-star bundle; the value is the idea and the template, not the machinery.Sidecar setup, minimal:
composer create-project symfony/skeleton changelog-svc
cd changelog-svc && composer require spiriitlabs/commit-history-bundle
config/bundles.php.resource: '@CommitHistoryBundle/config/routes.php'.config/packages/commit_history.yaml — this is the only knob surface (repository path, provider, rendering options). Copy the key shape from the package README; keys moved between releases.bin/console commit-history:import.Port setup, minimal: clone the repo read-only so you can see src/ — the git log format string, the entity fields, and the Provider interface. Write php artisan changelog:import, a commits migration keyed on hash, and a Blade partial. Don't copy the Symfony wiring; copy the shape.
Where to look first: config/packages/commit_history.yaml (Symfony) or the bundle's Provider interface (either path — it's the seam you'll actually extend). Then bin/console list commit-history for the real command names in your installed version, not the README's.
First use case: a public /changelog page populated from git log so you stop hand-maintaining CHANGELOG.md and stop forgetting to announce releases. Second, the one that usually justifies the dependency: an in-app "What's new" panel with unread tracking for logged-in users.
Import on deploy, never on request. Hook the import command into the deploy pipeline after git pull --tags so the DB always matches the deployed tag. Running it per-request is the classic mistake — it shells out to git and will dominate TTFB.
git pull --tags
bin/console commit-history:import # sidecar
# or: php artisan changelog:import # ported version
bin/console cache:clear
Idempotency is the whole game. Key off the commit hash so re-running the importer is a no-op for existing rows. If you can't confirm the bundle upserts by hash in your installed version, wrap the command in your own and dedupe before flush — or in the ported version, upsert() on hash.
Scheduled refresh for continuously-deployed environments. If deploys are infrequent but the changelog matters (SaaS with per-merge deploys), register the import as a cron or a Laravel Schedule::command('changelog:import')->everyFifteenMinutes()->withoutOverlapping(). Tag the job so it can't stampede across workers.
Tag/version grouping is the presentation layer you'll spend real time on. Commits are a flat stream; users want "v2.3.0 — 12 March". Map git tags to display versions either at import time (annotate the row) or at render time (join against a version table you own). Render-time keeps the imported data honest and lets you re-group without re-importing.
Custom provider for non-git sources. The Provider interface is the intended extension point for GitHub/GitLab APIs, Jira, or an internal release service. Implement it, register it as a service, and point config at your service ID. In a ported Laravel version this is a CommitProvider contract with a GitLogProvider and a GitHubProvider binding — same idea, and it's how you avoid needing a full git checkout on the web host, which in containerised deploys you often don't have.
Render two ways from one dataset:
templates/bundles/CommitHistoryBundle/ rather than editing vendor/. In Blade, use resources/views/vendor/commit-history/ with a published namespace if you've ported it.Cache-Control and an ETag keyed on the newest commit hash.Laravel sidecar consumer: a thin CommitClient wrapping Http::baseUrl(config('services.changelog.url')), cached for an hour and busted by the newest-hash ETag. That's the entire day-to-day integration surface on the Laravel side.
class CommitClient
{
public function latest(int $limit = 20): Collection
{
return Cache::remember("changelog:latest:{$limit}", now()->addHour(), fn () =>
Http::baseUrl(config('services.changelog.url'))
->get('/api/commits', ['limit' => $limit])
->throw()
->collect()
);
}
}
Cache aggressively. The changelog changes at most a few times a day. Cache the rendered page and the JSON on the newest-hash key; invalidate from a post-import hook or a webhook from the sidecar.
Multi-repo / monorepo. One config entry per repository, each with its own provider instance and a namespace column on the entity. Don't try to make one importer span two repos — the git log argument surface gets ugly fast.
V1.1.0 ships with an empty changelog. The release notes are a bare compare link (V1.0.1...V1.1.0) with no enumerated changes. Treat it as unknown-risk: before bumping, composer update spiriitlabs/commit-history-bundle --dry-run, then diff vendor/spiriitlabs/commit-history-bundle between the two tags and check specifically for (a) new/renamed config keys, (b) entity schema changes, (c) renamed console commands, (d) new Symfony/PHP constraints in composer.json. A minor bump in a bundle that owns a DB table is a migration event until proven otherwise.
The git binary and the checkout are real runtime dependencies. proc_open on git is blocked in many hardened PHP images (and in most Laravel Forge/Vapor/serverless setups), and shallow clones (--depth=1) silently produce a one-commit history — which looks like "the importer is broken" rather than "your clone is shallow". Unshallow, or switch to an API-based provider. This is the single most common way the sidecar approach fails in CI-built containers.
Commit messages are attacker-controlled if you accept outside PRs. If anything in the chain renders message bodies as Markdown → HTML, sanitise before output. A changelog page is a public page; a merged PR with <img onerror> in the commit body is stored XSS. In Blade, run it through your usual sanitiser; don't {!! !!} the message.
Timezone drift. Git records author dates with offsets; the DB column may be naive. Import a UTC timestamp and format at render, or your "released 3 hours ago" badges will lie.
Don't let Doctrine --force your schema. The entity is vendor code. If you regenerate migrations after an upgrade, review the diff; auto-generated ALTER TABLE on a changelog table with real rows is how you lose the table. Same caution applies if you've ported the schema — write the migration once, then treat it as yours.
Route and access control are yours. The bundle registers a route; whether it's public, behind ROLE_ADMIN, or absent in production is a decision it won't make for you. In the sidecar, put it behind a shared secret or internal network; in a ported Laravel version, a policy + ->middleware('auth'). Check bin/console debug:router | grep -i commit (Symfony) or php artisan route:list | grep changelog (Laravel) after install and after every upgrade.
Overriding templates beats forking. Template resolution is by bundle path; put overrides in templates/bundles/CommitHistoryBundle/. Anything you patch in vendor/ disappears on the next composer update — and given how terse this release's notes are, you will be updating.
Debugging checklist when nothing renders: bin/console debug:container | grep -i commit (is the service registered?), then debug:config commit_history (is the repo path actually set?), then whether the table has rows, then whether the provider's repo path resolves from the web user's home directory — a path that works for your shell usually doesn't work for www-data. For the Laravel sidecar consumer, add ->throw() on the HTTP call and log the response body; silent null returns from a cached client are usually a stale sidecar build.
Pin and vendor deliberately. Two stars and a single-maintainer release cadence mean you should pin the version in composer.json and, if you port the parser, copy it into app/Support/Changelog/ with the MIT header intact rather than depending on the repo being maintained indefinitely. The idea is durable; the package is optional.
How can I help you explore Laravel packages today?