Back to Beads

Repository Context

docs/REPO_CONTEXT.md

1.0.36.6 KB
Original Source

Repository Context

This document explains how beads resolves repository context when commands run from different directories than where .beads/ lives.

Problem

Git commands must run in the correct repository, but users may invoke bd from:

  • A different repository (using BEADS_DIR environment variable)
  • A git worktree (separate working directory, shared .beads/)
  • A subdirectory within the repository

Without centralized handling, each command must implement its own path resolution, leading to bugs when assumptions don't match reality.

Solution: RepoContext API

The RepoContext API provides a single source of truth for repository resolution:

go
import "github.com/steveyegge/beads/internal/beads"

rc, err := beads.GetRepoContext()
if err != nil {
    return err
}

// Run git in beads repository (not CWD)
cmd := rc.GitCmd(ctx, "status")
output, err := cmd.Output()

When to Use Each Method

MethodUse CaseExample
GitCmd()Git commands for beads operationsgit add .beads/, git push
GitCmdCWD()Git commands for user's working repogit status (show user's changes)
RelPath()Convert absolute path to repo-relativeDisplay paths in output

GitCmd() vs GitCmdCWD()

The distinction matters when BEADS_DIR points to a different repository:

go
rc, _ := beads.GetRepoContext()

// GitCmd: runs in the beads repository
// Use for: committing .beads/, pushing/pulling beads data
cmd := rc.GitCmd(ctx, "add", ".beads/issues.jsonl")

// GitCmdCWD: runs in user's current repository
// Use for: checking user's uncommitted changes, status display
cmd := rc.GitCmdCWD(ctx, "status", "--porcelain")

Scenarios

Normal Repository

CWD is inside the repository containing .beads/:

/project/
├── .beads/
├── src/
└── README.md

$ cd /project/src
$ bd dolt push
# GitCmd() runs in /project (correct)

BEADS_DIR Redirect

User is in one repository but managing beads in another:

$ cd /repo-a          # Has uncommitted changes
$ export BEADS_DIR=/repo-b/.beads
$ bd dolt push
# GitCmd() runs in /repo-b (correct, not /repo-a)

This pattern is common for:

  • Fork contribution tracking (your tracker in separate repo)
  • Shared team databases
  • Monorepo setups

Git Worktree

User is in a worktree but .beads/ lives in main repository:

/project/                    # Main repo
├── .beads/
├── .worktrees/
│   └── feature-branch/      # Worktree (CWD)
└── src/

$ cd /project/.worktrees/feature-branch
$ bd dolt push
# GitCmd() runs in /project (main repo, where .beads lives)

Combined: Worktree + Redirect

Both worktree and BEADS_DIR can be active simultaneously:

$ cd /repo-a/.worktrees/branch-x
$ export BEADS_DIR=/repo-b/.beads
$ bd dolt push
# GitCmd() runs in /repo-b (BEADS_DIR takes precedence)

RepoContext Fields

FieldDescription
BeadsDirActual .beads/ directory (after following redirects)
RepoRootRepository root containing BeadsDir
CWDRepoRootRepository root containing user's CWD (may differ)
IsRedirectedTrue if BEADS_DIR points to different repo than CWD
IsWorktreeTrue if CWD is in a git worktree

Security

Git Hooks Disabled

GitCmd() disables git hooks and templates to prevent code execution in potentially malicious repositories:

go
cmd.Env = append(os.Environ(),
    "GIT_HOOKS_PATH=",   // Disable hooks
    "GIT_TEMPLATE_DIR=", // Disable templates
)

This protects against scenarios where BEADS_DIR points to an untrusted repository that contains malicious .git/hooks/ scripts.

Path Boundary Validation

GetRepoContext() validates that BEADS_DIR does not point to sensitive system directories:

  • /etc, /usr, /var, /root (Unix system directories)
  • /System, /Library (macOS system directories)
  • Other users' home directories

Temporary directories (e.g., /var/folders on macOS) are explicitly allowed for test environments.

Server Mode Handling

CLI vs Server Context

For CLI commands, GetRepoContext() caches the result via sync.Once because:

  • CWD doesn't change during command execution
  • BEADS_DIR doesn't change during command execution
  • Repeated filesystem access would be wasteful

For the Dolt server (long-running process), this caching is inappropriate:

  • User may create new worktrees
  • BEADS_DIR may change via direnv
  • Multiple workspaces may be active simultaneously

Workspace-Specific API

The server uses GetRepoContextForWorkspace() for fresh resolution:

go
// For server mode: fresh resolution per-operation (no caching)
rc, err := beads.GetRepoContextForWorkspace(workspacePath)

// Validation hook for detecting stale contexts
if err := rc.Validate(); err != nil {
    // Context is stale, need fresh resolution
}

This function:

  • Does NOT cache results
  • Does NOT respect BEADS_DIR (workspace path is explicit)
  • Resolves worktree relationships correctly
  • Validates that paths still exist

Migration Guide

Before (scattered resolution)

go
func doGitOperation(ctx context.Context) error {
    // Each function resolved paths differently
    beadsDir := beads.FindBeadsDir()
    redirectInfo := beads.GetRedirectInfo()
    var repoRoot string
    if redirectInfo.IsRedirected {
        repoRoot = filepath.Dir(beadsDir)
    } else {
        repoRoot = getRepoRootForWorktree(ctx)
    }
    cmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "status")
    // ...
}

After (centralized)

go
func doGitOperation(ctx context.Context) error {
    rc, err := beads.GetRepoContext()
    if err != nil {
        return err
    }
    cmd := rc.GitCmd(ctx, "status")
    // ...
}

Key Changes for Contributors

  1. Replace direct exec.Command: Use rc.GitCmd() or rc.GitCmdCWD()
  2. Remove manual path resolution: RepoContext handles all scenarios
  3. Clear caches in tests: Call beads.ResetCaches() in test cleanup

Testing

Tests use beads.ResetCaches() to clear cached context between test cases:

go
func TestSomething(t *testing.T) {
    t.Cleanup(func() {
        beads.ResetCaches()
        git.ResetCaches()
    })
    // Test code...
}

Implementation Notes

  • Result is cached via sync.Once for CLI efficiency
  • CWD and BEADS_DIR don't change during command execution
  • Uses cmd.Dir pattern (not -C flag) for Go-idiomatic execution
  • Security mitigations implemented for git hooks and path traversal