git

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package git wraps the git plumbing commands we need to build the optional initial commit on a freshly-init'd bare repo. Plumbing-only, no working tree: hash-object → update-index (with GIT_INDEX_FILE) → write-tree → commit-tree → update-ref. This is what `git` itself does internally; we drive it from outside via short-lived subprocesses so we don't need to vendor a Go-native git implementation.

Every shell-out is constrained to a caller-supplied gitDir we control (`storage.RepoFS.RepoPath` produces it from a strict path whitelist). `git -C` is used instead of changing the process working directory.

Index

Constants

This section is empty.

Variables

View Source
var ErrBlameOnBinary = errors.New("git: blame target is binary")

ErrBlameOnBinary is returned when the requested path is a binary blob. Blame on binary is meaningless; refuse early.

View Source
var ErrBlameTooLarge = errors.New("git: blame target too large")

ErrBlameTooLarge is returned when the file exceeds the cap. The handler renders the "blame too expensive" placeholder.

View Source
var ErrBlobTooLarge = errors.New("git: blob exceeds size cap")

ErrBlobTooLarge is returned when a maxBytes cap is hit on ReadBlobBytes.

View Source
var ErrCommitNotFound = errors.New("git: commit not found")

ErrCommitNotFound is returned by GetCommit when the SHA doesn't resolve to a commit on this repo.

View Source
var ErrNotABlob = errors.New("git: not a blob")

ErrNotABlob is the inverse — used by ReadBlob.

View Source
var ErrNotATree = errors.New("git: not a tree")

ErrNotATree is returned by LsTree when the requested path turns out to be a blob (so the caller should fall through to BlobInfo).

View Source
var ErrPathNotFound = errors.New("git: path not found")

ErrPathNotFound is for paths that don't exist on the ref.

View Source
var ErrRefNotFound = errors.New("git: ref not found")

ErrRefNotFound is returned when git can't resolve a ref or commit. Distinguished from generic exec failures so handlers can render a 404-leaning response.

View Source
var ErrRefRaced = errors.New("repogit: ref moved concurrently")

ErrRefRaced is the typed sentinel UpdateRefCAS returns when the ref moved between our read and our update.

Functions

func AheadBehind

func AheadBehind(ctx context.Context, gitDir, base, head string) (ahead, behind int, err error)

AheadBehind returns the number of commits unique to head (ahead) and unique to base (behind), computed via `git rev-list --left-right --count base...head`. Output shape is "<behind>\t<ahead>" — the left side is base, right is head.

When base or head doesn't exist on the repo we surface the typed ErrRefNotFound so callers can render "—" instead of a number.

func AheadBehindCacheStats

func AheadBehindCacheStats() lru.Stats

AheadBehindCacheStats exposes hit/miss counters for the /metrics surface and bench reports.

func CountCommits

func CountCommits(ctx context.Context, gitDir, ref string) (int, error)

CountCommits returns the number of commits reachable from ref.

func FetchIntoNamespace

func FetchIntoNamespace(ctx context.Context, dstRepoDir, srcRepoDir, srcRef, dstRef string) error

FetchIntoNamespace fetches a single ref from `srcRepoDir` into `dstRepoDir` under the supplied refspec. The dst-side ref name is the second half of the refspec. Used by S27 cross-fork PR support to pull a fork's head branch into the base repo's `refs/shithub-pr/<pr_id>/head` namespace (private — never advertised via `info/refs`).

Idempotent at the git layer; calling repeatedly with the same refspec just updates the dst ref.

func HasAnyBranch

func HasAnyBranch(ctx context.Context, gitDir string) (bool, error)

HasAnyBranch reports whether the bare repo at gitDir has at least one ref under refs/heads/. Used by the repo home view to fork between the "quick setup" empty-state and the post-push view.

func InvalidateAheadBehind

func InvalidateAheadBehind(key AheadBehindKey)

InvalidateAheadBehind drops one specific (repo, base, head) entry. Use from push:process when the exact OIDs are known.

func InvalidateAheadBehindForRepo

func InvalidateAheadBehindForRepo(repoID int64)

InvalidateAheadBehindForRepo drops every cached entry whose key matches repoID. Called from push:process so a force-push that rewrites history doesn't surface stale ahead/behind counts.

The current implementation is approximate: the LRU exposes Delete per-key, not by-prefix scan. Push handlers that know the specific OIDs that moved should call InvalidateAheadBehind directly; this helper is kept as a documented future extension point so the API is stable when a richer scan lands.

func IsAncestor

func IsAncestor(ctx context.Context, gitDir, a, b string) (bool, error)

IsAncestor reports whether commit a is an ancestor of commit b. Used by the pre-receive force-push detector: a fast-forward is `IsAncestor(old, new)`.

func ListAllPaths

func ListAllPaths(ctx context.Context, gitDir, ref string) ([]string, error)

ListAllPaths runs `git ls-tree -r --name-only` and returns every blob path under the ref. Used by the "Go to file" finder. Filters out submodule-style entries (commit type) which shouldn't surface in the file finder.

func ReadBlobBytes

func ReadBlobBytes(ctx context.Context, gitDir, ref, path string, maxBytes int64) ([]byte, error)

ReadBlobBytes reads the entire blob at `<ref>:<path>`. Caller-imposed max-size limit is the right guard — git itself doesn't bound the stream. Pass 0 for "no cap"; otherwise an oversize read returns ErrBlobTooLarge.

func ResolveRef

func ResolveRef(refs []string, segments []string) (ref, path string, ok bool)

ResolveRef takes the URL segments after /tree/ or /blob/ and finds the longest-prefix match against the supplied refs. Hex-SHA shortcut: if the first segment is exactly 40 hex chars, treat it as a SHA. The returned `path` is the joined remainder after the matched ref (no leading slash).

Example: refs = ["main", "feature/x"], URL = ["feature", "x", "sub", "f.go"] → ref="feature/x", path="sub/f.go".

func ResolveRefOID

func ResolveRefOID(ctx context.Context, gitDir, ref string) (string, error)

ResolveRefOID returns the full SHA for `ref` via `git rev-parse`. Returns ErrRefNotFound when git can't resolve.

func SetSymbolicRef

func SetSymbolicRef(ctx context.Context, gitDir, ref, target string) error

SetSymbolicRef updates HEAD (or any other symbolic ref) atomically. Used by the default-branch change to point HEAD at the new branch.

func StreamBlob

func StreamBlob(ctx context.Context, gitDir, ref, path string, w io.Writer) error

StreamBlob writes the blob bytes to w. For raw downloads we never buffer; this lets the response stream as `git cat-file -p` produces.

func UpdateRefCAS

func UpdateRefCAS(ctx context.Context, gitDir, ref, newOID, oldOID string) error

UpdateRefCAS performs an atomic compare-and-swap on a ref: only succeeds if the ref currently points at oldOID. Returns ErrRefRaced when the ref moved underneath us (a concurrent push is the canonical case). Used by S27's sync-fork to fast-forward the fork's default branch only when nothing else has touched it.

The git update-ref `<oldvalue>` argument is what enforces the CAS; passing it gives git's exact-match semantics (no off-by-one races even when oldvalue happens to equal newvalue).

func WeeklyCommitActivity

func WeeklyCommitActivity(ctx context.Context, gitDir, ref string, bucketCount int, now time.Time) ([]int, error)

WeeklyCommitActivity counts commits by UTC week, oldest bucket first. It is intentionally small and read-only so list surfaces can render GitHub-style activity sparklines without parsing full commit rows.

Types

type AheadBehindKey

type AheadBehindKey struct {
	RepoID  int64
	BaseOID string
	HeadOID string
}

AheadBehindKey is the (repo_id, base_oid, head_oid) tuple. The OIDs make the cache safe across pushes — when either ref moves, the new OID forms a different key. Old keys age out via LRU eviction; explicit invalidation isn't strictly required but helps keep the working set small (push handlers call InvalidateRepo).

type AheadBehindResult

type AheadBehindResult struct {
	Ahead, Behind int
}

AheadBehindResult is the cached payload.

func AheadBehindCached

func AheadBehindCached(ctx context.Context, gitDir string, key AheadBehindKey) (AheadBehindResult, error)

AheadBehindCached is the cached + single-flighted variant of AheadBehind. Callers pass the resolved OIDs (not ref names) so the key is stable across ref-name renames and the cache is never poisoned by a stale ref pointer.

On cache miss the underlying `git rev-list` runs once even when many requests arrive concurrently for the same key (single-flight dogpile guard). The result is cached until LRU eviction or explicit InvalidateRepo.

type BlameChunk

type BlameChunk struct {
	OID         string
	ShortOID    string
	AuthorName  string
	AuthorEmail string
	AuthorWhen  time.Time
	StartLine   int // first FinalLineNo in the chunk
	EndLine     int
	Lines       []BlameLine
}

BlameChunk groups consecutive lines that share an originating commit. The handler renders each chunk with one header (commit/author/age) + N body rows.

func Blame

func Blame(ctx context.Context, gitDir string, opts BlameOptions) ([]BlameChunk, error)

Blame runs `git blame --line-porcelain` and returns chunks. The caller groups by chunk for rendering. We size-check the blob via StatPath first so a giant file fails fast without forking blame.

type BlameLine

type BlameLine struct {
	OID         string
	ShortOID    string
	AuthorName  string
	AuthorEmail string
	AuthorWhen  time.Time
	OrigLineNo  int    // line number in the originating commit
	FinalLineNo int    // line number in the final file (1-indexed)
	Content     string // the source line, no trailing newline
}

BlameLine is one source line + the commit that last touched it.

type BlameOptions

type BlameOptions struct {
	Ref  string
	Path string

	// MaxBytes / MaxLines are the safety caps. 0 → use defaults
	// (5 MiB / 50k lines per S18 spec).
	MaxBytes int64
	MaxLines int
}

BlameOptions configures Blame. Currently just the file ref + path, but the shape leaves room for "older" navigation in a follow-up.

type BlobInfo

type BlobInfo struct {
	OID  string
	Size int64
}

BlobInfo is the result of `git cat-file -e -p` style introspection.

type Commit

type Commit struct {
	OID         string // full 40-char SHA
	ShortOID    string // git's --abbrev result, typically 7 chars
	AuthorName  string
	AuthorEmail string
	AuthorWhen  time.Time
	Subject     string
	Body        string // empty for list view; populated by GetCommit
}

Commit is one commit row from the commits list. Only the cheap fields arrive via `git log --format=...`; the per-file stats live on CommitDetail.

func CommitsBetween

func CommitsBetween(ctx context.Context, gitDir, base, head string, max int) ([]Commit, error)

CommitsBetween returns the commits unique to head (the right side of the symmetric range). Used by the compare view's commits list.

func Log

func Log(ctx context.Context, gitDir string, o LogOptions) ([]Commit, error)

Log returns Commits for the requested page. The format string packs every field we render onto one line per commit, separated by ASCII unit-separators (\x1f), with body terminated by ASCII record-separator (\x1e) so newlines inside the body don't break parsing.

type CommitDetail

type CommitDetail struct {
	Commit
	CommitterName  string
	CommitterEmail string
	CommitterWhen  time.Time
	Parents        []string // each is a full OID
	TreeOID        string
	Files          []FileChange
}

CommitDetail holds the single-commit-view data: the Commit plus committer fields, parents, tree OID, and the per-file change list.

func CommitsBetweenDetail

func CommitsBetweenDetail(ctx context.Context, gitDir, baseOID, headOID string, max int) ([]CommitDetail, error)

CommitsBetweenDetail returns commits unique to head, with author + committer + body, suitable for refreshing pull_request_commits. The result preserves head-side oldest-first ordering by inverting log's default newest-first via --reverse.

func GetCommit

func GetCommit(ctx context.Context, gitDir, sha string) (CommitDetail, error)

GetCommit returns the full detail for one SHA. Two commands: one for the commit metadata + parents (cheap, single-line), one for the file stats. Combining them in one --pretty walk would save a fork but complicate parsing.

type FileChange

type FileChange struct {
	Status  string
	Path    string
	OldPath string // populated for R/C
	Insert  int
	Delete  int
	Binary  bool
}

FileChange is one row from `git diff-tree --numstat --no-commit-id -r`. Status is git's letter code: A added, M modified, D deleted, R renamed, C copied, T type-changed.

func DiffStat

func DiffStat(ctx context.Context, gitDir, sha string) ([]FileChange, error)

DiffStat returns the per-file change list for a SHA. We run two commands: --name-status for the letter and rename pairs, --numstat for +/- counts. Two passes is two forks, but the parsing stays simple; combining via --raw is harder to read.

type FileEntry

type FileEntry struct {
	Path string
	Body []byte
}

FileEntry is one file destined for the initial commit. Path is the repository-relative slash-separated path (no leading slash). Body is the raw bytes; we hash whatever we're given without normalization.

type HeadCommit

type HeadCommit struct {
	OID         string
	Subject     string
	AuthorName  string
	AuthorEmail string
	AuthorWhen  time.Time
}

HeadCommit is a read-only view of one commit. Returned by HeadOf for the repo home page; richer commit-info reads belong to S17.

func HeadOf

func HeadOf(ctx context.Context, gitDir, branch string) (HeadCommit, bool, error)

HeadOf returns the head commit on the named branch. ok=false (no error) when the ref doesn't exist — callers can branch on that without distinguishing missing-ref from any other failure.

type InitialCommit

type InitialCommit struct {
	GitDir      string // absolute bare-repo path (must already exist + be inited)
	AuthorName  string
	AuthorEmail string
	Message     string    // defaults to "Initial commit"
	Branch      string    // defaults to "trunk"
	When        time.Time // defaults to time.Now()
	Files       []FileEntry
}

InitialCommit describes the single commit produced for a freshly initialized repo whose owner ticked "initialize with README/license/ .gitignore" on the create form. Everything except Files defaults to sensible values; callers must always set GitDir + Author* + Files.

func (InitialCommit) Build

func (ic InitialCommit) Build(ctx context.Context) (string, error)

Build runs the full plumbing sequence and returns the new commit OID. On error the bare repo may have orphan blobs/trees written; the caller is expected to RemoveAll the bare repo dir on failure.

type LogOptions

type LogOptions struct {
	Ref      string
	MaxCount int
	Skip     int
	Path     string
	Author   string
	Since    time.Time
	Until    time.Time
	Follow   bool // --follow; only meaningful when Path is set
}

LogOptions tunes the commits-list query. Zero values are sensible:

  • MaxCount 0 → 30
  • Skip 0 → 0
  • Path "" → log over all paths
  • Author "" → no author filter
  • Since/Until zero → no date filter

type MergeOptions

type MergeOptions struct {
	GitDir         string
	BaseRef        string // e.g. "refs/heads/trunk"
	BaseOID        string
	HeadOID        string
	Method         string // "merge" | "squash" | "rebase"
	AuthorName     string
	AuthorEmail    string
	CommitterName  string
	CommitterEmail string
	When           time.Time
	Subject        string
	Body           string
	WorktreesDir   string // parent dir for the temp worktree (must share volume with GitDir)
}

MergeOptions configures a worktree-based merge.

type MergeResult

type MergeResult struct {
	NewBaseOID string // the new tip of base_ref after the merge
	MergedOID  string // for "merge" method: the merge commit; "squash"/"rebase": same as NewBaseOID
}

MergeResult is what PerformMerge returns on success.

func PerformMerge

func PerformMerge(ctx context.Context, opts MergeOptions) (MergeResult, error)

PerformMerge executes the requested merge strategy in a temp worktree rooted at WorktreesDir. The worktree is removed on every exit path (success or failure). Returns the new base-ref tip.

type MergeTreeResult

type MergeTreeResult struct {
	TreeOID       string
	ConflictPaths []string
	HasConflict   bool
}

MergeTreeResult captures the output of `git merge-tree --write-tree --merge-base=<base> <base> <head>`. ConflictPaths is empty when the merge is clean. Git ≥ 2.38 required.

func ProbeMerge

func ProbeMerge(ctx context.Context, gitDir, baseOID, headOID string) (MergeTreeResult, error)

ProbeMerge runs `git merge-tree --write-tree --no-messages <base> <head>` and lets git auto-compute the merge base. Exit 0 = clean merge (TreeOID set); exit 1 = conflicts (ConflictPaths populated). Anything else is wrapped.

type PRFileChange

type PRFileChange struct {
	Path      string
	OldPath   string
	Status    string // "added" | "modified" | "deleted" | "renamed" | "copied"
	Additions int
	Deletions int
}

PRFileChange is a per-file row for pull_request_files. Status mirrors the migration enum; OldPath is non-empty for renames + copies.

func FilesChangedBetween

func FilesChangedBetween(ctx context.Context, gitDir, baseOID, headOID string) ([]PRFileChange, error)

FilesChangedBetween returns the change set head-side, computed as `git diff --name-status --numstat <base>...<head>` (three-dot: changes from merge-base to head). Status is git's letter code, renames carry the old path as the second column.

type RefEntry

type RefEntry struct {
	Name string // short name (without refs/heads/ or refs/tags/)
	OID  string // 40-char hex sha
}

RefEntry is one ref from for-each-ref.

type RefListing

type RefListing struct {
	Branches []RefEntry // refs/heads/<name>
	Tags     []RefEntry // refs/tags/<name>
}

ListRefs runs `git for-each-ref` and returns every ref under refs/heads/ and refs/tags/. Used by the ref-resolver to do longest-prefix matching against URLs like /tree/feature/x/sub/dir.

Returns separate lists so callers can fork on UX (branches vs tags).

func ListRefs

func ListRefs(ctx context.Context, gitDir string) (RefListing, error)

ListRefs enumerates branches and tags. Empty repos return empty slices, not an error.

type TreeBlob

type TreeBlob struct {
	Path string
	Size int64
}

TreeBlob is one blob returned by a recursive tree walk.

func ListBlobs

func ListBlobs(ctx context.Context, gitDir, ref string) ([]TreeBlob, error)

ListBlobs returns every blob under ref with its git-reported byte size. It is the recursive counterpart to LsTree and intentionally keeps only blobs because callers use it for file-level summaries such as language bars.

type TreeEntry

type TreeEntry struct {
	Kind TreeEntryKind
	Mode string // 100644, 100755, 040000, 160000, 120000
	OID  string
	Size int64  // -1 when N/A (trees, submodules)
	Name string // basename relative to the listed path
}

TreeEntry is one row from `git ls-tree --long --full-tree`.

func LsTree

func LsTree(ctx context.Context, gitDir, ref, path string) ([]TreeEntry, error)

LsTree lists entries at <ref>:<path>. Empty path lists the repo root. Returns an empty slice when the path doesn't exist or is itself a blob — callers should fall back to BlobInfo.

type TreeEntryKind

type TreeEntryKind string

TreeEntryKind is one of the four shapes git tree entries take.

const (
	EntryTree    TreeEntryKind = "tree"
	EntryBlob    TreeEntryKind = "blob"
	EntrySubmod  TreeEntryKind = "commit" // a "commit" entry in a tree is a submodule pointer
	EntrySymlink TreeEntryKind = "symlink"
)

func StatPath

func StatPath(ctx context.Context, gitDir, ref, path string) (kind TreeEntryKind, oid string, size int64, err error)

StatPath returns the kind + OID + size for `<ref>:<path>`. Used by the handler to decide whether to render tree or blob without a second round-trip.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL