A CLI for tangled.sh
at main 3.7 kB view raw
1package pr 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "os" 10 "os/exec" 11 "path" 12 "strings" 13 "sync" 14 15 "github.com/charmbracelet/huh" 16 "github.com/charmbracelet/huh/spinner" 17 "github.com/spf13/cobra" 18 "github.com/zalando/go-keyring" 19 "tangled.sh/rockorager.dev/knit" 20 "tangled.sh/rockorager.dev/knit/auth" 21 "tangled.sh/rockorager.dev/knit/config" 22 "tangled.sh/rockorager.dev/knit/git" 23) 24 25func Create(cmd *cobra.Command, args []string) error { 26 if len(args) != 1 { 27 return fmt.Errorf("invalid args: %s", args) 28 } 29 30 handle := config.DefaultHandleForHost(knit.DefaultHost) 31 if handle == "" { 32 return knit.ErrRequiresAuth 33 } 34 35 client, err := auth.NewClient(knit.DefaultHost, handle) 36 if errors.Is(err, keyring.ErrNotFound) { 37 return knit.ErrRequiresAuth 38 } 39 _ = client 40 41 paths, err := git.FormatPatchToTmp(args[0]) 42 if err != nil { 43 return err 44 } 45 46 editor := exec.Command(knit.Editor(), paths...) 47 editor.Stdout = os.Stdout 48 editor.Stderr = os.Stderr 49 editor.Stdin = os.Stdin 50 if err := editor.Run(); err != nil { 51 return err 52 } 53 54 // Combine the result into a single slice 55 var patch []byte 56 for _, p := range paths { 57 b, err := os.ReadFile(p) 58 if err != nil { 59 return err 60 } 61 patch = append(patch, b...) 62 } 63 64 // Get available remotes 65 remotes, err := git.Remotes() 66 if err != nil { 67 return err 68 } 69 70 if len(remotes) == 0 { 71 return fmt.Errorf("no configured remotes") 72 } 73 74 remote := remotes[0] 75 if len(remotes) > 1 { 76 options := make([]huh.Option[*git.Remote], 0, len(remotes)) 77 for _, remote := range remotes { 78 p := path.Join(remote.Host, remote.Path) 79 options = append(options, huh.NewOption(p, remote)) 80 } 81 huh.NewSelect[*git.Remote](). 82 Title("Select a remote to create a pull request"). 83 Value(&remote). 84 Options(options...). 85 Run() 86 } 87 88 branches, err := git.RemoteBranches(remote.Name) 89 if err != nil { 90 return err 91 } 92 93 options := make([]huh.Option[string], 0, len(branches)) 94 for _, branch := range branches { 95 options = append(options, huh.NewOption(branch, branch)) 96 } 97 98 var ( 99 targetBranch string 100 title string 101 description string 102 ) 103 104 if err := huh.NewSelect[string](). 105 Title("Target branch"). 106 Options(options...). 107 Value(&targetBranch).Run(); err != nil { 108 return err 109 } 110 111 if err := huh.NewInput(). 112 Title("Title"). 113 Placeholder("(optional)"). 114 Value(&title).Run(); err != nil { 115 return err 116 } 117 118 if err := huh.NewText(). 119 Title("Description"). 120 Placeholder("(optional)"). 121 Value(&description).Run(); err != nil { 122 return err 123 } 124 125 var submit bool 126 if err := huh.NewConfirm(). 127 Title(fmt.Sprintf("Submit PR to %s/%s?", remote.Host, remote.Path)). 128 Value(&submit). 129 Run(); err != nil { 130 return err 131 } 132 133 if !submit { 134 return fmt.Errorf("PR submission canceled") 135 } 136 137 form := url.Values{} 138 form.Add("title", title) 139 form.Add("body", description) 140 form.Add("targetBranch", targetBranch) 141 form.Add("patch", string(patch)) 142 143 p := remote.Path 144 if !strings.HasPrefix(p, "@") { 145 p = "@" + p 146 } 147 148 u := url.URL{ 149 Scheme: "https", 150 Host: remote.Host, 151 Path: path.Join(p, "/pulls/new"), 152 } 153 154 ctx, stop := context.WithCancel(cmd.Context()) 155 defer stop() 156 wg := &sync.WaitGroup{} 157 wg.Add(1) 158 go func(wg *sync.WaitGroup) { 159 spinner.New(). 160 Context(ctx). 161 Title("Creating PR..."). 162 Run() 163 wg.Done() 164 }(wg) 165 166 resp, err := client.Post( 167 u.String(), 168 "application/x-www-form-urlencoded", 169 strings.NewReader(form.Encode()), 170 ) 171 if err != nil { 172 return err 173 } 174 defer resp.Body.Close() 175 auth.SaveCookies(resp.Cookies(), knit.DefaultHost, handle) 176 177 if resp.StatusCode != http.StatusOK { 178 return fmt.Errorf("unexpected status code: %d", resp.StatusCode) 179 } 180 181 stop() 182 wg.Wait() 183 fmt.Printf("\x1b[32m✔\x1b[m PR created!\r\n") 184 185 return nil 186}