Repository navigation
perf(vcs): read the reviewed commit from the checkout #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e8ed3ce
perf(vcs): read the reviewed commit from the checkout
jdziat a0e33eb
fix(vcs): list symlinks and read trees outside the lock
jdziat 38b75ce
fix(vcs): list submodules and say when the checkout is unusable
jdziat 6d7d333
fix(vcs): retry the checkout after a transient git failure
jdziat 2d26daa
docs(findings): record the design read from the checkout
jdziat 37c5922
docs(findings): count the files at the reviewed commit
jdziat File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| package vcs | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "sort" | ||
| "strings" | ||
| "sync" | ||
| ) | ||
|
|
||
| // checkoutTree is one commit's file list, read from the checkout in a single | ||
| // git call. Directory listings and file modes are answered from it, so a | ||
| // repository of ninety directories costs one process instead of ninety | ||
| // requests. | ||
| type checkoutTree struct { | ||
| modes map[string]string | ||
| dirs map[string][]string | ||
| } | ||
|
|
||
| // checkoutIndex holds the trees read so far, keyed by commit. | ||
| // | ||
| // The lock guards the maps only. A tree is built outside it and then | ||
| // published, so one slow git call does not stall reads of other commits; two | ||
| // callers racing on an uncached commit read it twice and keep one. | ||
| type checkoutIndex struct { | ||
| mu sync.Mutex | ||
| trees map[string]*checkoutTree | ||
| heads map[string]string | ||
| // warned holds the commits already reported, so a checkout that keeps | ||
| // failing logs once and not once per file. | ||
| warned map[string]bool | ||
| } | ||
|
|
||
| // checkoutTree returns the commit's tree from the checkout, or nil when the | ||
| // checkout cannot supply it. A nil answer sends the caller to the API. | ||
| // | ||
| // A commit the checkout does not hold is remembered, because asking again | ||
| // cannot change the answer. A git call that failed after the commit was found | ||
| // is not remembered: it may be transient, and caching it would send every | ||
| // later read of the commit through the API. | ||
| // | ||
| // The commit is read from git objects and never the working tree: the review | ||
| // is of the commit named, and the checkout may have something else out. | ||
| func (g *GitHub) checkoutTree(ctx context.Context, sha string) *checkoutTree { | ||
| if g.Checkout == "" || sha == "" || strings.HasPrefix(sha, "-") { | ||
| return nil | ||
| } | ||
| g.index.mu.Lock() | ||
| t, seen := g.index.trees[sha] | ||
| g.index.mu.Unlock() | ||
| if seen { | ||
| return t | ||
| } | ||
| t, held, err := readCheckoutTree(ctx, g.Checkout, sha) | ||
| if err != nil { | ||
| if ctx.Err() != nil { | ||
| return nil | ||
| } | ||
| g.warnOnce(sha, "checkout could not be read; reading through the API", err) | ||
| if held { | ||
| return nil | ||
| } | ||
| } | ||
| g.index.mu.Lock() | ||
| defer g.index.mu.Unlock() | ||
| if kept, raced := g.index.trees[sha]; raced { | ||
| return kept | ||
| } | ||
| if g.index.trees == nil { | ||
| g.index.trees = map[string]*checkoutTree{} | ||
| } | ||
| g.index.trees[sha] = t | ||
| return t | ||
| } | ||
|
|
||
| // warnOnce logs a checkout problem the first time it is seen for a commit. | ||
| func (g *GitHub) warnOnce(sha, msg string, err error) { | ||
| g.index.mu.Lock() | ||
| seen := g.index.warned[sha] | ||
| if g.index.warned == nil { | ||
| g.index.warned = map[string]bool{} | ||
| } | ||
| g.index.warned[sha] = true | ||
| g.index.mu.Unlock() | ||
| if !seen { | ||
| slog.Warn(msg, "checkout", g.Checkout, "commit", short(sha), "err", err) | ||
| } | ||
| } | ||
|
|
||
| // readCheckoutTree reads one commit's tree. held reports whether the checkout | ||
| // has the commit: false with an error is a miss to remember, true with an | ||
| // error is a failure that should be retried. | ||
| func readCheckoutTree(ctx context.Context, dir, sha string) (t *checkoutTree, held bool, err error) { | ||
| local := &Local{Dir: dir} | ||
| if _, err := local.gitRaw(ctx, "cat-file", "-e", sha+"^{commit}"); err != nil { | ||
| return nil, false, fmt.Errorf("commit not in checkout: %w", err) | ||
| } | ||
| out, err := local.gitRaw(ctx, "ls-tree", "-r", "-t", "-z", sha) | ||
| if err != nil { | ||
|
open-nitpick[bot] marked this conversation as resolved.
|
||
| return nil, true, fmt.Errorf("list tree: %w", err) | ||
| } | ||
| t = &checkoutTree{modes: map[string]string{}, dirs: map[string][]string{}} | ||
| for _, rec := range bytes.Split(out, []byte{0}) { | ||
| meta, name, ok := strings.Cut(string(rec), "\t") | ||
| if !ok || name == "" { | ||
| continue | ||
| } | ||
| fields := strings.Fields(meta) | ||
| if len(fields) < 2 { | ||
| continue | ||
| } | ||
| parent := "" | ||
| if i := strings.LastIndex(name, "/"); i >= 0 { | ||
| parent = name[:i] | ||
| } | ||
| base := name[strings.LastIndex(name, "/")+1:] | ||
| switch fields[1] { | ||
| case "tree": | ||
| t.dirs[parent] = append(t.dirs[parent], base+"/") | ||
| if _, has := t.dirs[name]; !has { | ||
| t.dirs[name] = nil | ||
| } | ||
| case "blob": | ||
|
open-nitpick[bot] marked this conversation as resolved.
|
||
| t.modes[name] = fields[0] | ||
| // The contents API types a symlink as a file, so it is listed here | ||
| // too. Only reading one is refused, in regular below. | ||
| t.dirs[parent] = append(t.dirs[parent], base) | ||
| case "commit": | ||
| // A submodule is typed as a file by the contents API as well, and has | ||
| // no blob here to read. | ||
| t.modes[name] = fields[0] | ||
| t.dirs[parent] = append(t.dirs[parent], base) | ||
| } | ||
| } | ||
| for _, names := range t.dirs { | ||
| sort.Strings(names) | ||
| } | ||
| return t, true, nil | ||
| } | ||
|
|
||
| // regular reports whether path is a regular file in the tree. A symlink or a | ||
| // submodule is not source, and the API path refuses them with its own error. | ||
| func (t *checkoutTree) regular(path string) bool { | ||
| mode := t.modes[path] | ||
| return mode == "100644" || mode == "100755" | ||
| } | ||
|
|
||
| // headSHA is the pull request's head commit, asked for once per pull request. | ||
| // It serves file and directory reads only. The approval check, Propose and its | ||
| // final re-read call PullRequest themselves, so a push during the run is still | ||
| // caught there. | ||
| // | ||
| // A read that names no revision used to fetch the pull request and its head | ||
| // commit again for every file, so a review reading four hundred files made | ||
| // well over a thousand requests. It also meant a push during the run could | ||
| // split the reads across two commits. | ||
| func (g *GitHub) headSHA(ctx context.Context, ref Ref) (string, error) { | ||
|
open-nitpick[bot] marked this conversation as resolved.
|
||
| key := ref.String() | ||
| g.index.mu.Lock() | ||
| sha, ok := g.index.heads[key] | ||
| g.index.mu.Unlock() | ||
| if ok { | ||
| return sha, nil | ||
| } | ||
| pr, err := g.PullRequest(ctx, ref) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| if pr.HeadSHA == "" { | ||
| return "", nil | ||
| } | ||
| g.index.mu.Lock() | ||
| if g.index.heads == nil { | ||
| g.index.heads = map[string]string{} | ||
| } | ||
| g.index.heads[key] = pr.HeadSHA | ||
| g.index.mu.Unlock() | ||
| return pr.HeadSHA, nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.