Commit 9f3d4f3fef78

Vincent Demeester <vincent@sbr.pm>
2026-01-27 13:18:54
fix(review-tool): support extensionless org archive files
Org-mode's default archiving creates files without .org extension (e.g., "work", "projects", "routines"). Updated parseArchiveDir to: - Process all files, not just .org extension - Skip known non-org files (binaries, backups, html, etc.) - Skip legacy.bak directory Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent adf5aff
Changed files (2)
tools
review-tool
internal
tools/review-tool/internal/sources/org.go
@@ -75,6 +75,7 @@ func (o *OrgSource) Fetch(ctx context.Context, start, end time.Time) (*activity.
 }
 
 // parseArchiveDir scans an archive directory for org files with relevant state changes.
+// It processes .org files and extensionless files (common for org-mode archives).
 func (o *OrgSource) parseArchiveDir(dir string, start, end time.Time) ([]activity.ActivityItem, error) {
 	var items []activity.ActivityItem
 
@@ -87,8 +88,9 @@ func (o *OrgSource) parseArchiveDir(dir string, start, end time.Time) ([]activit
 			return nil
 		}
 
-		// Only process .org files
-		if !strings.HasSuffix(strings.ToLower(path), ".org") {
+		// Skip known non-org files
+		lower := strings.ToLower(path)
+		if shouldSkipArchiveFile(lower) {
 			return nil
 		}
 
@@ -104,6 +106,36 @@ func (o *OrgSource) parseArchiveDir(dir string, start, end time.Time) ([]activit
 	return items, err
 }
 
+// shouldSkipArchiveFile returns true for files that are definitely not org-mode files.
+func shouldSkipArchiveFile(path string) bool {
+	// Skip backup and temp files
+	if strings.HasSuffix(path, "~") || strings.HasSuffix(path, ".bak") ||
+		strings.Contains(path, "#") || strings.HasSuffix(path, ".swp") {
+		return true
+	}
+
+	// Skip known binary/non-org extensions
+	skipExtensions := []string{
+		".html", ".htm", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".svg",
+		".css", ".js", ".json", ".yaml", ".yml", ".xml",
+		".tar", ".gz", ".xz", ".zip", ".bz2",
+		".ttf", ".woff", ".woff2", ".eot",
+		".md", ".txt", ".log", ".ico", ".gpg",
+	}
+	for _, ext := range skipExtensions {
+		if strings.HasSuffix(path, ext) {
+			return true
+		}
+	}
+
+	// Skip legacy.bak directory
+	if strings.Contains(path, "legacy.bak") {
+		return true
+	}
+
+	return false
+}
+
 func (o *OrgSource) parseOrgFile(filePath string, start, end time.Time) ([]activity.ActivityItem, error) {
 	file, err := os.Open(filePath)
 	if err != nil {
tools/review-tool/internal/sources/org_test.go
@@ -248,6 +248,7 @@ func TestOrgSource_ParsesArchiveDirectory(t *testing.T) {
 		t.Fatalf("failed to create archive directory: %v", err)
 	}
 
+	// Archive file WITH .org extension
 	archiveContent := `#+title: Archived Work
 
 * Archived
@@ -261,6 +262,25 @@ func TestOrgSource_ParsesArchiveDirectory(t *testing.T) {
 		t.Fatalf("failed to write archive org file: %v", err)
 	}
 
+	// Archive file WITHOUT extension (common org-mode archive format)
+	noExtContent := `#    -*- mode: org -*-
+
+Archived entries from file /home/user/todos.org
+
+* DONE Task from extensionless archive
+CLOSED: [2026-01-23 Thu 16:00]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-01-23 Thu 16:00
+:END:
+:LOGBOOK:
+- State "DONE"       from "TODO"       [2026-01-23 Thu 16:00]
+:END:
+`
+	noExtFile := filepath.Join(archiveDir, "work")
+	if err := os.WriteFile(noExtFile, []byte(noExtContent), 0o644); err != nil {
+		t.Fatalf("failed to write extensionless archive file: %v", err)
+	}
+
 	cfg := &config.OrgConfig{
 		Enabled:             true,
 		Files:               []string{mainFile},
@@ -280,18 +300,19 @@ func TestOrgSource_ParsesArchiveDirectory(t *testing.T) {
 		t.Fatalf("Fetch() error = %v", err)
 	}
 
-	// Should find both the main task AND the archived task
-	if len(act.Items) != 2 {
-		t.Errorf("expected 2 items (main + archive), got %d", len(act.Items))
+	// Should find all 3 tasks: main + .org archive + extensionless archive
+	if len(act.Items) != 3 {
+		t.Errorf("expected 3 items (main + 2 archives), got %d", len(act.Items))
 		for _, item := range act.Items {
 			t.Logf("  %s: %s (%s)", item.Type, item.Title, item.Timestamp.Format("2006-01-02"))
 		}
 		return
 	}
 
-	// Check for both tasks
+	// Check for all tasks
 	foundMain := false
 	foundArchived := false
+	foundNoExt := false
 	for _, item := range act.Items {
 		if item.Title == "Main task done this week" {
 			foundMain = true
@@ -299,12 +320,18 @@ func TestOrgSource_ParsesArchiveDirectory(t *testing.T) {
 		if item.Title == "Archived task from last week" {
 			foundArchived = true
 		}
+		if item.Title == "Task from extensionless archive" {
+			foundNoExt = true
+		}
 	}
 
 	if !foundMain {
 		t.Error("did not find main task")
 	}
 	if !foundArchived {
-		t.Error("did not find archived task")
+		t.Error("did not find archived task (.org)")
+	}
+	if !foundNoExt {
+		t.Error("did not find task from extensionless archive")
 	}
 }