From f445ac7521973f93e2f993ed3ff07208a2af2974 Mon Sep 17 00:00:00 2001 From: 6543 <6543@noreply.gitea.io> Date: Wed, 30 Sep 2020 05:11:33 +0000 Subject: [PATCH] Refactor: apply new internal structurs (#206) fix lint fix lint Move print TrackedTimesList to print package Move AbsPathWithExpansion to utils/path.go rename module intern to config Move Subcomands into it's own Packages Split times subcomands into own sourcefiles Split repos subcomands into own sourcefiles Split releases subcomands into own sourcefiles Split pulls subcomands into own sourcefiles Split milestones subcomands into own sourcefiles Split login subcomands into own sourcefiles Split labels subcomands into own sourcefiles split issues subcomands into own sourcefiles mv Move Interactive Login Creation to interact package Move Add Login function to intern/login.go apply from review lint: add description to exported func smal nits Move DetailViews stdout print func to print package Refactor: * Move Config & Login routines into intern package * rename global var in cmd * Move help func to utils Co-authored-by: 6543 <6543@obermui.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/206 Reviewed-by: Norwin Reviewed-by: Lunny Xiao --- cmd/config.go | 286 ------------ cmd/{ => flags}/flags.go | 91 +--- cmd/issues.go | 200 +-------- cmd/issues/close.go | 46 ++ cmd/issues/create.go | 59 +++ cmd/issues/list.go | 92 ++++ cmd/issues/reopen.go | 26 ++ cmd/labels.go | 205 +-------- cmd/labels/create.go | 109 +++++ cmd/{labels_test.go => labels/create_test.go} | 2 +- cmd/labels/delete.go | 39 ++ cmd/labels/update.go | 75 ++++ cmd/login.go | 349 +-------------- cmd/login/add.go | 83 ++++ cmd/login/default.go | 52 +++ cmd/login/edit.go | 26 ++ cmd/login/list.go | 56 +++ cmd/logout.go | 14 +- cmd/milestones.go | 234 +--------- cmd/milestones/close.go | 32 ++ cmd/milestones/create.go | 69 +++ cmd/milestones/delete.go | 31 ++ .../issues.go} | 33 +- cmd/milestones/list.go | 93 ++++ cmd/milestones/reopen.go | 42 ++ cmd/notifications.go | 18 +- cmd/open.go | 6 +- cmd/pulls.go | 413 +----------------- cmd/pulls/checkout.go | 97 ++++ cmd/pulls/clean.go | 105 +++++ cmd/pulls/create.go | 145 ++++++ cmd/pulls/list.go | 93 ++++ cmd/releases.go | 300 +------------ cmd/releases/create.go | 102 +++++ cmd/releases/delete.go | 46 ++ cmd/releases/edit.go | 102 +++++ cmd/releases/list.go | 94 ++++ cmd/repos.go | 289 +----------- cmd/repos/create.go | 120 +++++ cmd/repos/list.go | 152 +++++++ cmd/times.go | 192 +------- cmd/times/add.go | 59 +++ cmd/times/delete.go | 57 +++ cmd/times/reset.go | 51 +++ modules/config/config.go | 160 +++++++ modules/config/login.go | 282 ++++++++++++ modules/git/auth.go | 22 +- modules/interact/login.go | 87 ++++ modules/print/issue.go | 31 ++ cmd/log.go => modules/print/list.go | 33 +- modules/print/milestone.go | 24 + modules/{utils/format.go => print/print.go} | 19 +- modules/print/pull.go | 31 ++ modules/print/repo.go | 44 ++ modules/print/times.go | 69 +++ modules/utils/parse.go | 18 + modules/utils/path.go | 34 ++ 57 files changed, 3127 insertions(+), 2512 deletions(-) delete mode 100644 cmd/config.go rename cmd/{ => flags}/flags.go (65%) create mode 100644 cmd/issues/close.go create mode 100644 cmd/issues/create.go create mode 100644 cmd/issues/list.go create mode 100644 cmd/issues/reopen.go create mode 100644 cmd/labels/create.go rename cmd/{labels_test.go => labels/create_test.go} (98%) create mode 100644 cmd/labels/delete.go create mode 100644 cmd/labels/update.go create mode 100644 cmd/login/add.go create mode 100644 cmd/login/default.go create mode 100644 cmd/login/edit.go create mode 100644 cmd/login/list.go create mode 100644 cmd/milestones/close.go create mode 100644 cmd/milestones/create.go create mode 100644 cmd/milestones/delete.go rename cmd/{milestone_issues.go => milestones/issues.go} (82%) create mode 100644 cmd/milestones/list.go create mode 100644 cmd/milestones/reopen.go create mode 100644 cmd/pulls/checkout.go create mode 100644 cmd/pulls/clean.go create mode 100644 cmd/pulls/create.go create mode 100644 cmd/pulls/list.go create mode 100644 cmd/releases/create.go create mode 100644 cmd/releases/delete.go create mode 100644 cmd/releases/edit.go create mode 100644 cmd/releases/list.go create mode 100644 cmd/repos/create.go create mode 100644 cmd/repos/list.go create mode 100644 cmd/times/add.go create mode 100644 cmd/times/delete.go create mode 100644 cmd/times/reset.go create mode 100644 modules/config/config.go create mode 100644 modules/config/login.go create mode 100644 modules/interact/login.go create mode 100644 modules/print/issue.go rename cmd/log.go => modules/print/list.go (75%) create mode 100644 modules/print/milestone.go rename modules/{utils/format.go => print/print.go} (61%) create mode 100644 modules/print/pull.go create mode 100644 modules/print/repo.go create mode 100644 modules/print/times.go create mode 100644 modules/utils/parse.go diff --git a/cmd/config.go b/cmd/config.go deleted file mode 100644 index 875f245..0000000 --- a/cmd/config.go +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package cmd - -import ( - "crypto/tls" - "errors" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/http/cookiejar" - "net/url" - "os" - "path/filepath" - "strings" - - "code.gitea.io/sdk/gitea" - "code.gitea.io/tea/modules/git" - "code.gitea.io/tea/modules/utils" - - "github.com/muesli/termenv" - "github.com/urfave/cli/v2" - "gopkg.in/yaml.v2" -) - -// Login represents a login to a gitea server, you even could add multiple logins for one gitea server -type Login struct { - Name string `yaml:"name"` - URL string `yaml:"url"` - Token string `yaml:"token"` - Default bool `yaml:"default"` - SSHHost string `yaml:"ssh_host"` - // optional path to the private key - SSHKey string `yaml:"ssh_key"` - Insecure bool `yaml:"insecure"` - // optional gitea username - User string `yaml:"user"` -} - -// Client returns a client to operate Gitea API -func (l *Login) Client() *gitea.Client { - httpClient := &http.Client{} - if l.Insecure { - cookieJar, _ := cookiejar.New(nil) - - httpClient = &http.Client{ - Jar: cookieJar, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }} - } - - client, err := gitea.NewClient(l.URL, - gitea.SetToken(l.Token), - gitea.SetHTTPClient(httpClient), - ) - if err != nil { - log.Fatal(err) - } - return client -} - -// GetSSHHost returns SSH host name -func (l *Login) GetSSHHost() string { - if l.SSHHost != "" { - return l.SSHHost - } - - u, err := url.Parse(l.URL) - if err != nil { - return "" - } - - return u.Hostname() -} - -// Config reprensents local configurations -type Config struct { - Logins []Login `yaml:"logins"` -} - -var ( - config Config - yamlConfigPath string -) - -func init() { - homeDir, err := utils.Home() - if err != nil { - log.Fatal("Retrieve home dir failed") - } - - dir := filepath.Join(homeDir, ".tea") - err = os.MkdirAll(dir, os.ModePerm) - if err != nil { - log.Fatal("Init tea config dir " + dir + " failed") - } - - yamlConfigPath = filepath.Join(dir, "tea.yml") -} - -func getGlamourTheme() string { - if termenv.HasDarkBackground() { - return "dark" - } - return "light" -} - -func getOwnerAndRepo(repoPath, user string) (string, string) { - if len(repoPath) == 0 { - return "", "" - } - p := strings.Split(repoPath, "/") - if len(p) >= 2 { - return p[0], p[1] - } - return user, repoPath -} - -func getDefaultLogin() (*Login, error) { - if len(config.Logins) == 0 { - return nil, errors.New("No available login") - } - for _, l := range config.Logins { - if l.Default { - return &l, nil - } - } - - return &config.Logins[0], nil -} - -func getLoginByName(name string) *Login { - for _, l := range config.Logins { - if l.Name == name { - return &l - } - } - return nil -} - -func addLogin(login Login) error { - for _, l := range config.Logins { - if l.Name == login.Name { - if l.URL == login.URL && l.Token == login.Token { - return nil - } - return errors.New("Login name has already been used") - } - if l.URL == login.URL && l.Token == login.Token { - return errors.New("URL has been added") - } - } - - u, err := url.Parse(login.URL) - if err != nil { - return err - } - - if login.SSHHost == "" { - login.SSHHost = u.Hostname() - } - config.Logins = append(config.Logins, login) - - return nil -} - -func isFileExist(fileName string) (bool, error) { - f, err := os.Stat(fileName) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - if f.IsDir() { - return false, errors.New("A directory with the same name exists") - } - return true, nil -} - -func loadConfig(ymlPath string) error { - exist, _ := isFileExist(ymlPath) - if exist { - Println("Found config file", ymlPath) - bs, err := ioutil.ReadFile(ymlPath) - if err != nil { - return err - } - - err = yaml.Unmarshal(bs, &config) - if err != nil { - return err - } - } - - return nil -} - -func saveConfig(ymlPath string) error { - bs, err := yaml.Marshal(&config) - if err != nil { - return err - } - return ioutil.WriteFile(ymlPath, bs, 0660) -} - -func curGitRepoPath(path string) (*Login, string, error) { - var err error - var repo *git.TeaRepo - if len(path) == 0 { - repo, err = git.RepoForWorkdir() - } else { - repo, err = git.RepoFromPath(path) - } - if err != nil { - return nil, "", err - } - gitConfig, err := repo.Config() - if err != nil { - return nil, "", err - } - - // if no remote - if len(gitConfig.Remotes) == 0 { - return nil, "", errors.New("No remote(s) found in this Git repository") - } - - // if only one remote exists - if len(gitConfig.Remotes) >= 1 && len(remoteValue) == 0 { - for remote := range gitConfig.Remotes { - remoteValue = remote - } - if len(gitConfig.Remotes) > 1 { - // if master branch is present, use it as the default remote - masterBranch, ok := gitConfig.Branches["master"] - if ok { - if len(masterBranch.Remote) > 0 { - remoteValue = masterBranch.Remote - } - } - } - } - - remoteConfig, ok := gitConfig.Remotes[remoteValue] - if !ok || remoteConfig == nil { - return nil, "", errors.New("Remote " + remoteValue + " not found in this Git repository") - } - - for _, l := range config.Logins { - for _, u := range remoteConfig.URLs { - p, err := git.ParseURL(strings.TrimSpace(u)) - if err != nil { - return nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error()) - } - if strings.EqualFold(p.Scheme, "http") || strings.EqualFold(p.Scheme, "https") { - if strings.HasPrefix(u, l.URL) { - ps := strings.Split(p.Path, "/") - path := strings.Join(ps[len(ps)-2:], "/") - return &l, strings.TrimSuffix(path, ".git"), nil - } - } else if strings.EqualFold(p.Scheme, "ssh") { - if l.GetSSHHost() == strings.Split(p.Host, ":")[0] { - return &l, strings.TrimLeft(strings.TrimSuffix(p.Path, ".git"), "/"), nil - } - } - } - } - - return nil, "", errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository") -} - -func getListOptions(ctx *cli.Context) gitea.ListOptions { - page := ctx.Int("page") - limit := ctx.Int("limit") - if limit != 0 && page == 0 { - page = 1 - } - return gitea.ListOptions{ - Page: page, - PageSize: limit, - } -} diff --git a/cmd/flags.go b/cmd/flags/flags.go similarity index 65% rename from cmd/flags.go rename to cmd/flags/flags.go index 133e12f..aba9dad 100644 --- a/cmd/flags.go +++ b/cmd/flags/flags.go @@ -2,23 +2,24 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package cmd +package flags import ( - "log" - - "code.gitea.io/tea/modules/utils" - + "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" ) // create global variables for global Flags to simplify // access to the options without requiring cli.Context var ( - loginValue string - repoValue string - outputValue string - remoteValue string + // GlobalLoginValue contain value of --login|-l arg + GlobalLoginValue string + // GlobalRepoValue contain value of --repo|-r arg + GlobalRepoValue string + // GlobalOutputValue contain value of --output|-o arg + GlobalOutputValue string + // GlobalRemoteValue contain value of --remote|-R arg + GlobalRemoteValue string ) // LoginFlag provides flag to specify tea login profile @@ -26,7 +27,7 @@ var LoginFlag = cli.StringFlag{ Name: "login", Aliases: []string{"l"}, Usage: "Use a different Gitea login. Optional", - Destination: &loginValue, + Destination: &GlobalLoginValue, } // RepoFlag provides flag to specify repository @@ -34,7 +35,7 @@ var RepoFlag = cli.StringFlag{ Name: "repo", Aliases: []string{"r"}, Usage: "Repository to interact with. Optional", - Destination: &repoValue, + Destination: &GlobalRepoValue, } // RemoteFlag provides flag to specify remote repository @@ -42,7 +43,7 @@ var RemoteFlag = cli.StringFlag{ Name: "remote", Aliases: []string{"R"}, Usage: "Discover Gitea login from remote. Optional", - Destination: &remoteValue, + Destination: &GlobalRemoteValue, } // OutputFlag provides flag to specify output type @@ -50,7 +51,7 @@ var OutputFlag = cli.StringFlag{ Name: "output", Aliases: []string{"o"}, Usage: "Output format. (csv, simple, table, tsv, yaml)", - Destination: &outputValue, + Destination: &GlobalOutputValue, } // StateFlag provides flag to specify issue/pr state, defaulting to "open" @@ -109,61 +110,15 @@ var IssuePRFlags = append([]cli.Flag{ &PaginationLimitFlag, }, AllDefaultFlags...) -// initCommand returns repository and *Login based on flags -func initCommand() (*Login, string, string) { - var login *Login - - err := loadConfig(yamlConfigPath) - if err != nil { - log.Fatal("load config file failed ", yamlConfigPath) +// GetListOptions return ListOptions based on PaginationFlags +func GetListOptions(ctx *cli.Context) gitea.ListOptions { + page := ctx.Int("page") + limit := ctx.Int("limit") + if limit != 0 && page == 0 { + page = 1 } - - if login, err = getDefaultLogin(); err != nil { - log.Fatal(err.Error()) + return gitea.ListOptions{ + Page: page, + PageSize: limit, } - - exist, err := utils.PathExists(repoValue) - if err != nil { - log.Fatal(err.Error()) - } - - if exist || len(repoValue) == 0 { - login, repoValue, err = curGitRepoPath(repoValue) - if err != nil { - log.Fatal(err.Error()) - } - } - - if loginValue != "" { - login = getLoginByName(loginValue) - if login == nil { - log.Fatal("Login name " + loginValue + " does not exist") - } - } - - owner, repo := getOwnerAndRepo(repoValue, login.User) - return login, owner, repo -} - -// initCommandLoginOnly return *Login based on flags -func initCommandLoginOnly() *Login { - err := loadConfig(yamlConfigPath) - if err != nil { - log.Fatal("load config file failed ", yamlConfigPath) - } - - var login *Login - if loginValue == "" { - login, err = getDefaultLogin() - if err != nil { - log.Fatal(err) - } - } else { - login = getLoginByName(loginValue) - if login == nil { - log.Fatal("Login name " + loginValue + " does not exist") - } - } - - return login } diff --git a/cmd/issues.go b/cmd/issues.go index a957e60..9987360 100644 --- a/cmd/issues.go +++ b/cmd/issues.go @@ -5,12 +5,12 @@ package cmd import ( - "fmt" - "log" - "strconv" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/cmd/issues" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + "code.gitea.io/tea/modules/utils" - "code.gitea.io/sdk/gitea" - "github.com/charmbracelet/glamour" "github.com/urfave/cli/v2" ) @@ -22,34 +22,25 @@ var CmdIssues = cli.Command{ ArgsUsage: "[]", Action: runIssues, Subcommands: []*cli.Command{ - &CmdIssuesList, - &CmdIssuesCreate, - &CmdIssuesReopen, - &CmdIssuesClose, + &issues.CmdIssuesList, + &issues.CmdIssuesCreate, + &issues.CmdIssuesReopen, + &issues.CmdIssuesClose, }, - Flags: IssuePRFlags, -} - -// CmdIssuesList represents a sub command of issues to list issues -var CmdIssuesList = cli.Command{ - Name: "ls", - Usage: "List issues of the repository", - Description: `List issues of the repository`, - Action: runIssuesList, - Flags: IssuePRFlags, + Flags: flags.IssuePRFlags, } func runIssues(ctx *cli.Context) error { if ctx.Args().Len() == 1 { - return runIssueDetail(ctx, ctx.Args().First()) + return runIssueDetail(ctx.Args().First()) } - return runIssuesList(ctx) + return issues.RunIssuesList(ctx) } -func runIssueDetail(ctx *cli.Context, index string) error { - login, owner, repo := initCommand() +func runIssueDetail(index string) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) - idx, err := argToIndex(index) + idx, err := utils.ArgToIndex(index) if err != nil { return err } @@ -57,165 +48,6 @@ func runIssueDetail(ctx *cli.Context, index string) error { if err != nil { return err } - - in := fmt.Sprintf("# #%d %s (%s)\n%s created %s\n\n%s\n", issue.Index, - issue.Title, - issue.State, - issue.Poster.UserName, - issue.Created.Format("2006-01-02 15:04:05"), - issue.Body, - ) - out, err := glamour.Render(in, getGlamourTheme()) - fmt.Print(out) + print.IssueDetails(issue) return nil } - -func runIssuesList(ctx *cli.Context) error { - login, owner, repo := initCommand() - - state := gitea.StateOpen - switch ctx.String("state") { - case "all": - state = gitea.StateAll - case "open": - state = gitea.StateOpen - case "closed": - state = gitea.StateClosed - } - - issues, _, err := login.Client().ListRepoIssues(owner, repo, gitea.ListIssueOption{ - ListOptions: getListOptions(ctx), - State: state, - Type: gitea.IssueTypeIssue, - }) - - if err != nil { - log.Fatal(err) - } - - headers := []string{ - "Index", - "Title", - "State", - "Author", - "Milestone", - "Updated", - } - - var values [][]string - - if len(issues) == 0 { - Output(outputValue, headers, values) - return nil - } - - for _, issue := range issues { - author := issue.Poster.FullName - if len(author) == 0 { - author = issue.Poster.UserName - } - mile := "" - if issue.Milestone != nil { - mile = issue.Milestone.Title - } - values = append( - values, - []string{ - strconv.FormatInt(issue.Index, 10), - issue.Title, - string(issue.State), - author, - mile, - issue.Updated.Format("2006-01-02 15:04:05"), - }, - ) - } - Output(outputValue, headers, values) - - return nil -} - -// CmdIssuesCreate represents a sub command of issues to create issue -var CmdIssuesCreate = cli.Command{ - Name: "create", - Usage: "Create an issue on repository", - Description: `Create an issue on repository`, - Action: runIssuesCreate, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "title", - Aliases: []string{"t"}, - Usage: "issue title to create", - }, - &cli.StringFlag{ - Name: "body", - Aliases: []string{"b"}, - Usage: "issue body to create", - }, - }, LoginRepoFlags...), -} - -func runIssuesCreate(ctx *cli.Context) error { - login, owner, repo := initCommand() - - _, _, err := login.Client().CreateIssue(owner, repo, gitea.CreateIssueOption{ - Title: ctx.String("title"), - Body: ctx.String("body"), - // TODO: - //Assignee string `json:"assignee"` - //Assignees []string `json:"assignees"` - //Deadline *time.Time `json:"due_date"` - //Milestone int64 `json:"milestone"` - //Labels []int64 `json:"labels"` - //Closed bool `json:"closed"` - }) - - if err != nil { - log.Fatal(err) - } - - return nil -} - -// CmdIssuesReopen represents a sub command of issues to open an issue -var CmdIssuesReopen = cli.Command{ - Name: "reopen", - Aliases: []string{"open"}, - Usage: "Change state of an issue to 'open'", - Description: `Change state of an issue to 'open'`, - ArgsUsage: "", - Action: func(ctx *cli.Context) error { - var s = gitea.StateOpen - return editIssueState(ctx, gitea.EditIssueOption{State: &s}) - }, - Flags: AllDefaultFlags, -} - -// CmdIssuesClose represents a sub command of issues to close an issue -var CmdIssuesClose = cli.Command{ - Name: "close", - Usage: "Change state of an issue to 'closed'", - Description: `Change state of an issue to 'closed'`, - ArgsUsage: "", - Action: func(ctx *cli.Context) error { - var s = gitea.StateClosed - return editIssueState(ctx, gitea.EditIssueOption{State: &s}) - }, - Flags: AllDefaultFlags, -} - -// editIssueState abstracts the arg parsing to edit the given issue -func editIssueState(ctx *cli.Context, opts gitea.EditIssueOption) error { - login, owner, repo := initCommand() - if ctx.Args().Len() == 0 { - log.Fatal(ctx.Command.ArgsUsage) - } - - index, err := argToIndex(ctx.Args().First()) - if err != nil { - return err - } - - _, _, err = login.Client().EditIssue(owner, repo, index, opts) - return err -} diff --git a/cmd/issues/close.go b/cmd/issues/close.go new file mode 100644 index 0000000..850dde4 --- /dev/null +++ b/cmd/issues/close.go @@ -0,0 +1,46 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import ( + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/utils" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdIssuesClose represents a sub command of issues to close an issue +var CmdIssuesClose = cli.Command{ + Name: "close", + Usage: "Change state of an issue to 'closed'", + Description: `Change state of an issue to 'closed'`, + ArgsUsage: "", + Action: func(ctx *cli.Context) error { + var s = gitea.StateClosed + return editIssueState(ctx, gitea.EditIssueOption{State: &s}) + }, + Flags: flags.AllDefaultFlags, +} + +// editIssueState abstracts the arg parsing to edit the given issue +func editIssueState(ctx *cli.Context, opts gitea.EditIssueOption) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + if ctx.Args().Len() == 0 { + log.Fatal(ctx.Command.ArgsUsage) + } + + index, err := utils.ArgToIndex(ctx.Args().First()) + if err != nil { + return err + } + + _, _, err = login.Client().EditIssue(owner, repo, index, opts) + // TODO: print (short)IssueDetails + return err +} diff --git a/cmd/issues/create.go b/cmd/issues/create.go new file mode 100644 index 0000000..60b3b21 --- /dev/null +++ b/cmd/issues/create.go @@ -0,0 +1,59 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import ( + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdIssuesCreate represents a sub command of issues to create issue +var CmdIssuesCreate = cli.Command{ + Name: "create", + Usage: "Create an issue on repository", + Description: `Create an issue on repository`, + Action: runIssuesCreate, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "title", + Aliases: []string{"t"}, + Usage: "issue title to create", + }, + &cli.StringFlag{ + Name: "body", + Aliases: []string{"b"}, + Usage: "issue body to create", + }, + }, flags.LoginRepoFlags...), +} + +func runIssuesCreate(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + _, _, err := login.Client().CreateIssue(owner, repo, gitea.CreateIssueOption{ + Title: ctx.String("title"), + Body: ctx.String("body"), + // TODO: + //Assignee string `json:"assignee"` + //Assignees []string `json:"assignees"` + //Deadline *time.Time `json:"due_date"` + //Milestone int64 `json:"milestone"` + //Labels []int64 `json:"labels"` + //Closed bool `json:"closed"` + }) + + if err != nil { + log.Fatal(err) + } + + // TODO: Print IssueDetails + + return nil +} diff --git a/cmd/issues/list.go b/cmd/issues/list.go new file mode 100644 index 0000000..1f7e7b8 --- /dev/null +++ b/cmd/issues/list.go @@ -0,0 +1,92 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import ( + "log" + "strconv" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdIssuesList represents a sub command of issues to list issues +var CmdIssuesList = cli.Command{ + Name: "ls", + Usage: "List issues of the repository", + Description: `List issues of the repository`, + Action: RunIssuesList, + Flags: flags.IssuePRFlags, +} + +// RunIssuesList list issues +func RunIssuesList(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + state := gitea.StateOpen + switch ctx.String("state") { + case "all": + state = gitea.StateAll + case "open": + state = gitea.StateOpen + case "closed": + state = gitea.StateClosed + } + + issues, _, err := login.Client().ListRepoIssues(owner, repo, gitea.ListIssueOption{ + ListOptions: flags.GetListOptions(ctx), + State: state, + Type: gitea.IssueTypeIssue, + }) + + if err != nil { + log.Fatal(err) + } + + headers := []string{ + "Index", + "Title", + "State", + "Author", + "Milestone", + "Updated", + } + + var values [][]string + + if len(issues) == 0 { + print.OutputList(flags.GlobalOutputValue, headers, values) + return nil + } + + for _, issue := range issues { + author := issue.Poster.FullName + if len(author) == 0 { + author = issue.Poster.UserName + } + mile := "" + if issue.Milestone != nil { + mile = issue.Milestone.Title + } + values = append( + values, + []string{ + strconv.FormatInt(issue.Index, 10), + issue.Title, + string(issue.State), + author, + mile, + issue.Updated.Format("2006-01-02 15:04:05"), + }, + ) + } + print.OutputList(flags.GlobalOutputValue, headers, values) + + return nil +} diff --git a/cmd/issues/reopen.go b/cmd/issues/reopen.go new file mode 100644 index 0000000..2d78990 --- /dev/null +++ b/cmd/issues/reopen.go @@ -0,0 +1,26 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import ( + "code.gitea.io/tea/cmd/flags" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdIssuesReopen represents a sub command of issues to open an issue +var CmdIssuesReopen = cli.Command{ + Name: "reopen", + Aliases: []string{"open"}, + Usage: "Change state of an issue to 'open'", + Description: `Change state of an issue to 'open'`, + ArgsUsage: "", + Action: func(ctx *cli.Context) error { + var s = gitea.StateOpen + return editIssueState(ctx, gitea.EditIssueOption{State: &s}) + }, + Flags: flags.AllDefaultFlags, +} diff --git a/cmd/labels.go b/cmd/labels.go index 6daf842..4b9a67b 100644 --- a/cmd/labels.go +++ b/cmd/labels.go @@ -5,15 +5,17 @@ package cmd import ( - "bufio" "fmt" "log" "os" "strconv" - "strings" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/cmd/labels" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" "code.gitea.io/sdk/gitea" - "github.com/muesli/termenv" "github.com/urfave/cli/v2" ) @@ -25,9 +27,9 @@ var CmdLabels = cli.Command{ Description: `Manage issue labels`, Action: runLabels, Subcommands: []*cli.Command{ - &CmdLabelCreate, - &CmdLabelUpdate, - &CmdLabelDelete, + &labels.CmdLabelCreate, + &labels.CmdLabelUpdate, + &labels.CmdLabelDelete, }, Flags: append([]cli.Flag{ &cli.StringFlag{ @@ -35,13 +37,13 @@ var CmdLabels = cli.Command{ Aliases: []string{"s"}, Usage: "Save all the labels as a file", }, - &PaginationPageFlag, - &PaginationLimitFlag, - }, AllDefaultFlags...), + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.AllDefaultFlags...), } func runLabels(ctx *cli.Context) error { - login, owner, repo := initCommand() + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) headers := []string{ "Index", @@ -52,13 +54,13 @@ func runLabels(ctx *cli.Context) error { var values [][]string - labels, _, err := login.Client().ListRepoLabels(owner, repo, gitea.ListLabelsOptions{ListOptions: getListOptions(ctx)}) + labels, _, err := login.Client().ListRepoLabels(owner, repo, gitea.ListLabelsOptions{ListOptions: flags.GetListOptions(ctx)}) if err != nil { log.Fatal(err) } if len(labels) == 0 { - Output(outputValue, headers, values) + print.OutputList(flags.GlobalOutputValue, headers, values) return nil } @@ -89,184 +91,7 @@ func runLabels(ctx *cli.Context) error { }, ) } - Output(outputValue, headers, values) - } - - return nil -} - -// CmdLabelCreate represents a sub command of labels to create label. -var CmdLabelCreate = cli.Command{ - Name: "create", - Usage: "Create a label", - Description: `Create a label`, - Action: runLabelCreate, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Usage: "label name", - }, - &cli.StringFlag{ - Name: "color", - Usage: "label color value", - }, - &cli.StringFlag{ - Name: "description", - Usage: "label description", - }, - &cli.StringFlag{ - Name: "file", - Usage: "indicate a label file", - }, - }, -} - -func splitLabelLine(line string) (string, string, string) { - fields := strings.SplitN(line, ";", 2) - var color, name, description string - if len(fields) < 1 { - return "", "", "" - } else if len(fields) >= 2 { - description = strings.TrimSpace(fields[1]) - } - fields = strings.Fields(fields[0]) - if len(fields) <= 0 { - return "", "", "" - } - color = fields[0] - if len(fields) == 2 { - name = fields[1] - } else if len(fields) > 2 { - name = strings.Join(fields[1:], " ") - } - return color, name, description -} - -func runLabelCreate(ctx *cli.Context) error { - login, owner, repo := initCommand() - - labelFile := ctx.String("file") - var err error - if len(labelFile) == 0 { - _, _, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{ - Name: ctx.String("name"), - Color: ctx.String("color"), - Description: ctx.String("description"), - }) - } else { - f, err := os.Open(labelFile) - if err != nil { - return err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - var i = 1 - // FIXME: if Gitea's API support create multiple labels once, we should move to that API. - for scanner.Scan() { - line := scanner.Text() - color, name, description := splitLabelLine(line) - if color == "" || name == "" { - log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line) - } else { - _, _, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{ - Name: name, - Color: color, - Description: description, - }) - } - - i++ - } - } - - if err != nil { - log.Fatal(err) - } - - return nil -} - -// CmdLabelUpdate represents a sub command of labels to update label. -var CmdLabelUpdate = cli.Command{ - Name: "update", - Usage: "Update a label", - Description: `Update a label`, - Action: runLabelUpdate, - Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "id", - Usage: "label id", - }, - &cli.StringFlag{ - Name: "name", - Usage: "label name", - }, - &cli.StringFlag{ - Name: "color", - Usage: "label color value", - }, - &cli.StringFlag{ - Name: "description", - Usage: "label description", - }, - }, -} - -func runLabelUpdate(ctx *cli.Context) error { - login, owner, repo := initCommand() - - id := ctx.Int64("id") - var pName, pColor, pDescription *string - name := ctx.String("name") - if name != "" { - pName = &name - } - - color := ctx.String("color") - if color != "" { - pColor = &color - } - - description := ctx.String("description") - if description != "" { - pDescription = &description - } - - var err error - _, _, err = login.Client().EditLabel(owner, repo, id, gitea.EditLabelOption{ - Name: pName, - Color: pColor, - Description: pDescription, - }) - - if err != nil { - log.Fatal(err) - } - - return nil -} - -// CmdLabelDelete represents a sub command of labels to delete label. -var CmdLabelDelete = cli.Command{ - Name: "delete", - Usage: "Delete a label", - Description: `Delete a label`, - Action: runLabelDelete, - Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "id", - Usage: "label id", - }, - }, -} - -func runLabelDelete(ctx *cli.Context) error { - login, owner, repo := initCommand() - - _, err := login.Client().DeleteLabel(owner, repo, ctx.Int64("id")) - if err != nil { - log.Fatal(err) + print.OutputList(flags.GlobalOutputValue, headers, values) } return nil diff --git a/cmd/labels/create.go b/cmd/labels/create.go new file mode 100644 index 0000000..83fdd97 --- /dev/null +++ b/cmd/labels/create.go @@ -0,0 +1,109 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package labels + +import ( + "bufio" + "log" + "os" + "strings" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdLabelCreate represents a sub command of labels to create label. +var CmdLabelCreate = cli.Command{ + Name: "create", + Usage: "Create a label", + Description: `Create a label`, + Action: runLabelCreate, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "label name", + }, + &cli.StringFlag{ + Name: "color", + Usage: "label color value", + }, + &cli.StringFlag{ + Name: "description", + Usage: "label description", + }, + &cli.StringFlag{ + Name: "file", + Usage: "indicate a label file", + }, + }, +} + +func runLabelCreate(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + labelFile := ctx.String("file") + var err error + if len(labelFile) == 0 { + _, _, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{ + Name: ctx.String("name"), + Color: ctx.String("color"), + Description: ctx.String("description"), + }) + } else { + f, err := os.Open(labelFile) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + var i = 1 + for scanner.Scan() { + line := scanner.Text() + color, name, description := splitLabelLine(line) + if color == "" || name == "" { + log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line) + } else { + _, _, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{ + Name: name, + Color: color, + Description: description, + }) + } + + i++ + } + } + + if err != nil { + log.Fatal(err) + } + + return nil +} + +func splitLabelLine(line string) (string, string, string) { + fields := strings.SplitN(line, ";", 2) + var color, name, description string + if len(fields) < 1 { + return "", "", "" + } else if len(fields) >= 2 { + description = strings.TrimSpace(fields[1]) + } + fields = strings.Fields(fields[0]) + if len(fields) <= 0 { + return "", "", "" + } + color = fields[0] + if len(fields) == 2 { + name = fields[1] + } else if len(fields) > 2 { + name = strings.Join(fields[1:], " ") + } + return color, name, description +} diff --git a/cmd/labels_test.go b/cmd/labels/create_test.go similarity index 98% rename from cmd/labels_test.go rename to cmd/labels/create_test.go index fd6b5b6..17e6e27 100644 --- a/cmd/labels_test.go +++ b/cmd/labels/create_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package cmd +package labels import ( "bufio" diff --git a/cmd/labels/delete.go b/cmd/labels/delete.go new file mode 100644 index 0000000..26e69b8 --- /dev/null +++ b/cmd/labels/delete.go @@ -0,0 +1,39 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package labels + +import ( + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "github.com/urfave/cli/v2" +) + +// CmdLabelDelete represents a sub command of labels to delete label. +var CmdLabelDelete = cli.Command{ + Name: "delete", + Usage: "Delete a label", + Description: `Delete a label`, + Action: runLabelDelete, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "id", + Usage: "label id", + }, + }, +} + +func runLabelDelete(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + _, err := login.Client().DeleteLabel(owner, repo, ctx.Int64("id")) + if err != nil { + log.Fatal(err) + } + + return nil +} diff --git a/cmd/labels/update.go b/cmd/labels/update.go new file mode 100644 index 0000000..3731452 --- /dev/null +++ b/cmd/labels/update.go @@ -0,0 +1,75 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package labels + +import ( + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdLabelUpdate represents a sub command of labels to update label. +var CmdLabelUpdate = cli.Command{ + Name: "update", + Usage: "Update a label", + Description: `Update a label`, + Action: runLabelUpdate, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "id", + Usage: "label id", + }, + &cli.StringFlag{ + Name: "name", + Usage: "label name", + }, + &cli.StringFlag{ + Name: "color", + Usage: "label color value", + }, + &cli.StringFlag{ + Name: "description", + Usage: "label description", + }, + }, +} + +func runLabelUpdate(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + id := ctx.Int64("id") + var pName, pColor, pDescription *string + name := ctx.String("name") + if name != "" { + pName = &name + } + + color := ctx.String("color") + if color != "" { + pColor = &color + } + + description := ctx.String("description") + if description != "" { + pDescription = &description + } + + var err error + _, _, err = login.Client().EditLabel(owner, repo, id, gitea.EditLabelOption{ + Name: pName, + Color: pColor, + Description: pDescription, + }) + + if err != nil { + log.Fatal(err) + } + + return nil +} diff --git a/cmd/login.go b/cmd/login.go index 97d6053..f1031e1 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -5,19 +5,8 @@ package cmd import ( - "crypto/tls" - "fmt" - "log" - "net/http" - "net/http/cookiejar" - "net/url" - "os" - "strings" - "time" + "code.gitea.io/tea/cmd/login" - "code.gitea.io/sdk/gitea" - - "github.com/skratchdot/open-golang/open" "github.com/urfave/cli/v2" ) @@ -26,337 +15,11 @@ var CmdLogin = cli.Command{ Name: "login", Usage: "Log in to a Gitea server", Description: `Log in to a Gitea server`, - Action: runLoginAddInteractive, + Action: login.RunLoginAddInteractive, // TODO show list if no arg & detail if login as arg Subcommands: []*cli.Command{ - &cmdLoginList, - &cmdLoginAdd, - &cmdLoginEdit, - &cmdLoginSetDefault, + &login.CmdLoginList, + &login.CmdLoginAdd, + &login.CmdLoginEdit, + &login.CmdLoginSetDefault, }, } - -// cmdLoginEdit represents to login a gitea server. -var cmdLoginEdit = cli.Command{ - Name: "edit", - Usage: "Edit Gitea logins", - Description: `Edit Gitea logins`, - Action: runLoginEdit, - Flags: []cli.Flag{&OutputFlag}, -} - -func runLoginEdit(ctx *cli.Context) error { - return open.Start(yamlConfigPath) -} - -// cmdLoginSetDefault represents to login a gitea server. -var cmdLoginSetDefault = cli.Command{ - Name: "default", - Usage: "Get or Set Default Login", - Description: `Get or Set Default Login`, - ArgsUsage: "", - Action: runLoginSetDefault, - Flags: []cli.Flag{&OutputFlag}, -} - -func runLoginSetDefault(ctx *cli.Context) error { - if err := loadConfig(yamlConfigPath); err != nil { - return err - } - if ctx.Args().Len() == 0 { - l, err := getDefaultLogin() - if err != nil { - return err - } - fmt.Printf("Default Login: %s\n", l.Name) - return nil - } - loginExist := false - for i := range config.Logins { - config.Logins[i].Default = false - if config.Logins[i].Name == ctx.Args().First() { - config.Logins[i].Default = true - loginExist = true - } - } - - if !loginExist { - return fmt.Errorf("login '%s' not found", ctx.Args().First()) - } - - return saveConfig(yamlConfigPath) -} - -// CmdLogin represents to login a gitea server. -var cmdLoginAdd = cli.Command{ - Name: "add", - Usage: "Add a Gitea login", - Description: `Add a Gitea login`, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Aliases: []string{"n"}, - Usage: "Login name", - }, - &cli.StringFlag{ - Name: "url", - Aliases: []string{"u"}, - Value: "https://try.gitea.io", - EnvVars: []string{"GITEA_SERVER_URL"}, - Usage: "Server URL", - Required: true, - }, - &cli.StringFlag{ - Name: "token", - Aliases: []string{"t"}, - Value: "", - EnvVars: []string{"GITEA_SERVER_TOKEN"}, - Usage: "Access token. Can be obtained from Settings > Applications", - }, - &cli.StringFlag{ - Name: "user", - Value: "", - EnvVars: []string{"GITEA_SERVER_USER"}, - Usage: "User for basic auth (will create token)", - }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"pwd"}, - Value: "", - EnvVars: []string{"GITEA_SERVER_PASSWORD"}, - Usage: "Password for basic auth (will create token)", - }, - &cli.StringFlag{ - Name: "ssh-key", - Aliases: []string{"s"}, - Usage: "Path to a SSH key to use for pull/push operations", - }, - &cli.BoolFlag{ - Name: "insecure", - Aliases: []string{"i"}, - Usage: "Disable TLS verification", - }, - }, - Action: runLoginAdd, -} - -func runLoginAdd(ctx *cli.Context) error { - return runLoginAddMain( - ctx.String("name"), - ctx.String("token"), - ctx.String("user"), - ctx.String("password"), - ctx.String("ssh-key"), - ctx.String("url"), - ctx.Bool("insecure")) -} - -func runLoginAddInteractive(ctx *cli.Context) error { - var stdin, name, token, user, passwd, sshKey, giteaURL string - var insecure = false - - fmt.Print("URL of Gitea instance: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - giteaURL = strings.TrimSpace(stdin) - if len(giteaURL) == 0 { - fmt.Println("URL is required!") - return nil - } - - parsedURL, err := url.Parse(giteaURL) - if err != nil { - return err - } - name = strings.ReplaceAll(strings.Title(parsedURL.Host), ".", "") - - fmt.Print("Name of new Login [" + name + "]: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - if len(strings.TrimSpace(stdin)) != 0 { - name = strings.TrimSpace(stdin) - } - - fmt.Print("Do you have a token [Yes/no]: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - if len(stdin) != 0 && strings.ToLower(stdin[:1]) == "n" { - fmt.Print("Username: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - user = strings.TrimSpace(stdin) - - fmt.Print("Password: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - passwd = strings.TrimSpace(stdin) - } else { - fmt.Print("Token: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - token = strings.TrimSpace(stdin) - } - - fmt.Print("Set Optional settings [yes/No]: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - if len(stdin) != 0 && strings.ToLower(stdin[:1]) == "y" { - fmt.Print("SSH Key Path: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - sshKey = strings.TrimSpace(stdin) - - fmt.Print("Allow Insecure connections [yes/No]: ") - if _, err := fmt.Scanln(&stdin); err != nil { - stdin = "" - } - insecure = len(stdin) != 0 && strings.ToLower(stdin[:1]) == "y" - } - - return runLoginAddMain(name, token, user, passwd, sshKey, giteaURL, insecure) -} - -func runLoginAddMain(name, token, user, passwd, sshKey, giteaURL string, insecure bool) error { - - if len(giteaURL) == 0 { - log.Fatal("You have to input Gitea server URL") - } - if len(token) == 0 && (len(user)+len(passwd)) == 0 { - log.Fatal("No token set") - } else if len(user) != 0 && len(passwd) == 0 { - log.Fatal("No password set") - } else if len(user) == 0 && len(passwd) != 0 { - log.Fatal("No user set") - } - - err := loadConfig(yamlConfigPath) - if err != nil { - log.Fatal("Unable to load config file " + yamlConfigPath) - } - - httpClient := &http.Client{} - if insecure { - cookieJar, _ := cookiejar.New(nil) - httpClient = &http.Client{ - Jar: cookieJar, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }} - } - client, err := gitea.NewClient(giteaURL, - gitea.SetToken(token), - gitea.SetBasicAuth(user, passwd), - gitea.SetHTTPClient(httpClient), - ) - if err != nil { - log.Fatal(err) - } - - u, _, err := client.GetMyUserInfo() - if err != nil { - log.Fatal(err) - } - - if len(token) == 0 { - // create token - host, _ := os.Hostname() - tl, _, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{}) - if err != nil { - return err - } - tokenName := host + "-tea" - for i := range tl { - if tl[i].Name == tokenName { - tokenName += time.Now().Format("2006-01-02_15-04-05") - break - } - } - t, _, err := client.CreateAccessToken(gitea.CreateAccessTokenOption{Name: tokenName}) - if err != nil { - return err - } - token = t.Token - } - - fmt.Println("Login successful! Login name " + u.UserName) - - if len(name) == 0 { - parsedURL, err := url.Parse(giteaURL) - if err != nil { - return err - } - name = strings.ReplaceAll(strings.Title(parsedURL.Host), ".", "") - for _, l := range config.Logins { - if l.Name == name { - name += "_" + u.UserName - break - } - } - } - - err = addLogin(Login{ - Name: name, - URL: giteaURL, - Token: token, - Insecure: insecure, - SSHKey: sshKey, - User: u.UserName, - }) - if err != nil { - log.Fatal(err) - } - - err = saveConfig(yamlConfigPath) - if err != nil { - log.Fatal(err) - } - - return nil -} - -// CmdLogin represents to login a gitea server. -var cmdLoginList = cli.Command{ - Name: "ls", - Usage: "List Gitea logins", - Description: `List Gitea logins`, - Action: runLoginList, - Flags: []cli.Flag{&OutputFlag}, -} - -func runLoginList(ctx *cli.Context) error { - err := loadConfig(yamlConfigPath) - if err != nil { - log.Fatal("Unable to load config file " + yamlConfigPath) - } - - headers := []string{ - "Name", - "URL", - "SSHHost", - "User", - "Default", - } - - var values [][]string - - for _, l := range config.Logins { - values = append(values, []string{ - l.Name, - l.URL, - l.GetSSHHost(), - l.User, - fmt.Sprint(l.Default), - }) - } - - Output(outputValue, headers, values) - - return nil -} diff --git a/cmd/login/add.go b/cmd/login/add.go new file mode 100644 index 0000000..2b1cc17 --- /dev/null +++ b/cmd/login/add.go @@ -0,0 +1,83 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package login + +import ( + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/interact" + + "github.com/urfave/cli/v2" +) + +// CmdLoginAdd represents to login a gitea server. +var CmdLoginAdd = cli.Command{ + Name: "add", + Usage: "Add a Gitea login", + Description: `Add a Gitea login`, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "Login name", + }, + &cli.StringFlag{ + Name: "url", + Aliases: []string{"u"}, + Value: "https://try.gitea.io", + EnvVars: []string{"GITEA_SERVER_URL"}, + Usage: "Server URL", + Required: true, + }, + &cli.StringFlag{ + Name: "token", + Aliases: []string{"t"}, + Value: "", + EnvVars: []string{"GITEA_SERVER_TOKEN"}, + Usage: "Access token. Can be obtained from Settings > Applications", + }, + &cli.StringFlag{ + Name: "user", + Value: "", + EnvVars: []string{"GITEA_SERVER_USER"}, + Usage: "User for basic auth (will create token)", + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"pwd"}, + Value: "", + EnvVars: []string{"GITEA_SERVER_PASSWORD"}, + Usage: "Password for basic auth (will create token)", + }, + &cli.StringFlag{ + Name: "ssh-key", + Aliases: []string{"s"}, + Usage: "Path to a SSH key to use for pull/push operations", + }, + &cli.BoolFlag{ + Name: "insecure", + Aliases: []string{"i"}, + Usage: "Disable TLS verification", + }, + }, + Action: runLoginAdd, +} + +func runLoginAdd(ctx *cli.Context) error { + // TODO: if no args -> interactive + return config.AddLogin( + ctx.String("name"), + ctx.String("token"), + ctx.String("user"), + ctx.String("password"), + ctx.String("ssh-key"), + ctx.String("url"), + ctx.Bool("insecure")) +} + +// RunLoginAddInteractive create login interactive +// TODO: should be unexported +func RunLoginAddInteractive(_ *cli.Context) error { + return interact.CreateLogin() +} diff --git a/cmd/login/default.go b/cmd/login/default.go new file mode 100644 index 0000000..7e8e60f --- /dev/null +++ b/cmd/login/default.go @@ -0,0 +1,52 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package login + +import ( + "fmt" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "github.com/urfave/cli/v2" +) + +// CmdLoginSetDefault represents to login a gitea server. +var CmdLoginSetDefault = cli.Command{ + Name: "default", + Usage: "Get or Set Default Login", + Description: `Get or Set Default Login`, + ArgsUsage: "", + Action: runLoginSetDefault, + Flags: []cli.Flag{&flags.OutputFlag}, +} + +func runLoginSetDefault(ctx *cli.Context) error { + if err := config.LoadConfig(); err != nil { + return err + } + if ctx.Args().Len() == 0 { + l, err := config.GetDefaultLogin() + if err != nil { + return err + } + fmt.Printf("Default Login: %s\n", l.Name) + return nil + } + loginExist := false + for i := range config.Config.Logins { + config.Config.Logins[i].Default = false + if config.Config.Logins[i].Name == ctx.Args().First() { + config.Config.Logins[i].Default = true + loginExist = true + } + } + + if !loginExist { + return fmt.Errorf("login '%s' not found", ctx.Args().First()) + } + + return config.SaveConfig() +} diff --git a/cmd/login/edit.go b/cmd/login/edit.go new file mode 100644 index 0000000..c8ffd1e --- /dev/null +++ b/cmd/login/edit.go @@ -0,0 +1,26 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package login + +import ( + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "github.com/skratchdot/open-golang/open" + "github.com/urfave/cli/v2" +) + +// CmdLoginEdit represents to login a gitea server. +var CmdLoginEdit = cli.Command{ + Name: "edit", + Usage: "Edit Gitea logins", + Description: `Edit Gitea logins`, + Action: runLoginEdit, + Flags: []cli.Flag{&flags.OutputFlag}, +} + +func runLoginEdit(ctx *cli.Context) error { + return open.Start(config.GetConfigPath()) +} diff --git a/cmd/login/list.go b/cmd/login/list.go new file mode 100644 index 0000000..7402677 --- /dev/null +++ b/cmd/login/list.go @@ -0,0 +1,56 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package login + +import ( + "fmt" + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "github.com/urfave/cli/v2" +) + +// CmdLoginList represents to login a gitea server. +var CmdLoginList = cli.Command{ + Name: "ls", + Usage: "List Gitea logins", + Description: `List Gitea logins`, + Action: runLoginList, + Flags: []cli.Flag{&flags.OutputFlag}, +} + +func runLoginList(ctx *cli.Context) error { + err := config.LoadConfig() + if err != nil { + log.Fatal(err) + } + + headers := []string{ + "Name", + "URL", + "SSHHost", + "User", + "Default", + } + + var values [][]string + + for _, l := range config.Config.Logins { + values = append(values, []string{ + l.Name, + l.URL, + l.GetSSHHost(), + l.User, + fmt.Sprint(l.Default), + }) + } + + print.OutputList(flags.GlobalOutputValue, headers, values) + + return nil +} diff --git a/cmd/logout.go b/cmd/logout.go index 192d519..23e5afe 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -9,6 +9,8 @@ import ( "log" "os" + "code.gitea.io/tea/modules/config" + "github.com/urfave/cli/v2" ) @@ -37,23 +39,23 @@ func runLogout(ctx *cli.Context) error { return errors.New("Please specify a login name") } - err := loadConfig(yamlConfigPath) + err := config.LoadConfig() if err != nil { - log.Fatal("Unable to load config file " + yamlConfigPath) + log.Fatal(err) } var idx = -1 - for i, l := range config.Logins { + for i, l := range config.Config.Logins { if l.Name == name { idx = i break } } if idx > -1 { - config.Logins = append(config.Logins[:idx], config.Logins[idx+1:]...) - err = saveConfig(yamlConfigPath) + config.Config.Logins = append(config.Config.Logins[:idx], config.Config.Logins[idx+1:]...) + err = config.SaveConfig() if err != nil { - log.Fatal("Unable to save config file " + yamlConfigPath) + log.Fatal(err) } } diff --git a/cmd/milestones.go b/cmd/milestones.go index 311cf11..5a14864 100644 --- a/cmd/milestones.go +++ b/cmd/milestones.go @@ -5,10 +5,11 @@ package cmd import ( - "fmt" - "log" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/cmd/milestones" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" - "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" ) @@ -21,42 +22,25 @@ var CmdMilestones = cli.Command{ ArgsUsage: "[]", Action: runMilestones, Subcommands: []*cli.Command{ - &CmdMilestonesList, - &CmdMilestonesCreate, - &CmdMilestonesClose, - &CmdMilestonesDelete, - &CmdMilestonesReopen, - &CmdMilestonesIssues, + &milestones.CmdMilestonesList, + &milestones.CmdMilestonesCreate, + &milestones.CmdMilestonesClose, + &milestones.CmdMilestonesDelete, + &milestones.CmdMilestonesReopen, + &milestones.CmdMilestonesIssues, }, - Flags: AllDefaultFlags, -} - -// CmdMilestonesList represents a sub command of milestones to list milestones -var CmdMilestonesList = cli.Command{ - Name: "ls", - Usage: "List milestones of the repository", - Description: `List milestones of the repository`, - Action: runMilestonesList, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "state", - Usage: "Filter by milestone state (all|open|closed)", - DefaultText: "open", - }, - &PaginationPageFlag, - &PaginationLimitFlag, - }, AllDefaultFlags...), + Flags: flags.AllDefaultFlags, } func runMilestones(ctx *cli.Context) error { if ctx.Args().Len() == 1 { - return runMilestoneDetail(ctx, ctx.Args().First()) + return runMilestoneDetail(ctx.Args().First()) } - return runMilestonesList(ctx) + return milestones.RunMilestonesList(ctx) } -func runMilestoneDetail(ctx *cli.Context, name string) error { - login, owner, repo := initCommand() +func runMilestoneDetail(name string) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() milestone, _, err := client.GetMilestoneByName(owner, repo, name) @@ -64,192 +48,6 @@ func runMilestoneDetail(ctx *cli.Context, name string) error { return err } - fmt.Printf("%s\n", - milestone.Title, - ) - if len(milestone.Description) != 0 { - fmt.Printf("\n%s\n", milestone.Description) - } - if milestone.Deadline != nil && !milestone.Deadline.IsZero() { - fmt.Printf("\nDeadline: %s\n", milestone.Deadline.Format("2006-01-02 15:04:05")) - } + print.MilestoneDetails(milestone) return nil } - -func runMilestonesList(ctx *cli.Context) error { - login, owner, repo := initCommand() - - state := gitea.StateOpen - switch ctx.String("state") { - case "all": - state = gitea.StateAll - case "closed": - state = gitea.StateClosed - } - - milestones, _, err := login.Client().ListRepoMilestones(owner, repo, gitea.ListMilestoneOption{ - ListOptions: getListOptions(ctx), - State: state, - }) - - if err != nil { - log.Fatal(err) - } - - headers := []string{ - "Title", - } - if state == gitea.StateAll { - headers = append(headers, "State") - } - headers = append(headers, - "Open/Closed Issues", - "DueDate", - ) - - var values [][]string - - for _, m := range milestones { - var deadline = "" - - if m.Deadline != nil && !m.Deadline.IsZero() { - deadline = m.Deadline.Format("2006-01-02 15:04:05") - } - - item := []string{ - m.Title, - } - if state == gitea.StateAll { - item = append(item, string(m.State)) - } - item = append(item, - fmt.Sprintf("%d/%d", m.OpenIssues, m.ClosedIssues), - deadline, - ) - - values = append(values, item) - } - Output(outputValue, headers, values) - - return nil -} - -// CmdMilestonesCreate represents a sub command of milestones to create milestone -var CmdMilestonesCreate = cli.Command{ - Name: "create", - Usage: "Create an milestone on repository", - Description: `Create an milestone on repository`, - Action: runMilestonesCreate, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "title", - Aliases: []string{"t"}, - Usage: "milestone title to create", - }, - &cli.StringFlag{ - Name: "description", - Aliases: []string{"d"}, - Usage: "milestone description to create", - }, - &cli.StringFlag{ - Name: "state", - Usage: "set milestone state (default is open)", - DefaultText: "open", - }, - }, AllDefaultFlags...), -} - -func runMilestonesCreate(ctx *cli.Context) error { - login, owner, repo := initCommand() - - title := ctx.String("title") - if len(title) == 0 { - fmt.Printf("Title is required\n") - return nil - } - - state := gitea.StateOpen - if ctx.String("state") == "closed" { - state = gitea.StateClosed - } - - mile, _, err := login.Client().CreateMilestone(owner, repo, gitea.CreateMilestoneOption{ - Title: title, - Description: ctx.String("description"), - State: state, - }) - if err != nil { - log.Fatal(err) - } - - return runMilestoneDetail(ctx, mile.Title) -} - -// CmdMilestonesClose represents a sub command of milestones to close an milestone -var CmdMilestonesClose = cli.Command{ - Name: "close", - Usage: "Change state of an milestone to 'closed'", - Description: `Change state of an milestone to 'closed'`, - ArgsUsage: "", - Action: func(ctx *cli.Context) error { - if ctx.Bool("force") { - return deleteMilestone(ctx) - } - return editMilestoneStatus(ctx, true) - }, - Flags: append([]cli.Flag{ - &cli.BoolFlag{ - Name: "force", - Aliases: []string{"f"}, - Usage: "delete milestone", - }, - }, AllDefaultFlags...), -} - -func editMilestoneStatus(ctx *cli.Context, close bool) error { - login, owner, repo := initCommand() - client := login.Client() - - state := gitea.StateOpen - if close { - state = gitea.StateClosed - } - _, _, err := client.EditMilestoneByName(owner, repo, ctx.Args().First(), gitea.EditMilestoneOption{ - State: &state, - Title: ctx.Args().First(), - }) - - return err -} - -// CmdMilestonesDelete represents a sub command of milestones to delete an milestone -var CmdMilestonesDelete = cli.Command{ - Name: "delete", - Aliases: []string{"rm"}, - Usage: "delete a milestone", - Description: "delete a milestone", - ArgsUsage: "", - Action: deleteMilestone, - Flags: AllDefaultFlags, -} - -func deleteMilestone(ctx *cli.Context) error { - login, owner, repo := initCommand() - client := login.Client() - - _, err := client.DeleteMilestoneByName(owner, repo, ctx.Args().First()) - return err -} - -// CmdMilestonesReopen represents a sub command of milestones to open an milestone -var CmdMilestonesReopen = cli.Command{ - Name: "reopen", - Aliases: []string{"open"}, - Usage: "Change state of an milestone to 'open'", - Description: `Change state of an milestone to 'open'`, - ArgsUsage: "", - Action: func(ctx *cli.Context) error { - return editMilestoneStatus(ctx, false) - }, - Flags: AllDefaultFlags, -} diff --git a/cmd/milestones/close.go b/cmd/milestones/close.go new file mode 100644 index 0000000..1ce65ef --- /dev/null +++ b/cmd/milestones/close.go @@ -0,0 +1,32 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package milestones + +import ( + "code.gitea.io/tea/cmd/flags" + + "github.com/urfave/cli/v2" +) + +// CmdMilestonesClose represents a sub command of milestones to close an milestone +var CmdMilestonesClose = cli.Command{ + Name: "close", + Usage: "Change state of an milestone to 'closed'", + Description: `Change state of an milestone to 'closed'`, + ArgsUsage: "", + Action: func(ctx *cli.Context) error { + if ctx.Bool("force") { + return deleteMilestone(ctx) + } + return editMilestoneStatus(ctx, true) + }, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "delete milestone", + }, + }, flags.AllDefaultFlags...), +} diff --git a/cmd/milestones/create.go b/cmd/milestones/create.go new file mode 100644 index 0000000..637c666 --- /dev/null +++ b/cmd/milestones/create.go @@ -0,0 +1,69 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package milestones + +import ( + "fmt" + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdMilestonesCreate represents a sub command of milestones to create milestone +var CmdMilestonesCreate = cli.Command{ + Name: "create", + Usage: "Create an milestone on repository", + Description: `Create an milestone on repository`, + Action: runMilestonesCreate, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "title", + Aliases: []string{"t"}, + Usage: "milestone title to create", + }, + &cli.StringFlag{ + Name: "description", + Aliases: []string{"d"}, + Usage: "milestone description to create", + }, + &cli.StringFlag{ + Name: "state", + Usage: "set milestone state (default is open)", + DefaultText: "open", + }, + }, flags.AllDefaultFlags...), +} + +func runMilestonesCreate(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + title := ctx.String("title") + if len(title) == 0 { + fmt.Printf("Title is required\n") + return nil + } + + state := gitea.StateOpen + if ctx.String("state") == "closed" { + state = gitea.StateClosed + } + + mile, _, err := login.Client().CreateMilestone(owner, repo, gitea.CreateMilestoneOption{ + Title: title, + Description: ctx.String("description"), + State: state, + }) + if err != nil { + log.Fatal(err) + } + + print.MilestoneDetails(mile) + return nil +} diff --git a/cmd/milestones/delete.go b/cmd/milestones/delete.go new file mode 100644 index 0000000..639272c --- /dev/null +++ b/cmd/milestones/delete.go @@ -0,0 +1,31 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package milestones + +import ( + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "github.com/urfave/cli/v2" +) + +// CmdMilestonesDelete represents a sub command of milestones to delete an milestone +var CmdMilestonesDelete = cli.Command{ + Name: "delete", + Aliases: []string{"rm"}, + Usage: "delete a milestone", + Description: "delete a milestone", + ArgsUsage: "", + Action: deleteMilestone, + Flags: flags.AllDefaultFlags, +} + +func deleteMilestone(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + client := login.Client() + + _, err := client.DeleteMilestoneByName(owner, repo, ctx.Args().First()) + return err +} diff --git a/cmd/milestone_issues.go b/cmd/milestones/issues.go similarity index 82% rename from cmd/milestone_issues.go rename to cmd/milestones/issues.go index 0043356..2a01372 100644 --- a/cmd/milestone_issues.go +++ b/cmd/milestones/issues.go @@ -2,12 +2,17 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package cmd +package milestones import ( "fmt" "strconv" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + "code.gitea.io/tea/modules/utils" + "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" ) @@ -34,9 +39,9 @@ var CmdMilestonesIssues = cli.Command{ Name: "kind", Usage: "Filter by kind (issue|pull)", }, - &PaginationPageFlag, - &PaginationLimitFlag, - }, AllDefaultFlags...), + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.AllDefaultFlags...), } // CmdMilestoneAddIssue represents a sub command of milestone issues to add an issue/pull to an milestone @@ -47,7 +52,7 @@ var CmdMilestoneAddIssue = cli.Command{ Description: "Add an issue/pull to an milestone", ArgsUsage: " ", Action: runMilestoneIssueAdd, - Flags: AllDefaultFlags, + Flags: flags.AllDefaultFlags, } // CmdMilestoneRemoveIssue represents a sub command of milestones to remove an issue/pull from an milestone @@ -58,11 +63,11 @@ var CmdMilestoneRemoveIssue = cli.Command{ Description: "Remove an issue/pull to an milestone", ArgsUsage: " ", Action: runMilestoneIssueRemove, - Flags: AllDefaultFlags, + Flags: flags.AllDefaultFlags, } func runMilestoneIssueList(ctx *cli.Context) error { - login, owner, repo := initCommand() + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() state := gitea.StateOpen @@ -91,7 +96,7 @@ func runMilestoneIssueList(ctx *cli.Context) error { } issues, _, err := client.ListRepoIssues(owner, repo, gitea.ListIssueOption{ - ListOptions: getListOptions(ctx), + ListOptions: flags.GetListOptions(ctx), Milestones: []string{milestone}, Type: kind, State: state, @@ -112,7 +117,7 @@ func runMilestoneIssueList(ctx *cli.Context) error { var values [][]string if len(issues) == 0 { - Output(outputValue, headers, values) + print.OutputList(flags.GlobalOutputValue, headers, values) return nil } @@ -137,12 +142,12 @@ func runMilestoneIssueList(ctx *cli.Context) error { }, ) } - Output(outputValue, headers, values) + print.OutputList(flags.GlobalOutputValue, headers, values) return nil } func runMilestoneIssueAdd(ctx *cli.Context) error { - login, owner, repo := initCommand() + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() if ctx.Args().Len() == 0 { return fmt.Errorf("need two arguments") @@ -150,7 +155,7 @@ func runMilestoneIssueAdd(ctx *cli.Context) error { mileName := ctx.Args().Get(0) issueIndex := ctx.Args().Get(1) - idx, err := argToIndex(issueIndex) + idx, err := utils.ArgToIndex(issueIndex) if err != nil { return err } @@ -168,7 +173,7 @@ func runMilestoneIssueAdd(ctx *cli.Context) error { } func runMilestoneIssueRemove(ctx *cli.Context) error { - login, owner, repo := initCommand() + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() if ctx.Args().Len() == 0 { return fmt.Errorf("need two arguments") @@ -176,7 +181,7 @@ func runMilestoneIssueRemove(ctx *cli.Context) error { mileName := ctx.Args().Get(0) issueIndex := ctx.Args().Get(1) - idx, err := argToIndex(issueIndex) + idx, err := utils.ArgToIndex(issueIndex) if err != nil { return err } diff --git a/cmd/milestones/list.go b/cmd/milestones/list.go new file mode 100644 index 0000000..d3fe60e --- /dev/null +++ b/cmd/milestones/list.go @@ -0,0 +1,93 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package milestones + +import ( + "fmt" + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdMilestonesList represents a sub command of milestones to list milestones +var CmdMilestonesList = cli.Command{ + Name: "ls", + Usage: "List milestones of the repository", + Description: `List milestones of the repository`, + Action: RunMilestonesList, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "state", + Usage: "Filter by milestone state (all|open|closed)", + DefaultText: "open", + }, + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.AllDefaultFlags...), +} + +// RunMilestonesList list milestones +func RunMilestonesList(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + state := gitea.StateOpen + switch ctx.String("state") { + case "all": + state = gitea.StateAll + case "closed": + state = gitea.StateClosed + } + + milestones, _, err := login.Client().ListRepoMilestones(owner, repo, gitea.ListMilestoneOption{ + ListOptions: flags.GetListOptions(ctx), + State: state, + }) + + if err != nil { + log.Fatal(err) + } + + headers := []string{ + "Title", + } + if state == gitea.StateAll { + headers = append(headers, "State") + } + headers = append(headers, + "Open/Closed Issues", + "DueDate", + ) + + var values [][]string + + for _, m := range milestones { + var deadline = "" + + if m.Deadline != nil && !m.Deadline.IsZero() { + deadline = m.Deadline.Format("2006-01-02 15:04:05") + } + + item := []string{ + m.Title, + } + if state == gitea.StateAll { + item = append(item, string(m.State)) + } + item = append(item, + fmt.Sprintf("%d/%d", m.OpenIssues, m.ClosedIssues), + deadline, + ) + + values = append(values, item) + } + print.OutputList(flags.GlobalOutputValue, headers, values) + + return nil +} diff --git a/cmd/milestones/reopen.go b/cmd/milestones/reopen.go new file mode 100644 index 0000000..d984f18 --- /dev/null +++ b/cmd/milestones/reopen.go @@ -0,0 +1,42 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package milestones + +import ( + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdMilestonesReopen represents a sub command of milestones to open an milestone +var CmdMilestonesReopen = cli.Command{ + Name: "reopen", + Aliases: []string{"open"}, + Usage: "Change state of an milestone to 'open'", + Description: `Change state of an milestone to 'open'`, + ArgsUsage: "", + Action: func(ctx *cli.Context) error { + return editMilestoneStatus(ctx, false) + }, + Flags: flags.AllDefaultFlags, +} + +func editMilestoneStatus(ctx *cli.Context, close bool) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + client := login.Client() + + state := gitea.StateOpen + if close { + state = gitea.StateClosed + } + _, _, err := client.EditMilestoneByName(owner, repo, ctx.Args().First(), gitea.EditMilestoneOption{ + State: &state, + Title: ctx.Args().First(), + }) + + return err +} diff --git a/cmd/notifications.go b/cmd/notifications.go index a393ae3..2aefb91 100644 --- a/cmd/notifications.go +++ b/cmd/notifications.go @@ -8,6 +8,10 @@ import ( "log" "strings" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" ) @@ -34,16 +38,16 @@ var CmdNotifications = cli.Command{ Aliases: []string{"pd"}, Usage: "show pinned notifications instead unread", }, - &PaginationPageFlag, - &PaginationLimitFlag, - }, AllDefaultFlags...), + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.AllDefaultFlags...), } func runNotifications(ctx *cli.Context) error { var news []*gitea.NotificationThread var err error - listOpts := getListOptions(ctx) + listOpts := flags.GetListOptions(ctx) if listOpts.Page == 0 { listOpts.Page = 1 } @@ -57,13 +61,13 @@ func runNotifications(ctx *cli.Context) error { } if ctx.Bool("all") { - login := initCommandLoginOnly() + login := config.InitCommandLoginOnly(flags.GlobalLoginValue) news, _, err = login.Client().ListNotifications(gitea.ListNotificationOptions{ ListOptions: listOpts, Status: status, }) } else { - login, owner, repo := initCommand() + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) news, _, err = login.Client().ListRepoNotifications(owner, repo, gitea.ListNotificationOptions{ ListOptions: listOpts, Status: status, @@ -107,7 +111,7 @@ func runNotifications(ctx *cli.Context) error { } if len(values) != 0 { - Output(outputValue, headers, values) + print.OutputList(flags.GlobalOutputValue, headers, values) } return nil } diff --git a/cmd/open.go b/cmd/open.go index fbe67b4..85811ed 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -9,6 +9,8 @@ import ( "path" "strings" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" local_git "code.gitea.io/tea/modules/git" "github.com/skratchdot/open-golang/open" @@ -21,11 +23,11 @@ var CmdOpen = cli.Command{ Usage: "Open something of the repository on web browser", Description: `Open something of the repository on web browser`, Action: runOpen, - Flags: append([]cli.Flag{}, LoginRepoFlags...), + Flags: append([]cli.Flag{}, flags.LoginRepoFlags...), } func runOpen(ctx *cli.Context) error { - login, owner, repo := initCommand() + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) var suffix string number := ctx.Args().Get(0) diff --git a/cmd/pulls.go b/cmd/pulls.go index cb053d8..7cd8079 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -6,16 +6,12 @@ package cmd import ( "fmt" - "log" - "strconv" - "strings" - local_git "code.gitea.io/tea/modules/git" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/cmd/pulls" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/utils" - "code.gitea.io/sdk/gitea" - "github.com/charmbracelet/glamour" - "github.com/go-git/go-git/v5" - git_config "github.com/go-git/go-git/v5/config" "github.com/urfave/cli/v2" ) @@ -27,35 +23,26 @@ var CmdPulls = cli.Command{ Description: `List, create, checkout and clean pull requests`, ArgsUsage: "[]", Action: runPulls, - Flags: IssuePRFlags, + Flags: flags.IssuePRFlags, Subcommands: []*cli.Command{ - &CmdPullsList, - &CmdPullsCheckout, - &CmdPullsClean, - &CmdPullsCreate, + &pulls.CmdPullsList, + &pulls.CmdPullsCheckout, + &pulls.CmdPullsClean, + &pulls.CmdPullsCreate, }, } func runPulls(ctx *cli.Context) error { if ctx.Args().Len() == 1 { - return runPullDetail(ctx, ctx.Args().First()) + return runPullDetail(ctx.Args().First()) } - return runPullsList(ctx) + return pulls.RunPullsList(ctx) } -// CmdPullsList represents a sub command of issues to list pulls -var CmdPullsList = cli.Command{ - Name: "ls", - Usage: "List pull requests of the repository", - Description: `List pull requests of the repository`, - Action: runPullsList, - Flags: IssuePRFlags, -} +func runPullDetail(index string) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) -func runPullDetail(ctx *cli.Context, index string) error { - login, owner, repo := initCommand() - - idx, err := argToIndex(index) + idx, err := utils.ArgToIndex(index) if err != nil { return err } @@ -73,375 +60,3 @@ func runPullDetail(ctx *cli.Context, index string) error { ) return nil } - -func runPullsList(ctx *cli.Context) error { - login, owner, repo := initCommand() - - state := gitea.StateOpen - switch ctx.String("state") { - case "all": - state = gitea.StateAll - case "open": - state = gitea.StateOpen - case "closed": - state = gitea.StateClosed - } - - prs, _, err := login.Client().ListRepoPullRequests(owner, repo, gitea.ListPullRequestsOptions{ - State: state, - }) - - if err != nil { - log.Fatal(err) - } - - headers := []string{ - "Index", - "Title", - "State", - "Author", - "Milestone", - "Updated", - } - - var values [][]string - - if len(prs) == 0 { - Output(outputValue, headers, values) - return nil - } - - for _, pr := range prs { - if pr == nil { - continue - } - author := pr.Poster.FullName - if len(author) == 0 { - author = pr.Poster.UserName - } - mile := "" - if pr.Milestone != nil { - mile = pr.Milestone.Title - } - values = append( - values, - []string{ - strconv.FormatInt(pr.Index, 10), - pr.Title, - string(pr.State), - author, - mile, - pr.Updated.Format("2006-01-02 15:04:05"), - }, - ) - } - Output(outputValue, headers, values) - - return nil -} - -// CmdPullsCheckout is a command to locally checkout the given PR -var CmdPullsCheckout = cli.Command{ - Name: "checkout", - Usage: "Locally check out the given PR", - Description: `Locally check out the given PR`, - Action: runPullsCheckout, - ArgsUsage: "", - Flags: AllDefaultFlags, -} - -func runPullsCheckout(ctx *cli.Context) error { - login, owner, repo := initCommand() - if ctx.Args().Len() != 1 { - log.Fatal("Must specify a PR index") - } - - // fetch PR source-repo & -branch from gitea - idx, err := argToIndex(ctx.Args().First()) - if err != nil { - return err - } - pr, _, err := login.Client().GetPullRequest(owner, repo, idx) - if err != nil { - return err - } - remoteURL := pr.Head.Repository.CloneURL - remoteBranchName := pr.Head.Ref - - // open local git repo - localRepo, err := local_git.RepoForWorkdir() - if err != nil { - return nil - } - - // verify related remote is in local repo, otherwise add it - newRemoteName := fmt.Sprintf("pulls/%v", pr.Head.Repository.Owner.UserName) - localRemote, err := localRepo.GetOrCreateRemote(remoteURL, newRemoteName) - if err != nil { - return err - } - - localRemoteName := localRemote.Config().Name - localBranchName := fmt.Sprintf("pulls/%v-%v", idx, remoteBranchName) - - // fetch remote - fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", - idx, remoteURL, remoteBranchName, localRemoteName) - - url, err := local_git.ParseURL(localRemote.Config().URLs[0]) - if err != nil { - return err - } - auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey) - if err != nil { - return err - } - - err = localRemote.Fetch(&git.FetchOptions{Auth: auth}) - if err == git.NoErrAlreadyUpToDate { - fmt.Println(err) - } else if err != nil { - return err - } - - // checkout local branch - fmt.Printf("Creating branch '%s'\n", localBranchName) - err = localRepo.TeaCreateBranch(localBranchName, remoteBranchName, localRemoteName) - if err == git.ErrBranchExists { - fmt.Println(err) - } else if err != nil { - return err - } - - fmt.Printf("Checking out PR %v\n", idx) - err = localRepo.TeaCheckout(localBranchName) - - return err -} - -// CmdPullsClean removes the remote and local feature branches, if a PR is merged. -var CmdPullsClean = cli.Command{ - Name: "clean", - Usage: "Deletes local & remote feature-branches for a closed pull request", - Description: `Deletes local & remote feature-branches for a closed pull request`, - ArgsUsage: "", - Action: runPullsClean, - Flags: append([]cli.Flag{ - &cli.BoolFlag{ - Name: "ignore-sha", - Usage: "Find the local branch by name instead of commit hash (less precise)", - }, - }, AllDefaultFlags...), -} - -func runPullsClean(ctx *cli.Context) error { - login, owner, repo := initCommand() - if ctx.Args().Len() != 1 { - return fmt.Errorf("Must specify a PR index") - } - - // fetch PR source-repo & -branch from gitea - idx, err := argToIndex(ctx.Args().First()) - if err != nil { - return err - } - pr, _, err := login.Client().GetPullRequest(owner, repo, idx) - if err != nil { - return err - } - if pr.State == gitea.StateOpen { - return fmt.Errorf("PR is still open, won't delete branches") - } - - // IDEA: abort if PR.Head.Repository.CloneURL does not match login.URL? - - r, err := local_git.RepoForWorkdir() - if err != nil { - return err - } - - // find a branch with matching sha or name, that has a remote matching the repo url - var branch *git_config.Branch - if ctx.Bool("ignore-sha") { - branch, err = r.TeaFindBranchByName(pr.Head.Ref, pr.Head.Repository.CloneURL) - } else { - branch, err = r.TeaFindBranchBySha(pr.Head.Sha, pr.Head.Repository.CloneURL) - } - if err != nil { - return err - } - if branch == nil { - if ctx.Bool("ignore-sha") { - return fmt.Errorf("Remote branch %s not found in local repo", pr.Head.Ref) - } - return fmt.Errorf(`Remote branch %s not found in local repo. -Either you don't track this PR, or the local branch has diverged from the remote. -If you still want to continue & are sure you don't loose any important commits, -call me again with the --ignore-sha flag`, pr.Head.Ref) - } - - // prepare deletion of local branch: - headRef, err := r.Head() - if err != nil { - return err - } - if headRef.Name().Short() == branch.Name { - fmt.Printf("Checking out 'master' to delete local branch '%s'\n", branch.Name) - err = r.TeaCheckout("master") - if err != nil { - return err - } - } - - // remove local & remote branch - fmt.Printf("Deleting local branch %s and remote branch %s\n", branch.Name, pr.Head.Ref) - url, err := r.TeaRemoteURL(branch.Remote) - if err != nil { - return err - } - auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey) - if err != nil { - return err - } - return r.TeaDeleteBranch(branch, pr.Head.Ref, auth) -} - -// CmdPullsCreate creates a pull request -var CmdPullsCreate = cli.Command{ - Name: "create", - Usage: "Create a pull-request", - Description: "Create a pull-request", - Action: runPullsCreate, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "head", - Usage: "Set head branch (default is current one)", - }, - &cli.StringFlag{ - Name: "base", - Aliases: []string{"b"}, - Usage: "Set base branch (default is default branch)", - }, - &cli.StringFlag{ - Name: "title", - Aliases: []string{"t"}, - Usage: "Set title of pull (default is head branch name)", - }, - &cli.StringFlag{ - Name: "description", - Aliases: []string{"d"}, - Usage: "Set body of new pull", - }, - }, AllDefaultFlags...), -} - -func runPullsCreate(ctx *cli.Context) error { - login, ownerArg, repoArg := initCommand() - client := login.Client() - - repo, _, err := client.GetRepo(ownerArg, repoArg) - if err != nil { - log.Fatal("could not fetch repo meta: ", err) - } - - // open local git repo - localRepo, err := local_git.RepoForWorkdir() - if err != nil { - log.Fatal("could not open local repo: ", err) - } - - // push if possible - log.Println("git push") - err = localRepo.Push(&git.PushOptions{}) - if err != nil && err != git.NoErrAlreadyUpToDate { - log.Printf("Error occurred during 'git push':\n%s\n", err.Error()) - } - - base := ctx.String("base") - // default is default branch - if len(base) == 0 { - base = repo.DefaultBranch - } - - head := ctx.String("head") - // default is current one - if len(head) == 0 { - headBranch, err := localRepo.Head() - if err != nil { - log.Fatal(err) - } - sha := headBranch.Hash().String() - - remote, err := localRepo.TeaFindBranchRemote("", sha) - if err != nil { - log.Fatal("could not determine remote for current branch: ", err) - } - - if remote == nil { - // if no remote branch is found for the local hash, we abort: - // user has probably not configured a remote for the local branch, - // or local branch does not represent remote state. - log.Fatal("no matching remote found for this branch. try git push -u ") - } - - branchName, err := localRepo.TeaGetCurrentBranchName() - if err != nil { - log.Fatal(err) - } - - url, err := local_git.ParseURL(remote.Config().URLs[0]) - if err != nil { - log.Fatal(err) - } - owner, _ := getOwnerAndRepo(strings.TrimLeft(url.Path, "/"), "") - head = fmt.Sprintf("%s:%s", owner, branchName) - } - - title := ctx.String("title") - // default is head branch name - if len(title) == 0 { - title = head - if strings.Contains(title, ":") { - title = strings.SplitN(title, ":", 2)[1] - } - title = strings.Replace(title, "-", " ", -1) - title = strings.Replace(title, "_", " ", -1) - title = strings.Title(strings.ToLower(title)) - } - // title is required - if len(title) == 0 { - fmt.Printf("Title is required") - return nil - } - - pr, _, err := client.CreatePullRequest(ownerArg, repoArg, gitea.CreatePullRequestOption{ - Head: head, - Base: base, - Title: title, - Body: ctx.String("description"), - }) - - if err != nil { - log.Fatalf("could not create PR from %s to %s:%s: %s", head, ownerArg, base, err) - } - - in := fmt.Sprintf("# #%d %s (%s)\n%s created %s\n\n%s\n", pr.Index, - pr.Title, - pr.State, - pr.Poster.UserName, - pr.Created.Format("2006-01-02 15:04:05"), - pr.Body, - ) - out, err := glamour.Render(in, getGlamourTheme()) - fmt.Print(out) - - fmt.Println(pr.HTMLURL) - return err -} - -func argToIndex(arg string) (int64, error) { - if strings.HasPrefix(arg, "#") { - arg = arg[1:] - } - return strconv.ParseInt(arg, 10, 64) -} diff --git a/cmd/pulls/checkout.go b/cmd/pulls/checkout.go new file mode 100644 index 0000000..389df33 --- /dev/null +++ b/cmd/pulls/checkout.go @@ -0,0 +1,97 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package pulls + +import ( + "fmt" + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + local_git "code.gitea.io/tea/modules/git" + "code.gitea.io/tea/modules/utils" + + "github.com/go-git/go-git/v5" + "github.com/urfave/cli/v2" +) + +// CmdPullsCheckout is a command to locally checkout the given PR +var CmdPullsCheckout = cli.Command{ + Name: "checkout", + Usage: "Locally check out the given PR", + Description: `Locally check out the given PR`, + Action: runPullsCheckout, + ArgsUsage: "", + Flags: flags.AllDefaultFlags, +} + +func runPullsCheckout(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + if ctx.Args().Len() != 1 { + log.Fatal("Must specify a PR index") + } + + // fetch PR source-repo & -branch from gitea + idx, err := utils.ArgToIndex(ctx.Args().First()) + if err != nil { + return err + } + pr, _, err := login.Client().GetPullRequest(owner, repo, idx) + if err != nil { + return err + } + remoteURL := pr.Head.Repository.CloneURL + remoteBranchName := pr.Head.Ref + + // open local git repo + localRepo, err := local_git.RepoForWorkdir() + if err != nil { + return nil + } + + // verify related remote is in local repo, otherwise add it + newRemoteName := fmt.Sprintf("pulls/%v", pr.Head.Repository.Owner.UserName) + localRemote, err := localRepo.GetOrCreateRemote(remoteURL, newRemoteName) + if err != nil { + return err + } + + localRemoteName := localRemote.Config().Name + localBranchName := fmt.Sprintf("pulls/%v-%v", idx, remoteBranchName) + + // fetch remote + fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", + idx, remoteURL, remoteBranchName, localRemoteName) + + url, err := local_git.ParseURL(localRemote.Config().URLs[0]) + if err != nil { + return err + } + auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey) + if err != nil { + return err + } + + err = localRemote.Fetch(&git.FetchOptions{Auth: auth}) + if err == git.NoErrAlreadyUpToDate { + fmt.Println(err) + } else if err != nil { + return err + } + + // checkout local branch + fmt.Printf("Creating branch '%s'\n", localBranchName) + err = localRepo.TeaCreateBranch(localBranchName, remoteBranchName, localRemoteName) + if err == git.ErrBranchExists { + fmt.Println(err) + } else if err != nil { + return err + } + + fmt.Printf("Checking out PR %v\n", idx) + err = localRepo.TeaCheckout(localBranchName) + + return err +} diff --git a/cmd/pulls/clean.go b/cmd/pulls/clean.go new file mode 100644 index 0000000..98c9f4b --- /dev/null +++ b/cmd/pulls/clean.go @@ -0,0 +1,105 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package pulls + +import ( + "fmt" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + local_git "code.gitea.io/tea/modules/git" + "code.gitea.io/tea/modules/utils" + + "code.gitea.io/sdk/gitea" + git_config "github.com/go-git/go-git/v5/config" + "github.com/urfave/cli/v2" +) + +// CmdPullsClean removes the remote and local feature branches, if a PR is merged. +var CmdPullsClean = cli.Command{ + Name: "clean", + Usage: "Deletes local & remote feature-branches for a closed pull request", + Description: `Deletes local & remote feature-branches for a closed pull request`, + ArgsUsage: "", + Action: runPullsClean, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "ignore-sha", + Usage: "Find the local branch by name instead of commit hash (less precise)", + }, + }, flags.AllDefaultFlags...), +} + +func runPullsClean(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + if ctx.Args().Len() != 1 { + return fmt.Errorf("Must specify a PR index") + } + + // fetch PR source-repo & -branch from gitea + idx, err := utils.ArgToIndex(ctx.Args().First()) + if err != nil { + return err + } + pr, _, err := login.Client().GetPullRequest(owner, repo, idx) + if err != nil { + return err + } + if pr.State == gitea.StateOpen { + return fmt.Errorf("PR is still open, won't delete branches") + } + + // IDEA: abort if PR.Head.Repository.CloneURL does not match login.URL? + + r, err := local_git.RepoForWorkdir() + if err != nil { + return err + } + + // find a branch with matching sha or name, that has a remote matching the repo url + var branch *git_config.Branch + if ctx.Bool("ignore-sha") { + branch, err = r.TeaFindBranchByName(pr.Head.Ref, pr.Head.Repository.CloneURL) + } else { + branch, err = r.TeaFindBranchBySha(pr.Head.Sha, pr.Head.Repository.CloneURL) + } + if err != nil { + return err + } + if branch == nil { + if ctx.Bool("ignore-sha") { + return fmt.Errorf("Remote branch %s not found in local repo", pr.Head.Ref) + } + return fmt.Errorf(`Remote branch %s not found in local repo. +Either you don't track this PR, or the local branch has diverged from the remote. +If you still want to continue & are sure you don't loose any important commits, +call me again with the --ignore-sha flag`, pr.Head.Ref) + } + + // prepare deletion of local branch: + headRef, err := r.Head() + if err != nil { + return err + } + if headRef.Name().Short() == branch.Name { + fmt.Printf("Checking out 'master' to delete local branch '%s'\n", branch.Name) + err = r.TeaCheckout("master") + if err != nil { + return err + } + } + + // remove local & remote branch + fmt.Printf("Deleting local branch %s and remote branch %s\n", branch.Name, pr.Head.Ref) + url, err := r.TeaRemoteURL(branch.Remote) + if err != nil { + return err + } + auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey) + if err != nil { + return err + } + return r.TeaDeleteBranch(branch, pr.Head.Ref, auth) +} diff --git a/cmd/pulls/create.go b/cmd/pulls/create.go new file mode 100644 index 0000000..ee6564c --- /dev/null +++ b/cmd/pulls/create.go @@ -0,0 +1,145 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package pulls + +import ( + "fmt" + "log" + "strings" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + local_git "code.gitea.io/tea/modules/git" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/go-git/go-git/v5" + "github.com/urfave/cli/v2" +) + +// CmdPullsCreate creates a pull request +var CmdPullsCreate = cli.Command{ + Name: "create", + Usage: "Create a pull-request", + Description: "Create a pull-request", + Action: runPullsCreate, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "head", + Usage: "Set head branch (default is current one)", + }, + &cli.StringFlag{ + Name: "base", + Aliases: []string{"b"}, + Usage: "Set base branch (default is default branch)", + }, + &cli.StringFlag{ + Name: "title", + Aliases: []string{"t"}, + Usage: "Set title of pull (default is head branch name)", + }, + &cli.StringFlag{ + Name: "description", + Aliases: []string{"d"}, + Usage: "Set body of new pull", + }, + }, flags.AllDefaultFlags...), +} + +func runPullsCreate(ctx *cli.Context) error { + login, ownerArg, repoArg := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + client := login.Client() + + repo, _, err := client.GetRepo(ownerArg, repoArg) + if err != nil { + log.Fatal("could not fetch repo meta: ", err) + } + + // open local git repo + localRepo, err := local_git.RepoForWorkdir() + if err != nil { + log.Fatal("could not open local repo: ", err) + } + + // push if possible + log.Println("git push") + err = localRepo.Push(&git.PushOptions{}) + if err != nil && err != git.NoErrAlreadyUpToDate { + log.Printf("Error occurred during 'git push':\n%s\n", err.Error()) + } + + base := ctx.String("base") + // default is default branch + if len(base) == 0 { + base = repo.DefaultBranch + } + + head := ctx.String("head") + // default is current one + if len(head) == 0 { + headBranch, err := localRepo.Head() + if err != nil { + log.Fatal(err) + } + sha := headBranch.Hash().String() + + remote, err := localRepo.TeaFindBranchRemote("", sha) + if err != nil { + log.Fatal("could not determine remote for current branch: ", err) + } + + if remote == nil { + // if no remote branch is found for the local hash, we abort: + // user has probably not configured a remote for the local branch, + // or local branch does not represent remote state. + log.Fatal("no matching remote found for this branch. try git push -u ") + } + + branchName, err := localRepo.TeaGetCurrentBranchName() + if err != nil { + log.Fatal(err) + } + + url, err := local_git.ParseURL(remote.Config().URLs[0]) + if err != nil { + log.Fatal(err) + } + owner, _ := config.GetOwnerAndRepo(strings.TrimLeft(url.Path, "/"), "") + head = fmt.Sprintf("%s:%s", owner, branchName) + } + + title := ctx.String("title") + // default is head branch name + if len(title) == 0 { + title = head + if strings.Contains(title, ":") { + title = strings.SplitN(title, ":", 2)[1] + } + title = strings.Replace(title, "-", " ", -1) + title = strings.Replace(title, "_", " ", -1) + title = strings.Title(strings.ToLower(title)) + } + // title is required + if len(title) == 0 { + fmt.Printf("Title is required") + return nil + } + + pr, _, err := client.CreatePullRequest(ownerArg, repoArg, gitea.CreatePullRequestOption{ + Head: head, + Base: base, + Title: title, + Body: ctx.String("description"), + }) + + if err != nil { + log.Fatalf("could not create PR from %s to %s:%s: %s", head, ownerArg, base, err) + } + + print.PullDetails(pr) + + fmt.Println(pr.HTMLURL) + return err +} diff --git a/cmd/pulls/list.go b/cmd/pulls/list.go new file mode 100644 index 0000000..833dbe6 --- /dev/null +++ b/cmd/pulls/list.go @@ -0,0 +1,93 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package pulls + +import ( + "log" + "strconv" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdPullsList represents a sub command of issues to list pulls +var CmdPullsList = cli.Command{ + Name: "ls", + Usage: "List pull requests of the repository", + Description: `List pull requests of the repository`, + Action: RunPullsList, + Flags: flags.IssuePRFlags, +} + +// RunPullsList return list of pulls +func RunPullsList(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + state := gitea.StateOpen + switch ctx.String("state") { + case "all": + state = gitea.StateAll + case "open": + state = gitea.StateOpen + case "closed": + state = gitea.StateClosed + } + + prs, _, err := login.Client().ListRepoPullRequests(owner, repo, gitea.ListPullRequestsOptions{ + State: state, + }) + + if err != nil { + log.Fatal(err) + } + + headers := []string{ + "Index", + "Title", + "State", + "Author", + "Milestone", + "Updated", + } + + var values [][]string + + if len(prs) == 0 { + print.OutputList(flags.GlobalOutputValue, headers, values) + return nil + } + + for _, pr := range prs { + if pr == nil { + continue + } + author := pr.Poster.FullName + if len(author) == 0 { + author = pr.Poster.UserName + } + mile := "" + if pr.Milestone != nil { + mile = pr.Milestone.Title + } + values = append( + values, + []string{ + strconv.FormatInt(pr.Index, 10), + pr.Title, + string(pr.State), + author, + mile, + pr.Updated.Format("2006-01-02 15:04:05"), + }, + ) + } + print.OutputList(flags.GlobalOutputValue, headers, values) + + return nil +} diff --git a/cmd/releases.go b/cmd/releases.go index 506664c..7db2a86 100644 --- a/cmd/releases.go +++ b/cmd/releases.go @@ -5,307 +5,25 @@ package cmd import ( - "fmt" - "log" - "net/http" - "os" - "path/filepath" - "strings" - - "code.gitea.io/sdk/gitea" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/cmd/releases" "github.com/urfave/cli/v2" ) // CmdReleases represents to login a gitea server. +// ToDo: ReleaseDetails var CmdReleases = cli.Command{ Name: "release", Aliases: []string{"releases"}, Usage: "Manage releases", Description: "Manage releases", - Action: runReleases, + Action: releases.RunReleasesList, Subcommands: []*cli.Command{ - &CmdReleaseList, - &CmdReleaseCreate, - &CmdReleaseDelete, - &CmdReleaseEdit, + &releases.CmdReleaseList, + &releases.CmdReleaseCreate, + &releases.CmdReleaseDelete, + &releases.CmdReleaseEdit, }, - Flags: AllDefaultFlags, -} - -// CmdReleaseList represents a sub command of Release to list releases -var CmdReleaseList = cli.Command{ - Name: "ls", - Usage: "List Releases", - Description: "List Releases", - Action: runReleases, - Flags: append([]cli.Flag{ - &PaginationPageFlag, - &PaginationLimitFlag, - }, AllDefaultFlags...), -} - -func runReleases(ctx *cli.Context) error { - login, owner, repo := initCommand() - - releases, _, err := login.Client().ListReleases(owner, repo, gitea.ListReleasesOptions{ListOptions: getListOptions(ctx)}) - if err != nil { - log.Fatal(err) - } - - headers := []string{ - "Tag-Name", - "Title", - "Published At", - "Status", - "Tar URL", - } - - var values [][]string - - if len(releases) == 0 { - Output(outputValue, headers, values) - return nil - } - - for _, release := range releases { - status := "released" - if release.IsDraft { - status = "draft" - } else if release.IsPrerelease { - status = "prerelease" - } - values = append( - values, - []string{ - release.TagName, - release.Title, - release.PublishedAt.Format("2006-01-02 15:04:05"), - status, - release.TarURL, - }, - ) - } - Output(outputValue, headers, values) - - return nil -} - -// CmdReleaseCreate represents a sub command of Release to create release -var CmdReleaseCreate = cli.Command{ - Name: "create", - Usage: "Create a release", - Description: `Create a release`, - Action: runReleaseCreate, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "tag", - Usage: "Tag name", - }, - &cli.StringFlag{ - Name: "target", - Usage: "Target refs, branch name or commit id", - }, - &cli.StringFlag{ - Name: "title", - Aliases: []string{"t"}, - Usage: "Release title", - }, - &cli.StringFlag{ - Name: "note", - Aliases: []string{"n"}, - Usage: "Release notes", - }, - &cli.BoolFlag{ - Name: "draft", - Aliases: []string{"d"}, - Usage: "Is a draft", - }, - &cli.BoolFlag{ - Name: "prerelease", - Aliases: []string{"p"}, - Usage: "Is a pre-release", - }, - &cli.StringSliceFlag{ - Name: "asset", - Aliases: []string{"a"}, - Usage: "List of files to attach", - }, - }, AllDefaultFlags...), -} - -func runReleaseCreate(ctx *cli.Context) error { - login, owner, repo := initCommand() - - release, resp, err := login.Client().CreateRelease(owner, repo, gitea.CreateReleaseOption{ - TagName: ctx.String("tag"), - Target: ctx.String("target"), - Title: ctx.String("title"), - Note: ctx.String("note"), - IsDraft: ctx.Bool("draft"), - IsPrerelease: ctx.Bool("prerelease"), - }) - - if err != nil { - if resp != nil && resp.StatusCode == http.StatusConflict { - fmt.Println("error: There already is a release for this tag") - return nil - } - log.Fatal(err) - } - - for _, asset := range ctx.StringSlice("asset") { - var file *os.File - - if file, err = os.Open(asset); err != nil { - log.Fatal(err) - } - - filePath := filepath.Base(asset) - - if _, _, err = login.Client().CreateReleaseAttachment(owner, repo, release.ID, file, filePath); err != nil { - file.Close() - log.Fatal(err) - } - - file.Close() - } - - return nil -} - -// CmdReleaseDelete represents a sub command of Release to delete a release -var CmdReleaseDelete = cli.Command{ - Name: "delete", - Usage: "Delete a release", - Description: `Delete a release`, - ArgsUsage: "", - Action: runReleaseDelete, - Flags: AllDefaultFlags, -} - -func runReleaseDelete(ctx *cli.Context) error { - login, owner, repo := initCommand() - client := login.Client() - - tag := ctx.Args().First() - if len(tag) == 0 { - fmt.Println("Release tag needed to delete") - return nil - } - - release, err := getReleaseByTag(owner, repo, tag, client) - if err != nil { - return err - } - if release == nil { - return nil - } - - _, err = client.DeleteRelease(owner, repo, release.ID) - return err -} - -func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) { - rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{}) - if err != nil { - return nil, err - } - if len(rl) == 0 { - fmt.Println("Repo does not have any release") - return nil, nil - } - for _, r := range rl { - if r.TagName == tag { - return r, nil - } - } - fmt.Println("Release tag does not exist") - return nil, nil -} - -// CmdReleaseEdit represents a sub command of Release to edit releases -var CmdReleaseEdit = cli.Command{ - Name: "edit", - Usage: "Edit a release", - Description: `Edit a release`, - ArgsUsage: "", - Action: runReleaseEdit, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "tag", - Usage: "Change Tag", - }, - &cli.StringFlag{ - Name: "target", - Usage: "Change Target", - }, - &cli.StringFlag{ - Name: "title", - Aliases: []string{"t"}, - Usage: "Change Title", - }, - &cli.StringFlag{ - Name: "note", - Aliases: []string{"n"}, - Usage: "Change Notes", - }, - &cli.StringFlag{ - Name: "draft", - Aliases: []string{"d"}, - Usage: "Mark as Draft [True/false]", - DefaultText: "true", - }, - &cli.StringFlag{ - Name: "prerelease", - Aliases: []string{"p"}, - Usage: "Mark as Pre-Release [True/false]", - DefaultText: "true", - }, - }, AllDefaultFlags...), -} - -func runReleaseEdit(ctx *cli.Context) error { - login, owner, repo := initCommand() - client := login.Client() - - tag := ctx.Args().First() - if len(tag) == 0 { - fmt.Println("Release tag needed to edit") - return nil - } - - release, err := getReleaseByTag(owner, repo, tag, client) - if err != nil { - return err - } - if release == nil { - return nil - } - - var isDraft, isPre *bool - bTrue := true - bFalse := false - if ctx.IsSet("draft") { - isDraft = &bFalse - if strings.ToLower(ctx.String("draft"))[:1] == "t" { - isDraft = &bTrue - } - } - if ctx.IsSet("prerelease") { - isPre = &bFalse - if strings.ToLower(ctx.String("prerelease"))[:1] == "t" { - isPre = &bTrue - } - } - - _, _, err = client.EditRelease(owner, repo, release.ID, gitea.EditReleaseOption{ - TagName: ctx.String("tag"), - Target: ctx.String("target"), - Title: ctx.String("title"), - Note: ctx.String("note"), - IsDraft: isDraft, - IsPrerelease: isPre, - }) - return err + Flags: flags.AllDefaultFlags, } diff --git a/cmd/releases/create.go b/cmd/releases/create.go new file mode 100644 index 0000000..6af1177 --- /dev/null +++ b/cmd/releases/create.go @@ -0,0 +1,102 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package releases + +import ( + "fmt" + "log" + "net/http" + "os" + "path/filepath" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdReleaseCreate represents a sub command of Release to create release +var CmdReleaseCreate = cli.Command{ + Name: "create", + Usage: "Create a release", + Description: `Create a release`, + Action: runReleaseCreate, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "tag", + Usage: "Tag name", + }, + &cli.StringFlag{ + Name: "target", + Usage: "Target refs, branch name or commit id", + }, + &cli.StringFlag{ + Name: "title", + Aliases: []string{"t"}, + Usage: "Release title", + }, + &cli.StringFlag{ + Name: "note", + Aliases: []string{"n"}, + Usage: "Release notes", + }, + &cli.BoolFlag{ + Name: "draft", + Aliases: []string{"d"}, + Usage: "Is a draft", + }, + &cli.BoolFlag{ + Name: "prerelease", + Aliases: []string{"p"}, + Usage: "Is a pre-release", + }, + &cli.StringSliceFlag{ + Name: "asset", + Aliases: []string{"a"}, + Usage: "List of files to attach", + }, + }, flags.AllDefaultFlags...), +} + +func runReleaseCreate(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + release, resp, err := login.Client().CreateRelease(owner, repo, gitea.CreateReleaseOption{ + TagName: ctx.String("tag"), + Target: ctx.String("target"), + Title: ctx.String("title"), + Note: ctx.String("note"), + IsDraft: ctx.Bool("draft"), + IsPrerelease: ctx.Bool("prerelease"), + }) + + if err != nil { + if resp != nil && resp.StatusCode == http.StatusConflict { + fmt.Println("error: There already is a release for this tag") + return nil + } + log.Fatal(err) + } + + for _, asset := range ctx.StringSlice("asset") { + var file *os.File + + if file, err = os.Open(asset); err != nil { + log.Fatal(err) + } + + filePath := filepath.Base(asset) + + if _, _, err = login.Client().CreateReleaseAttachment(owner, repo, release.ID, file, filePath); err != nil { + file.Close() + log.Fatal(err) + } + + file.Close() + } + + return nil +} diff --git a/cmd/releases/delete.go b/cmd/releases/delete.go new file mode 100644 index 0000000..8c4f06f --- /dev/null +++ b/cmd/releases/delete.go @@ -0,0 +1,46 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package releases + +import ( + "fmt" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "github.com/urfave/cli/v2" +) + +// CmdReleaseDelete represents a sub command of Release to delete a release +var CmdReleaseDelete = cli.Command{ + Name: "delete", + Usage: "Delete a release", + Description: `Delete a release`, + ArgsUsage: "", + Action: runReleaseDelete, + Flags: flags.AllDefaultFlags, +} + +func runReleaseDelete(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + client := login.Client() + + tag := ctx.Args().First() + if len(tag) == 0 { + fmt.Println("Release tag needed to delete") + return nil + } + + release, err := getReleaseByTag(owner, repo, tag, client) + if err != nil { + return err + } + if release == nil { + return nil + } + + _, err = client.DeleteRelease(owner, repo, release.ID) + return err +} diff --git a/cmd/releases/edit.go b/cmd/releases/edit.go new file mode 100644 index 0000000..7e69981 --- /dev/null +++ b/cmd/releases/edit.go @@ -0,0 +1,102 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package releases + +import ( + "fmt" + "strings" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdReleaseEdit represents a sub command of Release to edit releases +var CmdReleaseEdit = cli.Command{ + Name: "edit", + Usage: "Edit a release", + Description: `Edit a release`, + ArgsUsage: "", + Action: runReleaseEdit, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "tag", + Usage: "Change Tag", + }, + &cli.StringFlag{ + Name: "target", + Usage: "Change Target", + }, + &cli.StringFlag{ + Name: "title", + Aliases: []string{"t"}, + Usage: "Change Title", + }, + &cli.StringFlag{ + Name: "note", + Aliases: []string{"n"}, + Usage: "Change Notes", + }, + &cli.StringFlag{ + Name: "draft", + Aliases: []string{"d"}, + Usage: "Mark as Draft [True/false]", + DefaultText: "true", + }, + &cli.StringFlag{ + Name: "prerelease", + Aliases: []string{"p"}, + Usage: "Mark as Pre-Release [True/false]", + DefaultText: "true", + }, + }, flags.AllDefaultFlags...), +} + +func runReleaseEdit(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + client := login.Client() + + tag := ctx.Args().First() + if len(tag) == 0 { + fmt.Println("Release tag needed to edit") + return nil + } + + release, err := getReleaseByTag(owner, repo, tag, client) + if err != nil { + return err + } + if release == nil { + return nil + } + + var isDraft, isPre *bool + bTrue := true + bFalse := false + if ctx.IsSet("draft") { + isDraft = &bFalse + if strings.ToLower(ctx.String("draft"))[:1] == "t" { + isDraft = &bTrue + } + } + if ctx.IsSet("prerelease") { + isPre = &bFalse + if strings.ToLower(ctx.String("prerelease"))[:1] == "t" { + isPre = &bTrue + } + } + + _, _, err = client.EditRelease(owner, repo, release.ID, gitea.EditReleaseOption{ + TagName: ctx.String("tag"), + Target: ctx.String("target"), + Title: ctx.String("title"), + Note: ctx.String("note"), + IsDraft: isDraft, + IsPrerelease: isPre, + }) + return err +} diff --git a/cmd/releases/list.go b/cmd/releases/list.go new file mode 100644 index 0000000..9e34903 --- /dev/null +++ b/cmd/releases/list.go @@ -0,0 +1,94 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package releases + +import ( + "fmt" + "log" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdReleaseList represents a sub command of Release to list releases +var CmdReleaseList = cli.Command{ + Name: "ls", + Usage: "List Releases", + Description: "List Releases", + Action: RunReleasesList, + Flags: append([]cli.Flag{ + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.AllDefaultFlags...), +} + +// RunReleasesList list releases +func RunReleasesList(ctx *cli.Context) error { + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + + releases, _, err := login.Client().ListReleases(owner, repo, gitea.ListReleasesOptions{ListOptions: flags.GetListOptions(ctx)}) + if err != nil { + log.Fatal(err) + } + + headers := []string{ + "Tag-Name", + "Title", + "Published At", + "Status", + "Tar URL", + } + + var values [][]string + + if len(releases) == 0 { + print.OutputList(flags.GlobalOutputValue, headers, values) + return nil + } + + for _, release := range releases { + status := "released" + if release.IsDraft { + status = "draft" + } else if release.IsPrerelease { + status = "prerelease" + } + values = append( + values, + []string{ + release.TagName, + release.Title, + release.PublishedAt.Format("2006-01-02 15:04:05"), + status, + release.TarURL, + }, + ) + } + print.OutputList(flags.GlobalOutputValue, headers, values) + + return nil +} + +func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) { + rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{}) + if err != nil { + return nil, err + } + if len(rl) == 0 { + fmt.Println("Repo does not have any release") + return nil, nil + } + for _, r := range rl { + if r.TagName == tag { + return r, nil + } + } + fmt.Println("Release tag does not exist") + return nil, nil +} diff --git a/cmd/repos.go b/cmd/repos.go index 33ca875..208a3b6 100644 --- a/cmd/repos.go +++ b/cmd/repos.go @@ -5,12 +5,10 @@ package cmd import ( - "fmt" - "log" - "net/http" - "strings" - - "code.gitea.io/tea/modules/utils" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/cmd/repos" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" @@ -24,224 +22,23 @@ var CmdRepos = cli.Command{ ArgsUsage: "[/]", Action: runRepos, Subcommands: []*cli.Command{ - &CmdReposList, - &CmdRepoCreate, + &repos.CmdReposList, + &repos.CmdRepoCreate, }, - Flags: LoginOutputFlags, -} - -// CmdReposList represents a sub command of repos to list them -var CmdReposList = cli.Command{ - Name: "ls", - Usage: "List available repositories", - Description: `List available repositories`, - Action: runReposList, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "mode", - Aliases: []string{"m"}, - Required: false, - Usage: "Filter by mode: fork, mirror, source", - }, - &cli.StringFlag{ - Name: "owner", - Aliases: []string{"O"}, - Required: false, - Usage: "Filter by owner", - }, - &cli.StringFlag{ - Name: "private", - Required: false, - Usage: "Filter private repos (true|false)", - }, - &cli.StringFlag{ - Name: "archived", - Required: false, - Usage: "Filter archived repos (true|false)", - }, - &PaginationPageFlag, - &PaginationLimitFlag, - }, LoginOutputFlags...), -} - -// CmdRepoCreate represents a sub command of repos to create one -var CmdRepoCreate = cli.Command{ - Name: "create", - Aliases: []string{"c"}, - Usage: "Create a repository", - Description: "Create a repository", - Action: runRepoCreate, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "name", - Aliases: []string{""}, - Required: true, - Usage: "name of new repo", - }, - &cli.StringFlag{ - Name: "owner", - Aliases: []string{"O"}, - Required: false, - Usage: "name of repo owner", - }, - &cli.BoolFlag{ - Name: "private", - Required: false, - Value: false, - Usage: "make repo private", - }, - &cli.StringFlag{ - Name: "description", - Aliases: []string{"desc"}, - Required: false, - Usage: "add description to repo", - }, - &cli.BoolFlag{ - Name: "init", - Required: false, - Value: false, - Usage: "initialize repo", - }, - &cli.StringFlag{ - Name: "labels", - Required: false, - Usage: "name of label set to add", - }, - &cli.StringFlag{ - Name: "gitignores", - Aliases: []string{"git"}, - Required: false, - Usage: "list of gitignore templates (need --init)", - }, - &cli.StringFlag{ - Name: "license", - Required: false, - Usage: "add license (need --init)", - }, - &cli.StringFlag{ - Name: "readme", - Required: false, - Usage: "use readme template (need --init)", - }, - &cli.StringFlag{ - Name: "branch", - Required: false, - Usage: "use custom default branch (need --init)", - }, - }, LoginOutputFlags...), + Flags: flags.LoginOutputFlags, } func runRepos(ctx *cli.Context) error { if ctx.Args().Len() == 1 { - return runRepoDetail(ctx, ctx.Args().First()) + return runRepoDetail(ctx.Args().First()) } - return runReposList(ctx) + return repos.RunReposList(ctx) } -// runReposList list repositories -func runReposList(ctx *cli.Context) error { - login := initCommandLoginOnly() +func runRepoDetail(path string) error { + login := config.InitCommandLoginOnly(flags.GlobalLoginValue) client := login.Client() - - var ownerID int64 - if ctx.IsSet("owner") { - // test if owner is a organisation - org, resp, err := client.GetOrg(ctx.String("owner")) - if err != nil { - if resp == nil || resp.StatusCode != http.StatusNotFound { - return err - } - // if owner is no org, its a user - user, _, err := client.GetUserInfo(ctx.String("owner")) - if err != nil { - return err - } - ownerID = user.ID - } else { - ownerID = org.ID - } - } else { - me, _, err := client.GetMyUserInfo() - if err != nil { - return err - } - ownerID = me.ID - } - - var isArchived *bool - if ctx.IsSet("archived") { - archived := strings.ToLower(ctx.String("archived"))[:1] == "t" - isArchived = &archived - } - - var isPrivate *bool - if ctx.IsSet("private") { - private := strings.ToLower(ctx.String("private"))[:1] == "t" - isArchived = &private - } - - mode := gitea.RepoTypeNone - switch ctx.String("mode") { - case "fork": - mode = gitea.RepoTypeFork - case "mirror": - mode = gitea.RepoTypeMirror - case "source": - mode = gitea.RepoTypeSource - } - - rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{ - ListOptions: getListOptions(ctx), - OwnerID: ownerID, - IsPrivate: isPrivate, - IsArchived: isArchived, - Type: mode, - }) - if err != nil { - return err - } - - if len(rps) == 0 { - log.Fatal("No repositories found", rps) - return nil - } - - headers := []string{ - "Name", - "Type", - "SSH", - "Owner", - } - var values [][]string - - for _, rp := range rps { - var mode = "source" - if rp.Fork { - mode = "fork" - } - if rp.Mirror { - mode = "mirror" - } - - values = append( - values, - []string{ - rp.FullName, - mode, - rp.SSHURL, - rp.Owner.UserName, - }, - ) - } - Output(outputValue, headers, values) - - return nil -} - -func runRepoDetail(_ *cli.Context, path string) error { - login := initCommandLoginOnly() - client := login.Client() - repoOwner, repoName := getOwnerAndRepo(path, login.User) + repoOwner, repoName := config.GetOwnerAndRepo(path, login.User) repo, _, err := client.GetRepo(repoOwner, repoName) if err != nil { return err @@ -251,66 +48,6 @@ func runRepoDetail(_ *cli.Context, path string) error { return err } - output := repo.FullName - if repo.Mirror { - output += " (mirror)" - } - if repo.Fork { - output += " (fork)" - } - if repo.Archived { - output += " (archived)" - } - if repo.Empty { - output += " (empty)" - } - output += "\n" - if len(topics) != 0 { - output += "Topics: " + strings.Join(topics, ", ") + "\n" - } - output += "\n" - output += repo.Description + "\n\n" - output += fmt.Sprintf( - "Open Issues: %d, Stars: %d, Forks: %d, Size: %s\n\n", - repo.OpenIssues, - repo.Stars, - repo.Forks, - utils.FormatSize(int64(repo.Size)), - ) - - fmt.Print(output) - return nil -} - -func runRepoCreate(ctx *cli.Context) error { - login := initCommandLoginOnly() - client := login.Client() - var ( - repo *gitea.Repository - err error - ) - opts := gitea.CreateRepoOption{ - Name: ctx.String("name"), - Description: ctx.String("description"), - Private: ctx.Bool("private"), - AutoInit: ctx.Bool("init"), - IssueLabels: ctx.String("labels"), - Gitignores: ctx.String("gitignores"), - License: ctx.String("license"), - Readme: ctx.String("readme"), - DefaultBranch: ctx.String("branch"), - } - if len(ctx.String("owner")) != 0 { - repo, _, err = client.CreateOrgRepo(ctx.String("owner"), opts) - } else { - repo, _, err = client.CreateRepo(opts) - } - if err != nil { - return err - } - if err = runRepoDetail(ctx, repo.FullName); err != nil { - return err - } - fmt.Printf("%s\n", repo.HTMLURL) + print.RepoDetails(repo, topics) return nil } diff --git a/cmd/repos/create.go b/cmd/repos/create.go new file mode 100644 index 0000000..1a21a3c --- /dev/null +++ b/cmd/repos/create.go @@ -0,0 +1,120 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repos + +import ( + "fmt" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdRepoCreate represents a sub command of repos to create one +var CmdRepoCreate = cli.Command{ + Name: "create", + Aliases: []string{"c"}, + Usage: "Create a repository", + Description: "Create a repository", + Action: runRepoCreate, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{""}, + Required: true, + Usage: "name of new repo", + }, + &cli.StringFlag{ + Name: "owner", + Aliases: []string{"O"}, + Required: false, + Usage: "name of repo owner", + }, + &cli.BoolFlag{ + Name: "private", + Required: false, + Value: false, + Usage: "make repo private", + }, + &cli.StringFlag{ + Name: "description", + Aliases: []string{"desc"}, + Required: false, + Usage: "add description to repo", + }, + &cli.BoolFlag{ + Name: "init", + Required: false, + Value: false, + Usage: "initialize repo", + }, + &cli.StringFlag{ + Name: "labels", + Required: false, + Usage: "name of label set to add", + }, + &cli.StringFlag{ + Name: "gitignores", + Aliases: []string{"git"}, + Required: false, + Usage: "list of gitignore templates (need --init)", + }, + &cli.StringFlag{ + Name: "license", + Required: false, + Usage: "add license (need --init)", + }, + &cli.StringFlag{ + Name: "readme", + Required: false, + Usage: "use readme template (need --init)", + }, + &cli.StringFlag{ + Name: "branch", + Required: false, + Usage: "use custom default branch (need --init)", + }, + }, flags.LoginOutputFlags...), +} + +func runRepoCreate(ctx *cli.Context) error { + login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + client := login.Client() + var ( + repo *gitea.Repository + err error + ) + opts := gitea.CreateRepoOption{ + Name: ctx.String("name"), + Description: ctx.String("description"), + Private: ctx.Bool("private"), + AutoInit: ctx.Bool("init"), + IssueLabels: ctx.String("labels"), + Gitignores: ctx.String("gitignores"), + License: ctx.String("license"), + Readme: ctx.String("readme"), + DefaultBranch: ctx.String("branch"), + } + if len(ctx.String("owner")) != 0 { + repo, _, err = client.CreateOrgRepo(ctx.String("owner"), opts) + } else { + repo, _, err = client.CreateRepo(opts) + } + if err != nil { + return err + } + + topics, _, err := client.ListRepoTopics(repo.Owner.UserName, repo.Name, gitea.ListRepoTopicsOptions{}) + if err != nil { + return err + } + print.RepoDetails(repo, topics) + + fmt.Printf("%s\n", repo.HTMLURL) + return nil +} diff --git a/cmd/repos/list.go b/cmd/repos/list.go new file mode 100644 index 0000000..482f5b2 --- /dev/null +++ b/cmd/repos/list.go @@ -0,0 +1,152 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repos + +import ( + "log" + "net/http" + "strings" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdReposList represents a sub command of repos to list them +var CmdReposList = cli.Command{ + Name: "ls", + Usage: "List available repositories", + Description: `List available repositories`, + Action: RunReposList, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "mode", + Aliases: []string{"m"}, + Required: false, + Usage: "Filter by mode: fork, mirror, source", + }, + &cli.StringFlag{ + Name: "owner", + Aliases: []string{"O"}, + Required: false, + Usage: "Filter by owner", + }, + &cli.StringFlag{ + Name: "private", + Required: false, + Usage: "Filter private repos (true|false)", + }, + &cli.StringFlag{ + Name: "archived", + Required: false, + Usage: "Filter archived repos (true|false)", + }, + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.LoginOutputFlags...), +} + +// RunReposList list repositories +func RunReposList(ctx *cli.Context) error { + login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + client := login.Client() + + var ownerID int64 + if ctx.IsSet("owner") { + // test if owner is a organisation + org, resp, err := client.GetOrg(ctx.String("owner")) + if err != nil { + if resp == nil || resp.StatusCode != http.StatusNotFound { + return err + } + // if owner is no org, its a user + user, _, err := client.GetUserInfo(ctx.String("owner")) + if err != nil { + return err + } + ownerID = user.ID + } else { + ownerID = org.ID + } + } else { + me, _, err := client.GetMyUserInfo() + if err != nil { + return err + } + ownerID = me.ID + } + + var isArchived *bool + if ctx.IsSet("archived") { + archived := strings.ToLower(ctx.String("archived"))[:1] == "t" + isArchived = &archived + } + + var isPrivate *bool + if ctx.IsSet("private") { + private := strings.ToLower(ctx.String("private"))[:1] == "t" + isArchived = &private + } + + mode := gitea.RepoTypeNone + switch ctx.String("mode") { + case "fork": + mode = gitea.RepoTypeFork + case "mirror": + mode = gitea.RepoTypeMirror + case "source": + mode = gitea.RepoTypeSource + } + + rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{ + ListOptions: flags.GetListOptions(ctx), + OwnerID: ownerID, + IsPrivate: isPrivate, + IsArchived: isArchived, + Type: mode, + }) + if err != nil { + return err + } + + if len(rps) == 0 { + log.Fatal("No repositories found", rps) + return nil + } + + headers := []string{ + "Name", + "Type", + "SSH", + "Owner", + } + var values [][]string + + for _, rp := range rps { + var mode = "source" + if rp.Fork { + mode = "fork" + } + if rp.Mirror { + mode = "mirror" + } + + values = append( + values, + []string{ + rp.FullName, + mode, + rp.SSHURL, + rp.Owner.UserName, + }, + ) + } + print.OutputList(flags.GlobalOutputValue, headers, values) + + return nil +} diff --git a/cmd/times.go b/cmd/times.go index d3d34b7..01da736 100644 --- a/cmd/times.go +++ b/cmd/times.go @@ -6,13 +6,16 @@ package cmd import ( "fmt" - "log" - "strconv" "strings" "time" - "code.gitea.io/sdk/gitea" + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/cmd/times" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + "code.gitea.io/tea/modules/utils" + "code.gitea.io/sdk/gitea" "github.com/araddon/dateparse" "github.com/urfave/cli/v2" ) @@ -28,9 +31,9 @@ var CmdTrackedTimes = cli.Command{ ArgsUsage: "[username | #issue]", Action: runTrackedTimes, Subcommands: []*cli.Command{ - &CmdTrackedTimesAdd, - &CmdTrackedTimesDelete, - &CmdTrackedTimesReset, + ×.CmdTrackedTimesAdd, + ×.CmdTrackedTimesDelete, + ×.CmdTrackedTimesReset, }, Flags: append([]cli.Flag{ &cli.StringFlag{ @@ -48,11 +51,11 @@ var CmdTrackedTimes = cli.Command{ Aliases: []string{"t"}, Usage: "Print the total duration at the end", }, - }, AllDefaultFlags...), + }, flags.AllDefaultFlags...), } func runTrackedTimes(ctx *cli.Context) error { - login, owner, repo := initCommand() + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil { @@ -69,7 +72,7 @@ func runTrackedTimes(ctx *cli.Context) error { times, _, err = client.GetRepoTrackedTimes(owner, repo) } else if strings.HasPrefix(user, "#") { // get all tracked times on the specified issue - issue, err := argToIndex(user) + issue, err := utils.ArgToIndex(user) if err != nil { return err } @@ -97,175 +100,6 @@ func runTrackedTimes(ctx *cli.Context) error { } } - printTrackedTimes(times, outputValue, from, until, ctx.Bool("total")) - return nil -} - -func formatDuration(seconds int64, outputType string) string { - switch outputType { - case "yaml": - case "csv": - return fmt.Sprint(seconds) - } - return time.Duration(1e9 * seconds).String() -} - -func printTrackedTimes(times []*gitea.TrackedTime, outputType string, from, until time.Time, printTotal bool) { - var outputValues [][]string - var totalDuration int64 - - localLoc, err := time.LoadLocation("Local") // local timezone for time formatting - if err != nil { - log.Fatal(err) - } - - for _, t := range times { - if !from.IsZero() && from.After(t.Created) { - continue - } - if !until.IsZero() && until.Before(t.Created) { - continue - } - - totalDuration += t.Time - - outputValues = append( - outputValues, - []string{ - t.Created.In(localLoc).Format("2006-01-02 15:04:05"), - "#" + strconv.FormatInt(t.Issue.Index, 10), - t.UserName, - formatDuration(t.Time, outputType), - }, - ) - } - - if printTotal { - outputValues = append(outputValues, []string{ - "TOTAL", "", "", formatDuration(totalDuration, outputType), - }) - } - - headers := []string{ - "Created", - "Issue", - "User", - "Duration", - } - Output(outputType, headers, outputValues) -} - -// CmdTrackedTimesAdd represents a sub command of times to add time to an issue -var CmdTrackedTimesAdd = cli.Command{ - Name: "add", - Usage: "Track spent time on an issue", - UsageText: "tea times add ", - Description: `Track spent time on an issue - Example: - tea times add 1 1h25m - `, - Action: runTrackedTimesAdd, - Flags: LoginRepoFlags, -} - -func runTrackedTimesAdd(ctx *cli.Context) error { - login, owner, repo := initCommand() - - if ctx.Args().Len() < 2 { - return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText) - } - - issue, err := argToIndex(ctx.Args().First()) - if err != nil { - log.Fatal(err) - } - - duration, err := time.ParseDuration(strings.Join(ctx.Args().Tail(), "")) - if err != nil { - log.Fatal(err) - } - - _, _, err = login.Client().AddTime(owner, repo, issue, gitea.AddTimeOption{ - Time: int64(duration.Seconds()), - }) - if err != nil { - log.Fatal(err) - } - - return nil -} - -// CmdTrackedTimesDelete is a sub command of CmdTrackedTimes, and removes time from an issue -var CmdTrackedTimesDelete = cli.Command{ - Name: "delete", - Aliases: []string{"rm"}, - Usage: "Delete a single tracked time on an issue", - UsageText: "tea times delete