Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

837 lines
22 KiB
Go

package parser
import (
"context"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"time"
"unicode"
gitrepo "go.kenn.io/kit/git/repo"
"golang.org/x/sync/singleflight"
)
// osStat is indirected through a var so tests can intercept stat
// calls from the git-root walker. Production code always uses
// os.Stat via this binding.
var osStat = os.Stat
var projectMarkers = []string{
"code", "projects", "repos", "src", "work", "dev",
}
var ignoredSystemDirs = map[string]bool{
"users": true, "home": true, "var": true,
"tmp": true, "private": true,
}
// NormalizeName converts dashes to underscores for consistent
// project name formatting.
func NormalizeName(s string) string {
return strings.ReplaceAll(s, "-", "_")
}
// GetProjectName converts an encoded Claude project directory name
// to a clean project name. Claude encodes paths like
// /Users/alice/code/my-app as -Users-alice-code-my-app.
func GetProjectName(dirName string) string {
if dirName == "" {
return ""
}
if !strings.HasPrefix(dirName, "-") {
return NormalizeName(dirName)
}
parts := strings.Split(dirName, "-")
// Strategy 1: find a known project parent directory marker
for _, marker := range projectMarkers {
for i, part := range parts {
if strings.EqualFold(part, marker) && i+1 < len(parts) {
result := strings.Join(parts[i+1:], "-")
if result != "" {
return NormalizeName(result)
}
}
}
}
// Strategy 2: use last non-system-directory component
for _, v := range slices.Backward(parts) {
if p := v; p != "" && !ignoredSystemDirs[strings.ToLower(p)] {
return NormalizeName(p)
}
}
return NormalizeName(dirName)
}
// ExtractProjectFromCwd extracts a project name from a working
// directory path. If cwd is inside a git repository (including
// linked worktrees), this returns the repository root directory
// name. Otherwise it falls back to the last path component.
func ExtractProjectFromCwd(cwd string) string {
return ExtractProjectFromCwdWithBranch(cwd, "")
}
// ExtractProjectFromCwdWithBranch extracts a canonical project
// name from cwd and optionally git branch metadata. Branch is
// used as a fallback heuristic when the original worktree path no
// longer exists on disk.
func ExtractProjectFromCwdWithBranch(
cwd, gitBranch string,
) string {
return extractProjectFromCwdWithBranch(context.TODO(), cwd, gitBranch)
}
// ExtractProjectFromCwdWithBranchContext extracts a canonical project name
// from cwd and optionally git branch metadata using ctx for git-backed
// repository resolution.
func ExtractProjectFromCwdWithBranchContext(
ctx context.Context, cwd, gitBranch string,
) string {
return extractProjectFromCwdWithBranch(ctx, cwd, gitBranch)
}
func extractProjectFromCwdWithBranch(
ctx context.Context, cwd, gitBranch string,
) string {
if cwd == "" {
return ""
}
if ctx == nil {
ctx = context.TODO()
}
winPath := looksLikeWindowsPath(cwd)
norm := cwd
if winPath {
norm = strings.ReplaceAll(cwd, "\\", "/")
}
cleaned := filepath.Clean(norm)
// Recognize worktree manager layouts before walking git roots.
// These layouts encode the owning project in the path even when
// the git root basename is a branch or generated worktree id.
if p := projectFromWorktreeLayout(cleaned); p != "" {
return NormalizeName(p)
}
// Skip the git-root walk when the cwd cannot resolve to a
// real local filesystem location. On macOS a bulk walk under
// an unbacked autofs prefix cascades through automountd into
// opendirectoryd (/usr/libexec/od_user_homes), so we probe
// the prefix once before walking.
if filepath.IsAbs(cleaned) && !isForeignOSPath(cwd, cleaned, winPath) {
if root := findGitRepoRoot(ctx, cleaned); root != "" {
name := filepath.Base(root)
if isInvalidPathBase(name) {
return ""
}
return NormalizeName(name)
}
}
name := filepath.Base(cleaned)
if isInvalidPathBase(name) {
return ""
}
name = trimBranchSuffix(name, gitBranch)
if isInvalidPathBase(name) {
return ""
}
return NormalizeName(name)
}
// worktreeLayout describes path fragments that identify worktree
// manager directory conventions. projectPart is the zero-based
// component after marker that contains the owning project name.
type worktreeLayout struct {
marker string
projectPart int
minParts int
roborevCIBareLayout bool
}
var worktreeLayouts []worktreeLayout
const roborevCIBareProject = "roborev_ci"
func init() {
sep := string(filepath.Separator)
worktreeLayouts = []worktreeLayout{
// .superset/worktrees/$PROJECT/$BRANCH[/...]
{marker: sep + ".superset" + sep + "worktrees" + sep, projectPart: 0, minParts: 2},
// conductor/workspaces/$PROJECT/$BRANCH[/...]
{marker: sep + "conductor" + sep + "workspaces" + sep, projectPart: 0, minParts: 2},
// ~/.config/middleman/worktrees/github.com/$OWNER/$REPO/$WORKTREE[/...]
{marker: sep + ".config" + sep + "middleman" + sep + "worktrees" + sep + "github.com" + sep, projectPart: 1, minParts: 3},
// ~/.codex/worktrees/$WORKTREE_ID/$REPO[/...]
{marker: sep + ".codex" + sep + "worktrees" + sep, projectPart: 1, minParts: 2},
// roborev CI: ~/.roborev/ci-worktrees/$REPO/roborev-ci-<jobID>-<id>[/...].
// roborev nests the ephemeral worktree under a repo-named parent so the
// owning project survives the generated leaf name. Anchored to the
// .roborev data dir (like the tool-anchored siblings above) so an
// unrelated path that merely contains a "ci-worktrees" directory is not
// matched.
{
marker: sep + ".roborev" + sep + "ci-worktrees" + sep,
projectPart: 0, minParts: 2, roborevCIBareLayout: true,
},
}
}
// projectFromWorktreeLayout detects known worktree manager
// directory layouts and extracts the project name component.
// Returns "" if the path does not match any known layout.
func projectFromWorktreeLayout(path string) string {
for _, layout := range worktreeLayouts {
_, rest, found := strings.Cut(path, layout.marker)
if !found {
continue
}
parts := strings.Split(rest, string(filepath.Separator))
if layout.roborevCIBareLayout && isRoborevCIWorktreeLeaf(parts[0]) {
return roborevCIBareProject
}
if len(parts) < layout.minParts {
continue
}
project := parts[layout.projectPart]
if isInvalidPathBase(project) {
continue
}
return project
}
return ""
}
func isRoborevCIWorktreeLeaf(name string) bool {
rest, ok := strings.CutPrefix(name, "roborev-ci-")
if !ok {
return false
}
job, id, ok := strings.Cut(rest, "-")
if !ok || job == "" || id == "" {
return false
}
return allASCIIDigits(job) && allASCIIDigits(id)
}
func allASCIIDigits(s string) bool {
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// autofsMountSource is indirected so tests can supply fixture
// output in lieu of running mount(8).
var autofsMountSource = runMountCommand
func runMountCommand() ([]byte, error) {
return exec.Command("/sbin/mount").Output()
}
// autofsPrefixes holds path prefixes that autofs is actively
// managing on this host, each with a trailing separator so
// strings.HasPrefix gives component-boundary matches. Populated
// at package init on darwin from the live mount table; other
// platforms leave it empty.
//
// Why we care: os.Stat into an autofs-managed prefix triggers
// automountd. For the default /home entry macOS resolves the map
// via /usr/libexec/od_user_homes, which asks opendirectoryd to
// enumerate every user record. Bulk remote-sync runs whose
// session cwds all share a /home/<user>/... prefix therefore peg
// opendirectoryd and automountd at hundreds of percent CPU.
//
// Sourcing the prefix set from mount(8) (rather than parsing
// /etc/auto_master directly) captures prefixes pulled in via
// +auto_master directory-service includes, which never appear
// in the local config file.
var autofsPrefixes = detectAutofsPrefixes()
// detectAutofsPrefixes returns the autofs-managed path prefixes
// reported by the running mount table. Non-darwin hosts and
// exec failures both return nil.
func detectAutofsPrefixes() []string {
if runtime.GOOS != "darwin" {
return nil
}
data, err := autofsMountSource()
if err != nil {
return nil
}
return parseMountOutputForAutofs(data)
}
// parseMountOutputForAutofs extracts the mount points of autofs
// filesystems from mount(8) output. Typical macOS lines look
// like:
//
// map auto_home on /System/Volumes/Data/home (autofs, automounted, nobrowse)
// server.example:/export on /corp/home (autofs, nobrowse)
//
// macOS presents the Data volume at / via an APFS firmlink, so
// /System/Volumes/Data is stripped to match the path form that
// client code observes.
func parseMountOutputForAutofs(data []byte) []string {
var out []string
for line := range strings.SplitSeq(string(data), "\n") {
if !strings.Contains(line, "(autofs") {
continue
}
_, after, ok := strings.Cut(line, " on ")
if !ok {
continue
}
rest := after
parenIdx := strings.LastIndex(rest, " (")
if parenIdx < 0 {
continue
}
mount := strings.TrimSpace(rest[:parenIdx])
mount = strings.TrimPrefix(mount, "/System/Volumes/Data")
if mount == "" || mount == "/" {
continue
}
out = append(out, strings.TrimRight(mount, "/")+"/")
}
return out
}
// isForeignOSPath reports whether cwd should bypass the local
// git-root walk.
//
// - Windows-convention paths on POSIX hosts (drive letters, UNC
// prefixes) cannot exist as real filesystem locations.
// - Paths under a local autofs prefix whose first component does
// not resolve: walking them on macOS triggers automountd per
// ancestor, and auto_home's resolver asks opendirectoryd to
// enumerate every user record — a hundred-percent-CPU storm
// under bulk remote sync.
//
// An autofs prefix that does resolve (e.g. an enterprise NFS-backed
// /home) falls through to the walk so repository roots are still
// found.
func isForeignOSPath(cwd, cleaned string, winPath bool) bool {
if winPath {
return runtime.GOOS != "windows"
}
for _, prefix := range autofsPrefixes {
if !strings.HasPrefix(cleaned, prefix) {
continue
}
return !autofsFirstLevelResolves(prefix, cleaned)
}
return false
}
// autofsProbeTTL bounds how long a probe result (positive or
// negative) is trusted. Long enough that a bulk sync pays for
// one probe per unique prefix user; short enough that a long-
// running server rediscovers a mount that came up after startup.
const autofsProbeTTL = 60 * time.Second
// nowFn is indirected so TTL-expiry tests can advance the clock.
var nowFn = time.Now
type autofsProbeEntry struct {
resolves bool
expires time.Time
}
// autofsProbes memoises whether the first path component under an
// autofs prefix resolves locally. Keyed by the probed path
// (e.g. "/home/wes"), so a bulk sync whose cwds all share a
// single <prefix>/<user> pays one probe rather than one per
// session.
//
// Writes go through autofsProbeSF to collapse concurrent misses
// for the same key into a single osStat call — critical because
// sync workers probe in parallel.
var (
autofsProbesMu sync.Mutex
autofsProbes = map[string]autofsProbeEntry{}
autofsProbeSF singleflight.Group
)
// resetAutofsProbes clears the cache and in-flight probes.
// Intended for tests that need deterministic probe counts.
func resetAutofsProbes() {
autofsProbesMu.Lock()
autofsProbes = map[string]autofsProbeEntry{}
autofsProbesMu.Unlock()
autofsProbeSF = singleflight.Group{}
}
// autofsFirstLevelResolves probes whether <prefix>/<first-comp>
// exists as a real filesystem entry. Results are cached for
// autofsProbeTTL and concurrent misses share a single probe.
func autofsFirstLevelResolves(prefix, cleaned string) bool {
rest := cleaned[len(prefix):]
if i := strings.IndexByte(rest, '/'); i >= 0 {
rest = rest[:i]
}
key := prefix + rest
if resolves, ok := lookupAutofsProbe(key); ok {
return resolves
}
v, _, _ := autofsProbeSF.Do(key, func() (any, error) {
// Re-check inside the singleflight slot: a prior
// caller for the same key may have just populated
// the cache while we were waiting for the slot.
if resolves, ok := lookupAutofsProbe(key); ok {
return resolves, nil
}
_, err := osStat(key)
resolves := err == nil
storeAutofsProbe(key, resolves)
return resolves, nil
})
return v.(bool)
}
func lookupAutofsProbe(key string) (bool, bool) {
autofsProbesMu.Lock()
defer autofsProbesMu.Unlock()
entry, ok := autofsProbes[key]
if !ok || !nowFn().Before(entry.expires) {
return false, false
}
return entry.resolves, true
}
func storeAutofsProbe(key string, resolves bool) {
autofsProbesMu.Lock()
autofsProbes[key] = autofsProbeEntry{
resolves: resolves,
expires: nowFn().Add(autofsProbeTTL),
}
autofsProbesMu.Unlock()
}
// looksLikeWindowsPath returns true when cwd appears to use
// Windows path conventions: a drive letter (e.g. "C:\...") or a
// UNC prefix ("\\server\..."). On POSIX, backslash is a legal
// filename character so we must not blindly rewrite it.
func looksLikeWindowsPath(cwd string) bool {
if len(cwd) >= 3 && cwd[1] == ':' && cwd[2] == '\\' {
c := cwd[0]
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
return true
}
}
if strings.HasPrefix(cwd, "\\\\") {
return true
}
return false
}
func isInvalidPathBase(name string) bool {
if name == "." || name == ".." || name == "/" || name == string(filepath.Separator) {
return true
}
if strings.ContainsAny(name, "/\\") {
return true
}
return false
}
// findGitRepoRoot walks upward from cwd to find the enclosing git
// repository root. Supports both standard repos (.git directory)
// and linked worktrees/submodules (.git file). When cwd no longer
// exists on disk, sibling directories are checked for worktree
// .git files that can reveal the true repo root.
func findGitRepoRoot(ctx context.Context, cwd string) string {
if cwd == "" {
return ""
}
dir := cwd
cwdMissing := false
if info, err := osStat(dir); err == nil {
if !info.IsDir() {
dir = filepath.Dir(dir)
}
} else {
// Avoid treating non-path strings as cwd.
if !strings.ContainsRune(dir, filepath.Separator) {
return ""
}
cwdMissing = true
dir = filepath.Dir(dir)
}
startDir := dir
// When the original path is gone, walk up to the first
// existing ancestor and check its children for worktree
// .git files. This handles nested worktrees (e.g.
// worktrees/project/branch/cmd/server) where the whole
// subtree may be deleted.
if cwdMissing {
sibDir := dir
for {
if _, err := osStat(sibDir); err == nil {
break
}
parent := filepath.Dir(sibDir)
if parent == sibDir {
break
}
sibDir = parent
}
if root := repoRootFromSiblings(sibDir, cwd); root != "" {
return root
}
}
root, conservative := findGitRepoRootLocal(dir)
if root != "" && !conservative {
return root
}
if !cwdMissing {
if gitRoot := gitMainRoot(ctx, startDir); gitRoot != "" {
return gitRoot
}
}
return root
}
func findGitRepoRootLocal(dir string) (root string, conservative bool) {
for {
gitPath := filepath.Join(dir, ".git")
info, err := osStat(gitPath)
if err == nil {
if info.IsDir() {
return dir, false
}
if info.Mode().IsRegular() {
if root := repoRootFromGitFile(dir, gitPath); root != "" {
if root == dir {
return root, true
}
return root, false
}
// Keep conservative fallback for gitfile repos
// when metadata cannot be parsed.
return dir, true
}
}
parent := filepath.Dir(dir)
if parent == dir {
return "", false
}
dir = parent
}
}
func gitMainRoot(ctx context.Context, dir string) string {
if ctx == nil || dir == "" {
return ""
}
opCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
root, err := gitrepo.MainRoot(opCtx, dir)
if err != nil {
return ""
}
return root
}
// repoRootFromSiblings checks child directories of dir for
// linked-worktree .git files and uses them to discover the
// true repo root. Submodule .git files are skipped, and all
// candidates must agree on the same root to avoid
// misattributing unrelated paths.
func repoRootFromSiblings(dir, cwd string) string {
// If dir is itself a repo or worktree, let the normal
// upward walk handle it.
if _, err := osStat(filepath.Join(dir, ".git")); err == nil {
return ""
}
entries, err := os.ReadDir(dir)
if err != nil {
return ""
}
worktreeMarker := string(filepath.Separator) + ".git" +
string(filepath.Separator) + "worktrees" +
string(filepath.Separator)
// Two-pass scan: first collect linked-worktree roots,
// then optionally include .git directory siblings only
// when worktree evidence exists.
type siblingInfo struct {
root string // resolved repo root
isDir bool // true = .git directory, false = .git file
}
var siblings []siblingInfo
for _, entry := range entries {
if !entry.IsDir() {
continue
}
gitPath := filepath.Join(dir, entry.Name(), ".git")
info, err := osStat(gitPath)
if err != nil {
continue
}
if info.IsDir() {
siblings = append(siblings, siblingInfo{
root: filepath.Join(dir, entry.Name()),
isDir: true,
})
continue
}
if !info.Mode().IsRegular() {
continue
}
gitDir := readGitDirFromFile(gitPath)
if gitDir == "" {
continue
}
if !filepath.IsAbs(gitDir) {
gitDir = filepath.Join(dir, entry.Name(), gitDir)
}
gitDir = filepath.Clean(gitDir)
if !strings.Contains(gitDir, worktreeMarker) {
continue
}
root := repoRootFromGitFile(
filepath.Join(dir, entry.Name()), gitPath,
)
if root == "" {
continue
}
siblings = append(siblings, siblingInfo{
root: root,
isDir: false,
})
}
// Count worktree and directory siblings.
var worktreeCount, dirCount int
var singleDirRoot string
for _, s := range siblings {
if s.isDir {
dirCount++
singleDirRoot = s.root
} else {
worktreeCount++
}
}
// With linked-worktree siblings, all candidates must
// agree on the same root. Without worktree siblings,
// accept a single main checkout only if its
// .git/worktrees/ exists, proving it has (or had)
// linked worktrees.
if worktreeCount == 0 {
if dirCount != 1 {
return ""
}
// Verify the deleted child matches a known worktree
// entry under .git/worktrees/.
if !deletedChildIsWorktree(dir, cwd, singleDirRoot) {
return ""
}
return singleDirRoot
}
var found string
for _, s := range siblings {
if found == "" {
found = s.root
} else if found != s.root {
return ""
}
}
return found
}
// deletedChildIsWorktree checks whether the first missing
// path component (the deleted child under dir) matches an
// entry in the repo's .git/worktrees/ directory.
func deletedChildIsWorktree(
dir, cwd, repoRoot string,
) bool {
rel, err := filepath.Rel(dir, cwd)
if err != nil || rel == "." {
return false
}
child := strings.SplitN(
filepath.ToSlash(rel), "/", 2,
)[0]
if child == "" {
return false
}
wtDir := filepath.Join(repoRoot, ".git", "worktrees")
entries, err := os.ReadDir(wtDir)
if err != nil {
return false
}
for _, e := range entries {
if e.Name() == child {
return true
}
}
return false
}
func repoRootFromGitFile(repoDir, gitFilePath string) string {
gitDir := readGitDirFromFile(gitFilePath)
if gitDir == "" {
return ""
}
if !filepath.IsAbs(gitDir) {
gitDir = filepath.Join(filepath.Dir(gitFilePath), gitDir)
}
gitDir = filepath.Clean(gitDir)
commonDir := readCommonDir(gitDir)
if commonDir != "" {
if filepath.Base(commonDir) == ".git" {
return filepath.Dir(commonDir)
}
}
// Fallback for linked worktrees if commondir is missing.
marker := string(filepath.Separator) + ".git" +
string(filepath.Separator) + "worktrees" +
string(filepath.Separator)
if root, _, found := strings.Cut(gitDir, marker); found {
if root != "" {
return filepath.Clean(root)
}
}
return repoDir
}
func readGitDirFromFile(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
for line := range strings.SplitSeq(string(b), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
const prefix = "gitdir:"
if strings.HasPrefix(strings.ToLower(line), prefix) {
return strings.TrimSpace(line[len(prefix):])
}
}
return ""
}
func readCommonDir(gitDir string) string {
b, err := os.ReadFile(filepath.Join(gitDir, "commondir"))
if err != nil {
return ""
}
value := strings.TrimSpace(string(b))
if value == "" {
return ""
}
if filepath.IsAbs(value) {
return filepath.Clean(value)
}
return filepath.Clean(filepath.Join(gitDir, value))
}
func trimBranchSuffix(name, gitBranch string) string {
branch := strings.TrimSpace(gitBranch)
if name == "" || branch == "" {
return name
}
branch = strings.TrimPrefix(branch, "refs/heads/")
branchToken := normalizeBranchToken(branch)
if branchToken == "" {
return name
}
if isDefaultBranchToken(branchToken) {
return name
}
for _, sep := range []string{"-", "_"} {
suffix := sep + branchToken
if strings.HasSuffix(
strings.ToLower(name),
strings.ToLower(suffix),
) {
base := strings.TrimRight(
name[:len(name)-len(suffix)], "-_",
)
if base != "" {
return base
}
}
}
return name
}
func normalizeBranchToken(branch string) string {
var b strings.Builder
b.Grow(len(branch))
lastDash := false
for _, r := range branch {
switch {
case unicode.IsLetter(r), unicode.IsDigit(r):
b.WriteRune(unicode.ToLower(r))
lastDash = false
case r == '/', r == '-', r == '_', r == '.', unicode.IsSpace(r):
if !lastDash {
b.WriteByte('-')
lastDash = true
}
default:
if !lastDash {
b.WriteByte('-')
lastDash = true
}
}
}
out := strings.Trim(b.String(), "-")
return out
}
func isDefaultBranchToken(branch string) bool {
switch strings.ToLower(strings.TrimSpace(branch)) {
case "main", "master", "trunk", "develop", "dev":
return true
default:
return false
}
}
// NeedsProjectReparse checks if a stored project name looks like
// an un-decoded encoded path that should be re-extracted.
func NeedsProjectReparse(project string) bool {
if strings.HasPrefix(project, "roborev_ci_") {
return true
}
bad := []string{
"_Users", "_home", "_private", "_tmp", "_var",
}
for _, prefix := range bad {
if strings.HasPrefix(project, prefix) {
return true
}
}
return strings.Contains(project, "_var_folders_") ||
strings.Contains(project, "_var_tmp_")
}