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