Git & repositories
Storage layout, protocols, server-side hook enforcement, browsing, templates, protection rules, LFS, importing, 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.
Importing from GitHub, GitLab, or Forgejo/Gitea#
GithubImportService (the name predates multi-provider support and stuck, to avoid a disruptive table rename) talks to whichever host started the job through one shared GitHostClient interface — createGitHostClient(provider, host?) returns a GitHub, GitLab, or Forgejo/Gitea implementation, and the import logic itself (resolve destination, create the repo, fetch into it) never branches on which one it's talking to. Each implementation is responsible for one correctness guarantee the hard way: when asked for "this login's own repos," return only repos actually owned by that login, never ones the caller merely has collaborator/member access to (GitHub's own /users/{u}/repos?type=all was found, via live testing, to include exactly that — hundreds of unrelated repos for a long-lived account). GithubImportService re-filters client-side as a second line of defense regardless of which host answered.
Three entry points map to three GithubImportKind values (SINGLE_REPO, OWNER_PROFILE, MIGRATE_PROFILE), stored on the job row and used to filter each import page's history to only the jobs it started — so a profile clone never shows up in the migrate page's list. ensureClonedOrg/ensureProject resolve (and reuse, never re-create) the shared Cloned organization and a per-owner project under it; the first time a given owner's project is created, it's seeded with that owner's avatar/bio/website via client.getOwnerProfile() — best-effort and non-fatal, since a profile-metadata fetch failing shouldn't take down the actual repo import. Migrating your own account applies the same profile fetch to your KHUB user record instead, filling in only fields you haven't already set by hand.
Every provider-returned URL (repo homepage, avatar, profile website) is validated as a plain http(s) URL before being persisted — a defense against a self-hosted instance under someone else's control returning something like a javascript: URI in a field the frontend renders as an <img src> or link.
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.
