diff --git a/docs/plans/2026-02-13-dynamic-shell-completions.md b/docs/plans/2026-02-13-dynamic-shell-completions.md new file mode 100644 index 0000000..1f8c3ff --- /dev/null +++ b/docs/plans/2026-02-13-dynamic-shell-completions.md @@ -0,0 +1,728 @@ +# Dynamic Shell Completions Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add dynamic shell completions so pressing Tab suggests actual values (workspace names, repo names, branch names, PR numbers, enum values) instead of only completing command/flag names. + +**Architecture:** Create a shared `internal/cmdutil/completion.go` helper module with reusable completion functions. Register completions via Cobra's `RegisterFlagCompletionFunc` (for flags) and `ValidArgsFunction` (for positional args). API-backed completions silently return no suggestions on auth/network failure. + +**Tech Stack:** Go, Cobra CLI (spf13/cobra) `RegisterFlagCompletionFunc` + `ValidArgsFunction` + `cobra.ShellCompDirective*` + +--- + +## Task 1: Create the Completion Helper Module + +**Files:** +- Create: `internal/cmdutil/completion.go` +- Create: `internal/cmdutil/completion_test.go` + +This module provides reusable functions that commands will call to register completions. + +**Step 1: Write the test file** + +```go +// internal/cmdutil/completion_test.go +package cmdutil + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestCompleteNoFiles(t *testing.T) { + // Verify that the noFiles directive is always included + got := cobra.ShellCompDirectiveNoFileComp + if got == cobra.ShellCompDirectiveDefault { + t.Error("expected NoFileComp directive, got Default") + } +} + +func TestCompleteStaticValues(t *testing.T) { + values := []string{"OPEN", "MERGED", "DECLINED"} + fn := StaticFlagCompletion(values) + completions, directive := fn(nil, nil, "") + + if directive != cobra.ShellCompDirectiveNoFileComp { + t.Errorf("expected NoFileComp directive, got %d", directive) + } + + if len(completions) != 3 { + t.Errorf("expected 3 completions, got %d", len(completions)) + } + + if completions[0] != "OPEN" || completions[1] != "MERGED" || completions[2] != "DECLINED" { + t.Errorf("unexpected completions: %v", completions) + } +} + +func TestCompleteStaticValuesFiltering(t *testing.T) { + values := []string{"OPEN", "MERGED", "DECLINED"} + fn := StaticFlagCompletion(values) + completions, _ := fn(nil, nil, "M") + + if len(completions) != 1 { + t.Errorf("expected 1 completion, got %d: %v", len(completions), completions) + } + if len(completions) > 0 && completions[0] != "MERGED" { + t.Errorf("expected MERGED, got %s", completions[0]) + } +} +``` + +**Step 2: Run the test to verify it fails** + +```bash +go test ./internal/cmdutil/ -run TestCompleteStaticValues -v +``` + +Expected: FAIL - `StaticFlagCompletion` undefined. + +**Step 3: Write the implementation** + +```go +// internal/cmdutil/completion.go +package cmdutil + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/rbansal42/bitbucket-cli/internal/api" + "github.com/rbansal42/bitbucket-cli/internal/config" + "github.com/rbansal42/bitbucket-cli/internal/git" +) + +// noFileComp is a shorthand for the "don't complete filenames" directive. +var noFileComp = cobra.ShellCompDirectiveNoFileComp + +// StaticFlagCompletion returns a completion function for a fixed set of values. +// It filters values by the current prefix typed by the user (toComplete). +func StaticFlagCompletion(values []string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var filtered []string + for _, v := range values { + if strings.HasPrefix(strings.ToUpper(v), strings.ToUpper(toComplete)) { + filtered = append(filtered, v) + } + } + return filtered, noFileComp + } +} + +// completionCtx returns a context with a short timeout suitable for completion. +func completionCtx() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 5*time.Second) +} + +// completionClient returns an API client for use during completion. +// Returns nil if authentication is not configured (user not logged in). +func completionClient() *api.Client { + client, err := GetAPIClient() + if err != nil { + return nil + } + return client +} + +// completionRepo resolves workspace and repoSlug from the --repo flag or git remote. +// Returns empty strings if resolution fails (not in a git repo, etc.). +func completionRepo(cmd *cobra.Command) (workspace, repoSlug string) { + repoFlag, _ := cmd.Flags().GetString("repo") + ws, slug, err := ParseRepository(repoFlag) + if err != nil { + return "", "" + } + return ws, slug +} + +// completionWorkspace resolves the workspace from the --workspace flag, default config, or git remote. +func completionWorkspace(cmd *cobra.Command) string { + ws, _ := cmd.Flags().GetString("workspace") + if ws != "" { + return ws + } + defaultWs, err := config.GetDefaultWorkspace() + if err == nil && defaultWs != "" { + return defaultWs + } + remote, err := git.GetDefaultRemote() + if err == nil { + return remote.Workspace + } + return "" +} + +// CompleteWorkspaceNames returns workspace slugs for the authenticated user. +func CompleteWorkspaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, noFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListWorkspaces(ctx, &api.WorkspaceListOptions{Limit: 50}) + if err != nil { + return nil, noFileComp + } + + var names []string + for _, ws := range result.Values { + slug := ws.Workspace.Slug + if strings.HasPrefix(slug, toComplete) { + names = append(names, slug) + } + } + return names, noFileComp +} + +// CompleteRepoNames returns repository full names (workspace/repo) for the current workspace. +func CompleteRepoNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, noFileComp + } + + // Determine workspace: from typed prefix, --workspace flag, default config, or git remote + ws := "" + if strings.Contains(toComplete, "/") { + parts := strings.SplitN(toComplete, "/", 2) + ws = parts[0] + toComplete = parts[1] + } + if ws == "" { + ws = completionWorkspace(cmd) + } + if ws == "" { + return nil, noFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListRepositories(ctx, ws, &api.RepositoryListOptions{Limit: 50}) + if err != nil { + return nil, noFileComp + } + + var names []string + for _, repo := range result.Values { + fullName := fmt.Sprintf("%s/%s", ws, repo.Slug) + if strings.HasPrefix(repo.Slug, toComplete) || strings.HasPrefix(fullName, toComplete) { + names = append(names, fullName) + } + } + return names, noFileComp +} + +// CompleteBranchNames returns branch names for the resolved repository. +func CompleteBranchNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, noFileComp + } + + ws, slug := completionRepo(cmd) + if ws == "" || slug == "" { + return nil, noFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListBranches(ctx, ws, slug, &api.BranchListOptions{Limit: 50}) + if err != nil { + return nil, noFileComp + } + + var names []string + for _, branch := range result.Values { + if strings.HasPrefix(branch.Name, toComplete) { + names = append(names, branch.Name) + } + } + return names, noFileComp +} + +// CompletePRNumbers returns open PR numbers with titles as descriptions. +func CompletePRNumbers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, noFileComp + } + + ws, slug := completionRepo(cmd) + if ws == "" || slug == "" { + return nil, noFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListPullRequests(ctx, ws, slug, &api.PRListOptions{ + State: api.PRStateOpen, + Limit: 30, + }) + if err != nil { + return nil, noFileComp + } + + var completions []string + for _, pr := range result.Values { + num := fmt.Sprintf("%d", pr.ID) + if strings.HasPrefix(num, toComplete) { + // Format: "123\tTitle of the PR" -- cobra uses \t to separate value from description + completions = append(completions, fmt.Sprintf("%d\t%s", pr.ID, TruncateString(pr.Title, 50))) + } + } + return completions, noFileComp +} + +// CompleteIssueIDs returns issue IDs with titles as descriptions. +func CompleteIssueIDs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, noFileComp + } + + ws, slug := completionRepo(cmd) + if ws == "" || slug == "" { + return nil, noFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListIssues(ctx, ws, slug, &api.IssueListOptions{Limit: 30}) + if err != nil { + return nil, noFileComp + } + + var completions []string + for _, issue := range result.Values { + num := fmt.Sprintf("%d", issue.ID) + if strings.HasPrefix(num, toComplete) { + completions = append(completions, fmt.Sprintf("%d\t%s", issue.ID, TruncateString(issue.Title, 50))) + } + } + return completions, noFileComp +} + +// CompleteWorkspaceMembers returns usernames of workspace members. +func CompleteWorkspaceMembers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, noFileComp + } + + ws, _ := completionRepo(cmd) + if ws == "" { + ws = completionWorkspace(cmd) + } + if ws == "" { + return nil, noFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListWorkspaceMembers(ctx, ws, &api.WorkspaceMemberListOptions{Limit: 50}) + if err != nil { + return nil, noFileComp + } + + var names []string + for _, member := range result.Values { + name := member.User.Nickname + if name == "" { + name = member.User.DisplayName + } + if strings.HasPrefix(name, toComplete) { + names = append(names, name) + } + } + return names, noFileComp +} +``` + +**Step 4: Run tests to verify they pass** + +```bash +go test ./internal/cmdutil/ -run TestComplete -v +``` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add internal/cmdutil/completion.go internal/cmdutil/completion_test.go +git commit -m "feat: add shared completion helper module for dynamic shell completions" +``` + +--- + +## Task 2: Register Static Enum Completions for PR Commands + +**Files:** +- Modify: `internal/cmd/pr/list.go:62` (add `--state` completion) +- Modify: `internal/cmd/pr/view.go:83-84` (add positional arg completion) +- Modify: `internal/cmd/pr/merge.go:98` (add positional arg completion) +- Modify: `internal/cmd/pr/close.go` (add positional arg completion) +- Modify: `internal/cmd/pr/edit.go` (add positional arg completion) +- Modify: `internal/cmd/pr/diff.go` (add positional arg completion) +- Modify: `internal/cmd/pr/comment.go` (add positional arg completion) +- Modify: `internal/cmd/pr/checks.go` (add positional arg completion) +- Modify: `internal/cmd/pr/review.go` (add positional arg completion) +- Modify: `internal/cmd/pr/reopen.go` (add positional arg completion) +- Modify: `internal/cmd/pr/checkout.go` (add positional arg completion) + +**Step 1: Add `--state` completion to `pr list`** + +In `internal/cmd/pr/list.go`, after line 66 (the `--repo` flag), add: + +```go + _ = cmd.RegisterFlagCompletionFunc("state", cmdutil.StaticFlagCompletion([]string{"OPEN", "MERGED", "DECLINED"})) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +**Step 2: Add `ValidArgsFunction` for PR numbers to all PR commands that take `` positional arg** + +For each of these commands: `view`, `merge`, `close`, `edit`, `diff`, `comment`, `checks`, `review`, `reopen`, `checkout` -- set `ValidArgsFunction` on the cobra.Command. For example, in `internal/cmd/pr/merge.go` after the cmd is created (around line 92): + +```go + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers +``` + +And in `internal/cmd/pr/view.go`, `view` accepts a PR number, URL, or branch, so: + +```go + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers +``` + +**Step 3: Add `--base` and `--head` branch completion to `pr create`** + +In `internal/cmd/pr/create.go`, after the flag definitions (after line 84): + +```go + _ = cmd.RegisterFlagCompletionFunc("base", cmdutil.CompleteBranchNames) + _ = cmd.RegisterFlagCompletionFunc("head", cmdutil.CompleteBranchNames) + _ = cmd.RegisterFlagCompletionFunc("reviewer", cmdutil.CompleteWorkspaceMembers) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +**Step 4: Register `--repo` completion on all PR commands that have it** + +For every PR subcommand file that defines `--repo`, add after the flag definition: + +```go + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +**Step 5: Build and verify compilation** + +```bash +go build ./... +``` + +Expected: Successful compilation. + +**Step 6: Commit** + +```bash +git add internal/cmd/pr/ +git commit -m "feat: add dynamic completions for PR commands (state, numbers, branches, reviewers)" +``` + +--- + +## Task 3: Register Static Enum Completions for Issue Commands + +**Files:** +- Modify: `internal/cmd/issue/list.go:69-71` (add `--state`, `--kind`, `--priority` completions) +- Modify: `internal/cmd/issue/create.go:61-62` (add `--kind`, `--priority` completions) +- Modify: `internal/cmd/issue/edit.go:85-86` (add `--kind`, `--priority` completions) +- Modify: `internal/cmd/issue/view.go` (add positional arg completion) +- Modify: `internal/cmd/issue/close.go` (add positional arg completion) +- Modify: `internal/cmd/issue/reopen.go` (add positional arg completion) +- Modify: `internal/cmd/issue/delete.go` (add positional arg completion) +- Modify: `internal/cmd/issue/comment.go` (add positional arg completion) + +**Step 1: Add enum completions to `issue list`** + +In `internal/cmd/issue/list.go`, after the flag definitions (after line 75): + +```go + _ = cmd.RegisterFlagCompletionFunc("state", cmdutil.StaticFlagCompletion([]string{ + "new", "open", "resolved", "on hold", "invalid", "duplicate", "wontfix", "closed", + })) + _ = cmd.RegisterFlagCompletionFunc("kind", cmdutil.StaticFlagCompletion([]string{ + "bug", "enhancement", "proposal", "task", + })) + _ = cmd.RegisterFlagCompletionFunc("priority", cmdutil.StaticFlagCompletion([]string{ + "trivial", "minor", "major", "critical", "blocker", + })) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +**Step 2: Add enum completions to `issue create`** + +In `internal/cmd/issue/create.go`, after line 64: + +```go + _ = cmd.RegisterFlagCompletionFunc("kind", cmdutil.StaticFlagCompletion([]string{ + "bug", "enhancement", "proposal", "task", + })) + _ = cmd.RegisterFlagCompletionFunc("priority", cmdutil.StaticFlagCompletion([]string{ + "trivial", "minor", "major", "critical", "blocker", + })) + _ = cmd.RegisterFlagCompletionFunc("assignee", cmdutil.CompleteWorkspaceMembers) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +**Step 3: Add enum completions to `issue edit`** + +In `internal/cmd/issue/edit.go`, after line 88: + +```go + _ = cmd.RegisterFlagCompletionFunc("kind", cmdutil.StaticFlagCompletion([]string{ + "bug", "enhancement", "proposal", "task", + })) + _ = cmd.RegisterFlagCompletionFunc("priority", cmdutil.StaticFlagCompletion([]string{ + "trivial", "minor", "major", "critical", "blocker", + })) + _ = cmd.RegisterFlagCompletionFunc("assignee", cmdutil.CompleteWorkspaceMembers) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +**Step 4: Add `ValidArgsFunction` to issue commands that take `` positional arg** + +For `view`, `close`, `reopen`, `delete`, `comment`, `edit`: + +```go + cmd.ValidArgsFunction = cmdutil.CompleteIssueIDs +``` + +**Step 5: Build and verify** + +```bash +go build ./... +``` + +**Step 6: Commit** + +```bash +git add internal/cmd/issue/ +git commit -m "feat: add dynamic completions for issue commands (state, kind, priority, IDs, assignees)" +``` + +--- + +## Task 4: Register Completions for Workspace-scoped Commands + +**Files:** +- Modify: `internal/cmd/repo/list.go` (add `--workspace` completion) +- Modify: `internal/cmd/repo/create.go` (add `--workspace` completion) +- Modify: `internal/cmd/repo/fork.go` (add `--workspace` completion) +- Modify: `internal/cmd/project/list.go` (add `--workspace` completion) +- Modify: `internal/cmd/project/create.go` (add `--workspace` completion) +- Modify: `internal/cmd/project/view.go` (add `--workspace` completion) +- Modify: `internal/cmd/snippet/list.go` (add `--workspace` completion) +- Modify: `internal/cmd/snippet/create.go` (add `--workspace` completion) +- Modify: `internal/cmd/snippet/view.go` (add `--workspace` completion) +- Modify: `internal/cmd/snippet/edit.go` (add `--workspace` completion) +- Modify: `internal/cmd/snippet/delete.go` (add `--workspace` completion) + +**Step 1: Add `--workspace` completion to all workspace-scoped commands** + +In each file listed above, add after the `--workspace` flag definition: + +```go + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) +``` + +**Step 2: Build and verify** + +```bash +go build ./... +``` + +**Step 3: Commit** + +```bash +git add internal/cmd/repo/ internal/cmd/project/ internal/cmd/snippet/ +git commit -m "feat: add workspace name completion for repo, project, and snippet commands" +``` + +--- + +## Task 5: Register Completions for Branch and Pipeline Commands + +**Files:** +- Modify: `internal/cmd/branch/list.go` (add `--repo` completion) +- Modify: `internal/cmd/branch/create.go` (add `--target` branch completion, `--repo` completion) +- Modify: `internal/cmd/branch/delete.go` (add positional arg branch completion, `--repo` completion) +- Modify: `internal/cmd/pipeline/list.go` (add `--repo` completion) +- Modify: `internal/cmd/pipeline/view.go` (add `--repo` completion) +- Modify: `internal/cmd/pipeline/run.go` (add `--branch` completion, `--repo` completion) +- Modify: `internal/cmd/pipeline/stop.go` (add `--repo` completion) +- Modify: `internal/cmd/pipeline/steps.go` (add `--repo` completion) +- Modify: `internal/cmd/pipeline/logs.go` (add `--repo` completion) + +**Step 1: Add completions to branch commands** + +In `branch/delete.go`: +```go + cmd.ValidArgsFunction = cmdutil.CompleteBranchNames + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +In `branch/create.go` (for `--target` flag that specifies the base branch): +```go + _ = cmd.RegisterFlagCompletionFunc("target", cmdutil.CompleteBranchNames) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +In `branch/list.go`: +```go + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +**Step 2: Add `--repo` completion to all pipeline commands, and `--branch` completion to `pipeline run`** + +In each pipeline command file, add: +```go + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +In `pipeline/run.go`, also add: +```go + _ = cmd.RegisterFlagCompletionFunc("branch", cmdutil.CompleteBranchNames) +``` + +**Step 3: Build and verify** + +```bash +go build ./... +``` + +**Step 4: Commit** + +```bash +git add internal/cmd/branch/ internal/cmd/pipeline/ +git commit -m "feat: add dynamic completions for branch and pipeline commands" +``` + +--- + +## Task 6: Add `--repo` Completion to Browse Command + +**Files:** +- Modify: `internal/cmd/browse/browse.go` + +**Step 1: Add `--repo` completion** + +After the `--repo` flag definition: +```go + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) +``` + +**Step 2: Build and verify** + +```bash +go build ./... +``` + +**Step 3: Commit** + +```bash +git add internal/cmd/browse/ +git commit -m "feat: add repo completion to browse command" +``` + +--- + +## Task 7: Full Build + Test + Manual Verification + +**Step 1: Run all tests** + +```bash +go test ./... -v +``` + +Expected: All tests PASS. + +**Step 2: Build the binary** + +```bash +go build -o bb ./cmd/bb/ +``` + +**Step 3: Verify static completions work** + +Test that enum completions are registered correctly by using Cobra's built-in `__complete` hidden command: + +```bash +# Test PR state completion +./bb __complete pr list --state "" +# Expected output includes: OPEN, MERGED, DECLINED + +# Test issue kind completion +./bb __complete issue create --kind "" +# Expected output includes: bug, enhancement, proposal, task + +# Test issue priority completion +./bb __complete issue list --priority "" +# Expected output includes: trivial, minor, major, critical, blocker +``` + +**Step 4: Verify dynamic completions fail gracefully without auth** + +```bash +# If not logged in, these should return empty (no error, no crash) +./bb __complete repo list --workspace "" +# Expected: returns completion directives but no crash +``` + +**Step 5: Commit (if any test fixes were needed)** + +```bash +git add -A +git commit -m "fix: address any issues found during completion testing" +``` + +--- + +## Summary + +| Task | Scope | Type | Commands Affected | +|------|-------|------|-------------------| +| 1 | Completion helper module | New file | All (shared) | +| 2 | PR command completions | Modify | pr list/view/merge/close/edit/diff/comment/checks/review/reopen/checkout/create | +| 3 | Issue command completions | Modify | issue list/create/edit/view/close/reopen/delete/comment | +| 4 | Workspace completions | Modify | repo list/create/fork, project list/create/view, snippet list/create/view/edit/delete | +| 5 | Branch + Pipeline completions | Modify | branch list/create/delete, pipeline list/view/run/stop/steps/logs | +| 6 | Browse completion | Modify | browse | +| 7 | Full verification | Testing | All | + +**Completion types implemented:** + +| Completion | Function | Source | +|------------|----------|--------| +| `--state` (PR) | `StaticFlagCompletion` | Hard-coded: OPEN, MERGED, DECLINED | +| `--state` (issue) | `StaticFlagCompletion` | Hard-coded: new, open, resolved, etc. | +| `--kind` | `StaticFlagCompletion` | Hard-coded: bug, enhancement, proposal, task | +| `--priority` | `StaticFlagCompletion` | Hard-coded: trivial, minor, major, critical, blocker | +| `--workspace` | `CompleteWorkspaceNames` | API: ListWorkspaces | +| `--repo` | `CompleteRepoNames` | API: ListRepositories | +| `--base`/`--head`/`--target`/`--branch` | `CompleteBranchNames` | API: ListBranches | +| `--reviewer`/`--assignee` | `CompleteWorkspaceMembers` | API: ListWorkspaceMembers | +| `` positional | `CompletePRNumbers` | API: ListPullRequests | +| `` positional | `CompleteIssueIDs` | API: ListIssues | +| `` positional | `CompleteBranchNames` | API: ListBranches | diff --git a/internal/cmd/branch/create.go b/internal/cmd/branch/create.go index 4731380..95c0c43 100644 --- a/internal/cmd/branch/create.go +++ b/internal/cmd/branch/create.go @@ -59,6 +59,9 @@ By default, this command detects the repository from your git remote.`, cmd.MarkFlagRequired("target") + _ = cmd.RegisterFlagCompletionFunc("target", cmdutil.CompleteBranchNames) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/branch/delete.go b/internal/cmd/branch/delete.go index 3be2b1b..1f1eb68 100644 --- a/internal/cmd/branch/delete.go +++ b/internal/cmd/branch/delete.go @@ -54,6 +54,9 @@ By default, this command detects the repository from your git remote.`, cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository in WORKSPACE/REPO format (detects from git remote if not specified)") cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Skip confirmation prompt") + cmd.ValidArgsFunction = cmdutil.CompleteBranchNames + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/branch/list.go b/internal/cmd/branch/list.go index a7c04b1..dff96f5 100644 --- a/internal/cmd/branch/list.go +++ b/internal/cmd/branch/list.go @@ -55,6 +55,8 @@ Use the --repo flag to specify a different repository.`, cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 30, "Maximum number of branches to list") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/browse/browse.go b/internal/cmd/browse/browse.go index 2255589..3db7f03 100644 --- a/internal/cmd/browse/browse.go +++ b/internal/cmd/browse/browse.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" + "github.com/rbansal42/bitbucket-cli/internal/cmdutil" "github.com/rbansal42/bitbucket-cli/internal/config" "github.com/rbansal42/bitbucket-cli/internal/git" "github.com/rbansal42/bitbucket-cli/internal/iostreams" @@ -17,16 +18,16 @@ import ( // NewCmdBrowse creates the browse command func NewCmdBrowse(streams *iostreams.IOStreams) *cobra.Command { var ( - branch string - commit string - noBrowser bool - repo string - settings bool - wiki bool - issues bool - prs bool - pipelines bool - downloads bool + branch string + commit string + noBrowser bool + repo string + settings bool + wiki bool + issues bool + prs bool + pipelines bool + downloads bool ) cmd := &cobra.Command{ @@ -143,6 +144,8 @@ Use flags to open specific sections like issues, pull requests, or settings.`, cmd.Flags().BoolVar(&pipelines, "pipelines", false, "Open pipelines page") cmd.Flags().BoolVar(&downloads, "downloads", false, "Open downloads page") + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/issue/close.go b/internal/cmd/issue/close.go index 20f1fb9..4bb830e 100644 --- a/internal/cmd/issue/close.go +++ b/internal/cmd/issue/close.go @@ -47,6 +47,9 @@ Optionally, you can add a comment explaining why the issue is being closed.`, cmd.Flags().StringVarP(&opts.comment, "comment", "c", "", "Add a closing comment") cmd.Flags().StringVar(&opts.repo, "repo", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompleteIssueIDs + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/issue/comment.go b/internal/cmd/issue/comment.go index cad6e53..724e672 100644 --- a/internal/cmd/issue/comment.go +++ b/internal/cmd/issue/comment.go @@ -40,6 +40,9 @@ func NewCmdComment(streams *iostreams.IOStreams) *cobra.Command { cmd.Flags().StringVarP(&opts.body, "body", "b", "", "Comment body text") cmd.Flags().StringVar(&opts.repo, "repo", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompleteIssueIDs + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/issue/create.go b/internal/cmd/issue/create.go index b6794b2..631a902 100644 --- a/internal/cmd/issue/create.go +++ b/internal/cmd/issue/create.go @@ -63,6 +63,15 @@ to enter a title interactively.`, cmd.Flags().StringVarP(&opts.assignee, "assignee", "a", "", "Assignee username") cmd.Flags().StringVar(&opts.repo, "repo", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("kind", cmdutil.StaticFlagCompletion([]string{ + "bug", "enhancement", "proposal", "task", + })) + _ = cmd.RegisterFlagCompletionFunc("priority", cmdutil.StaticFlagCompletion([]string{ + "trivial", "minor", "major", "critical", "blocker", + })) + _ = cmd.RegisterFlagCompletionFunc("assignee", cmdutil.CompleteWorkspaceMembers) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } @@ -147,5 +156,3 @@ func runCreate(opts *createOptions) error { return nil } - - diff --git a/internal/cmd/issue/delete.go b/internal/cmd/issue/delete.go index c465a84..3a2264c 100644 --- a/internal/cmd/issue/delete.go +++ b/internal/cmd/issue/delete.go @@ -49,6 +49,9 @@ You will be prompted to confirm deletion unless the --yes flag is provided.`, cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, "Skip confirmation prompt") cmd.Flags().StringVar(&opts.repo, "repo", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompleteIssueIDs + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/issue/edit.go b/internal/cmd/issue/edit.go index 737648e..e4655b7 100644 --- a/internal/cmd/issue/edit.go +++ b/internal/cmd/issue/edit.go @@ -87,6 +87,16 @@ Use an empty string for --assignee to clear the assignee.`, cmd.Flags().StringVarP(&opts.assignee, "assignee", "a", "", "New assignee username (use \"\" to clear)") cmd.Flags().StringVar(&opts.repo, "repo", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompleteIssueIDs + _ = cmd.RegisterFlagCompletionFunc("kind", cmdutil.StaticFlagCompletion([]string{ + "bug", "enhancement", "proposal", "task", + })) + _ = cmd.RegisterFlagCompletionFunc("priority", cmdutil.StaticFlagCompletion([]string{ + "trivial", "minor", "major", "critical", "blocker", + })) + _ = cmd.RegisterFlagCompletionFunc("assignee", cmdutil.CompleteWorkspaceMembers) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/issue/list.go b/internal/cmd/issue/list.go index 1e80c86..f19c11d 100644 --- a/internal/cmd/issue/list.go +++ b/internal/cmd/issue/list.go @@ -74,6 +74,18 @@ priority, or assignee.`, cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") cmd.Flags().StringVar(&opts.Repo, "repo", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("state", cmdutil.StaticFlagCompletion([]string{ + "new", "open", "resolved", "on hold", "invalid", "duplicate", "wontfix", "closed", + })) + _ = cmd.RegisterFlagCompletionFunc("kind", cmdutil.StaticFlagCompletion([]string{ + "bug", "enhancement", "proposal", "task", + })) + _ = cmd.RegisterFlagCompletionFunc("priority", cmdutil.StaticFlagCompletion([]string{ + "trivial", "minor", "major", "critical", "blocker", + })) + _ = cmd.RegisterFlagCompletionFunc("assignee", cmdutil.CompleteWorkspaceMembers) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/issue/reopen.go b/internal/cmd/issue/reopen.go index 1f237b8..90e2bbf 100644 --- a/internal/cmd/issue/reopen.go +++ b/internal/cmd/issue/reopen.go @@ -43,6 +43,9 @@ additional work is needed.`, cmd.Flags().StringVar(&opts.repo, "repo", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompleteIssueIDs + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/issue/view.go b/internal/cmd/issue/view.go index bc83957..1528aa5 100644 --- a/internal/cmd/issue/view.go +++ b/internal/cmd/issue/view.go @@ -59,6 +59,9 @@ content, and other metadata. Use --comments to also show comments.`, cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Output in JSON format") cmd.Flags().StringVar(&opts.repo, "repo", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompleteIssueIDs + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pipeline/list.go b/internal/cmd/pipeline/list.go index 6552372..5d9c6b2 100644 --- a/internal/cmd/pipeline/list.go +++ b/internal/cmd/pipeline/list.go @@ -65,6 +65,8 @@ by pipeline status (PENDING, IN_PROGRESS, COMPLETED, FAILED, etc.).`, cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pipeline/logs.go b/internal/cmd/pipeline/logs.go index f1ef406..2f3d8b9 100644 --- a/internal/cmd/pipeline/logs.go +++ b/internal/cmd/pipeline/logs.go @@ -55,6 +55,8 @@ Step numbers can be obtained from 'bb pipeline steps'.`, cmd.Flags().StringVarP(&opts.Step, "step", "s", "", "Step UUID or step number (default: first failed step or last step)") cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pipeline/run.go b/internal/cmd/pipeline/run.go index e079206..03c9c70 100644 --- a/internal/cmd/pipeline/run.go +++ b/internal/cmd/pipeline/run.go @@ -59,6 +59,9 @@ pipeline defined in bitbucket-pipelines.yml with --custom.`, cmd.Flags().StringVar(&opts.custom, "custom", "", "Custom pipeline name (for custom pipelines in bitbucket-pipelines.yml)") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("branch", cmdutil.CompleteBranchNames) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pipeline/steps.go b/internal/cmd/pipeline/steps.go index 4b628fc..f0b9288 100644 --- a/internal/cmd/pipeline/steps.go +++ b/internal/cmd/pipeline/steps.go @@ -55,6 +55,8 @@ that step's logs.`, cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pipeline/stop.go b/internal/cmd/pipeline/stop.go index 2dcfb32..6333ad8 100644 --- a/internal/cmd/pipeline/stop.go +++ b/internal/cmd/pipeline/stop.go @@ -55,6 +55,8 @@ You will be prompted to confirm the stop action unless the --yes flag is provide cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, "Skip confirmation prompt") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pipeline/view.go b/internal/cmd/pipeline/view.go index a7da2dc..3bf4a99 100644 --- a/internal/cmd/pipeline/view.go +++ b/internal/cmd/pipeline/view.go @@ -59,6 +59,8 @@ You can specify a pipeline by its build number or UUID.`, cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/checkout.go b/internal/cmd/pr/checkout.go index 702338a..fffbd9e 100644 --- a/internal/cmd/pr/checkout.go +++ b/internal/cmd/pr/checkout.go @@ -64,6 +64,9 @@ use --force to overwrite it.`, cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Overwrite existing local branch") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/checks.go b/internal/cmd/pr/checks.go index 8dccc82..0535f34 100644 --- a/internal/cmd/pr/checks.go +++ b/internal/cmd/pr/checks.go @@ -59,6 +59,9 @@ associated with the pull request.`, cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/close.go b/internal/cmd/pr/close.go index 20f2c8a..795ad8a 100644 --- a/internal/cmd/pr/close.go +++ b/internal/cmd/pr/close.go @@ -46,6 +46,9 @@ Optionally, you can add a comment explaining why the PR is being closed.`, cmd.Flags().StringVarP(&opts.comment, "comment", "c", "", "Add a closing comment") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/comment.go b/internal/cmd/pr/comment.go index 497ab7f..8f6cf5e 100644 --- a/internal/cmd/pr/comment.go +++ b/internal/cmd/pr/comment.go @@ -47,6 +47,9 @@ for you to enter the comment text.`, cmd.Flags().StringVarP(&opts.body, "body", "b", "", "Comment body text") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/create.go b/internal/cmd/pr/create.go index 5344f0a..2905e3d 100644 --- a/internal/cmd/pr/create.go +++ b/internal/cmd/pr/create.go @@ -83,6 +83,11 @@ If --body is not provided, an editor will open for you to write the description. cmd.Flags().BoolVar(&opts.noMaintainerEdit, "no-maintainer-edit", false, "Disable maintainer edits (not supported by Bitbucket)") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("base", cmdutil.CompleteBranchNames) + _ = cmd.RegisterFlagCompletionFunc("head", cmdutil.CompleteBranchNames) + _ = cmd.RegisterFlagCompletionFunc("reviewer", cmdutil.CompleteWorkspaceMembers) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/diff.go b/internal/cmd/pr/diff.go index ab058f9..0410632 100644 --- a/internal/cmd/pr/diff.go +++ b/internal/cmd/pr/diff.go @@ -51,6 +51,9 @@ by default when stdout is a terminal, and disabled when piped.`, cmd.Flags().BoolVar(&opts.noColor, "no-color", false, "Disable color output") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/edit.go b/internal/cmd/pr/edit.go index f6af3c0..1090461 100644 --- a/internal/cmd/pr/edit.go +++ b/internal/cmd/pr/edit.go @@ -67,6 +67,10 @@ At least one of --title, --body, or --base must be specified.`, cmd.Flags().StringVar(&opts.base, "base", "", "New destination branch") cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Output in JSON format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("base", cmdutil.CompleteBranchNames) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/list.go b/internal/cmd/pr/list.go index 8cb5fb3..1048f39 100644 --- a/internal/cmd/pr/list.go +++ b/internal/cmd/pr/list.go @@ -65,6 +65,9 @@ by state (OPEN, MERGED, DECLINED).`, cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + _ = cmd.RegisterFlagCompletionFunc("state", cmdutil.StaticFlagCompletion([]string{"OPEN", "MERGED", "DECLINED"})) + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/merge.go b/internal/cmd/pr/merge.go index b8cd1db..08352b0 100644 --- a/internal/cmd/pr/merge.go +++ b/internal/cmd/pr/merge.go @@ -102,6 +102,9 @@ may not support rebase merge for all repositories).`, cmd.Flags().Bool("squash", false, "Use squash merge") cmd.Flags().Bool("rebase", false, "Use rebase merge (if supported)") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/reopen.go b/internal/cmd/pr/reopen.go index a31c334..7de46f8 100644 --- a/internal/cmd/pr/reopen.go +++ b/internal/cmd/pr/reopen.go @@ -41,6 +41,9 @@ Only declined pull requests can be reopened. Merged pull requests cannot be reop cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/review.go b/internal/cmd/pr/review.go index c916212..34f6d82 100644 --- a/internal/cmd/pr/review.go +++ b/internal/cmd/pr/review.go @@ -55,6 +55,9 @@ At least one action flag (--approve, --request-changes, or --comment) must be sp cmd.Flags().StringVarP(&opts.body, "body", "b", "", "Review comment body") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Repository in WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/pr/view.go b/internal/cmd/pr/view.go index 9958555..3a91d48 100644 --- a/internal/cmd/pr/view.go +++ b/internal/cmd/pr/view.go @@ -82,6 +82,9 @@ You can specify a pull request by number, URL, or branch name.`, cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Output in JSON format") cmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "Select a repository using the WORKSPACE/REPO format") + cmd.ValidArgsFunction = cmdutil.CompletePRNumbers + _ = cmd.RegisterFlagCompletionFunc("repo", cmdutil.CompleteRepoNames) + return cmd } diff --git a/internal/cmd/project/create.go b/internal/cmd/project/create.go index e1855a2..c518da5 100644 --- a/internal/cmd/project/create.go +++ b/internal/cmd/project/create.go @@ -75,6 +75,8 @@ identifier (e.g., "PROJ", "DEV", "CORE").`, cmd.Flags().BoolVarP(&opts.private, "private", "p", true, "Create a private project (default: true)") cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Output in JSON format") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/project/list.go b/internal/cmd/project/list.go index caf332a..2daf5e3 100644 --- a/internal/cmd/project/list.go +++ b/internal/cmd/project/list.go @@ -61,6 +61,8 @@ This command shows projects you have access to in the specified workspace.`, cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 30, "Maximum number of projects to list") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/project/view.go b/internal/cmd/project/view.go index 7f48d1e..0eb2903 100644 --- a/internal/cmd/project/view.go +++ b/internal/cmd/project/view.go @@ -66,6 +66,8 @@ short uppercase identifiers like "PROJ" or "DEV".`, cmd.Flags().BoolVar(&opts.web, "web", false, "Open the project in a web browser") cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Output in JSON format") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/repo/create.go b/internal/cmd/repo/create.go index ccd2971..b172831 100644 --- a/internal/cmd/repo/create.go +++ b/internal/cmd/repo/create.go @@ -98,6 +98,8 @@ a public repository instead.`, cmd.Flags().BoolVarP(&opts.clone, "clone", "c", false, "Clone the repository after creation") cmd.Flags().StringVar(&opts.gitignore, "gitignore", "", "Initialize with gitignore template") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/repo/fork.go b/internal/cmd/repo/fork.go index df3b69f..77e9fa9 100644 --- a/internal/cmd/repo/fork.go +++ b/internal/cmd/repo/fork.go @@ -75,6 +75,8 @@ as a new remote (default name: "fork").`, cmd.Flags().BoolVarP(&opts.clone, "clone", "c", false, "Clone the fork after creation") cmd.Flags().StringVar(&opts.remoteName, "remote-name", "fork", "Name for the new remote when in an existing clone") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/repo/list.go b/internal/cmd/repo/list.go index db937bc..c358667 100644 --- a/internal/cmd/repo/list.go +++ b/internal/cmd/repo/list.go @@ -66,6 +66,8 @@ By default, repositories are sorted by last updated time.`, cmd.Flags().StringVarP(&opts.Sort, "sort", "s", "-updated_on", "Sort field (name, -updated_on)") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/snippet/create.go b/internal/cmd/snippet/create.go index d11185a..1456c8f 100644 --- a/internal/cmd/snippet/create.go +++ b/internal/cmd/snippet/create.go @@ -59,6 +59,8 @@ If no files are specified, reads from stdin.`, cmd.MarkFlagRequired("title") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/snippet/delete.go b/internal/cmd/snippet/delete.go index 9d722d2..e302507 100644 --- a/internal/cmd/snippet/delete.go +++ b/internal/cmd/snippet/delete.go @@ -54,6 +54,8 @@ Use --force to skip the confirmation prompt.`, cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Skip confirmation prompt") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/snippet/edit.go b/internal/cmd/snippet/edit.go index 519bffb..2a4b3b7 100644 --- a/internal/cmd/snippet/edit.go +++ b/internal/cmd/snippet/edit.go @@ -58,6 +58,8 @@ You can update the title and/or add/update files.`, cmd.Flags().StringArrayVarP(&opts.Files, "file", "f", nil, "File to update (can be repeated)") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/snippet/list.go b/internal/cmd/snippet/list.go index 937f1e6..7ebe8ab 100644 --- a/internal/cmd/snippet/list.go +++ b/internal/cmd/snippet/list.go @@ -57,6 +57,8 @@ Snippets are workspace-scoped and can be filtered by your role.`, cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 30, "Maximum number of snippets to list") cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmd/snippet/view.go b/internal/cmd/snippet/view.go index e9afbd7..f510fe9 100644 --- a/internal/cmd/snippet/view.go +++ b/internal/cmd/snippet/view.go @@ -60,6 +60,8 @@ By default, shows snippet metadata. Use --raw to view file contents.`, cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output in JSON format") cmd.Flags().BoolVar(&opts.Raw, "raw", false, "Show raw file contents") + _ = cmd.RegisterFlagCompletionFunc("workspace", cmdutil.CompleteWorkspaceNames) + return cmd } diff --git a/internal/cmdutil/completion.go b/internal/cmdutil/completion.go new file mode 100644 index 0000000..cd4c470 --- /dev/null +++ b/internal/cmdutil/completion.go @@ -0,0 +1,277 @@ +package cmdutil + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/rbansal42/bitbucket-cli/internal/api" + "github.com/rbansal42/bitbucket-cli/internal/config" + "github.com/rbansal42/bitbucket-cli/internal/git" + "github.com/spf13/cobra" +) + +// completionTimeout is the maximum time allowed for completion API calls. +const completionTimeout = 5 * time.Second + +// StaticFlagCompletion returns a completion function compatible with +// cobra.RegisterFlagCompletionFunc. It filters values by the toComplete +// prefix (case-insensitive) and always returns ShellCompDirectiveNoFileComp. +func StaticFlagCompletion(values []string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return filterPrefix(values, toComplete), cobra.ShellCompDirectiveNoFileComp + } +} + +// completionCtx returns a context with the completion timeout. +func completionCtx() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), completionTimeout) +} + +// completionClient returns an authenticated API client for completions. +// Returns nil on any error (completions must never crash). +func completionClient() *api.Client { + client, err := GetAPIClient() + if err != nil { + return nil + } + return client +} + +// completionRepo resolves the workspace and repo slug from the --repo flag +// or the current git remote. Returns empty strings on failure. +func completionRepo(cmd *cobra.Command) (workspace, repoSlug string) { + repoFlag, _ := cmd.Flags().GetString("repo") + if repoFlag != "" { + ws, slug, err := ParseRepository(repoFlag) + if err != nil { + return "", "" + } + return ws, slug + } + + remote, err := git.GetDefaultRemote() + if err != nil { + return "", "" + } + return remote.Workspace, remote.RepoSlug +} + +// completionWorkspace resolves the workspace from the --workspace flag, +// then default config, then git remote. Returns empty string on failure. +func completionWorkspace(cmd *cobra.Command) string { + // Try --workspace flag first + ws, _ := cmd.Flags().GetString("workspace") + if ws != "" { + return ws + } + + // Try default workspace from config + ws, err := config.GetDefaultWorkspace() + if err == nil && ws != "" { + return ws + } + + // Try git remote + remote, err := git.GetDefaultRemote() + if err != nil { + return "" + } + return remote.Workspace +} + +// CompleteWorkspaceNames provides completion for workspace names. +func CompleteWorkspaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListWorkspaces(ctx, &api.WorkspaceListOptions{Limit: 50}) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var names []string + for _, m := range result.Values { + if m.Workspace != nil { + names = append(names, m.Workspace.Slug) + } + } + + return filterPrefix(names, toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// CompleteRepoNames provides completion for repository names in workspace/repo format. +func CompleteRepoNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ws := completionWorkspace(cmd) + if ws == "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListRepositories(ctx, ws, &api.RepositoryListOptions{Limit: 50}) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var names []string + for _, repo := range result.Values { + names = append(names, fmt.Sprintf("%s/%s", ws, repo.Slug)) + } + + return filterPrefix(names, toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// CompleteBranchNames provides completion for branch names. +func CompleteBranchNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ws, slug := completionRepo(cmd) + if ws == "" || slug == "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListBranches(ctx, ws, slug, &api.BranchListOptions{Limit: 50}) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var names []string + for _, b := range result.Values { + names = append(names, b.Name) + } + + return filterPrefix(names, toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// CompletePRNumbers provides completion for pull request numbers with title descriptions. +func CompletePRNumbers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ws, slug := completionRepo(cmd) + if ws == "" || slug == "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListPullRequests(ctx, ws, slug, &api.PRListOptions{State: api.PRStateOpen, Limit: 30}) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var completions []string + for _, pr := range result.Values { + completions = append(completions, fmt.Sprintf("%d\t%s", pr.ID, pr.Title)) + } + + return filterPrefix(completions, toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// CompleteIssueIDs provides completion for issue IDs with title descriptions. +func CompleteIssueIDs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ws, slug := completionRepo(cmd) + if ws == "" || slug == "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListIssues(ctx, ws, slug, &api.IssueListOptions{Limit: 30}) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var completions []string + for _, issue := range result.Values { + completions = append(completions, fmt.Sprintf("%d\t%s", issue.ID, issue.Title)) + } + + return filterPrefix(completions, toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// CompleteWorkspaceMembers provides completion for workspace member nicknames. +func CompleteWorkspaceMembers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client := completionClient() + if client == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ws := completionWorkspace(cmd) + if ws == "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + ctx, cancel := completionCtx() + defer cancel() + + result, err := client.ListWorkspaceMembers(ctx, ws, &api.WorkspaceMemberListOptions{Limit: 50}) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var names []string + for _, m := range result.Values { + if m.User != nil { + name := m.User.Nickname + if name == "" { + name = m.User.DisplayName + } + if name != "" { + names = append(names, name) + } + } + } + + return filterPrefix(names, toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// filterPrefix filters values by the toComplete prefix (case-insensitive). +// For tab-separated values ("id\tdescription"), only the part before the tab is matched. +func filterPrefix(values []string, toComplete string) []string { + if toComplete == "" { + return values + } + + prefix := strings.ToLower(toComplete) + var filtered []string + for _, v := range values { + // For tab-separated values, match only the key part + matchPart := v + if idx := strings.Index(v, "\t"); idx >= 0 { + matchPart = v[:idx] + } + if strings.HasPrefix(strings.ToLower(matchPart), prefix) { + filtered = append(filtered, v) + } + } + return filtered +} diff --git a/internal/cmdutil/completion_test.go b/internal/cmdutil/completion_test.go new file mode 100644 index 0000000..2e82350 --- /dev/null +++ b/internal/cmdutil/completion_test.go @@ -0,0 +1,196 @@ +package cmdutil + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestStaticFlagCompletion(t *testing.T) { + values := []string{"open", "merged", "declined"} + fn := StaticFlagCompletion(values) + + cmd := &cobra.Command{} + result, directive := fn(cmd, nil, "") + + if directive != cobra.ShellCompDirectiveNoFileComp { + t.Errorf("expected ShellCompDirectiveNoFileComp, got %v", directive) + } + + if len(result) != len(values) { + t.Errorf("expected %d values, got %d", len(values), len(result)) + } + + for i, v := range values { + if result[i] != v { + t.Errorf("expected result[%d] = %q, got %q", i, v, result[i]) + } + } +} + +func TestStaticFlagCompletionFiltering(t *testing.T) { + values := []string{"open", "merged", "declined"} + fn := StaticFlagCompletion(values) + + cmd := &cobra.Command{} + + tests := []struct { + name string + toComplete string + expected []string + }{ + { + name: "prefix match lowercase", + toComplete: "o", + expected: []string{"open"}, + }, + { + name: "prefix match uppercase (case-insensitive)", + toComplete: "O", + expected: []string{"open"}, + }, + { + name: "prefix match multiple", + toComplete: "m", + expected: []string{"merged"}, + }, + { + name: "prefix match d", + toComplete: "d", + expected: []string{"declined"}, + }, + { + name: "no match", + toComplete: "x", + expected: nil, + }, + { + name: "exact match", + toComplete: "open", + expected: []string{"open"}, + }, + { + name: "case-insensitive full match", + toComplete: "OPEN", + expected: []string{"open"}, + }, + { + name: "partial match me", + toComplete: "me", + expected: []string{"merged"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, directive := fn(cmd, nil, tt.toComplete) + + if directive != cobra.ShellCompDirectiveNoFileComp { + t.Errorf("expected ShellCompDirectiveNoFileComp, got %v", directive) + } + + if len(result) != len(tt.expected) { + t.Errorf("expected %d results, got %d: %v", len(tt.expected), len(result), result) + return + } + + for i, v := range tt.expected { + if result[i] != v { + t.Errorf("expected result[%d] = %q, got %q", i, v, result[i]) + } + } + }) + } +} + +func TestStaticFlagCompletionEmpty(t *testing.T) { + values := []string{"alpha", "beta", "gamma"} + fn := StaticFlagCompletion(values) + + cmd := &cobra.Command{} + result, directive := fn(cmd, nil, "") + + if directive != cobra.ShellCompDirectiveNoFileComp { + t.Errorf("expected ShellCompDirectiveNoFileComp, got %v", directive) + } + + if len(result) != len(values) { + t.Errorf("expected %d values when toComplete is empty, got %d", len(values), len(result)) + } + + for i, v := range values { + if result[i] != v { + t.Errorf("expected result[%d] = %q, got %q", i, v, result[i]) + } + } +} + +func TestFilterPrefixWithTabSeparatedValues(t *testing.T) { + values := []string{"123\tFix login bug", "456\tAdd feature", "12\tUpdate docs"} + + tests := []struct { + name string + toComplete string + expected []string + }{ + { + name: "match by numeric prefix", + toComplete: "1", + expected: []string{"123\tFix login bug", "12\tUpdate docs"}, + }, + { + name: "exact number match", + toComplete: "123", + expected: []string{"123\tFix login bug"}, + }, + { + name: "no match on description text", + toComplete: "Fix", + expected: nil, + }, + { + name: "match all with empty prefix", + toComplete: "", + expected: values, + }, + { + name: "match single digit 4", + toComplete: "4", + expected: []string{"456\tAdd feature"}, + }, + { + name: "no match", + toComplete: "9", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := filterPrefix(values, tt.toComplete) + if len(result) != len(tt.expected) { + t.Errorf("expected %d results, got %d: %v", len(tt.expected), len(result), result) + return + } + for i, v := range tt.expected { + if result[i] != v { + t.Errorf("expected result[%d] = %q, got %q", i, v, result[i]) + } + } + }) + } +} + +func TestCompletionCtx(t *testing.T) { + ctx, cancel := completionCtx() + defer cancel() + + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("expected context to have a deadline") + } + + if deadline.IsZero() { + t.Error("expected non-zero deadline") + } +}