staged

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package staged answers three questions about a change that is staged but not committed: which files it touches, which bytes of them, and which bytes the suite should be run against.

The third is the one that is easy to miss and expensive to get wrong. Mutants come from the staged content, but the test command runs against a whole tree, and a tracked file left dirty in the worktree decides what the suite proves. Measured on a fixture built for it: pointed at the worktree instead of the index, seven of eight verdicts moved — a score of 0.13 against 1.00 for the identical eight mutants of an identical file. A guard that only inspects the staged paths cannot see that, because the file that moved them was never staged.

So the index is materialised and the release is pointed at that. It is not an optimisation and it is not belt-and-braces; it is the difference between measuring the change and measuring the desk it was written on.

Index

Constants

View Source
const ConfigName = ".ditto.json"

ConfigName is the file a repository uses to name what the sandbox is missing.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config added in v0.6.0

type Config struct {
	// Generated are repository-relative paths copied from the working tree into
	// the sandbox after the index is materialised.
	//
	// Every one of them must be untracked. A tracked path has an index version,
	// and letting the working tree's win is exactly the hole the sandbox exists
	// to close — so naming one here is refused rather than obeyed.
	Generated []string `json:"generated"`
}

Config is what a repository tells ditto about itself.

There is exactly one thing to say, and it exists because of one honest gap: a sandbox is built from the index, and some repositories do not build from their index alone. A generated directory the build needs — an embedded frontend bundle, generated bindings — is on disk and not in git, so the sandbox arrives without it and the package that needs it cannot compile.

Reading the index is not the part to give up: it is what makes a verdict about the change rather than about the desk it was written on, measured at 7 of 8 verdicts moving when a release read the worktree instead. So this does not widen what is read. It names, one path at a time, what git does not carry.

type OSRunner

type OSRunner struct{}

OSRunner runs processes with the inherited git addressing removed.

func (OSRunner) Output

func (OSRunner) Output(dir, name string, args ...string) ([]byte, error)

type Range

type Range struct {
	Start int
	End   int
}

Range is a half-open byte span within one file, counted from its first byte.

type Repository

type Repository struct {
	// contains filtered or unexported fields
}

Repository is a checkout the staged questions can be asked of.

func New

func New(runner Runner, directory string) (*Repository, error)

New binds the questions to a checkout. The root is resolved by git rather than taken on trust, so a command run from a subdirectory means the same thing.

func (*Repository) CopyGenerated added in v0.6.0

func (r *Repository) CopyGenerated(sandbox *Sandbox, generated []string) ([]string, error)

CopyGenerated puts the configured paths into a sandbox and reports what it copied, so a run says which of its bytes did not come from the index.

A path that does not exist is refused rather than skipped. It is named because the build needs it, and a sandbox quietly missing it produces the failure this setting was added to prevent.

func (*Repository) Files

func (r *Repository) Files(excludedPrefixes []string) ([]string, error)

Files lists the staged Go sources worth mutating: not tests, because they are the oracle rather than the subject, and not anything under a prefix the caller excluded.

func (*Repository) LoadConfig added in v0.6.0

func (r *Repository) LoadConfig() (Config, error)

LoadConfig reads the repository's configuration. A repository without one is not an error: most do build from their index, and those need to say nothing.

func (*Repository) Materialize

func (r *Repository) Materialize() (*Sandbox, error)

Materialize writes the index into a temporary directory.

func (*Repository) RejectPartial

func (r *Repository) RejectPartial(files []string) error

RejectPartial refuses a staged file that also has unstaged edits.

The scope is derived from the index and the mutants are written into a copy of the index, so worktree-only edits to a staged file are content the verdict was never about. Reporting on it as though it were is worse than refusing.

func (*Repository) Root

func (r *Repository) Root() string

Root is the directory every other answer is relative to.

func (*Repository) ScopeOf

func (r *Repository) ScopeOf(files []string) (Scope, error)

ScopeOf converts each staged diff into byte ranges of the staged content.

It fails open rather than guessing. A diff that cannot be parsed, or a range that does not land on index bytes, produces a whole-file scope and says why — mutating too much is a cost, and mutating the wrong bytes is a wrong answer.

type Runner

type Runner interface {
	Output(dir, name string, args ...string) ([]byte, error)
}

Runner is the seam over running a process, so the git work can be tested without a repository.

type Sandbox

type Sandbox struct {
	Root string
	// contains filtered or unexported fields
}

Sandbox is a checkout of the index, and the thing a release must be pointed at.

The measurement that put this here: with a release pointed at the worktree instead, and one tracked file left dirty and unstaged, seven of eight verdicts moved — 0.13 against 1.00 for the identical eight mutants of an identical file. RejectPartial does not cover it, because the file that moved them was never staged and a check over staged paths never looks at it.

`git checkout-index` also happens to be the cheap way to do it: it writes exactly the staged content and nothing else, with no second walk of the tree.

func (*Sandbox) Close

func (s *Sandbox) Close()

Close removes the sandbox. It is safe to call more than once.

type Scope

type Scope struct {
	Files   []string
	Ranges  map[string][]Range
	Derived bool
	Reason  string
}

Scope is what a staged change justifies mutating.

Ranges is keyed by file and stays keyed by file. A byte offset only means something against the file it was measured in, so a scope that has lost track of which file a range came from makes every file answer to every range: the mutant count then grows as the square of the file count, and the extra ones land in code no diff touched.

Jump to

Keyboard shortcuts

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