package pr import ( "context" "errors" "fmt" "net/http" "net/url" "os" "os/exec" "path" "strings" "sync" "github.com/charmbracelet/huh" "github.com/charmbracelet/huh/spinner" "github.com/spf13/cobra" "github.com/zalando/go-keyring" "tangled.sh/rockorager.dev/knit" "tangled.sh/rockorager.dev/knit/auth" "tangled.sh/rockorager.dev/knit/config" "tangled.sh/rockorager.dev/knit/git" ) func Create(cmd *cobra.Command, args []string) error { if len(args) != 1 { return fmt.Errorf("invalid args: %s", args) } handle := config.DefaultHandleForHost(knit.DefaultHost) if handle == "" { return knit.ErrRequiresAuth } client, err := auth.NewClient(knit.DefaultHost, handle) if errors.Is(err, keyring.ErrNotFound) { return knit.ErrRequiresAuth } _ = client paths, err := git.FormatPatchToTmp(args[0]) if err != nil { return err } editor := exec.Command(knit.Editor(), paths...) editor.Stdout = os.Stdout editor.Stderr = os.Stderr editor.Stdin = os.Stdin if err := editor.Run(); err != nil { return err } // Combine the result into a single slice var patch []byte for _, p := range paths { b, err := os.ReadFile(p) if err != nil { return err } patch = append(patch, b...) } // Get available remotes remotes, err := git.Remotes() if err != nil { return err } if len(remotes) == 0 { return fmt.Errorf("no configured remotes") } remote := remotes[0] if len(remotes) > 1 { options := make([]huh.Option[*git.Remote], 0, len(remotes)) for _, remote := range remotes { p := path.Join(remote.Host, remote.Path) options = append(options, huh.NewOption(p, remote)) } huh.NewSelect[*git.Remote](). Title("Select a remote to create a pull request"). Value(&remote). Options(options...). Run() } branches, err := git.RemoteBranches(remote.Name) if err != nil { return err } options := make([]huh.Option[string], 0, len(branches)) for _, branch := range branches { options = append(options, huh.NewOption(branch, branch)) } var ( targetBranch string title string description string ) if err := huh.NewSelect[string](). Title("Target branch"). Options(options...). Value(&targetBranch).Run(); err != nil { return err } if err := huh.NewInput(). Title("Title"). Placeholder("(optional)"). Value(&title).Run(); err != nil { return err } if err := huh.NewText(). Title("Description"). Placeholder("(optional)"). Value(&description).Run(); err != nil { return err } var submit bool if err := huh.NewConfirm(). Title(fmt.Sprintf("Submit PR to %s/%s?", remote.Host, remote.Path)). Value(&submit). Run(); err != nil { return err } if !submit { return fmt.Errorf("PR submission canceled") } form := url.Values{} form.Add("title", title) form.Add("body", description) form.Add("targetBranch", targetBranch) form.Add("patch", string(patch)) p := remote.Path if !strings.HasPrefix(p, "@") { p = "@" + p } u := url.URL{ Scheme: "https", Host: remote.Host, Path: path.Join(p, "/pulls/new"), } ctx, stop := context.WithCancel(cmd.Context()) defer stop() wg := &sync.WaitGroup{} wg.Add(1) go func(wg *sync.WaitGroup) { spinner.New(). Context(ctx). Title("Creating PR..."). Run() wg.Done() }(wg) resp, err := client.Post( u.String(), "application/x-www-form-urlencoded", strings.NewReader(form.Encode()), ) if err != nil { return err } defer resp.Body.Close() auth.SaveCookies(resp.Cookies(), knit.DefaultHost, handle) if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status code: %d", resp.StatusCode) } stop() wg.Wait() fmt.Printf("\x1b[32m✔\x1b[m PR created!\r\n") return nil }