feat(deletions): auto-compact during sync and git history fallback fixes
- Add Count function to deletions package for fast line counting - Add maybeAutoCompactDeletions to sync (opt-in via deletions.auto_compact config) - Fix regex escaping in batchCheckGitHistory (bd-bgs) - Add 30s timeout to git history commands (bd-f0n) - Use git rev-parse --show-toplevel for proper repo root detection (bd-bhd) - Add tests for Count and auto-compact functionality Closes: bd-qsm, bd-bgs, bd-f0n, bd-bhd
This commit is contained in:
@@ -180,6 +180,35 @@ func DefaultPath(beadsDir string) string {
|
||||
return filepath.Join(beadsDir, "deletions.jsonl")
|
||||
}
|
||||
|
||||
// Count returns the number of lines in the deletions manifest.
|
||||
// This is a fast operation that doesn't parse JSON, just counts lines.
|
||||
// Returns 0 if the file doesn't exist or is empty.
|
||||
func Count(path string) (int, error) {
|
||||
f, err := os.Open(path) // #nosec G304 - controlled path from caller
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("failed to open deletions file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
count := 0
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line != "" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return 0, fmt.Errorf("error reading deletions file: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// DefaultRetentionDays is the default number of days to retain deletion records.
|
||||
const DefaultRetentionDays = 7
|
||||
|
||||
|
||||
@@ -546,3 +546,64 @@ func TestPruneDeletions_ZeroRetention(t *testing.T) {
|
||||
t.Errorf("expected 1 pruned with 0 retention, got %d", result.PrunedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCount_Empty(t *testing.T) {
|
||||
// Non-existent file should return 0
|
||||
count, err := Count("/nonexistent/path/deletions.jsonl")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error for non-existent file, got: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("expected 0 count for non-existent file, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCount_WithRecords(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "deletions.jsonl")
|
||||
|
||||
now := time.Now()
|
||||
records := []DeletionRecord{
|
||||
{ID: "bd-001", Timestamp: now, Actor: "user1"},
|
||||
{ID: "bd-002", Timestamp: now, Actor: "user2"},
|
||||
{ID: "bd-003", Timestamp: now, Actor: "user3"},
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
if err := AppendDeletion(path, r); err != nil {
|
||||
t.Fatalf("AppendDeletion failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
count, err := Count(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Count failed: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Errorf("expected 3, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCount_WithEmptyLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "deletions.jsonl")
|
||||
|
||||
// Write content with empty lines
|
||||
content := `{"id":"bd-001","ts":"2024-01-01T00:00:00Z","by":"user1"}
|
||||
|
||||
{"id":"bd-002","ts":"2024-01-02T00:00:00Z","by":"user2"}
|
||||
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
count, err := Count(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Count failed: %v", err)
|
||||
}
|
||||
// Should count only non-empty lines
|
||||
if count != 2 {
|
||||
t.Errorf("expected 2 (excluding empty lines), got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user