Documentation
¶
Overview ¶
Package gitlog reads and parses git history via the system `git` binary (os/exec), hidden behind the Source interface so a go-git backend could replace it without touching commands (see docs/TECHNICAL_PLAN.md §4).
Index ¶
- Constants
- Variables
- func ChangedFileCount(commits []Commit) int
- func DefaultConcurrency(repoCount int) int
- func DiffFileCount(diff string) int
- func DiffLineStats(diff string) (added, removed int)
- func DiffPaths(diff string) []string
- func MissingRequiredCategories(cl Changelog, required []string) []string
- type AuthorStat
- type Changelog
- type ChangelogEntry
- type Commit
- type Digest
- type FileChange
- type FileStat
- type RepoResult
- type Runner
- func (r *Runner) Diff(ctx context.Context, revRange string) (string, error)
- func (r *Runner) DiffForCommit(ctx context.Context, hash string) (string, error)
- func (r *Runner) DiffStaged(ctx context.Context) (string, error)
- func (r *Runner) FetchRef(ctx context.Context, remote, ref string) error
- func (r *Runner) LatestTag(ctx context.Context) (string, error)
- func (r *Runner) Log(ctx context.Context, revRange string) ([]Commit, error)
- func (r *Runner) LogSince(ctx context.Context, since time.Time) ([]Commit, error)
- func (r *Runner) ObjectExists(ctx context.Context, sha string) bool
- func (r *Runner) RemoteNames(ctx context.Context) ([]string, error)
- func (r *Runner) RemoteURL(ctx context.Context, remote string) (string, error)
- type Source
- type TopicStat
Constants ¶
const ( CategoryAdded = "Added" CategoryChanged = "Changed" CategoryDeprecated = "Deprecated" CategoryRemoved = "Removed" CategoryFixed = "Fixed" CategorySecurity = "Security" CategoryOther = "Other" )
Keep a Changelog category names (§9.2). CategoryOther is the catch-all for non-conventional commits and conventional types with no direct mapping (docs/style/test/build/ci/chore).
Variables ¶
var CategoryOrder = []string{ CategoryAdded, CategoryChanged, CategoryDeprecated, CategoryRemoved, CategoryFixed, CategorySecurity, CategoryOther, }
CategoryOrder is the fixed, documented print order for changelog output (§9.4): breaking changes first, then Keep a Changelog categories, Other last.
Functions ¶
func ChangedFileCount ¶
ChangedFileCount counts distinct changed file paths across all commits.
func DefaultConcurrency ¶
DefaultConcurrency returns a sensible worker-pool size for CollectDigests: GOMAXPROCS, capped at the number of repositories (§10.4). Not user-configurable in Этап 3 (no --concurrency flag).
func DiffFileCount ¶ added in v0.2.0
DiffFileCount counts the number of distinct files in a unified diff by counting "diff --git " headers. Used by HeuristicRisk so it scores the already glob-filtered diff, not the unfiltered commit file list.
func DiffLineStats ¶
DiffLineStats counts added/removed content lines in a unified diff, ignoring the +++/--- file headers. Shared by the risk heuristic, the offline provider, and the review command's stats block — extracted here (rather than duplicated per caller) since it operates purely on diff text owned by this package.
func DiffPaths ¶ added in v0.4.0
DiffPaths extracts the changed (b-side) file paths from a unified diff by parsing "diff --git a/x b/y" headers. Used as the sensitive-path signal when no commit metadata exists (e.g. a staged review, where there is no commit history to scan file paths from).
func MissingRequiredCategories ¶
MissingRequiredCategories reports which of the required category names have zero entries in cl, preserving the input order (§9.3). Unknown category names (typos in policy config) are reported too — CategorizeCommits only ever writes to CategoryOrder keys, so an unknown name is always "missing".
Types ¶
type AuthorStat ¶
AuthorStat is the per-author aggregate for a digest (§10.2).
type Changelog ¶
type Changelog struct {
// Categories maps every CategoryOrder name to its entries (possibly
// empty, but the key is always present — §9.4 JSON contract).
Categories map[string][]ChangelogEntry
// Breaking is the ordered list of breaking-change entries, duplicated
// from their home category (§9.2).
Breaking []ChangelogEntry
}
Changelog is the categorized result of a commit range (§9.2, §9.4).
func CategorizeCommits ¶
CategorizeCommits classifies commits into Keep a Changelog categories (§9.2). Merge commits (Subject starting with "Merge ") are excluded entirely. Duplicate hashes keep only the first occurrence.
type ChangelogEntry ¶
type ChangelogEntry struct {
Hash string // short (7-char) hash, per §9.4 uniform styling
Subject string // subject with the "type(scope)!:" prefix stripped
Category string
Breaking bool
// BreakingText is the callout text used in the "⚠ BREAKING CHANGES"
// section: the first line after "BREAKING CHANGE:" in the body, or the
// stripped subject if the only marker was "!". Empty when !Breaking.
BreakingText string
}
ChangelogEntry is one commit rendered into a changelog category.
type Commit ¶
type Commit struct {
Hash string
Author string
Date time.Time
Subject string
Body string
Files []FileChange
}
Commit is a single commit parsed from `git log` output.
func ParseLog ¶
ParseLog parses the output of
git log --pretty=format:%H%x00%an%x00%aI%x00%s%x00%b%x00 --name-status <range>
The format emits exactly 5 literal NULs per commit — one after each of hash/author/date/subject/body — regardless of field content (an empty %s or %b still emits its trailing NUL), so a flat split on the single NUL byte yields exactly 5*N+1 tokens for N commits. Each group of 5 tokens is (head, author, date, subject, body); empty subjects/bodies are simply empty strings in their positions, never a collapsed token. Never split on "\n": commit bodies legitimately contain newlines. NUL is the only byte git refuses to store in a commit message (fsck nulInCommit), so a field can never contain one.
The head token carries the --name-status block of the PREVIOUS commit (if any) followed by this commit's hash on the last line; the first record has no preceding block, and the final "extra" token past the last 5-token group is the name-status block of the last commit (no hash follows it).
type Digest ¶
type Digest struct {
Since time.Time
Until time.Time
Commits int
FilesChanged int
LinesAdded int
LinesRemoved int
ByAuthor []AuthorStat
ByTopic []TopicStat
TopFiles []FileStat
}
Digest is the deterministic aggregation result for one repository over one time window (§10.2, §10.3).
func AggregateDigest ¶
AggregateDigest computes the deterministic digest for commits observed in [since, until]. diffs maps commit hash to that commit's unified diff text (collected by the caller via Runner.DiffForCommit) — used only for the per-author added/removed line counts.
type FileChange ¶
type FileChange struct {
// Status is the raw git status letter(s): "A", "M", "D", "R100", "C75", ...
Status string
// Path is the (new) path of the file.
Path string
// Old is the previous path for renames/copies (R/C statuses); empty otherwise.
Old string
}
FileChange is one entry of a `--name-status` block.
type RepoResult ¶
RepoResult is the per-repository outcome of a multi-repo digest collection (§10.4). Exactly one of Digest/Err is meaningful: when Err is non-nil, the repo failed (bad path, not a git repo, git failure) and the other fields besides Path/Since/Until are zero values — callers must check Err before reading Digest.
func CollectDigests ¶
func CollectDigests(ctx context.Context, repoPaths []string, since time.Time, concurrency int) []RepoResult
CollectDigests runs AggregateDigest over each repository path concurrently, using a bounded worker pool of `concurrency` goroutines communicating over a job-index channel (§10.4) — not an unbounded fan-out, and not a third-party errgroup, per the project's "teach raw stdlib concurrency" principle (docs/TECHNICAL_PLAN.md §2).
Results are written to results[i] by exactly one goroutine (the one that claims job i), so no mutex is needed, and the returned slice preserves the input order of repoPaths regardless of completion order — required for deterministic output and golden tests.
A single bad repository (missing directory, not a git repository, git failure, or "no commits in window") never aborts the others: errors are isolated into that repo's RepoResult.Err. Cancelling ctx (e.g. Ctrl-C) stops workers from starting new repos; already-running `git` invocations are killed by exec.CommandContext as usual.
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner implements Source by shelling out to the system `git` binary.
func NewRunner ¶
NewRunner returns a Runner operating in dir (empty = current directory). It fails with a friendly error if `git` is not installed in PATH.
func (*Runner) DiffForCommit ¶
DiffForCommit runs `git show --format= <hash>` and returns the unified diff introduced by that single commit (no commit message, just the diff body).
func (*Runner) DiffStaged ¶ added in v0.4.0
DiffStaged runs `git diff --cached` and returns the unified diff of staged changes. Nothing staged is not an error — git exits 0 with empty output, so callers branch on the empty string.
func (*Runner) FetchRef ¶ added in v0.4.0
FetchRef runs `git fetch --no-tags <remote> <ref>` to make a remote ref/SHA (e.g. `pull/N/head` or a base branch) available locally. The remote is a parameter, not a hardcoded "origin": in fork workflows the PR's repository is often configured as "upstream" while "origin" points at the fork.
func (*Runner) LatestTag ¶
LatestTag runs `git describe --tags --abbrev=0` and returns the most recent reachable tag. When the repository has no tags, git exits non-zero; that specific case is not an error here (§9.5) — LatestTag returns ("", nil) so callers can branch on an empty string instead of inspecting error text. Any describe failure (no tags, unborn HEAD, ...) degrades to "no tag" — the exact stderr wording varies by git version/locale, so it is not worth pattern-matching; every failure means the same thing to the caller.
func (*Runner) Log ¶
Log runs `git log` over revRange with the control-separator format plus --name-status and parses the output into Commits.
func (*Runner) LogSince ¶
LogSince runs `git log --since=<RFC3339>` with the control-separator format plus --name-status and parses the output into Commits (§10.1). The window is open-ended on the upper bound (implicitly "since..HEAD" of the current checkout), matching every other command's branch-agnostic behavior.
func (*Runner) ObjectExists ¶ added in v0.4.0
ObjectExists runs `git cat-file -e <sha>^{commit}` and reports whether the commit object is available locally. Any failure (missing object, malformed SHA, ...) is false, never an error — this is a boolean probe, not an operation that can "fail".
func (*Runner) RemoteNames ¶ added in v0.4.0
RemoteNames runs `git remote` and returns the configured remote names. A repository without remotes yields an empty slice, not an error.
type Source ¶
type Source interface {
// Log returns the commits in the given revision range (e.g. "HEAD~5..HEAD").
Log(ctx context.Context, revRange string) ([]Commit, error)
// Diff returns the unified diff text for the given revision range.
Diff(ctx context.Context, revRange string) (string, error)
// DiffStaged returns the unified diff of staged (indexed, not yet
// committed) changes, like `git diff --cached`. An empty string means
// nothing is staged.
DiffStaged(ctx context.Context) (string, error)
// LatestTag returns the most recent reachable tag from HEAD (like
// `git describe --tags --abbrev=0`). An empty string with a nil error
// means no tags exist — a legitimate result, not a failure (see
// docs/TECHNICAL_PLAN.md §9.5).
LatestTag(ctx context.Context) (string, error)
// LogSince returns the commits reachable from HEAD committed after since
// (like `git log --since=<RFC3339>`).
LogSince(ctx context.Context, since time.Time) ([]Commit, error)
// DiffForCommit returns the unified diff text introduced by a single
// commit (like `git show --format= <hash>`).
DiffForCommit(ctx context.Context, hash string) (string, error)
// ObjectExists reports whether a git object (commit SHA) is available
// locally without a network call — used for a best-effort fetch (skip
// fetching what's already there, which also keeps PR review testable
// without real git fetches).
ObjectExists(ctx context.Context, sha string) bool
// FetchRef runs `git fetch --no-tags <remote> <ref>` for a remote ref/SHA
// not yet available locally (e.g. a PR head or a base commit needed for
// a merge-base diff).
FetchRef(ctx context.Context, remote, ref string) error
// RemoteNames returns the configured remote names (like `git remote`),
// one per line of git output. An empty slice with a nil error means the
// repository has no remotes.
RemoteNames(ctx context.Context) ([]string, error)
// RemoteURL returns the fetch URL of the given remote (like
// `git remote get-url <remote>`). Errors when the remote does not exist.
RemoteURL(ctx context.Context, remote string) (string, error)
}
Source provides git history for a revision range. It exists so that the os/exec-based Runner could later be swapped for a go-git implementation without touching command code.