1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
package main
import (
"bufio"
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"codeberg.org/emersion/go-scfg"
"github.com/spf13/cobra"
"git.sr.ht/~xenrox/hut/srht/metasrht"
"git.sr.ht/~xenrox/hut/termfmt"
)
type Config struct {
Instances []*InstanceConfig `scfg:"instance"`
}
type InstanceConfig struct {
Name string `scfg:",param"`
AccessToken string `scfg:"access-token"`
AccessTokenCmd []string `scfg:"access-token-cmd"`
Builds *ServiceConfig `scfg:"builds"`
Git *ServiceConfig `scfg:"git"`
Hg *ServiceConfig `scfg:"hg"`
Lists *ServiceConfig `scfg:"lists"`
Meta *ServiceConfig `scfg:"meta"`
Pages *ServiceConfig `scfg:"pages"`
Paste *ServiceConfig `scfg:"paste"`
Todo *ServiceConfig `scfg:"todo"`
}
func (instance *InstanceConfig) match(name string) bool {
if instancesEqual(name, instance.Name) {
return true
}
for _, service := range instance.Services() {
if service.Origin != "" && stripProtocol(service.Origin) == name {
return true
}
}
return false
}
func (instance *InstanceConfig) Services() map[string]*ServiceConfig {
all := map[string]*ServiceConfig{
"builds": instance.Builds,
"git": instance.Git,
"hg": instance.Hg,
"lists": instance.Lists,
"meta": instance.Meta,
"pages": instance.Pages,
"paste": instance.Paste,
"todo": instance.Todo,
}
m := make(map[string]*ServiceConfig)
for name, service := range all {
if service != nil {
m[name] = service
}
}
return m
}
type ServiceConfig struct {
Origin string `scfg:"origin"`
}
func instancesEqual(a, b string) bool {
return a == b || strings.HasSuffix(a, "."+b) || strings.HasSuffix(b, "."+a)
}
func loadConfigFile(filename string) (*Config, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
cfg := new(Config)
if err := scfg.NewDecoder(f).Decode(cfg); err != nil {
return nil, err
}
instanceNames := make(map[string]struct{})
for _, instance := range cfg.Instances {
if _, ok := instanceNames[instance.Name]; ok {
return nil, fmt.Errorf("duplicate instance name %q", instance.Name)
}
instanceNames[instance.Name] = struct{}{}
if instance.AccessTokenCmd != nil && len(instance.AccessTokenCmd) == 0 {
return nil, fmt.Errorf("instance %q: missing command name in access-token-cmd directive", instance.Name)
}
if instance.AccessToken == "" && len(instance.AccessTokenCmd) == 0 {
return nil, fmt.Errorf("instance %q: missing access-token or access-token-cmd", instance.Name)
}
if instance.AccessToken != "" && len(instance.AccessTokenCmd) > 0 {
return nil, fmt.Errorf("instance %q: access-token and access-token-cmd can't be both specified", instance.Name)
}
}
return cfg, nil
}
func loadConfig(cmd *cobra.Command) *Config {
type configContextKey struct{}
if v := cmd.Context().Value(configContextKey{}); v != nil {
return v.(*Config)
}
customConfigFile := true
configFile, err := cmd.Flags().GetString("config")
if err != nil {
log.Fatal(err)
} else if configFile == "" {
configFile = defaultConfigFilename()
customConfigFile = false
}
cfg, err := loadConfigFile(configFile)
if err != nil {
// This error message doesn't make sense if a config was
// provided with "--config". In that case, the normal log
// message is always desired.
if !customConfigFile && errors.Is(err, os.ErrNotExist) {
os.Stderr.WriteString("Looks like hut's config file hasn't been set up yet.\nRun `hut init` to configure it.\n")
os.Exit(1)
}
log.Fatalf("failed to load config file: %v", err)
}
ctx := cmd.Context()
ctx = context.WithValue(ctx, configContextKey{}, cfg)
cmd.SetContext(ctx)
return cfg
}
func defaultConfigFilename() string {
configDir, err := os.UserConfigDir()
if err != nil {
log.Fatalf("failed to get user config dir: %v", err)
}
return filepath.Join(configDir, "hut", "config")
}
func newInitCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Initialize hut",
Args: cobra.ExactArgs(0),
}
cmd.Run = func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
filename, err := cmd.Flags().GetString("config")
if err != nil {
log.Fatal(err)
} else if filename == "" {
filename = defaultConfigFilename()
}
// Perform an early sanity check to avoid asking the user to login if
// the config file already exists
if _, err := os.Stat(filename); err == nil {
log.Fatalf("config file %q already exists (delete it if you want to overwrite it)", filename)
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Fatal(err)
}
instance, err := cmd.Flags().GetString("instance")
if err != nil {
log.Fatal(err)
} else if instance == "" {
instance = "sr.ht"
}
baseURL := "https://meta." + instance
fmt.Printf("Generate a new OAuth2 access token at:\n")
fmt.Printf("%s/oauth2/personal-token\n", baseURL)
fmt.Printf("Then copy-paste it here: ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
token := strings.TrimSpace(scanner.Text())
if err := scanner.Err(); err != nil {
log.Fatalf("failed to read token from stdin: %v", err)
} else if token == "" {
log.Fatal("no token provided")
}
config := fmt.Sprintf("instance %q {\n access-token %q\n}\n", instance, token)
c := createClientWithToken(baseURL, token, false)
user, err := metasrht.FetchMe(c.Client, ctx)
if err != nil {
log.Fatalf("failed to check OAuth2 token: %v", err)
}
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
log.Fatalf("failed to create config file parent directory: %v", err)
}
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if os.IsExist(err) {
log.Fatalf("config file %q already exists (delete it if you want to overwrite it)", filename)
} else if err != nil {
log.Fatalf("failed to create config file: %v", err)
}
defer f.Close()
if _, err := f.WriteString(config); err != nil {
log.Fatalf("failed to write config file: %v", err)
}
if err := f.Close(); err != nil {
log.Fatalf("failed to close config file: %v", err)
}
log.Printf("hut initialized for user %v\n", termfmt.Bold.String(user.CanonicalName))
}
return cmd
}
|