krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: 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}