fix: set git index flags on init when sync-branch is configured (#1158)

Fresh clones with sync-branch configured in .beads/config.yaml would show
.beads/issues.jsonl as modified in git status because the git index flags
(skip-worktree, assume-unchanged) are local-only and don't transfer via clone.

This fix ensures bd init sets these flags in two scenarios:
1. `bd init --branch <name>` - when user explicitly sets sync branch
2. `bd init` on cloned repo - when sync-branch already exists in config.yaml

Added SetSyncBranchGitignoreFlags() helper and two tests for coverage.
This commit is contained in:
Damir Vandic
2026-01-19 19:11:16 +01:00
committed by GitHub
parent 065ca3d6af
commit b09aee377f
3 changed files with 124 additions and 0 deletions

View File

@@ -683,3 +683,10 @@ func FixSyncBranchGitignore() error {
return fix.SyncBranchGitignore(cwd)
}
// SetSyncBranchGitignoreFlags sets git index flags on .beads/*.jsonl files.
// This is called directly by init when --branch is specified, bypassing the
// GetFromYAML() check since the in-memory config may not be updated yet.
func SetSyncBranchGitignoreFlags(path string) error {
return fix.SyncBranchGitignore(path)
}

View File

@@ -542,6 +542,23 @@ With --stealth: configures per-repository git settings for invisible beads usage
}
}
// Set git index flags to hide JSONL from git status when sync.branch is configured.
// These flags are local-only (don't transfer via git clone), so each clone needs them set.
// This fixes the issue where fresh clones show .beads/issues.jsonl as modified.
if isGitRepo() {
if branch != "" {
// --branch flag was passed: set flags directly (in-memory config not updated yet)
if err := doctor.SetSyncBranchGitignoreFlags(cwd); err != nil && !quiet {
fmt.Fprintf(os.Stderr, "Warning: failed to set git index flags: %v\n", err)
}
} else {
// No --branch flag: check if sync-branch exists in config.yaml (cloned repo scenario)
if err := doctor.FixSyncBranchGitignore(); err != nil && !quiet {
fmt.Fprintf(os.Stderr, "Warning: failed to set git index flags: %v\n", err)
}
}
}
// Add "landing the plane" instructions to AGENTS.md and @AGENTS.md
// Skip in stealth mode (user wants invisible setup) and quiet mode (suppress all output)
if !stealth {

View File

@@ -235,6 +235,106 @@ func TestInitWithSyncBranch(t *testing.T) {
}
}
// TestInitWithSyncBranchSetsGitExclude verifies that init with --branch sets up
// .git/info/exclude to hide untracked JSONL files from git status.
// This fixes the issue where fresh clones show .beads/issues.jsonl as modified.
func TestInitWithSyncBranchSetsGitExclude(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
// Reset Cobra flags
initCmd.Flags().Set("branch", "")
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize git repo
if err := runCommandInDir(tmpDir, "git", "init", "--initial-branch=dev"); err != nil {
t.Fatalf("Failed to init git: %v", err)
}
// Configure git user for commits
_ = runCommandInDir(tmpDir, "git", "config", "user.email", "test@test.com")
_ = runCommandInDir(tmpDir, "git", "config", "user.name", "Test")
// Run bd init with --branch flag
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--branch", "beads-sync", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init with --branch failed: %v", err)
}
// Verify .git/info/exclude contains the JSONL patterns
// (On fresh init, files are untracked so they go to exclude instead of index flags)
// Note: issues.jsonl only exists after first export, but interactions.jsonl is always created
excludePath := filepath.Join(tmpDir, ".git", "info", "exclude")
content, err := os.ReadFile(excludePath)
if err != nil {
t.Fatalf("Failed to read .git/info/exclude: %v", err)
}
excludeContent := string(content)
if !strings.Contains(excludeContent, ".beads/interactions.jsonl") {
t.Errorf("Expected .git/info/exclude to contain '.beads/interactions.jsonl', got:\n%s", excludeContent)
}
}
// TestInitWithExistingSyncBranchConfig verifies that init without --branch flag
// still sets git index flags when sync-branch is already configured in config.yaml.
// This is the "fresh clone" scenario where config.yaml exists from the clone.
func TestInitWithExistingSyncBranchConfig(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
// Reset Cobra flags
initCmd.Flags().Set("branch", "")
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize git repo
if err := runCommandInDir(tmpDir, "git", "init", "--initial-branch=dev"); err != nil {
t.Fatalf("Failed to init git: %v", err)
}
_ = runCommandInDir(tmpDir, "git", "config", "user.email", "test@test.com")
_ = runCommandInDir(tmpDir, "git", "config", "user.name", "Test")
// Create .beads directory with config.yaml containing sync-branch (simulating a clone)
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
configYaml := `sync-branch: "beads-sync"
`
if err := os.WriteFile(filepath.Join(beadsDir, "config.yaml"), []byte(configYaml), 0644); err != nil {
t.Fatalf("Failed to write config.yaml: %v", err)
}
// Create interactions.jsonl (normally exists in cloned repos)
if err := os.WriteFile(filepath.Join(beadsDir, "interactions.jsonl"), []byte{}, 0644); err != nil {
t.Fatalf("Failed to write interactions.jsonl: %v", err)
}
// Run bd init WITHOUT --branch flag (sync-branch already in config.yaml)
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet", "--force"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init failed: %v", err)
}
// Verify .git/info/exclude contains the JSONL patterns
excludePath := filepath.Join(tmpDir, ".git", "info", "exclude")
content, err := os.ReadFile(excludePath)
if err != nil {
t.Fatalf("Failed to read .git/info/exclude: %v", err)
}
excludeContent := string(content)
if !strings.Contains(excludeContent, ".beads/interactions.jsonl") {
t.Errorf("Expected .git/info/exclude to contain '.beads/interactions.jsonl' when sync-branch is in config.yaml, got:\n%s", excludeContent)
}
}
// TestInitWithoutBranchFlag verifies that sync.branch is NOT auto-set when --branch is omitted
// GH#807: This was the root cause - init was auto-detecting current branch (e.g., main)
func TestInitWithoutBranchFlag(t *testing.T) {