github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/gist/create/create.go (about) 1 package create 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "path" 12 "regexp" 13 "sort" 14 "strings" 15 16 "github.com/MakeNowJust/heredoc" 17 "github.com/cli/cli/api" 18 "github.com/cli/cli/internal/config" 19 "github.com/cli/cli/pkg/cmd/gist/shared" 20 "github.com/cli/cli/pkg/cmdutil" 21 "github.com/cli/cli/pkg/iostreams" 22 "github.com/cli/cli/utils" 23 "github.com/spf13/cobra" 24 ) 25 26 type browser interface { 27 Browse(string) error 28 } 29 30 type CreateOptions struct { 31 IO *iostreams.IOStreams 32 33 Description string 34 Public bool 35 Filenames []string 36 FilenameOverride string 37 WebMode bool 38 39 Config func() (config.Config, error) 40 HttpClient func() (*http.Client, error) 41 Browser browser 42 } 43 44 func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { 45 opts := CreateOptions{ 46 IO: f.IOStreams, 47 Config: f.Config, 48 HttpClient: f.HttpClient, 49 Browser: f.Browser, 50 } 51 52 cmd := &cobra.Command{ 53 Use: "create [<filename>... | -]", 54 Short: "Create a new gist", 55 Long: heredoc.Doc(` 56 Create a new GitHub gist with given contents. 57 58 Gists can be created from one or multiple files. Alternatively, pass "-" as 59 file name to read from standard input. 60 61 By default, gists are secret; use '--public' to make publicly listed ones. 62 `), 63 Example: heredoc.Doc(` 64 # publish file 'hello.py' as a public gist 65 $ gh gist create --public hello.py 66 67 # create a gist with a description 68 $ gh gist create hello.py -d "my Hello-World program in Python" 69 70 # create a gist containing several files 71 $ gh gist create hello.py world.py cool.txt 72 73 # read from standard input to create a gist 74 $ gh gist create - 75 76 # create a gist from output piped from another command 77 $ cat cool.txt | gh gist create 78 `), 79 Args: func(cmd *cobra.Command, args []string) error { 80 if len(args) > 0 { 81 return nil 82 } 83 if opts.IO.IsStdinTTY() { 84 return &cmdutil.FlagError{Err: errors.New("no filenames passed and nothing on STDIN")} 85 } 86 return nil 87 }, 88 RunE: func(c *cobra.Command, args []string) error { 89 opts.Filenames = args 90 91 if runF != nil { 92 return runF(&opts) 93 } 94 return createRun(&opts) 95 }, 96 } 97 98 cmd.Flags().StringVarP(&opts.Description, "desc", "d", "", "A description for this gist") 99 cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the web browser with created gist") 100 cmd.Flags().BoolVarP(&opts.Public, "public", "p", false, "List the gist publicly (default: secret)") 101 cmd.Flags().StringVarP(&opts.FilenameOverride, "filename", "f", "", "Provide a filename to be used when reading from STDIN") 102 return cmd 103 } 104 105 func createRun(opts *CreateOptions) error { 106 fileArgs := opts.Filenames 107 if len(fileArgs) == 0 { 108 fileArgs = []string{"-"} 109 } 110 111 files, err := processFiles(opts.IO.In, opts.FilenameOverride, fileArgs) 112 if err != nil { 113 return fmt.Errorf("failed to collect files for posting: %w", err) 114 } 115 116 gistName := guessGistName(files) 117 118 processMessage := "Creating gist..." 119 completionMessage := "Created gist" 120 if gistName != "" { 121 if len(files) > 1 { 122 processMessage = "Creating gist with multiple files" 123 } else { 124 processMessage = fmt.Sprintf("Creating gist %s", gistName) 125 } 126 completionMessage = fmt.Sprintf("Created gist %s", gistName) 127 } 128 129 cs := opts.IO.ColorScheme() 130 131 errOut := opts.IO.ErrOut 132 fmt.Fprintf(errOut, "%s %s\n", cs.Gray("-"), processMessage) 133 134 httpClient, err := opts.HttpClient() 135 if err != nil { 136 return err 137 } 138 139 cfg, err := opts.Config() 140 if err != nil { 141 return err 142 } 143 144 host, err := cfg.DefaultHost() 145 if err != nil { 146 return err 147 } 148 149 gist, err := createGist(httpClient, host, opts.Description, opts.Public, files) 150 if err != nil { 151 var httpError api.HTTPError 152 if errors.As(err, &httpError) { 153 if httpError.OAuthScopes != "" && !strings.Contains(httpError.OAuthScopes, "gist") { 154 return fmt.Errorf("This command requires the 'gist' OAuth scope.\nPlease re-authenticate with: gh auth refresh -h %s -s gist", host) 155 } 156 if httpError.StatusCode == http.StatusUnprocessableEntity { 157 if detectEmptyFiles(files) { 158 fmt.Fprintf(errOut, "%s Failed to create gist: %s\n", cs.FailureIcon(), "a gist file cannot be blank") 159 return cmdutil.SilentError 160 } 161 } 162 } 163 return fmt.Errorf("%s Failed to create gist: %w", cs.Red("X"), err) 164 } 165 166 fmt.Fprintf(errOut, "%s %s\n", cs.SuccessIconWithColor(cs.Green), completionMessage) 167 168 if opts.WebMode { 169 fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", utils.DisplayURL(gist.HTMLURL)) 170 171 return opts.Browser.Browse(gist.HTMLURL) 172 } 173 174 fmt.Fprintln(opts.IO.Out, gist.HTMLURL) 175 176 return nil 177 } 178 179 func processFiles(stdin io.ReadCloser, filenameOverride string, filenames []string) (map[string]*shared.GistFile, error) { 180 fs := map[string]*shared.GistFile{} 181 182 if len(filenames) == 0 { 183 return nil, errors.New("no files passed") 184 } 185 186 for i, f := range filenames { 187 var filename string 188 var content []byte 189 var err error 190 191 if f == "-" { 192 if filenameOverride != "" { 193 filename = filenameOverride 194 } else { 195 filename = fmt.Sprintf("gistfile%d.txt", i) 196 } 197 content, err = ioutil.ReadAll(stdin) 198 if err != nil { 199 return fs, fmt.Errorf("failed to read from stdin: %w", err) 200 } 201 stdin.Close() 202 203 if shared.IsBinaryContents(content) { 204 return nil, fmt.Errorf("binary file contents not supported") 205 } 206 } else { 207 isBinary, err := shared.IsBinaryFile(f) 208 if err != nil { 209 return fs, fmt.Errorf("failed to read file %s: %w", f, err) 210 } 211 if isBinary { 212 return nil, fmt.Errorf("failed to upload %s: binary file not supported", f) 213 } 214 215 content, err = ioutil.ReadFile(f) 216 if err != nil { 217 return fs, fmt.Errorf("failed to read file %s: %w", f, err) 218 } 219 220 filename = path.Base(f) 221 } 222 223 fs[filename] = &shared.GistFile{ 224 Content: string(content), 225 } 226 } 227 228 return fs, nil 229 } 230 231 func guessGistName(files map[string]*shared.GistFile) string { 232 filenames := make([]string, 0, len(files)) 233 gistName := "" 234 235 re := regexp.MustCompile(`^gistfile\d+\.txt$`) 236 for k := range files { 237 if !re.MatchString(k) { 238 filenames = append(filenames, k) 239 } 240 } 241 242 if len(filenames) > 0 { 243 sort.Strings(filenames) 244 gistName = filenames[0] 245 } 246 247 return gistName 248 } 249 250 func createGist(client *http.Client, hostname, description string, public bool, files map[string]*shared.GistFile) (*shared.Gist, error) { 251 path := "gists" 252 253 body := &shared.Gist{ 254 Description: description, 255 Public: public, 256 Files: files, 257 } 258 259 result := shared.Gist{} 260 261 requestByte, err := json.Marshal(body) 262 if err != nil { 263 return nil, err 264 } 265 requestBody := bytes.NewReader(requestByte) 266 267 apiClient := api.NewClientFromHTTP(client) 268 err = apiClient.REST(hostname, "POST", path, requestBody, &result) 269 if err != nil { 270 return nil, err 271 } 272 273 return &result, nil 274 } 275 276 func detectEmptyFiles(files map[string]*shared.GistFile) bool { 277 for _, file := range files { 278 if strings.TrimSpace(file.Content) == "" { 279 return true 280 } 281 } 282 return false 283 }