Files
iris-context/internal/workspace/workspace.go
T

163 lines
4.2 KiB
Go

package workspace
import (
"os"
"path/filepath"
"strings"
"sync"
)
type WorkspaceInfo struct {
HasGit bool
GitBranch string
HasNodeProject bool
HasGoProject bool
HasRustProject bool
HasPythonProject bool
HasDockerfile bool
HasMakefile bool
HasJustfile bool
HasK8s bool
SignatureFiles []string
}
var signatureChecks = []struct {
path string
field func(*WorkspaceInfo)
}{
{".git", func(w *WorkspaceInfo) { w.HasGit = true }},
{"package.json", func(w *WorkspaceInfo) { w.HasNodeProject = true }},
{"go.mod", func(w *WorkspaceInfo) { w.HasGoProject = true }},
{"Cargo.toml", func(w *WorkspaceInfo) { w.HasRustProject = true }},
{"Dockerfile", func(w *WorkspaceInfo) { w.HasDockerfile = true }},
{"Makefile", func(w *WorkspaceInfo) { w.HasMakefile = true }},
{"justfile", func(w *WorkspaceInfo) { w.HasJustfile = true }},
{"pyproject.toml", func(w *WorkspaceInfo) { w.HasPythonProject = true }},
{"requirements.txt", func(w *WorkspaceInfo) { w.HasPythonProject = true }},
{"Chart.yaml", func(w *WorkspaceInfo) { w.HasK8s = true }},
{"k8s", func(w *WorkspaceInfo) { w.HasK8s = true }},
{"kubernetes", func(w *WorkspaceInfo) { w.HasK8s = true }},
{"docker-compose.yml", func(w *WorkspaceInfo) { w.HasDockerfile = true }},
{"docker-compose.yaml", func(w *WorkspaceInfo) { w.HasDockerfile = true }},
{"Taskfile.yml", nil},
{"pom.xml", nil},
{"build.gradle", nil},
{"CMakeLists.txt", nil},
}
// Detect scans the given directory for signature files and returns workspace metadata
func Detect(cwd string) WorkspaceInfo {
var info WorkspaceInfo
for _, check := range signatureChecks {
fullPath := filepath.Join(cwd, check.path)
if _, err := os.Stat(fullPath); err == nil {
info.SignatureFiles = append(info.SignatureFiles, check.path)
if check.field != nil {
check.field(&info)
}
}
}
info.HasGit, info.GitBranch = detectGitInfo(cwd)
return info
}
func resolveGitHeadPath(cwd string) (hasGit bool, headPath string) {
dir := cwd
for dir != "" {
gitPath := filepath.Join(dir, ".git")
info, err := os.Stat(gitPath)
if err == nil {
if info.IsDir() {
return true, filepath.Join(gitPath, "HEAD")
}
content, errRead := os.ReadFile(gitPath)
if errRead == nil {
s := strings.TrimSpace(string(content))
if after, ok := strings.CutPrefix(s, "gitdir: "); ok {
gitDir := strings.TrimSpace(after)
if !filepath.IsAbs(gitDir) {
gitDir = filepath.Join(dir, gitDir)
}
return true, filepath.Join(gitDir, "HEAD")
}
}
return true, "" // found .git but couldn't resolve HEAD
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return false, ""
}
func detectGitInfo(cwd string) (hasGit bool, branch string) {
hasGit, headPath := resolveGitHeadPath(cwd)
if headPath != "" {
if data, errHead := os.ReadFile(headPath); errHead == nil {
s := strings.TrimSpace(string(data))
if after, ok := strings.CutPrefix(s, "ref: refs/heads/"); ok {
return hasGit, after
}
}
}
return hasGit, ""
}
// GitRoot returns the repository root containing cwd, or an empty string.
func GitRoot(cwd string) string {
dir := filepath.Clean(cwd)
for {
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
return ""
}
dir = parent
}
}
type cacheEntry struct {
key string // cwd + "|" + dirModTime
info WorkspaceInfo
}
var (
wsCache *cacheEntry
wsCacheMu sync.Mutex
)
// DetectCached returns cached workspace info, invalidating when directory modtime changes
// or when the Git HEAD file changes (to catch branch switches that don't affect cwd modtime).
func DetectCached(cwd string) WorkspaceInfo {
dirInfo, err := os.Stat(cwd)
if err != nil {
return Detect(cwd)
}
key := cwd + "|" + dirInfo.ModTime().String()
// Incorporate Git HEAD modtime into cache key to catch branch switches
if _, headPath := resolveGitHeadPath(cwd); headPath != "" {
if headInfo, err := os.Stat(headPath); err == nil {
key += "|HEAD:" + headInfo.ModTime().String()
}
}
wsCacheMu.Lock()
defer wsCacheMu.Unlock()
if wsCache != nil && wsCache.key == key {
return wsCache.info
}
info := Detect(cwd)
wsCache = &cacheEntry{key: key, info: info}
return info
}