Git & repositories
Storage layout, protocols, server-side hook enforcement, browsing, templates, protection rules, LFS, mirrors, and scanning.
Storage layout#
Every repository is a bare Git repository on disk at <git_data volume>/<repositoryId>.git — addressed internally by UUID, not by its (mutable) org/project/repo slug path, so renaming any ancestor never touches the repository on disk or breaks an existing clone that re-resolves the remote by following a redirect from the old slug.
Protocols#
- Smart HTTP:
apps/apiimplementsgit-upload-pack/git-receive-packover HTTP directly (GET .../info/refs?service=...,POST .../git-upload-pack,POST .../git-receive-pack), authenticated via HTTP Basic auth using a personal access token as the password. - SSH:
services/ssh-gitterminates the SSH transport, authenticates the connecting public key againstSshKeyrows, and proxies the samegit-upload-pack/git-receive-packinvocations against the identical bare repository — both protocols are two doors into the same storage, not two separate implementations that could drift.
Server-side enforcement via Git hooks#
Every bare repository is provisioned with a pre-receive and post-receive hook that call back into the API over an internal-only HTTP endpoint (authenticated with a shared secret, KHUB_INTERNAL_SECRET — never reachable from outside the container network):
pre-receiveruns before any ref is updated and can reject the entire push. This is where branch protection (required pull request, disallowed force-push, restricted push roles), tag protection, and any configured custom pre-receive hooks are enforced — enforcement happens here, not in the UI, so it applies equally to a push from git on the command line, from the SSH transport, or from an internal call like a template's initial commit.post-receiveruns after a successful update and is what flips a repository fromisEmptyto populated on its first push, triggers webhookPUSHevents, kicks off matching CI pipelines, and records therepository.pushedaudit log entry.
A small number of privileged, server-initiated writes (merging a pull request, applying a repository template, committing a generated README/license/.gitignore at creation) explicitly bypass the pre-receive protection check with an internal flag — they still go through the identical commit/push machinery a real git push would, just with branch-protection re-implemented at the call site to enforce the equivalent rule before invoking it, so there is exactly one code path that actually writes to a ref.
Browsing, blame, and diff#
The Code tab shells out to git directly for every read operation — ls-tree, log, blame, diff, grep, archive — against the same bare repository, so what you see browsing is guaranteed to match what a clone produces (there's no separate indexed copy of file content that can fall out of sync, other than the code search index).
License and .gitignore templates#
Creating a repository with "initialize with a README" checked can also seed a license and a .gitignore in the same first commit — both pulled from GET /api/repository-templates, a public, unauthenticated endpoint (the same list a logged-out visitor to a public fork-and-clone flow would need).
- Licenses — 18 SPDX-listed texts covering the common permissive, copyleft, and public-domain families: MIT, Apache-2.0, BSD (2- and 3-clause), ISC, 0BSD, the Unlicense, MPL-2.0, the full GNU family (GPL-2.0, GPL-3.0, LGPL-2.1, LGPL-3.0, AGPL-3.0), CC0-1.0, the Boost Software License, the Artistic License 2.0, CDDL-1.0, and EPL-2.0. Short, copyright-line-style licenses are templated with
{{year}}/{{fullname}}substituted at creation time; the long-form copyleft licenses are committed as their exact, complete canonical text. - .gitignore templates — 22 templates spanning general-purpose languages and runtimes (Node, Python, Rust, Go, Java, C/C++, Ruby, PHP, Swift, Kotlin/Android, Dart/Flutter, C#/.NET, Elixir), infrastructure (Terraform, Docker), game development (Unity), and editors/OSes (Visual Studio Code, JetBrains, Vim, macOS, Windows, Linux).
Both lists are plain data — adding another license or ignore template is a one-line addition, not a schema change, so the set can keep growing without a migration.
Branch protection#
Configured per branch-name pattern (glob), with independently toggleable rules:
- Require a pull request to change the branch (no direct push, including by maintainers, unless
restrictPush's allowed roles are set otherwise). - Require a minimum number of approvals.
- Require CI to pass (specific named status checks, or "the whole pipeline").
- Require CODEOWNERS review before merge is enabled — see Pull requests & code review.
- Require signed commits, and separately, require signed tags (tag protection).
- Restrict who can push directly and who can force-push, each to a minimum role.
Tag protection#
Independent of branch protection: restrict which roles can create tags matching a pattern (e.g. v*), and optionally require the tag object itself to be GPG/SSH-signed.
Large files (Git LFS)#
Implements the Git LFS HTTP batch API (/info/lfs/objects/batch) for upload/download, storing objects content-addressed by OID in the lfs_data volume, so LFS pointers resolve without needing a third-party LFS host.
Mirrors#
A repository mirror periodically (or on a manual trigger) fetches from — or pushes to — a configured external Git remote using stored, encrypted credentials. Errors are sanitized before being surfaced in the UI or logs specifically to strip any embedded credentials from the remote URL (a mirror URL of the form https://user:token@host/repo.git never has its token echoed back anywhere).
Security scanning#
- Secret scanning runs pattern-based detection over pushed content for common credential/API-key shapes and flags matches without blocking the push by default.
- Dependency scanning parses manifest files (
package.json, etc.) and checks them against a known-vulnerability dataset.
Both surface findings under Settings → Security and can be wired into branch protection as a required status check.
