github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/gist/edit/edit.go (about) 1 package edit 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 15 "github.com/AlecAivazis/survey/v2" 16 "github.com/ungtb10d/cli/v2/api" 17 "github.com/ungtb10d/cli/v2/internal/config" 18 "github.com/ungtb10d/cli/v2/pkg/cmd/gist/shared" 19 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 20 "github.com/ungtb10d/cli/v2/pkg/iostreams" 21 "github.com/ungtb10d/cli/v2/pkg/prompt" 22 "github.com/ungtb10d/cli/v2/pkg/surveyext" 23 "github.com/spf13/cobra" 24 ) 25 26 type EditOptions struct { 27 IO *iostreams.IOStreams 28 HttpClient func() (*http.Client, error) 29 Config func() (config.Config, error) 30 31 Edit func(string, string, string, *iostreams.IOStreams) (string, error) 32 33 Selector string 34 EditFilename string 35 AddFilename string 36 SourceFile string 37 Description string 38 } 39 40 func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command { 41 opts := EditOptions{ 42 IO: f.IOStreams, 43 HttpClient: f.HttpClient, 44 Config: f.Config, 45 Edit: func(editorCmd, filename, defaultContent string, io *iostreams.IOStreams) (string, error) { 46 return surveyext.Edit( 47 editorCmd, 48 "*."+filename, 49 defaultContent, 50 io.In, io.Out, io.ErrOut) 51 }, 52 } 53 54 cmd := &cobra.Command{ 55 Use: "edit {<id> | <url>} [<filename>]", 56 Short: "Edit one of your gists", 57 Args: func(cmd *cobra.Command, args []string) error { 58 if len(args) < 1 { 59 return cmdutil.FlagErrorf("cannot edit: gist argument required") 60 } 61 if len(args) > 2 { 62 return cmdutil.FlagErrorf("too many arguments") 63 } 64 return nil 65 }, 66 RunE: func(c *cobra.Command, args []string) error { 67 opts.Selector = args[0] 68 if len(args) > 1 { 69 opts.SourceFile = args[1] 70 } 71 72 if runF != nil { 73 return runF(&opts) 74 } 75 76 return editRun(&opts) 77 }, 78 } 79 80 cmd.Flags().StringVarP(&opts.AddFilename, "add", "a", "", "Add a new file to the gist") 81 cmd.Flags().StringVarP(&opts.Description, "desc", "d", "", "New description for the gist") 82 cmd.Flags().StringVarP(&opts.EditFilename, "filename", "f", "", "Select a file to edit") 83 84 return cmd 85 } 86 87 func editRun(opts *EditOptions) error { 88 gistID := opts.Selector 89 90 if strings.Contains(gistID, "/") { 91 id, err := shared.GistIDFromURL(gistID) 92 if err != nil { 93 return err 94 } 95 gistID = id 96 } 97 98 client, err := opts.HttpClient() 99 if err != nil { 100 return err 101 } 102 103 apiClient := api.NewClientFromHTTP(client) 104 105 cfg, err := opts.Config() 106 if err != nil { 107 return err 108 } 109 110 host, _ := cfg.DefaultHost() 111 112 gist, err := shared.GetGist(client, host, gistID) 113 if err != nil { 114 if errors.Is(err, shared.NotFoundErr) { 115 return fmt.Errorf("gist not found: %s", gistID) 116 } 117 return err 118 } 119 120 username, err := api.CurrentLoginName(apiClient, host) 121 if err != nil { 122 return err 123 } 124 125 if username != gist.Owner.Login { 126 return errors.New("you do not own this gist") 127 } 128 129 shouldUpdate := false 130 if opts.Description != "" { 131 shouldUpdate = true 132 gist.Description = opts.Description 133 } 134 135 if opts.AddFilename != "" { 136 var input io.Reader 137 switch src := opts.SourceFile; { 138 case src == "-": 139 input = opts.IO.In 140 case src != "": 141 f, err := os.Open(src) 142 if err != nil { 143 return err 144 } 145 defer func() { 146 _ = f.Close() 147 }() 148 input = f 149 default: 150 f, err := os.Open(opts.AddFilename) 151 if err != nil { 152 return err 153 } 154 defer func() { 155 _ = f.Close() 156 }() 157 input = f 158 } 159 160 content, err := io.ReadAll(input) 161 if err != nil { 162 return fmt.Errorf("read content: %w", err) 163 } 164 165 files, err := getFilesToAdd(opts.AddFilename, content) 166 if err != nil { 167 return err 168 } 169 170 gist.Files = files 171 return updateGist(apiClient, host, gist) 172 } 173 174 filesToUpdate := map[string]string{} 175 176 for { 177 filename := opts.EditFilename 178 candidates := []string{} 179 for filename := range gist.Files { 180 candidates = append(candidates, filename) 181 } 182 183 sort.Strings(candidates) 184 185 if filename == "" { 186 if len(candidates) == 1 { 187 filename = candidates[0] 188 } else { 189 if !opts.IO.CanPrompt() { 190 return errors.New("unsure what file to edit; either specify --filename or run interactively") 191 } 192 //nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter 193 err = prompt.SurveyAskOne(&survey.Select{ 194 Message: "Edit which file?", 195 Options: candidates, 196 }, &filename) 197 198 if err != nil { 199 return fmt.Errorf("could not prompt: %w", err) 200 } 201 } 202 } 203 204 gistFile, found := gist.Files[filename] 205 if !found { 206 return fmt.Errorf("gist has no file %q", filename) 207 } 208 if shared.IsBinaryContents([]byte(gistFile.Content)) { 209 return fmt.Errorf("editing binary files not supported") 210 } 211 212 var text string 213 if src := opts.SourceFile; src != "" { 214 if src == "-" { 215 data, err := io.ReadAll(opts.IO.In) 216 if err != nil { 217 return fmt.Errorf("read from stdin: %w", err) 218 } 219 text = string(data) 220 } else { 221 data, err := os.ReadFile(src) 222 if err != nil { 223 return fmt.Errorf("read %s: %w", src, err) 224 } 225 text = string(data) 226 } 227 } else { 228 editorCommand, err := cmdutil.DetermineEditor(opts.Config) 229 if err != nil { 230 return err 231 } 232 233 data, err := opts.Edit(editorCommand, filename, gistFile.Content, opts.IO) 234 if err != nil { 235 return err 236 } 237 text = data 238 } 239 240 if text != gistFile.Content { 241 gistFile.Content = text // so it appears if they re-edit 242 filesToUpdate[filename] = text 243 } 244 245 if !opts.IO.CanPrompt() || 246 len(candidates) == 1 || 247 opts.EditFilename != "" { 248 break 249 } 250 251 choice := "" 252 253 //nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter 254 err = prompt.SurveyAskOne(&survey.Select{ 255 Message: "What next?", 256 Options: []string{ 257 "Edit another file", 258 "Submit", 259 "Cancel", 260 }, 261 }, &choice) 262 263 if err != nil { 264 return fmt.Errorf("could not prompt: %w", err) 265 } 266 267 stop := false 268 269 switch choice { 270 case "Edit another file": 271 continue 272 case "Submit": 273 stop = true 274 case "Cancel": 275 return cmdutil.CancelError 276 } 277 278 if stop { 279 break 280 } 281 } 282 283 if len(filesToUpdate) > 0 { 284 shouldUpdate = true 285 } 286 287 if !shouldUpdate { 288 return nil 289 } 290 291 return updateGist(apiClient, host, gist) 292 } 293 294 func updateGist(apiClient *api.Client, hostname string, gist *shared.Gist) error { 295 body := shared.Gist{ 296 Description: gist.Description, 297 Files: gist.Files, 298 } 299 300 path := "gists/" + gist.ID 301 302 requestByte, err := json.Marshal(body) 303 if err != nil { 304 return err 305 } 306 307 requestBody := bytes.NewReader(requestByte) 308 309 result := shared.Gist{} 310 311 err = apiClient.REST(hostname, "POST", path, requestBody, &result) 312 313 if err != nil { 314 return err 315 } 316 317 return nil 318 } 319 320 func getFilesToAdd(file string, content []byte) (map[string]*shared.GistFile, error) { 321 if shared.IsBinaryContents(content) { 322 return nil, fmt.Errorf("failed to upload %s: binary file not supported", file) 323 } 324 325 if len(content) == 0 { 326 return nil, errors.New("file contents cannot be empty") 327 } 328 329 filename := filepath.Base(file) 330 return map[string]*shared.GistFile{ 331 filename: { 332 Filename: filename, 333 Content: string(content), 334 }, 335 }, nil 336 }