From c356948c2299c08172316c732e3dcdf7b3973a60 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Tue, 3 Feb 2026 12:41:00 +0000 Subject: [PATCH] knotserver/git: improve lastCommit calculations for dirs and files bigger portions of the logic for directories and files are now shared. Signed-off-by: oppiliappan --- knotserver/git/last_commit.go | 147 ++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 60 deletions(-) diff --git a/knotserver/git/last_commit.go b/knotserver/git/last_commit.go index fd722689..820e77e1 100644 --- a/knotserver/git/last_commit.go +++ b/knotserver/git/last_commit.go @@ -6,14 +6,16 @@ import ( "crypto/sha256" "fmt" "io" + "iter" "os/exec" "path" + "strconv" "strings" "time" "github.com/dgraph-io/ristretto" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" + "tangled.org/core/sets" "tangled.org/core/types" ) @@ -73,42 +75,58 @@ func (g *GitRepo) streamingGitLog(ctx context.Context, extraArgs ...string) (io. type commit struct { hash plumbing.Hash when time.Time - files []string + files sets.Set[string] message string } +func newCommit() commit { + return commit{ + files: sets.New[string](), + } +} + +type lastCommitDir struct { + dir string + entries []string +} + +func (l lastCommitDir) children() iter.Seq[string] { + return func(yield func(string) bool) { + for _, child := range l.entries { + if !yield(path.Join(l.dir, child)) { + return + } + } + } +} + func cacheKey(g *GitRepo, path string) string { sep := byte(':') hash := sha256.Sum256(fmt.Append([]byte{}, g.path, sep, g.h.String(), sep, path)) return fmt.Sprintf("%x", hash) } -func (g *GitRepo) calculateCommitTimeIn(ctx context.Context, subtree *object.Tree, parent string, timeout time.Duration) (map[string]commit, error) { +func (g *GitRepo) lastCommitDirIn(ctx context.Context, parent lastCommitDir, timeout time.Duration) (map[string]commit, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - return g.calculateCommitTime(ctx, subtree, parent) + return g.lastCommitDir(ctx, parent) } -func (g *GitRepo) calculateCommitTime(ctx context.Context, subtree *object.Tree, parent string) (map[string]commit, error) { - filesToDo := make(map[string]struct{}) +func (g *GitRepo) lastCommitDir(ctx context.Context, parent lastCommitDir) (map[string]commit, error) { + filesToDo := sets.Collect(parent.children()) filesDone := make(map[string]commit) - for _, e := range subtree.Entries { - fpath := path.Clean(path.Join(parent, e.Name)) - filesToDo[fpath] = struct{}{} - } - for _, e := range subtree.Entries { - f := path.Clean(path.Join(parent, e.Name)) - cacheKey := cacheKey(g, f) + for p := range filesToDo.All() { + cacheKey := cacheKey(g, p) if cached, ok := commitCache.Get(cacheKey); ok { - filesDone[f] = cached.(commit) - delete(filesToDo, f) + filesDone[p] = cached.(commit) + filesToDo.Remove(p) } else { - filesToDo[f] = struct{}{} + filesToDo.Insert(p) } } - if len(filesToDo) == 0 { + if filesToDo.IsEmpty() { return filesDone, nil } @@ -116,17 +134,25 @@ func (g *GitRepo) calculateCommitTime(ctx context.Context, subtree *object.Tree, defer cancel() pathSpec := "." - if parent != "" { - pathSpec = parent + if parent.dir != "" { + pathSpec = parent.dir + } + if filesToDo.Len() == 1 { + // this is an optimization for the scenario where we want to calculate + // the last commit for just one path, we can directly set the pathspec to that path + for s := range filesToDo.All() { + pathSpec = s + } } - output, err := g.streamingGitLog(ctx, "--pretty=format:%H,%ad,%s", "--date=iso", "--name-only", "--", pathSpec) + + output, err := g.streamingGitLog(ctx, "--pretty=format:%H,%ad,%s", "--date=unix", "--name-only", "--", pathSpec) if err != nil { return nil, err } defer output.Close() // Ensure the git process is properly cleaned up reader := bufio.NewReader(output) - var current commit + current := newCommit() for { line, err := reader.ReadString('\n') if err != nil && err != io.EOF { @@ -137,33 +163,34 @@ func (g *GitRepo) calculateCommitTime(ctx context.Context, subtree *object.Tree, if line == "" { if !current.hash.IsZero() { // we have a fully parsed commit - for _, f := range current.files { - if _, ok := filesToDo[f]; ok { + for f := range current.files.All() { + if filesToDo.Contains(f) { filesDone[f] = current - delete(filesToDo, f) + filesToDo.Remove(f) commitCache.Set(cacheKey(g, f), current, 0) } } - if len(filesToDo) == 0 { - cancel() + if filesToDo.IsEmpty() { break } - current = commit{} + current = newCommit() } } else if current.hash.IsZero() { parts := strings.SplitN(line, ",", 3) if len(parts) == 3 { current.hash = plumbing.NewHash(parts[0]) - current.when, _ = time.Parse("2006-01-02 15:04:05 -0700", parts[1]) + epochTime, _ := strconv.ParseInt(parts[1], 10, 64) + current.when = time.Unix(epochTime, 0) current.message = parts[2] } } else { // all ancestors along this path should also be included file := path.Clean(line) - ancestors := ancestors(file) - current.files = append(current.files, file) - current.files = append(current.files, ancestors...) + current.files.Insert(file) + for _, a := range ancestors(file) { + current.files.Insert(a) + } } if err == io.EOF { @@ -174,44 +201,31 @@ func (g *GitRepo) calculateCommitTime(ctx context.Context, subtree *object.Tree, return filesDone, nil } -func ancestors(p string) []string { - var ancestors []string - - for { - p = path.Dir(p) - if p == "." || p == "/" { - break - } - ancestors = append(ancestors, p) +// LastCommitFile returns the last commit information for a specific file path +func (g *GitRepo) LastCommitFile(ctx context.Context, filePath string) (*types.LastCommitInfo, error) { + parent, child := path.Split(filePath) + parent = path.Clean(parent) + if parent == "." { + parent = "" } - return ancestors -} -// GetLastCommitForPath returns the last commit information for a specific file path -func (g *GitRepo) GetLastCommitForPath(ctx context.Context, filePath string) (*types.LastCommitInfo, error) { - c, err := g.r.CommitObject(g.h) - if err != nil { - return nil, fmt.Errorf("commit object: %w", err) + lastCommitDir := lastCommitDir{ + dir: parent, + entries: []string{child}, } - tree, err := c.Tree() + times, err := g.lastCommitDirIn(ctx, lastCommitDir, 2*time.Second) if err != nil { - return nil, fmt.Errorf("file tree: %w", err) - } - - // parent directory for calculateCommitTime - parent := path.Dir(filePath) - if parent == "." { - parent = "" + return nil, fmt.Errorf("calculate commit time: %w", err) } - times, err := g.calculateCommitTimeIn(ctx, tree, parent, 2*time.Second) - if err != nil { - return nil, fmt.Errorf("calculate commit time: %w", err) + // extract the only element of the map, the commit info of the current path + var commitInfo *commit + for _, c := range times { + commitInfo = &c } - commitInfo, ok := times[filePath] - if !ok { + if commitInfo == nil { return nil, fmt.Errorf("no commit found for path: %s", filePath) } @@ -221,3 +235,16 @@ func (g *GitRepo) GetLastCommitForPath(ctx context.Context, filePath string) (*t When: commitInfo.when, }, nil } + +func ancestors(p string) []string { + var ancestors []string + + for { + p = path.Dir(p) + if p == "." || p == "/" { + break + } + ancestors = append(ancestors, p) + } + return ancestors +} -- 2.51.2