Add --fields to notification & milestone listings #422

Merged
6543 merged 22 commits from noerw/tea:fields-flag into main 2022-09-13 19:08:19 +00:00
33 changed files with 662 additions and 162 deletions
Showing only changes of commit c4e7f97655 - Show all commits

View File

@ -8,16 +8,19 @@
```
tea - command line tool to interact with Gitea
version 0.7.0-preview
version 0.8.0-preview
USAGE
tea command [subcommand] [command options] [arguments...]
DESCRIPTION
tea is a productivity helper for Gitea. It can be used to manage most entities on one
or multiple Gitea instances and provides local helpers like 'tea pull checkout'.
tea makes use of context provided by the repository in $PWD if available, but is still
usable independently of $PWD. Configuration is persisted in $XDG_CONFIG_HOME/tea.
tea is a productivity helper for Gitea. It can be used to manage most entities on
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
tea tries to make use of context provided by the repository in $PWD if available.
tea works best in a upstream/fork workflow, when the local main branch tracks the
upstream repo. tea assumes that local git state is published on the remote before
doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
COMMANDS
help, h Shows a list of commands or help for one command
@ -30,13 +33,16 @@
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
comment, c Add a comment to an issue / pr
HELPERS:
open, o Open something of the repository in web browser
notifications, notification, n Show notifications
clone, C Clone a repository locally
SETUP:
logins, login Log in to a Gitea server
logout Log out from a Gitea server
shellcompletion, autocomplete Install shell completion for tea
whoami Show current logged in user
OPTIONS
--help, -h show help (default: false)

89
cmd/clone.go Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2021 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"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdRepoClone represents a sub command of repos to create a local copy
var CmdRepoClone = cli.Command{
Name: "clone",
Aliases: []string{"C"},
Usage: "Clone a repository locally",
Description: `Clone a repository locally, without a local git installation required.
The repo slug can be specified in different formats:
gitea/tea
tea
gitea.com/gitea/tea
git@gitea.com:gitea/tea
https://gitea.com/gitea/tea
ssh://gitea.com:22/gitea/tea
When a host is specified in the repo-slug, it will override the login specified with --login.
`,
Category: catHelpers,
Action: runRepoClone,
ArgsUsage: "<repo-slug> [target dir]",
Flags: []cli.Flag{
&cli.IntFlag{
Name: "depth",
Aliases: []string{"d"},
Usage: "num commits to fetch, defaults to all",
},
&flags.LoginFlag,
},
}
func runRepoClone(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
args := ctx.Args()
if args.Len() < 1 {
return cli.ShowCommandHelp(cmd, "clone")
}
dir := args.Get(1)
var (
login *config.Login = ctx.Login
owner string = ctx.Login.User
repo string
)
// parse first arg as repo specifier
repoSlug := args.Get(0)
url, err := git.ParseURL(repoSlug)
if err != nil {
return err
}
owner, repo = utils.GetOwnerAndRepo(url.Path, login.User)
if url.Host != "" {
login = config.GetLoginByHost(url.Host)
if login == nil {
return fmt.Errorf("No login configured matching host '%s', run `tea login add` first", url.Host)
}
}
_, err = task.RepoClone(
dir,
login,
owner,
repo,
interact.PromptPassword,
ctx.Int("depth"),
)
return err
}

View File

@ -21,15 +21,19 @@ type CsvFlag struct {
// NewCsvFlag creates a CsvFlag, while setting its usage string and default values
func NewCsvFlag(name, usage string, aliases, availableValues, defaults []string) *CsvFlag {
var availableDesc string
if len(availableValues) != 0 {
availableDesc = " Available values:"
}
return &CsvFlag{
AvailableFields: availableValues,
StringFlag: cli.StringFlag{
Name: name,
Aliases: aliases,
Value: strings.Join(defaults, ","),
Usage: fmt.Sprintf(`Comma-separated list of %s. Available values:
Usage: fmt.Sprintf(`Comma-separated list of %s.%s
%s
`, usage, strings.Join(availableValues, ",")),
`, usage, availableDesc, strings.Join(availableValues, ",")),
},
}
}

View File

@ -5,14 +5,6 @@
package flags
import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
@ -44,13 +36,6 @@ var OutputFlag = cli.StringFlag{
Usage: "Output format. (csv, simple, table, tsv, yaml)",
}
// StateFlag provides flag to specify issue/pr state, defaulting to "open"
var StateFlag = cli.StringFlag{
Name: "state",
Usage: "Filter by state (all|open|closed)",
DefaultText: "open",
}
// PaginationPageFlag provides flag for pagination options
var PaginationPageFlag = cli.StringFlag{
Name: "page",
@ -93,13 +78,6 @@ var AllDefaultFlags = append([]cli.Flag{
&RemoteFlag,
}, LoginOutputFlags...)
// IssuePRFlags defines flags that should be available on issue & pr listing flags.
var IssuePRFlags = append([]cli.Flag{
&StateFlag,
&PaginationPageFlag,
&PaginationLimitFlag,
}, AllDefaultFlags...)
// NotificationFlags defines flags that should be available on notifications.
var NotificationFlags = append([]cli.Flag{
NotificationStateFlag,
@ -121,82 +99,6 @@ var NotificationStateFlag = NewCsvFlag(
[]string{"unread", "pinned"},
)
// IssuePREditFlags defines flags for properties of issues and PRs
var IssuePREditFlags = append([]cli.Flag{
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
},
&cli.StringFlag{
Name: "assignees",
Aliases: []string{"a"},
Usage: "Comma-separated list of usernames to assign",
},
&cli.StringFlag{
Name: "labels",
Aliases: []string{"L"},
Usage: "Comma-separated list of labels to assign",
},
&cli.StringFlag{
Name: "deadline",
Aliases: []string{"D"},
Usage: "Deadline timestamp to assign",
},
&cli.StringFlag{
Name: "milestone",
Aliases: []string{"m"},
Usage: "Milestone to assign",
},
}, LoginRepoFlags...)
// GetIssuePREditFlags parses all IssuePREditFlags
func GetIssuePREditFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) {
opts := gitea.CreateIssueOption{
Title: ctx.String("title"),
Body: ctx.String("description"),
Assignees: strings.Split(ctx.String("assignees"), ","),
}
var err error
date := ctx.String("deadline")
if date != "" {
t, err := dateparse.ParseAny(date)
if err != nil {
return nil, err
}
opts.Deadline = &t
}
client := ctx.Login.Client()
labelNames := strings.Split(ctx.String("labels"), ",")
if len(labelNames) != 0 {
if client == nil {
client = ctx.Login.Client()
}
if opts.Labels, err = task.ResolveLabelNames(client, ctx.Owner, ctx.Repo, labelNames); err != nil {
return nil, err
}
}
if milestoneName := ctx.String("milestone"); len(milestoneName) != 0 {
if client == nil {
client = ctx.Login.Client()
}
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
if err != nil {
return nil, fmt.Errorf("Milestone '%s' not found", milestoneName)
}
opts.Milestone = ms.ID
}
return &opts, nil
}
// FieldsFlag generates a flag selecting printable fields.
// To retrieve the value, use f.GetValues()
func FieldsFlag(availableFields, defaultFields []string) *CsvFlag {

161
cmd/flags/issue_pr.go Normal file
View File

@ -0,0 +1,161 @@
// Copyright 2019 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 flags
import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
// StateFlag provides flag to specify issue/pr state, defaulting to "open"
var StateFlag = cli.StringFlag{
Name: "state",
Usage: "Filter by state (all|open|closed)",
DefaultText: "open",
}
// MilestoneFilterFlag is a CSV flag used to filter issues by milestones
var MilestoneFilterFlag = NewCsvFlag(
"milestones",
"milestones to match issues against",
[]string{"m"}, nil, nil)
// LabelFilterFlag is a CSV flag used to filter issues by labels
var LabelFilterFlag = NewCsvFlag(
"labels",
"labels to match issues against",
[]string{"L"}, nil, nil)
// PRListingFlags defines flags that should be available on pr listing flags.
var PRListingFlags = append([]cli.Flag{
&StateFlag,
&PaginationPageFlag,
&PaginationLimitFlag,
}, AllDefaultFlags...)
// IssueListingFlags defines flags that should be available on issue listing flags.
var IssueListingFlags = append([]cli.Flag{
&StateFlag,
&cli.StringFlag{
Name: "kind",
Aliases: []string{"K"},
Usage: "Wether to return `issues`, `pulls`, or `all` (you can use this to apply advanced search filters to PRs)",
DefaultText: "issues",
},
&cli.StringFlag{
Name: "keyword",
Aliases: []string{"k"},
Usage: "Filter by search string",
},
LabelFilterFlag,
MilestoneFilterFlag,
&cli.StringFlag{
Name: "author",
Aliases: []string{"A"},
},
&cli.StringFlag{
Name: "assignee",
Aliases: []string{"a"},
},
&cli.StringFlag{
Name: "mentions",
Aliases: []string{"M"},
},
&cli.StringFlag{
Name: "from",
Aliases: []string{"F"},
Usage: "Filter by activity after this date",
},
&cli.StringFlag{
Name: "until",
Aliases: []string{"u"},
Usage: "Filter by activity before this date",
},
&PaginationPageFlag,
&PaginationLimitFlag,
}, AllDefaultFlags...)
// IssuePREditFlags defines flags for properties of issues and PRs
var IssuePREditFlags = append([]cli.Flag{
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
},
&cli.StringFlag{
Name: "assignees",
Aliases: []string{"a"},
Usage: "Comma-separated list of usernames to assign",
},
&cli.StringFlag{
Name: "labels",
Aliases: []string{"L"},
Usage: "Comma-separated list of labels to assign",
},
&cli.StringFlag{
Name: "deadline",
Aliases: []string{"D"},
Usage: "Deadline timestamp to assign",
},
&cli.StringFlag{
Name: "milestone",
Aliases: []string{"m"},
Usage: "Milestone to assign",
},
}, LoginRepoFlags...)
// GetIssuePREditFlags parses all IssuePREditFlags
func GetIssuePREditFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) {
opts := gitea.CreateIssueOption{
Title: ctx.String("title"),
Body: ctx.String("description"),
Assignees: strings.Split(ctx.String("assignees"), ","),
}
var err error
date := ctx.String("deadline")
if date != "" {
t, err := dateparse.ParseAny(date)
if err != nil {
return nil, err
}
opts.Deadline = &t
}
client := ctx.Login.Client()
labelNames := strings.Split(ctx.String("labels"), ",")
if len(labelNames) != 0 {
if client == nil {
client = ctx.Login.Client()
}
if opts.Labels, err = task.ResolveLabelNames(client, ctx.Owner, ctx.Repo, labelNames); err != nil {
return nil, err
}
}
if milestoneName := ctx.String("milestone"); len(milestoneName) != 0 {
if client == nil {
client = ctx.Login.Client()
}
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
if err != nil {
return nil, fmt.Errorf("Milestone '%s' not found", milestoneName)
}
opts.Milestone = ms.ID
}
return &opts, nil
}

View File

@ -5,11 +5,15 @@
package issues
import (
"fmt"
"time"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
@ -24,7 +28,7 @@ var CmdIssuesList = cli.Command{
Usage: "List issues of the repository",
Description: `List issues of the repository`,
Action: RunIssuesList,
Flags: append([]cli.Flag{issueFieldsFlag}, flags.IssuePRFlags...),
Flags: append([]cli.Flag{issueFieldsFlag}, flags.IssueListingFlags...),
}
// RunIssuesList list issues
@ -36,16 +40,57 @@ func RunIssuesList(cmd *cli.Context) error {
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "open":
case "", "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
default:
return fmt.Errorf("unknown state '%s'", ctx.String("state"))
}
kind := gitea.IssueTypeIssue
switch ctx.String("kind") {
case "", "issues", "issue":
kind = gitea.IssueTypeIssue
case "pulls", "pull", "pr":
kind = gitea.IssueTypePull
case "all":
kind = gitea.IssueTypeAll
default:
return fmt.Errorf("unknown kind '%s'", ctx.String("kind"))
}
var err error
var from, until time.Time
if ctx.IsSet("from") {
from, err = dateparse.ParseLocal(ctx.String("from"))
if err != nil {
return err
}
}
if ctx.IsSet("until") {
until, err = dateparse.ParseLocal(ctx.String("until"))
if err != nil {
return err
}
}
// ignore error, as we don't do any input validation on these flags
labels, _ := flags.LabelFilterFlag.GetValues(cmd)
milestones, _ := flags.MilestoneFilterFlag.GetValues(cmd)
issues, _, err := ctx.Login.Client().ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(),
State: state,
Type: gitea.IssueTypeIssue,
Type: kind,
KeyWord: ctx.String("keyword"),
CreatedBy: ctx.String("author"),
AssignedBy: ctx.String("assigned-to"),
MentionedBy: ctx.String("mentions"),
Labels: labels,
Milestones: milestones,
Since: from,
Before: until,
})
if err != nil {

View File

@ -18,17 +18,17 @@ var CmdPullsCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a pull-request",
Description: "Create a pull-request",
Description: "Create a pull-request in the current repo",
Action: runPullsCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "head",
Usage: "Set head branch (default is current one)",
Usage: "Branch name of the PR source (default is current one). To specify a different head repo, use <user>:<branch>",
},
&cli.StringFlag{
Name: "base",
Aliases: []string{"b"},
Usage: "Set base branch (default is default branch)",
Usage: "Branch name of the PR target (default is repos default branch)",
},
}, flags.IssuePREditFlags...),
}

View File

@ -24,7 +24,7 @@ var CmdPullsList = cli.Command{
Usage: "List pull requests of the repository",
Description: `List pull requests of the repository`,
Action: RunPullsList,
Flags: append([]cli.Flag{pullFieldsFlag}, flags.IssuePRFlags...),
Flags: append([]cli.Flag{pullFieldsFlag}, flags.PRListingFlags...),
}
// RunPullsList return list of pulls

View File

@ -27,11 +27,11 @@ var CmdReleaseCreate = cli.Command{
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "tag",
Usage: "Tag name",
Usage: "Tag name. If the tag does not exist yet, it will be created by Gitea",
},
&cli.StringFlag{
Name: "target",
Usage: "Target refs, branch name or commit id",
Usage: "Target branch name or commit hash. Defaults to the default branch of the repo",
},
&cli.StringFlag{
Name: "title",
@ -56,7 +56,7 @@ var CmdReleaseCreate = cli.Command{
&cli.StringSliceFlag{
Name: "asset",
Aliases: []string{"a"},
Usage: "List of files to attach",
Usage: "Path to file attachment. Can be specified multiple times",
},
}, flags.AllDefaultFlags...),
}

View File

@ -27,6 +27,7 @@ var CmdRepos = cli.Command{
&repos.CmdReposList,
&repos.CmdReposSearch,
&repos.CmdRepoCreate,
&repos.CmdRepoCreateFromTemplate,
&repos.CmdRepoFork,
},
Flags: repos.CmdReposListFlags,

View File

@ -0,0 +1,121 @@
// Copyright 2021 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/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdRepoCreateFromTemplate represents a sub command of repos to generate one from a template repo
var CmdRepoCreateFromTemplate = cli.Command{
Name: "create-from-template",
Aliases: []string{"ct"},
Usage: "Create a repository based on an existing template",
Description: "Create a repository based on an existing template",
Action: runRepoCreateFromTemplate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "template",
Aliases: []string{"t"},
Required: true,
Usage: "source template to copy from",
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Required: true,
Usage: "name of new repo",
},
&cli.StringFlag{
Name: "owner",
Aliases: []string{"O"},
Usage: "name of repo owner",
},
&cli.BoolFlag{
Name: "private",
Usage: "make new repo private",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"desc"},
Usage: "add custom description to repo",
},
&cli.BoolFlag{
Name: "content",
Value: true,
Usage: "copy git content from template",
},
&cli.BoolFlag{
Name: "githooks",
Value: true,
Usage: "copy git hooks from template",
},
&cli.BoolFlag{
Name: "avatar",
Value: true,
Usage: "copy repo avatar from template",
},
&cli.BoolFlag{
Name: "labels",
Value: true,
Usage: "copy repo labels from template",
},
&cli.BoolFlag{
Name: "topics",
Value: true,
Usage: "copy topics from template",
},
&cli.BoolFlag{
Name: "webhooks",
Usage: "copy webhooks from template",
},
}, flags.LoginOutputFlags...),
}
func runRepoCreateFromTemplate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
templateOwner, templateRepo := utils.GetOwnerAndRepo(ctx.String("template"), ctx.Login.User)
owner := ctx.Login.User
if ctx.IsSet("owner") {
owner = ctx.String("owner")
}
opts := gitea.CreateRepoFromTemplateOption{
Name: ctx.String("name"),
Owner: owner,
Description: ctx.String("description"),
Private: ctx.Bool("private"),
GitContent: ctx.Bool("content"),
GitHooks: ctx.Bool("githooks"),
Avatar: ctx.Bool("avatar"),
Labels: ctx.Bool("labels"),
Topics: ctx.Bool("topics"),
Webhooks: ctx.Bool("webhooks"),
}
repo, _, err := client.CreateRepoFromTemplate(templateOwner, templateRepo, 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
}

2
go.mod
View File

@ -37,5 +37,3 @@ require (
golang.org/x/tools v0.1.5 // indirect
gopkg.in/yaml.v2 v2.4.0
)
replace github.com/charmbracelet/glamour => github.com/noerw/glamour v0.3.0-patch

4
go.sum
View File

@ -39,6 +39,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/glamour v0.3.0 h1:3H+ZrKlSg8s+WU6V7eF2eRVYt8lCueffbi7r2+ffGkc=
github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@ -129,8 +131,6 @@ github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/f
github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/noerw/glamour v0.3.0-patch h1:yc3wdbUIySok6KYeX5BtWnlj+PvP1uYeCeTSwq2rtSw=
github.com/noerw/glamour v0.3.0-patch/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

12
main.go
View File

@ -49,6 +49,7 @@ func main() {
&cmd.CmdOpen,
&cmd.CmdNotifications,
&cmd.CmdRepoClone,
}
app.EnableBashCompletion = true
err := app.Run(os.Args)
@ -68,10 +69,13 @@ func formatBuiltWith(Tags string) string {
return " built with: " + strings.Replace(Tags, " ", ", ", -1)
}
var appDescription = `tea is a productivity helper for Gitea. It can be used to manage most entities on one
or multiple Gitea instances and provides local helpers like 'tea pull checkout'.
tea makes use of context provided by the repository in $PWD if available, but is still
usable independently of $PWD. Configuration is persisted in $XDG_CONFIG_HOME/tea.
var appDescription = `tea is a productivity helper for Gitea. It can be used to manage most entities on
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
tea tries to make use of context provided by the repository in $PWD if available.
tea works best in a upstream/fork workflow, when the local main branch tracks the
upstream repo. tea assumes that local git state is published on the remote before
doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
`
var helpTemplate = bold(`

View File

@ -111,6 +111,25 @@ func GetLoginByToken(token string) *Login {
return nil
}
// GetLoginByHost finds a login by it's server URL
func GetLoginByHost(host string) *Login {
err := loadConfig()
if err != nil {
log.Fatal(err)
}
for _, l := range config.Logins {
loginURL, err := url.Parse(l.URL)
if err != nil {
log.Fatal(err)
}
if loginURL.Host == host {
return &l
}
}
return nil
}
// DeleteLogin delete a login by name from config
func DeleteLogin(name string) error {
var idx = -1

View File

@ -22,13 +22,18 @@ type URLParser struct {
func (p *URLParser) Parse(rawURL string) (u *url.URL, err error) {
rawURL = strings.TrimSpace(rawURL)
// convert the weird git ssh url format to a canonical url:
// git@gitea.com:gitea/tea -> ssh://git@gitea.com/gitea/tea
if !protocolRe.MatchString(rawURL) &&
strings.Contains(rawURL, ":") &&
// not a Windows path
!strings.Contains(rawURL, "\\") {
rawURL = "ssh://" + strings.Replace(rawURL, ":", "/", 1)
if !protocolRe.MatchString(rawURL) {
// convert the weird git ssh url format to a canonical url:
// git@gitea.com:gitea/tea -> ssh://git@gitea.com/gitea/tea
if strings.Contains(rawURL, ":") &&
// not a Windows path
!strings.Contains(rawURL, "\\") {
rawURL = "ssh://" + strings.Replace(rawURL, ":", "/", 1)
} else if !strings.Contains(rawURL, "@") &&
strings.Count(rawURL, "/") == 2 {
// match cases like gitea.com/gitea/tea
rawURL = "https://" + rawURL
}
}
u, err = url.Parse(rawURL)

56
modules/git/url_test.go Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2019 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 git
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseUrl(t *testing.T) {
u, err := ParseURL("ssh://git@gitea.com:3000/gitea/tea")
assert.NoError(t, err)
assert.Equal(t, "gitea.com:3000", u.Host)
assert.Equal(t, "ssh", u.Scheme)
assert.Equal(t, "/gitea/tea", u.Path)
u, err = ParseURL("https://gitea.com/gitea/tea")
assert.NoError(t, err)
assert.Equal(t, "gitea.com", u.Host)
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "/gitea/tea", u.Path)
u, err = ParseURL("git@gitea.com:gitea/tea")
assert.NoError(t, err)
assert.Equal(t, "gitea.com", u.Host)
assert.Equal(t, "ssh", u.Scheme)
assert.Equal(t, "/gitea/tea", u.Path)
u, err = ParseURL("gitea.com/gitea/tea")
assert.NoError(t, err)
assert.Equal(t, "gitea.com", u.Host)
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "/gitea/tea", u.Path)
u, err = ParseURL("foo/bar")
assert.NoError(t, err)
assert.Equal(t, "", u.Host)
assert.Equal(t, "", u.Scheme)
assert.Equal(t, "foo/bar", u.Path)
u, err = ParseURL("/foo/bar")
assert.NoError(t, err)
assert.Equal(t, "", u.Host)
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "/foo/bar", u.Path)
// this case is unintuitive, but to ambiguous to be handled differently
u, err = ParseURL("gitea.com")
assert.NoError(t, err)
assert.Equal(t, "", u.Host)
assert.Equal(t, "", u.Scheme)
assert.Equal(t, "gitea.com", u.Path)
}

View File

@ -15,7 +15,7 @@ import (
func Comments(comments []*gitea.Comment) {
var baseURL string
if len(comments) != 0 {
baseURL = comments[0].HTMLURL
baseURL = getRepoURL(comments[0].HTMLURL)
}
var out = make([]string, len(comments))
@ -32,7 +32,7 @@ func Comments(comments []*gitea.Comment) {
// Comment renders a comment to stdout
func Comment(c *gitea.Comment) {
outputMarkdown(formatComment(c), c.HTMLURL)
outputMarkdown(formatComment(c), getRepoURL(c.HTMLURL))
}
func formatComment(c *gitea.Comment) string {

View File

@ -6,12 +6,20 @@ package print
import (
"fmt"
"regexp"
"time"
"code.gitea.io/sdk/gitea"
"github.com/muesli/termenv"
)
// captures the repo URL part <host>/<owner>/<repo> of an url
var repoURLRegex = regexp.MustCompile("^([[:alnum:]]+://[^/]+(?:/[[:alnum:]]+){2})/.*")
func getRepoURL(resourceURL string) string {
return repoURLRegex.ReplaceAllString(resourceURL, "$1/")
}
// formatSize get kb in int and return string
func formatSize(kb int64) string {
if kb < 1024 {

View File

@ -28,7 +28,7 @@ func IssueDetails(issue *gitea.Issue, reactions []*gitea.Reaction) {
out += fmt.Sprintf("\n---\n\n%s\n", formatReactions(reactions))
}
outputMarkdown(out, issue.HTMLURL)
outputMarkdown(out, getRepoURL(issue.HTMLURL))
}
func formatReactions(reactions []*gitea.Reaction) string {

View File

@ -64,7 +64,7 @@ func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *g
}
}
outputMarkdown(out, pr.HTMLURL)
outputMarkdown(out, getRepoURL(pr.HTMLURL))
}
func formatPRHead(pr *gitea.PullRequest) string {

View File

@ -108,7 +108,7 @@ func GetDefaultPRHead(localRepo *local_git.TeaRepo) (owner, branch string, err e
if err != nil {
return
}
owner, _ = utils.GetOwnerAndRepo(strings.TrimLeft(url.Path, "/"), "")
owner, _ = utils.GetOwnerAndRepo(url.Path, "")
return
}

View File

@ -0,0 +1,93 @@
// Copyright 2021 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 task
import (
"fmt"
"net/url"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
local_git "code.gitea.io/tea/modules/git"
"github.com/go-git/go-git/v5"
git_config "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
)
// RepoClone creates a local git clone in the given path, and sets up upstream remote
// for fork repos, for good usability with tea.
func RepoClone(
path string,
login *config.Login,
repoOwner, repoName string,
callback func(string) (string, error),
depth int,
) (*local_git.TeaRepo, error) {
repoMeta, _, err := login.Client().GetRepo(repoOwner, repoName)
if err != nil {
return nil, err
}
originURL, err := cloneURL(repoMeta, login)
if err != nil {
return nil, err
}
auth, err := local_git.GetAuthForURL(originURL, login.Token, login.SSHKey, callback)
if err != nil {
return nil, err
}
// default path behaviour as native git
if path == "" {
path = repoName
}
repo, err := git.PlainClone(path, false, &git.CloneOptions{
URL: originURL.String(),
Auth: auth,
Depth: depth,
InsecureSkipTLS: login.Insecure,
})
if err != nil {
return nil, err
}
// set up upstream remote for forks
if repoMeta.Fork && repoMeta.Parent != nil {
upstreamURL, err := cloneURL(repoMeta.Parent, login)
if err != nil {
return nil, err
}
upstreamBranch := repoMeta.Parent.DefaultBranch
repo.CreateRemote(&git_config.RemoteConfig{
Name: "upstream",
URLs: []string{upstreamURL.String()},
})
repoConf, err := repo.Config()
if err != nil {
return nil, err
}
if b, ok := repoConf.Branches[upstreamBranch]; ok {
b.Remote = "upstream"
b.Merge = plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", upstreamBranch))
}
if err = repo.SetConfig(repoConf); err != nil {
return nil, err
}
}
return &local_git.TeaRepo{Repository: repo}, nil
}
func cloneURL(repo *gitea.Repository, login *config.Login) (*url.URL, error) {
urlStr := repo.CloneURL
if login.SSHKey != "" {
urlStr = repo.SSHURL
}
return local_git.ParseURL(urlStr)
}

View File

@ -33,7 +33,7 @@ func GetOwnerAndRepo(repoPath, user string) (string, string) {
if len(repoPath) == 0 {
return "", ""
}
p := strings.Split(repoPath, "/")
p := strings.Split(strings.TrimLeft(repoPath, "/"), "/")
if len(p) >= 2 {
return p[0], p[1]
}

View File

@ -6,10 +6,10 @@
<a href="https://pkg.go.dev/github.com/charmbracelet/glamour?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/glamour/actions"><img src="https://github.com/charmbracelet/glamour/workflows/build/badge.svg" alt="Build Status"></a>
<a href="https://coveralls.io/github/charmbracelet/glamour?branch=master"><img src="https://coveralls.io/repos/github/charmbracelet/glamour/badge.svg?branch=master" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/charmbracelet/glamour"><img src="https://goreportcard.com/badge/charmbracelet/glamour" alt="Go ReportCard"></a>
<a href="http://goreportcard.com/report/charmbracelet/glamour"><img src="http://goreportcard.com/badge/charmbracelet/glamour" alt="Go ReportCard"></a>
</p>
Stylesheet-based markdown rendering for your CLI apps.
Write handsome command-line tools with *Glamour*.
![Glamour dark style example](https://stuff.charm.sh/glamour/glamour-example.png)

View File

@ -129,9 +129,6 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
}
if node.Parent().(*ast.List).IsOrdered() {
e = l
if node.Parent().(*ast.List).Start != 1 {
e += uint(node.Parent().(*ast.List).Start) - 1
}
}
post := "\n"
@ -156,7 +153,6 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
return Element{
Exiting: post,
Renderer: &ItemElement{
IsOrdered: node.Parent().(*ast.List).IsOrdered(),
Enumeration: e,
},
}

View File

@ -25,7 +25,7 @@ func (e *ImageElement) Render(w io.Writer, ctx RenderContext) error {
}
if len(e.URL) > 0 {
el := &BaseElement{
Token: resolveURL(e.BaseURL, e.URL),
Token: resolveRelativeURL(e.BaseURL, e.URL),
Prefix: " ",
Style: ctx.options.Styles.Image,
}

View File

@ -64,7 +64,7 @@ func (e *LinkElement) Render(w io.Writer, ctx RenderContext) error {
}
el := &BaseElement{
Token: resolveURL(e.BaseURL, e.URL),
Token: resolveRelativeURL(e.BaseURL, e.URL),
Prefix: pre,
Style: style,
}

View File

@ -7,13 +7,12 @@ import (
// An ItemElement is used to render items inside a list.
type ItemElement struct {
IsOrdered bool
Enumeration uint
}
func (e *ItemElement) Render(w io.Writer, ctx RenderContext) error {
var el *BaseElement
if e.IsOrdered {
if e.Enumeration > 0 {
el = &BaseElement{
Style: ctx.options.Styles.Enumeration,
Prefix: strconv.FormatInt(int64(e.Enumeration), 10),

View File

@ -38,7 +38,7 @@ func (e *ParagraphElement) Finish(w io.Writer, ctx RenderContext) error {
mw := NewMarginWriter(ctx, w, rules)
if len(strings.TrimSpace(bs.Current().Block.String())) > 0 {
flow := wordwrap.NewWriter(int(bs.Width(ctx)))
flow.KeepNewlines = ctx.options.PreserveNewLines
flow.KeepNewlines = false
_, _ = flow.Write(bs.Current().Block.Bytes())
flow.Close()

View File

@ -3,6 +3,7 @@ package ansi
import (
"io"
"net/url"
"strings"
"github.com/muesli/termenv"
east "github.com/yuin/goldmark-emoji/ast"
@ -14,11 +15,10 @@ import (
// Options is used to configure an ANSIRenderer.
type Options struct {
BaseURL string
WordWrap int
PreserveNewLines bool
ColorProfile termenv.Profile
Styles StyleConfig
BaseURL string
WordWrap int
ColorProfile termenv.Profile
Styles StyleConfig
}
// ANSIRenderer renders markdown content as ANSI escaped sequences.
@ -149,7 +149,7 @@ func isChild(node ast.Node) bool {
return false
}
func resolveURL(baseURL string, rel string) string {
func resolveRelativeURL(baseURL string, rel string) string {
u, err := url.Parse(rel)
if err != nil {
return rel
@ -157,6 +157,7 @@ func resolveURL(baseURL string, rel string) string {
if u.IsAbs() {
return rel
}
u.Path = strings.TrimPrefix(u.Path, "/")
base, err := url.Parse(baseURL)
if err != nil {

View File

@ -185,14 +185,6 @@ func WithWordWrap(wordWrap int) TermRendererOption {
}
}
// WithWordWrap sets a TermRenderer's word wrap.
func WithPreservedNewLines() TermRendererOption {
return func(tr *TermRenderer) error {
tr.ansiOptions.PreserveNewLines = true
return nil
}
}
// WithEmoji sets a TermRenderer's emoji rendering.
func WithEmoji() TermRendererOption {
return func(tr *TermRenderer) error {

2
vendor/modules.txt vendored
View File

@ -74,7 +74,7 @@ github.com/araddon/dateparse
# github.com/aymerick/douceur v0.2.0
github.com/aymerick/douceur/css
github.com/aymerick/douceur/parser
# github.com/charmbracelet/glamour v0.3.0 => github.com/noerw/glamour v0.3.0-patch
# github.com/charmbracelet/glamour v0.3.0
github.com/charmbracelet/glamour
github.com/charmbracelet/glamour/ansi
# github.com/cpuguy83/go-md2man/v2 v2.0.1