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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user