Help us shape the future of Go! Take the 2026 Go Developer Survey

workspace

package
v0.1.70 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

README

workspace

workspace prepares the directory a run works in from a source git checkout, and syncs folder state between directories using git as the transport.

shell.Prepare / shell.SetupEnv build every checkout on it, so SetupResult.Workspace is set whenever a setup has a checkout.

Kinds

Each kind is an Adapter registered under its Kind:

Kind Directory Carries
head the source checkout itself, in place everything it already has
clean a new git worktree at Base (or an Existing branch) nothing from the source's uncommitted work
dirty a new git worktree the source's staged, unstaged and untracked work (Workspace.Carried)

With Ignored: true, worktree kinds also clone the source's gitignored content (node_modules/, build caches, .env) into the worktree. Clones are copy-on-write where the filesystem supports it (APFS clonefile, btrfs/XFS FICLONE) and byte copies otherwise; files are never hardlinked. Paths the worktree's branch tracks keep the branch's content.

The cloned content is recorded in Workspace.Ignored as compact entries: a file, or a whole directory such as vendor/. A directory is expanded into its files when the branch tracks something inside it. ws.IsExcluded(path) keeps that content out of every commit, snapshot and push, even when the worktree's branch doesn't ignore it. That is the case that leaked .npmrc from an old PR branch.

Worktree kinds write a manifest to their own git directory (.git/worktrees/<name>/workspace.json, never committed). workspace.Open(ctx, dir) reopens it from any directory inside the worktree, so a later hook or process sees the same excludes. Any other checkout opens as a head workspace with no excludes. ws.Changes(ctx) lists uncommitted paths minus excluded ones.

ws, err := workspace.Prepare(ctx, workspace.Options{
	Source:  ".",
	Kind:    workspace.KindDirty,
	BaseDir: ".shell",
	Ignored: true,
})
if err != nil {
	return err
}
defer ws.Release() // removes the worktree unless Keep is set

ctx = workspace.NewContext(ctx, ws) // workspace.FromContext(ctx) reads it back

Snapshots

ws.Snapshot(ctx, name) commits the working state to refs/snapshots/<name>. The snapshot holds tracked and untracked files. It never holds gitignored files, or anything listed in Workspace.Ignored, even when the worktree's branch doesn't ignore them. The commit is built in a private index, so HEAD, the index, the working tree and the stash are never touched.

  • The first parent is Base, the HEAD the state is relative to.
  • The second parent is the previous snapshot under the same name, so successive snapshots form a chain that fast-forwards.
  • An unchanged state returns the existing snapshot rather than cutting a new one.

Commit and push pipeline

Every commit and push made through a workspace runs one pipeline:

  • ws.Commit: stage, then hooks, then restage, then gates, then commit.
  • ws.Push: compute what's outgoing, then gates, then push.
gates, err := workspace.ResolveGates([]workspace.GateSpec{
	{Name: workspace.SecretNamesGateName},
	{Name: workspace.MaxSizeGateName, Config: map[string]any{"bytes": 1 << 20}},
})

result, err := ws.Commit(ctx, workspace.CommitOptions{
	Message:  "fix: handle empty input",
	Trailers: []string{"Gavel-Issue-Id: 123"},
	Hooks:    []workspace.Hook{formatter}, // may rewrite files; the pipeline restages
	Gates:    gates,
})

err = ws.Push(ctx, workspace.PushOptions{
	Remote: "https://example.com/acme/app.git",
	Ref:    "refs/heads/fix/empty-input",
	Lease:  expectedRemoteSHA, // --force-with-lease against exactly this SHA
	Gates:  gates,
})

Built in. The excludes gate is the only one, and it always runs. It refuses excluded paths at commit and at push.

Configured. Every other gate runs only when it's listed:

  • secret-names refuses credential file names (.env, .npmrc, *.pem, …).
  • max-size refuses blobs over config.bytes.

Hosts register their own gates (linters, secret scanners) with workspace.RegisterGate. An unknown name fails ResolveGates.

Commit details.

  • Mode is commit (default), fixup (needs Anchor) or amend.
  • With Paths nil, every Change is committed.
  • An empty filtered set returns ErrNothingStaged.
  • Git's own hooks are skipped unless GitHooks is set, since the pipeline is the hook mechanism.
  • A refusing gate returns a *GateError naming what it blocked, and leaves HEAD untouched.

What push gates see.

  • Check.Paths is every path any commit in the outgoing range touched. That includes a file added and removed again, because it still travels in history.
  • Check.Changes is the net diff.
  • The range starts at From, else Lease, else the remote tip. When the remote tip isn't known locally, the gates inspect everything Source carries.
  • A refusing gate sends nothing.

Push, pull and diff of snapshots

snap, err := ws.Snapshot(ctx, "laptop")
err = ws.Push(ctx, workspace.PushOptions{Remote: "git@example.com:acme/sync.git", Ref: snap.Ref, Source: snap.Commit})

// elsewhere, in a checkout of the same repository at snap.Base
pulled, err := other.Pull(ctx, workspace.PullOptions{Remote: "git@example.com:acme/sync.git", Name: "laptop"})

// what changed between two snapshots, or against the current working state ("")
diff, err := ws.Diff(ctx, workspace.DiffOptions{From: pulled.Commit, Patch: true})
  • Remote is a remote name, URL or repository path. Leave it empty to pull between worktrees of one repository, since they share refs.
  • Pull writes and removes exactly the files that differ, leaves gitignored content alone, and moves neither HEAD nor the index. The pulled state shows as uncommitted work.
  • Pull is fast-forward only. HEAD must be the snapshot's Base, and the working state must be clean or equal to a snapshot this one descends from. Otherwise it fails and names the local changes that would be lost. Force: true overwrites them.
  • A push is a plain fast-forward of refs/snapshots/<name>. A diverged remote snapshot is rejected, never overwritten.

Documentation

Overview

Package workspace prepares the directory a run works in from a source git checkout — the checkout itself at HEAD, a clean worktree, or a worktree carrying the source's uncommitted work — and syncs folder state between workspaces as git snapshots.

A snapshot is a commit of the working state (tracked and untracked files, never gitignored ones) stored under refs/snapshots/<name>. It is built in a private index, so taking one never touches HEAD, the index or the stash. Pushing and pulling snapshots uses git itself as the folder sync protocol.

Index

Constants

View Source
const (
	SecretNamesGateName = "secret-names"
	MaxSizeGateName     = "max-size"
)
View Source
const ExcludesGateName = "excludes"

ExcludesGateName is the one gate that always runs: content cloned from the source's gitignored files never leaves the workspace.

Variables

View Source
var ErrNothingStaged = errors.New("workspace: nothing staged to commit")

ErrNothingStaged is returned when, after excludes are dropped, there is nothing to commit.

Functions

func LooksSecret

func LooksSecret(path string) bool

LooksSecret reports whether a repo-relative path names a credential file, by base name (including the .env.production family) or by extension.

func NewContext

func NewContext(ctx context.Context, ws *Workspace) context.Context

NewContext returns ctx carrying ws, for code that resolves the directory it works in from the context.

func RegisterGate

func RegisterGate(name string, factory GateFactory)

RegisterGate makes a host gate (a linter, a secret scanner) selectable by name. It panics on a duplicate or reserved name, as a registration mistake.

Types

type Adapter

type Adapter interface {
	Prepare(ctx context.Context, source string, opts Options) (*Workspace, error)
}

Adapter prepares one kind of workspace.

type Change

type Change struct {
	Status Status `json:"status"`
	Path   string `json:"path"`
	From   string `json:"from,omitempty"`
}

Change is one changed path; From is the original path of a rename or copy.

type Check

type Check struct {
	Phase    Phase
	From, To string
	Paths    []string
	Changes  []Change
}

Check is what a gate inspects. For a commit, Paths is the staged set and Changes the index against From (HEAD). For a push, From..To is the outgoing range, Paths every path any commit in it touched — including ones a later commit removed, which still travel in history — and Changes the net diff.

type CommitMode

type CommitMode string

CommitMode is how Commit records the staged change.

const (
	CommitNew   CommitMode = "commit"
	CommitFixup CommitMode = "fixup"
	CommitAmend CommitMode = "amend"
)

type CommitOptions

type CommitOptions struct {
	// Paths are staged before committing; nil stages every Change. Excluded
	// paths are always dropped.
	Paths []string
	Mode  CommitMode
	// Anchor is the commit a fixup targets.
	Anchor string
	// Message is required for a new commit; an amend keeps the old message
	// when it is empty.
	Message  string
	Trailers []string
	// Author overrides the configured identity for author and committer.
	Author *Signature
	Hooks  []Hook
	Gates  []Gate
	// GitHooks runs the repository's own git hooks; off by default because the
	// workspace pipeline is the hook/gate mechanism.
	GitHooks bool
}

CommitOptions configures Commit.

type CommitResult

type CommitResult struct {
	SHA   string
	Paths []string
}

CommitResult is the commit Commit made and the paths it carried.

type DiffOptions

type DiffOptions struct {
	From, To string
	// Patch also renders a binary-safe unified diff.
	Patch bool
}

DiffOptions selects the two states to compare. Each is a revision — a snapshot commit, refs/snapshots/<name>, or any commit-ish — and empty means the workspace's current working state.

type DiffResult

type DiffResult struct {
	Changes []Change `json:"changes"`
	Patch   string   `json:"patch,omitempty"`
}

type Gate

type Gate interface {
	Name() string
	Check(ctx context.Context, ws *Workspace, c Check) error
}

Gate inspects a Check and may block it; it never mutates the workspace.

func ResolveGates

func ResolveGates(specs []GateSpec) ([]Gate, error)

ResolveGates builds the configured gates. No spec means no gates beyond the built-in excludes gate; an unknown name fails.

type GateError

type GateError struct {
	Gate     string
	Phase    Phase
	Findings []string
}

GateError is a gate's refusal, naming what it blocked.

func (*GateError) Error

func (e *GateError) Error() string

type GateFactory

type GateFactory func(config map[string]any) (Gate, error)

GateFactory builds a gate from its configuration; it must reject keys it does not understand.

type GateSpec

type GateSpec struct {
	Name   string         `json:"name" yaml:"name"`
	Config map[string]any `json:"config,omitempty" yaml:"config,omitempty"`
}

GateSpec selects a registered gate by name, with its configuration.

type Hook

type Hook interface {
	Name() string
	Run(ctx context.Context, ws *Workspace, paths []string) error
}

Hook runs before a commit's gates and may rewrite files (formatters, tidy). The pipeline restages the commit's paths after every hook has run.

type Kind

type Kind string

Kind names how a workspace relates to its source checkout.

const (
	// KindHead is the source checkout itself, used in place.
	KindHead Kind = "head"
	// KindClean is a new git worktree at Base with none of the source's
	// uncommitted work.
	KindClean Kind = "clean"
	// KindDirty is a new git worktree that also carries the source's staged,
	// unstaged and untracked work.
	KindDirty Kind = "dirty"
)

type Options

type Options struct {
	Source string
	Kind   Kind

	// BaseDir holds generated worktrees (BaseDir/worktrees/<branch>-<uuid>)
	// unless Path names the worktree directory explicitly.
	BaseDir string
	Path    string
	// Branch is the branch the worktree creates, or with Existing the branch it
	// checks out as-is. Unset, a new branch is named <Prefix>/<uuid>.
	Branch   string
	Prefix   string
	Existing bool
	// Base is the commit-ish a new branch starts from (default: the source's
	// HEAD).
	Base string
	// Ignored copy-on-write clones the source's gitignored content
	// (node_modules/, build caches, .env) into the worktree. Cloned paths are
	// recorded in Workspace.Ignored and never enter a snapshot.
	Ignored bool
	// Keep leaves the worktree on disk when the workspace is released.
	Keep bool
}

Options configures Prepare. Source is any directory inside the source checkout; every other field applies only to worktree kinds.

type Phase

type Phase string

Phase is the pipeline step a Check comes from.

const (
	PhaseCommit Phase = "commit"
	PhasePush   Phase = "push"
)

type PullOptions

type PullOptions struct {
	Remote string
	Name   string
	// Force overwrites local changes the snapshot does not descend from.
	Force bool
}

PullOptions configures Workspace.Pull. With Remote empty the snapshot is read from this repository's own refs, which every worktree of it shares.

type PushOptions

type PushOptions struct {
	Remote string
	Ref    string
	// Source is the commit-ish pushed (default HEAD), e.g. a snapshot commit.
	Source string
	// Lease is the SHA Ref must still hold on the remote; set, the push may
	// rewrite history, but only over exactly that SHA (--force-with-lease).
	Lease string
	// From is the lower bound of the outgoing range the gates inspect. Unset it
	// is Lease, else Ref's current tip on Remote; when neither is known
	// locally, the gates see the whole of Source.
	From          string
	Atomic        bool
	ServerOptions []string
	Gates         []Gate
}

PushOptions configures Push. Remote is a remote name, URL or repository path; Ref is the full ref written there (refs/heads/x, refs/snapshots/x).

type Signature

type Signature struct{ Name, Email string }

Signature is a commit author.

type Snapshot

type Snapshot struct {
	Name     string
	Ref      string
	Commit   string
	Tree     string
	Base     string
	Previous string
}

Snapshot is a commit of a workspace's working state. Its first parent is Base, the commit the state is relative to; its second parent, when set, is the Previous snapshot under the same name, so successive snapshots form a chain that fast-forwards.

type Status

type Status string

Status is how a path changed between two states.

const (
	StatusAdded       Status = "added"
	StatusModified    Status = "modified"
	StatusDeleted     Status = "deleted"
	StatusRenamed     Status = "renamed"
	StatusCopied      Status = "copied"
	StatusTypeChanged Status = "type-changed"
)

type Workspace

type Workspace struct {
	Kind Kind
	// Source is the root of the source checkout.
	Source string
	// Dir is the root the run works in: Source for KindHead, else the worktree.
	Dir    string
	Branch string
	// Base is the commit Dir's HEAD pointed at when it was prepared.
	Base string
	// Carried lists the source's uncommitted paths copied in (KindDirty).
	Carried []string
	// Ignored lists the gitignored content cloned in from the source as
	// compact entries: a file, or a directory ending in "/". It is recorded in
	// the worktree's manifest, and IsExcluded keeps it out of every commit,
	// snapshot and push even where the worktree's branch does not ignore it.
	Ignored []string
	// contains filtered or unexported fields
}

Workspace is a prepared directory and what it was prepared from.

func FromContext

func FromContext(ctx context.Context) (*Workspace, bool)

FromContext returns the workspace NewContext attached to ctx.

func Open

func Open(ctx context.Context, dir string) (*Workspace, error)

Open returns the workspace dir belongs to. A worktree Prepare created is reopened from its manifest with the excludes recorded then; any other checkout is a head workspace with none. The returned workspace does not own the directory: Release on it is a no-op.

func Prepare

func Prepare(ctx context.Context, opts Options) (*Workspace, error)

Prepare resolves opts.Source to its checkout root and prepares the workspace opts.Kind names.

func (*Workspace) Changes

func (w *Workspace) Changes(ctx context.Context) ([]string, error)

Changes lists every path with uncommitted work — staged, unstaged or untracked — except gitignored and excluded content.

func (*Workspace) Commit

func (w *Workspace) Commit(ctx context.Context, opts CommitOptions) (*CommitResult, error)

Commit stages opts.Paths, runs the hooks and restages, runs the gates over the final staged set, and commits. A refusing gate leaves HEAD untouched.

func (*Workspace) Diff

func (w *Workspace) Diff(ctx context.Context, opts DiffOptions) (*DiffResult, error)

Diff compares two states of the workspace.

func (*Workspace) IsExcluded

func (w *Workspace) IsExcluded(path string) bool

IsExcluded reports whether path is content the workspace cloned from the source's gitignored files, which no commit, snapshot or push may carry.

func (*Workspace) Outgoing

func (w *Workspace) Outgoing(ctx context.Context, from, to string) ([]Change, error)

Outgoing is the net change between two commits; an empty from means the empty tree, so every path in to is reported as added.

func (*Workspace) Pull

func (w *Workspace) Pull(ctx context.Context, opts PullOptions) (*Snapshot, error)

Pull makes the working state match a snapshot: files it holds are written, files it lacks are removed, and gitignored content is left alone. HEAD and the index are not moved, so the pulled state shows as uncommitted work.

Without Force a pull must fast-forward: HEAD must be the snapshot's Base and the working state must be clean or equal to a snapshot this one descends from. Anything else fails, naming the local changes that would be lost.

func (*Workspace) Push

func (w *Workspace) Push(ctx context.Context, opts PushOptions) error

Push runs the gates over everything the push would send, then pushes. A refusing gate sends nothing.

func (*Workspace) Release

func (w *Workspace) Release() error

Release tears the workspace down: a worktree is removed unless kept, and the source checkout of a head workspace is never touched.

func (*Workspace) Snapshot

func (w *Workspace) Snapshot(ctx context.Context, name string) (*Snapshot, error)

Snapshot records the working state — tracked and untracked files, never gitignored ones or the paths in Ignored — as refs/snapshots/<name>. HEAD, the index and the working tree are left untouched. An unchanged state returns the existing snapshot instead of cutting a new one.

Jump to

Keyboard shortcuts

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