or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

config.mdindex.mdobjects.mdplumbing.mdreferences.mdremote-operations.mdrepository-operations.mdstorage.mdtransport.mdworktree-operations.md
tile.json

worktree-operations.mddocs/

Worktree Operations

This document covers all operations for working with the worktree (working directory) in go-git, including file management, staging, committing, and checkout operations.

Overview

The Worktree represents the working directory of a Git repository. It provides methods for:

  • Checking file status (modified, staged, untracked)
  • Adding and removing files from the staging area
  • Creating commits
  • Checking out branches and commits
  • Resetting worktree state
  • Cleaning untracked files

Worktree Type

type Worktree struct {
    Filesystem billy.Filesystem
    Excludes   []gitignore.Pattern
    // contains filtered or unexported fields
}

Getting the Worktree

func (r *Repository) Worktree() (*Worktree, error)

Example:

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Now you can perform worktree operations
    _ = w
}

Note: This will return ErrIsBareRepository for bare repositories.

Checking Status

Status - Get Worktree Status

func (w *Worktree) Status() (Status, error)

type Status map[string]*FileStatus

type FileStatus struct {
    Staging  StatusCode
    Worktree StatusCode
    Extra    string
}

type StatusCode byte

Status Codes:

const (
    Unmodified         StatusCode = ' '
    Untracked          StatusCode = '?'
    Modified           StatusCode = 'M'
    Added              StatusCode = 'A'
    Deleted            StatusCode = 'D'
    Renamed            StatusCode = 'R'
    Copied             StatusCode = 'C'
    UpdatedButUnmerged StatusCode = 'U'
)

Example:

package main

import (
    "fmt"
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Get status
    status, err := w.Status()
    if err != nil {
        panic(err)
    }

    // Check if worktree is clean
    if status.IsClean() {
        fmt.Println("Worktree is clean")
    } else {
        fmt.Println("Modified files:")
        for file, stat := range status {
            if stat.Worktree != git.Unmodified {
                fmt.Printf("  %s (worktree: %c, staging: %c)\n",
                    file, stat.Worktree, stat.Staging)
            }
        }
    }

    // Check specific file
    fileStat := status.File("README.md")
    if fileStat.Worktree == git.Modified {
        fmt.Println("README.md is modified")
    }

    // Check if file is untracked
    if status.IsUntracked("newfile.txt") {
        fmt.Println("newfile.txt is untracked")
    }
}

Status Methods

func (s Status) IsClean() bool
func (s Status) File(path string) *FileStatus
func (s Status) IsUntracked(path string) bool
func (s Status) String() string

Adding Files

Add - Add Single File

func (w *Worktree) Add(path string) (plumbing.Hash, error)

Add a file to the staging area.

Example:

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Create a new file
    filename := filepath.Join("/tmp/repo", "README.md")
    err = os.WriteFile(filename, []byte("# My Project"), 0644)
    if err != nil {
        panic(err)
    }

    // Add file to staging area
    hash, err := w.Add("README.md")
    if err != nil {
        panic(err)
    }

    fmt.Println("Added file with hash:", hash.String())
}

AddGlob - Add Files by Pattern

func (w *Worktree) AddGlob(pattern string) error

Add files matching a glob pattern.

Example:

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Add all .go files
    err = w.AddGlob("*.go")
    if err != nil {
        panic(err)
    }

    // Add all files in docs directory
    err = w.AddGlob("docs/*")
    if err != nil {
        panic(err)
    }

    // Add all files recursively
    err = w.AddGlob(".")
    if err != nil {
        panic(err)
    }
}

AddWithOptions - Add with Options

func (w *Worktree) AddWithOptions(opts *AddOptions) error

type AddOptions struct {
    All        bool
    Path       string
    Glob       string
    SkipStatus bool
}

Example:

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Add all modified and new files (like git add -A)
    err = w.AddWithOptions(&git.AddOptions{
        All: true,
    })
    if err != nil {
        panic(err)
    }

    // Add specific path
    err = w.AddWithOptions(&git.AddOptions{
        Path: "src/main.go",
    })
    if err != nil {
        panic(err)
    }

    // Add by glob pattern
    err = w.AddWithOptions(&git.AddOptions{
        Glob: "*.md",
    })
    if err != nil {
        panic(err)
    }
}

Removing Files

Remove - Remove Single File

func (w *Worktree) Remove(path string) (plumbing.Hash, error)

Remove a file from the worktree and staging area.

Example:

package main

import (
    "fmt"
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Remove file
    hash, err := w.Remove("oldfile.txt")
    if err != nil {
        panic(err)
    }

    fmt.Println("Removed file with hash:", hash.String())
}

RemoveGlob - Remove Files by Pattern

func (w *Worktree) RemoveGlob(pattern string) error

Example:

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Remove all .tmp files
    err = w.RemoveGlob("*.tmp")
    if err != nil {
        panic(err)
    }
}

Move - Move/Rename File

func (w *Worktree) Move(from, to string) (plumbing.Hash, error)

Example:

package main

import (
    "fmt"
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Rename file
    hash, err := w.Move("old-name.txt", "new-name.txt")
    if err != nil {
        panic(err)
    }

    fmt.Println("Moved file with hash:", hash.String())
}

Creating Commits

Commit - Create Commit

func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error)

type CommitOptions struct {
    All               bool
    AllowEmptyCommits bool
    Author            *object.Signature
    Committer         *object.Signature
    Parents           []plumbing.Hash
    SignKey           *openpgp.Entity
    Signer            *Signer
    Amend             bool
}

Example:

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "time"
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing/object"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Create a file
    filename := filepath.Join("/tmp/repo", "example.txt")
    err = os.WriteFile(filename, []byte("Hello World"), 0644)
    if err != nil {
        panic(err)
    }

    // Add to staging
    _, err = w.Add("example.txt")
    if err != nil {
        panic(err)
    }

    // Create commit
    commit, err := w.Commit("Add example file", &git.CommitOptions{
        Author: &object.Signature{
            Name:  "John Doe",
            Email: "john@doe.org",
            When:  time.Now(),
        },
    })
    if err != nil {
        panic(err)
    }

    // Get commit object
    obj, err := r.CommitObject(commit)
    if err != nil {
        panic(err)
    }

    fmt.Println(obj)
}

Commit with All Option

Automatically stage all modified tracked files:

package main

import (
    "time"
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing/object"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Commit all modified files (like git commit -a)
    commit, err := w.Commit("Update all files", &git.CommitOptions{
        All: true,
        Author: &object.Signature{
            Name:  "John Doe",
            Email: "john@doe.org",
            When:  time.Now(),
        },
    })
    if err != nil {
        panic(err)
    }

    _ = commit
}

Empty Commits

package main

import (
    "time"
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing/object"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Create empty commit (like git commit --allow-empty)
    commit, err := w.Commit("Empty commit", &git.CommitOptions{
        AllowEmptyCommits: true,
        Author: &object.Signature{
            Name:  "John Doe",
            Email: "john@doe.org",
            When:  time.Now(),
        },
    })
    if err != nil {
        panic(err)
    }

    _ = commit
}

Amending Commits

package main

import (
    "time"
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing/object"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Amend the previous commit
    commit, err := w.Commit("Updated commit message", &git.CommitOptions{
        Amend: true,
        Author: &object.Signature{
            Name:  "John Doe",
            Email: "john@doe.org",
            When:  time.Now(),
        },
    })
    if err != nil {
        panic(err)
    }

    _ = commit
}

Checkout Operations

Checkout - Checkout Branch or Commit

func (w *Worktree) Checkout(opts *CheckoutOptions) error

type CheckoutOptions struct {
    Hash                      plumbing.Hash
    Branch                    plumbing.ReferenceName
    Create                    bool
    Force                     bool
    Keep                      bool
    SparseCheckoutDirectories []string
}

Example - Checkout Branch:

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Checkout existing branch
    err = w.Checkout(&git.CheckoutOptions{
        Branch: plumbing.ReferenceName("refs/heads/develop"),
    })
    if err != nil {
        panic(err)
    }
}

Example - Create and Checkout New Branch:

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Create and checkout new branch (like git checkout -b feature)
    err = w.Checkout(&git.CheckoutOptions{
        Branch: plumbing.ReferenceName("refs/heads/feature"),
        Create: true,
    })
    if err != nil {
        panic(err)
    }
}

Example - Checkout Specific Commit:

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Checkout specific commit (detached HEAD)
    err = w.Checkout(&git.CheckoutOptions{
        Hash: plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"),
    })
    if err != nil {
        panic(err)
    }
}

Example - Force Checkout:

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Force checkout, discarding local changes
    err = w.Checkout(&git.CheckoutOptions{
        Branch: plumbing.ReferenceName("refs/heads/main"),
        Force:  true,
    })
    if err != nil {
        panic(err)
    }
}

Example - Sparse Checkout:

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Sparse checkout - only checkout specific directories
    err = w.Checkout(&git.CheckoutOptions{
        Branch: plumbing.ReferenceName("refs/heads/main"),
        SparseCheckoutDirectories: []string{"docs", "src"},
    })
    if err != nil {
        panic(err)
    }
}

Reset Operations

Reset - Reset Worktree

func (w *Worktree) Reset(opts *ResetOptions) error

type ResetOptions struct {
    Commit plumbing.Hash
    Mode   ResetMode
    Files  []string
}

type ResetMode int

const (
    MixedReset  ResetMode = iota // Default
    HardReset                     // Reset worktree and index
    MergeReset                    // Keep worktree changes
    SoftReset                     // Only move HEAD
)

Example - Soft Reset:

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Soft reset - move HEAD but keep changes staged
    err = w.Reset(&git.ResetOptions{
        Commit: plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"),
        Mode:   git.SoftReset,
    })
    if err != nil {
        panic(err)
    }
}

Example - Hard Reset:

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Hard reset - discard all changes
    err = w.Reset(&git.ResetOptions{
        Commit: plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"),
        Mode:   git.HardReset,
    })
    if err != nil {
        panic(err)
    }
}

Example - Reset Specific Files:

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Reset specific files to HEAD
    ref, err := r.Head()
    if err != nil {
        panic(err)
    }

    err = w.Reset(&git.ResetOptions{
        Commit: ref.Hash(),
        Files:  []string{"README.md", "main.go"},
    })
    if err != nil {
        panic(err)
    }
}

Pull Operations

Pull - Pull from Remote

func (w *Worktree) Pull(o *PullOptions) error
func (w *Worktree) PullContext(ctx context.Context, o *PullOptions) error

See Remote Operations for detailed pull documentation.

Quick Example:

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Pull latest changes
    err = w.Pull(&git.PullOptions{RemoteName: "origin"})
    if err != nil && err != git.NoErrAlreadyUpToDate {
        panic(err)
    }
}

Other Operations

Clean - Remove Untracked Files

func (w *Worktree) Clean(opts *CleanOptions) error

type CleanOptions struct {
    Dir   bool
    Flags CleanStatusFlags
}

type CleanStatusFlags uint

const (
    CleanUntracked CleanStatusFlags = 1 << iota
    CleanIgnored
)

Example:

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Clean untracked files
    err = w.Clean(&git.CleanOptions{
        Dir:   true,
        Flags: git.CleanUntracked,
    })
    if err != nil {
        panic(err)
    }
}

Restore - Restore Files

func (w *Worktree) Restore(opts *RestoreOptions) error

type RestoreOptions struct {
    Files    []string
    Hash     plumbing.Hash
    Branch   plumbing.ReferenceName
    Staged   bool
    Worktree bool
}

Example:

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Restore specific files from HEAD
    err = w.Restore(&git.RestoreOptions{
        Files:    []string{"README.md"},
        Worktree: true,
    })
    if err != nil {
        panic(err)
    }
}

Grep - Search Files

func (w *Worktree) Grep(opts *GrepOptions) ([]GrepResult, error)

type GrepOptions struct {
    Patterns      []*regexp.Regexp
    InvertMatch   bool
    CommitHash    plumbing.Hash
    ReferenceName plumbing.ReferenceName
    PathSpecs     []string
}

type GrepResult struct {
    FileName   string
    LineNumber int
    Content    string
    TreeName   string
}

Example:

package main

import (
    "fmt"
    "regexp"
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Search for pattern
    pattern := regexp.MustCompile("TODO")
    results, err := w.Grep(&git.GrepOptions{
        Patterns: []*regexp.Regexp{pattern},
    })
    if err != nil {
        panic(err)
    }

    for _, result := range results {
        fmt.Printf("%s:%d: %s\n", result.FileName, result.LineNumber, result.Content)
    }
}

Submodules

Submodule - Get Submodule

func (w *Worktree) Submodule(name string) (*Submodule, error)
func (w *Worktree) Submodules() (Submodules, error)

Example:

package main

import (
    "fmt"
    "github.com/go-git/go-git/v5"
)

func main() {
    r, err := git.PlainOpen("/tmp/repo")
    if err != nil {
        panic(err)
    }

    w, err := r.Worktree()
    if err != nil {
        panic(err)
    }

    // Get all submodules
    submodules, err := w.Submodules()
    if err != nil {
        panic(err)
    }

    // Initialize and update all submodules
    err = submodules.Init()
    if err != nil {
        panic(err)
    }

    err = submodules.Update(&git.SubmoduleUpdateOptions{
        Init: true,
    })
    if err != nil {
        panic(err)
    }

    fmt.Printf("Updated %d submodules\n", len(submodules))
}

Common Errors

var (
    ErrUnableToAddFile      = errors.New("unable to add file")
    ErrUnableToStageFile    = errors.New("unable to stage file")
    ErrUnableToUnstageFile  = errors.New("unable to unstage file")
    ErrUnableToCheckout     = errors.New("unable to checkout")
    ErrUnstagedChanges      = errors.New("worktree contains unstaged changes")
    ErrGlobNoMatches        = errors.New("glob pattern did not match any files")
    ErrForceNeeded          = errors.New("force is required")
    ErrEmptyCommit          = errors.New("empty commit")
    ErrSubmoduleNotFound    = errors.New("submodule not found")
)

Best Practices

1. Always Check Status Before Operations

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, _ := git.PlainOpen("/tmp/repo")
    w, _ := r.Worktree()

    status, err := w.Status()
    if err != nil {
        panic(err)
    }

    if !status.IsClean() {
        println("Warning: worktree has uncommitted changes")
    }
}

2. Use All Option for Convenience

package main

import (
    "time"
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing/object"
)

func main() {
    r, _ := git.PlainOpen("/tmp/repo")
    w, _ := r.Worktree()

    // Automatically stage all modified files
    _, _ = w.Commit("Quick update", &git.CommitOptions{
        All: true,
        Author: &object.Signature{
            Name:  "Auto Bot",
            Email: "bot@example.com",
            When:  time.Now(),
        },
    })
}

3. Handle NoErrAlreadyUpToDate

package main

import (
    "github.com/go-git/go-git/v5"
)

func main() {
    r, _ := git.PlainOpen("/tmp/repo")
    w, _ := r.Worktree()

    err := w.Pull(&git.PullOptions{RemoteName: "origin"})
    if err == git.NoErrAlreadyUpToDate {
        println("Already up to date")
    } else if err != nil {
        panic(err)
    }
}

4. Use Force Carefully

package main

import (
    "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func main() {
    r, _ := git.PlainOpen("/tmp/repo")
    w, _ := r.Worktree()

    // Check for changes first
    status, _ := w.Status()
    if !status.IsClean() {
        println("Warning: This will discard local changes")
    }

    // Force checkout
    _ = w.Checkout(&git.CheckoutOptions{
        Branch: plumbing.ReferenceName("refs/heads/main"),
        Force:  true,
    })
}

See Also