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 <noerw@noreply.gitea.io>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
6543 2020-09-30 05:11:33 +00:00 committed by Lunny Xiao
parent 9602c149ca
commit f445ac7521
57 changed files with 3127 additions and 2512 deletions

View File

@ -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,
}
}

View File

@ -2,23 +2,24 @@
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package cmd package flags
import ( import (
"log" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// create global variables for global Flags to simplify // create global variables for global Flags to simplify
// access to the options without requiring cli.Context // access to the options without requiring cli.Context
var ( var (
loginValue string // GlobalLoginValue contain value of --login|-l arg
repoValue string GlobalLoginValue string
outputValue string // GlobalRepoValue contain value of --repo|-r arg
remoteValue string 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 // LoginFlag provides flag to specify tea login profile
@ -26,7 +27,7 @@ var LoginFlag = cli.StringFlag{
Name: "login", Name: "login",
Aliases: []string{"l"}, Aliases: []string{"l"},
Usage: "Use a different Gitea login. Optional", Usage: "Use a different Gitea login. Optional",
Destination: &loginValue, Destination: &GlobalLoginValue,
} }
// RepoFlag provides flag to specify repository // RepoFlag provides flag to specify repository
@ -34,7 +35,7 @@ var RepoFlag = cli.StringFlag{
Name: "repo", Name: "repo",
Aliases: []string{"r"}, Aliases: []string{"r"},
Usage: "Repository to interact with. Optional", Usage: "Repository to interact with. Optional",
Destination: &repoValue, Destination: &GlobalRepoValue,
} }
// RemoteFlag provides flag to specify remote repository // RemoteFlag provides flag to specify remote repository
@ -42,7 +43,7 @@ var RemoteFlag = cli.StringFlag{
Name: "remote", Name: "remote",
Aliases: []string{"R"}, Aliases: []string{"R"},
Usage: "Discover Gitea login from remote. Optional", Usage: "Discover Gitea login from remote. Optional",
Destination: &remoteValue, Destination: &GlobalRemoteValue,
} }
// OutputFlag provides flag to specify output type // OutputFlag provides flag to specify output type
@ -50,7 +51,7 @@ var OutputFlag = cli.StringFlag{
Name: "output", Name: "output",
Aliases: []string{"o"}, Aliases: []string{"o"},
Usage: "Output format. (csv, simple, table, tsv, yaml)", Usage: "Output format. (csv, simple, table, tsv, yaml)",
Destination: &outputValue, Destination: &GlobalOutputValue,
} }
// StateFlag provides flag to specify issue/pr state, defaulting to "open" // StateFlag provides flag to specify issue/pr state, defaulting to "open"
@ -109,61 +110,15 @@ var IssuePRFlags = append([]cli.Flag{
&PaginationLimitFlag, &PaginationLimitFlag,
}, AllDefaultFlags...) }, AllDefaultFlags...)
// initCommand returns repository and *Login based on flags // GetListOptions return ListOptions based on PaginationFlags
func initCommand() (*Login, string, string) { func GetListOptions(ctx *cli.Context) gitea.ListOptions {
var login *Login page := ctx.Int("page")
limit := ctx.Int("limit")
err := loadConfig(yamlConfigPath) if limit != 0 && page == 0 {
if err != nil { page = 1
log.Fatal("load config file failed ", yamlConfigPath)
} }
return gitea.ListOptions{
if login, err = getDefaultLogin(); err != nil { Page: page,
log.Fatal(err.Error()) 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
}

View File

@ -5,12 +5,12 @@
package cmd package cmd
import ( import (
"fmt" "code.gitea.io/tea/cmd/flags"
"log" "code.gitea.io/tea/cmd/issues"
"strconv" "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" "github.com/urfave/cli/v2"
) )
@ -22,34 +22,25 @@ var CmdIssues = cli.Command{
ArgsUsage: "[<issue index>]", ArgsUsage: "[<issue index>]",
Action: runIssues, Action: runIssues,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&CmdIssuesList, &issues.CmdIssuesList,
&CmdIssuesCreate, &issues.CmdIssuesCreate,
&CmdIssuesReopen, &issues.CmdIssuesReopen,
&CmdIssuesClose, &issues.CmdIssuesClose,
}, },
Flags: IssuePRFlags, Flags: 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,
} }
func runIssues(ctx *cli.Context) error { func runIssues(ctx *cli.Context) error {
if ctx.Args().Len() == 1 { 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 { func runIssueDetail(index string) error {
login, owner, repo := initCommand() login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
idx, err := argToIndex(index) idx, err := utils.ArgToIndex(index)
if err != nil { if err != nil {
return err return err
} }
@ -57,165 +48,6 @@ func runIssueDetail(ctx *cli.Context, index string) error {
if err != nil { if err != nil {
return err return err
} }
print.IssueDetails(issue)
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)
return nil 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: "<issue index>",
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: "<issue index>",
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
}

46
cmd/issues/close.go Normal file
View File

@ -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: "<issue index>",
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
}

59
cmd/issues/create.go Normal file
View File

@ -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
}

92
cmd/issues/list.go Normal file
View File

@ -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
}

26
cmd/issues/reopen.go Normal file
View File

@ -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: "<issue index>",
Action: func(ctx *cli.Context) error {
var s = gitea.StateOpen
return editIssueState(ctx, gitea.EditIssueOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}

View File

@ -5,15 +5,17 @@
package cmd package cmd
import ( import (
"bufio"
"fmt" "fmt"
"log" "log"
"os" "os"
"strconv" "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" "code.gitea.io/sdk/gitea"
"github.com/muesli/termenv" "github.com/muesli/termenv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -25,9 +27,9 @@ var CmdLabels = cli.Command{
Description: `Manage issue labels`, Description: `Manage issue labels`,
Action: runLabels, Action: runLabels,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&CmdLabelCreate, &labels.CmdLabelCreate,
&CmdLabelUpdate, &labels.CmdLabelUpdate,
&CmdLabelDelete, &labels.CmdLabelDelete,
}, },
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
@ -35,13 +37,13 @@ var CmdLabels = cli.Command{
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Save all the labels as a file", Usage: "Save all the labels as a file",
}, },
&PaginationPageFlag, &flags.PaginationPageFlag,
&PaginationLimitFlag, &flags.PaginationLimitFlag,
}, AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runLabels(ctx *cli.Context) error { func runLabels(ctx *cli.Context) error {
login, owner, repo := initCommand() login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
headers := []string{ headers := []string{
"Index", "Index",
@ -52,13 +54,13 @@ func runLabels(ctx *cli.Context) error {
var values [][]string 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(labels) == 0 { if len(labels) == 0 {
Output(outputValue, headers, values) print.OutputList(flags.GlobalOutputValue, headers, values)
return nil return nil
} }
@ -89,184 +91,7 @@ func runLabels(ctx *cli.Context) error {
}, },
) )
} }
Output(outputValue, headers, values) print.OutputList(flags.GlobalOutputValue, 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)
} }
return nil return nil

109
cmd/labels/create.go Normal file
View File

@ -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
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package cmd package labels
import ( import (
"bufio" "bufio"

39
cmd/labels/delete.go Normal file
View File

@ -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
}

75
cmd/labels/update.go Normal file
View File

@ -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
}

View File

@ -5,19 +5,8 @@
package cmd package cmd
import ( import (
"crypto/tls" "code.gitea.io/tea/cmd/login"
"fmt"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"strings"
"time"
"code.gitea.io/sdk/gitea"
"github.com/skratchdot/open-golang/open"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -26,337 +15,11 @@ var CmdLogin = cli.Command{
Name: "login", Name: "login",
Usage: "Log in to a Gitea server", Usage: "Log in to a Gitea server",
Description: `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{ Subcommands: []*cli.Command{
&cmdLoginList, &login.CmdLoginList,
&cmdLoginAdd, &login.CmdLoginAdd,
&cmdLoginEdit, &login.CmdLoginEdit,
&cmdLoginSetDefault, &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: "<Login>",
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
}

83
cmd/login/add.go Normal file
View File

@ -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()
}

52
cmd/login/default.go Normal file
View File

@ -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: "<Login>",
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()
}

26
cmd/login/edit.go Normal file
View File

@ -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())
}

56
cmd/login/list.go Normal file
View File

@ -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
}

View File

@ -9,6 +9,8 @@ import (
"log" "log"
"os" "os"
"code.gitea.io/tea/modules/config"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -37,23 +39,23 @@ func runLogout(ctx *cli.Context) error {
return errors.New("Please specify a login name") return errors.New("Please specify a login name")
} }
err := loadConfig(yamlConfigPath) err := config.LoadConfig()
if err != nil { if err != nil {
log.Fatal("Unable to load config file " + yamlConfigPath) log.Fatal(err)
} }
var idx = -1 var idx = -1
for i, l := range config.Logins { for i, l := range config.Config.Logins {
if l.Name == name { if l.Name == name {
idx = i idx = i
break break
} }
} }
if idx > -1 { if idx > -1 {
config.Logins = append(config.Logins[:idx], config.Logins[idx+1:]...) config.Config.Logins = append(config.Config.Logins[:idx], config.Config.Logins[idx+1:]...)
err = saveConfig(yamlConfigPath) err = config.SaveConfig()
if err != nil { if err != nil {
log.Fatal("Unable to save config file " + yamlConfigPath) log.Fatal(err)
} }
} }

View File

@ -5,10 +5,11 @@
package cmd package cmd
import ( import (
"fmt" "code.gitea.io/tea/cmd/flags"
"log" "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" "github.com/urfave/cli/v2"
) )
@ -21,42 +22,25 @@ var CmdMilestones = cli.Command{
ArgsUsage: "[<milestone name>]", ArgsUsage: "[<milestone name>]",
Action: runMilestones, Action: runMilestones,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&CmdMilestonesList, &milestones.CmdMilestonesList,
&CmdMilestonesCreate, &milestones.CmdMilestonesCreate,
&CmdMilestonesClose, &milestones.CmdMilestonesClose,
&CmdMilestonesDelete, &milestones.CmdMilestonesDelete,
&CmdMilestonesReopen, &milestones.CmdMilestonesReopen,
&CmdMilestonesIssues, &milestones.CmdMilestonesIssues,
}, },
Flags: AllDefaultFlags, Flags: 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...),
} }
func runMilestones(ctx *cli.Context) error { func runMilestones(ctx *cli.Context) error {
if ctx.Args().Len() == 1 { 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 { func runMilestoneDetail(name string) error {
login, owner, repo := initCommand() login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
client := login.Client() client := login.Client()
milestone, _, err := client.GetMilestoneByName(owner, repo, name) milestone, _, err := client.GetMilestoneByName(owner, repo, name)
@ -64,192 +48,6 @@ func runMilestoneDetail(ctx *cli.Context, name string) error {
return err return err
} }
fmt.Printf("%s\n", print.MilestoneDetails(milestone)
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"))
}
return nil 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: "<milestone name>",
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: "<milestone name>",
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: "<milestone name>",
Action: func(ctx *cli.Context) error {
return editMilestoneStatus(ctx, false)
},
Flags: AllDefaultFlags,
}

32
cmd/milestones/close.go Normal file
View File

@ -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: "<milestone name>",
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...),
}

69
cmd/milestones/create.go Normal file
View File

@ -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
}

31
cmd/milestones/delete.go Normal file
View File

@ -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: "<milestone name>",
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
}

View File

@ -2,12 +2,17 @@
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package cmd package milestones
import ( import (
"fmt" "fmt"
"strconv" "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" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -34,9 +39,9 @@ var CmdMilestonesIssues = cli.Command{
Name: "kind", Name: "kind",
Usage: "Filter by kind (issue|pull)", Usage: "Filter by kind (issue|pull)",
}, },
&PaginationPageFlag, &flags.PaginationPageFlag,
&PaginationLimitFlag, &flags.PaginationLimitFlag,
}, AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
// CmdMilestoneAddIssue represents a sub command of milestone issues to add an issue/pull to an milestone // 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", Description: "Add an issue/pull to an milestone",
ArgsUsage: "<milestone name> <issue/pull index>", ArgsUsage: "<milestone name> <issue/pull index>",
Action: runMilestoneIssueAdd, Action: runMilestoneIssueAdd,
Flags: AllDefaultFlags, Flags: flags.AllDefaultFlags,
} }
// CmdMilestoneRemoveIssue represents a sub command of milestones to remove an issue/pull from an milestone // 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", Description: "Remove an issue/pull to an milestone",
ArgsUsage: "<milestone name> <issue/pull index>", ArgsUsage: "<milestone name> <issue/pull index>",
Action: runMilestoneIssueRemove, Action: runMilestoneIssueRemove,
Flags: AllDefaultFlags, Flags: flags.AllDefaultFlags,
} }
func runMilestoneIssueList(ctx *cli.Context) error { func runMilestoneIssueList(ctx *cli.Context) error {
login, owner, repo := initCommand() login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
client := login.Client() client := login.Client()
state := gitea.StateOpen state := gitea.StateOpen
@ -91,7 +96,7 @@ func runMilestoneIssueList(ctx *cli.Context) error {
} }
issues, _, err := client.ListRepoIssues(owner, repo, gitea.ListIssueOption{ issues, _, err := client.ListRepoIssues(owner, repo, gitea.ListIssueOption{
ListOptions: getListOptions(ctx), ListOptions: flags.GetListOptions(ctx),
Milestones: []string{milestone}, Milestones: []string{milestone},
Type: kind, Type: kind,
State: state, State: state,
@ -112,7 +117,7 @@ func runMilestoneIssueList(ctx *cli.Context) error {
var values [][]string var values [][]string
if len(issues) == 0 { if len(issues) == 0 {
Output(outputValue, headers, values) print.OutputList(flags.GlobalOutputValue, headers, values)
return nil return nil
} }
@ -137,12 +142,12 @@ func runMilestoneIssueList(ctx *cli.Context) error {
}, },
) )
} }
Output(outputValue, headers, values) print.OutputList(flags.GlobalOutputValue, headers, values)
return nil return nil
} }
func runMilestoneIssueAdd(ctx *cli.Context) error { func runMilestoneIssueAdd(ctx *cli.Context) error {
login, owner, repo := initCommand() login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
client := login.Client() client := login.Client()
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
return fmt.Errorf("need two arguments") return fmt.Errorf("need two arguments")
@ -150,7 +155,7 @@ func runMilestoneIssueAdd(ctx *cli.Context) error {
mileName := ctx.Args().Get(0) mileName := ctx.Args().Get(0)
issueIndex := ctx.Args().Get(1) issueIndex := ctx.Args().Get(1)
idx, err := argToIndex(issueIndex) idx, err := utils.ArgToIndex(issueIndex)
if err != nil { if err != nil {
return err return err
} }
@ -168,7 +173,7 @@ func runMilestoneIssueAdd(ctx *cli.Context) error {
} }
func runMilestoneIssueRemove(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() client := login.Client()
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
return fmt.Errorf("need two arguments") return fmt.Errorf("need two arguments")
@ -176,7 +181,7 @@ func runMilestoneIssueRemove(ctx *cli.Context) error {
mileName := ctx.Args().Get(0) mileName := ctx.Args().Get(0)
issueIndex := ctx.Args().Get(1) issueIndex := ctx.Args().Get(1)
idx, err := argToIndex(issueIndex) idx, err := utils.ArgToIndex(issueIndex)
if err != nil { if err != nil {
return err return err
} }

93
cmd/milestones/list.go Normal file
View File

@ -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
}

42
cmd/milestones/reopen.go Normal file
View File

@ -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: "<milestone name>",
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
}

View File

@ -8,6 +8,10 @@ import (
"log" "log"
"strings" "strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -34,16 +38,16 @@ var CmdNotifications = cli.Command{
Aliases: []string{"pd"}, Aliases: []string{"pd"},
Usage: "show pinned notifications instead unread", Usage: "show pinned notifications instead unread",
}, },
&PaginationPageFlag, &flags.PaginationPageFlag,
&PaginationLimitFlag, &flags.PaginationLimitFlag,
}, AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runNotifications(ctx *cli.Context) error { func runNotifications(ctx *cli.Context) error {
var news []*gitea.NotificationThread var news []*gitea.NotificationThread
var err error var err error
listOpts := getListOptions(ctx) listOpts := flags.GetListOptions(ctx)
if listOpts.Page == 0 { if listOpts.Page == 0 {
listOpts.Page = 1 listOpts.Page = 1
} }
@ -57,13 +61,13 @@ func runNotifications(ctx *cli.Context) error {
} }
if ctx.Bool("all") { if ctx.Bool("all") {
login := initCommandLoginOnly() login := config.InitCommandLoginOnly(flags.GlobalLoginValue)
news, _, err = login.Client().ListNotifications(gitea.ListNotificationOptions{ news, _, err = login.Client().ListNotifications(gitea.ListNotificationOptions{
ListOptions: listOpts, ListOptions: listOpts,
Status: status, Status: status,
}) })
} else { } 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{ news, _, err = login.Client().ListRepoNotifications(owner, repo, gitea.ListNotificationOptions{
ListOptions: listOpts, ListOptions: listOpts,
Status: status, Status: status,
@ -107,7 +111,7 @@ func runNotifications(ctx *cli.Context) error {
} }
if len(values) != 0 { if len(values) != 0 {
Output(outputValue, headers, values) print.OutputList(flags.GlobalOutputValue, headers, values)
} }
return nil return nil
} }

View File

@ -9,6 +9,8 @@ import (
"path" "path"
"strings" "strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
local_git "code.gitea.io/tea/modules/git" local_git "code.gitea.io/tea/modules/git"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
@ -21,11 +23,11 @@ var CmdOpen = cli.Command{
Usage: "Open something of the repository on web browser", Usage: "Open something of the repository on web browser",
Description: `Open something of the repository on web browser`, Description: `Open something of the repository on web browser`,
Action: runOpen, Action: runOpen,
Flags: append([]cli.Flag{}, LoginRepoFlags...), Flags: append([]cli.Flag{}, flags.LoginRepoFlags...),
} }
func runOpen(ctx *cli.Context) error { func runOpen(ctx *cli.Context) error {
login, owner, repo := initCommand() login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
var suffix string var suffix string
number := ctx.Args().Get(0) number := ctx.Args().Get(0)

View File

@ -6,16 +6,12 @@ package cmd
import ( import (
"fmt" "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" "github.com/urfave/cli/v2"
) )
@ -27,35 +23,26 @@ var CmdPulls = cli.Command{
Description: `List, create, checkout and clean pull requests`, Description: `List, create, checkout and clean pull requests`,
ArgsUsage: "[<pull index>]", ArgsUsage: "[<pull index>]",
Action: runPulls, Action: runPulls,
Flags: IssuePRFlags, Flags: flags.IssuePRFlags,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&CmdPullsList, &pulls.CmdPullsList,
&CmdPullsCheckout, &pulls.CmdPullsCheckout,
&CmdPullsClean, &pulls.CmdPullsClean,
&CmdPullsCreate, &pulls.CmdPullsCreate,
}, },
} }
func runPulls(ctx *cli.Context) error { func runPulls(ctx *cli.Context) error {
if ctx.Args().Len() == 1 { 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 func runPullDetail(index string) error {
var CmdPullsList = cli.Command{ login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
Name: "ls",
Usage: "List pull requests of the repository",
Description: `List pull requests of the repository`,
Action: runPullsList,
Flags: IssuePRFlags,
}
func runPullDetail(ctx *cli.Context, index string) error { idx, err := utils.ArgToIndex(index)
login, owner, repo := initCommand()
idx, err := argToIndex(index)
if err != nil { if err != nil {
return err return err
} }
@ -73,375 +60,3 @@ func runPullDetail(ctx *cli.Context, index string) error {
) )
return nil 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: "<pull index>",
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: "<pull index>",
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 <remote> <branch>")
}
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)
}

97
cmd/pulls/checkout.go Normal file
View File

@ -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: "<pull index>",
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
}

105
cmd/pulls/clean.go Normal file
View File

@ -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: "<pull index>",
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)
}

145
cmd/pulls/create.go Normal file
View File

@ -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 <remote> <branch>")
}
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
}

93
cmd/pulls/list.go Normal file
View File

@ -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
}

View File

@ -5,307 +5,25 @@
package cmd package cmd
import ( import (
"fmt" "code.gitea.io/tea/cmd/flags"
"log" "code.gitea.io/tea/cmd/releases"
"net/http"
"os"
"path/filepath"
"strings"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// CmdReleases represents to login a gitea server. // CmdReleases represents to login a gitea server.
// ToDo: ReleaseDetails
var CmdReleases = cli.Command{ var CmdReleases = cli.Command{
Name: "release", Name: "release",
Aliases: []string{"releases"}, Aliases: []string{"releases"},
Usage: "Manage releases", Usage: "Manage releases",
Description: "Manage releases", Description: "Manage releases",
Action: runReleases, Action: releases.RunReleasesList,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&CmdReleaseList, &releases.CmdReleaseList,
&CmdReleaseCreate, &releases.CmdReleaseCreate,
&CmdReleaseDelete, &releases.CmdReleaseDelete,
&CmdReleaseEdit, &releases.CmdReleaseEdit,
}, },
Flags: AllDefaultFlags, Flags: 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: "<release tag>",
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: "<release tag>",
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
} }

102
cmd/releases/create.go Normal file
View File

@ -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
}

46
cmd/releases/delete.go Normal file
View File

@ -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: "<release tag>",
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
}

102
cmd/releases/edit.go Normal file
View File

@ -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: "<release tag>",
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
}

94
cmd/releases/list.go Normal file
View File

@ -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
}

View File

@ -5,12 +5,10 @@
package cmd package cmd
import ( import (
"fmt" "code.gitea.io/tea/cmd/flags"
"log" "code.gitea.io/tea/cmd/repos"
"net/http" "code.gitea.io/tea/modules/config"
"strings" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -24,224 +22,23 @@ var CmdRepos = cli.Command{
ArgsUsage: "[<repo owner>/<repo name>]", ArgsUsage: "[<repo owner>/<repo name>]",
Action: runRepos, Action: runRepos,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&CmdReposList, &repos.CmdReposList,
&CmdRepoCreate, &repos.CmdRepoCreate,
}, },
Flags: LoginOutputFlags, Flags: 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...),
} }
func runRepos(ctx *cli.Context) error { func runRepos(ctx *cli.Context) error {
if ctx.Args().Len() == 1 { 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 runRepoDetail(path string) error {
func runReposList(ctx *cli.Context) error { login := config.InitCommandLoginOnly(flags.GlobalLoginValue)
login := initCommandLoginOnly()
client := login.Client() client := login.Client()
repoOwner, repoName := config.GetOwnerAndRepo(path, login.User)
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)
repo, _, err := client.GetRepo(repoOwner, repoName) repo, _, err := client.GetRepo(repoOwner, repoName)
if err != nil { if err != nil {
return err return err
@ -251,66 +48,6 @@ func runRepoDetail(_ *cli.Context, path string) error {
return err return err
} }
output := repo.FullName print.RepoDetails(repo, topics)
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)
return nil return nil
} }

120
cmd/repos/create.go Normal file
View File

@ -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
}

152
cmd/repos/list.go Normal file
View File

@ -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
}

View File

@ -6,13 +6,16 @@ package cmd
import ( import (
"fmt" "fmt"
"log"
"strconv"
"strings" "strings"
"time" "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/araddon/dateparse"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -28,9 +31,9 @@ var CmdTrackedTimes = cli.Command{
ArgsUsage: "[username | #issue]", ArgsUsage: "[username | #issue]",
Action: runTrackedTimes, Action: runTrackedTimes,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&CmdTrackedTimesAdd, &times.CmdTrackedTimesAdd,
&CmdTrackedTimesDelete, &times.CmdTrackedTimesDelete,
&CmdTrackedTimesReset, &times.CmdTrackedTimesReset,
}, },
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
@ -48,11 +51,11 @@ var CmdTrackedTimes = cli.Command{
Aliases: []string{"t"}, Aliases: []string{"t"},
Usage: "Print the total duration at the end", Usage: "Print the total duration at the end",
}, },
}, AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runTrackedTimes(ctx *cli.Context) error { func runTrackedTimes(ctx *cli.Context) error {
login, owner, repo := initCommand() login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
client := login.Client() client := login.Client()
if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil { if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil {
@ -69,7 +72,7 @@ func runTrackedTimes(ctx *cli.Context) error {
times, _, err = client.GetRepoTrackedTimes(owner, repo) times, _, err = client.GetRepoTrackedTimes(owner, repo)
} else if strings.HasPrefix(user, "#") { } else if strings.HasPrefix(user, "#") {
// get all tracked times on the specified issue // get all tracked times on the specified issue
issue, err := argToIndex(user) issue, err := utils.ArgToIndex(user)
if err != nil { if err != nil {
return err return err
} }
@ -97,175 +100,6 @@ func runTrackedTimes(ctx *cli.Context) error {
} }
} }
printTrackedTimes(times, outputValue, from, until, ctx.Bool("total")) print.TrackedTimesList(times, flags.GlobalOutputValue, 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 <issue> <duration>",
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 <issue> <time ID>",
Action: runTrackedTimesDelete,
Flags: LoginRepoFlags,
}
func runTrackedTimesDelete(ctx *cli.Context) error {
login, owner, repo := initCommand()
client := login.Client()
if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil {
return err
}
if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := argToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
}
timeID, err := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
if err != nil {
log.Fatal(err)
}
_, err = client.DeleteTime(owner, repo, issue, timeID)
if err != nil {
log.Fatal(err)
}
return nil
}
// CmdTrackedTimesReset is a subcommand of CmdTrackedTimes, and
// clears all tracked times on an issue.
var CmdTrackedTimesReset = cli.Command{
Name: "reset",
Usage: "Reset tracked time on an issue",
UsageText: "tea times reset <issue>",
Action: runTrackedTimesReset,
Flags: LoginRepoFlags,
}
func runTrackedTimesReset(ctx *cli.Context) error {
login, owner, repo := initCommand()
client := login.Client()
if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil {
return err
}
if ctx.Args().Len() != 1 {
return fmt.Errorf("No issue specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := argToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
}
_, err = client.ResetIssueTime(owner, repo, issue)
if err != nil {
log.Fatal(err)
}
return nil return nil
} }

59
cmd/times/add.go Normal file
View File

@ -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 times
import (
"fmt"
"log"
"strings"
"time"
"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"
)
// 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 <issue> <duration>",
Description: `Track spent time on an issue
Example:
tea times add 1 1h25m
`,
Action: runTrackedTimesAdd,
Flags: flags.LoginRepoFlags,
}
func runTrackedTimesAdd(ctx *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := utils.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
}

57
cmd/times/delete.go Normal file
View File

@ -0,0 +1,57 @@
// 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 times
import (
"fmt"
"log"
"strconv"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// 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 <issue> <time ID>",
Action: runTrackedTimesDelete,
Flags: flags.LoginRepoFlags,
}
func runTrackedTimesDelete(ctx *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
client := login.Client()
if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil {
return err
}
if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
}
timeID, err := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
if err != nil {
log.Fatal(err)
}
_, err = client.DeleteTime(owner, repo, issue, timeID)
if err != nil {
log.Fatal(err)
}
return nil
}

51
cmd/times/reset.go Normal file
View File

@ -0,0 +1,51 @@
// 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 times
import (
"fmt"
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdTrackedTimesReset is a subcommand of CmdTrackedTimes, and
// clears all tracked times on an issue.
var CmdTrackedTimesReset = cli.Command{
Name: "reset",
Usage: "Reset tracked time on an issue",
UsageText: "tea times reset <issue>",
Action: runTrackedTimesReset,
Flags: flags.LoginRepoFlags,
}
func runTrackedTimesReset(ctx *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
client := login.Client()
if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil {
return err
}
if ctx.Args().Len() != 1 {
return fmt.Errorf("No issue specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
}
_, err = client.ResetIssueTime(owner, repo, issue)
if err != nil {
log.Fatal(err)
}
return nil
}

160
modules/config/config.go Normal file
View File

@ -0,0 +1,160 @@
// 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 config
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/utils"
"gopkg.in/yaml.v2"
)
// LocalConfig represents local configurations
type LocalConfig struct {
Logins []Login `yaml:"logins"`
}
var (
// Config contain if loaded local tea config
Config LocalConfig
yamlConfigPath string
)
// TODO: do not use init function to detect the tea configuration, use GetConfigPath()
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")
}
// GetConfigPath return path to tea config file
func GetConfigPath() string {
return yamlConfigPath
}
// LoadConfig load config into global Config var
func LoadConfig() error {
ymlPath := GetConfigPath()
exist, _ := utils.FileExist(ymlPath)
if exist {
fmt.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
}
// SaveConfig save config from global Config var into config file
func SaveConfig() error {
ymlPath := GetConfigPath()
bs, err := yaml.Marshal(Config)
if err != nil {
return err
}
return ioutil.WriteFile(ymlPath, bs, 0660)
}
func curGitRepoPath(repoValue, remoteValue string) (*Login, string, error) {
var err error
var repo *git.TeaRepo
if len(repoValue) == 0 {
repo, err = git.RepoForWorkdir()
} else {
repo, err = git.RepoFromPath(repoValue)
}
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")
}
// GetOwnerAndRepo return repoOwner and repoName
// based on relative path and default owner (if not in path)
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
}

282
modules/config/login.go Normal file
View File

@ -0,0 +1,282 @@
// 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 config
import (
"crypto/tls"
"errors"
"fmt"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"strings"
"time"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
)
// 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()
}
// GetDefaultLogin return the default login
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
}
// GetLoginByName get login by name
func GetLoginByName(name string) *Login {
for _, l := range Config.Logins {
if l.Name == name {
return &l
}
}
return nil
}
// AddLogin add login to config ( global var & file)
func AddLogin(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()
if err != nil {
log.Fatal(err)
}
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 = addLoginToConfig(Login{
Name: name,
URL: giteaURL,
Token: token,
Insecure: insecure,
SSHKey: sshKey,
User: u.UserName,
})
if err != nil {
log.Fatal(err)
}
err = SaveConfig()
if err != nil {
log.Fatal(err)
}
return nil
}
// addLoginToConfig add a login to global Config var
func addLoginToConfig(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
}
// InitCommand returns repository and *Login based on flags
func InitCommand(repoValue, loginValue, remoteValue string) (*Login, string, string) {
var login *Login
err := LoadConfig()
if err != nil {
log.Fatal("load config file failed ", yamlConfigPath)
}
if login, err = GetDefaultLogin(); err != nil {
log.Fatal(err.Error())
}
exist, err := utils.PathExists(repoValue)
if err != nil {
log.Fatal(err.Error())
}
if exist || len(repoValue) == 0 {
login, repoValue, err = curGitRepoPath(repoValue, remoteValue)
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(loginValue string) *Login {
err := LoadConfig()
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
}

View File

@ -10,10 +10,10 @@ import (
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os" "os"
"os/user"
"path/filepath"
"strings" "strings"
"code.gitea.io/tea/modules/utils"
git_transport "github.com/go-git/go-git/v5/plumbing/transport" git_transport "github.com/go-git/go-git/v5/plumbing/transport"
gogit_http "github.com/go-git/go-git/v5/plumbing/transport/http" gogit_http "github.com/go-git/go-git/v5/plumbing/transport/http"
gogit_ssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" gogit_ssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
@ -67,9 +67,9 @@ func GetAuthForURL(remoteURL *url.URL, httpUser, keyFile string) (auth git_trans
func readSSHPrivKey(keyFile string) (sig ssh.Signer, err error) { func readSSHPrivKey(keyFile string) (sig ssh.Signer, err error) {
if keyFile != "" { if keyFile != "" {
keyFile, err = absPathWithExpansion(keyFile) keyFile, err = utils.AbsPathWithExpansion(keyFile)
} else { } else {
keyFile, err = absPathWithExpansion("~/.ssh/id_rsa") keyFile, err = utils.AbsPathWithExpansion("~/.ssh/id_rsa")
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -104,17 +104,3 @@ func promptPass(domain string) (string, error) {
pass, err := terminal.ReadPassword(0) pass, err := terminal.ReadPassword(0)
return string(pass), err return string(pass), err
} }
func absPathWithExpansion(p string) (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
if p == "~" {
return u.HomeDir, nil
} else if strings.HasPrefix(p, "~/") {
return filepath.Join(u.HomeDir, p[2:]), nil
} else {
return filepath.Abs(p)
}
}

87
modules/interact/login.go Normal file
View File

@ -0,0 +1,87 @@
// 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 interact
import (
"fmt"
"net/url"
"strings"
"code.gitea.io/tea/modules/config"
)
// CreateLogin create an login interactive
func CreateLogin() 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 config.AddLogin(name, token, user, passwd, sshKey, giteaURL, insecure)
}

31
modules/print/issue.go Normal file
View File

@ -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 print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"github.com/charmbracelet/glamour"
)
// IssueDetails print an issue rendered to stdout
func IssueDetails(issue *gitea.Issue) {
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())
if err != nil {
// TODO: better Error handling
fmt.Printf("Error:\n%v\n\n", err)
return
}
fmt.Print(out)
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package cmd package print
import ( import (
"fmt" "fmt"
@ -17,27 +17,8 @@ var (
showLog bool showLog bool
) )
// Println println content according the flag // errorf printf content as an error information
func Println(a ...interface{}) { func errorf(format string, a ...interface{}) {
if showLog {
fmt.Println(a...)
}
}
// Printf printf content according the flag
func Printf(format string, a ...interface{}) {
if showLog {
fmt.Printf(format, a...)
}
}
// Error println content as an error information
func Error(a ...interface{}) {
fmt.Println(a...)
}
// Errorf printf content as an error information
func Errorf(format string, a ...interface{}) {
fmt.Printf(format, a...) fmt.Printf(format, a...)
} }
@ -91,9 +72,9 @@ func outputyaml(headers []string, values [][]string) {
} }
} }
// Output provides general function to convert given information // OutputList provides general function to convert given list of items
// into several outputs // into several outputs (table, csv, simple, tsv, yaml)
func Output(output string, headers []string, values [][]string) { func OutputList(output string, headers []string, values [][]string) {
switch { switch {
case output == "" || output == "table": case output == "" || output == "table":
outputtable(headers, values) outputtable(headers, values)
@ -106,6 +87,6 @@ func Output(output string, headers []string, values [][]string) {
case output == "yaml": case output == "yaml":
outputyaml(headers, values) outputyaml(headers, values)
default: default:
Errorf("unknown output type '" + output + "', available types are:\n- csv: comma-separated values\n- simple: space-separated values\n- table: auto-aligned table format (default)\n- tsv: tab-separated values\n- yaml: YAML format\n") errorf("unknown output type '" + output + "', available types are:\n- csv: comma-separated values\n- simple: space-separated values\n- table: auto-aligned table format (default)\n- tsv: tab-separated values\n- yaml: YAML format\n")
} }
} }

View File

@ -0,0 +1,24 @@
// 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 print
import (
"fmt"
"code.gitea.io/sdk/gitea"
)
// MilestoneDetails print an milestone formatted to stdout
func MilestoneDetails(milestone *gitea.Milestone) {
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"))
}
}

View File

@ -2,12 +2,23 @@
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package utils package print
import "fmt" import (
"fmt"
// FormatSize get kb in int and return string "github.com/muesli/termenv"
func FormatSize(kb int64) string { )
func getGlamourTheme() string {
if termenv.HasDarkBackground() {
return "dark"
}
return "light"
}
// formatSize get kb in int and return string
func formatSize(kb int64) string {
if kb < 1024 { if kb < 1024 {
return fmt.Sprintf("%d Kb", kb) return fmt.Sprintf("%d Kb", kb)
} }

31
modules/print/pull.go Normal file
View File

@ -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 print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"github.com/charmbracelet/glamour"
)
// PullDetails print an pull rendered to stdout
func PullDetails(pr *gitea.PullRequest) {
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())
if err != nil {
// TODO: better Error handling
fmt.Printf("Error:\n%v\n\n", err)
return
}
fmt.Print(out)
}

44
modules/print/repo.go Normal file
View File

@ -0,0 +1,44 @@
// 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 print
import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
)
// RepoDetails print an repo formatted to stdout
func RepoDetails(repo *gitea.Repository, topics []string) {
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,
formatSize(int64(repo.Size)),
)
fmt.Print(output)
}

69
modules/print/times.go Normal file
View File

@ -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 print
import (
"fmt"
"log"
"strconv"
"time"
"code.gitea.io/sdk/gitea"
)
func formatDuration(seconds int64, outputType string) string {
switch outputType {
case "yaml":
case "csv":
return fmt.Sprint(seconds)
}
return time.Duration(1e9 * seconds).String()
}
// TrackedTimesList print list of tracked times to stdout
func TrackedTimesList(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",
}
OutputList(outputType, headers, outputValues)
}

18
modules/utils/parse.go Normal file
View File

@ -0,0 +1,18 @@
// 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 utils
import (
"strconv"
"strings"
)
// ArgToIndex take issue/pull index as string and return int64
func ArgToIndex(arg string) (int64, error) {
if strings.HasPrefix(arg, "#") {
arg = arg[1:]
}
return strconv.ParseInt(arg, 10, 64)
}

View File

@ -5,7 +5,11 @@
package utils package utils
import ( import (
"errors"
"os" "os"
"os/user"
"path/filepath"
"strings"
) )
// PathExists returns whether the given file or directory exists or not // PathExists returns whether the given file or directory exists or not
@ -19,3 +23,33 @@ func PathExists(path string) (bool, error) {
} }
return true, err return true, err
} }
// FileExist returns whether the given file exists or not
func FileExist(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
}
// AbsPathWithExpansion expand path beginning with "~/" to absolute path
func AbsPathWithExpansion(p string) (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
if p == "~" {
return u.HomeDir, nil
} else if strings.HasPrefix(p, "~/") {
return filepath.Join(u.HomeDir, p[2:]), nil
} else {
return filepath.Abs(p)
}
}