feat(spec): support tool aliases (#107)

closes #57

IRIS now automatically read and suggest internal configuration aliases
for Git (`.gitconfig`, `.git/config`) and Cargo (`.cargo/config.toml`)

- Added the `Provider` interface and extended registry for CLI tools
that define aliases
- Git: parse aliases via `git config --show-scope`, supporting raw
tab/space separation and scope differentiation
- Cargo: traverse the directory tree upwards to root to read the
`.cargo/config.toml` configuration
- 0ms hot-path caching: key caching is calculated entirely via `os.Stat`
mtime (no subprocess spawn) combined with a 2-second TTL window to
ensure 0ms latency when typing
- Lookup integration: display alias suggestions at the subcommand level
and automatically expand aliases as the user enters further arguments
(ignore shell pipeline commands starting with `!`)
This commit is contained in:
VERSE
2026-08-02 14:30:46 +07:00
committed by GitHub
parent 476ca3946b
commit 6faa2e3274
10 changed files with 732 additions and 2 deletions
+137
View File
@@ -0,0 +1,137 @@
package alias
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/BurntSushi/toml"
)
type CargoProvider struct {
cacheKey string
cached []AliasEntry
mu sync.Mutex
}
func (p *CargoProvider) ToolName() string {
return "cargo"
}
func (p *CargoProvider) GetAliases(cwd string) []AliasEntry {
key := p.buildCacheKey(cwd)
p.mu.Lock()
defer p.mu.Unlock()
if p.cacheKey != "" && p.cacheKey == key {
return p.cached
}
p.cached = p.parse(cwd)
p.cacheKey = key
return p.cached
}
func (p *CargoProvider) buildCacheKey(cwd string) string {
var sb strings.Builder
sb.WriteString(cwd)
dir := cwd
for {
localConfig := filepath.Join(dir, ".cargo", "config.toml")
if info, err := os.Stat(localConfig); err == nil {
fmt.Fprintf(&sb, "|local:%s", info.ModTime().String())
}
localConfig2 := filepath.Join(dir, ".cargo", "config")
if info, err := os.Stat(localConfig2); err == nil {
fmt.Fprintf(&sb, "|local:%s", info.ModTime().String())
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
if cargoHome := os.Getenv("CARGO_HOME"); cargoHome != "" {
globalConfig := filepath.Join(cargoHome, "config.toml")
if info, err := os.Stat(globalConfig); err == nil {
fmt.Fprintf(&sb, "|global:%s", info.ModTime().String())
}
} else if home, err := os.UserHomeDir(); err == nil {
globalConfig := filepath.Join(home, ".cargo", "config.toml")
if info, err := os.Stat(globalConfig); err == nil {
fmt.Fprintf(&sb, "|global:%s", info.ModTime().String())
}
}
return sb.String()
}
func (p *CargoProvider) parse(cwd string) []AliasEntry {
var rawEntries []AliasEntry
dir := cwd
for {
rawEntries = append(rawEntries, p.parseFile(filepath.Join(dir, ".cargo", "config.toml"), "local")...)
rawEntries = append(rawEntries, p.parseFile(filepath.Join(dir, ".cargo", "config"), "local")...)
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
if cargoHome := os.Getenv("CARGO_HOME"); cargoHome != "" {
rawEntries = append(rawEntries, p.parseFile(filepath.Join(cargoHome, "config.toml"), "global")...)
rawEntries = append(rawEntries, p.parseFile(filepath.Join(cargoHome, "config"), "global")...)
} else if home, err := os.UserHomeDir(); err == nil {
rawEntries = append(rawEntries, p.parseFile(filepath.Join(home, ".cargo", "config.toml"), "global")...)
rawEntries = append(rawEntries, p.parseFile(filepath.Join(home, ".cargo", "config"), "global")...)
}
seen := make(map[string]bool)
var entries []AliasEntry
for _, e := range rawEntries {
if !seen[e.Name] {
seen[e.Name] = true
entries = append(entries, e)
}
}
return entries
}
func (p *CargoProvider) parseFile(path, scope string) []AliasEntry {
var config struct {
Alias map[string]interface{} `toml:"alias"`
}
if _, err := toml.DecodeFile(path, &config); err != nil {
return nil
}
var entries []AliasEntry
for k, v := range config.Alias {
switch val := v.(type) {
case string:
entries = append(entries, AliasEntry{Name: k, Expansion: val, Scope: scope})
case []interface{}:
var parts []string
for _, item := range val {
if s, ok := item.(string); ok {
parts = append(parts, s)
}
}
entries = append(entries, AliasEntry{Name: k, Expansion: strings.Join(parts, " "), Scope: scope})
}
}
return entries
}
func init() {
Register(&CargoProvider{})
}
+166
View File
@@ -0,0 +1,166 @@
package alias
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
type GitProvider struct {
cacheKey string
cached []AliasEntry
lastCheck time.Time
mu sync.Mutex
}
func (p *GitProvider) ToolName() string {
return "git"
}
func (p *GitProvider) GetAliases(cwd string) []AliasEntry {
p.mu.Lock()
defer p.mu.Unlock()
now := time.Now()
if p.cached != nil && now.Sub(p.lastCheck) < 2*time.Second {
return p.cached
}
key := p.buildCacheKey(cwd)
p.lastCheck = now
if p.cacheKey != "" && p.cacheKey == key {
return p.cached
}
p.cached = p.parse(cwd)
p.cacheKey = key
return p.cached
}
func (p *GitProvider) buildCacheKey(cwd string) string {
var sb strings.Builder
sb.WriteString(cwd)
// Check local config via fast os.Stat traversal
if configPath := resolveGitConfigPath(cwd); configPath != "" {
if info, err := os.Stat(configPath); err == nil {
fmt.Fprintf(&sb, "|local:%s", info.ModTime().String())
}
}
// Check global config
if home, err := os.UserHomeDir(); err == nil {
globalConfig := filepath.Join(home, ".gitconfig")
if info, err := os.Stat(globalConfig); err == nil {
fmt.Fprintf(&sb, "|global:%s", info.ModTime().String())
}
}
return sb.String()
}
func resolveGitConfigPath(cwd string) string {
dir := cwd
for dir != "" {
gitPath := filepath.Join(dir, ".git")
info, err := os.Stat(gitPath)
if err == nil {
if info.IsDir() {
return filepath.Join(gitPath, "config")
}
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 filepath.Join(gitDir, "config")
}
}
return filepath.Join(dir, ".git", "config")
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return ""
}
func (p *GitProvider) parse(cwd string) []AliasEntry {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Try with --show-scope first
cmd := exec.CommandContext(ctx, "git", "config", "--get-regexp", "--show-scope", "^alias\\.")
cmd.Dir = cwd
out, err := cmd.Output()
hasScope := true
if err != nil {
ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel2()
// Fallback for older git
cmd = exec.CommandContext(ctx2, "git", "config", "--get-regexp", "^alias\\.")
cmd.Dir = cwd
out, err = cmd.Output()
hasScope = false
if err != nil {
return nil
}
}
return p.parseOutput(out, hasScope)
}
func (p *GitProvider) parseOutput(out []byte, hasScope bool) []AliasEntry {
var entries []AliasEntry
lines := strings.Split(string(bytes.TrimSpace(out)), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if hasScope {
idx := strings.IndexAny(line, " \t")
if idx == -1 {
continue
}
scope := line[:idx]
rest := strings.TrimSpace(line[idx:])
parts := strings.SplitN(rest, " ", 2)
if len(parts) == 2 {
name := strings.TrimPrefix(parts[0], "alias.")
entries = append(entries, AliasEntry{
Name: name,
Expansion: parts[1],
Scope: scope,
})
}
} else {
parts := strings.SplitN(line, " ", 2)
if len(parts) == 2 {
name := strings.TrimPrefix(parts[0], "alias.")
entries = append(entries, AliasEntry{
Name: name,
Expansion: parts[1],
Scope: "",
})
}
}
}
return entries
}
func init() {
Register(&GitProvider{})
}
+40
View File
@@ -0,0 +1,40 @@
package alias
import (
"sync"
)
type AliasEntry struct {
Name string
Expansion string
Scope string // "local" | "global"
}
type Provider interface {
ToolName() string
GetAliases(cwd string) []AliasEntry
}
var (
providers = make(map[string]Provider)
mu sync.RWMutex
)
func Register(p Provider) {
mu.Lock()
defer mu.Unlock()
providers[p.ToolName()] = p
}
// Reset clears the registered providers map, restoring it to its initial empty state.
func Reset() {
mu.Lock()
defer mu.Unlock()
providers = make(map[string]Provider)
}
func GetProvider(toolName string) Provider {
mu.RLock()
defer mu.RUnlock()
return providers[toolName]
}
+123
View File
@@ -0,0 +1,123 @@
package alias
import (
"os"
"path/filepath"
"testing"
)
func TestCargoProvider(t *testing.T) {
t.Setenv("CARGO_HOME", t.TempDir())
tempDir := t.TempDir()
cargoDir := filepath.Join(tempDir, ".cargo")
if err := os.Mkdir(cargoDir, 0755); err != nil {
t.Fatal(err)
}
configToml := `
[alias]
b = "build"
c = "check"
t = ["test", "--", "--nocapture"]
`
if err := os.WriteFile(filepath.Join(cargoDir, "config.toml"), []byte(configToml), 0644); err != nil {
t.Fatal(err)
}
p := &CargoProvider{}
aliases := p.parse(tempDir)
expected := map[string]string{
"b": "build",
"c": "check",
"t": "test -- --nocapture",
}
aliasMap := make(map[string]string)
for _, a := range aliases {
aliasMap[a.Name] = a.Expansion
}
for k, wantExp := range expected {
gotExp, ok := aliasMap[k]
if !ok {
t.Errorf("expected alias %q to be present", k)
} else if gotExp != wantExp {
t.Errorf("expected alias %q to map to %q, got %q", k, wantExp, gotExp)
}
}
}
func TestGitProviderParseOutput(t *testing.T) {
outputWithScope := []byte("global\talias.recent !git for-each-ref --sort=committerdate --format=\"%(committerdate:relative) %(refname:short)\" refs/heads/ | tail -10\nlocal\talias.co checkout\n")
p := &GitProvider{}
entries := p.parseOutput(outputWithScope, true)
if len(entries) != 2 {
t.Fatalf("expected 2 entries, got %d: %v", len(entries), entries)
}
if entries[0].Name != "recent" {
t.Errorf("expected name 'recent', got %q", entries[0].Name)
}
if entries[0].Scope != "global" {
t.Errorf("expected scope 'global', got %q", entries[0].Scope)
}
if entries[0].Expansion != "!git for-each-ref --sort=committerdate --format=\"%(committerdate:relative) %(refname:short)\" refs/heads/ | tail -10" {
t.Errorf("unexpected expansion: %q", entries[0].Expansion)
}
if entries[1].Name != "co" {
t.Errorf("expected name 'co', got %q", entries[1].Name)
}
if entries[1].Scope != "local" {
t.Errorf("expected scope 'local', got %q", entries[1].Scope)
}
if entries[1].Expansion != "checkout" {
t.Errorf("expected expansion 'checkout', got %q", entries[1].Expansion)
}
}
func TestGetProvider_UnregisteredCommand(t *testing.T) {
p := GetProvider("unregistered_command_xyz")
if p != nil {
t.Errorf("expected nil for unregistered command, got %v", p)
}
}
func TestResolveGitConfigPath_WalkUp(t *testing.T) {
tempDir := t.TempDir()
gitDir := filepath.Join(tempDir, ".git")
_ = os.Mkdir(gitDir, 0755)
configPath := filepath.Join(gitDir, "config")
_ = os.WriteFile(configPath, []byte("[alias]\nst = status"), 0644)
subDir := filepath.Join(tempDir, "a", "b", "c")
_ = os.MkdirAll(subDir, 0755)
resolved := resolveGitConfigPath(subDir)
if resolved != configPath {
t.Errorf("expected %q, got %q", configPath, resolved)
}
}
func TestResolveGitConfigPath_WorktreeFile(t *testing.T) {
tempDir := t.TempDir()
mainGitDir := filepath.Join(tempDir, "mainrepo", ".git")
_ = os.MkdirAll(mainGitDir, 0755)
mainConfig := filepath.Join(mainGitDir, "config")
_ = os.WriteFile(mainConfig, []byte("[alias]\nst = status"), 0644)
wtDir := filepath.Join(tempDir, "wt")
_ = os.MkdirAll(wtDir, 0755)
wtGitFile := filepath.Join(wtDir, ".git")
_ = os.WriteFile(wtGitFile, []byte("gitdir: "+mainGitDir), 0644)
wtSubDir := filepath.Join(wtDir, "sub")
_ = os.MkdirAll(wtSubDir, 0755)
resolved := resolveGitConfigPath(wtSubDir)
if resolved != mainConfig {
t.Errorf("expected %q, got %q", mainConfig, resolved)
}
}