twig

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jan 8, 2026 License: MIT Imports: 12 Imported by: 0

README

twig

A CLI tool that creates, deletes, and manages git worktrees and branches in a single command. Focused on simplifying git operations, keeping features minimal.

Design Philosophy

twig treats branches and worktrees as a single unified concept. Users don't need to think about whether they're managing a "branch" or a "worktree" - they simply work with named development contexts.

  • twig add feat/x creates both the branch and worktree together
  • twig remove feat/x deletes both together
  • Even if a worktree directory is deleted externally, twig remove still works

This 1:1 mapping simplifies the mental model: one name, one workspace, one command.

Motivation

twig is designed to be friendly for both humans and agentic coding tools, and to integrate easily with other CLI tools:

  • --quiet minimizes output to paths only, making it easy to pass to other tools
  • For human use, --verbose and interactive confirmations ensure safety

Examples:

cd $(twig add feat/x -q)            # cd into the created worktree
twig list -q | fzf                  # select a worktree with fzf
twig list -q | xargs -I {} code {}  # open all worktrees in VSCode
twig clean -v                       # confirm before deletion, show all skipped items

Features

Create worktree and branch in one command

twig add feat/xxx executes worktree creation, branch creation, and symlink setup all at once. Use --source to create from any branch regardless of current worktree. Set default_source in config to always branch from a fixed base (e.g., main).

Create new worktrees with personal settings like .envrc and Claude configs carried over. Start working immediately in new worktrees.

Move uncommitted changes to a new branch

Use --carry to move changes to a new worktree, or --sync to copy them to both. Use --file with --carry to move only specific files matching a glob pattern.

Examples:

  • Move refactoring ideas to a separate worktree and continue main work
  • Extract WIP changes to a new branch before switching tasks
Clean up worktrees no longer needed

twig clean removes worktrees that are merged, have upstream gone, or are prunable.

Installation

Requires Git 2.15+.

Homebrew
brew install 708u/tap/twig
Go
go install github.com/708u/twig/cmd/twig@latest

Shell Completion

Shell completion is available for all commands and flags. For example, twig remove <TAB> completes existing branch names.

Add the following to your shell configuration:

Bash
# Add to ~/.bashrc
eval "$(twig completion bash)"
Zsh
# Add to ~/.zshrc
eval "$(twig completion zsh)"
Fish
# Add to ~/.config/fish/config.fish
twig completion fish | source

Quick Start

# Initialize settings
twig init

# Create a new worktree and branch
twig add feat/new-feature

# Copy uncommitted changes to a new worktree
twig add feat/wip --sync

# Move uncommitted changes to a new worktree
twig add feat/wip --carry

# List worktrees
twig list

# Clean up worktrees no longer needed
twig clean

# Delete a specific worktree
twig remove feat/done

Configuration

Configure in .twig/settings.toml:

  • worktree_destination_base_dir: Destination directory for worktrees
  • default_source: Source branch for symlinks (creates symlinks from main even when adding from a derived worktree)
  • symlinks: Glob patterns for symlink targets

Personal settings can be overridden in .twig/settings.local.toml (.gitignore recommended).

  • extra_symlinks: Add personal patterns while preserving team settings

Details: docs/reference/configuration.md

Command Specs

Command Description
init Initialize settings
add Create worktree and branch
list List worktrees
remove Delete worktree and branch (multiple supported)
clean Bulk delete merged worktrees

See the documentation above for detailed flags and specifications.

License

MIT

Documentation

Index

Constants

View Source
const (
	GitCmdWorktree   = "worktree"
	GitCmdBranch     = "branch"
	GitCmdStash      = "stash"
	GitCmdStatus     = "status"
	GitCmdRevParse   = "rev-parse"
	GitCmdDiff       = "diff"
	GitCmdFetch      = "fetch"
	GitCmdForEachRef = "for-each-ref"
)

Git command names.

View Source
const (
	GitWorktreeAdd    = "add"
	GitWorktreeRemove = "remove"
	GitWorktreeList   = "list"
	GitWorktreePrune  = "prune"
)

Git worktree subcommands.

View Source
const (
	GitStashPush  = "push"
	GitStashApply = "apply"
	GitStashDrop  = "drop"
	GitStashList  = "list"
)

Git stash subcommands.

View Source
const (
	PorcelainWorktreePrefix = "worktree "
	PorcelainHEADPrefix     = "HEAD "
	PorcelainBranchPrefix   = "branch refs/heads/"
	PorcelainDetached       = "detached"
	PorcelainBare           = "bare"
	PorcelainLocked         = "locked"
	PorcelainPrunable       = "prunable"
)

Porcelain output format prefixes and values.

View Source
const RefsHeadsPrefix = "refs/heads/"

RefsHeadsPrefix is the git refs prefix for local branches.

Variables

This section is empty.

Functions

This section is empty.

Types

type AddCommand

type AddCommand struct {
	FS           FileSystem
	Git          *GitRunner
	Config       *Config
	Sync         bool
	CarryFrom    string
	FilePatterns []string
	Lock         bool
	LockReason   string
}

AddCommand creates git worktrees with symlinks.

func NewAddCommand

func NewAddCommand(fs FileSystem, git *GitRunner, cfg *Config, opts AddOptions) *AddCommand

NewAddCommand creates an AddCommand with explicit dependencies (for testing).

func NewDefaultAddCommand

func NewDefaultAddCommand(cfg *Config, opts AddOptions) *AddCommand

NewDefaultAddCommand creates an AddCommand with production defaults.

func (*AddCommand) Run

func (c *AddCommand) Run(name string) (AddResult, error)

Run creates a new worktree for the given branch name.

type AddFormatOptions

type AddFormatOptions struct {
	Verbose bool
	Quiet   bool
}

AddFormatOptions configures add output formatting.

type AddOptions

type AddOptions struct {
	Sync         bool
	CarryFrom    string   // empty: no carry, non-empty: resolved path to carry from
	FilePatterns []string // file patterns to carry (empty means all files)
	Lock         bool
	LockReason   string
}

AddOptions holds options for the add command.

type AddResult

type AddResult struct {
	Branch         string
	WorktreePath   string
	Symlinks       []SymlinkResult
	GitOutput      []byte
	ChangesSynced  bool
	ChangesCarried bool
}

AddResult holds the result of an add operation.

func (AddResult) Format

func (r AddResult) Format(opts AddFormatOptions) FormatResult

Format formats the AddResult for display.

type BranchDeleteOption

type BranchDeleteOption func(*branchDeleteOptions)

BranchDeleteOption is a functional option for BranchDelete.

func WithForceDelete

func WithForceDelete() BranchDeleteOption

WithForceDelete forces branch deletion even if not fully merged.

type CleanCandidate

type CleanCandidate struct {
	Branch       string
	WorktreePath string
	Prunable     bool
	Skipped      bool
	SkipReason   SkipReason
	CleanReason  CleanReason
}

CleanCandidate represents a worktree that can be cleaned.

type CleanCommand

type CleanCommand struct {
	FS     FileSystem
	Git    *GitRunner
	Config *Config
}

CleanCommand removes merged worktrees that are no longer needed.

func NewCleanCommand

func NewCleanCommand(fs FileSystem, git *GitRunner, cfg *Config) *CleanCommand

NewCleanCommand creates a new CleanCommand with explicit dependencies. Use this for testing or when custom dependencies are needed.

func NewDefaultCleanCommand

func NewDefaultCleanCommand(cfg *Config) *CleanCommand

NewDefaultCleanCommand creates a new CleanCommand with production dependencies.

func (*CleanCommand) Run

func (c *CleanCommand) Run(cwd string, opts CleanOptions) (CleanResult, error)

Run analyzes worktrees and optionally removes them. cwd is the current working directory (absolute path) passed from CLI layer.

type CleanOptions

type CleanOptions struct {
	Yes     bool               // Execute without confirmation
	Check   bool               // Show candidates only (no prompt)
	Target  string             // Target branch for merge check (auto-detect if empty)
	Verbose bool               // Show skip reasons
	Force   WorktreeForceLevel // Force level: -f for unclean, -ff for locked
}

CleanOptions configures the clean operation.

type CleanReason added in v0.2.0

type CleanReason string

CleanReason describes why a branch is cleanable.

const (
	CleanMerged       CleanReason = "merged"
	CleanUpstreamGone CleanReason = "upstream gone"
)

type CleanResult

type CleanResult struct {
	Candidates   []CleanCandidate
	Removed      []RemovedWorktree
	TargetBranch string
	Pruned       bool
	Check        bool // --check mode (show candidates only, no prompt)
}

CleanResult aggregates results from clean operations.

func (CleanResult) CleanableCount

func (r CleanResult) CleanableCount() int

CleanableCount returns the number of worktrees that can be cleaned.

func (CleanResult) Format

func (r CleanResult) Format(opts FormatOptions) FormatResult

Format formats the CleanResult for display.

type Config

type Config struct {
	Symlinks            []string `toml:"symlinks"`
	ExtraSymlinks       []string `toml:"extra_symlinks"`
	WorktreeDestBaseDir string   `toml:"worktree_destination_base_dir"`
	DefaultSource       string   `toml:"default_source"`
	WorktreeSourceDir   string   // Set by LoadConfig to the config load directory
}

Config holds the merged configuration for the application. All path fields are resolved to absolute paths by LoadConfig.

type FileSystem

type FileSystem interface {
	Stat(name string) (fs.FileInfo, error)
	Symlink(oldname, newname string) error
	IsNotExist(err error) bool
	Glob(dir, pattern string) ([]string, error)
	MkdirAll(path string, perm fs.FileMode) error
	ReadDir(name string) ([]os.DirEntry, error)
	Remove(name string) error
	WriteFile(name string, data []byte, perm fs.FileMode) error
}

FileSystem abstracts filesystem operations for testability.

type FormatOptions

type FormatOptions struct {
	Verbose bool
}

FormatOptions configures output formatting.

type FormatResult

type FormatResult struct {
	Stdout string
	Stderr string
}

FormatResult holds formatted output strings.

type Formatter

type Formatter interface {
	Format(opts FormatOptions) FormatResult
}

Formatter formats command results.

type GitError

type GitError struct {
	Op     GitOp
	Stderr string
	Err    error
}

GitError represents an error from a git operation with structured information.

func (*GitError) Error

func (e *GitError) Error() string

func (*GitError) Hint

func (e *GitError) Hint() string

Hint returns a helpful hint message based on the error content.

func (*GitError) Unwrap

func (e *GitError) Unwrap() error

type GitExecutor

type GitExecutor interface {
	// Run executes git with args and returns stdout.
	Run(args ...string) ([]byte, error)
}

GitExecutor abstracts git command execution for testability. Commands are fixed to "git" - only subcommands and args are passed.

type GitOp

type GitOp int

GitOp represents the type of git operation.

const (
	OpWorktreeRemove GitOp = iota + 1
	OpBranchDelete
)

func (GitOp) String

func (op GitOp) String() string

type GitRunner

type GitRunner struct {
	Executor GitExecutor
	Dir      string
}

GitRunner provides git operations using GitExecutor.

func NewGitRunner

func NewGitRunner(dir string) *GitRunner

NewGitRunner creates a new GitRunner with the default executor.

func (*GitRunner) BranchDelete

func (g *GitRunner) BranchDelete(branch string, opts ...BranchDeleteOption) ([]byte, error)

BranchDelete deletes a local branch. By default uses -d (safe delete). Use WithForceDelete() to use -D (force delete).

func (*GitRunner) BranchList

func (g *GitRunner) BranchList() ([]string, error)

BranchList returns all local branch names.

func (*GitRunner) ChangedFiles

func (g *GitRunner) ChangedFiles() ([]string, error)

ChangedFiles returns a list of files with uncommitted changes including staged, unstaged, and untracked files.

func (*GitRunner) Fetch added in v0.3.0

func (g *GitRunner) Fetch(remote string, refspec ...string) error

Fetch fetches the specified refspec from the remote.

func (*GitRunner) FindRemoteForBranch added in v0.3.0

func (g *GitRunner) FindRemoteForBranch(branch string) (string, error)

FindRemoteForBranch finds the remote that has the specified branch. Returns the remote name if exactly one remote has the branch. Returns empty string if no remote has the branch. Returns error if multiple remotes have the branch (ambiguous).

func (*GitRunner) FindRemotesForBranch added in v0.3.0

func (g *GitRunner) FindRemotesForBranch(branch string) []string

FindRemotesForBranch returns all remotes that have the specified branch in local remote-tracking branches. This checks refs/remotes/*/<branch> locally without network access.

func (*GitRunner) HasChanges

func (g *GitRunner) HasChanges() (bool, error)

HasChanges checks if there are any uncommitted changes (staged, unstaged, or untracked).

func (*GitRunner) InDir

func (g *GitRunner) InDir(dir string) *GitRunner

InDir returns a GitRunner that executes commands in the specified directory.

func (*GitRunner) IsBranchMerged

func (g *GitRunner) IsBranchMerged(branch, target string) (bool, error)

IsBranchMerged checks if branch is merged into target. First checks using git branch --merged (detects traditional merges). If not found, falls back to checking if upstream is gone (squash/rebase merges).

func (*GitRunner) IsBranchUpstreamGone added in v0.2.0

func (g *GitRunner) IsBranchUpstreamGone(branch string) (bool, error)

IsBranchUpstreamGone checks if the branch's upstream tracking branch is gone. This indicates the remote branch was deleted, typically after a PR merge.

func (*GitRunner) LocalBranchExists added in v0.3.0

func (g *GitRunner) LocalBranchExists(branch string) bool

LocalBranchExists checks if a branch exists in the local repository.

func (*GitRunner) Run

func (g *GitRunner) Run(args ...string) ([]byte, error)

Run executes git command with -C flag.

func (*GitRunner) StashApplyByHash

func (g *GitRunner) StashApplyByHash(hash string) ([]byte, error)

StashApplyByHash applies the stash with the given hash without dropping it.

func (*GitRunner) StashDropByHash

func (g *GitRunner) StashDropByHash(hash string) ([]byte, error)

StashDropByHash drops the stash with the given hash.

func (*GitRunner) StashPopByHash

func (g *GitRunner) StashPopByHash(hash string) ([]byte, error)

StashPopByHash applies and drops the stash with the given hash.

func (*GitRunner) StashPush

func (g *GitRunner) StashPush(message string, pathspecs ...string) (string, error)

StashPush stashes changes including untracked files. If pathspecs are provided, only matching files are stashed. Returns the stash commit hash for later reference.

Race condition note: There is a small race window between "stash push" and "rev-parse stash@{0}". If another process creates a stash in this window, we may get the wrong hash. However, this window is very small (milliseconds) and acceptable in practice.

Why not use "stash create" + "stash store" pattern? "stash create" does not support -u/--include-untracked option (git limitation). It can only stash tracked file changes, not untracked files. See: https://git-scm.com/docs/git-stash

func (*GitRunner) WorktreeAdd

func (g *GitRunner) WorktreeAdd(path, branch string, opts ...WorktreeAddOption) ([]byte, error)

WorktreeAdd creates a new worktree at the specified path.

func (*GitRunner) WorktreeFindByBranch

func (g *GitRunner) WorktreeFindByBranch(branch string) (*Worktree, error)

WorktreeFindByBranch returns the Worktree for the given branch. Returns an error if the branch is not checked out in any worktree.

func (*GitRunner) WorktreeList

func (g *GitRunner) WorktreeList() ([]Worktree, error)

WorktreeList returns all worktrees with their paths and branches.

func (*GitRunner) WorktreeListBranches

func (g *GitRunner) WorktreeListBranches() ([]string, error)

WorktreeListBranches returns a list of branch names currently checked out in worktrees.

func (*GitRunner) WorktreePrune

func (g *GitRunner) WorktreePrune() ([]byte, error)

WorktreePrune removes references to worktrees that no longer exist.

func (*GitRunner) WorktreeRemove

func (g *GitRunner) WorktreeRemove(path string, opts ...WorktreeRemoveOption) ([]byte, error)

WorktreeRemove removes the worktree at the given path. By default fails if there are uncommitted changes. Use WithForceRemove() to force.

type InitCommand

type InitCommand struct {
	FS FileSystem
}

InitCommand initializes twig configuration in a directory.

func NewDefaultInitCommand

func NewDefaultInitCommand() *InitCommand

NewDefaultInitCommand creates an InitCommand with production defaults.

func NewInitCommand

func NewInitCommand(fs FileSystem) *InitCommand

NewInitCommand creates an InitCommand with explicit dependencies (for testing).

func (*InitCommand) Run

func (c *InitCommand) Run(dir string, opts InitOptions) (InitResult, error)

Run executes the init command.

type InitFormatOptions

type InitFormatOptions struct {
	Verbose bool
}

InitFormatOptions holds formatting options for InitResult.

type InitOptions

type InitOptions struct {
	Force bool
}

InitOptions holds options for the init command.

type InitResult

type InitResult struct {
	ConfigDir    string
	SettingsPath string
	Created      bool
	Skipped      bool
	Overwritten  bool
}

InitResult holds the result of the init command.

func (InitResult) Format

func (r InitResult) Format(opts InitFormatOptions) FormatResult

Format formats the result for output.

type ListCommand

type ListCommand struct {
	Git *GitRunner
}

ListCommand lists all worktrees.

func NewDefaultListCommand

func NewDefaultListCommand(dir string) *ListCommand

NewDefaultListCommand creates a ListCommand with production defaults.

func NewListCommand

func NewListCommand(git *GitRunner) *ListCommand

NewListCommand creates a ListCommand with explicit dependencies (for testing).

func (*ListCommand) Run

func (c *ListCommand) Run() (ListResult, error)

Run lists all worktrees.

type ListFormatOptions

type ListFormatOptions struct {
	Quiet bool
}

ListFormatOptions configures list output formatting.

type ListResult

type ListResult struct {
	Worktrees []Worktree
}

ListResult holds the result of a list operation.

func (ListResult) Format

func (r ListResult) Format(opts ListFormatOptions) FormatResult

Format formats the ListResult for display.

type LoadConfigResult

type LoadConfigResult struct {
	Config   *Config
	Warnings []string
}

LoadConfigResult contains the loaded config and any warnings.

func LoadConfig

func LoadConfig(dir string) (*LoadConfigResult, error)

type RemoveCommand

type RemoveCommand struct {
	FS     FileSystem
	Git    *GitRunner
	Config *Config
}

RemoveCommand removes git worktrees with their associated branches.

func NewDefaultRemoveCommand

func NewDefaultRemoveCommand(cfg *Config) *RemoveCommand

NewDefaultRemoveCommand creates a RemoveCommand with production defaults.

func NewRemoveCommand

func NewRemoveCommand(fs FileSystem, git *GitRunner, cfg *Config) *RemoveCommand

NewRemoveCommand creates a RemoveCommand with explicit dependencies.

func (*RemoveCommand) Run

func (c *RemoveCommand) Run(branch string, cwd string, opts RemoveOptions) (RemovedWorktree, error)

Run removes the worktree and branch for the given branch name. cwd is used to prevent removal when inside the target worktree.

type RemoveOptions

type RemoveOptions struct {
	// Force specifies the force level.
	// Matches git worktree behavior: -f for unclean, -f -f for locked.
	Force  WorktreeForceLevel
	DryRun bool
}

RemoveOptions configures the remove operation.

type RemoveResult

type RemoveResult struct {
	Removed []RemovedWorktree
}

RemoveResult aggregates results from remove operations.

func (RemoveResult) ErrorCount

func (r RemoveResult) ErrorCount() int

ErrorCount returns the number of failed removals.

func (RemoveResult) Format

func (r RemoveResult) Format(opts FormatOptions) FormatResult

Format formats the RemoveResult for display.

func (RemoveResult) HasErrors

func (r RemoveResult) HasErrors() bool

HasErrors returns true if any errors occurred.

type RemovedWorktree

type RemovedWorktree struct {
	Branch       string
	WorktreePath string
	CleanedDirs  []string // Empty parent directories that were removed
	Pruned       bool     // Stale worktree record was pruned (directory was already deleted)
	DryRun       bool
	GitOutput    []byte
	Err          error // nil if success
}

RemovedWorktree holds the result of a single worktree removal.

func (RemovedWorktree) Format

Format formats the RemovedWorktree for display.

type SkipReason

type SkipReason string

SkipReason describes why a worktree was skipped.

const (
	SkipNotMerged  SkipReason = "not merged"
	SkipHasChanges SkipReason = "has uncommitted changes"
	SkipLocked     SkipReason = "locked"
	SkipCurrentDir SkipReason = "current directory"
	SkipDetached   SkipReason = "detached HEAD"
)

type SymlinkResult

type SymlinkResult struct {
	Src     string
	Dst     string
	Skipped bool
	Reason  string
}

SymlinkResult holds information about a symlink operation.

type Worktree added in v0.2.0

type Worktree struct {
	Path           string
	Branch         string
	HEAD           string
	Detached       bool
	Locked         bool
	LockReason     string
	Prunable       bool
	PrunableReason string
	Bare           bool
}

Worktree holds worktree path and branch information.

func (Worktree) ShortHEAD added in v0.2.0

func (w Worktree) ShortHEAD() string

ShortHEAD returns the first 7 characters of the HEAD commit hash.

type WorktreeAddOption

type WorktreeAddOption func(*worktreeAddOptions)

WorktreeAddOption is a functional option for WorktreeAdd.

func WithCreateBranch

func WithCreateBranch() WorktreeAddOption

WithCreateBranch creates a new branch when adding the worktree.

func WithLock

func WithLock() WorktreeAddOption

WithLock locks the worktree after creation.

func WithLockReason

func WithLockReason(reason string) WorktreeAddOption

WithLockReason sets the reason for locking the worktree.

type WorktreeForceLevel

type WorktreeForceLevel uint8

WorktreeForceLevel represents the force level for worktree removal. Matches git worktree remove behavior.

const (
	// WorktreeForceLevelNone means no force - fail on uncommitted changes or locked.
	WorktreeForceLevelNone WorktreeForceLevel = iota
	// WorktreeForceLevelUnclean removes unclean worktrees (-f).
	WorktreeForceLevelUnclean
	// WorktreeForceLevelLocked also removes locked worktrees (-f -f).
	WorktreeForceLevelLocked
)

type WorktreeRemoveOption

type WorktreeRemoveOption func(*worktreeRemoveOptions)

WorktreeRemoveOption is a functional option for WorktreeRemove.

func WithForceRemove

func WithForceRemove(level WorktreeForceLevel) WorktreeRemoveOption

WithForceRemove forces worktree removal.

Directories

Path Synopsis
cmd
twig command
internal

Jump to

Keyboard shortcuts

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