Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 32 additions & 19 deletions pkg/cli/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,27 @@ import (

var repoLog = logger.New("cli:repo")

// repoSlugCacheState holds the cached repository slug and protects it with a mutex.
// Using a mutex-guarded struct instead of sync.Once avoids the data race that arises
// when resetting sync.Once via struct assignment (= sync.Once{}) after first use.
type repoSlugCacheState struct {
mu sync.Mutex
result string
err error
done bool
}

// Global cache for current repository info
var (
getCurrentRepoSlugOnce sync.Once
currentRepoSlugResult string
currentRepoSlugError error
)
var currentRepoSlugCache repoSlugCacheState

// ClearCurrentRepoSlugCache clears the current repository slug cache
// This is useful for testing or when repository context might have changed
// ClearCurrentRepoSlugCache clears the current repository slug cache.
// This is useful for testing or when repository context might have changed.
func ClearCurrentRepoSlugCache() {
getCurrentRepoSlugOnce = sync.Once{}
currentRepoSlugResult = ""
currentRepoSlugError = nil
currentRepoSlugCache.mu.Lock()
defer currentRepoSlugCache.mu.Unlock()
currentRepoSlugCache.result = ""
currentRepoSlugCache.err = nil
currentRepoSlugCache.done = false
}

// getCurrentRepoSlugUncached gets the current repository slug (owner/repo) using gh CLI (uncached)
Expand Down Expand Up @@ -91,19 +99,24 @@ func getCurrentRepoSlugUncached() (string, error) {
return repoPath, nil
}

// GetCurrentRepoSlug gets the current repository slug with caching using sync.Once
// This is the recommended function to use for repository access across the codebase
// GetCurrentRepoSlug gets the current repository slug with caching.
// This is the recommended function to use for repository access across the codebase.
func GetCurrentRepoSlug() (string, error) {
getCurrentRepoSlugOnce.Do(func() {
currentRepoSlugResult, currentRepoSlugError = getCurrentRepoSlugUncached()
})
currentRepoSlugCache.mu.Lock()
if !currentRepoSlugCache.done {
currentRepoSlugCache.result, currentRepoSlugCache.err = getCurrentRepoSlugUncached()
currentRepoSlugCache.done = true
}
result := currentRepoSlugCache.result
err := currentRepoSlugCache.err
currentRepoSlugCache.mu.Unlock()

if currentRepoSlugError != nil {
return "", currentRepoSlugError
if err != nil {
return "", err
}

repoLog.Printf("Using cached repository slug: %s", currentRepoSlugResult)
return currentRepoSlugResult, nil
repoLog.Printf("Using cached repository slug: %s", result)
return result, nil
}

// SplitRepoSlug wraps repoutil.SplitRepoSlug for backward compatibility.
Expand Down
46 changes: 31 additions & 15 deletions pkg/workflow/repository_features_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,26 @@ type RepositoryFeatures struct {
HasIssues bool
}

// currentRepositoryCacheState holds the cached current repository and protects it
// with a mutex. Using a mutex-guarded struct instead of sync.Once avoids the data
// race that arises when resetting sync.Once via struct assignment (= sync.Once{})
// after first use.
type currentRepositoryCacheState struct {
mu sync.Mutex
result string
err error
done bool
}

// Global cache for repository features and current repository info
var (
repositoryFeaturesCache = sync.Map{} // sync.Map is thread-safe and efficient for read-heavy workloads
repositoryFeaturesLoggedCache = sync.Map{} // Tracks which repositories have had their success messages logged
getCurrentRepositoryOnce sync.Once
currentRepositoryResult string
currentRepositoryError error
currentRepositoryCache currentRepositoryCacheState
)

// ClearRepositoryFeaturesCache clears the repository features cache
// This is useful for testing or when repository settings might have changed
// ClearRepositoryFeaturesCache clears the repository features cache.
// This is useful for testing or when repository settings might have changed.
func ClearRepositoryFeaturesCache() {
// Clear the features cache
repositoryFeaturesCache.Range(func(key, value any) bool {
Expand All @@ -85,9 +94,11 @@ func ClearRepositoryFeaturesCache() {
})

// Reset the current repository cache
getCurrentRepositoryOnce = sync.Once{}
currentRepositoryResult = ""
currentRepositoryError = nil
currentRepositoryCache.mu.Lock()
currentRepositoryCache.result = ""
currentRepositoryCache.err = nil
currentRepositoryCache.done = false
currentRepositoryCache.mu.Unlock()

repositoryFeaturesLog.Print("Repository features and current repository caches cleared")
}
Expand Down Expand Up @@ -181,16 +192,21 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error

// getCurrentRepository gets the current repository from git context (with caching)
func getCurrentRepository() (string, error) {
getCurrentRepositoryOnce.Do(func() {
currentRepositoryResult, currentRepositoryError = getCurrentRepositoryUncached()
})
currentRepositoryCache.mu.Lock()
if !currentRepositoryCache.done {
currentRepositoryCache.result, currentRepositoryCache.err = getCurrentRepositoryUncached()
currentRepositoryCache.done = true
}
result := currentRepositoryCache.result
err := currentRepositoryCache.err
currentRepositoryCache.mu.Unlock()

if currentRepositoryError != nil {
return "", currentRepositoryError
if err != nil {
return "", err
}

repositoryFeaturesLog.Printf("Using cached current repository: %s", currentRepositoryResult)
return currentRepositoryResult, nil
repositoryFeaturesLog.Printf("Using cached current repository: %s", result)
return result, nil
}

// getCurrentRepositoryUncached fetches the current repository from gh CLI (no caching)
Expand Down
Loading