mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
fix(config): print only the value of config get and add display preferences
tea config get printed "key<TAB>value", so $(tea config get editor) was not usable; it now prints the value alone, and --show-source appends the source (D18). config get reads the instance's user, and instance settings come from the selected instance (--host, TEA_HOST, default_host or the sole instance). Unknown keys list the valid ones, and keys that need --host or --user say so. config path no longer loads the settings, so it works when hosts.yml is broken. config list is a table on a terminal. config set confirms the change on a terminal and warns when default_host has no account. config.yml stores the hyperlinks (auto, always, never) and markdown (auto, raw) preferences that tables and views now read, instead of the TEA_HYPERLINKS and TEA_MARKDOWN variables. The environment table moves out of tea config --help into tea help environment. alias set refuses to replace an alias without --clobber, drops a leading "tea", and explains how to quote an expansion with spaces or flags. alias delete takes several names or --all, alias list is a table, and an empty list says so on a terminal. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
9 files changed
+687
-271
No files matched your search
+1
-7
@@ -279,7 +279,7 @@ func (a *application) checkOutputFlags(cmd *cobra.Command) error {
|
|||||||
|
|
||||||
// offline reports commands that must work without valid settings.
|
// offline reports commands that must work without valid settings.
|
||||||
func offline(cmd *cobra.Command) bool {
|
func offline(cmd *cobra.Command) bool {
|
||||||
if cmd.Annotations["tea.group"] == "true" {
|
if cmd.Annotations["tea.group"] == "true" || cmd.Annotations["tea.offline"] == "true" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
switch cmd.Name() {
|
switch cmd.Name() {
|
||||||
@@ -492,12 +492,6 @@ func commandIndex(root *cobra.Command, args []string) int {
|
|||||||
return len(args)
|
return len(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// configKeys are the preferences, instance settings and account settings that config get reads.
|
|
||||||
var configKeys = []string{
|
|
||||||
"browser", "color", "default_host", "editor", "git_protocol", "insecure", "oauth_client_id", "pager", "prompt",
|
|
||||||
"remote", "ssh_agent", "ssh_certificate_principal", "ssh_host", "ssh_key", "ssh_key_fingerprint", "version",
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandOutputFields(cmd *cobra.Command) []string {
|
func commandOutputFields(cmd *cobra.Command) []string {
|
||||||
if cmd.Name() == "status" && cmd.Parent() != nil {
|
if cmd.Name() == "status" && cmd.Parent() != nil {
|
||||||
if cmd.Parent().Name() == "pr" {
|
if cmd.Parent().Name() == "pr" {
|
||||||
|
|||||||
@@ -415,16 +415,11 @@ func walkCommands(cmd *cobra.Command, visit func(*cobra.Command)) {
|
|||||||
// have yet to complete; see TestHelpTextsAreComplete.
|
// have yet to complete; see TestHelpTextsAreComplete.
|
||||||
const (
|
const (
|
||||||
shortsToFix = `
|
shortsToFix = `
|
||||||
config get
|
|
||||||
config list
|
|
||||||
notification mark-read
|
notification mark-read
|
||||||
notification mark-unread
|
notification mark-unread
|
||||||
pr review cancel-request
|
pr review cancel-request
|
||||||
`
|
`
|
||||||
examplesToWrite = `
|
examplesToWrite = `
|
||||||
alias delete
|
|
||||||
alias list
|
|
||||||
alias set
|
|
||||||
api
|
api
|
||||||
auth login
|
auth login
|
||||||
auth logout
|
auth logout
|
||||||
@@ -439,10 +434,6 @@ const (
|
|||||||
branch rename
|
branch rename
|
||||||
branch unprotect
|
branch unprotect
|
||||||
branch view
|
branch view
|
||||||
config get
|
|
||||||
config list
|
|
||||||
config path
|
|
||||||
config set
|
|
||||||
doctor
|
doctor
|
||||||
issue attachment delete
|
issue attachment delete
|
||||||
issue attachment download
|
issue attachment download
|
||||||
@@ -557,7 +548,6 @@ const (
|
|||||||
wiki view
|
wiki view
|
||||||
`
|
`
|
||||||
longsToWrite = `
|
longsToWrite = `
|
||||||
alias
|
|
||||||
auth
|
auth
|
||||||
branch
|
branch
|
||||||
issue
|
issue
|
||||||
|
|||||||
@@ -255,15 +255,6 @@ func (a *application) completeAliases(_ *cobra.Command, _ []string, toComplete s
|
|||||||
return matching(describe(slices.Sorted(maps.Keys(aliases)), func(name string) (string, string) { return name, aliases[name] }), toComplete), cobra.ShellCompDirectiveNoFileComp
|
return matching(describe(slices.Sorted(maps.Keys(aliases)), func(name string) (string, string) { return name, aliases[name] }), toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|
||||||
// configValues lists the values of the settings that accept a fixed set.
|
|
||||||
var configValues = map[string][]string{
|
|
||||||
"git_protocol": {"https", "ssh"},
|
|
||||||
"prompt": {"enabled", "disabled"},
|
|
||||||
"color": {"auto", "always", "never"},
|
|
||||||
"insecure": {"true", "false"},
|
|
||||||
"ssh_agent": {"true", "false"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeConfig completes a setting name, then its value: the fixed values, the configured instances
|
// completeConfig completes a setting name, then its value: the fixed values, the configured instances
|
||||||
// for default_host, or the Git remotes for remote.
|
// for default_host, or the Git remotes for remote.
|
||||||
func (a *application) completeConfig(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
func (a *application) completeConfig(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
|
|||||||
+476
-234
@@ -6,8 +6,10 @@ package app
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/tea/internal/aliases"
|
"gitea.dev/tea/internal/aliases"
|
||||||
@@ -17,6 +19,26 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Settings that tea config reads and writes, by where they are stored.
|
||||||
|
var (
|
||||||
|
preferenceKeys = []string{"browser", "color", "default_host", "editor", "git_protocol", "hyperlinks", "markdown", "pager", "prompt", "remote"}
|
||||||
|
instanceKeys = []string{"git_protocol", "insecure", "oauth_client_id", "ssh_host", "user"}
|
||||||
|
accountKeys = []string{"ssh_agent", "ssh_certificate_principal", "ssh_key", "ssh_key_fingerprint"}
|
||||||
|
// configKeys are every setting that config get reads, in alphabetical order.
|
||||||
|
configKeys = slices.Compact(slices.Sorted(slices.Values(slices.Concat(preferenceKeys, instanceKeys, accountKeys, []string{"version"}))))
|
||||||
|
)
|
||||||
|
|
||||||
|
// configValues lists the values of the settings that accept a fixed set.
|
||||||
|
var configValues = map[string][]string{
|
||||||
|
"git_protocol": {"https", "ssh"},
|
||||||
|
"prompt": {"enabled", "disabled"},
|
||||||
|
"color": {"auto", "always", "never"},
|
||||||
|
"hyperlinks": {"auto", "always", "never"},
|
||||||
|
"markdown": {"auto", "raw"},
|
||||||
|
"insecure": {"true", "false"},
|
||||||
|
"ssh_agent": {"true", "false"},
|
||||||
|
}
|
||||||
|
|
||||||
func (a *application) effectivePreferences(snapshot settings.Snapshot) (settings.Preferences, map[string]string, error) {
|
func (a *application) effectivePreferences(snapshot settings.Snapshot) (settings.Preferences, map[string]string, error) {
|
||||||
preferences := snapshot.Preferences
|
preferences := snapshot.Preferences
|
||||||
sources := map[string]string{}
|
sources := map[string]string{}
|
||||||
@@ -42,7 +64,7 @@ func (a *application) effectivePreferences(snapshot settings.Snapshot) (settings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if host != "" {
|
if host != "" {
|
||||||
normalized, err := settings.NormalizeHost(host)
|
normalized, err := settings.ResolveHost(host, snapshot.Hosts.Hosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return preferences, nil, clierr.Wrap(clierr.Usage, err)
|
return preferences, nil, clierr.Wrap(clierr.Usage, err)
|
||||||
}
|
}
|
||||||
@@ -85,269 +107,489 @@ func (a *application) effectivePreferences(snapshot settings.Snapshot) (settings
|
|||||||
return preferences, sources, nil
|
return preferences, sources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configSetting is one effective setting and where its value comes from.
|
||||||
|
type configSetting struct {
|
||||||
|
Key string
|
||||||
|
Value any
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
|
||||||
|
// settingValues returns the effective preferences, the settings of the selected instance when it is
|
||||||
|
// configured, and with --user the settings of that account, each with its source.
|
||||||
|
func (a *application) settingValues(snapshot settings.Snapshot) (map[string]any, map[string]string, error) {
|
||||||
|
if a.options.user != "" && a.options.host == "" {
|
||||||
|
return nil, nil, clierr.Wrap(clierr.Usage, errors.New("account settings require both --host and --user"))
|
||||||
|
}
|
||||||
|
preferences, sources, err := a.effectivePreferences(snapshot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
values := map[string]any{
|
||||||
|
"version": preferences.Version, "default_host": preferences.DefaultHost,
|
||||||
|
"git_protocol": preferences.GitProtocol, "editor": preferences.Editor,
|
||||||
|
"prompt": preferences.Prompt, "pager": preferences.Pager,
|
||||||
|
"browser": preferences.Browser, "color": preferences.Color,
|
||||||
|
"hyperlinks": preferences.Hyperlinks, "markdown": preferences.Markdown,
|
||||||
|
"remote": preferences.Remote,
|
||||||
|
}
|
||||||
|
host, configured := snapshot.Hosts.Hosts[preferences.DefaultHost]
|
||||||
|
if !configured {
|
||||||
|
if a.options.user != "" {
|
||||||
|
return nil, nil, clierr.Wrap(clierr.Usage, fmt.Errorf("instance %s is not configured; run tea auth login --host %s", preferences.DefaultHost, preferences.DefaultHost))
|
||||||
|
}
|
||||||
|
return values, sources, nil
|
||||||
|
}
|
||||||
|
for key, value := range map[string]any{"user": host.User, "ssh_host": host.SSHHost, "insecure": host.Insecure, "oauth_client_id": host.OAuthClientID} {
|
||||||
|
values[key] = value
|
||||||
|
sources[key] = "hosts.yml " + preferences.DefaultHost
|
||||||
|
}
|
||||||
|
if a.options.user != "" {
|
||||||
|
user, exists := matchAccount(host.Users, a.options.user)
|
||||||
|
if !exists {
|
||||||
|
return nil, nil, clierr.Wrap(clierr.Usage, fmt.Errorf("account %q is not logged in on %s (accounts: %s)", a.options.user, preferences.DefaultHost, accountNames(host.Users)))
|
||||||
|
}
|
||||||
|
account := host.Users[user]
|
||||||
|
for key, value := range map[string]any{"ssh_key": account.SSHKey, "ssh_agent": account.SSHAgent, "ssh_certificate_principal": account.SSHCertPrincipal, "ssh_key_fingerprint": account.SSHKeyFingerprint} {
|
||||||
|
values[key] = value
|
||||||
|
sources[key] = "hosts.yml " + preferences.DefaultHost + " user " + user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values, sources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknownSetting explains why key has no value: it names an account or instance setting that needs
|
||||||
|
// --host or --user, or no setting at all.
|
||||||
|
func unknownSetting(key string) error {
|
||||||
|
switch {
|
||||||
|
case slices.Contains(accountKeys, key):
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("%s is an account setting; select the account with --host URL --user NAME", key))
|
||||||
|
case slices.Contains(instanceKeys, key):
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("%s is an instance setting; select a configured instance with --host URL", key))
|
||||||
|
}
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("unknown setting %q; settings: %s", key, strings.Join(configKeys, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
func (a *application) configCommand() *cobra.Command {
|
func (a *application) configCommand() *cobra.Command {
|
||||||
group := &cobra.Command{Use: "config", Short: "Manage preferences", Long: `Manage preferences.
|
group := &cobra.Command{Use: "config", Short: "Manage settings", Long: `Manage tea's settings.
|
||||||
|
|
||||||
Settings live in config.yml and hosts.yml in the configuration directory
|
Preferences live in config.yml and instance settings in hosts.yml, in the configuration
|
||||||
($TEA_CONFIG_DIR, or tea under the XDG config directory); tea config path prints both.
|
directory ($TEA_CONFIG_DIR, or tea under the XDG config directory); tea config path prints
|
||||||
|
both files. Environment variables that override settings are listed in tea help environment.
|
||||||
|
|
||||||
config.yml preferences (tea config set <key> <value>):
|
Preferences (tea config set <key> <value>):
|
||||||
default_host instance URL used when no --host, environment or repository selects one
|
default_host instance used when no --host, environment variable or repository selects one
|
||||||
git_protocol https or ssh for clone and checkout remotes
|
git_protocol https or ssh, for clone and checkout remotes
|
||||||
editor command used to edit bodies
|
editor command that edits bodies
|
||||||
prompt enabled or disabled
|
prompt enabled or disabled
|
||||||
pager command used to page human output
|
pager command that pages long output
|
||||||
browser command used to open web pages; never disables opening
|
browser command that opens web pages; never disables opening
|
||||||
color auto, always or never
|
color auto, always or never
|
||||||
remote Git remote used for repository discovery
|
hyperlinks auto, always or never: link the first column of lists to its web page
|
||||||
aliases command aliases; manage them with tea alias
|
markdown auto or raw: render markdown bodies on a terminal, or print them as written
|
||||||
|
remote Git remote that selects the repository
|
||||||
|
aliases command shortcuts; manage them with tea alias
|
||||||
|
|
||||||
hosts.yml per-instance settings (tea config set --host <url> <key> <value>):
|
Instance settings (tea config set --host <url> <key> <value>):
|
||||||
user active account, changed only by tea auth switch
|
user active account; change it with tea auth switch
|
||||||
ssh_host SSH host[:port] when it differs from the web host
|
ssh_host SSH host[:port] when it differs from the web host
|
||||||
insecure true skips TLS certificate verification
|
insecure true skips TLS certificate verification
|
||||||
git_protocol https or ssh, overriding config.yml for this instance
|
git_protocol https or ssh, overriding the preference for this instance
|
||||||
oauth_client_id OAuth application used by tea auth login
|
oauth_client_id OAuth application used by tea auth login --oauth
|
||||||
|
|
||||||
Per-account SSH settings (tea config set --host <url> --user <name> <key> <value>):
|
Account settings (tea config set --host <url> --user <name> <key> <value>):
|
||||||
ssh_key, ssh_agent, ssh_certificate_principal, ssh_key_fingerprint
|
ssh_key, ssh_agent, ssh_certificate_principal, ssh_key_fingerprint`}
|
||||||
|
group.AddCommand(&cobra.Command{
|
||||||
Environment overrides:
|
Use: "path", Short: "Print the paths of the settings files", Args: argsExact(0),
|
||||||
TEA_CONFIG_DIR configuration directory
|
Annotations: map[string]string{"tea.text": "config path prints text", "tea.offline": "true"},
|
||||||
TEA_HOST, GITEA_INSTANCE_URL instance, overriding default_host
|
Example: "$ tea config path\n# Settings of another configuration directory\n$ TEA_CONFIG_DIR=~/work/tea tea config path",
|
||||||
GITEA_TOKEN access token for that instance instead of stored credentials
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
GITEA_INSTANCE_INSECURE with GITEA_TOKEN, skip TLS verification when true
|
_, err := fmt.Fprintf(cmd.OutOrStdout(), "config\t%s\nhosts\t%s\n", filepath.Join(a.deps.Settings.Dir, "config.yml"), filepath.Join(a.deps.Settings.Dir, "hosts.yml"))
|
||||||
GITEA_INSTANCE_SSH_HOST with GITEA_TOKEN, SSH host that identifies the instance's remotes
|
return err
|
||||||
TEA_USER account on the selected instance
|
},
|
||||||
TEA_REPO repository as owner/name or URL
|
})
|
||||||
TEA_EDITOR editor, overriding config.yml
|
var getSource bool
|
||||||
TEA_PAGER pager, overriding config.yml
|
get := &cobra.Command{
|
||||||
TEA_COLOR color preference; NO_COLOR disables automatic color
|
Use: "get <key>", Short: "Print the value of a setting", Args: argsExact(1), ValidArgsFunction: a.completeConfig,
|
||||||
VISUAL, EDITOR, PAGER, BROWSER editor (VISUAL first), pager and browser when config.yml sets none`}
|
Example: `$ tea config get editor
|
||||||
group.AddCommand(&cobra.Command{Use: "path", Short: "Show configuration file paths", Annotations: textOutput("config path prints text"), Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
# Show where the value comes from
|
||||||
_, err := fmt.Fprintf(cmd.OutOrStdout(), "config\t%s\nhosts\t%s\n", filepath.Join(a.deps.Settings.Dir, "config.yml"), filepath.Join(a.deps.Settings.Dir, "hosts.yml"))
|
$ tea config get default_host --show-source
|
||||||
return err
|
# Read an instance or account setting
|
||||||
}})
|
$ tea config get ssh_host --host https://gitea.com
|
||||||
for _, name := range []string{"get", "list"} {
|
$ tea config get ssh_key --host https://gitea.com --user alice`,
|
||||||
var source bool
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
command := &cobra.Command{Use: name, Short: "Show effective preferences"}
|
|
||||||
command.Flags().BoolVar(&source, "show-source", false, "Show the source of each effective value")
|
|
||||||
if name == "get" {
|
|
||||||
command.Use = "get <key>"
|
|
||||||
command.Args = argsExact(1)
|
|
||||||
} else {
|
|
||||||
command.Args = argsExact(0)
|
|
||||||
}
|
|
||||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
|
||||||
if a.options.user != "" && a.options.host == "" {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("account settings require both --host and --user"))
|
|
||||||
}
|
|
||||||
snapshot, err := a.deps.Settings.Load()
|
snapshot, err := a.deps.Settings.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
preferences, sources, err := a.effectivePreferences(snapshot)
|
values, sources, err := a.settingValues(snapshot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, exists := snapshot.Hosts.Hosts[preferences.DefaultHost]; a.options.user != "" && !exists {
|
value, ok := values[args[0]]
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("instance is not configured; run tea auth login first"))
|
if !ok {
|
||||||
}
|
return unknownSetting(args[0])
|
||||||
values := map[string]any{
|
|
||||||
"version": preferences.Version, "default_host": preferences.DefaultHost,
|
|
||||||
"git_protocol": preferences.GitProtocol, "editor": preferences.Editor,
|
|
||||||
"prompt": preferences.Prompt, "pager": preferences.Pager,
|
|
||||||
"browser": preferences.Browser, "color": preferences.Color,
|
|
||||||
"remote": preferences.Remote,
|
|
||||||
}
|
|
||||||
if host, exists := snapshot.Hosts.Hosts[preferences.DefaultHost]; a.options.host != "" && exists {
|
|
||||||
for key, value := range map[string]any{"ssh_host": host.SSHHost, "insecure": host.Insecure, "oauth_client_id": host.OAuthClientID} {
|
|
||||||
values[key] = value
|
|
||||||
sources[key] = "hosts.yml " + preferences.DefaultHost
|
|
||||||
}
|
|
||||||
if a.options.user != "" {
|
|
||||||
user, exists := matchAccount(host.Users, a.options.user)
|
|
||||||
if !exists {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("account is not configured on the selected instance"))
|
|
||||||
}
|
|
||||||
account := host.Users[user]
|
|
||||||
for key, value := range map[string]any{"ssh_key": account.SSHKey, "ssh_agent": account.SSHAgent, "ssh_certificate_principal": account.SSHCertPrincipal, "ssh_key_fingerprint": account.SSHKeyFingerprint} {
|
|
||||||
values[key] = value
|
|
||||||
sources[key] = "hosts.yml " + preferences.DefaultHost + " user " + user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(args) > 0 {
|
|
||||||
value, ok := values[args[0]]
|
|
||||||
if !ok {
|
|
||||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("unknown preference %q", args[0]))
|
|
||||||
}
|
|
||||||
values = map[string]any{args[0]: value}
|
|
||||||
}
|
}
|
||||||
if a.options.json != "" {
|
if a.options.json != "" {
|
||||||
if source {
|
return a.writeSettingsJSON(cmd, map[string]any{args[0]: value}, sources, getSource)
|
||||||
results := map[string]any{}
|
|
||||||
for key, value := range values {
|
|
||||||
origin := sources[key]
|
|
||||||
if origin == "" {
|
|
||||||
origin = "default"
|
|
||||||
}
|
|
||||||
results[key] = struct {
|
|
||||||
Value any `json:"value"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
}{value, origin}
|
|
||||||
}
|
|
||||||
return a.writeJSON(cmd, results)
|
|
||||||
}
|
|
||||||
return a.writeJSON(cmd, values)
|
|
||||||
}
|
}
|
||||||
keys := make([]string, 0, len(values))
|
line := fmt.Sprint(value)
|
||||||
for key := range values {
|
if getSource {
|
||||||
keys = append(keys, key)
|
line += "\t" + settingSource(sources, args[0])
|
||||||
}
|
}
|
||||||
sort.Strings(keys)
|
_, err = fmt.Fprintln(cmd.OutOrStdout(), line)
|
||||||
for _, key := range keys {
|
return err
|
||||||
suffix := ""
|
},
|
||||||
if source {
|
}
|
||||||
origin := "default"
|
get.Flags().BoolVar(&getSource, "show-source", false, "Also print where the value comes from")
|
||||||
if sources[key] != "" {
|
group.AddCommand(get)
|
||||||
origin = sources[key]
|
var listSource bool
|
||||||
|
list := &cobra.Command{
|
||||||
|
Use: "list", Short: "List settings and their values", Args: argsExact(0),
|
||||||
|
Example: `$ tea config list
|
||||||
|
# Include the settings of an instance and where each value comes from
|
||||||
|
$ tea config list --host https://gitea.com --show-source`,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
snapshot, err := a.deps.Settings.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values, sources, err := a.settingValues(snapshot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if a.options.json != "" {
|
||||||
|
return a.writeSettingsJSON(cmd, values, sources, listSource)
|
||||||
|
}
|
||||||
|
items := make([]configSetting, 0, len(values))
|
||||||
|
for _, key := range slices.Sorted(maps.Keys(values)) {
|
||||||
|
items = append(items, configSetting{key, values[key], settingSource(sources, key)})
|
||||||
|
}
|
||||||
|
columns := []terminal.Column{{Header: "Key"}, {Header: "Value", Flex: true}}
|
||||||
|
if listSource {
|
||||||
|
columns = append(columns, terminal.Column{Header: "Source"})
|
||||||
|
}
|
||||||
|
return writeTable(a, cmd, items, listSpec[configSetting]{
|
||||||
|
Columns: columns,
|
||||||
|
Row: func(item configSetting) []terminal.Cell {
|
||||||
|
row := []terminal.Cell{{Text: item.Key}, {Text: fmt.Sprint(item.Value)}}
|
||||||
|
if listSource {
|
||||||
|
row = append(row, terminal.Cell{Text: item.Source, Muted: true})
|
||||||
}
|
}
|
||||||
suffix = "\t" + origin
|
return row
|
||||||
}
|
},
|
||||||
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\t%v%s\n", key, values[key], suffix); err != nil {
|
}, listPage{Number: 1})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
list.Flags().BoolVar(&listSource, "show-source", false, "Show where each value comes from")
|
||||||
|
group.AddCommand(list)
|
||||||
|
group.AddCommand(&cobra.Command{
|
||||||
|
Use: "set <key> <value>", Short: "Change a setting", Args: argsExact(2), ValidArgsFunction: a.completeConfig,
|
||||||
|
Annotations: textOutput("config set does not return a resource"),
|
||||||
|
Example: `$ tea config set editor "code --wait"
|
||||||
|
$ tea config set git_protocol ssh
|
||||||
|
# Keep raw markdown and plain IDs on a terminal
|
||||||
|
$ tea config set markdown raw
|
||||||
|
$ tea config set hyperlinks never
|
||||||
|
# Change an instance or account setting
|
||||||
|
$ tea config set ssh_host git.example.com:2222 --host https://example.com
|
||||||
|
$ tea config set ssh_key ~/.ssh/id_ed25519 --host https://example.com --user alice`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
key, value := args[0], args[1]
|
||||||
|
if a.options.user != "" && a.options.host == "" {
|
||||||
|
return clierr.Wrap(clierr.Usage, errors.New("account settings require both --host and --user"))
|
||||||
|
}
|
||||||
|
if a.options.host == "" {
|
||||||
|
value, err := a.setPreference(key, value)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
a.reportSet(cmd, key, value, "")
|
||||||
|
if key == "default_host" && value != "" {
|
||||||
|
a.warnWithoutAccount(cmd)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
instance, err := a.setInstanceSetting(key, value)
|
||||||
}
|
|
||||||
group.AddCommand(command)
|
|
||||||
}
|
|
||||||
group.AddCommand(&cobra.Command{Use: "set <key> <value>", Short: "Set a preference, or an instance setting with --host", Annotations: textOutput("config set does not return a resource"), Args: argsExact(2), RunE: func(_ *cobra.Command, args []string) error {
|
|
||||||
if a.options.user != "" && a.options.host == "" {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("account settings require both --host and --user"))
|
|
||||||
}
|
|
||||||
if a.options.host != "" {
|
|
||||||
instance, err := settings.NormalizeHost(a.options.host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return clierr.Wrap(clierr.Usage, err)
|
return err
|
||||||
}
|
}
|
||||||
return a.deps.Settings.Update(settings.TargetHosts, func(s *settings.Snapshot) error {
|
scope := " for " + instance
|
||||||
host, exists := s.Hosts.Hosts[instance]
|
if a.options.user != "" {
|
||||||
if !exists {
|
scope = " for " + a.options.user + " on " + instance
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("instance is not configured; run tea auth login first"))
|
|
||||||
}
|
|
||||||
if a.options.user != "" {
|
|
||||||
user, exists := matchAccount(host.Users, a.options.user)
|
|
||||||
if !exists {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("account is not configured on the selected instance"))
|
|
||||||
}
|
|
||||||
account := host.Users[user]
|
|
||||||
switch args[0] {
|
|
||||||
case "ssh_key":
|
|
||||||
account.SSHKey = args[1]
|
|
||||||
case "ssh_agent":
|
|
||||||
if args[1] != "true" && args[1] != "false" {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("ssh_agent must be true or false"))
|
|
||||||
}
|
|
||||||
account.SSHAgent = args[1] == "true"
|
|
||||||
case "ssh_certificate_principal":
|
|
||||||
account.SSHCertPrincipal = args[1]
|
|
||||||
case "ssh_key_fingerprint":
|
|
||||||
account.SSHKeyFingerprint = args[1]
|
|
||||||
default:
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("supported account settings: ssh_key, ssh_agent, ssh_certificate_principal, ssh_key_fingerprint"))
|
|
||||||
}
|
|
||||||
host.Users[user] = account
|
|
||||||
s.Hosts.Hosts[instance] = host
|
|
||||||
return clierr.Wrap(clierr.Usage, s.Validate())
|
|
||||||
}
|
|
||||||
switch args[0] {
|
|
||||||
case "git_protocol":
|
|
||||||
host.GitProtocol = args[1]
|
|
||||||
case "ssh_host":
|
|
||||||
host.SSHHost = args[1]
|
|
||||||
case "insecure":
|
|
||||||
if args[1] != "true" && args[1] != "false" {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("insecure must be true or false"))
|
|
||||||
}
|
|
||||||
host.Insecure = args[1] == "true"
|
|
||||||
case "oauth_client_id":
|
|
||||||
host.OAuthClientID = args[1]
|
|
||||||
default:
|
|
||||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("preference %q cannot be set per instance; supported keys: git_protocol, ssh_host, insecure, oauth_client_id", args[0]))
|
|
||||||
}
|
|
||||||
s.Hosts.Hosts[instance] = host
|
|
||||||
return clierr.Wrap(clierr.Usage, s.Validate())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return a.deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error {
|
|
||||||
p := &s.Preferences
|
|
||||||
switch args[0] {
|
|
||||||
case "default_host":
|
|
||||||
if args[1] == "" {
|
|
||||||
p.DefaultHost = ""
|
|
||||||
break
|
|
||||||
}
|
|
||||||
normalized, err := settings.NormalizeHost(args[1])
|
|
||||||
if err != nil {
|
|
||||||
return clierr.Wrap(clierr.Usage, err)
|
|
||||||
}
|
|
||||||
p.DefaultHost = normalized
|
|
||||||
case "git_protocol":
|
|
||||||
p.GitProtocol = args[1]
|
|
||||||
case "editor":
|
|
||||||
p.Editor = args[1]
|
|
||||||
case "prompt":
|
|
||||||
p.Prompt = args[1]
|
|
||||||
case "pager":
|
|
||||||
p.Pager = args[1]
|
|
||||||
case "browser":
|
|
||||||
p.Browser = args[1]
|
|
||||||
case "color":
|
|
||||||
p.Color = args[1]
|
|
||||||
case "remote":
|
|
||||||
p.Remote = args[1]
|
|
||||||
default:
|
|
||||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("unknown or immutable preference %q", args[0]))
|
|
||||||
}
|
}
|
||||||
return clierr.Wrap(clierr.Usage, s.Validate())
|
a.reportSet(cmd, key, value, scope)
|
||||||
})
|
return nil
|
||||||
}})
|
},
|
||||||
|
})
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *application) aliasCommand() *cobra.Command {
|
// settingSource names where the value of key comes from.
|
||||||
group := &cobra.Command{Use: "alias", Short: "Manage shortcuts to built-in commands"}
|
func settingSource(sources map[string]string, key string) string {
|
||||||
group.AddCommand(&cobra.Command{Use: "set <name> <expansion>", Short: "Define a command alias", Annotations: textOutput("alias set does not return a resource"), Args: argsExact(2), RunE: func(cmd *cobra.Command, args []string) error {
|
if source := sources[key]; source != "" {
|
||||||
return a.deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error {
|
return source
|
||||||
s.Preferences.Aliases[args[0]] = args[1]
|
}
|
||||||
return clierr.Wrap(clierr.Usage, aliases.Check(args[0], s.Preferences.Aliases, builtins(cmd.Root())))
|
return "default"
|
||||||
})
|
}
|
||||||
}})
|
|
||||||
group.AddCommand(&cobra.Command{Use: "delete <name>", Short: "Remove a command alias", Annotations: textOutput("alias deletion does not return a resource"), Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
// writeSettingsJSON writes settings as a key-value object, or with source as objects of value and source.
|
||||||
return a.deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error {
|
func (a *application) writeSettingsJSON(cmd *cobra.Command, values map[string]any, sources map[string]string, source bool) error {
|
||||||
if _, ok := s.Preferences.Aliases[args[0]]; !ok {
|
if !source {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("alias does not exist"))
|
return a.writeJSON(cmd, values)
|
||||||
|
}
|
||||||
|
type sourced struct {
|
||||||
|
Value any `json:"value"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
results := map[string]sourced{}
|
||||||
|
for key, value := range values {
|
||||||
|
results[key] = sourced{value, settingSource(sources, key)}
|
||||||
|
}
|
||||||
|
return a.writeJSON(cmd, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reportSet confirms a changed setting; scope names the instance or account it belongs to.
|
||||||
|
func (a *application) reportSet(cmd *cobra.Command, key, value, scope string) {
|
||||||
|
if value == "" {
|
||||||
|
a.done(cmd, "Cleared %s%s", key, scope)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.done(cmd, "Set %s%s to %s", key, scope, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// warnWithoutAccount points out a default_host that has no account to act as yet.
|
||||||
|
func (a *application) warnWithoutAccount(cmd *cobra.Command) {
|
||||||
|
snapshot, err := a.deps.Settings.Load()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setPreference changes a preference in config.yml and returns the stored value, such as a
|
||||||
|
// normalized default_host.
|
||||||
|
func (a *application) setPreference(key, value string) (string, error) {
|
||||||
|
return value, a.deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error {
|
||||||
|
p := &s.Preferences
|
||||||
|
switch key {
|
||||||
|
case "default_host":
|
||||||
|
if value != "" {
|
||||||
|
normalized, err := settings.ResolveHost(value, s.Hosts.Hosts)
|
||||||
|
if err != nil {
|
||||||
|
return clierr.Wrap(clierr.Usage, err)
|
||||||
|
}
|
||||||
|
value = normalized
|
||||||
|
}
|
||||||
|
p.DefaultHost = value
|
||||||
|
case "git_protocol":
|
||||||
|
p.GitProtocol = value
|
||||||
|
case "editor":
|
||||||
|
p.Editor = value
|
||||||
|
case "prompt":
|
||||||
|
p.Prompt = value
|
||||||
|
case "pager":
|
||||||
|
p.Pager = value
|
||||||
|
case "browser":
|
||||||
|
p.Browser = value
|
||||||
|
case "color":
|
||||||
|
p.Color = value
|
||||||
|
case "hyperlinks":
|
||||||
|
p.Hyperlinks = value
|
||||||
|
case "markdown":
|
||||||
|
p.Markdown = value
|
||||||
|
case "remote":
|
||||||
|
p.Remote = value
|
||||||
|
case "user":
|
||||||
|
return clierr.Wrap(clierr.Usage, errors.New("the active account changes with tea auth switch"))
|
||||||
|
default:
|
||||||
|
if slices.Contains(instanceKeys, key) || slices.Contains(accountKeys, key) {
|
||||||
|
return unknownSetting(key)
|
||||||
|
}
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("unknown setting %q; preferences: %s", key, strings.Join(preferenceKeys, ", ")))
|
||||||
|
}
|
||||||
|
return clierr.Wrap(clierr.Usage, s.Validate())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// setInstanceSetting changes a setting of the --host instance in hosts.yml, or with --user one of its
|
||||||
|
// account's settings, and returns the instance.
|
||||||
|
func (a *application) setInstanceSetting(key, value string) (string, error) {
|
||||||
|
var instance string
|
||||||
|
return instance, a.deps.Settings.Update(settings.TargetHosts, func(s *settings.Snapshot) error {
|
||||||
|
var err error
|
||||||
|
if instance, err = settings.ResolveHost(a.options.host, s.Hosts.Hosts); err != nil {
|
||||||
|
return clierr.Wrap(clierr.Usage, err)
|
||||||
|
}
|
||||||
|
host, exists := s.Hosts.Hosts[instance]
|
||||||
|
if !exists {
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("instance %s is not configured; run tea auth login --host %s", instance, instance))
|
||||||
|
}
|
||||||
|
if a.options.user != "" {
|
||||||
|
user, exists := matchAccount(host.Users, a.options.user)
|
||||||
|
if !exists {
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("account %q is not logged in on %s (accounts: %s)", a.options.user, instance, accountNames(host.Users)))
|
||||||
|
}
|
||||||
|
account := host.Users[user]
|
||||||
|
switch key {
|
||||||
|
case "ssh_key":
|
||||||
|
account.SSHKey = value
|
||||||
|
case "ssh_agent":
|
||||||
|
if value != "true" && value != "false" {
|
||||||
|
return clierr.Wrap(clierr.Usage, errors.New("ssh_agent must be true or false"))
|
||||||
|
}
|
||||||
|
account.SSHAgent = value == "true"
|
||||||
|
case "ssh_certificate_principal":
|
||||||
|
account.SSHCertPrincipal = value
|
||||||
|
case "ssh_key_fingerprint":
|
||||||
|
account.SSHKeyFingerprint = value
|
||||||
|
default:
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("%q is not an account setting; account settings: %s", key, strings.Join(accountKeys, ", ")))
|
||||||
|
}
|
||||||
|
host.Users[user] = account
|
||||||
|
s.Hosts.Hosts[instance] = host
|
||||||
|
return clierr.Wrap(clierr.Usage, s.Validate())
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "git_protocol":
|
||||||
|
host.GitProtocol = value
|
||||||
|
case "ssh_host":
|
||||||
|
host.SSHHost = value
|
||||||
|
case "insecure":
|
||||||
|
if value != "true" && value != "false" {
|
||||||
|
return clierr.Wrap(clierr.Usage, errors.New("insecure must be true or false"))
|
||||||
|
}
|
||||||
|
host.Insecure = value == "true"
|
||||||
|
case "oauth_client_id":
|
||||||
|
host.OAuthClientID = value
|
||||||
|
case "user":
|
||||||
|
return clierr.Wrap(clierr.Usage, errors.New("the active account changes with tea auth switch"))
|
||||||
|
default:
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("%q is not an instance setting; instance settings: git_protocol, insecure, oauth_client_id, ssh_host", key))
|
||||||
|
}
|
||||||
|
s.Hosts.Hosts[instance] = host
|
||||||
|
return clierr.Wrap(clierr.Usage, s.Validate())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) aliasCommand() *cobra.Command {
|
||||||
|
group := &cobra.Command{Use: "alias", Short: "Create shortcuts for tea commands", Long: `Create shortcuts for tea commands.
|
||||||
|
|
||||||
|
An alias expands to a tea command with its arguments before tea runs it: after
|
||||||
|
tea alias set il "issue list --label bug", tea il --state all runs
|
||||||
|
tea issue list --label bug --state all. $1, $2 and so on in the expansion take the
|
||||||
|
arguments in that position; other arguments follow the expansion. Aliases never run a
|
||||||
|
shell, and built-in commands always win over an alias of the same name.`}
|
||||||
|
var clobber bool
|
||||||
|
set := &cobra.Command{
|
||||||
|
Use: "set <name> <expansion>", Short: "Create a shortcut for a tea command",
|
||||||
|
Annotations: textOutput("alias set does not return a resource"),
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) > 2 {
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("the expansion must be one argument; quote it: tea alias set %s %q", args[0], strings.Join(args[1:], " ")))
|
||||||
|
}
|
||||||
|
return argsExact(2)(cmd, args)
|
||||||
|
},
|
||||||
|
Example: `# tea il runs tea issue list --label bug
|
||||||
|
$ tea alias set il "issue list --label bug"
|
||||||
|
# $1 takes the first argument: tea review 13 runs tea pr review 13 --approve
|
||||||
|
$ tea alias set review 'pr review $1 --approve'
|
||||||
|
# Replace an existing alias
|
||||||
|
$ tea alias set il "issue list --assignee @me" --clobber`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
name, expansion := args[0], strings.TrimPrefix(strings.TrimSpace(args[1]), "tea ")
|
||||||
|
var previous string
|
||||||
|
err := a.deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error {
|
||||||
|
previous = s.Preferences.Aliases[name]
|
||||||
|
if previous != "" && !clobber {
|
||||||
|
return fmt.Errorf("alias %s already expands to %q; replace it with --clobber", name, previous)
|
||||||
|
}
|
||||||
|
s.Preferences.Aliases[name] = expansion
|
||||||
|
return clierr.Wrap(clierr.Usage, aliases.Check(name, s.Preferences.Aliases, builtins(cmd.Root())))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if previous != "" {
|
||||||
|
a.done(cmd, "Changed alias %s from %q to %q", name, previous, expansion)
|
||||||
|
} else {
|
||||||
|
a.done(cmd, "Added alias %s for %q", name, expansion)
|
||||||
}
|
}
|
||||||
delete(s.Preferences.Aliases, args[0])
|
|
||||||
return nil
|
return nil
|
||||||
})
|
},
|
||||||
}})
|
}
|
||||||
group.AddCommand(&cobra.Command{Use: "list", Short: "List command aliases", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
set.Flags().BoolVar(&clobber, "clobber", false, "Replace an existing alias of the same name")
|
||||||
s, err := a.deps.Settings.Load()
|
set.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
|
||||||
if err != nil {
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("%w; quote an expansion with flags: tea alias set il \"issue list --state all\"", err))
|
||||||
return err
|
})
|
||||||
}
|
group.AddCommand(set)
|
||||||
if a.options.json != "" {
|
var all bool
|
||||||
return a.writeJSON(cmd, s.Preferences.Aliases)
|
remove := &cobra.Command{
|
||||||
}
|
Use: "delete [<name>...]", Short: "Delete shortcuts", ValidArgsFunction: a.completeAliases,
|
||||||
names := make([]string, 0, len(s.Preferences.Aliases))
|
Annotations: textOutput("alias deletion does not return a resource"),
|
||||||
for name := range s.Preferences.Aliases {
|
Args: func(_ *cobra.Command, args []string) error {
|
||||||
names = append(names, name)
|
if len(args) == 0 && !all {
|
||||||
}
|
return clierr.Wrap(clierr.Usage, errors.New("name the aliases to delete, or pass --all"))
|
||||||
sort.Strings(names)
|
}
|
||||||
return a.writeHuman(cmd, table(names, func(name string) []terminal.Cell { return terminal.Row(name, s.Preferences.Aliases[name]) }, a.colorEnabled()))
|
if len(args) > 0 && all {
|
||||||
}})
|
return clierr.Wrap(clierr.Usage, errors.New("alias names and --all are mutually exclusive"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Example: "$ tea alias delete il\n$ tea alias delete il review\n$ tea alias delete --all",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
deleted := map[string]string{}
|
||||||
|
err := a.deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error {
|
||||||
|
names := args
|
||||||
|
if all {
|
||||||
|
names = slices.Collect(maps.Keys(s.Preferences.Aliases))
|
||||||
|
}
|
||||||
|
var missing []string
|
||||||
|
for _, name := range names {
|
||||||
|
expansion, ok := s.Preferences.Aliases[name]
|
||||||
|
if !ok {
|
||||||
|
missing = append(missing, strconv.Quote(name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deleted[name] = expansion
|
||||||
|
delete(s.Preferences.Aliases, name)
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("no alias named %s; tea alias list shows them", strings.Join(missing, ", ")))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(deleted) == 0 {
|
||||||
|
a.warn(cmd, "No aliases to delete")
|
||||||
|
}
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(deleted)) {
|
||||||
|
a.done(cmd, "Deleted alias %s (%s)", name, deleted[name])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
remove.Flags().BoolVar(&all, "all", false, "Delete every alias")
|
||||||
|
group.AddCommand(remove)
|
||||||
|
group.AddCommand(&cobra.Command{
|
||||||
|
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",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
s, err := a.deps.Settings.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if a.options.json != "" {
|
||||||
|
return a.writeJSON(cmd, s.Preferences.Aliases)
|
||||||
|
}
|
||||||
|
return writeTable(a, cmd, slices.Sorted(maps.Keys(s.Preferences.Aliases)), listSpec[string]{
|
||||||
|
Columns: []terminal.Column{{Header: "Name"}, {Header: "Expansion", Flex: true}},
|
||||||
|
Row: func(name string) []terminal.Cell {
|
||||||
|
return []terminal.Cell{{Text: name, Bold: true}, {Text: s.Preferences.Aliases[name]}}
|
||||||
|
},
|
||||||
|
Empty: "no aliases configured; create one with: tea alias set <name> <expansion>",
|
||||||
|
}, listPage{Number: 1})
|
||||||
|
},
|
||||||
|
})
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
+164
-3
@@ -7,7 +7,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.dev/tea/internal/clierr"
|
"gitea.dev/tea/internal/clierr"
|
||||||
"gitea.dev/tea/internal/settings"
|
"gitea.dev/tea/internal/settings"
|
||||||
@@ -94,11 +96,170 @@ func TestConfigHelpDocumentsSettingsAndEnvironment(t *testing.T) {
|
|||||||
deps, out, _ := testDeps(t)
|
deps, out, _ := testDeps(t)
|
||||||
require.NoError(t, Execute(context.Background(), deps, []string{"config", "--help"}))
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "--help"}))
|
||||||
for _, name := range []string{
|
for _, name := range []string{
|
||||||
"default_host", "git_protocol", "editor", "prompt", "pager", "browser", "color", "remote", "aliases",
|
"default_host", "git_protocol", "editor", "prompt", "pager", "browser", "color", "hyperlinks", "markdown", "remote", "aliases",
|
||||||
"user", "ssh_host", "insecure", "oauth_client_id", "ssh_key", "ssh_agent", "ssh_certificate_principal", "ssh_key_fingerprint",
|
"user", "ssh_host", "insecure", "oauth_client_id", "ssh_key", "ssh_agent", "ssh_certificate_principal", "ssh_key_fingerprint",
|
||||||
"TEA_CONFIG_DIR", "TEA_HOST", "GITEA_INSTANCE_URL", "GITEA_TOKEN", "TEA_USER", "TEA_REPO", "TEA_EDITOR", "VISUAL", "EDITOR",
|
"tea help environment",
|
||||||
"TEA_PAGER", "PAGER", "TEA_COLOR", "NO_COLOR", "BROWSER",
|
|
||||||
} {
|
} {
|
||||||
require.Contains(t, out.String(), name)
|
require.Contains(t, out.String(), name)
|
||||||
}
|
}
|
||||||
|
require.NotContains(t, out.String(), "GITEA_TOKEN", "the environment table lives in tea help environment")
|
||||||
|
out.Reset()
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"help", "environment"}))
|
||||||
|
for _, name := range []string{"TEA_CONFIG_DIR", "TEA_HOST", "GITEA_INSTANCE_URL", "GITEA_TOKEN", "TEA_USER", "TEA_REPO", "TEA_EDITOR", "VISUAL", "EDITOR", "TEA_PAGER", "PAGER", "TEA_COLOR", "NO_COLOR", "BROWSER"} {
|
||||||
|
require.Contains(t, out.String(), name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigGetPrintsOnlyTheValue(t *testing.T) {
|
||||||
|
deps, out, _ := testDeps(t)
|
||||||
|
configureHost(t, deps, "http://localhost:3001", settings.Host{User: "alice", Users: tokenAccounts(settings.StoreFile, "alice", "bob")})
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "set", "editor", "code --wait"}))
|
||||||
|
for _, tc := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"config", "get", "editor"}, "code --wait\n"},
|
||||||
|
{[]string{"config", "get", "editor", "--show-source"}, "code --wait\tconfig.yml\n"},
|
||||||
|
{[]string{"config", "get", "pager"}, "\n"},
|
||||||
|
{[]string{"config", "get", "hyperlinks", "--show-source"}, "auto\tdefault\n"},
|
||||||
|
{[]string{"config", "get", "user", "--host", "http://localhost:3001"}, "alice\n"},
|
||||||
|
{[]string{"config", "get", "user", "--host", "localhost:3001", "--show-source"}, "alice\thosts.yml http://localhost:3001\n"},
|
||||||
|
{[]string{"config", "get", "user"}, "alice\n"},
|
||||||
|
{[]string{"config", "get", "insecure"}, "false\n"},
|
||||||
|
} {
|
||||||
|
t.Run(strings.Join(tc.args[2:], " "), func(t *testing.T) {
|
||||||
|
out.Reset()
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, tc.args))
|
||||||
|
require.Equal(t, tc.want, out.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigNamesValidSettings(t *testing.T) {
|
||||||
|
deps, _, _ := testDeps(t)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"config", "get", "nope"}, `unknown setting "nope"; settings: browser, color, default_host, editor, git_protocol, hyperlinks, insecure, markdown,`},
|
||||||
|
{[]string{"config", "get", "user"}, "user is an instance setting; select a configured instance with --host URL"},
|
||||||
|
{[]string{"config", "get", "ssh_key"}, "ssh_key is an account setting; select the account with --host URL --user NAME"},
|
||||||
|
{[]string{"config", "set", "nope", "1"}, `unknown setting "nope"; preferences: browser, color, default_host,`},
|
||||||
|
{[]string{"config", "set", "user", "bob"}, "the active account changes with tea auth switch"},
|
||||||
|
{[]string{"config", "set", "hyperlinks", "off"}, "hyperlinks must be auto, always, or never"},
|
||||||
|
{[]string{"config", "set", "markdown", "never"}, "markdown must be auto or raw"},
|
||||||
|
} {
|
||||||
|
t.Run(strings.Join(tc.args[1:], " "), func(t *testing.T) {
|
||||||
|
err := Execute(context.Background(), deps, tc.args)
|
||||||
|
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
|
||||||
|
require.ErrorContains(t, err, tc.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigPathWorksWithBrokenSettings(t *testing.T) {
|
||||||
|
deps, out, _ := testDeps(t)
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(deps.Settings.Dir, "hosts.yml"), []byte("version: '1'\nhosts: [\n"), 0o600))
|
||||||
|
require.Error(t, Execute(context.Background(), deps, []string{"config", "list"}))
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "path"}))
|
||||||
|
require.Equal(t, "config\t"+filepath.Join(deps.Settings.Dir, "config.yml")+"\nhosts\t"+filepath.Join(deps.Settings.Dir, "hosts.yml")+"\n", out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSetReportsChangesOnATerminal(t *testing.T) {
|
||||||
|
deps, out, diagnostics := testDeps(t)
|
||||||
|
deps.Streams.OutTTY = true
|
||||||
|
configureHost(t, deps, "https://example.org", settings.Host{User: "alice", Users: tokenAccounts(settings.StoreFile, "alice")})
|
||||||
|
for _, tc := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"config", "set", "markdown", "raw"}, "✓ Set markdown to raw\n"},
|
||||||
|
{[]string{"config", "set", "editor", ""}, "✓ Cleared editor\n"},
|
||||||
|
{[]string{"config", "set", "ssh_host", "git.example.org:2222", "--host", "example.org"}, "✓ Set ssh_host for https://example.org to git.example.org:2222\n"},
|
||||||
|
{[]string{"config", "set", "default_host", "https://example.org"}, "✓ Set default_host to https://example.org\n"},
|
||||||
|
{[]string{"config", "set", "default_host", "localhost:3002"}, "✓ Set default_host to https://localhost:3002\n! No account on https://localhost:3002 yet; log in with: tea auth login --host https://localhost:3002\n"},
|
||||||
|
} {
|
||||||
|
t.Run(strings.Join(tc.args[2:], " "), func(t *testing.T) {
|
||||||
|
diagnostics.Reset()
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, tc.args))
|
||||||
|
require.Equal(t, tc.want, withoutANSI(diagnostics.String()))
|
||||||
|
require.Empty(t, out.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
snapshot, err := deps.Settings.Load()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "raw", snapshot.Preferences.Markdown)
|
||||||
|
require.Equal(t, "git.example.org:2222", snapshot.Hosts.Hosts["https://example.org"].SSHHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplayFollowsHyperlinkAndMarkdownPreferences(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
config []string
|
||||||
|
links, rich bool
|
||||||
|
}{
|
||||||
|
{nil, true, true},
|
||||||
|
{[]string{"hyperlinks", "never"}, false, true},
|
||||||
|
{[]string{"markdown", "raw"}, true, false},
|
||||||
|
} {
|
||||||
|
deps, _, _ := testDeps(t)
|
||||||
|
deps.Streams.OutTTY, deps.Now = true, time.Now
|
||||||
|
for i := 0; i < len(tc.config); i += 2 {
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "set", tc.config[i], tc.config[i+1]}))
|
||||||
|
}
|
||||||
|
d := (&application{deps: deps}).display()
|
||||||
|
require.Equal(t, tc.links, d.Links, tc.config)
|
||||||
|
require.Equal(t, tc.rich, d.Markdown, tc.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAliasSetGuardsExistingAliasesAndQuoting(t *testing.T) {
|
||||||
|
deps, _, diagnostics := testDeps(t)
|
||||||
|
deps.Streams.OutTTY = true
|
||||||
|
run := func(args ...string) error {
|
||||||
|
return Execute(context.Background(), deps, append([]string{"alias", "set"}, args...))
|
||||||
|
}
|
||||||
|
require.NoError(t, run("il", "tea issue list --label bug"))
|
||||||
|
require.Equal(t, "✓ Added alias il for \"issue list --label bug\"\n", withoutANSI(diagnostics.String()))
|
||||||
|
err := run("il", "issue list")
|
||||||
|
require.ErrorContains(t, err, `alias il already expands to "issue list --label bug"; replace it with --clobber`)
|
||||||
|
diagnostics.Reset()
|
||||||
|
require.NoError(t, run("il", "issue list --state all", "--clobber"))
|
||||||
|
require.Equal(t, "✓ Changed alias il from \"issue list --label bug\" to \"issue list --state all\"\n", withoutANSI(diagnostics.String()))
|
||||||
|
err = run("ls", "issue", "list")
|
||||||
|
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
|
||||||
|
require.EqualError(t, err, `the expansion must be one argument; quote it: tea alias set ls "issue list"`)
|
||||||
|
err = run("il", "issue", "list", "--state", "all")
|
||||||
|
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
|
||||||
|
require.ErrorContains(t, err, `unknown flag: --state; quote an expansion with flags: tea alias set il "issue list --state all"`)
|
||||||
|
snapshot, err := deps.Settings.Load()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, map[string]string{"il": "issue list --state all"}, snapshot.Preferences.Aliases)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAliasDeleteAndList(t *testing.T) {
|
||||||
|
deps, out, diagnostics := testDeps(t)
|
||||||
|
deps.Streams.OutTTY = true
|
||||||
|
run := func(args ...string) error {
|
||||||
|
return Execute(context.Background(), deps, append([]string{"alias"}, args...))
|
||||||
|
}
|
||||||
|
require.NoError(t, run("list"))
|
||||||
|
require.Empty(t, out.String())
|
||||||
|
require.Equal(t, "no aliases configured; create one with: tea alias set <name> <expansion>\n", diagnostics.String())
|
||||||
|
for _, name := range []string{"a", "b", "c"} {
|
||||||
|
require.NoError(t, run("set", name, "issue list"))
|
||||||
|
}
|
||||||
|
diagnostics.Reset()
|
||||||
|
require.NoError(t, run("list"))
|
||||||
|
require.Equal(t, "NAME EXPANSION\na issue list\nb issue list\nc issue list\n", withoutANSI(out.String()))
|
||||||
|
err := run("delete", "a", "nope", "zz")
|
||||||
|
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
|
||||||
|
require.EqualError(t, err, `no alias named "nope", "zz"; tea alias list shows them`)
|
||||||
|
require.NoError(t, run("delete", "a", "b"))
|
||||||
|
require.Equal(t, "✓ Deleted alias a (issue list)\n✓ Deleted alias b (issue list)\n", withoutANSI(diagnostics.String()))
|
||||||
|
require.Equal(t, clierr.Usage, clierr.ExitCode(run("delete")))
|
||||||
|
require.Equal(t, clierr.Usage, clierr.ExitCode(run("delete", "c", "--all")))
|
||||||
|
require.NoError(t, run("delete", "--all"))
|
||||||
|
snapshot, err := deps.Settings.Load()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, snapshot.Preferences.Aliases)
|
||||||
}
|
}
|
||||||
@@ -30,8 +30,7 @@ func (a *application) colorEnabled() bool {
|
|||||||
return terminal.ColorEnabled(preferences.Color, a.deps.Streams.OutTTY, a.deps.Getenv)
|
return terminal.ColorEnabled(preferences.Color, a.deps.Streams.OutTTY, a.deps.Getenv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// display describes stdout for tables and views. The hyperlinks (auto, always, never) and markdown
|
// display describes stdout for tables and views under the color, hyperlinks and markdown preferences.
|
||||||
// (auto, raw) preferences are read from TEA_HYPERLINKS and TEA_MARKDOWN until config.yml stores them.
|
|
||||||
func (a *application) display() terminal.Display {
|
func (a *application) display() terminal.Display {
|
||||||
preferences, _ := a.preferences()
|
preferences, _ := a.preferences()
|
||||||
streams := a.deps.Streams
|
streams := a.deps.Streams
|
||||||
@@ -39,8 +38,8 @@ func (a *application) display() terminal.Display {
|
|||||||
TTY: streams.OutTTY,
|
TTY: streams.OutTTY,
|
||||||
Color: terminal.ColorEnabled(preferences.Color, streams.OutTTY, a.deps.Getenv),
|
Color: terminal.ColorEnabled(preferences.Color, streams.OutTTY, a.deps.Getenv),
|
||||||
TrueColor: terminal.TrueColor(a.deps.Getenv),
|
TrueColor: terminal.TrueColor(a.deps.Getenv),
|
||||||
Links: terminal.HyperlinksEnabled(a.deps.Getenv("TEA_HYPERLINKS"), streams.OutTTY, a.deps.Getenv),
|
Links: terminal.HyperlinksEnabled(preferences.Hyperlinks, streams.OutTTY, a.deps.Getenv),
|
||||||
Markdown: terminal.MarkdownEnabled(a.deps.Getenv("TEA_MARKDOWN"), streams.OutTTY),
|
Markdown: terminal.MarkdownEnabled(preferences.Markdown, streams.OutTTY),
|
||||||
Width: streams.Width,
|
Width: streams.Width,
|
||||||
Now: a.deps.Now(),
|
Now: a.deps.Now(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.dev/tea/internal/settings"
|
||||||
"gitea.dev/tea/internal/terminal"
|
"gitea.dev/tea/internal/terminal"
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -51,6 +52,7 @@ func pagedDeps(t *testing.T, serverURL string, env map[string]string, config ...
|
|||||||
for i := 0; i < len(config); i += 2 {
|
for i := 0; i < len(config); i += 2 {
|
||||||
require.NoError(t, Execute(context.Background(), deps, []string{"config", "set", config[i], config[i+1]}))
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "set", config[i], config[i+1]}))
|
||||||
}
|
}
|
||||||
|
diagnostics.Reset()
|
||||||
return deps, out, diagnostics, pager
|
return deps, out, diagnostics, pager
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +176,7 @@ func TestMultiItemHumanOutputPagesOnce(t *testing.T) {
|
|||||||
{[]string{"issue", "comment", "list", "5"}, "1 alice\none\n\n2 bob\ntwo\n\n"},
|
{[]string{"issue", "comment", "list", "5"}, "1 alice\none\n\n2 bob\ntwo\n\n"},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.args[0]+" "+tc.args[1], func(t *testing.T) {
|
t.Run(tc.args[0]+" "+tc.args[1], func(t *testing.T) {
|
||||||
deps, out, _, pager := pagedDeps(t, server.URL, map[string]string{"TEA_HYPERLINKS": "never"}, "pager", "fake-pager", "color", "never")
|
deps, out, _, pager := pagedDeps(t, server.URL, nil, "pager", "fake-pager", "color", "never", "hyperlinks", "never")
|
||||||
require.NoError(t, Execute(context.Background(), deps, append(tc.args, "-R", "alice/project", "--yes")))
|
require.NoError(t, Execute(context.Background(), deps, append(tc.args, "-R", "alice/project", "--yes")))
|
||||||
require.Equal(t, 1, pager.calls)
|
require.Equal(t, 1, pager.calls)
|
||||||
require.Equal(t, tc.want, pager.input)
|
require.Equal(t, tc.want, pager.input)
|
||||||
@@ -341,7 +343,11 @@ func TestWriteView(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
deps, out, _ := testDeps(t)
|
deps, out, _ := testDeps(t)
|
||||||
deps.Streams.OutTTY = tc.tty
|
deps.Streams.OutTTY = tc.tty
|
||||||
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1", "TEA_MARKDOWN": "raw", "TEA_HYPERLINKS": "never"})
|
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1"})
|
||||||
|
require.NoError(t, deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error {
|
||||||
|
s.Preferences.Markdown, s.Preferences.Hyperlinks = "raw", "never"
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
a, cmd := outputApplication(deps, tc.json)
|
a, cmd := outputApplication(deps, tc.json)
|
||||||
require.NoError(t, writeView(a, cmd, item, view))
|
require.NoError(t, writeView(a, cmd, item, view))
|
||||||
require.Equal(t, tc.want, out.String())
|
require.Equal(t, tc.want, out.String())
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ type Preferences struct {
|
|||||||
Pager string `yaml:"pager,omitempty" json:"pager"`
|
Pager string `yaml:"pager,omitempty" json:"pager"`
|
||||||
Browser string `yaml:"browser,omitempty" json:"browser"`
|
Browser string `yaml:"browser,omitempty" json:"browser"`
|
||||||
Color string `yaml:"color,omitempty" json:"color"`
|
Color string `yaml:"color,omitempty" json:"color"`
|
||||||
|
Hyperlinks string `yaml:"hyperlinks,omitempty" json:"hyperlinks"`
|
||||||
|
Markdown string `yaml:"markdown,omitempty" json:"markdown"`
|
||||||
Remote string `yaml:"remote,omitempty" json:"remote"`
|
Remote string `yaml:"remote,omitempty" json:"remote"`
|
||||||
Aliases map[string]string `yaml:"aliases,omitempty" json:"aliases"`
|
Aliases map[string]string `yaml:"aliases,omitempty" json:"aliases"`
|
||||||
}
|
}
|
||||||
@@ -114,7 +116,7 @@ func New(dir string) *Store {
|
|||||||
|
|
||||||
// Defaults returns a fresh preferences value with built-in settings.
|
// Defaults returns a fresh preferences value with built-in settings.
|
||||||
func Defaults() Preferences {
|
func Defaults() Preferences {
|
||||||
return Preferences{Version: Version, GitProtocol: "https", Prompt: "enabled", Color: "auto", Aliases: map[string]string{}}
|
return Preferences{Version: Version, GitProtocol: "https", Prompt: "enabled", Color: "auto", Hyperlinks: "auto", Markdown: "auto", Aliases: map[string]string{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stored omits built-in defaults so that only explicit settings are persisted.
|
// stored omits built-in defaults so that only explicit settings are persisted.
|
||||||
@@ -129,6 +131,12 @@ func (p Preferences) stored() Preferences {
|
|||||||
if p.Color == defaults.Color {
|
if p.Color == defaults.Color {
|
||||||
p.Color = ""
|
p.Color = ""
|
||||||
}
|
}
|
||||||
|
if p.Hyperlinks == defaults.Hyperlinks {
|
||||||
|
p.Hyperlinks = ""
|
||||||
|
}
|
||||||
|
if p.Markdown == defaults.Markdown {
|
||||||
|
p.Markdown = ""
|
||||||
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +255,12 @@ func (s Snapshot) Validate() error {
|
|||||||
if p.Color != "auto" && p.Color != "always" && p.Color != "never" {
|
if p.Color != "auto" && p.Color != "always" && p.Color != "never" {
|
||||||
return errors.New("color must be auto, always, or never")
|
return errors.New("color must be auto, always, or never")
|
||||||
}
|
}
|
||||||
|
if p.Hyperlinks != "auto" && p.Hyperlinks != "always" && p.Hyperlinks != "never" {
|
||||||
|
return errors.New("hyperlinks must be auto, always, or never")
|
||||||
|
}
|
||||||
|
if p.Markdown != "auto" && p.Markdown != "raw" {
|
||||||
|
return errors.New("markdown must be auto or raw")
|
||||||
|
}
|
||||||
if p.DefaultHost != "" {
|
if p.DefaultHost != "" {
|
||||||
normalized, err := NormalizeHost(p.DefaultHost)
|
normalized, err := NormalizeHost(p.DefaultHost)
|
||||||
if err != nil || normalized != p.DefaultHost {
|
if err != nil || normalized != p.DefaultHost {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func TestUpdatePersistsOnlyExplicitPreferences(t *testing.T) {
|
|||||||
snapshot, err := s.Load()
|
snapshot, err := s.Load()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, snapshot.Configured["editor"])
|
require.True(t, snapshot.Configured["editor"])
|
||||||
for _, key := range []string{"git_protocol", "prompt", "color", "aliases"} {
|
for _, key := range []string{"git_protocol", "prompt", "color", "hyperlinks", "markdown", "aliases"} {
|
||||||
require.False(t, snapshot.Configured[key], key)
|
require.False(t, snapshot.Configured[key], key)
|
||||||
}
|
}
|
||||||
require.Equal(t, Defaults().GitProtocol, snapshot.Preferences.GitProtocol)
|
require.Equal(t, Defaults().GitProtocol, snapshot.Preferences.GitProtocol)
|
||||||
@@ -130,6 +130,25 @@ func TestUpdatePersistsOnlyExplicitPreferences(t *testing.T) {
|
|||||||
require.Equal(t, "version: \"1\"\neditor: vi\ncolor: never\n", string(data))
|
require.Equal(t, "version: \"1\"\neditor: vi\ncolor: never\n", string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDisplayPreferencesAcceptOnlyTheirValues(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
config string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"hyperlinks: never\n", true},
|
||||||
|
{"hyperlinks: always\nmarkdown: raw\n", true},
|
||||||
|
{"hyperlinks: off\n", false},
|
||||||
|
{"markdown: never\n", false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.config, func(t *testing.T) {
|
||||||
|
s := New(t.TempDir())
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(s.Dir, "config.yml"), []byte("version: '1'\n"+tc.config), 0o600))
|
||||||
|
_, err := s.Load()
|
||||||
|
require.Equal(t, tc.valid, err == nil, "%v", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTokenlessAccounts(t *testing.T) {
|
func TestTokenlessAccounts(t *testing.T) {
|
||||||
for method, want := range map[string]bool{AuthToken: false, AuthOAuth: false, AuthSSH: true, AuthHTTPSign: true} {
|
for method, want := range map[string]bool{AuthToken: false, AuthOAuth: false, AuthSSH: true, AuthHTTPSign: true} {
|
||||||
require.Equal(t, want, Account{AuthMethod: method}.Tokenless(), method)
|
require.Equal(t, want, Account{AuthMethod: method}.Tokenless(), method)
|
||||||
|
|||||||
Reference in new issue
Block a user