feat: deterministic match_dirs tie-breaking by config file order
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / functional (push) Blocked by required conditions

This commit is contained in:
sid 2026-03-23 12:55:49 -06:00
parent ac780231a8
commit 830eba1c0e
2 changed files with 113 additions and 0 deletions

View file

@ -20,6 +20,7 @@ type HostConfig struct {
User string `yaml:"user,omitempty"`
GitProtocol string `yaml:"git_protocol,omitempty"`
MatchDirs []string `yaml:"match_dirs,omitempty"`
Order int `yaml:"-"` // config file order, set at load time
}
func GetConfigDir() (string, error) {
@ -67,9 +68,43 @@ func LoadFromPath(path string) (*Config, error) {
cfg.Hosts = make(map[string]HostConfig)
}
// Parse again with yaml.Node to capture config file order for hosts
assignHostOrder(&cfg, data)
return &cfg, nil
}
// assignHostOrder walks the YAML document tree to find the "hosts" mapping
// and stamps each HostConfig.Order with its position in the file.
func assignHostOrder(cfg *Config, data []byte) {
var doc yaml.Node
if err := yaml.Unmarshal(data, &doc); err != nil || len(doc.Content) == 0 {
return
}
root := doc.Content[0] // mapping node
if root.Kind != yaml.MappingNode {
return
}
for i := 0; i+1 < len(root.Content); i += 2 {
if root.Content[i].Value == "hosts" {
hostsNode := root.Content[i+1]
if hostsNode.Kind != yaml.MappingNode {
return
}
order := 0
for j := 0; j+1 < len(hostsNode.Content); j += 2 {
key := hostsNode.Content[j].Value
if h, ok := cfg.Hosts[key]; ok {
h.Order = order
cfg.Hosts[key] = h
order++
}
}
return
}
}
}
func (c *Config) Save() error {
path, err := GetConfigPath()
if err != nil {
@ -133,6 +168,8 @@ func (c *Config) GetHost(hostname string, detectedHost string, cwd string) (Host
// prefix of cwd. Returns "" if no match is found.
// Both cwd and match_dirs entries are resolved through filepath.EvalSymlinks
// to handle symlinks (e.g. macOS /tmp → /private/tmp).
// On ties (same prefix length from multiple hosts), the host appearing first
// in the config file wins and a warning is printed to stderr.
func (c *Config) ResolveHostByPath(cwd string) string {
if cwd == "" {
return ""
@ -145,6 +182,8 @@ func (c *Config) ResolveHostByPath(cwd string) string {
bestHost := ""
bestLen := 0
bestOrder := 0
tied := false
for hostname, host := range c.Hosts {
for _, dir := range host.MatchDirs {
@ -167,11 +206,24 @@ func (c *Config) ResolveHostByPath(cwd string) string {
if len(dir) > bestLen {
bestLen = len(dir)
bestHost = hostname
bestOrder = host.Order
tied = false
} else if len(dir) == bestLen && hostname != bestHost {
// Tie — pick the host with the lower Order (earlier in config)
if host.Order < bestOrder {
bestHost = hostname
bestOrder = host.Order
}
tied = true
}
}
}
}
if tied {
fmt.Fprintf(os.Stderr, "warning: multiple hosts match directory %q with the same specificity; using %s (first in config)\n", cwd, bestHost)
}
return bestHost
}