git / git-bundle
GitHub-like git repository viewer for Symfony, powered by libgit2 via the php-git2 extension
Requires
- php: ^8.1
- ext-git2: *
- symfony/config: ^6.0|^7.0|^8.0
- symfony/dependency-injection: ^6.0|^7.0|^8.0
- symfony/framework-bundle: ^6.0|^7.0|^8.0
- symfony/http-kernel: ^6.0|^7.0|^8.0
- symfony/routing: ^6.0|^7.0|^8.0
- symfony/twig-bundle: ^6.0|^7.0|^8.0
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-23 01:37:32 UTC
README
A GitHub-like, read-only git repository viewer embedded in your Symfony application — browse trees, blobs, commit history, diffs, branches and tags of any local repository from your own site, behind your own security layer.
Philosophy
- In-process, powered by libgit2. All repository reads go through
libgit2 via the php-git2
PHP extension. Requests never shell out to the
gitbinary and never talk to an external service (no gitweb/cgit/GitLab needed): a repository is treated as a plain data source, opened and object-read directly by PHP. - Read-only by construction. The bundle exposes lookups only (trees, blobs, commits, refs). Nothing writes to the repositories.
- Safe by object addressing. Paths in URLs are resolved through git tree objects, never through the filesystem — path traversal outside the repository is impossible by design. Every route is additionally gated by a configurable role.
- Repositories are declarative. You list repositories in config; anything
declared with a
urlis cloned (a bare mirror) when itspathdoes not exist yet, and fetched (--all --prune) otherwise, bybin/console git:sync.cache:warmupnever talks to a remote: a warmup builds from what is on disk and must not need the network. Deploying a new viewer is: add 4 lines of YAML, rungit:sync.
Try it in one command
A self-contained demo (bare Symfony skeleton + this bundle, browsing the libgit2 repository itself) ships in example/Dockerfile:
docker build -t git-bundle-demo -f example/Dockerfile . docker run --rm -p 8000:8000 git-bundle-demo # → http://localhost:8000/git (libgit2 is cloned at startup by git:sync)
Requirements
-
PHP ≥ 8.1
-
The php-git2 extension (PHP bindings for the libgit2 C library). It is not bundled with PHP; install libgit2 then build the extension:
apt install libgit2-dev # or brew install libgit2 git clone https://github.com/RogerGee/php-git2 && cd php-git2 phpize && ./configure --with-libgit2=/usr && make && make install docker-php-ext-enable git2 # or add "extension=git2.so" to php.ini
Installation
composer require git/git-bundle:^1.0
Enable it (Flex usually does this) in config/bundles.php:
Git\GitBundle::class => ['all' => true],
Configuration
config/packages/git.yaml:
git: route_prefix: /git # URL prefix for all viewer routes access_role: ROLE_ADMIN # role required to browse (PUBLIC_ACCESS to open up) repositories: hellogitworld: url: 'https://github.com/githubtraining/hellogitworld' # cloned by git:sync path: '%kernel.project_dir%/var/repos/hellogitworld.git' label: 'Hello Git World' description: 'Demo repository' default_branch: master app: path: '%kernel.project_dir%' # any existing local repo works too label: 'This application' default_branch: HEAD
Routing: on Symfony ≥ 7.3 the controller's attribute routes are registered
automatically (the URL prefix comes from git.route_prefix via a class-level
#[Route]). On older versions import them once in config/routes/git.yaml:
git: resource: '@GitBundle/Resources/config/routes.php'
Do not add an import prefix: — the routes are already prefixed.
Routes
| URL | View |
|---|---|
/git |
repository index |
/git/{repo} |
redirect to the default branch tree |
/git/{repo}/tree/{ref}/{path} |
directory listing |
/git/{repo}/blob/{ref}/{path} |
file contents |
/git/{repo}/log/{ref} |
commit history (paginated) |
/git/{repo}/commit/{sha} |
commit details + diff |
/git/{repo}/branches, /git/{repo}/tags |
refs |
/git/{repo}/graph.json |
the commit graph, as JSON (see below) |
The commit graph
/git/{repo}/graph.json (route git_graph) answers the history of every
branch at once, for a page that draws it: the commits reachable from any
branch, tag or remote branch, children before their parents, each with its
parents and the references pointing at it.
{
"repository": "app",
"default_branch": "main",
"commits": [
{
"sha": "5d3c…", "short": "5d3c1f0a", "subject": "Merge feature",
"author": {"name": "Ada", "email": "ada@example.org"}, "date": "2026-10-05T14:02:11+00:00",
"parents": ["91ab…", "c07e…"],
"refs": [{"type": "branch", "name": "main", "head": true}, {"type": "tag", "name": "v1.2.0"}]
}
],
"next": null
}
?limit= (200 by default, 500 at most) and ?offset= page through it; next
is the offset of the following page, null on the last. A reference's type
is branch, remote, tag or head (a detached HEAD); the checked-out
branch carries head: true. The same access rules apply as to the pages.
In PHP: Git2Service::getCommitGraph($repo, $limit, $offset) returns the
CommitInfos, their refs filled; getReferencesByCommit($repo) the
references alone, by commit.
Repositories from your application
Beyond git.repositories, any service implementing
Git\Repository\RepositoryProviderInterface adds repositories. The interface
is autoconfigured with the git.repository_provider tag. A typical provider
returns the repositories of an application's own entities: its clients'
projects, the software it publishes. Each entry has the same shape as a
configured one:
final class ProjectRepositories implements RepositoryProviderInterface { public function getRepositories(): array { return ['acme-shop' => ['path' => '/srv/repos/acme-shop.git', 'url' => 'git@…', 'label' => 'Acme shop']]; } }
When a name exists in both, the configured repository wins. Providers are queried lazily, once per request.
bin/console git:sync [name…] clones or fetches the repositories that have a
url - all of them, or those named. It is the only command of the bundle that
talks to a remote: run it at a deploy, and from a cron line to keep the mirrors
fresh and to clone the repositories a provider adds.
One repository at a time
access_role lets a user into the viewer. Setting repository_attribute
adds a check on each repository, isGranted(<attribute>, <repository name>),
so a voter can grant each user their own repositories:
git: access_role: ROLE_USER repository_attribute: GIT_VIEW
The repository index lists only the granted ones.
Service API
Git\Service\Git2Service is public and autowirable:
getCommitLog(),getCommit(),getTree(),getBlob(),getBranches(),getTags(): the viewer's readsgetTag($repo, $name): the commit a tag points at, plus the message and date of an annotated tagwalkTree($repo, $ref, $path = ''): every file of a tree, as a generatorpath => [content, filemode], read from the object database. Good for building an archive of a release.
An unknown ref or tag is a NotFoundHttpException (404).
Tests
vendor/bin/phpunit # needs php-git2 and the git binary (fixtures are built with it)
Notes
- Cloning and fetching happen in
git:sync(Git\Repository\RepositorySync), never incache:warmup: until 2026-10 a cache warmer (RepositoryWarmer) did it, so every warmup needed the network and fetched every remote. The class remains as a deprecated name ofRepositorySync, no longer a warmer. A large remote takes a while to mirror: prefer bare mirrors on fast storage. - Licensed MIT (
LICENSE) since 2026-10-09; earlier versions remain published under LGPL-3.0-or-later.