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
- Variables
- func IsProtected(name string) bool
- type AddOptions
- type AnalyzeOptions
- type Branch
- type BranchManager
- type BranchType
- type CleanupReport
- type CleanupService
- type CleanupStrategy
- type Commit
- type Conflict
- type ConflictSeverity
- type CreateOptions
- type DeleteFailure
- type DeleteOptions
- type ExecuteOptions
- type ExecuteResult
- type Kind
- type ListOptions
- type Naming
- type ParallelStatus
- type ParallelWorkflow
- type RemoteDeleteGuard
- type RemoveOptions
- type RetireBasis
- type RetireRefusal
- type SortBy
- type SwitchInfo
- type WorkContext
- type Worktree
- type WorktreeManager
Constants ¶
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 ¶
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.
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 ¶
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 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.
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 ¶
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" )
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 ¶
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.
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 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.