fix cache bug, bump version to 1.0.1
3 files changed,  +103, -22
M flake.nix
+1, -1
1@@ -12,7 +12,7 @@
2         pkgs = nixpkgs.legacyPackages.${system};
3 
4         # Read the version from go.mod or use a default
5-        version = "1.0.0";
6+        version = "1.0.1";
7 
8         # Build the bug CLI tool
9         bug = pkgs.buildGoModule {
A loadbugs_stale_test.go
+73, -0
 1@@ -0,0 +1,73 @@
 2+package main
 3+
 4+import (
 5+	"os/exec"
 6+	"testing"
 7+	"time"
 8+
 9+	"github.com/git-bug/git-bug/entities/bug"
10+	"github.com/git-bug/git-bug/entities/identity"
11+	"github.com/git-bug/git-bug/repository"
12+)
13+
14+// TestLoadBugs_SeesRefsAddedWithoutCacheUpdate reproduces the sovrn
15+// regression where `bug list` missed 34 of 55 refs because
16+// cache.NewRepoCache loaded stale on-disk excerpts (.git/cache/bugs).
17+// The second bug is committed directly via bug.Create+Commit, bypassing
18+// excerpt updates, simulating refs added by fetch/another binary.
19+func TestLoadBugs_SeesRefsAddedWithoutCacheUpdate(t *testing.T) {
20+	tmpDir := t.TempDir()
21+
22+	initCmd := exec.Command("git", "init")
23+	initCmd.Dir = tmpDir
24+	if err := initCmd.Run(); err != nil {
25+		t.Fatalf("failed to init git repo: %v", err)
26+	}
27+
28+	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
29+		t.Fatalf("failed to create identity: %v", err)
30+	}
31+
32+	// First bug via cached path (populates excerpts).
33+	if err := runNew(tmpDir, "First bug", "first", ""); err != nil {
34+		t.Fatalf("runNew failed: %v", err)
35+	}
36+
37+	// Second bug directly, bypassing cache excerpt writes.
38+	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
39+	if err != nil {
40+		t.Fatalf("failed to open repo: %v", err)
41+	}
42+	author, err := identity.GetUserIdentity(repo)
43+	if err != nil {
44+		repo.Close()
45+		t.Fatalf("failed to get user identity: %v", err)
46+	}
47+	b, _, err := bug.Create(author, time.Now().Unix(), "Second bug", "second", nil, nil)
48+	if err != nil {
49+		repo.Close()
50+		t.Fatalf("bug.Create failed: %v", err)
51+	}
52+	if err := b.Commit(repo); err != nil {
53+		repo.Close()
54+		t.Fatalf("bug Commit failed: %v", err)
55+	}
56+	repo.Close()
57+
58+	issues, err := LoadBugs(tmpDir)
59+	if err != nil {
60+		t.Fatalf("LoadBugs failed: %v", err)
61+	}
62+
63+	if len(issues) != 2 {
64+		t.Fatalf("LoadBugs returned %d issues, want 2 (stale cache hides refs added without excerpt update)", len(issues))
65+	}
66+
67+	found := map[string]bool{}
68+	for _, iss := range issues {
69+		found[iss.Title] = true
70+	}
71+	if !found["First bug"] || !found["Second bug"] {
72+		t.Fatalf("LoadBugs missing titles, got %v", found)
73+	}
74+}
M main.go
+29, -21
 1@@ -41,17 +41,20 @@ func LoadBugs(repoPath string) ([]BugIssue, error) {
 2 	}
 3 	defer repo.Close()
 4 
 5-	// Create cache to access bugs with metadata support
 6-	// Use NewRepoCache and wait for build events to ensure cache is fully built
 7+	// Open cache for parent metadata lookups (SetMetadata is only
 8+	// visible via BugCache.Snapshot, not via Compile). The excerpt
 9+	// list itself can be stale on disk (NewRepoCache prefers stored
10+	// excerpts without checking for new refs), so never use AllIds
11+	// as the source of truth for which bugs exist.
12 	cacheInstance, events := cache.NewRepoCache(repo)
13-	defer cacheInstance.Close()
14-
15-	// Wait for cache to be fully built
16 	for ev := range events {
17 		if ev.Err != nil {
18+			cacheInstance.Close()
19 			return nil, fmt.Errorf("failed to build bug cache: %w", ev.Err)
20 		}
21 	}
22+	defer cacheInstance.Close()
23+	bugCache := cacheInstance.Bugs()
24 
25 	// Generate short IDs for all issues
26 	gen := NewShortIDGenerator(repoPath)
27@@ -62,20 +65,17 @@ func LoadBugs(repoPath string) ([]BugIssue, error) {
28 
29 	var issues []BugIssue
30 
31-	// Use cache to list bugs with metadata
32-	bugCache := cacheInstance.Bugs()
33-	ids := bugCache.AllIds()
34-
35-	for _, id := range ids {
36-		// Use Resolve to get BugCache which has Snapshot() with GetCreateMetadata
37-		b, err := bugCache.Resolve(id)
38-		if err != nil {
39+	// Enumerate via ReadAll (reads refs directly, never stale) and
40+	// enrich each bug with parent metadata from the cache when available.
41+	for streamedBug := range bug.ReadAll(repo) {
42+		if streamedBug.Err != nil {
43 			continue // Skip errors, process what we can
44 		}
45 
46-		snap := b.Snapshot()
47+		b := streamedBug.Entity
48+		snap := b.Compile()
49 
50-		fullID := id.String()
51+		fullID := b.Id().String()
52 		shortID, _ := shortIDMap.GetShortID(fullID)
53 
54 		labels := make([]string, len(snap.Labels))
55@@ -83,10 +83,13 @@ func LoadBugs(repoPath string) ([]BugIssue, error) {
56 			labels[i] = string(label)
57 		}
58 
59-		// Read parent metadata from CreateMetadata
60+		// Read parent metadata via cache snapshot when available.
61+		// Fall back to no parent if the cache excerpt is stale/missing.
62 		parentID := ""
63-		if parent, ok := snap.GetCreateMetadata("parent"); ok {
64-			parentID = parent
65+		if cached, err := bugCache.Resolve(b.Id()); err == nil {
66+			if parent, ok := cached.Snapshot().GetCreateMetadata("parent"); ok {
67+				parentID = parent
68+			}
69 		}
70 
71 		issues = append(issues, BugIssue{
72@@ -649,15 +652,20 @@ func promptUser(prompt string) (string, error) {
73 // invalidateGitBugCache deletes the git-bug cache directories to force a rebuild.
74 // This ensures the `bug` command and `git-bug` binary stay in sync when either
75 // is used to modify issues or identities. Failures are silent (best effort).
76+//
77+// NOTE: OpenGoGitRepo is always called with an empty namespace, so the
78+// LocalStorage root is $Repo/.git and excerpts live in .git/cache (not
79+// .git/git-bug/cache). Deleting the wrong path is a silent no-op that
80+// leaves stale excerpts behind.
81 func invalidateGitBugCache(repoPath string) {
82-	gitBugPath := filepath.Join(repoPath, ".git", "git-bug")
83+	gitDir := filepath.Join(repoPath, ".git")
84 
85 	// Remove cache directory (serialized excerpts)
86-	cachePath := filepath.Join(gitBugPath, "cache")
87+	cachePath := filepath.Join(gitDir, "cache")
88 	_ = os.RemoveAll(cachePath)
89 
90 	// Remove indexes directory (bleve search indexes)
91-	indexesPath := filepath.Join(gitBugPath, "indexes")
92+	indexesPath := filepath.Join(gitDir, "indexes")
93 	_ = os.RemoveAll(indexesPath)
94 }
95