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:"))