feat: v0.3.0d — add PR checks, iostreams, aliases, and broad enhancements

Add PR checks command, iostreams/text packages for colored table output,
top-level run/workflow aliases matching gh CLI structure. Enhance actions,
issues, PRs, releases, repos, labels, milestones, and wiki commands with
improved flags, JSON output, and error handling.
This commit is contained in:
sid 2026-03-23 11:42:44 -06:00
parent 7c0dcc8696
commit 113505de95
29 changed files with 3131 additions and 542 deletions

View file

@ -6,11 +6,11 @@ import (
"os/exec"
"path/filepath"
"strings"
"text/tabwriter"
"code.gitea.io/sdk/gitea"
"forgejo.zerova.net/sid/fgj-sid/internal/api"
"forgejo.zerova.net/sid/fgj-sid/internal/config"
"forgejo.zerova.net/sid/fgj-sid/internal/text"
"github.com/spf13/cobra"
)
@ -78,17 +78,34 @@ var repoEditCmd = &cobra.Command{
# Change default branch
fgj repo edit --default-branch develop
# Rename a repository
fgj repo edit owner/repo --name new-name
# Edit current repo (auto-detected from git context)
fgj repo edit --public`,
Args: cobra.MaximumNArgs(1),
RunE: runRepoEdit,
}
var repoRenameCmd = &cobra.Command{
Use: "rename <new-name>",
Short: "Rename a repository",
Long: "Rename an existing repository. This is a shorthand for `fgj repo edit --name <new-name>`.",
Example: ` # Rename current repo
fgj repo rename new-name
# Rename a specific repo
fgj repo rename new-name -R owner/old-name`,
Args: cobra.ExactArgs(1),
RunE: runRepoRename,
}
func init() {
rootCmd.AddCommand(repoCmd)
repoCmd.AddCommand(repoCloneCmd)
repoCmd.AddCommand(repoCreateCmd)
repoCmd.AddCommand(repoEditCmd)
repoCmd.AddCommand(repoRenameCmd)
repoCmd.AddCommand(repoForkCmd)
repoCmd.AddCommand(repoListCmd)
repoCmd.AddCommand(repoViewCmd)
@ -104,16 +121,25 @@ func init() {
repoCreateCmd.Flags().StringP("team", "t", "", "Team name to be granted access (org repos only)")
repoCreateCmd.MarkFlagsMutuallyExclusive("public", "private")
addJSONFlags(repoViewCmd, "Output repository as JSON")
repoViewCmd.Flags().BoolP("web", "w", false, "Open in web browser")
addJSONFlags(repoListCmd, "Output repositories as JSON")
repoCloneCmd.Flags().StringP("protocol", "p", "https", "Clone protocol: https or ssh")
repoEditCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
repoEditCmd.Flags().String("name", "", "Rename the repository")
repoEditCmd.Flags().StringP("description", "d", "", "Repository description")
repoEditCmd.Flags().String("homepage", "", "Repository home page URL")
repoEditCmd.Flags().String("default-branch", "", "Default branch name")
repoEditCmd.Flags().Bool("private", false, "Make the repository private")
repoEditCmd.Flags().Bool("public", false, "Make the repository public")
repoEditCmd.Flags().Bool("json", false, "Output updated repository as JSON")
addJSONFlags(repoEditCmd, "Output updated repository as JSON")
repoEditCmd.MarkFlagsMutuallyExclusive("public", "private")
repoRenameCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
addJSONFlags(repoRenameCmd, "Output updated repository as JSON")
}
func runRepoView(cmd *cobra.Command, args []string) error {
@ -137,23 +163,36 @@ func runRepoView(cmd *cobra.Command, args []string) error {
return err
}
ios.StartSpinner("Fetching repository...")
repository, _, err := client.GetRepo(owner, name)
ios.StopSpinner()
if err != nil {
return fmt.Errorf("failed to get repository: %w", err)
}
fmt.Printf("Repository: %s/%s\n", repository.Owner.UserName, repository.Name)
fmt.Printf("Description: %s\n", repository.Description)
fmt.Printf("URL: %s\n", repository.HTMLURL)
fmt.Printf("Clone URL (HTTPS): %s\n", repository.CloneURL)
fmt.Printf("Clone URL (SSH): %s\n", repository.SSHURL)
fmt.Printf("Default Branch: %s\n", repository.DefaultBranch)
fmt.Printf("Stars: %d\n", repository.Stars)
fmt.Printf("Forks: %d\n", repository.Forks)
fmt.Printf("Open Issues: %d\n", repository.OpenIssues)
fmt.Printf("Private: %v\n", repository.Private)
fmt.Printf("Created: %s\n", repository.Created.Format("2006-01-02 15:04:05"))
fmt.Printf("Updated: %s\n", repository.Updated.Format("2006-01-02 15:04:05"))
if web, _ := cmd.Flags().GetBool("web"); web {
return ios.OpenInBrowser(repository.HTMLURL)
}
if wantJSON(cmd) {
return outputJSON(cmd, repository)
}
cs := ios.ColorScheme()
isTTY := ios.IsStdoutTTY()
fmt.Fprintf(ios.Out, "Repository: %s\n", cs.Bold(fmt.Sprintf("%s/%s", repository.Owner.UserName, repository.Name)))
fmt.Fprintf(ios.Out, "Description: %s\n", repository.Description)
fmt.Fprintf(ios.Out, "URL: %s\n", repository.HTMLURL)
fmt.Fprintf(ios.Out, "Clone URL (HTTPS): %s\n", repository.CloneURL)
fmt.Fprintf(ios.Out, "Clone URL (SSH): %s\n", repository.SSHURL)
fmt.Fprintf(ios.Out, "Default Branch: %s\n", repository.DefaultBranch)
fmt.Fprintf(ios.Out, "Stars: %d\n", repository.Stars)
fmt.Fprintf(ios.Out, "Forks: %d\n", repository.Forks)
fmt.Fprintf(ios.Out, "Open Issues: %d\n", repository.OpenIssues)
fmt.Fprintf(ios.Out, "Private: %v\n", repository.Private)
fmt.Fprintf(ios.Out, "Created: %s\n", text.FormatDate(repository.Created, isTTY))
fmt.Fprintf(ios.Out, "Updated: %s\n", text.FormatDate(repository.Updated, isTTY))
return nil
}
@ -169,37 +208,39 @@ func runRepoList(cmd *cobra.Command, args []string) error {
return err
}
ios.StartSpinner("Fetching repositories...")
user, _, err := client.GetMyUserInfo()
if err != nil {
ios.StopSpinner()
return fmt.Errorf("failed to get user info: %w", err)
}
repos, _, err := client.ListUserRepos(user.UserName, gitea.ListReposOptions{})
ios.StopSpinner()
if err != nil {
return fmt.Errorf("failed to list repositories: %w", err)
}
if wantJSON(cmd) {
return outputJSON(cmd, repos)
}
if len(repos) == 0 {
fmt.Println("No repositories found")
fmt.Fprintln(ios.Out, "No repositories found")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "NAME\tVISIBILITY\tDESCRIPTION\n")
tp := ios.NewTablePrinter()
tp.AddHeader("NAME", "VISIBILITY", "DESCRIPTION")
for _, repo := range repos {
visibility := "public"
if repo.Private {
visibility = "private"
}
desc := repo.Description
if len(desc) > 50 {
desc = desc[:47] + "..."
}
_, _ = fmt.Fprintf(w, "%s/%s\t%s\t%s\n", repo.Owner.UserName, repo.Name, visibility, desc)
desc := text.Truncate(repo.Description, 50)
tp.AddRow(fmt.Sprintf("%s/%s", repo.Owner.UserName, repo.Name), visibility, desc)
}
_ = w.Flush()
return nil
return tp.Render()
}
func runRepoClone(cmd *cobra.Command, args []string) error {
@ -221,7 +262,9 @@ func runRepoClone(cmd *cobra.Command, args []string) error {
return err
}
ios.StartSpinner("Fetching repository info...")
repository, _, err := client.GetRepo(owner, name)
ios.StopSpinner()
if err != nil {
return fmt.Errorf("failed to get repository: %w", err)
}
@ -241,7 +284,7 @@ func runRepoClone(cmd *cobra.Command, args []string) error {
destination = name
}
fmt.Printf("Cloning %s/%s to %s...\n", owner, name, destination)
fmt.Fprintf(ios.Out, "Cloning %s/%s to %s...\n", owner, name, destination)
// Create parent directory if it doesn't exist
if dir := filepath.Dir(destination); dir != "." {
@ -250,17 +293,21 @@ func runRepoClone(cmd *cobra.Command, args []string) error {
}
}
ios.StartSpinner("Cloning repository...")
// Execute git clone
gitCmd := exec.Command("git", "clone", cloneURL, destination)
gitCmd.Stdout = os.Stdout
gitCmd.Stderr = os.Stderr
gitCmd.Stdin = os.Stdin
gitCmd.Stdout = ios.Out
gitCmd.Stderr = ios.ErrOut
gitCmd.Stdin = ios.In
if err := gitCmd.Run(); err != nil {
ios.StopSpinner()
return fmt.Errorf("failed to clone repository: %w", err)
}
ios.StopSpinner()
fmt.Printf("Repository cloned successfully to %s\n", destination)
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Repository cloned successfully to %s\n", cs.SuccessIcon(), destination)
return nil
}
@ -282,14 +329,17 @@ func runRepoFork(cmd *cobra.Command, args []string) error {
return err
}
ios.StartSpinner("Forking repository...")
fork, _, err := client.CreateFork(owner, name, gitea.CreateForkOption{})
ios.StopSpinner()
if err != nil {
return fmt.Errorf("failed to fork repository: %w", err)
}
fmt.Printf("Repository forked successfully\n")
fmt.Printf("View at: %s\n", fork.HTMLURL)
fmt.Printf("Clone URL: %s\n", fork.CloneURL)
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Repository forked successfully\n", cs.SuccessIcon())
fmt.Fprintf(ios.Out, "View at: %s\n", fork.HTMLURL)
fmt.Fprintf(ios.Out, "Clone URL: %s\n", fork.CloneURL)
return nil
}
@ -335,12 +385,14 @@ func runRepoCreate(cmd *cobra.Command, args []string) error {
License: license,
}
ios.StartSpinner("Creating repository...")
var repo *gitea.Repository
if isOrg {
repo, _, err = client.CreateOrgRepo(org, opt)
} else {
repo, _, err = client.CreateRepo(opt)
}
ios.StopSpinner()
if err != nil {
return fmt.Errorf("failed to create repository: %w", err)
}
@ -354,7 +406,7 @@ func runRepoCreate(cmd *cobra.Command, args []string) error {
} else {
user, _, userErr := client.GetMyUserInfo()
if userErr != nil {
fmt.Fprintf(os.Stderr, "warning: repository created but could not determine owner for homepage: %v\n", userErr)
fmt.Fprintf(ios.ErrOut, "warning: repository created but could not determine owner for homepage: %v\n", userErr)
homepage = "" // skip EditRepo
} else {
ownerName = user.UserName
@ -366,23 +418,24 @@ func runRepoCreate(cmd *cobra.Command, args []string) error {
Website: &homepage,
})
if err != nil {
fmt.Fprintf(os.Stderr, "warning: repository created but failed to set homepage: %v\n", err)
fmt.Fprintf(ios.ErrOut, "warning: repository created but failed to set homepage: %v\n", err)
}
}
}
if team != "" {
if !isOrg {
fmt.Fprintln(os.Stderr, "warning: --team is only meaningful for organization repositories")
fmt.Fprintln(ios.ErrOut, "warning: --team is only meaningful for organization repositories")
} else {
_, err = client.AddRepoTeam(org, repo.Name, team)
if err != nil {
fmt.Fprintf(os.Stderr, "warning: repository created but failed to add team %q: %v\n", team, err)
fmt.Fprintf(ios.ErrOut, "warning: repository created but failed to add team %q: %v\n", team, err)
}
}
}
fmt.Printf("Repository created: %s\n", repo.HTMLURL)
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Repository created: %s\n", cs.SuccessIcon(), repo.HTMLURL)
if doClone {
cloneURL := repo.CloneURL
@ -391,11 +444,11 @@ func runRepoCreate(cmd *cobra.Command, args []string) error {
cloneURL = repo.SSHURL
}
}
fmt.Printf("Cloning into %s...\n", repo.Name)
fmt.Fprintf(ios.Out, "Cloning into %s...\n", repo.Name)
gitCmd := exec.Command("git", "clone", cloneURL, repo.Name)
gitCmd.Stdout = os.Stdout
gitCmd.Stderr = os.Stderr
gitCmd.Stdin = os.Stdin
gitCmd.Stdout = ios.Out
gitCmd.Stderr = ios.ErrOut
gitCmd.Stdin = ios.In
if err := gitCmd.Run(); err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
@ -449,6 +502,11 @@ func runRepoEdit(cmd *cobra.Command, args []string) error {
opt := gitea.EditRepoOption{}
changed := false
if cmd.Flags().Changed("name") {
n, _ := cmd.Flags().GetString("name")
opt.Name = &n
changed = true
}
if cmd.Flags().Changed("description") {
d, _ := cmd.Flags().GetString("description")
opt.Description = &d
@ -476,36 +534,84 @@ func runRepoEdit(cmd *cobra.Command, args []string) error {
}
if !changed {
return fmt.Errorf("no changes specified; use flags like --public, --private, --description, --homepage, or --default-branch")
return fmt.Errorf("no changes specified; use flags like --name, --public, --private, --description, --homepage, or --default-branch")
}
ios.StartSpinner("Updating repository...")
repository, _, err := client.EditRepo(owner, name, opt)
ios.StopSpinner()
if err != nil {
return fmt.Errorf("failed to edit repository: %w", err)
}
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
return writeJSON(repository)
if wantJSON(cmd) {
return outputJSON(cmd, repository)
}
fmt.Printf("Repository updated: %s\n", repository.HTMLURL)
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Repository updated: %s\n", cs.SuccessIcon(), repository.HTMLURL)
if opt.Name != nil {
fmt.Fprintf(ios.Out, "Renamed to: %s\n", repository.FullName)
}
if opt.Private != nil {
if *opt.Private {
fmt.Println("Visibility: private")
fmt.Fprintln(ios.Out, "Visibility: private")
} else {
fmt.Println("Visibility: public")
fmt.Fprintln(ios.Out, "Visibility: public")
}
}
if opt.Description != nil {
fmt.Printf("Description: %s\n", *opt.Description)
fmt.Fprintf(ios.Out, "Description: %s\n", *opt.Description)
}
if opt.Website != nil {
fmt.Printf("Homepage: %s\n", *opt.Website)
fmt.Fprintf(ios.Out, "Homepage: %s\n", *opt.Website)
}
if opt.DefaultBranch != nil {
fmt.Printf("Default branch: %s\n", *opt.DefaultBranch)
fmt.Fprintf(ios.Out, "Default branch: %s\n", *opt.DefaultBranch)
}
return nil
}
func runRepoRename(cmd *cobra.Command, args []string) error {
var repo string
if r, _ := cmd.Flags().GetString("repo"); r != "" {
repo = r
}
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
newName := args[0]
cfg, err := config.Load()
if err != nil {
return err
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
if err != nil {
return err
}
opt := gitea.EditRepoOption{
Name: &newName,
}
ios.StartSpinner("Renaming repository...")
repository, _, err := client.EditRepo(owner, name, opt)
ios.StopSpinner()
if err != nil {
return fmt.Errorf("failed to rename repository: %w", err)
}
if wantJSON(cmd) {
return outputJSON(cmd, repository)
}
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Renamed %s/%s to %s\n", cs.SuccessIcon(), owner, name, repository.FullName)
return nil
}