feat: skip already-indexed revisions, add --force flag

When indexing a revision that already exists, the indexer now returns
early with information about the existing revision instead of re-indexing.
Use the --force flag to re-index an existing revision.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 18:59:44 +01:00
parent ae6a4d6cf9
commit 730f2d7610
4 changed files with 61 additions and 14 deletions

View File

@@ -1,9 +1,5 @@
# TODO - Future Improvements # TODO - Future Improvements
## Quick Wins
- [ ] Check if revision exists before indexing (skip or require `--force`)
## Usability ## Usability
- [ ] Progress reporting during indexing ("Fetching nixpkgs... Parsing options... Indexing files...") - [ ] Progress reporting during indexing ("Fetching nixpkgs... Parsing options... Indexing files...")

View File

@@ -50,12 +50,17 @@ func main() {
Name: "no-files", Name: "no-files",
Usage: "Skip indexing file contents (faster, disables get_file tool)", Usage: "Skip indexing file contents (faster, disables get_file tool)",
}, },
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Force re-indexing even if revision already exists",
},
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if c.NArg() < 1 { if c.NArg() < 1 {
return fmt.Errorf("revision argument required") return fmt.Errorf("revision argument required")
} }
return runIndex(c, c.Args().First(), !c.Bool("no-files")) return runIndex(c, c.Args().First(), !c.Bool("no-files"), c.Bool("force"))
}, },
}, },
{ {
@@ -162,7 +167,7 @@ func runServe(c *cli.Context) error {
return server.Run(ctx, os.Stdin, os.Stdout) return server.Run(ctx, os.Stdin, os.Stdout)
} }
func runIndex(c *cli.Context, revision string, indexFiles bool) error { func runIndex(c *cli.Context, revision string, indexFiles bool, force bool) error {
ctx := context.Background() ctx := context.Background()
store, err := openStore(c.String("database")) store, err := openStore(c.String("database"))
@@ -178,11 +183,22 @@ func runIndex(c *cli.Context, revision string, indexFiles bool) error {
indexer := nixos.NewIndexer(store) indexer := nixos.NewIndexer(store)
fmt.Printf("Indexing revision: %s\n", revision) fmt.Printf("Indexing revision: %s\n", revision)
result, err := indexer.IndexRevision(ctx, revision)
var result *nixos.IndexResult
if force {
result, err = indexer.ReindexRevision(ctx, revision)
} else {
result, err = indexer.IndexRevision(ctx, revision)
}
if err != nil { if err != nil {
return fmt.Errorf("indexing failed: %w", err) return fmt.Errorf("indexing failed: %w", err)
} }
if result.AlreadyIndexed {
fmt.Printf("Revision already indexed (%d options). Use --force to re-index.\n", result.OptionCount)
return nil
}
fmt.Printf("Indexed %d options in %s\n", result.OptionCount, result.Duration) fmt.Printf("Indexed %d options in %s\n", result.OptionCount, result.Duration)
fmt.Printf("Git hash: %s\n", result.Revision.GitHash) fmt.Printf("Git hash: %s\n", result.Revision.GitHash)
if result.Revision.ChannelName != "" { if result.Revision.ChannelName != "" {

View File

@@ -217,6 +217,20 @@ func (s *Server) makeIndexHandler(indexer *nixos.Indexer) ToolHandler {
return ErrorContent(fmt.Errorf("indexing failed: %w", err)), nil return ErrorContent(fmt.Errorf("indexing failed: %w", err)), nil
} }
// If already indexed, return early with info
if result.AlreadyIndexed {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Revision already indexed: %s\n", result.Revision.GitHash))
if result.Revision.ChannelName != "" {
sb.WriteString(fmt.Sprintf("Channel: %s\n", result.Revision.ChannelName))
}
sb.WriteString(fmt.Sprintf("Options: %d\n", result.OptionCount))
sb.WriteString(fmt.Sprintf("Indexed at: %s\n", result.Revision.IndexedAt.Format("2006-01-02 15:04")))
return CallToolResult{
Content: []Content{TextContent(sb.String())},
}, nil
}
// Index files by default // Index files by default
fileCount, err := indexer.IndexFiles(ctx, result.Revision.ID, result.Revision.GitHash) fileCount, err := indexer.IndexFiles(ctx, result.Revision.ID, result.Revision.GitHash)
if err != nil { if err != nil {

View File

@@ -39,6 +39,7 @@ type IndexResult struct {
OptionCount int OptionCount int
FileCount int FileCount int
Duration time.Duration Duration time.Duration
AlreadyIndexed bool // True if revision was already indexed (skipped)
} }
// IndexRevision indexes a nixpkgs revision by git hash or channel name. // IndexRevision indexes a nixpkgs revision by git hash or channel name.
@@ -58,6 +59,7 @@ func (idx *Indexer) IndexRevision(ctx context.Context, revision string) (*IndexR
Revision: existing, Revision: existing,
OptionCount: existing.OptionCount, OptionCount: existing.OptionCount,
Duration: time.Since(start), Duration: time.Since(start),
AlreadyIndexed: true,
}, nil }, nil
} }
@@ -112,6 +114,25 @@ func (idx *Indexer) IndexRevision(ctx context.Context, revision string) (*IndexR
}, nil }, nil
} }
// ReindexRevision forces re-indexing of a revision, deleting existing data first.
func (idx *Indexer) ReindexRevision(ctx context.Context, revision string) (*IndexResult, error) {
ref := resolveRevision(revision)
// Delete existing revision if present
existing, err := idx.store.GetRevision(ctx, ref)
if err != nil {
return nil, fmt.Errorf("failed to check existing revision: %w", err)
}
if existing != nil {
if err := idx.store.DeleteRevision(ctx, existing.ID); err != nil {
return nil, fmt.Errorf("failed to delete existing revision: %w", err)
}
}
// Now index fresh
return idx.IndexRevision(ctx, revision)
}
// buildOptions builds options.json for a nixpkgs revision. // buildOptions builds options.json for a nixpkgs revision.
func (idx *Indexer) buildOptions(ctx context.Context, ref string) (string, func(), error) { func (idx *Indexer) buildOptions(ctx context.Context, ref string) (string, func(), error) {
// Create temp directory // Create temp directory