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  }