gitlog

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 11 Imported by: 0

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

View Source
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

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

func ChangedFileCount(commits []Commit) int

ChangedFileCount counts distinct changed file paths across all commits.

func DefaultConcurrency

func DefaultConcurrency(repoCount int) int

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

func DiffFileCount(diff string) int

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

func DiffLineStats(diff string) (added, removed int)

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

func DiffPaths(diff string) []string

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

func MissingRequiredCategories(cl Changelog, required []string) []string

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

type AuthorStat struct {
	Author       string
	Commits      int
	LinesAdded   int
	LinesRemoved int
}

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

func CategorizeCommits(commits []Commit) Changelog

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

func ParseLog(out string) ([]Commit, error)

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

func AggregateDigest(commits []Commit, diffs map[string]string, since, until time.Time) Digest

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 FileStat

type FileStat struct {
	Path    string
	Commits int
}

FileStat is the per-file aggregate for a digest (§10.2).

type RepoResult

type RepoResult struct {
	Path   string
	Since  time.Time
	Until  time.Time
	Digest Digest
	Err    error
}

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

func NewRunner(dir string) (*Runner, error)

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) Diff

func (r *Runner) Diff(ctx context.Context, revRange string) (string, error)

Diff runs `git diff` over revRange and returns the raw unified diff text.

func (*Runner) DiffForCommit

func (r *Runner) DiffForCommit(ctx context.Context, hash string) (string, error)

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

func (r *Runner) DiffStaged(ctx context.Context) (string, error)

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

func (r *Runner) FetchRef(ctx context.Context, remote, ref string) error

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

func (r *Runner) LatestTag(ctx context.Context) (string, error)

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

func (r *Runner) Log(ctx context.Context, revRange string) ([]Commit, error)

Log runs `git log` over revRange with the control-separator format plus --name-status and parses the output into Commits.

func (*Runner) LogSince

func (r *Runner) LogSince(ctx context.Context, since time.Time) ([]Commit, error)

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

func (r *Runner) ObjectExists(ctx context.Context, sha string) bool

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

func (r *Runner) RemoteNames(ctx context.Context) ([]string, error)

RemoteNames runs `git remote` and returns the configured remote names. A repository without remotes yields an empty slice, not an error.

func (*Runner) RemoteURL added in v0.4.0

func (r *Runner) RemoteURL(ctx context.Context, remote string) (string, error)

RemoteURL runs `git remote get-url <remote>` and returns the fetch URL. A nonexistent remote is an error (git exits non-zero).

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.

type TopicStat

type TopicStat struct {
	Topic   string
	Commits int
}

TopicStat is the per-conventional-commit-type aggregate for a digest (§10.2). Topic is the lowercased conventional-commit type, or "other" for non-conventional / unmapped commits.

Jump to

Keyboard shortcuts

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