package main import ( "bytes" "errors" "flag" "fmt" "io" "os" "os/user" "path/filepath" "regexp" "strings" "github.com/adrg/xdg" "github.com/charmbracelet/huh" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/peterbourgon/ff/v3" "github.com/peterbourgon/ff/v3/ffyaml" ) type args struct { username string password string passwordFile string domain string cli []string } func defaultConfig() string { def, err := xdg.ConfigFile("gist/config.yaml") if err != nil { return "config.yaml" } return def } func defaultUsername() string { u, err := user.Current() if err != nil { return "" } return u.Username } func parseArgs(args []string) (a args, e error) { fs := flag.NewFlagSet("gist", flag.ExitOnError) fs.String("config", defaultConfig(), "Path to config file") fs.StringVar(&a.username, "username", defaultUsername(), "opengist username") fs.StringVar(&a.username, "u", a.username, "--username") fs.StringVar(&a.password, "password", "", "opengist password") fs.StringVar(&a.password, "p", a.password, "--password") fs.StringVar(&a.passwordFile, "password-file", "", "Path to a file containing the opengist password") fs.StringVar(&a.passwordFile, "f", a.passwordFile, "--password-file") fs.StringVar(&a.domain, "domain", "", "opengist domain") fs.StringVar(&a.domain, "d", a.domain, "--domain") if err := ff.Parse(fs, args, ff.WithConfigFileFlag("config"), ff.WithAllowMissingConfigFile(true), ff.WithConfigFileParser(ffyaml.Parser), ff.WithEnvVarPrefix("GIST"), ); err != nil { return a, err } a.cli = fs.Args() return a, nil } func copyFile(src, dest string) error { fi, err := os.Lstat(src) if err != nil { return err } srcFi, err := os.Open(src) if err != nil { return fmt.Errorf("could not open src: %w", err) } defer srcFi.Close() destFi, err := os.Create(dest) if err != nil { return fmt.Errorf("could not create dest: %w", err) } defer destFi.Close() if err := os.Chmod(dest, fi.Mode()); err != nil { return err } if _, err := io.Copy(destFi, srcFi); err != nil { return fmt.Errorf("could not copy %s to %s: %w", src, dest, err) } return nil } func maine() error { args, err := parseArgs(os.Args[1:]) if err != nil { return fmt.Errorf("could not parse args: %w", err) } required := func(s string) error { if strings.TrimSpace(s) == "" { return errors.New("value is required") } return nil } var prompts []huh.Field if args.username == "" { prompts = append(prompts, huh.NewInput(). Title("Opengist username"). Validate(required). Value(&args.username), ) } if args.password == "" && args.passwordFile == "" { prompts = append(prompts, huh.NewInput(). Title("Opengist password"). Password(true). Validate(required). Value(&args.password), ) } if args.domain == "" { prompts = append(prompts, huh.NewInput(). Title("Opengist domain"). Validate(required). Value(&args.domain), ) } if len(prompts) > 0 { if err := huh.NewForm(huh.NewGroup(prompts...)). WithTheme(huh.ThemeCatppuccin()). Run(); err != nil { return fmt.Errorf("missing fields are required: %w", err) } } if args.passwordFile != "" { b, err := os.ReadFile(args.passwordFile) if err != nil { return fmt.Errorf("could not read opengist password file: %w", err) } args.password = strings.TrimSpace(string(b)) } tmp, err := os.MkdirTemp(os.TempDir(), "gist*") if err != nil { return fmt.Errorf("could not make temp dir: %w", err) } defer func() { if err := os.RemoveAll(tmp); err != nil { fmt.Printf("could not clean up temp dir at %q: %v\n", tmp, err) } }() repo, err := git.PlainInit(tmp, false) if err != nil { return fmt.Errorf("could not init git repo: %w", err) } if err := repo.CreateBranch(&config.Branch{ Name: "main", }); err != nil { return fmt.Errorf("could not create main branch: %w", err) } if _, err := repo.CreateRemote(&config.RemoteConfig{ Name: "origin", URLs: []string{fmt.Sprintf("https://%s/init", args.domain)}, }); err != nil { return fmt.Errorf("could not create origin remote: %w", err) } for _, fp := range args.cli { glob, err := filepath.Glob(fp) if err != nil { return fmt.Errorf("invalid glob: %w", err) } for _, g := range glob { if err := copyFile(g, filepath.Join(tmp, filepath.Base(g))); err != nil { return fmt.Errorf("could not copy file: %w", err) } } } tree, err := repo.Worktree() if err != nil { return fmt.Errorf("could not get git worktree: %w", err) } if err := tree.AddGlob("."); err != nil { return fmt.Errorf("could not add all files to staging area: %w", err) } if _, err := tree.Commit("gist cli", &git.CommitOptions{}); err != nil { return fmt.Errorf("could not commit files: %w", err) } var buf bytes.Buffer if err := repo.Push(&git.PushOptions{ Auth: &http.BasicAuth{ Username: args.username, Password: args.password, }, Progress: &buf, }); err != nil { return fmt.Errorf("could not push to remote: %w", err) } re := regexp.MustCompile(fmt.Sprintf(`https://%s/%s/\w+`, args.domain, args.username)) u := re.FindString(buf.String()) // FIXME Progress is currently broken (?) for Push, this can be removed once fixed u = fmt.Sprintf("https://%s/all", args.domain) if u == "" { return errors.New("no gist URL found") } fmt.Println(u) return nil } func main() { if err := maine(); err != nil { fmt.Println(err) } }