internal/cliconfig/cliconfig.go
124 lines · 3597 bytes
11 symbols in this file
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}