git

package
v1.17.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package git wraps the git plumbing commands `--since` needs (rev-parse, merge-base, cat-file, ls-tree) via the system git binary, never porcelain commands (git diff, git show, git log) — porcelain output is for human consumption and isn't a stable, documented contract across git versions.

Index

Constants

View Source
const AllZerosSHA = "0000000000000000000000000000000000000000"

AllZerosSHA is git's sentinel for "this ref did not exist" — the value GitHub gives github.event.before on a branch's first push, and the value git's own pre-receive/post-receive hooks use for a created/deleted ref.

Variables

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

ErrRefNotFound is returned by resolveCommit when git could not resolve a ref to a commit — a nonexistent ref, a too-shallow history, or any other plain git-resolution failure. It is not returned for infrastructure failures (git binary missing, context canceled), which propagate as-is.

Functions

func IsNotAGitRepository

func IsNotAGitRepository(err error) bool

IsNotAGitRepository reports whether err (as returned by Root, or any other Repo method) is git's own "fatal: not a git repository (or any of the parent directories): .git" failure -- the common case of running --since against a directory that was never a git repository at all, as opposed to some other infrastructure failure. Matched by substring against git's own stable, documented wording, since git's rev-parse exit code (128) is a generic "fatal error" shared by many unrelated failures and can't distinguish this case on its own.

Types

type AlertDeletion

type AlertDeletion struct {
	// CRDIdentifier is the surviving CRD's own identifier, informational
	// only — dispatch resolves the check rule to delete by name (see
	// PrometheusAlertName.CheckRuleName), not by this identifier.
	CRDIdentifier string
	asset.PrometheusAlertName
}

AlertDeletion is a single PrometheusRule alerting rule that disappeared from a CRD that otherwise still exists (its own CRD-level identifier is present in both snapshots). Detected by (group, alert) name, since the CRD's shared identifier cannot distinguish between the alerts it contains.

type Deletion

type Deletion struct {
	Kind       string
	Identifier string
	// Path is the path the asset was found at in the "before" snapshot,
	// kept for diagnostic/logging purposes only — deletion is dispatched by
	// (Kind, Identifier), never by path.
	Path string
	// SpamFilterUsesOrigin records whether the deleted spam filter carried a
	// dash0.com/origin label, when Kind is "spamfilter" (meaningless for
	// every other kind). false means the filter was identified by
	// dash0.com/id alone — its live id may have been reassigned server-side
	// since this identifier was recorded, so the delete dispatch warns
	// rather than deleting silently.
	SpamFilterUsesOrigin bool
	// PrometheusAlerts carries every alerting rule the CRD had at the
	// "before" snapshot, when Kind is "prometheusrule" (nil for every other
	// kind, and for a CRD with zero or one alert). A CRD with two or more
	// alerts has each alert's real check rule living at its own derived id
	// (asset.DeriveAlertCheckRuleID), not at the CRD's literal Identifier --
	// the delete dispatch needs this list to compute and delete each of
	// those derived ids individually, since Identifier alone only ever
	// named a check rule for a single-alert CRD.
	PrometheusAlerts []asset.PrometheusAlertName
}

Deletion is one asset --since determined must be deleted: its identifier was present in the "before" Snapshot (git, at <ref>) and is absent from the "after" Snapshot (current disk contents).

type DeletionPlan

type DeletionPlan struct {
	ByIdentifier []Deletion
	AlertsByName []AlertDeletion
	NoIdentifier []string
}

DeletionPlan is the result of diffing two Snapshots: everything --since determined must be deleted, plus the set of deleted documents that had no stable identifier at all (which must fail the whole run rather than be silently skipped or silently applied).

func Diff

func Diff(before, after Snapshot) DeletionPlan

Diff compares before (the Snapshot at <ref>) against after (the Snapshot of current disk contents) and returns everything that must be deleted. This is a pure two-point comparison — an asset created and deleted again between <ref> and now is invisible to it, by design (see design.md).

func (DeletionPlan) IsEmpty

func (p DeletionPlan) IsEmpty() bool

IsEmpty reports whether the plan calls for no deletions and has no no-identifier failures to surface.

type IdentifierKey

type IdentifierKey struct {
	Kind       string
	Identifier string
}

IdentifierKey uniquely identifies one asset across a Snapshot — kind plus its upsert identifier (id or origin, depending on the kind; see asset.ExtractIdentifier). Keying by kind as well as identifier means two different asset kinds can never collide even if their identifier strings happen to match.

type NoIdentifierDoc

type NoIdentifierDoc struct {
	Kind     string
	FilePath string
}

NoIdentifierDoc records a document that carries no stable identifier: its kind, and the file path it came from (without any multi-document suffix).

type RefState

type RefState string

RefState classifies a `--since` ref before any deletion detection runs. It is a named string type, rather than an int paired with a separate Stringer, so each constant's declaration doubles as its own printable representation (in log lines, test failure output, etc.) — there is no parallel name-mapping switch that could drift out of sync.

const (
	// RefEmpty means the ref was the empty string — --since was not passed,
	// or was passed as "".
	RefEmpty RefState = "RefEmpty"
	// RefAllZeros means the ref was git's all-zeros sentinel (AllZerosSHA) —
	// e.g. GitHub's github.event.before on a branch's first push. There is
	// no "before" state to compare against.
	RefAllZeros RefState = "RefAllZeros"
	// RefResolvedAncestor means the ref resolved to a real commit that is an
	// ancestor of HEAD — the ordinary, expected case.
	RefResolvedAncestor RefState = "RefResolvedAncestor"
	// RefResolvedNonAncestor means the ref resolved to a real commit, but
	// that commit is not an ancestor of HEAD (e.g. after a force-push or
	// history rewrite). Callers must not silently treat this like the
	// ancestor case.
	RefResolvedNonAncestor RefState = "RefResolvedNonAncestor"
	// RefUnresolvable means git could not resolve the ref to a commit at all
	// (typo, too-shallow clone, ref genuinely doesn't exist).
	RefUnresolvable RefState = "RefUnresolvable"
)

type Repo

type Repo struct {
	Dir string
}

Repo is a lightweight handle to a git working tree, used to run plumbing commands against it via the system git binary. Dir may be the working tree's root or any directory inside it — git -C resolves it either way.

func (Repo) ClassifyRef

func (r Repo) ClassifyRef(ctx context.Context, ref string) (state RefState, resolvedSHA string, err error)

ClassifyRef resolves ref against repo and classifies it into a RefState. resolvedSHA is populated only for RefResolvedAncestor and RefResolvedNonAncestor; it is the commit --since's two-point diff should read the "before" state from.

err is reserved for infrastructure failures (git binary missing, context canceled, HEAD itself unresolvable) — a ref that simply doesn't resolve is not an error, it's the RefUnresolvable state.

func (Repo) ExplainUnresolvableRef

func (r Repo) ExplainUnresolvableRef(ctx context.Context, ref string) string

ExplainUnresolvableRef returns a best-effort, more specific reason why ref (already known to be RefUnresolvable) failed to resolve, or "" if it can't tell. Today it recognizes exactly one shape: a simple "<base>~N" or "<base>^N" expression (or the bare "~"/"^" shorthand for N=1) whose base resolves fine, but whose own history has fewer than N+1 commits -- e.g. --since HEAD~1 in a repository with only one commit. That's a materially different situation from a typo'd ref or a too-shallow clone (the two reasons the generic RefUnresolvable message suggests), and it's often the very first wall someone hits setting up a fresh repo to try --since against. Deliberately narrow: no attempt is made to parse the rest of git's revision syntax (^{...}, @{...}, a mix of ~ and ^, etc.) -- those fall through to the generic message unchanged.

func (Repo) HasSkipWorktreeFiles

func (r Repo) HasSkipWorktreeFiles(ctx context.Context, scope string) (bool, error)

HasSkipWorktreeFiles reports whether any tracked file under scope carries git's skip-worktree bit, which sparse-checkout sets on everything outside the cone. Such a file is in the commit but not on disk, so the ref side of a --since diff sees it and the disk side does not, making it look deleted.

func (Repo) IsAncestor

func (r Repo) IsAncestor(ctx context.Context, ancestor, descendant string) (bool, error)

IsAncestor runs `git merge-base --is-ancestor <ancestor> <descendant>`, reporting whether ancestor is reachable from descendant. Per git's own documented convention for --is-ancestor, exit 0 means true and exit 1 means false; any other outcome is a genuine error (e.g. one of the refs doesn't exist).

func (Repo) IsTreeAtRef

func (r Repo) IsTreeAtRef(ctx context.Context, ref, path string) (bool, error)

IsTreeAtRef runs `git cat-file -t <ref>:<path>` and reports whether path was a directory (a "tree" object) at ref, as opposed to a file (a "blob"). path == "" means the repository root itself, which is always a tree.

Used to recover whether a --since target that no longer exists on disk at all was a single file or a directory the last time it did exist, so dry-run rendering can group its output the same way it would have while the target still existed, instead of defaulting to one shape regardless.

func (Repo) ListYAMLFilesAtRef

func (r Repo) ListYAMLFilesAtRef(ctx context.Context, ref, scope string) ([]string, error)

ListYAMLFilesAtRef runs `git ls-tree -r -z --name-only <ref> [-- <scope>]`, returning every .yaml/.yml file at that ref within scope (a repo-relative directory or file path; empty scope lists the whole tree). Hidden files and directories (any path component starting with ".") are skipped, matching apply's existing discoverFiles behavior for disk scans, so the git-side and disk-side listings stay consistent — including exempting the scope itself from the hidden check: a dot-prefixed -f target (e.g. -f .dash0-assets/) is a deliberate user choice, not something to skip, the same way FindNonHiddenYAMLFiles never applies IsHiddenPath to its walk root. Every path component *inside* scope is still checked normally.

The .yaml/.yml extension check is likewise skipped when scope names a single file exactly (line == scope; a directory scope's entries are always listed as scope/<something>, never scope itself, so this can only match a genuine single-file target) — apply's own single-file create/update path (readMultiDocumentYAML) has no extension check at all, so -f config.json must be scanned by --since the same way it's read by every other apply path, not silently excluded from both snapshots because of its extension.

func (Repo) ReadFileAtRef

func (r Repo) ReadFileAtRef(ctx context.Context, ref, path string) ([]byte, error)

ReadFileAtRef runs `git cat-file -p <ref>:<path>`, returning the file's content at that ref. path must be relative to the repo root, using forward slashes (git's own path convention).

func (Repo) Root

func (r Repo) Root(ctx context.Context) (string, error)

Root runs `git rev-parse --show-toplevel`, returning the absolute path to the repository root containing r.Dir. Callers use this to anchor repo-relative paths consistently between BuildSnapshotFromRef (whose paths are always repo-root-relative, per git ls-tree's own behavior) and BuildSnapshotFromDisk.

Its own error is deliberately unadorned (no "failed to determine repository root for %s" wrapping): callers that need a clean, single-line message for the common "not a git repository at all" case should check IsNotAGitRepository(err) themselves and build their own message from scratch, rather than trying to make a wrapped chain of "failed to determine..." / "git rev-parse ...: exit status 128 (stderr: fatal: not a git repository...)" read well. r.Dir is included so a caller that does want the raw error for anything else still knows which directory failed.

type Snapshot

type Snapshot struct {
	// Identifiers maps every document's (kind, identifier) to the
	// repo-relative (or scope-relative) path it was found at, for every
	// document that carries a stable identifier.
	Identifiers map[IdentifierKey]string

	// NoIdentifier maps the doc path (the file path, plus a "#<index>"
	// suffix for the second and later documents in a multi-document file)
	// of every document with no stable identifier to its details. Diff
	// checks FilePath (not the doc path itself) against the other
	// snapshot's Paths, since a no-identifier document can only be
	// identified as "deleted" by its underlying file disappearing.
	NoIdentifier map[string]NoIdentifierDoc

	// PrometheusAlertsByIdentifier maps a PrometheusRule CRD's identifier to
	// the (group, alert) pairs it contains, for detecting an individual
	// alerting rule removed from a CRD that otherwise still exists.
	PrometheusAlertsByIdentifier map[string][]asset.PrometheusAlertName

	// PrometheusRecordingRoleByIdentifier maps a PrometheusRule CRD's
	// identifier to whether it has at least one recording rule. Recorded for
	// every PrometheusRule CRD identifier found, even when false, so Diff
	// can tell "this CRD never had a recording role" apart from "this CRD
	// doesn't exist in this snapshot at all" -- the same map-presence
	// pattern PrometheusAlertsByIdentifier already relies on. Diff uses this
	// to detect a CRD that survives but whose recording-rule role
	// disappeared entirely (its last `record:` entry removed), a case a
	// per-alert-name diff can't catch: Dash0 models a CRD's recording rules
	// as one server-side resource, not one per record.
	PrometheusRecordingRoleByIdentifier map[string]bool

	// SpamFilterUsesOriginByIdentifier maps a spam filter's identifier to
	// whether it carries a dash0.com/origin label (per
	// asset.SpamFilterUsesOrigin). Diff carries this into Deletion so --since
	// can warn when deleting an ID-only spam filter, whose id may have been
	// reassigned server-side since this identifier was recorded.
	SpamFilterUsesOriginByIdentifier map[string]bool

	// Paths is the set of every file path scanned, regardless of whether it
	// parsed into a recognized kind. Used to check whether a NoIdentifier
	// document's file still exists at all in the other snapshot.
	Paths map[string]bool

	// RawContent maps every file path scanned to its raw content, as read at
	// that point in time. Populated only by BuildSnapshotFromRef (nil/empty
	// from BuildSnapshotFromDisk, which has no caller that needs it): every
	// file --since's deletion detection ever names came from the "before"
	// (ref) snapshot, so retaining the content already read while building
	// it lets a caller resolve a deletion's display name (see
	// resolveDeletionNames in internal/apply/since.go) from memory instead
	// of shelling out to `git cat-file` a second time for the same blob.
	RawContent map[string][]byte
}

Snapshot is the set of asset identifiers found across a scanned scope (a directory or a single file) at one point in time — either a git ref or the current disk contents. --since's deletion detection is a diff between two Snapshots, never a commit-by-commit history scan.

func BuildSnapshotFromDisk

func BuildSnapshotFromDisk(ctx context.Context, scope, repoRoot string) (Snapshot, error)

BuildSnapshotFromDisk builds a Snapshot from the current contents of scope on disk (an absolute or working-directory-relative directory or file path). Hidden files and directories are skipped, matching the git-ref side (ListYAMLFilesAtRef) and apply's own discoverFiles behavior.

repoRoot anchors the relative paths recorded in the returned Snapshot: it must be the same repository root used to resolve the ref passed to BuildSnapshotFromRef, so the two Snapshots' paths line up for Diff's NoIdentifier check (git ls-tree always prints paths relative to the repo root, regardless of any pathspec scope, so the disk side must match that convention rather than being relative to scope itself).

ctx is honored for cancellation between files (checked once per visited entry) — this function does no I/O that itself accepts a context today, but taking one keeps the signature consistent with the rest of this package's public API and forward-compatible with future callers that need to bound how long a large directory scan can run.

scope not existing on disk at all is not an error: every asset definition under it may have been deleted, taking the directory itself with them (or, for a single-file scope, the one file it named). That carries the same meaning as an existing-but-empty directory -- the "after" state has nothing -- so every identifier the "before" snapshot (BuildSnapshotFromRef) found becomes a deletion candidate, the same as it would for a survived, merely-emptied directory.

func BuildSnapshotFromRef

func BuildSnapshotFromRef(ctx context.Context, repo Repo, ref, scope string) (Snapshot, error)

BuildSnapshotFromRef builds a Snapshot from the contents of scope (a repo-relative directory or file path; "" scans the whole repo) as they existed at ref.

Jump to

Keyboard shortcuts

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