bidet

commit cba0e159e0fcd8f08f13af5673509418e8fe9e67

tree

parent:
04789f728342d5429f9f308edb4f0bc670338b70

nmyk <nick@nmyk.io>

2026-04-21T16:37:35-04:00

make log traversal faster

shell out to git binary for commit history

diff --git a/internal/core/repo.go b/internal/core/repo.go
index 5d9e44bcd6bb04f7c3fd00bb666480dc49004065..5a947efa56094282502ac9e815ebe5b4724d9c0f 100644
--- a/internal/core/repo.go
+++ b/internal/core/repo.go
@@ -58,6 +58,10 @@ 	}
 	return &Repo{repo, repoName}, nil
 }
 
+func (repo *Repo) Path() string {
+	return repo.Name + dotGit
+}
+
 func (repo *Repo) Load(entry *object.TreeEntry) (string, error) {
 	file, err := repo.BlobObject(entry.Hash)
 	if err != nil {
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 42ffcef08fe69a5cd48062bc594eed4a65a83b1b..5b6180c9f368e86454f109a4aa8d81e90228ad97 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -1,14 +1,17 @@
 package handlers
 
 import (
+	"bufio"
 	"bytes"
 	"compress/gzip"
+	"context"
 	"embed"
 	"errors"
 	"fmt"
 	"html/template"
 	"io"
 	"net/http"
+	"os/exec"
 	"path"
 	"regexp"
 	"slices"
@@ -21,7 +24,6 @@ 	"github.com/go-git/go-git/v6"
 	"github.com/go-git/go-git/v6/plumbing"
 	"github.com/go-git/go-git/v6/plumbing/filemode"
 	"github.com/go-git/go-git/v6/plumbing/object"
-	"github.com/go-git/go-git/v6/plumbing/storer"
 	"github.com/go-git/go-git/v6/plumbing/transport"
 	"nmyk.io/bidet/internal/core"
 )
@@ -270,57 +272,6 @@ 	PrevPage *Cursor
 	NextPage *Cursor
 }
 
-func commitChangesDir(c *object.Commit, dir string) (bool, error) {
-	tree, err := c.Tree()
-	if err != nil {
-		return false, err
-	}
-	parentIter := c.Parents()
-	parentCount := 0
-	hasChange := false
-	err = parentIter.ForEach(func(p *object.Commit) error {
-		parentCount++
-		pt, err := p.Tree()
-		if err != nil {
-			return err
-		}
-		// Diff only the subtree for the directory
-		changes, err := object.DiffTree(pt, tree)
-		if err != nil {
-			return err
-		}
-		for _, ch := range changes {
-			if strings.HasPrefix(ch.From.Name, dir+"/") || strings.HasPrefix(ch.To.Name, dir+"/") {
-				hasChange = true
-				return storer.ErrStop // stop iteration: we found a relevant change
-			}
-		}
-		return nil
-	})
-	if err == storer.ErrStop {
-		return true, nil
-	}
-	if err != nil {
-		return false, err
-	}
-	// root commit
-	if parentCount == 0 {
-		return true, nil
-	}
-	return hasChange, nil
-}
-
-func pathIsFile(t *object.Tree, path string) (bool, error) {
-	if path == "" {
-		return false, nil
-	}
-	entry, err := t.FindEntry(path)
-	if err != nil {
-		return false, err
-	}
-	return entry.Mode.IsFile(), nil
-}
-
 const commitsPageSize = 35
 
 var page = regexp.MustCompile(`^([0-9a-fA-F]+) ([0-9]+)$`)
@@ -363,8 +314,8 @@ 	beforeParam := r.URL.Query().Get("before")
 	afterParam := r.URL.Query().Get("after")
 	if beforeParam != "" && afterParam != "" {
 		http.NotFound(w, r)
+		return
 	}
-
 	beforeCursor := parseCursor(beforeParam)
 	afterCursor := parseCursor(afterParam)
 	isBefore := beforeCursor != nil
@@ -373,40 +324,40 @@
 	var cursor *Cursor
 	if isBefore && beforeCursor.Offset < commitsPageSize {
 		http.NotFound(w, r)
+		return
 	} else if isBefore {
 		cursor = beforeCursor
 	} else if isAfter {
 		cursor = afterCursor
 	}
 
-	logOpts := git.LogOptions{From: commit.Hash}
-	var dir string
+	args := []string{
+		"log",
+		"--pretty=format:%H",
+		commit.Hash.String(),
+	}
 	if filePath != "" {
-		tree, err := commit.Tree()
-		if err != nil {
-			s.error(w, err)
-			return
-		}
-		isFile, err := pathIsFile(tree, filePath)
-		if err != nil {
-			http.NotFound(w, r)
-			return
-		}
-		if isFile {
-			logOpts.FileName = &filePath
-		} else {
-			dir = filePath
-		}
+		args = append(args, "--", filePath)
 	}
+	ctx, cancel := context.WithCancel(r.Context())
+	defer cancel()
 
-	iter, err := repo.Log(&logOpts)
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = path.Join(s.Dir, repo.Path())
+	stdout, err := cmd.StdoutPipe()
 	if err != nil {
 		s.error(w, err)
 		return
 	}
+	if err := cmd.Start(); err != nil {
+		s.error(w, err)
+		return
+	}
+
+	scanner := bufio.NewScanner(stdout)
 
 	var commits []CommitMeta
-	var foundAnchor bool
+	var stopEarly bool
 	var start, offset, end int
 	if isBefore {
 		start = cursor.Offset - commitsPageSize
@@ -417,25 +368,21 @@ 		end = cursor.Offset + commitsPageSize
 	} else {
 		start = 0
 		end = commitsPageSize - 1
-		foundAnchor = true
 	}
-	err = iter.ForEach(func(c *object.Commit) error {
+
+	for scanner.Scan() {
 		if offset >= end {
-			return io.EOF
+			stopEarly = true
+			cancel()
+			break
 		}
-		if dir != "" {
-			ok, err := commitChangesDir(c, dir)
-			if err != nil {
-				return err
-			}
-			if !ok {
-				return nil
-			}
-		}
-		if !foundAnchor {
-			foundAnchor = c.Hash.String() == cursor.AnchorHash
+		hashStr := scanner.Text()
+		c, err := repo.CommitObject(plumbing.NewHash(hashStr))
+		if err != nil {
+			s.error(w, err)
+			return
 		}
-		if foundAnchor && offset >= start && offset < end {
+		if offset >= start && offset < end {
 			commits = append(commits, CommitMeta{
 				Hash:    c.Hash.String(),
 				Author:  fmtAuthor(c.Author.Name, c.Author.Email),
@@ -443,16 +390,14 @@ 				When:    c.Author.When,
 				Message: c.Message,
 			})
 		}
-		if foundAnchor {
-			offset++
-		}
-		return nil
-	})
-
-	if err != nil && err != io.EOF {
+		offset++
+	}
+	if err := scanner.Err(); err != nil && !errors.Is(err, context.Canceled) {
 		s.error(w, err)
 		return
 	}
+
+	_ = cmd.Wait()
 
 	var next, prev *Cursor
 	if len(commits) > 0 {
@@ -485,7 +430,7 @@ 	}
 	if prev != nil && prev.Offset < commitsPageSize {
 		prev = nil
 	}
-	if !errors.Is(err, io.EOF) {
+	if !stopEarly {
 		next = nil
 	}
 	data := CommitsData{