-
Notifications
You must be signed in to change notification settings - Fork 0
feat(repo): #54 enrich repo list and add move command #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |
| "context" | ||
| "fmt" | ||
| "net/url" | ||
| "sync" | ||
|
|
||
| "github.com/tyrantkhan/bb/internal/api" | ||
| "github.com/tyrantkhan/bb/internal/cmdutil" | ||
|
|
@@ -12,6 +13,29 @@ import ( | |
| "github.com/urfave/cli/v3" | ||
| ) | ||
|
|
||
| type repoDetails struct { | ||
| LastCommit string | ||
| OpenPRs int | ||
| } | ||
|
|
||
| func fetchRepoDetails(client *api.Client, workspace, slug string) repoDetails { | ||
| var d repoDetails | ||
|
|
||
| // Last commit on default branch. | ||
| commits, err := api.Paginate[models.Commit](client, fmt.Sprintf("/2.0/repositories/%s/%s/commits", workspace, slug), 1) | ||
| if err == nil && len(commits) > 0 { | ||
| d.LastCommit = commits[0].Date | ||
| } | ||
|
|
||
| // Open PR count. | ||
| count, err := api.Count(client, fmt.Sprintf("/2.0/repositories/%s/%s/pullrequests?state=OPEN", workspace, slug)) | ||
| if err == nil { | ||
| d.OpenPRs = count | ||
| } | ||
|
|
||
| return d | ||
| } | ||
|
|
||
| func newCmdList() *cli.Command { | ||
| return &cli.Command{ | ||
| Name: "list", | ||
|
|
@@ -29,6 +53,10 @@ func newCmdList() *cli.Command { | |
| Name: "exclude-project", | ||
| Usage: "Exclude repos in this project", | ||
| }, | ||
| &cli.BoolFlag{ | ||
| Name: "details", | ||
| Usage: "Include last commit date and open PR count (slower)", | ||
| }, | ||
| }, | ||
| Action: cmdutil.NoArgs(func(ctx context.Context, cmd *cli.Command) error { | ||
| f := cmdutil.GetFactory(ctx) | ||
|
|
@@ -67,6 +95,48 @@ func newCmdList() *cli.Command { | |
| } | ||
|
|
||
| format := cmdutil.GetFormat(ctx, cmd) | ||
| details := cmd.Bool("details") | ||
|
|
||
| if details { | ||
| // Fetch details concurrently with bounded parallelism. | ||
| detailsMap := make([]repoDetails, len(repos)) | ||
| var wg sync.WaitGroup | ||
| sem := make(chan struct{}, 10) | ||
|
|
||
| for i, r := range repos { | ||
| wg.Add(1) | ||
| go func(idx int, slug string) { | ||
| defer wg.Done() | ||
| sem <- struct{}{} | ||
| detailsMap[idx] = fetchRepoDetails(client, workspace, slug) | ||
| <-sem | ||
| }(i, r.Slug) | ||
| } | ||
| wg.Wait() | ||
|
|
||
| headers := []string{"Name", "Project", "Slug", "Language", "Last Commit", "Open PRs", "Updated"} | ||
| rows := make([][]string, len(repos)) | ||
| for i, r := range repos { | ||
| projectKey := "" | ||
| if r.Project != nil { | ||
| projectKey = r.Project.Key | ||
| } | ||
| lastCommit := "-" | ||
| if detailsMap[i].LastCommit != "" { | ||
| lastCommit = models.FormatTime(detailsMap[i].LastCommit) | ||
| } | ||
| rows[i] = []string{ | ||
| r.Name, | ||
| projectKey, | ||
| r.Slug, | ||
| r.Language, | ||
| lastCommit, | ||
| fmt.Sprintf("%d", detailsMap[i].OpenPRs), | ||
| models.FormatTime(r.UpdatedOn), | ||
| } | ||
| } | ||
| return output.Format(format, repos, headers, rows) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the |
||
| } | ||
|
|
||
| headers := []string{"Name", "Project", "Slug", "Visibility", "Language", "Updated"} | ||
| rows := make([][]string, len(repos)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package repo | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/tyrantkhan/bb/internal/api" | ||
| "github.com/tyrantkhan/bb/internal/cmdutil" | ||
| "github.com/tyrantkhan/bb/internal/models" | ||
| "github.com/tyrantkhan/bb/internal/output" | ||
| "github.com/urfave/cli/v3" | ||
| ) | ||
|
|
||
| func newCmdMove() *cli.Command { | ||
| return &cli.Command{ | ||
| Name: "move", | ||
| Usage: "Move a repository to a different project", | ||
| ArgsUsage: "<slug>", | ||
| Flags: []cli.Flag{ | ||
| cmdutil.WorkspaceFlag, | ||
| cmdutil.FormatFlag, | ||
| &cli.StringFlag{ | ||
| Name: "project", | ||
| Usage: "Destination project key", | ||
| }, | ||
| &cli.StringFlag{ | ||
| Name: "prefix", | ||
| Usage: "Prefix to add to the repo name (e.g. \"Archived-\")", | ||
| }, | ||
| }, | ||
| Action: func(ctx context.Context, cmd *cli.Command) error { | ||
| f := cmdutil.GetFactory(ctx) | ||
| client, err := f.APIClient() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| workspace, err := cmdutil.ResolveWorkspace(ctx, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| slug := cmd.Args().First() | ||
| if slug == "" { | ||
| return fmt.Errorf("repository slug is required") | ||
| } | ||
|
|
||
| if err := api.ValidateSlug("repo", slug); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| projectKey := cmd.String("project") | ||
| if projectKey == "" { | ||
| return fmt.Errorf("--project flag is required") | ||
| } | ||
|
|
||
| // Fetch current repo to get existing name. | ||
| path := fmt.Sprintf("/2.0/repositories/%s/%s", workspace, slug) | ||
| resp, err := client.Get(path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| var repo models.Repository | ||
| if err := api.DecodeJSON(resp, &repo); err != nil { | ||
| return fmt.Errorf("failed to decode repository: %w", err) | ||
| } | ||
|
|
||
| body := map[string]interface{}{ | ||
| "project": map[string]string{ | ||
| "key": projectKey, | ||
| }, | ||
| } | ||
|
|
||
| // Add prefix to name if specified and not already present. | ||
| prefix := cmd.String("prefix") | ||
| newName := repo.Name | ||
| if prefix != "" && !strings.HasPrefix(repo.Name, prefix) { | ||
| newName = prefix + repo.Name | ||
| body["name"] = newName | ||
| } | ||
|
|
||
| resp, err = client.Put(path, body) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| var updated models.Repository | ||
| if err := api.DecodeJSON(resp, &updated); err != nil { | ||
| return fmt.Errorf("failed to decode updated repository: %w", err) | ||
| } | ||
|
|
||
| format := cmdutil.GetFormat(ctx, cmd) | ||
| if format == "json" { | ||
| return output.RenderJSON(updated) | ||
| } | ||
|
|
||
| fmt.Fprintln(f.IOOut, output.Success.Render( | ||
| fmt.Sprintf("Repository moved: %s → project %s", updated.FullName, projectKey), | ||
| )) | ||
| if newName != repo.Name { | ||
| fmt.Fprintf(f.IOOut, "%s %s → %s\n", output.Bold.Render("Renamed:"), repo.Name, updated.Name) | ||
| } | ||
| fmt.Fprintf(f.IOOut, "%s %s\n", output.Bold.Render("Slug:"), updated.Slug) | ||
| if updated.Project != nil { | ||
| fmt.Fprintf(f.IOOut, "%s %s (%s)\n", output.Bold.Render("Project:"), updated.Project.Name, updated.Project.Key) | ||
| } | ||
|
|
||
| return nil | ||
| }, | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ func NewCmdRepo() *cli.Command { | |
| newCmdView(), | ||
| newCmdClone(), | ||
| newCmdCreate(), | ||
| newCmdMove(), | ||
| }, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| --- | ||
| name: bb | ||
| version: 1.1.0 | ||
| version: 1.2.0 | ||
| description: Use the bb CLI to interact with Bitbucket Cloud — manage PRs, repos, and pipelines. Use when the user asks about Bitbucket pull requests, repositories, pipelines, or wants to perform Bitbucket operations. | ||
| allowed-tools: Bash(bb *) | ||
| --- | ||
|
|
@@ -35,9 +35,12 @@ bb pr edit 42 --title "New title" # edit PR fields | |
| bb repo list # list repos in workspace | ||
| bb repo list --project PROJ # filter by project | ||
| bb repo list --exclude-project PROJ # exclude a project | ||
| bb repo list --details # include last commit date and open PR count | ||
| bb repo view myrepo # view details | ||
| bb repo create --name myrepo --private | ||
| bb repo clone myrepo --protocol ssh | ||
| bb repo move myrepo --project AR # move repo to a different project | ||
| bb repo move myrepo --project AR --prefix "Archived-" # move and add prefix | ||
| ``` | ||
|
|
||
| ### Search | ||
|
|
@@ -86,9 +89,11 @@ bb pipeline stop {uuid} # stop running pipeline | |
| | Command | Description | | ||
| |---|---| | ||
| | `bb repo list` | List repositories in a workspace | | ||
| | `bb repo list --details` | Include last commit date and open PR count (slower) | | ||
| | `bb repo view [slug]` | View repository details | | ||
| | `bb repo create` | Create a new repository | | ||
| | `bb repo clone <slug>` | Clone a repository | | ||
| | `bb repo move <slug> --project KEY` | Move a repo to a different project (`--prefix` to rename) | | ||
|
|
||
| ### Pull Requests | ||
|
|
||
|
|
@@ -114,6 +119,12 @@ bb pipeline stop {uuid} # stop running pipeline | |
| |---|---| | ||
| | `bb search code <query>` | Search for code across repos (`--repo`, `--extension`, `--language`, `--path`) | | ||
|
|
||
| ### Workspaces | ||
|
|
||
| | Command | Description | | ||
| |---|---| | ||
| | `bb workspace members` | List workspace members (`--workspace`, `--limit`, `--format`) | | ||
|
Comment on lines
+122
to
+126
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation for |
||
|
|
||
| ### Pipelines | ||
|
|
||
| | Command | Description | | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Visibilitycolumn is missing in the detailed view, which creates an inconsistency with the default list view. It's recommended to include it to maintain a consistent user experience across both views.