krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

main: internal/cliconfig/cliconfig.go · raw

  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}
 25
 26func (i Instance) SSHUser() string {
 27	if i.User != "" {
 28		return i.User
 29	}
 30	return "git"
 31}
 32
 33// CloneURL returns the ssh:// URL for owner/name on this instance.
 34func (i Instance) CloneURL(repo string) string {
 35	hostport := i.Host
 36	if i.Port != 0 && i.Port != 22 {
 37		hostport = fmt.Sprintf("%s:%d", i.Host, i.Port)
 38	}
 39	return fmt.Sprintf("ssh://%s@%s/%s.git", i.SSHUser(), hostport, repo)
 40}
 41
 42type Config struct {
 43	Default   string              `toml:"default,omitempty"`
 44	Instances map[string]Instance `toml:"instances"`
 45}
 46
 47func Path() string {
 48	if x := os.Getenv("XDG_CONFIG_HOME"); x != "" {
 49		return filepath.Join(x, "gitbay", "config.toml")
 50	}
 51	home, _ := os.UserHomeDir()
 52	return filepath.Join(home, ".config", "gitbay", "config.toml")
 53}
 54
 55func Load() (Config, error) {
 56	cfg := Config{Instances: map[string]Instance{}}
 57	raw, err := os.ReadFile(Path())
 58	if os.IsNotExist(err) {
 59		return cfg, nil
 60	}
 61	if err != nil {
 62		return cfg, err
 63	}
 64	if err := toml.Unmarshal(raw, &cfg); err != nil {
 65		return cfg, fmt.Errorf("%s: %w", Path(), err)
 66	}
 67	if cfg.Instances == nil {
 68		cfg.Instances = map[string]Instance{}
 69	}
 70	return cfg, nil
 71}
 72
 73func Save(cfg Config) error {
 74	p := Path()
 75	if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
 76		return err
 77	}
 78	var b strings.Builder
 79	if err := toml.NewEncoder(&b).Encode(cfg); err != nil {
 80		return err
 81	}
 82	return os.WriteFile(p, []byte(b.String()), 0o600)
 83}
 84
 85// DefaultInstance returns the configured default (or the only) instance.
 86func (c Config) DefaultInstance() (Instance, string, error) {
 87	if c.Default != "" {
 88		if inst, ok := c.Instances[c.Default]; ok {
 89			return inst, c.Default, nil
 90		}
 91		return Instance{}, "", fmt.Errorf("default instance %q is not configured", c.Default)
 92	}
 93	if len(c.Instances) == 1 {
 94		for name, inst := range c.Instances {
 95			return inst, name, nil
 96		}
 97	}
 98	return Instance{}, "", fmt.Errorf("no gitbay instance configured; run: gitbay remote add <name> <host>")
 99}
100
101var (
102	sshURLPat = regexp.MustCompile(`^ssh://(?:([^@/]+)@)?([^:/]+)(?::(\d+))?/(.+?)(?:\.git)?/?$`)
103	scpPat    = regexp.MustCompile(`^(?:([^@/]+)@)?([^:/]+):(.+?)(?:\.git)?$`)
104)
105
106// ParseRemoteURL extracts the instance coordinates and owner/name from a
107// git remote URL in ssh:// or scp-like form.
108func ParseRemoteURL(url string) (Instance, string, bool) {
109	if m := sshURLPat.FindStringSubmatch(url); m != nil {
110		inst := Instance{Host: m[2], User: m[1]}
111		if m[3] != "" {
112			fmt.Sscanf(m[3], "%d", &inst.Port)
113		}
114		return inst, strings.Trim(m[4], "/"), true
115	}
116	if m := scpPat.FindStringSubmatch(url); m != nil && !strings.Contains(url, "://") {
117		return Instance{Host: m[2], User: m[1]}, strings.Trim(m[3], "/"), true
118	}
119	return Instance{}, "", false
120}