feat(git): more commands, tests, add Registry conflict between tests
This commit is contained in:
@@ -44,3 +44,8 @@ var Registry = map[string]*Spec{}
|
||||
func Register(s *Spec) {
|
||||
Registry[s.Name] = s
|
||||
}
|
||||
|
||||
// ResetRegistry clears all registered specs — use in tests only
|
||||
func ResetRegistry() {
|
||||
Registry = make(map[string]*Spec)
|
||||
}
|
||||
|
||||
+139
-54
@@ -13,40 +13,9 @@ func GitRemoteGenerator(tokens []string, prefix string, partial string) []core.S
|
||||
return getGitResults(prefix, "remote")
|
||||
}
|
||||
|
||||
// GitBranchGenerator suggests git branches
|
||||
func GitBranchGenerator(tokens []string, prefix string, partial string) []core.Suggestion {
|
||||
// check if we are in "create" mode (-b or -B)
|
||||
isCreateMode := false
|
||||
argCount := 0
|
||||
for _, t := range tokens {
|
||||
if t == "-b" || t == "-B" {
|
||||
isCreateMode = true
|
||||
}
|
||||
// count non-flag, non-command tokens to see where we are
|
||||
if t != "git" && t != "checkout" && t != "switch" && !strings.HasPrefix(t, "-") {
|
||||
argCount++
|
||||
}
|
||||
}
|
||||
|
||||
// if in create mode and this is the first argument after flags,
|
||||
// we shouldn't suggest existing branches as the new branch name
|
||||
if isCreateMode && argCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return getGitResults(prefix, "branch", "--format=%(refname:short)")
|
||||
}
|
||||
|
||||
// GitPushPullGenerator suggests remotes for the first arg, and branches for the second
|
||||
func GitPushPullGenerator(tokens []string, prefix string, partial string) []core.Suggestion {
|
||||
// tokens are like ["git", "push"] or ["git", "push", "origin"]
|
||||
if len(tokens) == 2 {
|
||||
return GitRemoteGenerator(tokens, prefix, partial)
|
||||
}
|
||||
if len(tokens) == 3 {
|
||||
return GitBranchGenerator(tokens, prefix, partial)
|
||||
}
|
||||
return nil
|
||||
// GitStashGenerator suggests git stashes
|
||||
func GitStashGenerator(tokens []string, prefix string, partial string) []core.Suggestion {
|
||||
return getGitResults(prefix, "stash", "list", "--format=%gd: %gs")
|
||||
}
|
||||
|
||||
func getGitResults(prefix string, args ...string) []core.Suggestion {
|
||||
@@ -58,6 +27,17 @@ func getGitResults(prefix string, args ...string) []core.Suggestion {
|
||||
return nil
|
||||
}
|
||||
|
||||
activeBranch := ""
|
||||
switch args[0] {
|
||||
case "branch":
|
||||
// Try to find the current active branch to filter it out later
|
||||
activeCmd := exec.CommandContext(context.Background(), "git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
activeCmd.Dir = cwd
|
||||
if activeOut, err := activeCmd.Output(); err == nil {
|
||||
activeBranch = strings.TrimSpace(string(activeOut))
|
||||
}
|
||||
}
|
||||
|
||||
lines := strings.Split(string(out), "\n")
|
||||
var results []core.Suggestion
|
||||
for _, line := range lines {
|
||||
@@ -65,17 +45,79 @@ func getGitResults(prefix string, args ...string) []core.Suggestion {
|
||||
if line == "" || strings.HasPrefix(line, "*") { // skip active branch marker if any
|
||||
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
|
||||
}
|
||||
if line == "" {
|
||||
if line == "" || line == activeBranch {
|
||||
continue
|
||||
}
|
||||
|
||||
// handle remote branches that look like "remotes/origin/main"
|
||||
line = strings.TrimPrefix(line, "remotes/")
|
||||
|
||||
suggestionCmd := line
|
||||
suggestionDesc := args[0]
|
||||
|
||||
// for stash list, the format is "stash@{0}: message"
|
||||
if args[0] == "stash" {
|
||||
parts := strings.SplitN(line, ": ", 2)
|
||||
if len(parts) == 2 {
|
||||
suggestionCmd = parts[0]
|
||||
suggestionDesc = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, core.Suggestion{
|
||||
Cmd: prefix + " " + line,
|
||||
Desc: args[0], // "remote" or "branch"
|
||||
Cmd: prefix + " " + suggestionCmd,
|
||||
Desc: suggestionDesc,
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// GitBranchGenerator suggests git branches
|
||||
func GitBranchGenerator(tokens []string, prefix string, partial string) []core.Suggestion {
|
||||
// check if we are in "create" mode (-b or -B or -c)
|
||||
isCreateMode := false
|
||||
for _, t := range tokens {
|
||||
if t == "-b" || t == "-B" || t == "-c" || t == "-C" {
|
||||
isCreateMode = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isCreateMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
return getGitResults(prefix, "branch", "-a", "--format=%(refname:short)")
|
||||
}
|
||||
|
||||
// GitPushPullGenerator suggests remotes for the first arg, and branches for the second
|
||||
func GitPushPullGenerator(tokens []string, prefix string, partial string) []core.Suggestion {
|
||||
// Filter out flags to find positional arguments
|
||||
args := []string{}
|
||||
for i := 1; i < len(tokens); i++ {
|
||||
t := tokens[i]
|
||||
if t != "" && !strings.HasPrefix(t, "-") {
|
||||
args = append(args, t)
|
||||
}
|
||||
}
|
||||
|
||||
// args[0] is subcommand (push/pull)
|
||||
// args[1] should be remote
|
||||
// args[2] should be branch
|
||||
|
||||
// If we only have subcommand, suggest remotes
|
||||
if len(args) == 1 {
|
||||
return GitRemoteGenerator(tokens, prefix, partial)
|
||||
}
|
||||
|
||||
// If we have subcommand + remote, suggest branches
|
||||
if len(args) == 2 {
|
||||
return GitBranchGenerator(tokens, prefix, partial)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
core.Register(&core.Spec{
|
||||
Name: "git",
|
||||
@@ -160,7 +202,16 @@ func init() {
|
||||
{
|
||||
Name: "checkout",
|
||||
Description: "switch branches",
|
||||
Generator: GitBranchGenerator,
|
||||
Generator: func(tokens []string, prefix string, partial string) []core.Suggestion {
|
||||
for _, t := range tokens {
|
||||
if t == "-b" || t == "-B" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
branches := GitBranchGenerator(tokens, prefix, partial)
|
||||
files := core.FileGenerator()(tokens, prefix, partial)
|
||||
return append(branches, files...)
|
||||
},
|
||||
Options: []core.Option{
|
||||
{Name: "-b", Description: "create new branch"},
|
||||
},
|
||||
@@ -197,6 +248,7 @@ func init() {
|
||||
{
|
||||
Name: "rebase",
|
||||
Description: "reapply commits",
|
||||
Generator: GitBranchGenerator,
|
||||
Options: []core.Option{
|
||||
{Name: "-i", Description: "interactive"},
|
||||
{Name: "--onto", Description: "rebase onto"},
|
||||
@@ -207,6 +259,7 @@ func init() {
|
||||
{
|
||||
Name: "log",
|
||||
Description: "show commit log",
|
||||
Generator: core.FileGenerator(),
|
||||
Options: []core.Option{
|
||||
{Name: "--oneline", Description: "compact format"},
|
||||
{Name: "--graph", Description: "show graph"},
|
||||
@@ -216,25 +269,33 @@ func init() {
|
||||
{
|
||||
Name: "diff",
|
||||
Description: "show changes",
|
||||
Generator: core.FileGenerator(),
|
||||
Options: []core.Option{
|
||||
{Name: "--staged", Description: "staged changes"},
|
||||
{Name: "--stat", Description: "diffstat only"},
|
||||
{Name: "--", Description: "separate paths"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stash",
|
||||
Description: "stash changes",
|
||||
Subcommands: []core.Subcommand{
|
||||
{Name: "pop", Description: "apply and drop"},
|
||||
{Name: "apply", Description: "apply stash"},
|
||||
{Name: "drop", Description: "remove stash"},
|
||||
{Name: "list", Description: "list stashes"},
|
||||
{Name: "show", Description: "show stash diff"},
|
||||
Name: "tag",
|
||||
Description: "manage tags",
|
||||
Generator: func(tokens []string, prefix string, partial string) []core.Suggestion { return getGitResults(prefix, "tag", "-l") },
|
||||
Options: []core.Option{
|
||||
{Name: "-a", Description: "annotated tag"},
|
||||
{Name: "-d", Description: "delete tag"},
|
||||
{Name: "-l", Description: "list tags"},
|
||||
{Name: "--delete", Description: "delete tag"},
|
||||
{Name: "-m", Description: "tag message"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "reset",
|
||||
Description: "reset HEAD",
|
||||
Generator: func(tokens []string, prefix string, partial string) []core.Suggestion {
|
||||
branches := GitBranchGenerator(tokens, prefix, partial)
|
||||
files := core.FileGenerator()(tokens, prefix, partial)
|
||||
return append(branches, files...)
|
||||
},
|
||||
Options: []core.Option{
|
||||
{Name: "--hard", Description: "discard changes"},
|
||||
{Name: "--soft", Description: "keep staged"},
|
||||
@@ -242,21 +303,45 @@ func init() {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tag",
|
||||
Description: "manage tags",
|
||||
Name: "restore",
|
||||
Description: "restore working tree files",
|
||||
Generator: core.FileGenerator(),
|
||||
Options: []core.Option{
|
||||
{Name: "-a", Description: "annotated tag"},
|
||||
{Name: "-d", Description: "delete tag"},
|
||||
{Name: "-l", Description: "list tags"},
|
||||
{Name: "-s", Description: "source tree"},
|
||||
{Name: "-W", Description: "working tree"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "rm",
|
||||
Description: "remove files",
|
||||
Generator: core.FileGenerator(),
|
||||
Options: []core.Option{
|
||||
{Name: "-f", Description: "force"},
|
||||
{Name: "-r", Description: "recursive"},
|
||||
{Name: "--cached", Description: "unstage only"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stash",
|
||||
Description: "stash changes",
|
||||
Subcommands: []core.Subcommand{
|
||||
{Name: "pop", Description: "apply and drop", Generator: GitStashGenerator, Options: []core.Option{{Name: "--index", Description: "try to reinstate index"}}},
|
||||
{Name: "apply", Description: "apply stash", Generator: GitStashGenerator},
|
||||
{Name: "drop", Description: "remove stash", Generator: GitStashGenerator},
|
||||
{Name: "list", Description: "list stashes"},
|
||||
{Name: "show", Description: "show stash diff", Generator: GitStashGenerator},
|
||||
{Name: "push", Description: "push to stash"},
|
||||
{Name: "branch", Description: "create branch from stash", Generator: GitBranchGenerator},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "remote",
|
||||
Description: "manage remotes",
|
||||
Subcommands: []core.Subcommand{
|
||||
{Name: "add", Description: "add remote"},
|
||||
{Name: "remove", Description: "remove remote"},
|
||||
{Name: "rename", Description: "rename remote"},
|
||||
{Name: "add", Description: "add remote", Options: []core.Option{{Name: "-f", Description: "fetch immediately"}}},
|
||||
{Name: "remove", Description: "remove remote", Generator: GitRemoteGenerator},
|
||||
{Name: "rename", Description: "rename remote", Generator: GitRemoteGenerator},
|
||||
{Name: "set-url", Description: "change remote url", Generator: GitRemoteGenerator},
|
||||
{Name: "-v", Description: "verbose list"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/versenilvis/iris/commands/core"
|
||||
_ "github.com/versenilvis/iris/commands/dev"
|
||||
)
|
||||
|
||||
func TestGitSuggestions(t *testing.T) {
|
||||
// Setup a real git repo in temp dir for testing branch generators
|
||||
tmp := t.TempDir()
|
||||
oldWd, _ := os.Getwd()
|
||||
_ = os.Chdir(tmp)
|
||||
defer func() { _ = os.Chdir(oldWd) }()
|
||||
|
||||
ctx := context.Background()
|
||||
// Initialize git repo
|
||||
_ = exec.CommandContext(ctx, "git", "init").Run()
|
||||
_ = os.WriteFile(filepath.Join(tmp, "file.go"), []byte("package main"), 0644)
|
||||
_ = exec.CommandContext(ctx, "git", "add", ".").Run()
|
||||
_ = exec.CommandContext(ctx, "git", "commit", "-m", "initial").Run()
|
||||
|
||||
// Create branches
|
||||
_ = exec.CommandContext(ctx, "git", "branch", "feature/login").Run()
|
||||
_ = exec.CommandContext(ctx, "git", "branch", "dev").Run()
|
||||
_ = exec.CommandContext(ctx, "git", "tag", "v1.0").Run()
|
||||
|
||||
// Setup stash
|
||||
_ = os.WriteFile(filepath.Join(tmp, "dirty.go"), []byte("dirty"), 0644)
|
||||
_ = exec.CommandContext(ctx, "git", "add", ".").Run()
|
||||
_ = exec.CommandContext(ctx, "git", "stash").Run()
|
||||
|
||||
t.Run("git top-level", func(t *testing.T) {
|
||||
res := core.Lookup("git ")
|
||||
if len(res) < 10 {
|
||||
t.Errorf("Expected many git subcommands, got %d", len(res))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("git tag -d show tags", func(t *testing.T) {
|
||||
res := core.Lookup("git tag -d ")
|
||||
found := false
|
||||
for _, r := range res {
|
||||
if strings.Contains(r.Cmd, "v1.0") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("git tag -d should suggest existing tags")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("git push HEAD options", func(t *testing.T) {
|
||||
// git push origin HEAD --force -> should show --force
|
||||
res := core.Lookup("git push origin HEAD --")
|
||||
found := false
|
||||
for _, r := range res {
|
||||
if strings.Contains(r.Cmd, "--force") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("git push origin HEAD -- should suggest --force")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("git push upstream options", func(t *testing.T) {
|
||||
// git push -u origin -> show branches
|
||||
res := core.Lookup("git push -u origin ")
|
||||
found := false
|
||||
for _, r := range res {
|
||||
if strings.Contains(r.Cmd, "dev") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("git push -u origin should suggest branches")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("git reset options", func(t *testing.T) {
|
||||
// git reset --soft origin/main -> should be accepted (just testing lookup doesn't crash)
|
||||
_ = core.Lookup("git reset --soft origin/main ")
|
||||
|
||||
// git reset HEAD -> show files
|
||||
res := core.Lookup("git reset HEAD ")
|
||||
found := false
|
||||
for _, r := range res {
|
||||
if strings.Contains(r.Cmd, "file.go") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("git reset HEAD should suggest file.go")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("git checkout -b no suggest", func(t *testing.T) {
|
||||
// git checkout -b -> should NOT suggest branches
|
||||
res := core.Lookup("git checkout -b ")
|
||||
for _, r := range res {
|
||||
if strings.Contains(r.Cmd, "dev") {
|
||||
t.Error("git checkout -b should not suggest existing branches")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("git switch -c no suggest", func(t *testing.T) {
|
||||
res := core.Lookup("git switch -c ")
|
||||
for _, r := range res {
|
||||
if strings.Contains(r.Cmd, "dev") {
|
||||
t.Error("git switch -c should not suggest existing branches")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stash entries", func(t *testing.T) {
|
||||
res := core.Lookup("git stash pop ")
|
||||
found := false
|
||||
for _, r := range res {
|
||||
if strings.Contains(r.Cmd, "stash@{0}") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("git stash pop should suggest stash@{0}")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not a git repo", func(t *testing.T) {
|
||||
emptyDir := t.TempDir()
|
||||
_ = os.Chdir(emptyDir)
|
||||
// Should not crash
|
||||
_ = core.Lookup("git status ")
|
||||
_ = os.Chdir(tmp)
|
||||
})
|
||||
|
||||
t.Run("active branch filter", func(t *testing.T) {
|
||||
// find current branch
|
||||
out, _ := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD").Output()
|
||||
current := strings.TrimSpace(string(out))
|
||||
|
||||
res := core.Lookup("git checkout ")
|
||||
for _, r := range res {
|
||||
if strings.Contains(r.Cmd, current) && !strings.Contains(r.Cmd, "remotes/") {
|
||||
t.Errorf("Should not suggest active branch '%s'", current)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user