github.com/tsuyoshiwada/git-prout@v0.0.0-20170402150409-5c51421d4bdb/cli.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "io" 6 "log" 7 "strings" 8 "time" 9 10 "github.com/briandowns/spinner" 11 "github.com/fatih/color" 12 kingpin "gopkg.in/alecthomas/kingpin.v2" 13 emoji "gopkg.in/kyokomi/emoji.v1" 14 ) 15 16 // Status code 17 const ( 18 ExitCodeOK = iota 19 ExitCodeParseFlagsError 20 ExitCodeError 21 ExitCodeNotFoundGit 22 ExitCodeOutsideWorkTree 23 ExitCodeInvalidRemote 24 ExitCodeFailedFetch 25 ExitCodeFailedUpdate 26 ExitCodeFailedCheckout 27 ) 28 29 // Kingpin app, and flags and args. 30 var ( 31 app = kingpin.New("git-prout", "").Version(Version) 32 33 debug = app.Flag("debug", "Enable debug mode.").Bool() 34 remote = app.Flag("remote", "Reference of remote.").Short('r').HintAction(GitListRemotes).Default("origin").String() 35 force = app.Flag("force", "Force execute pull or checkout.").Short('f').Bool() 36 quiet = app.Flag("quiet", "Silence any progress and errors (other than parse error).").Short('q').Bool() 37 number = app.Arg("number", "ID number of pull request").Required().Int() 38 ) 39 40 // Create a new spinner. 41 func newSpinner(w io.Writer, msg string, complete string) *spinner.Spinner { 42 green := color.New(color.FgGreen).SprintFunc() 43 blue := color.New(color.FgCyan).SprintFunc() 44 45 s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 46 s.Writer = w 47 s.Color("cyan") 48 s.Suffix = " " + msg + "..." 49 s.FinalMSG = blue(string(0x2713)) + " " + green(complete) + "\n" 50 51 return s 52 } 53 54 // Dummy writer. 55 type silentWriter struct{} 56 57 func (w *silentWriter) Write([]byte) (int, error) { 58 return 0, nil 59 } 60 61 // CLI is command Runner. 62 type CLI struct { 63 outStream io.Writer 64 errStream io.Writer 65 terminate func(status int) 66 } 67 68 func (cli *CLI) printDebug(msg string) *CLI { 69 if *debug { 70 log.Printf("%s %s", color.New(color.FgRed).Sprint("[debug]"), msg) 71 } 72 return cli 73 } 74 75 func (cli *CLI) printError(msg string) *CLI { 76 color.New(color.FgWhite, color.BgRed).Fprintf(cli.errStream, "\rError: %s\n", msg) 77 return cli 78 } 79 80 func (cli *CLI) printSuccess(msg string) *CLI { 81 emoji.Fprintf(cli.outStream, "\n:sparkles: %s\n%s\n", color.New(color.FgGreen).Sprint("Done!"), msg) 82 return cli 83 } 84 85 // Run processing based on arguments. 86 func (cli *CLI) Run(args []string) { 87 88 // Initialize 89 app.ErrorWriter(cli.errStream) 90 app.UsageWriter(cli.errStream) 91 app.Terminate(cli.terminate) 92 app.HelpFlag.Short('h') 93 app.UsageTemplate(helpText) 94 95 // Parse 96 if _, err := app.Parse(args[1:]); err != nil { 97 cli.printError(err.Error()) 98 cli.terminate(ExitCodeParseFlagsError) 99 return 100 } 101 102 cli.printDebug("Enable debug mode.") 103 104 // Silent 105 if *quiet { 106 cli.outStream = &silentWriter{} 107 cli.errStream = &silentWriter{} 108 cli.printDebug("Enable quiet mode.") 109 } 110 111 // Phase 1: Check 112 s := newSpinner(cli.outStream, "Checking", "Checked") 113 s.Start() 114 115 if !hasGitCommand() { 116 cli.printError("'git' command is required.") 117 cli.terminate(ExitCodeNotFoundGit) 118 return 119 } 120 121 if !isInsideGitWorkTree() { 122 cli.printError("'git-prout' needs to be executed in work tree.") 123 cli.terminate(ExitCodeOutsideWorkTree) 124 return 125 } 126 127 if !GitIsValidRemote(*remote) { 128 cli.printError(fmt.Sprintf("'%s' is invalid remote.", *remote)) 129 cli.printDebug("remotes -> [" + strings.Join(GitListRemotes(), ", ") + "]") 130 cli.terminate(ExitCodeInvalidRemote) 131 return 132 } 133 134 currentBranch, err := GitCurrentBranch() 135 if err != nil { 136 cli.printError("Failed to acquire the current branch.") 137 cli.printDebug(err.Error()) 138 cli.terminate(ExitCodeError) 139 return 140 } 141 142 pr := NewPR(*remote, *number, *force) // PR 143 isUpdate := pr.Branch == currentBranch // Mode 144 145 s.Stop() 146 cli.printDebug(fmt.Sprintf("Cheked, isUpdate = %t", isUpdate)) 147 148 // Phase 2: Fetch 149 s = newSpinner(cli.outStream, "Fetching", "Fetched") 150 s.Start() 151 if _, err := pr.Fetch(); err != nil { 152 cli.printError(fmt.Sprintf("Failed to fetch remote ref '%s %s'.", pr.Remote, pr.Ref)) 153 cli.printDebug(err.Error()) 154 cli.terminate(ExitCodeFailedFetch) 155 return 156 } 157 s.Stop() 158 cli.printDebug(fmt.Sprintf("Fetched, %s %s.", pr.Remote, pr.Ref)) 159 160 // Phase 3: Update or Checkout 161 if isUpdate { 162 s = newSpinner(cli.outStream, "Updating", "Updated") 163 s.Start() 164 if _, err := pr.Apply(); err != nil { 165 cli.printError("Failed to update.") 166 cli.printDebug(err.Error()) 167 cli.terminate(ExitCodeFailedUpdate) 168 return 169 } 170 171 } else { 172 s = newSpinner(cli.outStream, "Checkout", "Checkout") 173 s.Start() 174 if _, err := pr.Checkout(); err != nil { 175 cli.printError("Failed to checkout.") 176 cli.printDebug(err.Error()) 177 cli.terminate(ExitCodeFailedCheckout) 178 return 179 } 180 } 181 s.Stop() 182 183 // Success 184 var msg string 185 186 if isUpdate { 187 msg = fmt.Sprintf("Updated a '%s' branch.", pr.Branch) 188 } else { 189 msg = fmt.Sprintf("Switched to a '%s' branch.", pr.Branch) 190 } 191 192 cli.printSuccess(msg) 193 } 194 195 // for Usage (help). 196 var title = color.New(color.FgYellow).SprintFunc() 197 198 var helpText = fmt.Sprintf(`{{define "FormatCommand"}} [options] {{range .Args}}\ 199 {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 200 {{end}}\ 201 {{define "FormatCommands"}}\ 202 {{range .FlattenedCommands}}\ 203 {{if not .Hidden}}\ 204 {{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}} 205 {{.Help|Wrap 4}} 206 {{end}}\ 207 {{end}}\ 208 {{end}}\ 209 {{define "FormatUsage"}}\ 210 {{template "FormatCommand" .}} 211 {{if .Help}} 212 {{.Help|Wrap 0}}\ 213 {{end}}\ 214 {{end}}\ 215 %s 216 {{.App.Name}}{{template "FormatUsage" .App}} 217 {{if .Context.Flags}}\ 218 %s 219 {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}} 220 {{end}}\ 221 {{if .Context.Args}}\ 222 %s 223 {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 224 {{end}}\ 225 `, title("Usage:"), title("Options:"), title("Arguments:"))