internal/cliconfig/cliconfig.go

32a5f76e5b270097b63a5bba9a43557cf50ad63d
gitbay/internal/cliconfig/cliconfig.go history · blame · raw

124 lines · 3597 bytes

  1// Package cliconfig manages the client-side configuration: named forge
  2// instances at ~/.config/forge/config.toml, and parsing of origin remote
  3// URLs so commands run inside a clone need no --repo argument.
  4package cliconfig
  5
  6import (
  7	"fmt"
  8	"os"
  9	"path/filepath"
 10	"regexp"
 11	"strings"
 12
 13	"github.com/BurntSushi/toml"
 14)
 15
 16type Instance struct {
 17	Host string `toml:"host"`
 18	Port int    `toml:"port,omitempty"`
 19	User string `toml:"user,omitempty"`
 20	// SSHOptions are extra arguments passed to the ssh binary verbatim,
 21	// e.g. ["-i", "~/.ssh/forge_ed25519"]. Most setups need none: the
 22	// system ssh already honors ~/.ssh/config and the agent.
 23	SSHOptions []string `toml:"ssh_options,omitempty"`
 24	// NoMultiplex turns off SSH connection sharing for this instance. On
 25	// by default: one handshake per five minutes instead of one per
 26	// command.
 27	NoMultiplex bool `toml:"no_multiplex,omitempty"`
 28}
 29
 30func (i Instance) SSHUser() string {
 31	if i.User != "" {
 32		return i.User
 33	}
 34	return "git"
 35}
 36
 37// CloneURL returns the ssh:// URL for owner/name on this instance.
 38func (i Instance) CloneURL(repo string) string {
 39	hostport := i.Host
 40	if i.Port != 0 && i.Port != 22 {
 41		hostport = fmt.Sprintf("%s:%d", i.Host, i.Port)
 42	}
 43	return fmt.Sprintf("ssh://%s@%s/%s.git", i.SSHUser(), hostport, repo)
 44}
 45
 46type Config struct {
 47	Default   string              `toml:"default,omitempty"`
 48	Instances map[string]Instance `toml:"instances"`
 49}
 50
 51func Path() string {
 52	if x := os.Getenv("XDG_CONFIG_HOME"); x != "" {
 53		return filepath.Join(x, "gitbay", "config.toml")
 54	}
 55	home, _ := os.UserHomeDir()
 56	return filepath.Join(home, ".config", "gitbay", "config.toml")
 57}
 58
 59func Load() (Config, error) {
 60	cfg := Config{Instances: map[string]Instance{}}
 61	raw, err := os.ReadFile(Path())
 62	if os.IsNotExist(err) {
 63		return cfg, nil
 64	}
 65	if err != nil {
 66		return cfg, err
 67	}
 68	if err := toml.Unmarshal(raw, &cfg); err != nil {
 69		return cfg, fmt.Errorf("%s: %w", Path(), err)
 70	}
 71	if cfg.Instances == nil {
 72		cfg.Instances = map[string]Instance{}
 73	}
 74	return cfg, nil
 75}
 76
 77func Save(cfg Config) error {
 78	p := Path()
 79	if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
 80		return err
 81	}
 82	var b strings.Builder
 83	if err := toml.NewEncoder(&b).Encode(cfg); err != nil {
 84		return err
 85	}
 86	return os.WriteFile(p, []byte(b.String()), 0o600)
 87}
 88
 89// DefaultInstance returns the configured default (or the only) instance.
 90func (c Config) DefaultInstance() (Instance, string, error) {
 91	if c.Default != "" {
 92		if inst, ok := c.Instances[c.Default]; ok {
 93			return inst, c.Default, nil
 94		}
 95		return Instance{}, "", fmt.Errorf("default instance %q is not configured", c.Default)
 96	}
 97	if len(c.Instances) == 1 {
 98		for name, inst := range c.Instances {
 99			return inst, name, nil
100		}
101	}
102	return Instance{}, "", fmt.Errorf("no gitbay instance configured; run: gitbay remote add <name> <host>")
103}
104
105var (
106	sshURLPat = regexp.MustCompile(`^ssh://(?:([^@/]+)@)?([^:/]+)(?::(\d+))?/(.+?)(?:\.git)?/?$`)
107	scpPat    = regexp.MustCompile(`^(?:([^@/]+)@)?([^:/]+):(.+?)(?:\.git)?$`)
108)
109
110// ParseRemoteURL extracts the instance coordinates and owner/name from a
111// git remote URL in ssh:// or scp-like form.
112func ParseRemoteURL(url string) (Instance, string, bool) {
113	if m := sshURLPat.FindStringSubmatch(url); m != nil {
114		inst := Instance{Host: m[2], User: m[1]}
115		if m[3] != "" {
116			fmt.Sscanf(m[3], "%d", &inst.Port)
117		}
118		return inst, strings.Trim(m[4], "/"), true
119	}
120	if m := scpPat.FindStringSubmatch(url); m != nil && !strings.Contains(url, "://") {
121		return Instance{Host: m[2], User: m[1]}, strings.Trim(m[3], "/"), true
122	}
123	return Instance{}, "", false
124}