mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
refactor: load settings once per run and reuse shared helpers
The application caches the settings snapshot and reloads it only after an update through the store, so output and prompt helpers no longer read and parse config.yml and hosts.yml several times per command. canPrompt and chooser replace the inline prompt-policy checks. Checks are tallied by one services.Tally, terminal.Plural replaces the two app copies, refKind.article/noun replace the comment group's own words, list commands use servicePage, and web URLs are built by repositoryURL and webURL in browser.go. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
42 files changed
+157
-204
No files matched your search
@@ -34,7 +34,7 @@ $ tea run jobs 34 --json id,conclusion --jq '.[] | select(.conclusion == "failur
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
spec := listSpec[services.Job]{Columns: jobColumns, Row: jobRow(a.deps.Now()), Empty: fmt.Sprintf("no jobs in run %d", run), Context: fmt.Sprintf("jobs in run %d", run)}
|
spec := listSpec[services.Job]{Columns: jobColumns, Row: jobRow(a.deps.Now()), Empty: fmt.Sprintf("no jobs in run %d", run), Context: fmt.Sprintf("jobs in run %d", run)}
|
||||||
return writeTable(a, cmd, jobs.Items, spec, listPage{Number: page, More: jobs.More, Total: jobs.Total, Counted: jobs.Counted})
|
return writeTable(a, cmd, jobs.Items, spec, servicePage(jobs, page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pageFlags(cmd, &limit, &page)
|
pageFlags(cmd, &limit, &page)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ $ tea run list --since 24h --json id,status,conclusion,workflow`,
|
|||||||
empty = "no runs match your filters in " + t.Repo
|
empty = "no runs match your filters in " + t.Repo
|
||||||
}
|
}
|
||||||
spec := listSpec[services.Run]{Columns: runColumns, Row: runRow(a.deps.Now()), Empty: empty, Context: "runs in " + t.Repo}
|
spec := listSpec[services.Run]{Columns: runColumns, Row: runRow(a.deps.Now()), Empty: empty, Context: "runs in " + t.Repo}
|
||||||
return writeTable(a, cmd, page.Items, spec, listPage{Number: filters.Page, More: page.More, Total: page.Total, Counted: page.Counted})
|
return writeTable(a, cmd, page.Items, spec, servicePage(page, filters.Page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pageFlags(cmd, &filters.Limit, &filters.Page)
|
pageFlags(cmd, &filters.Limit, &filters.Page)
|
||||||
@@ -202,7 +202,7 @@ $ tea run view 34 --json jobs --jq '.jobs[] | {name, conclusion}'`,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if web {
|
if web {
|
||||||
return a.openBrowser(cmd, fmt.Sprintf("%s/%s/actions/runs/%d", strings.TrimSuffix(t.Host, "/"), t.Repo, id))
|
return a.openBrowser(cmd, fmt.Sprintf("%s/actions/runs/%d", repositoryURL(t), id))
|
||||||
}
|
}
|
||||||
service := services.Actions{Client: t.client, Repository: t.Repo}
|
service := services.Actions{Client: t.client, Repository: t.Repo}
|
||||||
if log {
|
if log {
|
||||||
@@ -413,7 +413,7 @@ func (a *application) runArg(cmd *cobra.Command, args []string, active bool) (ta
|
|||||||
return a.resolveRef(cmd, args[0], runRef)
|
return a.resolveRef(cmd, args[0], runRef)
|
||||||
}
|
}
|
||||||
missing := clierr.Wrap(clierr.Usage, &argumentError{problem: "missing run ID", cmd: cmd})
|
missing := clierr.Wrap(clierr.Usage, &argumentError{problem: "missing run ID", cmd: cmd})
|
||||||
chooser, ok := a.runPicker()
|
chooser, ok := a.chooser()
|
||||||
if !ok {
|
if !ok {
|
||||||
return target{}, 0, missing
|
return target{}, 0, missing
|
||||||
}
|
}
|
||||||
@@ -449,16 +449,6 @@ func (a *application) runArg(cmd *cobra.Command, args []string, active bool) (ta
|
|||||||
return t, id, err
|
return t, id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// runPicker returns the prompter's selection prompt when prompts are allowed.
|
|
||||||
func (a *application) runPicker() (terminal.Chooser, bool) {
|
|
||||||
chooser, ok := a.deps.Prompter.(terminal.Chooser)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
s, err := a.deps.Settings.Load()
|
|
||||||
return chooser, err == nil && a.policy(s).CanPrompt(a.deps.Streams)
|
|
||||||
}
|
|
||||||
|
|
||||||
// accountNote reminds people who pass the global --user, which selects the account tea acts as,
|
// accountNote reminds people who pass the global --user, which selects the account tea acts as,
|
||||||
// of the flag they may have meant, as gh's --user means something else.
|
// of the flag they may have meant, as gh's --user means something else.
|
||||||
func (a *application) accountNote(cmd *cobra.Command, t target, hint string) {
|
func (a *application) accountNote(cmd *cobra.Command, t target, hint string) {
|
||||||
|
|||||||
@@ -283,11 +283,7 @@ func (a *application) actionValue(cmd *cobra.Command, secret bool, name, body, b
|
|||||||
}
|
}
|
||||||
return strings.TrimSuffix(strings.TrimSuffix(string(data), "\n"), "\r"), nil
|
return strings.TrimSuffix(strings.TrimSuffix(string(data), "\n"), "\r"), nil
|
||||||
}
|
}
|
||||||
s, err := a.deps.Settings.Load()
|
if !a.canPrompt() {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !a.policy(s).CanPrompt(a.deps.Streams) {
|
|
||||||
return "", missing
|
return "", missing
|
||||||
}
|
}
|
||||||
if secret {
|
if secret {
|
||||||
|
|||||||
@@ -341,11 +341,7 @@ func (a *application) dispatchValues(cmd *cobra.Command, workflow services.Workf
|
|||||||
}
|
}
|
||||||
return nil, clierr.Wrap(clierr.Usage, fmt.Errorf("unknown input %q for %s; inputs: %s", key, workflow.Label(), strings.Join(names, ", ")))
|
return nil, clierr.Wrap(clierr.Usage, fmt.Errorf("unknown input %q for %s; inputs: %s", key, workflow.Label(), strings.Join(names, ", ")))
|
||||||
}
|
}
|
||||||
s, err := a.deps.Settings.Load()
|
canPrompt := a.canPrompt()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
canPrompt := a.policy(s).CanPrompt(a.deps.Streams)
|
|
||||||
for _, input := range declared {
|
for _, input := range declared {
|
||||||
value, given := values[input.Name]
|
value, given := values[input.Name]
|
||||||
missing := input.Required && input.Default == ""
|
missing := input.Required && input.Default == ""
|
||||||
@@ -357,9 +353,11 @@ func (a *application) dispatchValues(cmd *cobra.Command, workflow services.Workf
|
|||||||
}
|
}
|
||||||
values[input.Name] = normalized
|
values[input.Name] = normalized
|
||||||
case canPrompt && (askAll || missing):
|
case canPrompt && (askAll || missing):
|
||||||
if values[input.Name], err = a.askInput(cmd, input); err != nil {
|
answer, err := a.askInput(cmd, input)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
values[input.Name] = answer
|
||||||
case missing:
|
case missing:
|
||||||
return nil, clierr.Wrap(clierr.Usage, fmt.Errorf("missing required input %s for %s; pass -f %s=<value>", input.Name, workflow.Label(), input.Name))
|
return nil, clierr.Wrap(clierr.Usage, fmt.Errorf("missing required input %s for %s; pass -f %s=<value>", input.Name, workflow.Label(), input.Name))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,11 +292,7 @@ func (a *application) adminUserPassword(cmd *cobra.Command, username, password,
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if password == "" && !cmd.Flags().Changed("password-file") {
|
if password == "" && !cmd.Flags().Changed("password-file") {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
if !a.canPrompt() {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !a.policy(snapshot).CanPrompt(a.deps.Streams) {
|
|
||||||
return "", clierr.Wrap(clierr.Usage, errors.New("missing password: supply --password-file or --password"))
|
return "", clierr.Wrap(clierr.Usage, errors.New("missing password: supply --password-file or --password"))
|
||||||
}
|
}
|
||||||
if password, err = a.deps.Prompter.SecretInput(cmd.Context(), "Password for "+username); err != nil {
|
if password, err = a.deps.Prompter.SecretInput(cmd.Context(), "Password for "+username); err != nil {
|
||||||
|
|||||||
+23
-1
@@ -14,6 +14,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -71,6 +72,27 @@ type options struct {
|
|||||||
type application struct {
|
type application struct {
|
||||||
deps Dependencies
|
deps Dependencies
|
||||||
options options
|
options options
|
||||||
|
|
||||||
|
// snapshot caches the settings for one run; generation is the store's generation it was loaded at.
|
||||||
|
mu sync.Mutex
|
||||||
|
snapshot *settings.Snapshot
|
||||||
|
generation uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// settings returns the settings, loaded once per run and again after tea changed them.
|
||||||
|
func (a *application) settings() (settings.Snapshot, error) {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
generation := a.deps.Settings.Generation()
|
||||||
|
if a.snapshot != nil && a.generation == generation {
|
||||||
|
return *a.snapshot, nil
|
||||||
|
}
|
||||||
|
snapshot, err := a.deps.Settings.Load()
|
||||||
|
if err != nil {
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
a.snapshot, a.generation = &snapshot, generation
|
||||||
|
return snapshot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRoot builds fresh commands. Construction performs no disk, network, or Git IO.
|
// NewRoot builds fresh commands. Construction performs no disk, network, or Git IO.
|
||||||
@@ -144,7 +166,7 @@ func NewRoot(deps Dependencies) *cobra.Command {
|
|||||||
if offline(cmd) {
|
if offline(cmd) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ $ tea run artifact list 34`,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
spec := listSpec[services.Artifact]{Columns: artifactColumns, Row: artifactRow, Empty: fmt.Sprintf("no artifacts in run %d", run), Context: fmt.Sprintf("artifacts of run %d", run)}
|
spec := listSpec[services.Artifact]{Columns: artifactColumns, Row: artifactRow, Empty: fmt.Sprintf("no artifacts in run %d", run), Context: fmt.Sprintf("artifacts of run %d", run)}
|
||||||
return writeTable(a, cmd, items.Items, spec, listPage{Number: page, More: items.More, Total: items.Total, Counted: items.Counted})
|
return writeTable(a, cmd, items.Items, spec, servicePage(items, page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pageFlags(list, &limit, &page)
|
pageFlags(list, &limit, &page)
|
||||||
|
|||||||
+7
-11
@@ -161,7 +161,7 @@ $ tea auth login --host gitea.com --ssh-key ~/.ssh/id_ed25519`,
|
|||||||
case selected == 1 && !oauth && cmd.Flags().Changed("scopes"):
|
case selected == 1 && !oauth && cmd.Flags().Changed("scopes"):
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--scopes requires --oauth"))
|
return clierr.Wrap(clierr.Usage, errors.New("--scopes requires --oauth"))
|
||||||
}
|
}
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -321,7 +321,7 @@ $ tea auth login --host gitea.com --ssh-key ~/.ssh/id_ed25519`,
|
|||||||
// confirmReplace asks before a login replaces the stored credential of an account that tea already
|
// confirmReplace asks before a login replaces the stored credential of an account that tea already
|
||||||
// has, and reports whether it does.
|
// has, and reports whether it does.
|
||||||
func (a *application) confirmReplace(cmd *cobra.Command, instance, user string) (bool, error) {
|
func (a *application) confirmReplace(cmd *cobra.Command, instance, user string) (bool, error) {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -437,7 +437,7 @@ credential is missing or rejected.`,
|
|||||||
$ tea auth status --verify
|
$ tea auth status --verify
|
||||||
$ tea auth status --host gitea.com --json`,
|
$ tea auth status --host gitea.com --json`,
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
s, err := a.deps.Settings.Load()
|
s, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -708,12 +708,8 @@ asks which one on a terminal. The instance is the one of the current repository,
|
|||||||
|
|
||||||
// chooseAccount asks which of the accounts of instance to use, or explains how to name one.
|
// chooseAccount asks which of the accounts of instance to use, or explains how to name one.
|
||||||
func (a *application) chooseAccount(cmd *cobra.Command, instance string, names []string, active string) (string, error) {
|
func (a *application) chooseAccount(cmd *cobra.Command, instance string, names []string, active string) (string, error) {
|
||||||
s, err := a.deps.Settings.Load()
|
chooser, ok := a.chooser()
|
||||||
if err != nil {
|
if !ok {
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
chooser, ok := a.deps.Prompter.(terminal.Chooser)
|
|
||||||
if !ok || !a.policy(s).CanPrompt(a.deps.Streams) {
|
|
||||||
return "", clierr.Wrap(clierr.Usage, fmt.Errorf("name the account: tea auth switch <user> (accounts on %s: %s)", instance, strings.Join(names, ", ")))
|
return "", clierr.Wrap(clierr.Usage, fmt.Errorf("name the account: tea auth switch <user> (accounts on %s: %s)", instance, strings.Join(names, ", ")))
|
||||||
}
|
}
|
||||||
choices := make([]terminal.Choice, len(names))
|
choices := make([]terminal.Choice, len(names))
|
||||||
@@ -774,7 +770,7 @@ instance.`,
|
|||||||
a.warn(cmd, "Could not ask git's credential helpers to forget %s on %s", t.User, t.Host)
|
a.warn(cmd, "Could not ask git's credential helpers to forget %s on %s", t.User, t.Host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s, err := a.deps.Settings.Load(); err == nil {
|
if s, err := a.settings(); err == nil {
|
||||||
if active := s.Hosts.Hosts[t.Host].User; active != "" && active != t.host.User {
|
if active := s.Hosts.Hosts[t.Host].User; active != "" && active != t.host.User {
|
||||||
a.done(cmd, "Switched active account for %s to %s", t.Host, active)
|
a.done(cmd, "Switched active account for %s to %s", t.Host, active)
|
||||||
}
|
}
|
||||||
@@ -833,7 +829,7 @@ $ tea auth token --host gitea.com --user alice`,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s, err := a.deps.Settings.Load()
|
s, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ $ tea branch list -R alice/project --limit 100 --json name,sha`,
|
|||||||
},
|
},
|
||||||
Context: "branches in " + target.Repo, Empty: "no branches in " + target.Repo,
|
Context: "branches in " + target.Repo, Empty: "no branches in " + target.Repo,
|
||||||
}
|
}
|
||||||
return writeTable(a, cmd, result.Items, spec, listPage{Number: page, More: result.More, Total: result.Total, Counted: result.Counted})
|
return writeTable(a, cmd, result.Items, spec, servicePage(result, page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pageFlags(command, &limit, &page)
|
pageFlags(command, &limit, &page)
|
||||||
@@ -145,7 +145,7 @@ func (a *application) branchViewCommand() *cobra.Command {
|
|||||||
protection := ""
|
protection := ""
|
||||||
if item.Protected {
|
if item.Protected {
|
||||||
badges = append(badges, terminal.Cell{Text: "protected", Color: "yellow"})
|
badges = append(badges, terminal.Cell{Text: "protected", Color: "yellow"})
|
||||||
protection = countNoun(int(item.RequiredApprovals), "required approval")
|
protection = terminal.Plural(int(item.RequiredApprovals), "required approval")
|
||||||
}
|
}
|
||||||
meta := ""
|
meta := ""
|
||||||
if updated := terminal.ParseTime(item.UpdatedAt); !updated.IsZero() {
|
if updated := terminal.ParseTime(item.UpdatedAt); !updated.IsZero() {
|
||||||
@@ -358,7 +358,7 @@ $ tea branch protect --pattern 'release/*' --allow-push acme/release`,
|
|||||||
|
|
||||||
// protectionSummary describes a protection rule, as in "1 approval required, no direct pushes".
|
// protectionSummary describes a protection rule, as in "1 approval required, no direct pushes".
|
||||||
func protectionSummary(rule services.Protection) string {
|
func protectionSummary(rule services.Protection) string {
|
||||||
parts := []string{countNoun(int(rule.RequiredApprovals), "approval") + " required"}
|
parts := []string{terminal.Plural(int(rule.RequiredApprovals), "approval") + " required"}
|
||||||
if pushers := len(rule.PushUsers) + len(rule.PushTeams); pushers > 0 {
|
if pushers := len(rule.PushUsers) + len(rule.PushTeams); pushers > 0 {
|
||||||
parts = append(parts, "pushes by "+strings.Join(append(append([]string{}, rule.PushUsers...), rule.PushTeams...), ", "))
|
parts = append(parts, "pushes by "+strings.Join(append(append([]string{}, rule.PushUsers...), rule.PushTeams...), ", "))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/tea/internal/terminal"
|
"gitea.dev/tea/internal/terminal"
|
||||||
@@ -27,3 +28,17 @@ func (a *application) openBrowser(command *cobra.Command, resource string) error
|
|||||||
func displayURL(url string) string {
|
func displayURL(url string) string {
|
||||||
return terminal.SafeLine(strings.TrimPrefix(strings.TrimPrefix(url, "https://"), "http://"))
|
return terminal.SafeLine(strings.TrimPrefix(strings.TrimPrefix(url, "https://"), "http://"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// webURL is the page of a path on the target's instance, such as an organization's profile.
|
||||||
|
func webURL(t target, path ...string) string {
|
||||||
|
escaped := make([]string, len(path))
|
||||||
|
for i, segment := range path {
|
||||||
|
escaped[i] = url.PathEscape(segment)
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(t.Host, "/") + "/" + strings.Join(escaped, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// repositoryURL returns the web URL of the target's repository.
|
||||||
|
func repositoryURL(t target) string {
|
||||||
|
return strings.TrimSuffix(t.Host, "/") + "/" + t.Repo
|
||||||
|
}
|
||||||
@@ -91,7 +91,7 @@ argument select it, and the branch tracks the head branch, so that git push upda
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-33
@@ -52,7 +52,8 @@ tea exits with 8 while checks are pending and with 1 when a check failed, like g
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
waiting := "checks to be reported"
|
waiting := "checks to be reported"
|
||||||
if len(latest) > 0 {
|
if len(latest) > 0 {
|
||||||
waiting = plural(pendingChecks(latest), "pending check")
|
_, pending, _ := services.Tally(latest)
|
||||||
|
waiting = terminal.Plural(pending, "pending check")
|
||||||
}
|
}
|
||||||
return timeoutError(err, timeout, "waiting for %s", waiting)
|
return timeoutError(err, timeout, "waiting for %s", waiting)
|
||||||
}
|
}
|
||||||
@@ -94,45 +95,18 @@ $ tea pr checks 13 --watch && tea pr merge 13`
|
|||||||
|
|
||||||
// checksOutcome is the exit status of checks: 1 when one failed, 8 while any is pending.
|
// checksOutcome is the exit status of checks: 1 when one failed, 8 while any is pending.
|
||||||
func checksOutcome(items []services.Check) error {
|
func checksOutcome(items []services.Check) error {
|
||||||
pending := false
|
switch failing, pending, _ := services.Tally(items); {
|
||||||
for _, item := range items {
|
case failing > 0:
|
||||||
switch {
|
return clierr.Quiet(clierr.Failure)
|
||||||
case item.Pending():
|
case pending > 0:
|
||||||
pending = true
|
|
||||||
case !item.Passed():
|
|
||||||
return clierr.Quiet(clierr.Failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pending {
|
|
||||||
return clierr.Quiet(clierr.Pending)
|
return clierr.Quiet(clierr.Pending)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pendingChecks counts the checks that have not finished.
|
|
||||||
func pendingChecks(items []services.Check) int {
|
|
||||||
pending := 0
|
|
||||||
for _, item := range items {
|
|
||||||
if item.Pending() {
|
|
||||||
pending++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pending
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkCounts words how the checks stand: "0 failing, 1 successful, and 2 pending checks".
|
// checkCounts words how the checks stand: "0 failing, 1 successful, and 2 pending checks".
|
||||||
func checkCounts(items []services.Check) string {
|
func checkCounts(items []services.Check) string {
|
||||||
var failing, passed, pending int
|
failing, pending, passed := services.Tally(items)
|
||||||
for _, item := range items {
|
|
||||||
switch {
|
|
||||||
case item.Pending():
|
|
||||||
pending++
|
|
||||||
case item.Passed():
|
|
||||||
passed++
|
|
||||||
default:
|
|
||||||
failing++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d failing, %d successful, and %d pending checks", failing, passed, pending)
|
return fmt.Sprintf("%d failing, %d successful, and %d pending checks", failing, passed, pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ const commentTitle = "Comment (markdown):"
|
|||||||
|
|
||||||
// commentNouns are the words for the commands of an issue or pull request comment group.
|
// commentNouns are the words for the commands of an issue or pull request comment group.
|
||||||
type commentNouns struct {
|
type commentNouns struct {
|
||||||
group, noun, arguments, example string
|
group, arguments, example string
|
||||||
}
|
}
|
||||||
|
|
||||||
func commentWords(kind refKind) commentNouns {
|
func commentWords(kind refKind) commentNouns {
|
||||||
if kind == pullRef {
|
if kind == pullRef {
|
||||||
return commentNouns{group: "pr", noun: "pull request", arguments: pullNumberArguments, example: "13"}
|
return commentNouns{group: "pr", arguments: pullNumberArguments, example: "13"}
|
||||||
}
|
}
|
||||||
return commentNouns{group: "issue", noun: "issue", arguments: issueArguments, example: "12"}
|
return commentNouns{group: "issue", arguments: issueArguments, example: "12"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pullNumberArguments is the ARGUMENTS help of pr comment and pr attachment commands, which take pull
|
// pullNumberArguments is the ARGUMENTS help of pr comment and pr attachment commands, which take pull
|
||||||
@@ -258,10 +258,10 @@ func (a *application) commentCommand(kind refKind) *cobra.Command {
|
|||||||
words := commentWords(kind)
|
words := commentWords(kind)
|
||||||
var flags commentFlags
|
var flags commentFlags
|
||||||
var editLast, deleteLast bool
|
var editLast, deleteLast bool
|
||||||
group := &cobra.Command{Use: "comment {<number> | <url>}", Short: "Add a comment to " + article(words.noun), Long: fmt.Sprintf(`Add a comment to %[1]s, or edit or delete your last comment on it.
|
group := &cobra.Command{Use: "comment {<number> | <url>}", Short: "Add a comment to " + kind.article(), Long: fmt.Sprintf(`Add a comment to %[1]s, or edit or delete your last comment on it.
|
||||||
|
|
||||||
Without --body, --body-file or --editor, the configured editor opens, or a multi-line prompt
|
Without --body, --body-file or --editor, the configured editor opens, or a multi-line prompt
|
||||||
asks for the comment. The subcommands list, view, edit and delete comments by their ID.`, article(words.noun)), Example: fmt.Sprintf(`$ tea %[1]s comment %[2]s --body "Fixed in #14"
|
asks for the comment. The subcommands list, view, edit and delete comments by their ID.`, kind.article()), Example: fmt.Sprintf(`$ tea %[1]s comment %[2]s --body "Fixed in #14"
|
||||||
$ tea %[1]s comment %[2]s --body-file notes.md
|
$ tea %[1]s comment %[2]s --body-file notes.md
|
||||||
# Change or remove your last comment
|
# Change or remove your last comment
|
||||||
$ tea %[1]s comment %[2]s --edit-last --body "Fixed in #15"
|
$ tea %[1]s comment %[2]s --edit-last --body "Fixed in #15"
|
||||||
@@ -269,7 +269,7 @@ $ tea %[1]s comment %[2]s --delete-last --yes
|
|||||||
$ tea %[1]s comment list %[2]s`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.confirm": "true"}, Args: commentArgs(kind), RunE: func(cmd *cobra.Command, args []string) error {
|
$ tea %[1]s comment list %[2]s`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.confirm": "true"}, Args: commentArgs(kind), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
if localFlagsChanged(cmd) {
|
if localFlagsChanged(cmd) {
|
||||||
return clierr.Wrap(clierr.Usage, &argumentError{problem: "missing " + words.noun + " number", cmd: cmd})
|
return clierr.Wrap(clierr.Usage, &argumentError{problem: "missing " + kind.noun() + " number", cmd: cmd})
|
||||||
}
|
}
|
||||||
return cmd.Help()
|
return cmd.Help()
|
||||||
}
|
}
|
||||||
@@ -308,7 +308,7 @@ $ tea %[1]s comment list %[2]s`, words.group, words.example), Annotations: map[s
|
|||||||
group.ValidArgsFunction = a.completeDiscussions(kind)
|
group.ValidArgsFunction = a.completeDiscussions(kind)
|
||||||
|
|
||||||
var limit, page int
|
var limit, page int
|
||||||
list := &cobra.Command{Use: "list {<number> | <url>}", Short: "List the comments on " + article(words.noun), Example: fmt.Sprintf(`$ tea %[1]s comment list %[2]s
|
list := &cobra.Command{Use: "list {<number> | <url>}", Short: "List the comments on " + kind.article(), Example: fmt.Sprintf(`$ tea %[1]s comment list %[2]s
|
||||||
$ tea %[1]s comment list %[2]s --json id,author,body`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
$ tea %[1]s comment list %[2]s --json id,author,body`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
t, n, err := a.resolveRef(cmd, args[0], kind)
|
t, n, err := a.resolveRef(cmd, args[0], kind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -345,7 +345,7 @@ $ tea %[1]s comment list %[2]s --json id,author,body`, words.group, words.exampl
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
var createFlags commentFlags
|
var createFlags commentFlags
|
||||||
create := &cobra.Command{Use: "create {<number> | <url>}", Aliases: []string{"add"}, Short: "Add a comment to " + article(words.noun), Example: fmt.Sprintf(`$ tea %[1]s comment create %[2]s --body "Looks good"
|
create := &cobra.Command{Use: "create {<number> | <url>}", Aliases: []string{"add"}, Short: "Add a comment to " + kind.article(), Example: fmt.Sprintf(`$ tea %[1]s comment create %[2]s --body "Looks good"
|
||||||
$ tea %[1]s comment add %[2]s --editor`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
$ tea %[1]s comment add %[2]s --editor`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := createFlags.validate(cmd); err != nil {
|
if err := createFlags.validate(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -415,14 +415,6 @@ func commentArgs(kind refKind) cobra.PositionalArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// article returns noun with its indefinite article: "an issue", "a pull request".
|
|
||||||
func article(noun string) string {
|
|
||||||
if strings.ContainsRune("aeiou", rune(noun[0])) {
|
|
||||||
return "an " + noun
|
|
||||||
}
|
|
||||||
return "a " + noun
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeDiscussions completes the open issues or pull requests that a command of kind takes.
|
// completeDiscussions completes the open issues or pull requests that a command of kind takes.
|
||||||
func (a *application) completeDiscussions(kind refKind) cobra.CompletionFunc {
|
func (a *application) completeDiscussions(kind refKind) cobra.CompletionFunc {
|
||||||
if kind == pullRef {
|
if kind == pullRef {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func completionContext(cmd *cobra.Command) (context.Context, context.CancelFunc)
|
|||||||
|
|
||||||
// completeHosts completes the configured instances with their accounts.
|
// completeHosts completes the configured instances with their accounts.
|
||||||
func (a *application) completeHosts(_ *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
func (a *application) completeHosts(_ *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ func (a *application) completeHosts(_ *cobra.Command, _ []string, toComplete str
|
|||||||
func (a *application) completeAccounts(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
func (a *application) completeAccounts(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
ctx, cancel := completionContext(cmd)
|
ctx, cancel := completionContext(cmd)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ func (a *application) completeLocalBranches(cmd *cobra.Command, _ []string, toCo
|
|||||||
|
|
||||||
// completeAliases completes the configured alias names with their expansions.
|
// completeAliases completes the configured alias names with their expansions.
|
||||||
func (a *application) completeAliases(_ *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
func (a *application) completeAliases(_ *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
@@ -449,7 +449,7 @@ func (a *application) completeRepos() cobra.CompletionFunc {
|
|||||||
remotes := func(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
remotes := func(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
ctx, cancel := completionContext(cmd)
|
ctx, cancel := completionContext(cmd)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ $ tea config get default_host --show-source
|
|||||||
$ tea config get ssh_host --host https://gitea.com
|
$ tea config get ssh_host --host https://gitea.com
|
||||||
$ tea config get ssh_key --host https://gitea.com --user alice`,
|
$ tea config get ssh_key --host https://gitea.com --user alice`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -247,7 +247,7 @@ $ tea config get ssh_key --host https://gitea.com --user alice`,
|
|||||||
# Include the settings of an instance
|
# Include the settings of an instance
|
||||||
$ tea config list --host https://gitea.com`,
|
$ tea config list --host https://gitea.com`,
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -348,7 +348,7 @@ func (a *application) reportSet(cmd *cobra.Command, key, value, scope string) {
|
|||||||
|
|
||||||
// warnWithoutAccount points out a default_host that has no account to act as yet.
|
// warnWithoutAccount points out a default_host that has no account to act as yet.
|
||||||
func (a *application) warnWithoutAccount(cmd *cobra.Command) {
|
func (a *application) warnWithoutAccount(cmd *cobra.Command) {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if host := snapshot.Preferences.DefaultHost; err == nil && len(snapshot.Hosts.Hosts[host].Users) == 0 {
|
if host := snapshot.Preferences.DefaultHost; err == nil && len(snapshot.Hosts.Hosts[host].Users) == 0 {
|
||||||
a.warn(cmd, "No account on %s yet; log in with: tea auth login --host %s", host, host)
|
a.warn(cmd, "No account on %s yet; log in with: tea auth login --host %s", host, host)
|
||||||
}
|
}
|
||||||
@@ -564,7 +564,7 @@ $ tea alias set il "issue list --assignee @me" --clobber`,
|
|||||||
Use: "list", Short: "List shortcuts", Args: argsExact(0),
|
Use: "list", Short: "List shortcuts", Args: argsExact(0),
|
||||||
Example: "$ tea alias list\n# Print the expansion of one alias\n$ tea alias list --jq .il",
|
Example: "$ tea alias list\n# Print the expansion of one alias\n$ tea alias list --jq .il",
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
s, err := a.deps.Settings.Load()
|
s, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,17 +200,7 @@ func reviewerSummary(item services.Pull, reviews []services.Review) string {
|
|||||||
|
|
||||||
// checkSummary counts checks by outcome: "1 failing, 2 pending, 3 successful", or "" without checks.
|
// checkSummary counts checks by outcome: "1 failing, 2 pending, 3 successful", or "" without checks.
|
||||||
func checkSummary(checks []services.Check) string {
|
func checkSummary(checks []services.Check) string {
|
||||||
var failing, pending, passed int
|
failing, pending, passed := services.Tally(checks)
|
||||||
for _, check := range checks {
|
|
||||||
switch {
|
|
||||||
case check.Pending():
|
|
||||||
pending++
|
|
||||||
case check.Passed():
|
|
||||||
passed++
|
|
||||||
default:
|
|
||||||
failing++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var parts []string
|
var parts []string
|
||||||
for _, count := range []struct {
|
for _, count := range []struct {
|
||||||
n int
|
n int
|
||||||
@@ -315,13 +305,13 @@ func (a *application) writeDiscussion(cmd *cobra.Command, target target, v discu
|
|||||||
_, err := fmt.Fprint(cmd.OutOrStdout(), b.String())
|
_, err := fmt.Fprint(cmd.OutOrStdout(), b.String())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
view.Meta = v.summary(now) + " • " + plural(int(v.comments), "comment")
|
view.Meta = v.summary(now) + " • " + terminal.Plural(int(v.comments), "comment")
|
||||||
view.URL = ""
|
view.URL = ""
|
||||||
muted := func(text string) string { return terminal.Colorize("muted", text, d.Color) }
|
muted := func(text string) string { return terminal.Colorize("muted", text, d.Color) }
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(view.Render(d))
|
b.WriteString(view.Render(d))
|
||||||
if !withComments && v.comments > 0 {
|
if !withComments && v.comments > 0 {
|
||||||
b.WriteString("\n" + muted(plural(int(v.comments), "comment")+" — use --comments to view them") + "\n")
|
b.WriteString("\n" + muted(terminal.Plural(int(v.comments), "comment")+" — use --comments to view them") + "\n")
|
||||||
}
|
}
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
heading := []string{terminal.Emphasis(terminal.SafeLine(comment.author), d.Color)}
|
heading := []string{terminal.Emphasis(terminal.SafeLine(comment.author), d.Color)}
|
||||||
@@ -346,10 +336,3 @@ func (a *application) writeDiscussion(cmd *cobra.Command, target target, v discu
|
|||||||
b.WriteString("\n" + muted("View this "+v.view.Kind+" on Gitea: "+terminal.SafeLine(v.view.URL)) + "\n")
|
b.WriteString("\n" + muted("View this "+v.view.Kind+" on Gitea: "+terminal.SafeLine(v.view.URL)) + "\n")
|
||||||
return a.writeHuman(cmd, b.String())
|
return a.writeHuman(cmd, b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func plural(n int, unit string) string {
|
|
||||||
if n == 1 {
|
|
||||||
return "1 " + unit
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d %ss", n, unit)
|
|
||||||
}
|
|
||||||
@@ -83,7 +83,7 @@ problem comes with the command that fixes it. tea doctor fails when a check fail
|
|||||||
func (a *application) diagnose(ctx context.Context) doctorReport {
|
func (a *application) diagnose(ctx context.Context) doctorReport {
|
||||||
var report doctorReport
|
var report doctorReport
|
||||||
add := func(check doctorCheck) { report.Checks = append(report.Checks, check) }
|
add := func(check doctorCheck) { report.Checks = append(report.Checks, check) }
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
add(doctorCheck{"Config", checkFailed, err.Error(), "repair or move the file; tea config path prints both files"})
|
add(doctorCheck{"Config", checkFailed, err.Error(), "repair or move the file; tea config path prints both files"})
|
||||||
add(a.gitCheck(ctx, ""))
|
add(a.gitCheck(ctx, ""))
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ func (a *application) git(ctx context.Context, args ...string) (string, error) {
|
|||||||
|
|
||||||
// gitIn runs Git in dir, which is relative to the process's working directory, not to --cwd.
|
// gitIn runs Git in dir, which is relative to the process's working directory, not to --cwd.
|
||||||
func (a *application) gitIn(ctx context.Context, dir string, args ...string) (string, error) {
|
func (a *application) gitIn(ctx context.Context, dir string, args ...string) (string, error) {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func (a *application) gitCredentialCommand() *cobra.Command {
|
|||||||
if args[0] != "get" {
|
if args[0] != "get" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
// preferences applies environment overrides to the stored preferences, matching config get --show-source.
|
// preferences applies environment overrides to the stored preferences, matching config get --show-source.
|
||||||
func (a *application) preferences() (settings.Preferences, error) {
|
func (a *application) preferences() (settings.Preferences, error) {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return settings.Preferences{}, err
|
return settings.Preferences{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ func attachmentView(item services.IssueAttachment) terminal.View {
|
|||||||
|
|
||||||
func (a *application) issueAttachmentCommand(kind refKind) *cobra.Command {
|
func (a *application) issueAttachmentCommand(kind refKind) *cobra.Command {
|
||||||
words := commentWords(kind)
|
words := commentWords(kind)
|
||||||
group := &cobra.Command{Use: "attachment", Short: "Manage the attachments of " + words.noun + "s", Long: fmt.Sprintf(`List, view, upload, download, rename and delete the files attached to %s. Attachments
|
group := &cobra.Command{Use: "attachment", Short: "Manage the attachments of " + kind.noun() + "s", Long: fmt.Sprintf(`List, view, upload, download, rename and delete the files attached to %s. Attachments
|
||||||
are addressed by the %s number and the attachment ID that attachment list shows.`, article(words.noun), words.noun)}
|
are addressed by the %s number and the attachment ID that attachment list shows.`, kind.article(), kind.noun())}
|
||||||
annotations := map[string]string{"tea.arguments": words.arguments}
|
annotations := map[string]string{"tea.arguments": words.arguments}
|
||||||
// discussionTarget resolves the issue or pull request argument of an attachment command.
|
// discussionTarget resolves the issue or pull request argument of an attachment command.
|
||||||
discussionTarget := func(cmd *cobra.Command, raw string) (target, int64, error) {
|
discussionTarget := func(cmd *cobra.Command, raw string) (target, int64, error) {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func issuesURL(t target, opts services.ListOptions) string {
|
|||||||
if opts.Search != "" {
|
if opts.Search != "" {
|
||||||
query.Set("q", opts.Search)
|
query.Set("q", opts.Search)
|
||||||
}
|
}
|
||||||
return strings.TrimRight(t.Host, "/") + "/" + t.Repo + "/" + section + "?" + query.Encode()
|
return repositoryURL(t) + "/" + section + "?" + query.Encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// issueListSpec describes issue tables of the issues where, such as "in alice/x": STATE shows on a
|
// issueListSpec describes issue tables of the issues where, such as "in alice/x": STATE shows on a
|
||||||
@@ -258,7 +258,7 @@ $ tea issue create --title "Idea" --web`, Args: argsExact(0), RunE: func(cmd *co
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
query := url.Values{"title": {title}, "body": {cmp.Or(body, templateBody)}}
|
query := url.Values{"title": {title}, "body": {cmp.Or(body, templateBody)}}
|
||||||
return a.openBrowser(cmd, strings.TrimRight(target.Host, "/")+"/"+target.Repo+"/issues/new?"+query.Encode())
|
return a.openBrowser(cmd, repositoryURL(target)+"/issues/new?"+query.Encode())
|
||||||
}
|
}
|
||||||
if bodyFile == "-" && title == "" {
|
if bodyFile == "-" && title == "" {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--body-file - requires --title; stdin cannot also supply a prompt"))
|
return clierr.Wrap(clierr.Usage, errors.New("--body-file - requires --title; stdin cannot also supply a prompt"))
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func milestoneName(t target, item services.Milestone) string {
|
|||||||
|
|
||||||
// milestoneURL is the web page of a milestone, which the API does not return.
|
// milestoneURL is the web page of a milestone, which the API does not return.
|
||||||
func milestoneURL(t target, item services.Milestone) string {
|
func milestoneURL(t target, item services.Milestone) string {
|
||||||
return strings.TrimRight(t.Host, "/") + "/" + t.Repo + "/milestone/" + strconv.FormatInt(item.ID, 10)
|
return repositoryURL(t) + "/milestone/" + strconv.FormatInt(item.ID, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
func milestoneView(t target, now time.Time) func(services.Milestone) terminal.View {
|
func milestoneView(t target, now time.Time) func(services.Milestone) terminal.View {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ list shows identify notification threads; they are not issue numbers.`}
|
|||||||
}, func(item services.Notification) string { return fmt.Sprintf(mark.done, notificationLabel(item)) })
|
}, func(item services.Notification) string { return fmt.Sprintf(mark.done, notificationLabel(item)) })
|
||||||
}
|
}
|
||||||
what := notificationsWith(filters) + " " + s.where
|
what := notificationsWith(filters) + " " + s.where
|
||||||
if settings, err := a.deps.Settings.Load(); err == nil && a.policy(settings).CanPrompt(a.deps.Streams) {
|
if a.canPrompt() {
|
||||||
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: fmt.Sprintf(mark.ask, what), DefaultYes: true}); err != nil {
|
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: fmt.Sprintf(mark.ask, what), DefaultYes: true}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ list shows identify notification threads; they are not issue numbers.`}
|
|||||||
a.warn(cmd, "No %s", what)
|
a.warn(cmd, "No %s", what)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
a.done(cmd, mark.done, plural(len(results), "notification"))
|
a.done(cmd, mark.done, terminal.Plural(len(results), "notification"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ $ tea notification list --type pull --since 24h`,
|
|||||||
what := notificationsWith(opts.Statuses) + " " + s.where
|
what := notificationsWith(opts.Statuses) + " " + s.where
|
||||||
return writeTable(a, cmd, page.Items, listSpec[services.Notification]{
|
return writeTable(a, cmd, page.Items, listSpec[services.Notification]{
|
||||||
Columns: notificationColumns, Row: notificationRow, Empty: "no " + what, Context: what,
|
Columns: notificationColumns, Row: notificationRow, Empty: "no " + what, Context: what,
|
||||||
}, listPage{Number: opts.Page, More: page.More, Total: page.Total, Counted: page.Counted})
|
}, servicePage(page, opts.Page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
notificationFilters(command, &opts, &updated)
|
notificationFilters(command, &opts, &updated)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -47,15 +46,6 @@ func countOf(n int, singular, plural string) string {
|
|||||||
return strconv.Itoa(n) + " " + plural
|
return strconv.Itoa(n) + " " + plural
|
||||||
}
|
}
|
||||||
|
|
||||||
// webURL is the page of a path on the target's instance, such as an organization's profile.
|
|
||||||
func webURL(t target, path ...string) string {
|
|
||||||
escaped := make([]string, len(path))
|
|
||||||
for i, segment := range path {
|
|
||||||
escaped[i] = url.PathEscape(segment)
|
|
||||||
}
|
|
||||||
return strings.TrimSuffix(t.Host, "/") + "/" + strings.Join(escaped, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *application) organizationCommand() *cobra.Command {
|
func (a *application) organizationCommand() *cobra.Command {
|
||||||
group := &cobra.Command{Use: "org", Short: "Manage organizations", Long: `Work with Gitea organizations: list the ones you belong to, view, create, edit and delete
|
group := &cobra.Command{Use: "org", Short: "Manage organizations", Long: `Work with Gitea organizations: list the ones you belong to, view, create, edit and delete
|
||||||
them, and manage their members.
|
them, and manage their members.
|
||||||
|
|||||||
+14
-2
@@ -12,13 +12,25 @@ import (
|
|||||||
// DefaultYes, destructive ones keep the default no, and irreversible ones set Typed. A declined
|
// DefaultYes, destructive ones keep the default no, and irreversible ones set Typed. A declined
|
||||||
// confirmation returns terminal.ErrCanceled.
|
// confirmation returns terminal.ErrCanceled.
|
||||||
func (a *application) confirmAction(cmd *cobra.Command, c terminal.Confirmation) error {
|
func (a *application) confirmAction(cmd *cobra.Command, c terminal.Confirmation) error {
|
||||||
s, err := a.deps.Settings.Load()
|
s, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return terminal.ConfirmAction(cmd.Context(), c, a.options.yes, a.deps.Streams, a.policy(s), a.deps.Prompter)
|
return terminal.ConfirmAction(cmd.Context(), c, a.options.yes, a.deps.Streams, a.policy(s), a.deps.Prompter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// canPrompt reports whether tea may ask questions on this terminal.
|
||||||
|
func (a *application) canPrompt() bool {
|
||||||
|
s, err := a.settings()
|
||||||
|
return err == nil && a.policy(s).CanPrompt(a.deps.Streams)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chooser returns the prompter's selection prompt when prompts are allowed.
|
||||||
|
func (a *application) chooser() (terminal.Chooser, bool) {
|
||||||
|
chooser, ok := a.deps.Prompter.(terminal.Chooser)
|
||||||
|
return chooser, ok && a.canPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
// deleteConfirmed asks to delete subject, such as `label "bug" from alice/x`, deletes it with del
|
// deleteConfirmed asks to delete subject, such as `label "bug" from alice/x`, deletes it with del
|
||||||
// and reports "Deleted <subject>".
|
// and reports "Deleted <subject>".
|
||||||
func (a *application) deleteConfirmed(cmd *cobra.Command, subject string, del func() error) error {
|
func (a *application) deleteConfirmed(cmd *cobra.Command, subject string, del func() error) error {
|
||||||
@@ -36,7 +48,7 @@ func (a *application) deleteConfirmed(cmd *cobra.Command, subject string, del fu
|
|||||||
// until the answer is non-empty and validate, when set, accepts it. Without prompts it returns a
|
// until the answer is non-empty and validate, when set, accepts it. Without prompts it returns a
|
||||||
// usage error naming --flag. It reports whether it prompted.
|
// usage error naming --flag. It reports whether it prompted.
|
||||||
func (a *application) requireInput(cmd *cobra.Command, value *string, flag, label string, validate func(string) error) (bool, error) {
|
func (a *application) requireInput(cmd *cobra.Command, value *string, flag, label string, validate func(string) error) (bool, error) {
|
||||||
s, err := a.deps.Settings.Load()
|
s, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func (a *application) pullCleanCommand() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ is not on the remote yet is pushed first, with its upstream set.`, Args: argsExa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s, err := a.deps.Settings.Load()
|
s, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ func (a *application) defaultPullHead(ctx context.Context) (owner, branch string
|
|||||||
if err != nil || raw == "" {
|
if err != nil || raw == "" {
|
||||||
return "", branch
|
return "", branch
|
||||||
}
|
}
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", branch
|
return "", branch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,12 +250,6 @@ func (a *application) reviewCreateCommand(use, short string) *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// canPrompt reports whether tea may ask questions on this terminal.
|
|
||||||
func (a *application) canPrompt() bool {
|
|
||||||
s, err := a.deps.Settings.Load()
|
|
||||||
return err == nil && a.policy(s).CanPrompt(a.deps.Streams)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ownReview refuses to approve or request changes on one's own pull request before any body is
|
// ownReview refuses to approve or request changes on one's own pull request before any body is
|
||||||
// written, as Gitea would after.
|
// written, as Gitea would after.
|
||||||
func ownReview(pull services.Pull, t target, state string) error {
|
func ownReview(pull services.Pull, t target, state string) error {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ each pull request, which tea only makes when --json names them.`, Args: argsExac
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Empty: empty, Context: what,
|
Empty: empty, Context: what,
|
||||||
}, listPage{Number: page, More: window.More, Total: window.Total, Counted: window.Counted})
|
}, servicePage(window, page))
|
||||||
}}
|
}}
|
||||||
enumFlag(cmd, &state, "state", "s", "open", "Filter by state", "open", "closed", "merged", "all")
|
enumFlag(cmd, &state, "state", "s", "open", "Filter by state", "open", "closed", "merged", "all")
|
||||||
pageFlags(cmd, &limit, &page)
|
pageFlags(cmd, &limit, &page)
|
||||||
@@ -163,11 +163,6 @@ func (a *application) jsonSelects(fields ...string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// repositoryURL returns the web URL of the target's repository.
|
|
||||||
func repositoryURL(t target) string {
|
|
||||||
return strings.TrimSuffix(t.Host, "/") + "/" + t.Repo
|
|
||||||
}
|
|
||||||
|
|
||||||
// milestoneFilter resolves a milestone given as an ID or a title (D10): a number is an ID first,
|
// milestoneFilter resolves a milestone given as an ID or a title (D10): a number is an ID first,
|
||||||
// then a title.
|
// then a title.
|
||||||
func milestoneFilter(ctx context.Context, t target, raw string) (int64, error) {
|
func milestoneFilter(ctx context.Context, t target, raw string) (int64, error) {
|
||||||
|
|||||||
+1
-1
@@ -177,7 +177,7 @@ func (a *application) resolveRefs(cmd *cobra.Command, raws []string, kind refKin
|
|||||||
|
|
||||||
// sameRepository reports whether two repository arguments name the same repository.
|
// sameRepository reports whether two repository arguments name the same repository.
|
||||||
func (a *application) sameRepository(first, second string) bool {
|
func (a *application) sameRepository(first, second string) bool {
|
||||||
s, err := a.deps.Settings.Load()
|
s, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return strings.EqualFold(first, second)
|
return strings.EqualFold(first, second)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ $ tea release list -R alice/project --json tag,title,draft`,
|
|||||||
},
|
},
|
||||||
Context: "releases in " + target.Repo, Empty: "no releases in " + target.Repo,
|
Context: "releases in " + target.Repo, Empty: "no releases in " + target.Repo,
|
||||||
}
|
}
|
||||||
return writeTable(a, cmd, result.Items, spec, listPage{Number: page, More: result.More, Total: result.Total, Counted: result.Counted})
|
return writeTable(a, cmd, result.Items, spec, servicePage(result, page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pageFlags(command, &limit, &page)
|
pageFlags(command, &limit, &page)
|
||||||
|
|||||||
+3
-11
@@ -173,7 +173,7 @@ $ tea repo list --starred --json full_name,url`,
|
|||||||
}
|
}
|
||||||
collection := repositoryCollection(opts)
|
collection := repositoryCollection(opts)
|
||||||
spec := listSpec[services.Repository]{Columns: repositoryColumns, Row: repositoryRow, Context: collection, Empty: "no " + collection}
|
spec := listSpec[services.Repository]{Columns: repositoryColumns, Row: repositoryRow, Context: collection, Empty: "no " + collection}
|
||||||
return writeTable(a, cmd, result.Items, spec, listPage{Number: opts.Page, More: result.More, Total: result.Total, Counted: result.Counted})
|
return writeTable(a, cmd, result.Items, spec, servicePage(result, opts.Page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pageFlags(list, &opts.Limit, &opts.Page)
|
pageFlags(list, &opts.Limit, &opts.Page)
|
||||||
@@ -294,8 +294,8 @@ func (a *application) repositoryView(item services.Repository, readme string) te
|
|||||||
badges = append(badges, badge)
|
badges = append(badges, badge)
|
||||||
}
|
}
|
||||||
meta := []string{
|
meta := []string{
|
||||||
fmt.Sprintf("★ %d", item.Stars), countNoun(item.Forks, "fork"), countNoun(item.OpenIssues, "open issue"),
|
fmt.Sprintf("★ %d", item.Stars), terminal.Plural(item.Forks, "fork"), terminal.Plural(item.OpenIssues, "open issue"),
|
||||||
countNoun(item.OpenPulls, "open pull request"),
|
terminal.Plural(item.OpenPulls, "open pull request"),
|
||||||
}
|
}
|
||||||
if updated := terminal.ParseTime(item.UpdatedAt); !updated.IsZero() {
|
if updated := terminal.ParseTime(item.UpdatedAt); !updated.IsZero() {
|
||||||
meta = append(meta, "updated "+terminal.RelativeTime(a.deps.Now(), updated))
|
meta = append(meta, "updated "+terminal.RelativeTime(a.deps.Now(), updated))
|
||||||
@@ -328,11 +328,3 @@ func parentName(item services.Repository) string {
|
|||||||
}
|
}
|
||||||
return item.Parent.FullName
|
return item.Parent.FullName
|
||||||
}
|
}
|
||||||
|
|
||||||
// countNoun counts n of a noun, as in "1 fork" or "3 forks".
|
|
||||||
func countNoun(n int, noun string) string {
|
|
||||||
if n == 1 {
|
|
||||||
return "1 " + noun
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d %ss", n, noun)
|
|
||||||
}
|
|
||||||
@@ -90,7 +90,7 @@ $ tea repo create project --source . --push`,
|
|||||||
if err := validateRepositoryName(opts.Name); err != nil {
|
if err := validateRepositoryName(opts.Name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
selected := *a
|
selected := &application{deps: a.deps, options: a.options}
|
||||||
selected.options.repo = template
|
selected.options.repo = template
|
||||||
target, err := selected.resolve(cmd.Context(), false)
|
target, err := selected.resolve(cmd.Context(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ $ tea repo sync bob/project`,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ $ tea search repo go --topic --sort updated --order desc`,
|
|||||||
what += " matching " + opts.Query
|
what += " matching " + opts.Query
|
||||||
}
|
}
|
||||||
spec := listSpec[services.Repository]{Columns: repositoryColumns, Row: repositoryRow, Context: what, Empty: "no " + what}
|
spec := listSpec[services.Repository]{Columns: repositoryColumns, Row: repositoryRow, Context: what, Empty: "no " + what}
|
||||||
return writeTable(a, cmd, result.Items, spec, listPage{Number: opts.Page, More: result.More, Total: result.Total, Counted: result.Counted})
|
return writeTable(a, cmd, result.Items, spec, servicePage(result, opts.Page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
flags := command.Flags()
|
flags := command.Flags()
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func (a *application) resolveRequest(ctx context.Context, req targetRequest) (ta
|
|||||||
|
|
||||||
// connect selects the instance, account and repository and builds the authenticated client.
|
// connect selects the instance, account and repository and builds the authenticated client.
|
||||||
func (a *application) connect(ctx context.Context, req targetRequest) (target, error) {
|
func (a *application) connect(ctx context.Context, req targetRequest) (target, error) {
|
||||||
s, err := a.deps.Settings.Load()
|
s, err := a.settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return target{}, err
|
return target{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,11 +365,7 @@ $ tea webhook create https://hooks.slack.com/services/T0/B0/X --org acme --type
|
|||||||
}
|
}
|
||||||
askAuthorization := !create && authSources == 0
|
askAuthorization := !create && authSources == 0
|
||||||
if askAuthorization {
|
if askAuthorization {
|
||||||
snapshot, err := a.deps.Settings.Load()
|
if !a.canPrompt() {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !a.policy(snapshot).CanPrompt(a.deps.Streams) {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("editing a webhook drops its stored authorization header unless it is sent again, and Gitea never returns it: pass --authorization-header-file or --authorization-header (use --clear-authorization only for a hook without one)"))
|
return clierr.Wrap(clierr.Usage, errors.New("editing a webhook drops its stored authorization header unless it is sent again, and Gitea never returns it: pass --authorization-header-file or --authorization-header (use --clear-authorization only for a hook without one)"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func (a *application) wikiListCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
Context: "wiki pages in " + target.Repo, Empty: "no wiki pages in " + target.Repo,
|
Context: "wiki pages in " + target.Repo, Empty: "no wiki pages in " + target.Repo,
|
||||||
}
|
}
|
||||||
return writeTable(a, cmd, result.Items, spec, listPage{Number: page, More: result.More, Total: result.Total, Counted: result.Counted})
|
return writeTable(a, cmd, result.Items, spec, servicePage(result, page))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pageFlags(command, &limit, &page)
|
pageFlags(command, &limit, &page)
|
||||||
@@ -102,7 +102,7 @@ func (a *application) wikiView(item services.WikiPage) terminal.View {
|
|||||||
meta = append(meta, "updated "+terminal.RelativeTime(a.deps.Now(), updated))
|
meta = append(meta, "updated "+terminal.RelativeTime(a.deps.Now(), updated))
|
||||||
}
|
}
|
||||||
if item.CommitCount > 0 {
|
if item.CommitCount > 0 {
|
||||||
meta = append(meta, countNoun(int(item.CommitCount), "revision"))
|
meta = append(meta, terminal.Plural(int(item.CommitCount), "revision"))
|
||||||
}
|
}
|
||||||
return terminal.View{
|
return terminal.View{
|
||||||
Title: item.Title, Meta: strings.Join(meta, " · "), Body: item.Body, Kind: "wiki page", URL: item.URL,
|
Title: item.Title, Meta: strings.Join(meta, " · "), Body: item.Body, Kind: "wiki page", URL: item.URL,
|
||||||
@@ -226,11 +226,7 @@ func (a *application) wikiBody(cmd *cobra.Command, editor bool, initial string)
|
|||||||
if editor {
|
if editor {
|
||||||
return a.editDraft(cmd, initial)
|
return a.editDraft(cmd, initial)
|
||||||
}
|
}
|
||||||
s, err := a.deps.Settings.Load()
|
if !a.canPrompt() {
|
||||||
if err != nil {
|
|
||||||
return terminal.Draft{}, err
|
|
||||||
}
|
|
||||||
if !a.policy(s).CanPrompt(a.deps.Streams) {
|
|
||||||
return terminal.Draft{}, clierr.Wrap(clierr.Usage, errors.New("missing required input: supply --body, --body-file or --editor"))
|
return terminal.Draft{}, clierr.Wrap(clierr.Usage, errors.New("missing required input: supply --body, --body-file or --editor"))
|
||||||
}
|
}
|
||||||
return a.bodyEditor(cmd)(cmd.Context(), initial)
|
return a.bodyEditor(cmd)(cmd.Context(), initial)
|
||||||
|
|||||||
+17
-10
@@ -32,6 +32,21 @@ func (c Check) Pending() bool { return c.State == "pending" }
|
|||||||
// Passed reports whether the check finished without blocking a merge; Gitea counts skipped as success.
|
// Passed reports whether the check finished without blocking a merge; Gitea counts skipped as success.
|
||||||
func (c Check) Passed() bool { return c.State == "success" || c.State == "skipped" }
|
func (c Check) Passed() bool { return c.State == "success" || c.State == "skipped" }
|
||||||
|
|
||||||
|
// Tally counts checks by outcome.
|
||||||
|
func Tally(items []Check) (failing, pending, passed int) {
|
||||||
|
for _, item := range items {
|
||||||
|
switch {
|
||||||
|
case item.Pending():
|
||||||
|
pending++
|
||||||
|
case item.Passed():
|
||||||
|
passed++
|
||||||
|
default:
|
||||||
|
failing++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failing, pending, passed
|
||||||
|
}
|
||||||
|
|
||||||
// Checks returns latest contexts for the PR head in its base repository, as Gitea does.
|
// Checks returns latest contexts for the PR head in its base repository, as Gitea does.
|
||||||
func (s Pulls) Checks(ctx context.Context, n int64) ([]Check, error) {
|
func (s Pulls) Checks(ctx context.Context, n int64) ([]Check, error) {
|
||||||
sha, err := s.checkHead(ctx, n)
|
sha, err := s.checkHead(ctx, n)
|
||||||
@@ -126,16 +141,8 @@ func (s Pulls) WatchChecks(ctx context.Context, n int64, interval time.Duration,
|
|||||||
}
|
}
|
||||||
previous = items
|
previous = items
|
||||||
}
|
}
|
||||||
pending := len(items) == 0
|
failing, pending, _ := Tally(items)
|
||||||
failed := false
|
if failing > 0 || (pending == 0 && len(items) > 0) {
|
||||||
for _, item := range items {
|
|
||||||
if item.Pending() {
|
|
||||||
pending = true
|
|
||||||
} else if !item.Passed() {
|
|
||||||
failed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if failed || !pending {
|
|
||||||
current, err := s.checkHead(ctx, n)
|
current, err := s.checkHead(ctx, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/tea/internal/filelock"
|
"gitea.dev/tea/internal/filelock"
|
||||||
@@ -102,8 +103,14 @@ type Store struct {
|
|||||||
Dir string
|
Dir string
|
||||||
LegacyPath string
|
LegacyPath string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
// generation counts the updates made through the store.
|
||||||
|
generation atomic.Uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generation changes whenever an update through the store ran, so callers can tell when a
|
||||||
|
// snapshot they loaded earlier may be stale.
|
||||||
|
func (s *Store) Generation() uint64 { return s.generation.Load() }
|
||||||
|
|
||||||
// New selects an explicit directory or the platform configuration directory.
|
// New selects an explicit directory or the platform configuration directory.
|
||||||
func New(dir string) *Store {
|
func New(dir string) *Store {
|
||||||
legacy := ""
|
legacy := ""
|
||||||
@@ -328,6 +335,7 @@ func (s *Store) Update(target Target, update func(*Snapshot) error) error {
|
|||||||
// Transaction runs a compensating action under the lock if publishing the target file fails.
|
// Transaction runs a compensating action under the lock if publishing the target file fails.
|
||||||
// It is used to keep credential storage and account metadata consistent.
|
// It is used to keep credential storage and account metadata consistent.
|
||||||
func (s *Store) Transaction(target Target, update func(*Snapshot) (func() error, error)) error {
|
func (s *Store) Transaction(target Target, update func(*Snapshot) (func() error, error)) error {
|
||||||
|
defer s.generation.Add(1)
|
||||||
return s.Locked(func(snapshot Snapshot) error {
|
return s.Locked(func(snapshot Snapshot) error {
|
||||||
unpublished := func() ([]byte, error) {
|
unpublished := func() ([]byte, error) {
|
||||||
if target == TargetHosts {
|
if target == TargetHosts {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func RelativeTime(now, t time.Time) string {
|
|||||||
default:
|
default:
|
||||||
n, unit = d/(365*day), "year"
|
n, unit = d/(365*day), "year"
|
||||||
}
|
}
|
||||||
return prefix + "about " + plural(int(n), unit) + suffix
|
return prefix + "about " + Plural(int(n), unit) + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp formats t for scripts: RFC3339 in UTC at second precision.
|
// Timestamp formats t for scripts: RFC3339 in UTC at second precision.
|
||||||
@@ -49,7 +49,8 @@ func ParseTime(raw string) time.Time {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func plural(n int, unit string) string {
|
// Plural counts n of a unit, as in "1 fork" or "3 forks".
|
||||||
|
func Plural(n int, unit string) string {
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
return "1 " + unit
|
return "1 " + unit
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user