From 91a3e2d6cec79b89478abda6e14104c1de739ebf Mon Sep 17 00:00:00 2001 From: Rex P Date: Tue, 26 May 2026 13:58:48 +1000 Subject: [PATCH] fix(importer): clean untracked files after checkout in reconciler --- go/internal/importer/git.go | 7 ++ go/internal/importer/git_test.go | 115 +++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/go/internal/importer/git.go b/go/internal/importer/git.go index 327b818eee5..a79e354b5d0 100644 --- a/go/internal/importer/git.go +++ b/go/internal/importer/git.go @@ -301,6 +301,13 @@ func handleReconcileGit(ctx context.Context, ch chan<- WorkItem, config Config, Branch: branch, Force: true, }) + if err != nil { + return nil, err + } + + err = wt.Clean(&git.CleanOptions{ + Dir: true, + }) return sharedRepo{ Repository: repo, diff --git a/go/internal/importer/git_test.go b/go/internal/importer/git_test.go index 8abb01965f6..ddb75f1c359 100644 --- a/go/internal/importer/git_test.go +++ b/go/internal/importer/git_test.go @@ -1,6 +1,8 @@ package importer import ( + "crypto/sha256" + "encoding/hex" "io" "os" "path/filepath" @@ -253,3 +255,116 @@ func TestHandleImportGit_Deletion(t *testing.T) { t.Errorf("Expected LastSyncedCommit %s, got %s", commitB.String(), sourceRepo.Git.LastSyncedCommit) } } + +func TestHandleReconcileGit_CleanUntracked(t *testing.T) { + // Setup a temporary git repo acting as the remote source + remoteDir := t.TempDir() + remoteRepo, err := git.PlainInit(remoteDir, false) + if err != nil { + t.Fatalf("Failed to init remote repo: %v", err) + } + remoteWt, err := remoteRepo.Worktree() + if err != nil { + t.Fatalf("Failed to get remote worktree: %v", err) + } + + // Initial commit: one tracked file + if err := os.WriteFile(filepath.Join(remoteDir, "CVE-A.json"), []byte("{}"), 0600); err != nil { + t.Fatalf("Failed to write file: %v", err) + } + if _, err := remoteWt.Add("CVE-A.json"); err != nil { + t.Fatalf("Failed to add file: %v", err) + } + _, err = remoteWt.Commit("Initial", &git.CommitOptions{ + Author: &object.Signature{Name: "Test", Email: "test@example.com", When: time.Now()}, + }) + if err != nil { + t.Fatalf("Failed to commit: %v", err) + } + + mockStore := &mockSourceRepositoryStore{ + updates: make(map[string]any), + } + mockVulnStore := &mockVulnerabilityStore{ + Entries: make(map[string][]*models.VulnSourceRef), + } + workDir := t.TempDir() + + config := Config{ + SourceRepoStore: mockStore, + VulnerabilityStore: mockVulnStore, + GitWorkDir: workDir, + } + + sourceRepo := &models.SourceRepository{ + Name: "test-git-repo", + Type: models.SourceRepositoryTypeGit, + Extension: ".json", + Git: &models.SourceRepoGit{ + URL: remoteDir, + }, + } + + // 1. Run reconcile first time to clone the repo + ch := make(chan WorkItem, 10) + err = handleReconcileGit(t.Context(), ch, config, sourceRepo) + if err != nil { + t.Fatalf("handleReconcileGit failed: %v", err) + } + close(ch) + + // Consume channel to avoid blocking + for range ch { + continue + } + + // Calculate local clone path + sha := sha256.Sum256([]byte(sourceRepo.Git.URL)) + localCloneDir := filepath.Join(workDir, hex.EncodeToString(sha[:])) + + // Verify the local clone exists and has the tracked file + trackedFilePath := filepath.Join(localCloneDir, "CVE-A.json") + if _, err := os.Stat(trackedFilePath); err != nil { + t.Fatalf("Expected tracked file to exist at %s, got error: %v", trackedFilePath, err) + } + + // 2. Create an untracked file in the local clone + untrackedFilePath := filepath.Join(localCloneDir, "untracked.json") + if err := os.WriteFile(untrackedFilePath, []byte("untracked"), 0600); err != nil { + t.Fatalf("Failed to write untracked file: %v", err) + } + + // Create an untracked directory with a file + untrackedSubDir := filepath.Join(localCloneDir, "untracked_dir") + if err := os.Mkdir(untrackedSubDir, 0755); err != nil { + t.Fatalf("Failed to create untracked dir: %v", err) + } + untrackedFileInDir := filepath.Join(untrackedSubDir, "file.json") + if err := os.WriteFile(untrackedFileInDir, []byte("untracked"), 0600); err != nil { + t.Fatalf("Failed to write untracked file in dir: %v", err) + } + + // 3. Run reconcile again. It should clean up untracked files/dirs. + ch2 := make(chan WorkItem, 10) + err = handleReconcileGit(t.Context(), ch2, config, sourceRepo) + if err != nil { + t.Fatalf("handleReconcileGit failed second time: %v", err) + } + close(ch2) + for range ch2 { + continue + } + + // 4. Verify untracked file and dir are GONE + if _, err := os.Stat(untrackedFilePath); !os.IsNotExist(err) { + t.Errorf("Expected untracked file to be deleted, but it still exists (or got error: %v)", err) + } + if _, err := os.Stat(untrackedSubDir); !os.IsNotExist(err) { + t.Errorf("Expected untracked directory to be deleted, but it still exists (or got error: %v)", err) + } + + // Verify tracked file still exists + if _, err := os.Stat(trackedFilePath); err != nil { + t.Errorf("Expected tracked file to still exist, got error: %v", err) + } +}