add skills command scaffold

This commit is contained in:
tommaso-moro 2026-03-30 17:26:41 +01:00 committed by Sam Morrow
parent f79cc02bdd
commit e57fb436fa
No known key found for this signature in database
11 changed files with 2206 additions and 0 deletions

View file

@ -0,0 +1,149 @@
// Package gitclient provides a shared adapter from the cli/cli git.Client
// (via cmdutil.Factory) to the narrow interfaces used by skills commands.
package gitclient
import (
"context"
"os"
"strings"
"github.com/cli/cli/v2/pkg/cmdutil"
)
// RootResolver can resolve the git repository root directory.
type RootResolver interface {
ToplevelDir() (string, error)
}
// RemoteResolver can resolve git remote URLs.
type RemoteResolver interface {
RemoteURL(name string) (string, error)
}
// Client is the full git operations interface used by skills commands.
type Client interface {
RootResolver
RemoteResolver
GitDir(dir string) error
Remotes() ([]string, error)
CurrentBranch(dir string) (string, error)
IsIgnored(dir, path string) bool
}
// FactoryClient adapts the cli/cli git.Client to the Client interface.
type FactoryClient struct {
F *cmdutil.Factory
}
// ToplevelDir returns the root directory of the current git repository.
func (g *FactoryClient) ToplevelDir() (string, error) {
cmd, err := g.F.GitClient.Command(context.Background(), "rev-parse", "--show-toplevel")
if err != nil {
return "", err
}
out, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// RemoteURL returns the URL configured for the named git remote.
func (g *FactoryClient) RemoteURL(name string) (string, error) {
cmd, err := g.F.GitClient.Command(context.Background(), "remote", "get-url", name)
if err != nil {
return "", err
}
out, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// GitDir validates that the given directory is inside a git repository.
func (g *FactoryClient) GitDir(dir string) error {
cmd, err := g.F.GitClient.Command(context.Background(), "-C", dir, "rev-parse", "--git-dir")
if err != nil {
return err
}
_, err = cmd.Output()
return err
}
// Remotes returns the list of configured git remote names.
func (g *FactoryClient) Remotes() ([]string, error) {
cmd, err := g.F.GitClient.Command(context.Background(), "remote")
if err != nil {
return nil, err
}
out, err := cmd.Output()
if err != nil {
return nil, err
}
return strings.Fields(string(out)), nil
}
// CurrentBranch returns the current branch name, or "" if HEAD is detached.
func (g *FactoryClient) CurrentBranch(dir string) (string, error) {
cmd, err := g.F.GitClient.Command(context.Background(), "-C", dir, "rev-parse", "--abbrev-ref", "HEAD")
if err != nil {
return "", err
}
out, err := cmd.Output()
if err != nil {
return "", err
}
branch := strings.TrimSpace(string(out))
if branch == "HEAD" {
return "", nil // detached HEAD
}
return branch, nil
}
// IsIgnored reports whether the given path is git-ignored in the given directory.
func (g *FactoryClient) IsIgnored(dir, path string) bool {
cmd, err := g.F.GitClient.Command(context.Background(), "-C", dir, "check-ignore", "-q", path)
if err != nil {
return false
}
_, err = cmd.Output()
return err == nil
}
// ResolveGitRoot returns the git repository root using the provided resolver,
// falling back to the current working directory on error.
func ResolveGitRoot(resolver RootResolver) string {
if resolver == nil {
if cwd, err := os.Getwd(); err == nil {
return cwd
}
return ""
}
root, err := resolver.ToplevelDir()
if err != nil {
if cwd, cwdErr := os.Getwd(); cwdErr == nil {
return cwd
}
return ""
}
return root
}
// ResolveHomeDir returns the user's home directory, or "" on error.
func ResolveHomeDir() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return home
}
// TruncateSHA returns the first 8 characters of a SHA, or the full string
// if it is shorter.
func TruncateSHA(sha string) string {
if len(sha) > 8 {
return sha[:8]
}
return sha
}

View file

@ -0,0 +1,49 @@
package gitclient
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type mockResolver struct {
root string
err error
}
func (m *mockResolver) ToplevelDir() (string, error) {
if m.err != nil {
return "", m.err
}
return m.root, nil
}
func TestResolveGitRoot(t *testing.T) {
t.Run("returns root on success", func(t *testing.T) {
got := ResolveGitRoot(&mockResolver{root: "/my/repo"})
assert.Equal(t, "/my/repo", got)
})
t.Run("falls back to cwd on error", func(t *testing.T) {
got := ResolveGitRoot(&mockResolver{err: fmt.Errorf("not a git repo")})
assert.NotEmpty(t, got) // falls back to cwd
})
t.Run("nil resolver falls back to cwd", func(t *testing.T) {
got := ResolveGitRoot(nil)
assert.NotEmpty(t, got) // falls back to cwd
})
}
func TestResolveHomeDir(t *testing.T) {
got := ResolveHomeDir()
assert.NotEmpty(t, got)
}
func TestTruncateSHA(t *testing.T) {
assert.Equal(t, "abcdef12", TruncateSHA("abcdef1234567890"))
assert.Equal(t, "short", TruncateSHA("short"))
assert.Equal(t, "12345678", TruncateSHA("12345678"))
assert.Equal(t, "", TruncateSHA(""))
}