-
Notifications
You must be signed in to change notification settings - Fork 447
Refactor getOrCreateListRepoClone into focused helpers in parser remote file listing
#44179
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
Changes from 2 commits
a3f1e34
8fb3f3f
410fe4a
b5d8ef4
23c1c2e
3f2dee4
6129ab3
b143c2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,10 +33,16 @@ var gitListCloneCache = struct { | |
|
|
||
| var gitListCloneGroup singleflight.Group | ||
|
|
||
| func getOrCreateListRepoClone(ctx context.Context, owner, repo, ref, host string) (string, error) { | ||
| type listRepoCloneConfig struct { | ||
| ref string | ||
| repoURL string | ||
| cacheKey string | ||
| } | ||
|
|
||
| func resolveListRepoCloneConfig(owner, repo, ref, host string) (listRepoCloneConfig, error) { | ||
| ref = strings.TrimSpace(ref) | ||
| if ref == "" { | ||
| return "", errors.New("git fallback requires a non-empty ref") | ||
| return listRepoCloneConfig{}, errors.New("git fallback requires a non-empty ref") | ||
| } | ||
|
|
||
| githubHost := GetGitHubHostForRepo(owner, repo) | ||
|
|
@@ -45,55 +51,63 @@ func getOrCreateListRepoClone(ctx context.Context, owner, repo, ref, host string | |
| } | ||
| repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) | ||
| cacheKey := fmt.Sprintf("%s|%s|%s|%s", githubHost, owner, repo, ref) | ||
| return listRepoCloneConfig{ | ||
| ref: ref, | ||
| repoURL: repoURL, | ||
| cacheKey: cacheKey, | ||
| }, nil | ||
| } | ||
|
|
||
| if cloneDir, found := func() (string, bool) { | ||
| gitListCloneCache.mu.Lock() | ||
| defer gitListCloneCache.mu.Unlock() | ||
| if cloneDir, ok := gitListCloneCache.dirs[cacheKey]; ok { | ||
| if stat, err := os.Stat(filepath.Join(cloneDir, ".git")); err == nil && stat.IsDir() { | ||
| return cloneDir, true | ||
| } | ||
| delete(gitListCloneCache.dirs, cacheKey) | ||
| func getCachedListRepoClone(cacheKey string) (string, bool) { | ||
| gitListCloneCache.mu.Lock() | ||
| defer gitListCloneCache.mu.Unlock() | ||
|
|
||
| if cloneDir, ok := gitListCloneCache.dirs[cacheKey]; ok { | ||
| if stat, err := os.Stat(filepath.Join(cloneDir, ".git")); err == nil && stat.IsDir() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filesystem I/O () is called while holding the global cache mutex, serializing all concurrent cache lookups. 💡 Details and suggested fixIn Under contention (multiple goroutines needing a cache-miss check simultaneously), every goroutine must wait in line behind the goroutine doing the Suggested fix — copy the path while locked, then stat without the lock: func getCachedListRepoClone(cacheKey string) (string, bool) {
gitListCloneCache.mu.Lock()
cloneDir, ok := gitListCloneCache.dirs[cacheKey]
gitListCloneCache.mu.Unlock()
if !ok {
return "", false
}
if stat, err := os.Stat(filepath.Join(cloneDir, ".git")); err == nil && stat.IsDir() {
return cloneDir, true
}
// Entry is stale — evict under lock.
gitListCloneCache.mu.Lock()
delete(gitListCloneCache.dirs, cacheKey)
gitListCloneCache.mu.Unlock()
return "", false
}Note: there is a benign TOCTOU window between the stat and the delete — a concurrent goroutine could re-add a fresh entry between those two steps — but singleflight ensures only one goroutine will actually clone, so the stale-eviction path is already racy in the original code.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 23c1c2e. |
||
| return cloneDir, true | ||
| } | ||
| return "", false | ||
| }(); found { | ||
| return cloneDir, nil | ||
| delete(gitListCloneCache.dirs, cacheKey) | ||
| } | ||
| return "", false | ||
| } | ||
|
|
||
| cloneDir, err, _ := gitListCloneGroup.Do(cacheKey, func() (any, error) { | ||
| if cloneDir, found := func() (string, bool) { | ||
| gitListCloneCache.mu.Lock() | ||
| defer gitListCloneCache.mu.Unlock() | ||
| if cloneDir, ok := gitListCloneCache.dirs[cacheKey]; ok { | ||
| if stat, err := os.Stat(filepath.Join(cloneDir, ".git")); err == nil && stat.IsDir() { | ||
| return cloneDir, true | ||
| } | ||
| delete(gitListCloneCache.dirs, cacheKey) | ||
| } | ||
| return "", false | ||
| }(); found { | ||
| return cloneDir, nil | ||
| } | ||
| func cloneAndCacheListRepoClone(ctx context.Context, owner, repo, ref, repoURL, cacheKey string) (string, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] 💡 Suggested refactorExtend the struct: type listRepoCloneConfig struct {
owner string
repo string
ref string
repoURL string
cacheKey string
}Simplify the function: func cloneAndCacheListRepoClone(ctx context.Context, cfg listRepoCloneConfig) (string, error) {
// use cfg.owner, cfg.repo, cfg.ref, cfg.repoURL, cfg.cacheKey
}
// call site becomes:
return cloneAndCacheListRepoClone(ctx, config)This makes @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 410fe4a. |
||
| tmpDir, err := os.MkdirTemp("", "gh-aw-list-*") | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to create temp directory: %w", err) | ||
| } | ||
|
|
||
| tmpDir, err := os.MkdirTemp("", "gh-aw-list-*") | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to create temp directory: %w", err) | ||
| cloneCmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", "--branch", ref, "--single-branch", "--filter=blob:none", "--no-checkout", repoURL, tmpDir) | ||
| cloneOutput, err := cloneCmd.CombinedOutput() | ||
| if err != nil { | ||
| if cleanupErr := os.RemoveAll(tmpDir); cleanupErr != nil { | ||
| remoteLog.Printf("Failed to clean up temp directory %q: %v", tmpDir, cleanupErr) | ||
| } | ||
| remoteLog.Printf("Failed to clone repository: %s", string(cloneOutput)) | ||
| return "", fmt.Errorf("failed to clone repository for %s/%s@%s: %w", owner, repo, ref, err) | ||
| } | ||
|
|
||
| cloneCmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", "--branch", ref, "--single-branch", "--filter=blob:none", "--no-checkout", repoURL, tmpDir) | ||
| cloneOutput, err := cloneCmd.CombinedOutput() | ||
| if err != nil { | ||
| if cleanupErr := os.RemoveAll(tmpDir); cleanupErr != nil { | ||
| remoteLog.Printf("Failed to clean up temp directory %q: %v", tmpDir, cleanupErr) | ||
| } | ||
| remoteLog.Printf("Failed to clone repository: %s", string(cloneOutput)) | ||
| return "", fmt.Errorf("failed to clone repository for %s/%s@%s: %w", owner, repo, ref, err) | ||
| } | ||
| gitListCloneCache.mu.Lock() | ||
|
pelikhan marked this conversation as resolved.
|
||
| defer gitListCloneCache.mu.Unlock() | ||
| gitListCloneCache.dirs[cacheKey] = tmpDir | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 Details and suggested fixThe function creates a If Suggested fix — add a defensive cache check before writing: gitListCloneCache.mu.Lock()
defer gitListCloneCache.mu.Unlock()
if existing, ok := gitListCloneCache.dirs[cacheKey]; ok {
// Another goroutine finished first; discard our redundant clone.
_ = os.RemoveAll(tmpDir)
return existing, nil
}
gitListCloneCache.dirs[cacheKey] = tmpDir
return tmpDir, nilAt minimum, add a doc-comment stating the singleflight precondition so callers cannot accidentally bypass it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 23c1c2e. |
||
| return tmpDir, nil | ||
| } | ||
|
|
||
| func getOrCreateListRepoClone(ctx context.Context, owner, repo, ref, host string) (string, error) { | ||
| config, err := resolveListRepoCloneConfig(owner, repo, ref, host) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| gitListCloneCache.mu.Lock() | ||
| gitListCloneCache.dirs[cacheKey] = tmpDir | ||
| gitListCloneCache.mu.Unlock() | ||
| return tmpDir, nil | ||
| if cloneDir, found := getCachedListRepoClone(config.cacheKey); found { | ||
| return cloneDir, nil | ||
| } | ||
|
|
||
| cloneDir, err, _ := gitListCloneGroup.Do(config.cacheKey, func() (any, error) { | ||
| if cloneDir, found := getCachedListRepoClone(config.cacheKey); found { | ||
| return cloneDir, nil | ||
| } | ||
| return cloneAndCacheListRepoClone(ctx, owner, repo, config.ref, config.repoURL, config.cacheKey) | ||
| }) | ||
| if err != nil { | ||
| return "", err | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd] The new
getCachedListRepoCloneandresolveListRepoCloneConfighelpers are now package-private, pure-ish functions that are straightforwardly unit-testable in isolation — but no tests cover them. At minimum,resolveListRepoCloneConfighas edge-case logic (empty ref, host override) that benefits from table-driven tests.💡 Suggested test skeleton
@copilot please address this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 410fe4a. Added
TestResolveListRepoCloneConfiginremote_list_files_test.gowith table-driven cases covering: empty ref error, whitespace-only ref error, host override in repoURL/cacheKey, cache key contains all identity fields, owner/repo fields on config, ref trimming, and public-org host selection.