3 files changed,
+103,
-22
+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 {
+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