Proper help text & new README structure #311

Merged
lunny merged 11 commits from noerw/tea:improve-app-help into master 2020-12-21 13:37:21 +00:00
51 changed files with 490 additions and 211 deletions
Showing only changes of commit 0e2ba56ab5 - Show all commits

View File

@ -8,7 +8,7 @@
```
tea - command line tool to interact with Gitea
version 0.6.0+9-g75d415b
version 0.6.0+17-g1c10f33
noerw marked this conversation as resolved Outdated
Outdated
Review

version 0.7.0 preview ... or so (should be changed on release)

version 0.7.0 preview ... or so (should be changed on release)
USAGE
tea command [subcommand] [command options] [arguments...]
@ -22,20 +22,21 @@
COMMANDS
help, h Shows a list of commands or help for one command
ENTITIES:
issues, issue List, create and update issues
pulls, pull, pr List, create, checkout and clean pull requests
issues, issue, i List, create and update issues
pulls, pull, pr Manage and checkout pull requests
labels, label Manage issue labels
milestones, milestone, ms List and create milestones
releases, release Manage releases
times, time Operate on tracked times of a repository's issues & pulls
releases, release, r Manage releases
times, time, t Operate on tracked times of a repository's issues & pulls
organizations, organization, org List, create, delete organizations
repos, repo Show repository details
HELPERS:
open Open something of the repository on web browser
notifications, notification, notif Show notifications
open, o Open something of the repository in web browser
notifications, notification, n Show notifications
SETUP:
logins, login Log in to a Gitea server
logout Log out from a Gitea server
logins, login Log in to a Gitea server
logout Log out from a Gitea server
shellcompletion, autocomplete Install shell completion for tea
OPTIONS
--help, -h show help (default: false)
@ -73,7 +74,7 @@
You can use the prebuilt binaries from [dl.gitea.io](https://dl.gitea.io/tea/)
Distribution packages exist for:
- **alpinelinux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge))**
- **alpinelinux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge))**
- **archlinux ([gitea-tea](https://aur.archlinux.org/packages/gitea-tea))**
To install from source, go 1.13 or newer is required:

106
cmd/autocomplete.go Normal file
View File

@ -0,0 +1,106 @@
// 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 cmd
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"github.com/adrg/xdg"
"github.com/urfave/cli/v2"
)
// CmdAutocomplete manages autocompletion
var CmdAutocomplete = cli.Command{
Name: "shellcompletion",
Aliases: []string{"autocomplete"},
Category: catSetup,
Usage: "Install shell completion for tea",
Description: "Install shell completion for tea",
ArgsUsage: "<shell type> (bash, zsh, powershell)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "install",
Usage: "Persist in shell config instead of printing commands",
},
},
Action: runAutocompleteAdd,
}
func runAutocompleteAdd(ctx *cli.Context) error {
var remoteFile, localFile, cmds string
shell := ctx.Args().First()
switch shell {
case "zsh":
remoteFile = "contrib/autocomplete.zsh"
localFile = "autocomplete.zsh"
cmds = "echo 'PROG=tea _CLI_ZSH_AUTOCOMPLETE_HACK=1 source %s' >> ~/.zshrc && source ~/.zshrc"
case "bash":
remoteFile = "contrib/autocomplete.sh"
localFile = "autocomplete.sh"
cmds = "echo 'PROG=tea source %s' >> ~/.bashrc && source ~/.bashrc"
case "powershell":
remoteFile = "contrib/autocomplete.ps1"
localFile = "tea.ps1"
cmds = "\"& %s\" >> $profile"
default:
return fmt.Errorf("Must specify valid shell type")
}
localPath, err := xdg.ConfigFile("tea/" + localFile)
if err != nil {
return err
}
cmds = fmt.Sprintf(cmds, localPath)
if err := saveAutoCompleteFile(remoteFile, localPath); err != nil {
return err
}
if ctx.Bool("install") {
fmt.Println("Installing in your shellrc")
installer := exec.Command(shell, "-c", cmds)
if shell == "powershell" {
installer = exec.Command("powershell.exe", "-Command", cmds)
}
out, err := installer.CombinedOutput()
if err != nil {
return fmt.Errorf("Couldn't run the commands: %s %s", err, out)
}
} else {
fmt.Println("\n# Run the following commands to install autocompletion (or use --install)")
fmt.Println(cmds)
}
return nil
}
func saveAutoCompleteFile(file, destPath string) error {
url := fmt.Sprintf("https://gitea.com/gitea/tea/raw/branch/master/%s", file)
fmt.Println("Fetching " + url)
res, err := http.Get(url)
if err != nil {
return err
}
defer res.Body.Close()
writer, err := os.Create(destPath)
if err != nil {
return err
}
defer writer.Close()
_, err = io.Copy(writer, res.Body)
return err
}

View File

@ -17,7 +17,7 @@ import (
// CmdIssues represents to login a gitea server.
var CmdIssues = cli.Command{
Name: "issues",
Aliases: []string{"issue"},
Aliases: []string{"issue", "i"},
Category: catEntities,
Usage: "List, create and update issues",
Description: "List, create and update issues",

View File

@ -5,7 +5,7 @@
package issues
import (
"log"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
@ -34,7 +34,7 @@ func editIssueState(cmd *cli.Context, opts gitea.EditIssueOption) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 {
log.Fatal(ctx.Command.ArgsUsage)
return fmt.Errorf(ctx.Command.ArgsUsage)
}
index, err := utils.ArgToIndex(ctx.Args().First())

View File

@ -16,6 +16,7 @@ import (
// CmdIssuesCreate represents a sub command of issues to create issue
var CmdIssuesCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create an issue on repository",
Description: `Create an issue on repository`,
Action: runIssuesCreate,

View File

@ -5,8 +5,6 @@
package issues
import (
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
@ -17,8 +15,8 @@ import (
// CmdIssuesList represents a sub command of issues to list issues
var CmdIssuesList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Usage: "List issues of the repository",
Description: `List issues of the repository`,
Action: RunIssuesList,
@ -47,7 +45,7 @@ func RunIssuesList(cmd *cli.Context) error {
})
if err != nil {
log.Fatal(err)
return err
}
print.IssuesList(issues, ctx.Output)

View File

@ -5,7 +5,7 @@
package cmd
import (
"log"
"fmt"
"code.gitea.io/tea/cmd/labels"
"github.com/urfave/cli/v2"
@ -35,6 +35,5 @@ func runLabels(ctx *cli.Context) error {
}
func runLabelsDetails(ctx *cli.Context) error {
log.Fatal("Not yet implemented.")
return nil
return fmt.Errorf("Not yet implemented")
}

View File

@ -19,6 +19,7 @@ import (
// CmdLabelCreate represents a sub command of labels to create label.
var CmdLabelCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a label",
Description: `Create a label`,
Action: runLabelCreate,
@ -80,11 +81,7 @@ func runLabelCreate(cmd *cli.Context) error {
}
}
if err != nil {
log.Fatal(err)
}
return nil
return err
}
func splitLabelLine(line string) (string, string, string) {

View File

@ -5,8 +5,6 @@
package labels
import (
"log"
"code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v2"
@ -15,6 +13,7 @@ import (
// CmdLabelDelete represents a sub command of labels to delete label.
var CmdLabelDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete a label",
Description: `Delete a label`,
Action: runLabelDelete,
@ -31,9 +30,5 @@ func runLabelDelete(cmd *cli.Context) error {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
_, err := ctx.Login.Client().DeleteLabel(ctx.Owner, ctx.Repo, ctx.Int64("id"))
if err != nil {
log.Fatal(err)
}
return nil
return err
}

View File

@ -5,8 +5,6 @@
package labels
import (
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
@ -18,8 +16,8 @@ import (
// CmdLabelsList represents a sub command of labels to list labels
var CmdLabelsList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Usage: "List labels",
Description: "List labels",
Action: RunLabelsList,
@ -44,7 +42,7 @@ func RunLabelsList(cmd *cli.Context) error {
ListOptions: ctx.GetListOptions(),
})
if err != nil {
log.Fatal(err)
return err
}
if ctx.IsSet("save") {

View File

@ -5,8 +5,6 @@
package labels
import (
"log"
"code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea"
@ -68,7 +66,7 @@ func runLabelUpdate(cmd *cli.Context) error {
})
if err != nil {
log.Fatal(err)
return err
}
return nil

View File

@ -15,6 +15,7 @@ import (
// CmdLoginEdit represents to login a gitea server.
var CmdLoginEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit Gitea logins",
Description: `Edit Gitea logins`,
Action: runLoginEdit,

View File

@ -5,8 +5,6 @@
package login
import (
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print"
@ -16,8 +14,8 @@ import (
// CmdLoginList represents to login a gitea server.
var CmdLoginList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Usage: "List Gitea logins",
Description: `List Gitea logins`,
Action: RunLoginList,
@ -28,7 +26,7 @@ var CmdLoginList = cli.Command{
func RunLoginList(cmd *cli.Context) error {
logins, err := config.GetLogins()
if err != nil {
log.Fatal(err)
return err
}
print.LoginsList(logins, cmd.String("output"))
return nil

View File

@ -6,7 +6,6 @@ package milestones
import (
"fmt"
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
@ -19,6 +18,7 @@ import (
// CmdMilestonesCreate represents a sub command of milestones to create milestone
var CmdMilestonesCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create an milestone on repository",
Description: `Create an milestone on repository`,
Action: runMilestonesCreate,
@ -62,7 +62,7 @@ func runMilestonesCreate(cmd *cli.Context) error {
State: state,
})
if err != nil {
log.Fatal(err)
return err
}
print.MilestoneDetails(mile)

View File

@ -5,8 +5,6 @@
package milestones
import (
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
@ -17,8 +15,8 @@ import (
// CmdMilestonesList represents a sub command of milestones to list milestones
var CmdMilestonesList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Usage: "List milestones of the repository",
Description: `List milestones of the repository`,
Action: RunMilestonesList,
@ -53,7 +51,7 @@ func RunMilestonesList(cmd *cli.Context) error {
})
if err != nil {
log.Fatal(err)
return err
}
print.MilestonesList(milestones, ctx.Output, state)

View File

@ -5,8 +5,6 @@
package cmd
import (
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
@ -18,7 +16,7 @@ import (
// CmdNotifications is the main command to operate with notifications
var CmdNotifications = cli.Command{
Name: "notifications",
Aliases: []string{"notification", "notif"},
Aliases: []string{"notification", "n"},
Category: catHelpers,
Usage: "Show notifications",
Description: "Show notifications, by default based of the current repo and unread one",
@ -77,7 +75,7 @@ func runNotifications(cmd *cli.Context) error {
})
}
if err != nil {
log.Fatal(err)
return err
}
print.NotificationsList(news, ctx.Output, ctx.Bool("all"))

View File

@ -5,7 +5,6 @@
package cmd
import (
"log"
"path"
"strings"
@ -20,9 +19,10 @@ import (
// CmdOpen represents a sub command of issues to open issue on the web browser
var CmdOpen = cli.Command{
Name: "open",
Aliases: []string{"o"},
Category: catHelpers,
Usage: "Open something of the repository on web browser",
Description: `Open something of the repository on web browser`,
Usage: "Open something of the repository in web browser",
Description: `Open something of the repository in web browser`,
Action: runOpen,
Flags: append([]cli.Flag{}, flags.LoginRepoFlags...),
}
@ -43,12 +43,11 @@ func runOpen(cmd *cli.Context) error {
case strings.EqualFold(number, "commits"):
repo, err := local_git.RepoForWorkdir()
if err != nil {
log.Fatal(err)
return err
}
b, err := repo.Head()
if err != nil {
log.Fatal(err)
return nil
return err
}
name := b.Name()
switch {
@ -75,11 +74,6 @@ func runOpen(cmd *cli.Context) error {
suffix = number
}
u := path.Join(ctx.Login.URL, ctx.RepoSlug, suffix)
err := open.Run(u)
if err != nil {
log.Fatal(err)
}
return nil
u := path.Join(ctx.Login.URL, ctx.Owner, ctx.Repo, suffix)
return open.Run(u)
}

View File

@ -5,7 +5,7 @@
package cmd
import (
"log"
"fmt"
"code.gitea.io/tea/cmd/organizations"
@ -35,7 +35,5 @@ func runOrganizations(ctx *cli.Context) error {
}
func runOrganizationDetail(path string) error {
log.Fatal("Not yet implemented.")
return nil
return fmt.Errorf("Not yet implemented")
}

View File

@ -5,7 +5,7 @@
package organizations
import (
"log"
"fmt"
"code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v2"
@ -28,14 +28,12 @@ func RunOrganizationDelete(cmd *cli.Context) error {
client := ctx.Login.Client()
if ctx.Args().Len() < 1 {
log.Fatal("You have to specify the organization name you want to delete.")
return nil
return fmt.Errorf("You have to specify the organization name you want to delete")
}
response, err := client.DeleteOrg(ctx.Args().First())
if response != nil && response.StatusCode == 404 {
log.Fatal("The given organization does not exist.")
return nil
return fmt.Errorf("The given organization does not exist")
}
return err

View File

@ -5,8 +5,6 @@
package organizations
import (
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
@ -17,8 +15,8 @@ import (
// CmdOrganizationList represents a sub command of organizations to list users organizations
var CmdOrganizationList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Usage: "List Organizations",
Description: "List users organizations",
Action: RunOrganizationList,
@ -37,7 +35,7 @@ func RunOrganizationList(cmd *cli.Context) error {
ListOptions: ctx.GetListOptions(),
})
if err != nil {
log.Fatal(err)
return err
}
print.OrganizationsList(userOrganizations, ctx.Output)

View File

@ -22,8 +22,8 @@ var CmdPulls = cli.Command{
Name: "pulls",
Aliases: []string{"pull", "pr"},
Category: catEntities,
Usage: "List, create, checkout and clean pull requests",
Description: `List, create, checkout and clean pull requests`,
Usage: "Manage and checkout pull requests",
Description: `Manage and checkout pull requests`,
ArgsUsage: "[<pull index>]",
Action: runPulls,
Flags: flags.IssuePRFlags,
@ -32,6 +32,8 @@ var CmdPulls = cli.Command{
&pulls.CmdPullsCheckout,
&pulls.CmdPullsClean,
&pulls.CmdPullsCreate,
&pulls.CmdPullsClose,
&pulls.CmdPullsReopen,
},
}
@ -61,6 +63,11 @@ func runPullDetail(cmd *cli.Context, index string) error {
fmt.Printf("error while loading reviews: %v\n", err)
}
print.PullDetails(pr, reviews)
ci, _, err := client.GetCombinedStatus(ctx.Owner, ctx.Repo, pr.Head.Sha)
if err != nil {
fmt.Printf("error while loading CI: %v\n", err)
}
print.PullDetails(pr, reviews, ci)
return nil
}

View File

@ -5,7 +5,7 @@
package pulls
import (
"log"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
@ -19,6 +19,7 @@ import (
// CmdPullsCheckout is a command to locally checkout the given PR
var CmdPullsCheckout = cli.Command{
Name: "checkout",
Aliases: []string{"co"},
Usage: "Locally check out the given PR",
Description: `Locally check out the given PR`,
Action: runPullsCheckout,
@ -30,7 +31,7 @@ func runPullsCheckout(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true})
if ctx.Args().Len() != 1 {
log.Fatal("Must specify a PR index")
return fmt.Errorf("Must specify a PR index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {

25
cmd/pulls/close.go Normal file
View File

@ -0,0 +1,25 @@
// 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 (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdPullsClose closes a given open pull request
var CmdPullsClose = cli.Command{
Name: "close",
Usage: "Change state of a pull request to 'closed'",
Description: `Change state of a pull request to 'closed'`,
ArgsUsage: "<pull index>",
Action: func(ctx *cli.Context) error {
var s = gitea.StateClosed
return editPullState(ctx, gitea.EditPullRequestOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}

View File

@ -16,6 +16,7 @@ import (
// CmdPullsCreate creates a pull request
var CmdPullsCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a pull-request",
Description: "Create a pull-request",
Action: runPullsCreate,

38
cmd/pulls/edit.go Normal file
View File

@ -0,0 +1,38 @@
// 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/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// editPullState abstracts the arg parsing to edit the given pull request
func editPullState(cmd *cli.Context, opts gitea.EditPullRequestOption) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 {
return fmt.Errorf("Please provide a Pull Request index")
}
index, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
pr, _, err := ctx.Login.Client().EditPullRequest(ctx.Owner, ctx.Repo, index, opts)
if err != nil {
return err
}
print.PullDetails(pr, nil, nil)
return nil
}

View File

@ -5,8 +5,6 @@
package pulls
import (
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
@ -17,8 +15,8 @@ import (
// CmdPullsList represents a sub command of issues to list pulls
var CmdPullsList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Usage: "List pull requests of the repository",
Description: `List pull requests of the repository`,
Action: RunPullsList,
@ -45,7 +43,7 @@ func RunPullsList(cmd *cli.Context) error {
})
if err != nil {
log.Fatal(err)
return err
}
print.PullsList(prs, ctx.Output)

26
cmd/pulls/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 pulls
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdPullsReopen reopens a given closed pull request
var CmdPullsReopen = cli.Command{
Name: "reopen",
Aliases: []string{"open"},
Usage: "Change state of a pull request to 'open'",
Description: `Change state of a pull request to 'open'`,
ArgsUsage: "<pull index>",
Action: func(ctx *cli.Context) error {
var s = gitea.StateOpen
return editPullState(ctx, gitea.EditPullRequestOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}

View File

@ -15,7 +15,7 @@ import (
// ToDo: ReleaseDetails
var CmdReleases = cli.Command{
Name: "releases",
Aliases: []string{"release"},
Aliases: []string{"release", "r"},
Category: catEntities,
Usage: "Manage releases",
Description: "Manage releases",

View File

@ -6,7 +6,6 @@ package releases
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
@ -21,6 +20,7 @@ import (
// CmdReleaseCreate represents a sub command of Release to create release
var CmdReleaseCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a release",
Description: `Create a release`,
Action: runReleaseCreate,
@ -76,24 +76,23 @@ func runReleaseCreate(cmd *cli.Context) error {
if err != nil {
if resp != nil && resp.StatusCode == http.StatusConflict {
fmt.Println("error: There already is a release for this tag")
return nil
return fmt.Errorf("There already is a release for this tag")
}
log.Fatal(err)
return err
}
for _, asset := range ctx.StringSlice("asset") {
var file *os.File
if file, err = os.Open(asset); err != nil {
log.Fatal(err)
return err
}
filePath := filepath.Base(asset)
if _, _, err = ctx.Login.Client().CreateReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, file, filePath); err != nil {
file.Close()
log.Fatal(err)
return err
}
file.Close()

View File

@ -16,6 +16,7 @@ import (
// CmdReleaseDelete represents a sub command of Release to delete a release
var CmdReleaseDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete a release",
Description: `Delete a release`,
ArgsUsage: "<release tag>",

View File

@ -18,6 +18,7 @@ import (
// CmdReleaseEdit represents a sub command of Release to edit releases
var CmdReleaseEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit a release",
Description: `Edit a release`,
ArgsUsage: "<release tag>",

View File

@ -6,7 +6,6 @@ package releases
import (
"fmt"
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
@ -18,8 +17,8 @@ import (
// CmdReleaseList represents a sub command of Release to list releases
var CmdReleaseList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Usage: "List Releases",
Description: "List Releases",
Action: RunReleasesList,
@ -38,7 +37,7 @@ func RunReleasesList(cmd *cli.Context) error {
ListOptions: ctx.GetListOptions(),
})
if err != nil {
log.Fatal(err)
return err
}
print.ReleasesList(releases, ctx.Output)

View File

@ -35,8 +35,8 @@ var CmdReposListFlags = append([]cli.Flag{
// CmdReposList represents a sub command of repos to list them
var CmdReposList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Usage: "List repositories you have access to",
Description: "List repositories you have access to",
Action: RunReposList,

View File

@ -5,7 +5,7 @@
package repos
import (
"log"
"fmt"
"strings"
"code.gitea.io/tea/cmd/flags"
@ -67,7 +67,7 @@ func runReposSearch(cmd *cli.Context) error {
if err != nil {
// HACK: the client does not return a response on 404, so we can't check res.StatusCode
if err.Error() != "404 Not Found" {
log.Fatal("could not find owner: ", err)
return fmt.Errorf("Could not find owner: %s", err)
}
// if owner is no org, its a user

View File

@ -12,7 +12,7 @@ import (
// CmdTrackedTimes represents the command to operate repositories' times.
var CmdTrackedTimes = cli.Command{
Name: "times",
Aliases: []string{"time"},
Aliases: []string{"time", "t"},
Category: catEntities,
Usage: "Operate on tracked times of a repository's issues & pulls",
Description: `Operate on tracked times of a repository's issues & pulls.

View File

@ -6,7 +6,6 @@ package times
import (
"fmt"
"log"
"strings"
"time"
@ -21,6 +20,7 @@ import (
// CmdTrackedTimesAdd represents a sub command of times to add time to an issue
var CmdTrackedTimesAdd = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Track spent time on an issue",
UsageText: "tea times add <issue> <duration>",
Description: `Track spent time on an issue
@ -41,20 +41,16 @@ func runTrackedTimesAdd(cmd *cli.Context) error {
issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
return err
}
duration, err := time.ParseDuration(strings.Join(ctx.Args().Tail(), ""))
if err != nil {
log.Fatal(err)
return err
}
_, _, err = ctx.Login.Client().AddTime(ctx.Owner, ctx.Repo, issue, gitea.AddTimeOption{
Time: int64(duration.Seconds()),
})
if err != nil {
log.Fatal(err)
}
return nil
return err
}

View File

@ -6,7 +6,6 @@ package times
import (
"fmt"
"log"
"strconv"
"code.gitea.io/tea/cmd/flags"
@ -37,18 +36,14 @@ func runTrackedTimesDelete(cmd *cli.Context) error {
issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
return err
}
timeID, err := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
if err != nil {
log.Fatal(err)
return err
}
_, err = client.DeleteTime(ctx.Owner, ctx.Repo, issue, timeID)
if err != nil {
log.Fatal(err)
}
return nil
return err
}

View File

@ -21,8 +21,8 @@ import (
// CmdTrackedTimesList represents a sub command of times to list them
var CmdTrackedTimesList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Name: "list",
Aliases: []string{"ls"},
Action: RunTimesList,
Usage: "Operate on tracked times of a repository's issues & pulls",
Description: `Operate on tracked times of a repository's issues & pulls.

View File

@ -6,7 +6,6 @@ package times
import (
"fmt"
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
@ -36,13 +35,9 @@ func runTrackedTimesReset(cmd *cli.Context) error {
issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
return err
}
_, err = client.ResetIssueTime(ctx.Owner, ctx.Repo, issue)
if err != nil {
log.Fatal(err)
}
return nil
return err
}

9
contrib/autocomplete.ps1 Normal file
View File

@ -0,0 +1,9 @@
$fn = $($MyInvocation.MyCommand.Name)
$name = $fn -replace "(.*)\.ps1$", '$1'
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
$other = "$wordToComplete --generate-bash-completion"
Invoke-Expression $other | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}

21
contrib/autocomplete.sh Normal file
View File

@ -0,0 +1,21 @@
#! /bin/bash
: ${PROG:=$(basename ${BASH_SOURCE})}
_cli_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
unset PROG

23
contrib/autocomplete.zsh Normal file
View File

@ -0,0 +1,23 @@
#compdef $PROG
_cli_zsh_autocomplete() {
local -a opts
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
else
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts
else
_files
fi
return
}
compdef _cli_zsh_autocomplete $PROG

View File

@ -1,4 +1,4 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// 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.
@ -7,7 +7,6 @@ package main // import "code.gitea.io/tea"
import (
"fmt"
"log"
"os"
"strings"
@ -35,6 +34,7 @@ func main() {
app.Commands = []*cli.Command{
&cmd.CmdLogin,
&cmd.CmdLogout,
&cmd.CmdAutocomplete,
&cmd.CmdIssues,
&cmd.CmdPulls,
@ -51,7 +51,10 @@ func main() {
app.EnableBashCompletion = true
err := app.Run(os.Args)
if err != nil {
log.Fatalf("Failed to run app with %s: %v", os.Args, err)
// app.Run already exits for errors implementing ErrorCoder,
// so we only handle generic errors with code 1 here.
fmt.Fprintf(app.ErrWriter, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@ -47,33 +47,28 @@ func (r TeaRepo) TeaCheckout(branchName string) error {
return tree.Checkout(&git.CheckoutOptions{Branch: localBranchRefName})
}
// TeaDeleteBranch removes the given branch locally, and if `remoteBranch` is
// not empty deletes it at it's remote repo.
func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string, auth git_transport.AuthMethod) error {
// TeaDeleteLocalBranch removes the given branch locally
func (r TeaRepo) TeaDeleteLocalBranch(branch *git_config.Branch) error {
err := r.DeleteBranch(branch.Name)
// if the branch is not found that's ok, as .git/config may have no entry if
// no remote tracking branch is configured for it (eg push without -u flag)
if err != nil && err.Error() != "branch not found" {
return err
}
err = r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name))
if err != nil {
return err
}
return r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name))
}
if remoteBranch != "" {
// delete remote branch via git protocol:
// an empty source in the refspec means remote deletion to git 🙃
refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch))
err = r.Push(&git.PushOptions{
RemoteName: branch.Remote,
RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)},
Prune: true,
Auth: auth,
})
}
return err
// TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol
func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth git_transport.AuthMethod) error {
// delete remote branch via git protocol:
// an empty source in the refspec means remote deletion to git 🙃
refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch))
return r.Push(&git.PushOptions{
RemoteName: remoteName,
RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)},
Prune: true,
Auth: auth,
})
}
// TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the

View File

@ -7,12 +7,21 @@ package print
import (
"fmt"
"strconv"
"strings"
"code.gitea.io/sdk/gitea"
)
var ciStatusSymbols = map[gitea.StatusState]string{
gitea.StatusSuccess: "✓ ",
gitea.StatusPending: "⭮ ",
gitea.StatusWarning: "⚠ ",
gitea.StatusError: "✘ ",
gitea.StatusFailure: "❌ ",
}
// PullDetails print an pull rendered to stdout
func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview) {
func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *gitea.CombinedStatus) {
base := pr.Base.Name
head := pr.Head.Name
if pr.Head.RepoID != pr.Base.RepoID {
@ -23,11 +32,16 @@ func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview) {
}
}
state := pr.State
if pr.Merged != nil {
state = "merged"
}
out := fmt.Sprintf(
"# #%d %s (%s)\n@%s created %s\t**%s** <- **%s**\n\n%s\n",
"# #%d %s (%s)\n@%s created %s\t**%s** <- **%s**\n\n%s\n\n",
pr.Index,
pr.Title,
pr.State,
state,
pr.Poster.UserName,
FormatTime(*pr.Created),
base,
@ -35,29 +49,70 @@ func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview) {
pr.Body,
)
if len(reviews) != 0 {
out += "\n"
revMap := make(map[string]gitea.ReviewStateType)
for _, review := range reviews {
switch review.State {
case gitea.ReviewStateApproved,
gitea.ReviewStateRequestChanges,
gitea.ReviewStateRequestReview:
revMap[review.Reviewer.UserName] = review.State
if ciStatus != nil || len(reviews) != 0 || pr.State == gitea.StateOpen {
out += "---\n"
}
out += formatReviews(reviews)
if ciStatus != nil {
var summary, errors string
for _, s := range ciStatus.Statuses {
summary += ciStatusSymbols[s.State]
if s.State != gitea.StatusSuccess {
errors += fmt.Sprintf(" - [**%s**:\t%s](%s)\n", s.Context, s.Description, s.TargetURL)
}
}
for k, v := range revMap {
out += fmt.Sprintf("\n @%s: %s", k, v)
if len(ciStatus.Statuses) != 0 {
out += fmt.Sprintf("- CI: %s\n%s", summary, errors)
}
}
if pr.State == gitea.StateOpen && pr.Mergeable {
out += "\nNo Conflicts"
if pr.State == gitea.StateOpen {
if pr.Mergeable {
out += "- No Conflicts\n"
} else {
out += "- **Conflicting files**\n"
}
}
outputMarkdown(out)
}
func formatReviews(reviews []*gitea.PullReview) string {
result := ""
if len(reviews) == 0 {
return result
}
// deduplicate reviews by user (via review time & userID),
reviewByUser := make(map[int64]*gitea.PullReview)
for _, review := range reviews {
switch review.State {
case gitea.ReviewStateApproved,
gitea.ReviewStateRequestChanges,
gitea.ReviewStateRequestReview:
if r, ok := reviewByUser[review.Reviewer.ID]; !ok || review.Submitted.After(r.Submitted) {
reviewByUser[review.Reviewer.ID] = review
}
}
}
// group reviews by type
usersByState := make(map[gitea.ReviewStateType][]string)
for _, r := range reviewByUser {
u := r.Reviewer.UserName
users := usersByState[r.State]
usersByState[r.State] = append(users, u)
}
// stringify
for state, user := range usersByState {
result += fmt.Sprintf("- %s by @%s\n", state, strings.Join(user, ", @"))
}
return result
}
// PullsList prints a listing of pulls
func PullsList(prs []*gitea.PullRequest, output string) {
t := tableWithHeader(

View File

@ -6,7 +6,6 @@ package task
import (
"fmt"
"log"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
@ -34,12 +33,12 @@ func CreateIssue(login *config.Login, repoOwner, repoName, title, description st
})
if err != nil {
log.Fatalf("could not create issue: %s", err)
return fmt.Errorf("could not create issue: %s", err)
}
print.IssueDetails(issue)
fmt.Println(issue.HTMLURL)
return err
return nil
}

View File

@ -6,7 +6,6 @@ package task
import (
"fmt"
"log"
"os"
"code.gitea.io/sdk/gitea"
@ -16,7 +15,7 @@ import (
func LabelsExport(labels []*gitea.Label, path string) error {
f, err := os.Create(path)
if err != nil {
log.Fatal(err)
return err
}
defer f.Close()

View File

@ -6,7 +6,6 @@ package task
import (
"fmt"
"log"
"os"
"time"
@ -21,7 +20,7 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bo
// checks ...
// ... if we have a url
if len(giteaURL) == 0 {
log.Fatal("You have to input Gitea server URL")
return fmt.Errorf("You have to input Gitea server URL")
}
// ... if there already exist a login with same name
@ -35,17 +34,17 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bo
// .. if we have enough information to authenticate
if len(token) == 0 && (len(user)+len(passwd)) == 0 {
log.Fatal("No token set")
return fmt.Errorf("No token set")
} else if len(user) != 0 && len(passwd) == 0 {
log.Fatal("No password set")
return fmt.Errorf("No password set")
} else if len(user) == 0 && len(passwd) != 0 {
log.Fatal("No user set")
return fmt.Errorf("No user set")
}
// Normalize URL
serverURL, err := utils.NormalizeURL(giteaURL)
if err != nil {
log.Fatal("Unable to parse URL", err)
return fmt.Errorf("Unable to parse URL: %s", err)
}
login := config.Login{
@ -60,23 +59,21 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bo
client := login.Client()
if len(token) == 0 {
login.Token, err = generateToken(client, user, passwd)
if err != nil {
log.Fatal(err)
if login.Token, err = generateToken(client, user, passwd); err != nil {
return err
}
}
// Verify if authentication works and get user info
u, _, err := client.GetMyUserInfo()
if err != nil {
log.Fatal(err)
return err
}
login.User = u.UserName
if len(login.Name) == 0 {
login.Name, err = GenerateLoginName(giteaURL, login.User)
if err != nil {
log.Fatal(err)
if login.Name, err = GenerateLoginName(giteaURL, login.User); err != nil {
return err
}
}
@ -91,9 +88,8 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bo
}
}
err = config.AddLogin(&login)
if err != nil {
log.Fatal(err)
if err = config.AddLogin(&login); err != nil {
return err
}
fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name)

View File

@ -27,6 +27,10 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64,
if err != nil {
return err
}
remoteDeleted := pr.Head.Ref == fmt.Sprintf("refs/pull/%d/head", pr.Index)
if remoteDeleted {
return fmt.Errorf("Can't checkout: remote head branch was already deleted")
}
remoteURL := pr.Head.Repository.CloneURL
if len(login.SSHKey) != 0 {

View File

@ -19,6 +19,9 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
client := login.Client()
repo, _, err := client.GetRepo(repoOwner, repoName)
if err != nil {
return err
}
defaultBranch := repo.DefaultBranch
if len(defaultBranch) == 0 {
defaultBranch = "master"
@ -33,7 +36,13 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
return fmt.Errorf("PR is still open, won't delete branches")
}
// IDEA: abort if PR.Head.Repository.CloneURL does not match login.URL?
// if remote head branch is already deleted, pr.Head.Ref points to "pulls/<idx>/head"
remoteBranch := pr.Head.Ref
remoteDeleted := remoteBranch == fmt.Sprintf("refs/pull/%d/head", pr.Index)
if remoteDeleted {
remoteBranch = pr.Head.Name // this still holds the original branch name
fmt.Printf("Remote branch '%s' already deleted.\n", remoteBranch)
}
r, err := local_git.RepoForWorkdir()
if err != nil {
@ -43,7 +52,7 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
// find a branch with matching sha or name, that has a remote matching the repo url
var branch *git_config.Branch
if ignoreSHA {
branch, err = r.TeaFindBranchByName(pr.Head.Ref, pr.Head.Repository.CloneURL)
branch, err = r.TeaFindBranchByName(remoteBranch, pr.Head.Repository.CloneURL)
} else {
branch, err = r.TeaFindBranchBySha(pr.Head.Sha, pr.Head.Repository.CloneURL)
}
@ -52,12 +61,12 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
}
if branch == nil {
if ignoreSHA {
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", remoteBranch)
}
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)
call me again with the --ignore-sha flag`, remoteBranch)
}
// prepare deletion of local branch:
@ -73,14 +82,23 @@ call me again with the --ignore-sha flag`, pr.Head.Ref)
}
// 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)
fmt.Printf("Deleting local branch %s\n", branch.Name)
err = r.TeaDeleteLocalBranch(branch)
if err != nil {
return err
}
auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback)
if err != nil {
return err
if !remoteDeleted && pr.Head.Repository.Permissions.Push {
fmt.Printf("Deleting remote branch %s\n", remoteBranch)
url, err := r.TeaRemoteURL(branch.Remote)
if err != nil {
return err
}
auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback)
if err != nil {
return err
}
err = r.TeaDeleteRemoteBranch(branch.Remote, remoteBranch, auth)
}
return r.TeaDeleteBranch(branch, pr.Head.Ref, auth)
return err
}

View File

@ -6,7 +6,6 @@ package task
import (
"fmt"
"log"
"strings"
"code.gitea.io/sdk/gitea"
@ -24,14 +23,14 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des
// open local git repo
localRepo, err := local_git.RepoForWorkdir()
if err != nil {
log.Fatal("could not open local repo: ", err)
return fmt.Errorf("Could not open local repo: %s", err)
}
// push if possible
log.Println("git push")
fmt.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())
fmt.Printf("Error occurred during 'git push':\n%s\n", err.Error())
}
// default is default branch
@ -74,10 +73,10 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des
})
if err != nil {
log.Fatalf("could not create PR from %s to %s:%s: %s", head, repoOwner, base, err)
return fmt.Errorf("Could not create PR from %s to %s:%s: %s", head, repoOwner, base, err)
}
print.PullDetails(pr, nil)
print.PullDetails(pr, nil, nil)
fmt.Println(pr.HTMLURL)