branch

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package branch provides git branch management operations.

This package handles branch listing, switching, cleanup, and worktree operations across single and multiple repositories.

Features

  • Branch listing and filtering
  • Safe branch switching
  • Cleanup of merged/stale/gone branches
  • Worktree management

Usage

service := branch.NewService(repoPath)
branches, err := service.List(branch.ListOptions{Remote: true})
err = service.Cleanup(branch.CleanupOptions{DryRun: true})

Index

Constants

View Source
const (
	DefaultWorkTemplate   = "feat/{task}"
	DefaultDeviceTemplate = "feat/{task}/{device}"
	DefaultAgentTemplate  = "agent/{task}/{agent}"
)

Default templates follow the convention the handoff commands assume: a task branch under feat/, a per-writer branch below it, and agent work in its own namespace so it is never mistaken for a person's.

Variables

View Source
var (
	// ErrBranchExists indicates the branch already exists.
	ErrBranchExists = errors.New("branch already exists")

	// ErrBranchNotFound indicates the branch doesn't exist.
	ErrBranchNotFound = errors.New("branch not found")

	// ErrInvalidName indicates invalid branch name.
	ErrInvalidName = errors.New("invalid branch name")

	// ErrInvalidRef indicates invalid starting ref.
	ErrInvalidRef = errors.New("invalid starting ref")

	// ErrProtectedBranch indicates operation on protected branch.
	ErrProtectedBranch = errors.New("cannot modify protected branch")

	// ErrBranchUnmerged indicates branch has unmerged changes.
	ErrBranchUnmerged = errors.New("branch has unmerged changes")

	// ErrBranchIsHead indicates branch is currently checked out.
	ErrBranchIsHead = errors.New("cannot delete currently checked out branch")

	// ErrDetachedHead indicates repository is in detached HEAD state.
	ErrDetachedHead = errors.New("repository in detached HEAD state")

	// ErrUpstreamNotSet indicates upstream branch is not configured.
	ErrUpstreamNotSet = errors.New("upstream branch not set")

	// ErrRemoteNotFound indicates remote doesn't exist.
	ErrRemoteNotFound = errors.New("remote not found")

	// ErrOperationCancelled indicates user canceled the operation.
	ErrOperationCancelled = errors.New("operation canceled by user")

	// ErrWorktreeExists indicates worktree path already exists.
	ErrWorktreeExists = errors.New("worktree path already exists")

	// ErrWorktreeNotFound indicates worktree doesn't exist.
	ErrWorktreeNotFound = errors.New("worktree not found")

	// ErrWorktreeDirty indicates worktree has uncommitted changes.
	ErrWorktreeDirty = errors.New("worktree has uncommitted changes")

	// ErrWorktreeMain indicates operation on main worktree.
	ErrWorktreeMain = errors.New("cannot remove main worktree")

	// ErrWorktreeLocked indicates worktree is locked.
	ErrWorktreeLocked = errors.New("worktree is locked")

	// ErrBranchInUse indicates branch is checked out in another worktree.
	ErrBranchInUse = errors.New("branch is checked out in another worktree")

	// ErrInvalidPath indicates invalid worktree path.
	ErrInvalidPath = errors.New("invalid worktree path")
)

Common errors for branch operations.

View Source
var ProtectedBranches = repository.ProtectedBranches

ProtectedBranches are branches that require --force to delete. It aliases the canonical list in pkg/repository so both cleanup paths share one source.

Functions

func IsProtected

func IsProtected(name string) bool

IsProtected checks if a branch name matches protected patterns. It delegates to pkg/repository, the single source of truth for protected-branch judgment (pkg/repository cannot import pkg/branch — the dependency runs branch → repository — so ownership lives in the lower package).

Types

type AddOptions

type AddOptions struct {
	Path         string // Worktree path (required)
	Branch       string // Branch name (required)
	CreateBranch bool   // Create new branch
	Force        bool   // Overwrite existing
	Detach       bool   // Detached HEAD
	Checkout     string // Specific commit to checkout
}

AddOptions configures worktree addition.

type AnalyzeOptions

type AnalyzeOptions struct {
	IncludeMerged     bool          // Include fully merged branches
	IncludeStale      bool          // Include stale branches (no activity)
	StaleThreshold    time.Duration // Threshold for stale (default: 30 days)
	IncludeRemote     bool          // Include remote branches
	IncludeGone       bool          // Include local branches whose upstream is gone
	IncludeSuperseded bool          // Include unmerged bot remotes whose version already landed
	Exclude           []string      // Patterns to exclude
	BaseBranch        string        // Base branch for merge detection (default: main/master)
	BotsOnly          bool          // Restrict candidates to Dependabot/Renovate/github-actions prefixes

	// IncludeNonCanonical enables retirement of branches that duplicate the
	// declared canonical branch. It requires CanonicalBranch; without a
	// declaration there is nothing to measure "non-canonical" against and the
	// classification yields no candidates.
	IncludeNonCanonical bool

	// CanonicalBranch is the repository's declared integration branch, read from
	// .gz-git.yaml. It is never guessed: detectBaseBranch's name heuristics
	// answer "which branch looks like a trunk", not "which trunk did this
	// repository declare", and only the latter can justify retiring the other.
	CanonicalBranch string

	// TaskPatterns is the declared task-branch allow-list (.gz-git.yaml
	// taskPattern). Branches matching it belong to the reclaim path and are
	// never retired here, even when they are ancestors of the canonical branch.
	TaskPatterns []string

	// CanonicalRemote names the one remote the declaration speaks for. A
	// .gz-git.yaml describes its own repository, never a third party's: in a
	// fork checkout that also tracks `upstream`, `upstream/master` is an
	// ancestor of `upstream/develop` and would otherwise classify as
	// non-canonical, aiming a delete at the upstream project on the strength of
	// a declaration that never mentioned it. Remote candidates on any other
	// remote are skipped. Empty means origin.
	CanonicalRemote string
}

AnalyzeOptions configures branch cleanup analysis.

type Branch

type Branch struct {
	Name       string     // Branch name
	Ref        string     // Full ref (refs/heads/...)
	SHA        string     // Commit SHA
	IsHead     bool       // Currently checked out
	IsMerged   bool       // Fully merged into base branch
	IsRemote   bool       // Remote branch
	Upstream   string     // Upstream branch (if set)
	AheadBy    int        // Commits ahead of upstream
	BehindBy   int        // Commits behind upstream
	LastCommit *Commit    // Last commit on this branch
	CreatedAt  *time.Time // Creation time (if available)
	UpdatedAt  *time.Time // Last update time
}

Branch represents a Git branch with metadata.

type BranchManager

type BranchManager interface {
	// Create creates a new branch.
	Create(ctx context.Context, repo *repository.Repository, opts CreateOptions) error

	// Delete deletes a branch.
	Delete(ctx context.Context, repo *repository.Repository, opts DeleteOptions) error

	// List lists branches.
	List(ctx context.Context, repo *repository.Repository, opts ListOptions) ([]*Branch, error)

	// Get retrieves a specific branch by name.
	Get(ctx context.Context, repo *repository.Repository, name string) (*Branch, error)

	// Current returns the currently checked out branch.
	Current(ctx context.Context, repo *repository.Repository) (*Branch, error)

	// Exists checks if a branch exists.
	Exists(ctx context.Context, repo *repository.Repository, name string) (bool, error)
}

BranchManager manages Git branch operations.

func NewManager

func NewManager() BranchManager

NewManager creates a new BranchManager.

func NewManagerWithExecutor

func NewManagerWithExecutor(executor *gitcmd.Executor) BranchManager

NewManagerWithExecutor creates a new BranchManager with custom executor.

func NewManagerWithRemoteDeleteGuard

func NewManagerWithRemoteDeleteGuard(guard RemoteDeleteGuard) BranchManager

NewManagerWithRemoteDeleteGuard creates a branch manager that asks guard before mutating a remote branch. The guard is supplied by the caller that owns the access policy, keeping this package independent of workspace config.

type BranchType

type BranchType string

BranchType represents branch purpose/category.

const (
	BranchTypeFeature    BranchType = "feature"    // feature/*
	BranchTypeFix        BranchType = "fix"        // fix/*
	BranchTypeHotfix     BranchType = "hotfix"     // hotfix/*
	BranchTypeRelease    BranchType = "release"    // release/*
	BranchTypeExperiment BranchType = "experiment" // experiment/*
	BranchTypeOther      BranchType = "other"      // Unclassified
)

BranchType values classify a branch by its purpose.

func InferType

func InferType(name string) BranchType

InferType infers branch type from name.

type CleanupReport

type CleanupReport struct {
	Merged []*Branch // Fully merged branches
	Stale  []*Branch // Stale branches

	// Orphaned holds *local* branches whose upstream no longer exists — what git
	// marks `[gone]` and what the CLI calls `--gone`. These are refs under
	// refs/heads, so deleting one removes only the local ref; the branch it used
	// to track is already gone from the remote.
	Orphaned []*Branch

	// Superseded holds unmerged remote bot branches whose version target is
	// already satisfied on the base. Comparison is versions, not ancestry.
	Superseded []*Branch

	// NonCanonical holds branches that duplicate the declared canonical branch:
	// they carry no commit the canonical branch lacks, and they are neither the
	// canonical branch itself nor a declared task branch. This is the one bucket
	// permitted to contain built-in protected names (master, develop), because
	// the question it answers is which trunk this repository declared — not
	// whether the name looks like a trunk.
	NonCanonical []*Branch

	Protected []*Branch // Protected (won't delete)
	Total     int       // Total branches analyzed

	// Refused holds trunk-named branches that reached the non-canonical
	// ancestry gate and were turned away by it.
	//
	// It exists because "not a candidate" and "checked, and it still holds
	// commits" are different facts and the operator acts on them differently.
	// Without it the second one is reported as the first: RetirableTrunkNames
	// is a subset of ProtectedBranches, so a refused master falls through to
	// Protected and is labeled by the very name protection the operator
	// passed --non-canonical to overrule. True, and useless.
	//
	// A refusal is a diagnostic, not a candidate. It is not counted by
	// CountBranches and nothing here is ever deleted.
	Refused []RetireRefusal

	// Bases records, for each non-canonical candidate, the fact that authorized
	// it: which ref its ancestry was measured against and where that ref pointed.
	//
	// It is a sidecar rather than a field on Branch because Branch is the
	// vocabulary every classification shares, and this fact belongs to exactly
	// one of them. Keyed by the candidate's Ref, which is the spelling that
	// survives normalizeCleanupBranch collapsing local and remote copies onto
	// the same Name.
	Bases []RetireBasis
}

CleanupReport summarizes branches eligible for cleanup.

func (*CleanupReport) CountBranches

func (r *CleanupReport) CountBranches() int

CountBranches returns the total number of branches in the report.

func (*CleanupReport) GetAllBranches

func (r *CleanupReport) GetAllBranches() []*Branch

GetAllBranches returns all branches eligible for cleanup.

func (*CleanupReport) IsEmpty

func (r *CleanupReport) IsEmpty() bool

IsEmpty checks if the report has no branches to clean up.

type CleanupService

type CleanupService interface {
	// Analyze analyzes branches for cleanup.
	Analyze(ctx context.Context, repo *repository.Repository, opts AnalyzeOptions) (*CleanupReport, error)

	// Execute performs cleanup based on report. The result names which branches
	// were deleted and which were not; a non-nil error means the run never
	// started, not that nothing was deleted.
	Execute(ctx context.Context, repo *repository.Repository, report *CleanupReport, opts ExecuteOptions) (*ExecuteResult, error)
}

CleanupService analyzes and cleans up branches.

func NewCleanupService

func NewCleanupService() CleanupService

NewCleanupService creates a new CleanupService.

func NewCleanupServiceWithDeps

func NewCleanupServiceWithDeps(executor *gitcmd.Executor, branchManager BranchManager) CleanupService

NewCleanupServiceWithDeps creates a new CleanupService with custom dependencies.

func NewCleanupServiceWithRemoteDeleteGuard

func NewCleanupServiceWithRemoteDeleteGuard(guard RemoteDeleteGuard) CleanupService

NewCleanupServiceWithRemoteDeleteGuard creates a cleanup service that asks guard immediately before deleting a remote branch. A nil guard preserves the existing read-write behavior.

type CleanupStrategy

type CleanupStrategy string

CleanupStrategy defines cleanup approach.

const (
	StrategyMerged   CleanupStrategy = "merged"   // Only merged branches
	StrategyStale    CleanupStrategy = "stale"    // Only stale branches
	StrategyOrphaned CleanupStrategy = "orphaned" // Only orphaned branches
	StrategyAll      CleanupStrategy = "all"      // All eligible branches
)

CleanupStrategy values select which categories of branches to remove.

type Commit

type Commit struct {
	SHA      string
	Author   string
	Email    string
	Date     time.Time
	Message  string
	ShortMsg string // First line of message
}

Commit represents a Git commit with metadata.

type Conflict

type Conflict struct {
	File      string   // File path
	Worktrees []string // Worktrees modifying this file
	Severity  ConflictSeverity
}

Conflict represents a potential conflict across worktrees.

type ConflictSeverity

type ConflictSeverity string

ConflictSeverity indicates conflict severity.

const (
	SeverityLow    ConflictSeverity = "low"    // Different files
	SeverityMedium ConflictSeverity = "medium" // Same directory
	SeverityHigh   ConflictSeverity = "high"   // Same file
)

ConflictSeverity values indicate how severe a branch conflict is.

type CreateOptions

type CreateOptions struct {
	Name     string // Branch name (required)
	StartRef string // Starting ref (default: HEAD)
	Checkout bool   // Checkout after creation
	Track    bool   // Set upstream tracking
	Force    bool   // Overwrite existing branch
	Validate bool   // Validate naming conventions (default: true)
}

CreateOptions configures branch creation.

type DeleteFailure

type DeleteFailure struct {
	Branch string
	Err    error
}

DeleteFailure records a branch that Execute could not delete.

type DeleteOptions

type DeleteOptions struct {
	Name    string // Branch name (required)
	Remote  bool   // Delete remote branch
	Force   bool   // Force delete (even if unmerged)
	DryRun  bool   // Preview deletion
	Confirm bool   // Skip confirmation prompt
}

DeleteOptions configures branch deletion.

type ExecuteOptions

type ExecuteOptions struct {
	DryRun  bool     // Preview only, don't delete
	Force   bool     // Force delete unmerged branches
	Remote  bool     // Also delete remote branches
	Confirm bool     // Skip confirmation prompts
	Exclude []string // Additional patterns to exclude

	// CanonicalBranch re-arms the non-canonical gate inside Execute. Analyze
	// already screened report.NonCanonical, but CleanupReport is a public type
	// and callers may hand-assemble one, so Execute re-verifies ancestry against
	// this branch before it will bypass built-in protection for any candidate.
	// Empty means no candidate may bypass it.
	CanonicalBranch string

	// CanonicalRemote is the AnalyzeOptions field of the same name, re-armed
	// here for the same reason CanonicalBranch is: Execute re-verifies rather
	// than trusting a report a caller may have hand-assembled. Empty means
	// origin.
	CanonicalRemote string
}

ExecuteOptions configures branch cleanup execution.

type ExecuteResult

type ExecuteResult struct {
	Deleted []string        // Branches removed
	Failed  []DeleteFailure // Branches that could not be removed, and why
	Skipped []string        // Branches not attempted (protected / excluded)
}

ExecuteResult reports what a cleanup run actually did.

Execute deletes each branch independently and does not stop at the first failure, so neither "it returned an error" nor "it returned nil" describes the outcome. Deleted, Failed, and Skipped together do.

type Kind

type Kind string

Kind names the role a branch plays when more than one writer works on the same task: the shared work branch, one machine's private slice of it, or one agent's. The three differ only by name, which is exactly why the name has to be predictable — nothing else distinguishes them.

const (
	// KindWork is the branch a task lives on when it has a single writer.
	KindWork Kind = "work"
	// KindDevice is one machine's slice of a task worked from several machines.
	KindDevice Kind = "device"
	// KindAgent is one agent's slice, kept apart from any human's branch.
	KindAgent Kind = "agent"
)

func ParseKind

func ParseKind(value string) (Kind, error)

ParseKind reads a kind from a flag value. An empty value means the work branch: the single-writer case is the one that needs no explanation.

type ListOptions

type ListOptions struct {
	All      bool   // Include remote branches
	Merged   bool   // Only merged branches
	Unmerged bool   // Only unmerged branches
	Pattern  string // Name pattern filter
	Sort     SortBy // Sort order
	Limit    int    // Max results (0 = unlimited)
	Remote   string // Specific remote (empty = all)
}

ListOptions configures branch listing.

type Naming

type Naming struct {
	Work   string `yaml:"work,omitempty"`
	Device string `yaml:"device,omitempty"`
	Agent  string `yaml:"agent,omitempty"`
}

Naming holds one branch-name template per kind. An empty template falls back to the default for that kind, so a config may override one and leave the rest.

func (*Naming) Resolve

func (n *Naming) Resolve(kind Kind, task string, id identity.Identity) (string, error)

Resolve builds the branch name for a task from the template for its kind.

Every substituted value is slugified first. A device name defaults to the hostname, and a hostname is free to contain dots and capitals that a branch name is not, so the alternative is a template that works on one machine and fails on the next.

func (*Naming) Template

func (n *Naming) Template(kind Kind) (string, error)

Template returns the template for a kind, falling back to the default.

type ParallelStatus

type ParallelStatus struct {
	TotalWorktrees  int            // Total number of worktrees
	ActiveWorktrees int            // Worktrees with changes
	Conflicts       int            // Number of conflicts
	Contexts        []*WorkContext // All contexts
}

ParallelStatus represents status across all worktrees.

func (*ParallelStatus) GetActiveContexts

func (s *ParallelStatus) GetActiveContexts() []*WorkContext

GetActiveContexts returns contexts with uncommitted changes.

func (*ParallelStatus) GetMainContext

func (s *ParallelStatus) GetMainContext() *WorkContext

GetMainContext returns the main worktree context.

func (*ParallelStatus) HasConflicts

func (s *ParallelStatus) HasConflicts() bool

HasConflicts checks if there are any conflicts.

func (*ParallelStatus) IsActive

func (s *ParallelStatus) IsActive() bool

IsActive checks if any worktree has uncommitted changes.

type ParallelWorkflow

type ParallelWorkflow interface {
	// GetActiveContexts returns all active worktree contexts.
	GetActiveContexts(ctx context.Context, repo *repository.Repository) ([]*WorkContext, error)

	// SwitchContext provides information for switching to a worktree.
	SwitchContext(ctx context.Context, repo *repository.Repository, path string) (*SwitchInfo, error)

	// DetectConflicts detects potential conflicts across worktrees.
	DetectConflicts(ctx context.Context, repo *repository.Repository) ([]*Conflict, error)

	// GetStatus gets status across all worktrees.
	GetStatus(ctx context.Context, repo *repository.Repository) (*ParallelStatus, error)
}

ParallelWorkflow manages parallel development workflows.

func NewParallelWorkflow

func NewParallelWorkflow() ParallelWorkflow

NewParallelWorkflow creates a new ParallelWorkflow.

func NewParallelWorkflowWithDeps

func NewParallelWorkflowWithDeps(executor *gitcmd.Executor, worktreeManager WorktreeManager) ParallelWorkflow

NewParallelWorkflowWithDeps creates a new ParallelWorkflow with custom dependencies.

type RemoteDeleteGuard

type RemoteDeleteGuard func(ctx context.Context, repo *repository.Repository) error

RemoteDeleteGuard authorizes a remote branch deletion at the caller's boundary. pkg/branch deliberately does not resolve workspace configuration: callers that own an access policy inject it here instead.

type RemoveOptions

type RemoveOptions struct {
	Path  string // Worktree path (required)
	Force bool   // Force removal (even with uncommitted changes)
}

RemoveOptions configures worktree removal.

type RetireBasis

type RetireBasis struct {
	// Ref is the candidate's own ref, matching Branch.Ref.
	Ref string
	// TargetRef is the ref the ancestry was measured against, spelled in full
	// so that refs/heads/develop and refs/remotes/origin/develop are visibly
	// different things.
	TargetRef string
	// TargetSHA is where TargetRef pointed when the measurement was taken. On a
	// remote-tracking target it is a cache from the last fetch, which is the
	// whole reason to show it.
	TargetSHA string
}

RetireBasis is the evidence behind one non-canonical candidate.

The operator deciding whether to pass --force sees a branch name and nothing else, and since the local→remote fallback landed, the name no longer implies the target: a local master may have been measured against refs/heads/develop or against refs/remotes/origin/develop — another machine's branch, as of the last fetch — and those two carry materially different risk. This is what makes "an informed --force" possible, which is the premise --non-canonical rests on.

type RetireRefusal

type RetireRefusal struct {
	// Branch is the bare branch name, as the candidate listings spell it.
	Branch string
	// IsRemote distinguishes the remote-tracking copy from the local one; both
	// print the same bare name, and they are refused independently.
	IsRemote bool
	// Reason is a whole sentence, not a code: it is the entire remedy the
	// operator gets, and a code would send them back to the source to read it.
	Reason string
}

RetireRefusal is one trunk-named branch the non-canonical gate examined and declined, with the reason in the operator's terms.

type SortBy

type SortBy string

SortBy defines branch sorting order.

const (
	SortByName     SortBy = "name"     // Alphabetical by name
	SortByDate     SortBy = "date"     // Most recent first
	SortByAuthor   SortBy = "author"   // Alphabetical by author
	SortByUpstream SortBy = "upstream" // Group by upstream
)

SortBy values for ordering branch lists.

type SwitchInfo

type SwitchInfo struct {
	FromPath   string // Current location
	ToPath     string // Target worktree path
	ToBranch   string // Target branch
	Command    string // Suggested command (cd)
	HasChanges bool   // Target has uncommitted changes
}

SwitchInfo provides information for context switching.

type WorkContext

type WorkContext struct {
	Path          string   // Worktree path
	Branch        string   // Current branch
	IsMain        bool     // Is main worktree
	HasChanges    bool     // Has uncommitted changes
	ModifiedFiles []string // List of modified files
}

WorkContext represents a development context (worktree).

type Worktree

type Worktree struct {
	Path       string // Worktree path
	Branch     string // Branch name
	Ref        string // Full ref (HEAD or commit SHA)
	IsMain     bool   // Is the main worktree
	IsLocked   bool   // Is locked
	IsPrunable bool   // Can be pruned
	IsBare     bool   // Is bare repository
	IsDetached bool   // Is detached HEAD
}

Worktree represents a Git worktree.

type WorktreeManager

type WorktreeManager interface {
	// Add adds a new worktree.
	Add(ctx context.Context, repo *repository.Repository, opts AddOptions) (*Worktree, error)

	// Remove removes a worktree.
	Remove(ctx context.Context, repo *repository.Repository, opts RemoveOptions) error

	// List lists all worktrees.
	List(ctx context.Context, repo *repository.Repository) ([]*Worktree, error)

	// Prune removes orphaned worktree metadata.
	Prune(ctx context.Context, repo *repository.Repository) error

	// Get retrieves a specific worktree by path.
	Get(ctx context.Context, repo *repository.Repository, path string) (*Worktree, error)

	// Exists checks if a worktree exists at the given path.
	Exists(ctx context.Context, repo *repository.Repository, path string) (bool, error)
}

WorktreeManager manages Git worktree operations.

func NewWorktreeManager

func NewWorktreeManager() WorktreeManager

NewWorktreeManager creates a new WorktreeManager.

func NewWorktreeManagerWithExecutor

func NewWorktreeManagerWithExecutor(executor *gitcmd.Executor) WorktreeManager

NewWorktreeManagerWithExecutor creates a new WorktreeManager with custom executor.

Jump to

Keyboard shortcuts

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