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

issueflow

package
v0.15.1 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyTrackerStatus

func ApplyTrackerStatus(
	ctx context.Context,
	t tracker.Tracker,
	errW io.Writer,
	issueID, trackerType string,
	pick func(ctx context.Context, issueID, trackerType string, statuses []string) (string, error),
)

ApplyTrackerStatus fetches tracker statuses, prompts the user to pick one, and applies it. All errors are non-fatal warnings. A nil tracker is a no-op.

func CloseCandidates added in v0.14.0

func CloseCandidates(ctx context.Context, s CandidateStore, c CandidateClient) ([]store.BranchRow, error)

CloseCandidates returns the branches a close picker should offer: every in-progress branch from the store, unioned with every refs/zf/branches/* entry (merged=false) that has no store row and whose feature branch resolves locally or via origin. Ref-derived rows carry IssueID == 0 as the "not yet tracked in this store" marker; the close flow promotes them via MaterializeBranch (before the merge) and TrackCandidate (after it commits).

Reads local refs only — callers should FetchBranchRefs first (getPickedBranch does so via ReconcileMergedFromRefs). Mirrors the cross-machine ref fallback in ResolveParentBranch. A ListBranchRefs failure degrades to store-only candidates (never fatal to close).

func IssueSlugForBranch added in v0.13.1

func IssueSlugForBranch(ctx context.Context, s *store.Store, branchName string) (string, error)

IssueSlugForBranch returns the issue slug owning branchName in the store, or "" when the branch is not tracked.

func MarkMergedFromRef added in v0.12.0

func MarkMergedFromRef(ctx context.Context, s *store.Store, client *git.Client, b store.BranchRow, now time.Time)

MarkMergedFromRef reads branch b's refs/zf/branches/<slug> ref and, when it carries Merged=true, marks the branch and its issue merged in the local store. now is shared across a reconcile pass so a batch of reconciled branches share one timestamp. Errors are non-fatal (best-effort cache sync).

func MaterializeBranch added in v0.14.0

func MaterializeBranch(
	ctx context.Context, c CandidateClient, picked store.BranchRow,
) (created bool, err error)

MaterializeBranch creates the local feature branch for a ref-derived close candidate when it is absent, starting from the ref the candidate resolved against (local or origin). Returns created=true when this call created the branch, so the close flow can roll it back if the close aborts before the merge commit lands. No store writes happen here — tracking is deferred to TrackCandidate so an aborted close leaves no spurious in-progress rows.

func ReconcileMergedFromRefs added in v0.12.0

func ReconcileMergedFromRefs(ctx context.Context, s *store.Store, client *git.Client)

ReconcileMergedFromRefs fetches refs/zf/branches/* (best-effort) and marks any in-progress branch whose ref has Merged=true as merged in the local store.

The branch ref is the cross-machine source of truth: the clone that closes a branch stamps Merged=true on its ref and pushes it (see the close flow's updateClosedStatus). Other clones' local stores lag until reconciled, so any picker that lists in-progress branches straight from the store (close, review sync, review request) would otherwise offer a branch that was already closed elsewhere. Call this before listing in-progress branches for such a picker.

All errors are non-fatal: the fetch is best-effort (offline ⇒ local refs only) and a failed status update just leaves the branch in the list, which is the pre-reconcile behaviour.

func ResolveParentBranch added in v0.12.0

func ResolveParentBranch(ctx context.Context, s ParentStore, c ParentClient, issueSlug, cfgBase string) (string, error)

ResolveParentBranch computes the merge target for an issue: the configured base (cfgBase, or DefaultBaseBranch when empty), redirected to the parent integration branch when issueSlug has a parent.

The store is checked first for the parent relation; on a cross-machine clone where the store has no record, the refs/zf/branches/<slug> git ref is the fallback (FetchBranchRefs runs best-effort first so a later read sees fresh refs). The parent's branch *name* is resolved from the store, then from the parent's own branch ref. Extracted verbatim from cmd/issue/close.go's resolveDefaultBase so close and commit share one implementation.

func RunIssueStart

func RunIssueStart(ctx context.Context, deps StartDeps, prompter StartPrompter) error

RunIssueStart is the prompter-driven core of the issue-start flow. Called from cmd/issue's startRunE (production, via a HuhStartPrompter) and from cmd/branch's newRunE (also production). Tests call it directly with a scriptedStartPrompter. TrackerFirst is carried inside deps.Flags.

Returns nil on two non-error exits: (1) pickIssue returns (nil, nil) — only possible when a test prompter does so; production prompters always return a non-nil issue or an error — and (2) prompter.ResolveBranchConflict returns (nil, nil) to signal the operator aborted or checked out an existing branch.

func TrackCandidate added in v0.14.0

func TrackCandidate(
	ctx context.Context, s CandidateStore, c CandidateClient, picked store.BranchRow,
) (store.BranchRow, error)

TrackCandidate promotes a ref-derived close candidate (IssueID == 0) into a tracked branch: it inserts the issue + branch rows and returns the now-tracked BranchRow (with a real IssueID) for post-merge bookkeeping. A candidate that is already tracked (IssueID != 0) is returned unchanged.

runClose calls this only after the merge commit lands, so a close that aborts on conflict or cancel never inserts rows — preserving the documented "aborts without touching anything" invariant for reviewer-initiated closes.

The inserted issue's TrackerType is read from the branch ref so `git zf issue list` classifies it correctly; the tracker-update gate in the close flow reads TrackerType from the ref + config, not this row, so an empty value here does not change tracker behavior.

Types

type BaseBrancher added in v0.12.0

type BaseBrancher interface {
	LocalBranchNames() ([]string, error)
	RemoteBranchNames() ([]string, error)
}

BaseBrancher is the slice of *git.Client that baseBranchCandidates needs: the local and remote-tracking branch listers. Declared as a tiny interface so candidate assembly is unit-testable without a real repository.

type BranchClient

type BranchClient interface {
	BranchExists(name string) (bool, error)
	Checkout(ctx context.Context, branch string) error
	IO() *pkg.IO
}

BranchClient is the subset of *git.Client that ResolveBranchConflict needs. Declared as a small interface so tests can substitute a fake without constructing a full *git.Client.

type CandidateClient added in v0.14.0

type CandidateClient interface {
	ListBranchRefs(ctx context.Context) ([]git.BranchRef, error)
	ReadBranchRef(ctx context.Context, issueSlug string) (*git.BranchRef, error)
	ResolveBranchRef(name string) (plumbing.Hash, error)
	BranchExists(name string) (bool, error)
	CreateLocalBranch(ctx context.Context, name, startPoint string) error
}

CandidateClient is the slice of *git.Client the close-candidate helpers need.

type CandidateStore added in v0.14.0

type CandidateStore interface {
	ListBranches(ctx context.Context, status store.BranchStatus) ([]store.BranchRow, error)
	InsertIssueWithBranch(ctx context.Context, issue *store.Issue, b *store.Branch) error
}

CandidateStore is the slice of *store.Store the close-candidate helpers need.

type HuhStartPrompter

type HuhStartPrompter struct{}

HuhStartPrompter is the production StartPrompter. It opens real huh forms. Constructed once per `issue start` (or `branch new`) invocation.

func NewHuhStartPrompter

func NewHuhStartPrompter() *HuhStartPrompter

NewHuhStartPrompter constructs the production huh-driven StartPrompter.

func (*HuhStartPrompter) ConfirmCreateBranch

func (p *HuhStartPrompter) ConfirmCreateBranch(ctx context.Context, message string) (bool, error)

func (*HuhStartPrompter) ConfirmCreateWorktree

func (p *HuhStartPrompter) ConfirmCreateWorktree(ctx context.Context, message string) (bool, error)

func (*HuhStartPrompter) NotifyTrackerError

func (p *HuhStartPrompter) NotifyTrackerError(ctx context.Context, message string) error

func (*HuhStartPrompter) PickBaseBranch

func (p *HuhStartPrompter) PickBaseBranch(ctx context.Context, defaultBase string, branches []string) (string, error)

func (*HuhStartPrompter) PickIssueFromTracker

func (p *HuhStartPrompter) PickIssueFromTracker(ctx context.Context, issues []tracker.Issue, allowedTypes []string) (*issuepkg.Issue, error)

func (*HuhStartPrompter) PickIssueFromUser

func (p *HuhStartPrompter) PickIssueFromUser(ctx context.Context, allowedTypes []string) (*issuepkg.Issue, error)

func (*HuhStartPrompter) PickTrackerStatus

func (p *HuhStartPrompter) PickTrackerStatus(
	ctx context.Context, issueID, trackerType string, statuses []string) (string, error)

func (*HuhStartPrompter) PickUseTracker

func (p *HuhStartPrompter) PickUseTracker(ctx context.Context, trackerType string, trackerFirst bool) (bool, error)

func (*HuhStartPrompter) PickUseWorktree

func (p *HuhStartPrompter) PickUseWorktree(ctx context.Context) (bool, error)

func (*HuhStartPrompter) ResolveBranchConflict

func (p *HuhStartPrompter) ResolveBranchConflict(ctx context.Context, client BranchClient, b *branch.Branch, picked *issuepkg.Issue) (*branch.Branch, error)

ResolveBranchConflict is the production conflict-resolution loop. It is the body of the pre-refactor cmd/issue/conflict.go:resolveBranchConflict lifted verbatim onto the prompter so tests can substitute a scripted result.

type ParentClient added in v0.12.0

type ParentClient interface {
	DefaultBaseBranch() (string, error)
	FetchBranchRefs(ctx context.Context) error
	ReadBranchRef(ctx context.Context, issueSlug string) (*git.BranchRef, error)
}

ParentClient is the slice of *git.Client that ResolveParentBranch needs.

type ParentStore added in v0.12.0

type ParentStore interface {
	GetParentIssue(ctx context.Context, childSlug string) (string, error)
	ListBranches(ctx context.Context, status store.BranchStatus) ([]store.BranchRow, error)
}

ParentStore is the slice of *store.Store that ResolveParentBranch needs.

type PendingReview added in v0.13.1

type PendingReview struct {
	EffectiveRef string             // "42@review" or "origin/42@review"
	Commits      int                // commits ahead of the feature branch
	Status       store.ReviewStatus // approved | changes_requested
}

PendingReview describes reviewer commits on <slug>@review that a reviewer decision (approved / changes_requested) says the developer must incorporate into the feature branch.

func PendingReviewCommits added in v0.13.1

func PendingReviewCommits(ctx context.Context, client *git.Client, slug, featureBranch string) (*PendingReview, error)

PendingReviewCommits reports reviewer commits awaiting incorporation for slug's featureBranch, or nil when nothing is pending. It reads only local refs — no network — so it is cheap enough for a pre-commit hook and works offline. The guard is armed only by a decided review ref: in_review means the reviewer hasn't decided, and a stale review branch with no ref (e.g. after a close) never trips it.

func PendingReviewForHEAD added in v0.13.1

func PendingReviewForHEAD(ctx context.Context, client *git.Client, s *store.Store) (*PendingReview, string, error)

PendingReviewForHEAD applies the commit-guard exemptions and returns the pending review for the currently checked-out branch, plus that branch name. It returns (nil, "", nil) whenever the guard must not trip: detached HEAD, an @review branch, a merge in progress (concluding a merge is exactly how incorporation happens), an untracked branch, or nothing pending.

type Prompter added in v0.11.3

type Prompter interface {
	// PickIssueFromUser opens the manual issue-input form (issue ID, subject, type).
	PickIssueFromUser(ctx context.Context, allowedTypes []string) (*issuepkg.Issue, error)

	// PickIssueFromTracker opens the picker over a pre-fetched issues list.
	PickIssueFromTracker(ctx context.Context, issues []tracker.Issue, allowedTypes []string) (*issuepkg.Issue, error)

	// NotifyTrackerError shows a one-line error note when the tracker errors
	// out or returns no open issues. Returning a non-nil error aborts the flow.
	NotifyTrackerError(ctx context.Context, message string) error
}

Prompter resolves the issue-input forms: manual entry, the tracker picker, and the tracker-error note. It is the slice of StartPrompter that getFromUser and getFromTracker drive, kept as its own interface so those helpers can be unit-tested without the rest of the start flow. It lives here in the application layer (not the issue domain package) because every method is a UI-form concern.

type StartDeps

type StartDeps struct {
	Client             *git.Client
	Cfg                *config.AppConfig
	Tracker            tracker.Tracker // nil when cfg.IssueTracker.Type == ""
	Flags              issue.IssueStartFlags
	BaseBranchOverride string // set by PickBaseBranch; skips store re-query in prepareBranch
}

StartDeps bundles the long-lived dependencies the start flow needs. Production code builds it via BuildStartDeps; tests inject directly. Exported because cmd/branch/branch.go's newRunE constructs one too.

func BuildStartDeps

func BuildStartDeps(
	cmd *cobra.Command,
	cfg *config.AppConfig,
	flags issue.IssueStartFlags,
) (StartDeps, error)

BuildStartDeps constructs the production StartDeps from a cobra command. Returns an error if the repo cannot be opened or if a configured tracker (cfg.IssueTracker.Type != "") fails to initialise. When cfg.IssueTracker.Type == "" the returned deps.Tracker is nil and RunIssueStart falls through to the manual issue-input flow.

type StartPrompter

type StartPrompter interface {
	// Prompter contributes the three issue-input methods used by
	// getFromUser and getFromTracker.
	Prompter

	// PickUseTracker drives the "fetch from tracker?" toggle. Called only when
	// cfg.IssueTracker.Type != "". trackerFirst controls the pre-selected
	// option (true for `issue start`, false for `branch new`).
	PickUseTracker(ctx context.Context, trackerType string, trackerFirst bool) (use bool, err error)

	// PickUseWorktree drives the "worktree vs plain branch?" toggle. Called
	// only when cfg.Branch.UseWorktree == nil.
	PickUseWorktree(ctx context.Context) (use bool, err error)

	// PickBaseBranch presents a picker over all local git branches.
	// defaultBase is pre-selected. Called only when --parent was not explicitly
	// provided and the repo has more than one local branch.
	PickBaseBranch(ctx context.Context, defaultBase string, branches []string) (baseBranch string, err error)

	// ConfirmCreateBranch gates client.CreateBranch. message is the full
	// human-readable "Create branch %q based on %q?" string.
	ConfirmCreateBranch(ctx context.Context, message string) (confirmed bool, err error)

	// ConfirmCreateWorktree gates client.CreateWorktree. message is the full
	// "Create worktree %q at %q based on %q?" string.
	ConfirmCreateWorktree(ctx context.Context, message string) (confirmed bool, err error)

	// PickTrackerStatus drives the tracker status-picker form. An empty return
	// signals "no selection" — the caller decides whether to treat that as
	// skip or abort.
	PickTrackerStatus(ctx context.Context, issueID, trackerType string, statuses []string) (string, error)

	// ResolveBranchConflict owns the conflict-resolution loop. It checks
	// whether b's name already exists locally and, if so, drives the operator
	// through a picker (checkout / variant / abort). Returns (b, nil) on the
	// clean no-collision path. Returns (nil, nil) to signal "stop here, do
	// not create or persist" (operator chose abort or checked out the
	// existing branch). Returns (nil, err) on input or git failures.
	ResolveBranchConflict(ctx context.Context, client BranchClient, b *branch.Branch, picked *issuepkg.Issue) (*branch.Branch, error)
}

StartPrompter resolves every user-facing decision in the issue-start flow. The production implementation drives huh forms; the test implementation in start_prompter_test.go returns canned values.

Methods correspond to the huh forms in the pre-refactor flow. The order below mirrors the order calls happen in RunIssueStart. Note: PickIssueFromTracker covers a single form that lets the operator pick both the issue and its branch type, so the mapping is interface→form, not 1:1 for every UI element.

Jump to

Keyboard shortcuts

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