feat: implement repo create command
This commit is contained in:
parent
73d54fde9c
commit
a43a79d78f
4 changed files with 248 additions and 2 deletions
150
cmd/repo.go
150
cmd/repo.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
|
|
@ -50,12 +51,35 @@ var repoForkCmd = &cobra.Command{
|
|||
RunE: runRepoFork,
|
||||
}
|
||||
|
||||
var repoCreateCmd = &cobra.Command{
|
||||
Use: "create <name>",
|
||||
Short: "Create a new repository",
|
||||
Long: `Create a new repository on the Forgejo instance.
|
||||
|
||||
Name can be "reponame" (creates under your account) or "org/reponame"
|
||||
(creates under an organization). Repositories are public by default; use --private to create a private one.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRepoCreate,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(repoCmd)
|
||||
repoCmd.AddCommand(repoViewCmd)
|
||||
repoCmd.AddCommand(repoListCmd)
|
||||
repoCmd.AddCommand(repoCloneCmd)
|
||||
repoCmd.AddCommand(repoCreateCmd)
|
||||
repoCmd.AddCommand(repoForkCmd)
|
||||
repoCmd.AddCommand(repoListCmd)
|
||||
repoCmd.AddCommand(repoViewCmd)
|
||||
|
||||
repoCreateCmd.Flags().StringP("description", "d", "", "Description of the repository")
|
||||
repoCreateCmd.Flags().Bool("private", false, "Make the new repository private")
|
||||
repoCreateCmd.Flags().Bool("public", false, "Make the new repository public")
|
||||
repoCreateCmd.Flags().Bool("add-readme", false, "Add a README file to the new repository")
|
||||
repoCreateCmd.Flags().StringP("gitignore", "g", "", "Gitignore template (e.g. Go, Python)")
|
||||
repoCreateCmd.Flags().StringP("license", "l", "", "License template (e.g. MIT, Apache-2.0)")
|
||||
repoCreateCmd.Flags().String("homepage", "", "Repository home page URL")
|
||||
repoCreateCmd.Flags().BoolP("clone", "c", false, "Clone the new repository to the current directory")
|
||||
repoCreateCmd.Flags().StringP("team", "t", "", "Team name to be granted access (org repos only)")
|
||||
repoCreateCmd.MarkFlagsMutuallyExclusive("public", "private")
|
||||
|
||||
repoCloneCmd.Flags().StringP("protocol", "p", "https", "Clone protocol: https or ssh")
|
||||
}
|
||||
|
|
@ -237,3 +261,125 @@ func runRepoFork(cmd *cobra.Command, args []string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRepoCreate(cmd *cobra.Command, args []string) error {
|
||||
inputName := args[0]
|
||||
org, repoName, isOrg, err := parseCreateName(inputName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
private, _ := cmd.Flags().GetBool("private")
|
||||
description, _ := cmd.Flags().GetString("description")
|
||||
addReadme, _ := cmd.Flags().GetBool("add-readme")
|
||||
gitignore, _ := cmd.Flags().GetString("gitignore")
|
||||
license, _ := cmd.Flags().GetString("license")
|
||||
homepage, _ := cmd.Flags().GetString("homepage")
|
||||
doClone, _ := cmd.Flags().GetBool("clone")
|
||||
team, _ := cmd.Flags().GetString("team")
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt := gitea.CreateRepoOption{
|
||||
Name: repoName,
|
||||
Description: description,
|
||||
Private: private,
|
||||
AutoInit: addReadme,
|
||||
Gitignores: gitignore,
|
||||
License: license,
|
||||
}
|
||||
|
||||
var repo *gitea.Repository
|
||||
if isOrg {
|
||||
repo, _, err = client.CreateOrgRepo(org, opt)
|
||||
} else {
|
||||
repo, _, err = client.CreateRepo(opt)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create repository: %w", err)
|
||||
}
|
||||
|
||||
if homepage != "" {
|
||||
ownerName := org
|
||||
if !isOrg {
|
||||
// For personal repos, get the owner from the created repo or fall back to API
|
||||
if repo.Owner != nil {
|
||||
ownerName = repo.Owner.UserName
|
||||
} 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)
|
||||
homepage = "" // skip EditRepo
|
||||
} else {
|
||||
ownerName = user.UserName
|
||||
}
|
||||
}
|
||||
}
|
||||
if homepage != "" {
|
||||
_, _, err = client.EditRepo(ownerName, repo.Name, gitea.EditRepoOption{
|
||||
Website: &homepage,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "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")
|
||||
} 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.Printf("Repository created: %s\n", repo.HTMLURL)
|
||||
|
||||
if doClone {
|
||||
cloneURL := repo.CloneURL
|
||||
if hostCfg, hostErr := cfg.GetHost("", getDetectedHost()); hostErr == nil {
|
||||
if hostCfg.GitProtocol == "ssh" {
|
||||
cloneURL = repo.SSHURL
|
||||
}
|
||||
}
|
||||
fmt.Printf("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
|
||||
if err := gitCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone repository: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCreateName parses the name argument for repo creation.
|
||||
// Returns org, repoName, and whether it targets an org.
|
||||
// Input: "reponame" → ("", "reponame", false)
|
||||
// Input: "org/reponame" → ("org", "reponame", true)
|
||||
func parseCreateName(name string) (org, repoName string, isOrg bool, err error) {
|
||||
parts := strings.SplitN(name, "/", 2)
|
||||
if len(parts) == 2 {
|
||||
if parts[0] == "" || parts[1] == "" {
|
||||
return "", "", false, fmt.Errorf("invalid repository name %q: org and repo name must not be empty", name)
|
||||
}
|
||||
return parts[0], parts[1], true, nil
|
||||
}
|
||||
if name == "" {
|
||||
return "", "", false, fmt.Errorf("repository name must not be empty")
|
||||
}
|
||||
return "", name, false, nil
|
||||
}
|
||||
|
|
|
|||
53
cmd/repo_create_test.go
Normal file
53
cmd/repo_create_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package cmd
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseCreateName(t *testing.T) {
|
||||
t.Run("simple repo name", func(t *testing.T) {
|
||||
org, repo, isOrg, err := parseCreateName("myrepo")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if org != "" || repo != "myrepo" || isOrg {
|
||||
t.Errorf("got (%q, %q, %v), want (%q, %q, %v)", org, repo, isOrg, "", "myrepo", false)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("org/repo name", func(t *testing.T) {
|
||||
org, repo, isOrg, err := parseCreateName("myorg/myrepo")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if org != "myorg" || repo != "myrepo" || !isOrg {
|
||||
t.Errorf("got (%q, %q, %v), want (%q, %q, %v)", org, repo, isOrg, "myorg", "myrepo", true)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
_, _, _, err := parseCreateName("")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty string, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("slash only", func(t *testing.T) {
|
||||
_, _, _, err := parseCreateName("/")
|
||||
if err == nil {
|
||||
t.Error("expected error for '/', got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty repo after slash", func(t *testing.T) {
|
||||
_, _, _, err := parseCreateName("org/")
|
||||
if err == nil {
|
||||
t.Error("expected error for 'org/', got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty org before slash", func(t *testing.T) {
|
||||
_, _, _, err := parseCreateName("/repo")
|
||||
if err == nil {
|
||||
t.Error("expected error for '/repo', got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -219,6 +219,14 @@ func (env *TestEnv) GetLabelIDs(labelNames []string) []int64 {
|
|||
return ids
|
||||
}
|
||||
|
||||
// CleanupRepo deletes a repository created during testing.
|
||||
func (env *TestEnv) CleanupRepo(owner, repoName string) {
|
||||
_, err := env.Client.DeleteRepo(owner, repoName)
|
||||
if err != nil {
|
||||
env.T.Logf("warning: failed to delete repo %s/%s: %v", owner, repoName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetBinaryPath returns the path to the built fgj binary
|
||||
func (env *TestEnv) GetBinaryPath() string {
|
||||
binaryPath := os.Getenv("FGJ_BINARY_PATH")
|
||||
|
|
|
|||
|
|
@ -940,3 +940,42 @@ func TestCLIReleaseList(t *testing.T) {
|
|||
|
||||
t.Logf("Successfully listed releases via CLI")
|
||||
}
|
||||
|
||||
// TestCLIRepoCreate verifies `fgj repo create` creates a repository
|
||||
func TestCLIRepoCreate(t *testing.T) {
|
||||
env := NewTestEnv(t)
|
||||
|
||||
repoName := fmt.Sprintf("fgj-test-create-%d", time.Now().UnixNano())
|
||||
defer env.CleanupRepo(env.Owner, repoName)
|
||||
|
||||
result := env.RunCLI(
|
||||
"--hostname", env.Hostname,
|
||||
"repo", "create", repoName,
|
||||
"--public",
|
||||
"-d", "Created by fgj functional test",
|
||||
)
|
||||
|
||||
if result.ExitCode != 0 {
|
||||
t.Fatalf("repo create failed with exit code %d: %s", result.ExitCode, result.Stderr)
|
||||
}
|
||||
|
||||
if !bytes.Contains([]byte(result.Stdout), []byte(repoName)) {
|
||||
t.Fatalf("expected output to contain repo name %q, got: %s", repoName, result.Stdout)
|
||||
}
|
||||
|
||||
repo, _, err := env.Client.GetRepo(env.Owner, repoName)
|
||||
if err != nil {
|
||||
t.Fatalf("repo was not created or not accessible: %v", err)
|
||||
}
|
||||
if repo.Name != repoName {
|
||||
t.Fatalf("expected repo name %q, got %q", repoName, repo.Name)
|
||||
}
|
||||
if repo.Private {
|
||||
t.Fatalf("expected public repo, got private")
|
||||
}
|
||||
if repo.Description != "Created by fgj functional test" {
|
||||
t.Fatalf("expected description %q, got %q", "Created by fgj functional test", repo.Description)
|
||||
}
|
||||
|
||||
t.Logf("Successfully created repository %s via CLI", repo.FullName)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue