diff --git a/.spr.yml b/.spr.yml index 7d5013f..a01cae2 100644 --- a/.spr.yml +++ b/.spr.yml @@ -4,5 +4,6 @@ requireChecks: true requireApproval: false showPRLink: true cleanupRemoteBranch: true -logGitCommands: true -logGitHubCalls: true +logGitCommands: false +logGitHubCalls: false +statusBitsHeader: true diff --git a/cmd/amend/main.go b/cmd/amend/main.go index 5e19044..370a6d6 100644 --- a/cmd/amend/main.go +++ b/cmd/amend/main.go @@ -44,7 +44,7 @@ func main() { } ctx := context.Background() - sd := spr.NewStackedPR(nil, nil, gitcmd, os.Stdout, false, false) + sd := spr.NewStackedPR(nil, nil, gitcmd, os.Stdout) sd.AmendCommit(ctx) } diff --git a/cmd/spr/main.go b/cmd/spr/main.go index 6864782..123c9a1 100644 --- a/cmd/spr/main.go +++ b/cmd/spr/main.go @@ -10,9 +10,10 @@ import ( "github.com/ejoffe/spr/git/realgit" "github.com/ejoffe/spr/github/githubclient" "github.com/ejoffe/spr/spr" - flags "github.com/jessevdk/go-flags" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + + "github.com/urfave/cli/v2" ) var ( @@ -26,35 +27,11 @@ func init() { log.Logger = log.With().Caller().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr}) } -// command line options -type opts struct { - Debug bool `long:"debug" description:"Show runtime debug info."` - Detail bool `short:"d" long:"detail" description:"Show detailed output."` - Merge bool `short:"m" long:"merge" description:"Merge all mergeable pull requests."` - Status bool `short:"s" long:"status" description:"Show status of open pull requests."` - Update bool `short:"u" long:"update" description:"Update and create pull requests for unmerged commits in the stack."` - Version bool `short:"v" long:"version" description:"Show version info."` -} - func main() { - // parse command line options - var opts opts - parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash) - _, err := parser.Parse() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if opts.Version { - fmt.Printf("spr version : %s : %s : %s\n", version, date, commit[:8]) - os.Exit(0) - } - gitcmd := realgit.NewGitCmd(&config.Config{}) // check that we are inside a git dir var output string - err = gitcmd.Git("status --porcelain", &output) + err := gitcmd.Git("status --porcelain", &output) if err != nil { fmt.Println(output) fmt.Println(err) @@ -73,27 +50,136 @@ func main() { gitcmd = realgit.NewGitCmd(&cfg) - if opts.Debug { - zerolog.SetGlobalLevel(zerolog.DebugLevel) - rake.LoadSources(&cfg, rake.DebugWriter(os.Stdout)) - } - ctx := context.Background() client := githubclient.NewGitHubClient(ctx, &cfg) - stackedpr := spr.NewStackedPR(&cfg, client, gitcmd, os.Stdout, opts.Debug, opts.Detail) + stackedpr := spr.NewStackedPR(&cfg, client, gitcmd, os.Stdout) - if opts.Update { - stackedpr.UpdatePullRequests(ctx) - } else if opts.Merge { - stackedpr.MergePullRequests(ctx) - stackedpr.UpdatePullRequests(ctx) - } else if opts.Status { - stackedpr.StatusPullRequests(ctx) - } else { - stackedpr.StatusPullRequests(ctx) + detailFlag := &cli.BoolFlag{ + Name: "detail", + Value: false, + Usage: "Show detailed status bits output", } - if opts.Debug { - stackedpr.DebugPrintSummary() + cli.AppHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + +GLOBAL OPTIONS: +{{range .VisibleFlags}}{{"\t"}}{{.}} +{{end}} +COMMANDS: +{{range .Commands}}{{if not .HideHelp}} {{join .Names ","}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}} +AUTHOR: {{range .Authors}}{{ . }}{{end}} +VERSION: {{.Version}} +` + + app := &cli.App{ + Name: "spr", + Usage: "Stacked Pull Requests on GitHub", + HideVersion: true, + Version: fmt.Sprintf("%s : %s : %s\n", version, date, commit[:8]), + EnableBashCompletion: true, + Authors: []*cli.Author{ + { + Name: "Eitan Joffe", + Email: "eitan@apomelo.com", + }, + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "detail", + Value: false, + Usage: "Show detailed status bits output", + }, + &cli.BoolFlag{ + Name: "profile", + Value: false, + Usage: "Show runtime profiling info", + }, + &cli.BoolFlag{ + Name: "verbose", + Value: false, + Usage: "Show verbose logging", + }, + &cli.BoolFlag{ + Name: "debug", + Value: false, + Usage: "Show runtime debug info", + }, + }, + Before: func(c *cli.Context) error { + if c.IsSet("debug") { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + rake.LoadSources(&cfg, rake.DebugWriter(os.Stdout)) + } + if c.IsSet("profile") { + stackedpr.ProfilingEnable() + } + if c.IsSet("detail") || cfg.StatusBitsHeader { + stackedpr.DetailEnabled = true + } + if c.IsSet("verbose") { + cfg.LogGitCommands = true + cfg.LogGitHubCalls = true + } + return nil + }, + Action: func(c *cli.Context) error { + stackedpr.StatusPullRequests(ctx) + return nil + }, + Commands: []*cli.Command{ + { + Name: "status", + Usage: "Show status of open pull requests", + Action: func(c *cli.Context) error { + stackedpr.StatusPullRequests(ctx) + return nil + }, + Flags: []cli.Flag{ + detailFlag, + }, + }, + { + Name: "update", + Usage: "Update and create pull requests for updated commits in the stack", + Action: func(c *cli.Context) error { + stackedpr.UpdatePullRequests(ctx) + return nil + }, + Flags: []cli.Flag{ + detailFlag, + }, + }, + { + Name: "merge", + Usage: "Merge all mergeable pull requests", + Action: func(c *cli.Context) error { + stackedpr.MergePullRequests(ctx) + stackedpr.UpdatePullRequests(ctx) + return nil + }, + Flags: []cli.Flag{ + detailFlag, + }, + }, + { + Name: "version", + Usage: "Show version info", + Action: func(c *cli.Context) error { + return cli.Exit(c.App.Version, 0) + }, + }, + }, + After: func(c *cli.Context) error { + if c.IsSet("profile") { + stackedpr.ProfilingSummary() + } + return nil + }, } + + app.Run(os.Args) } diff --git a/config/config.go b/config/config.go index ab89fda..c15b413 100644 --- a/config/config.go +++ b/config/config.go @@ -21,6 +21,7 @@ type Config struct { CleanupRemoteBranch bool `default:"true" yaml:"cleanupRemoteBranch"` LogGitCommands bool `default:"false" yaml:"logGitCommands"` LogGitHubCalls bool `default:"false" yaml:"logGitHubCalls"` + StatusBitsHeader bool `default:"true" yaml:"statusBitsHeader"` } func ConfigFilePath(gitcmd git.GitInterface) string { diff --git a/go.mod b/go.mod index 964e555..ac00807 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect github.com/stretchr/testify v1.7.0 + github.com/urfave/cli/v2 v2.3.0 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 ) diff --git a/go.sum b/go.sum index 45abcc2..2666a63 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ejoffe/profiletimer v0.1.0 h1:Zq1WcdZhCMqXdsAVF1Vv95iTqKz7D54+hik7GDaAmJI= @@ -123,14 +125,20 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.22.0 h1:XrVUjV4K+izZpKXZHlPrYQiDtmdGiCylnT4i43AAWxg= github.com/rs/zerolog v1.22.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa h1:jozR3igKlnYCj9IVHOVump59bp07oIRoLQ/CcjMYIUA= github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -381,8 +389,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/readme.md b/readme.md index b4a7ffc..a4c58dc 100644 --- a/readme.md +++ b/readme.md @@ -146,6 +146,7 @@ These are the available configurations: | cleanupRemoteBranch | bool | true | delete remote branch after pull request merges | | logGitCommands | bool | false | logs all git commands to stdout | | logGitHubCalls | bool | false | logs all github api calls to stdout | +| statusBitsHeader | bool | true | show status bits type headers | Happy Coding! ------------- diff --git a/spr/spr.go b/spr/spr.go index b21ac43..384e1dd 100644 --- a/spr/spr.go +++ b/spr/spr.go @@ -18,39 +18,24 @@ import ( ) // NewStackedPR constructs and returns a new stackediff instance. -func NewStackedPR(config *config.Config, github github.GitHubInterface, gitcmd git.GitInterface, writer io.Writer, - debug bool, detail bool) *stackediff { - if debug { - return &stackediff{ - config: config, - github: github, - gitcmd: gitcmd, - writer: writer, - debug: true, - detail: false, - profiletimer: profiletimer.StartProfileTimer(), - } - } +func NewStackedPR(config *config.Config, github github.GitHubInterface, gitcmd git.GitInterface, writer io.Writer) *stackediff { return &stackediff{ config: config, github: github, gitcmd: gitcmd, writer: writer, - debug: false, - detail: detail, profiletimer: profiletimer.StartNoopTimer(), } } type stackediff struct { - config *config.Config - github github.GitHubInterface - gitcmd git.GitInterface - writer io.Writer - debug bool - detail bool - profiletimer profiletimer.Timer + config *config.Config + github github.GitHubInterface + gitcmd git.GitInterface + writer io.Writer + profiletimer profiletimer.Timer + DetailEnabled bool } // AmendCommit enables one to easily ammend a commit in the middle of a stack @@ -233,22 +218,29 @@ func (sd *stackediff) StatusPullRequests(ctx context.Context) { sd.profiletimer.Step("StatusPullRequests::Start") githubInfo := sd.github.GetInfo(ctx, sd.gitcmd) - for i := len(githubInfo.PullRequests) - 1; i >= 0; i-- { - pr := githubInfo.PullRequests[i] - fmt.Fprintf(sd.writer, "%s\n", pr.String(sd.config)) - } - if sd.detail { - fmt.Fprint(sd.writer, detailMessage) + if len(githubInfo.PullRequests) == 0 { + fmt.Fprintf(sd.writer, "pull request stack is empty\n") + } else { + if sd.DetailEnabled { + fmt.Fprint(sd.writer, detailMessage) + } + for i := len(githubInfo.PullRequests) - 1; i >= 0; i-- { + pr := githubInfo.PullRequests[i] + fmt.Fprintf(sd.writer, "%s\n", pr.String(sd.config)) + } } sd.profiletimer.Step("StatusPullRequests::End") } -// DebugPrintSummary prints debug info if debug mode is enabled. -func (sd *stackediff) DebugPrintSummary() { - if sd.debug { - err := sd.profiletimer.ShowResults() - check(err) - } +// ProfilingEnable enables stopwatch profiling +func (sd *stackediff) ProfilingEnable() { + sd.profiletimer = profiletimer.StartProfileTimer() +} + +// ProfilingSummary prints profiling info to stdout +func (sd *stackediff) ProfilingSummary() { + err := sd.profiletimer.ShowResults() + check(err) } // getLocalCommitStack returns a list of unmerged commits @@ -456,9 +448,10 @@ func check(err error) { } } -var detailMessage = ` ││││ - │││└─ stack check - ││└── no merge conflicts - │└─── pull request approved - └──── github checks pass +var detailMessage = ` + ┌─ github checks pass + │┌── pull request approved + ││┌─── no merge conflicts + │││┌──── stack check + ││││ ` diff --git a/spr/spr_test.go b/spr/spr_test.go index 038f932..71afdb2 100644 --- a/spr/spr_test.go +++ b/spr/spr_test.go @@ -30,7 +30,7 @@ func TestSPRBasicFlowFourCommits(t *testing.T) { LocalBranch: "master", } var output bytes.Buffer - s := NewStackedPR(&cfg, githubmock, gitmock, &output, false, false) + s := NewStackedPR(&cfg, githubmock, gitmock, &output) ctx := context.Background() @@ -58,7 +58,8 @@ func TestSPRBasicFlowFourCommits(t *testing.T) { // 'git spr -s' :: StatusPullRequest githubmock.ExpectGetInfo() s.StatusPullRequests(ctx) - assert.Equal("", output.String()) + assert.Equal("pull request stack is empty\n", output.String()) + output.Reset() // 'git spr -u' :: UpdatePullRequest :: commits=[c1] githubmock.ExpectGetInfo() @@ -137,7 +138,7 @@ func TestSPRAmendCommit(t *testing.T) { LocalBranch: "master", } var output bytes.Buffer - s := NewStackedPR(&cfg, githubmock, gitmock, &output, false, false) + s := NewStackedPR(&cfg, githubmock, gitmock, &output) ctx := context.Background() @@ -155,7 +156,7 @@ func TestSPRAmendCommit(t *testing.T) { // 'git spr -s' :: StatusPullRequest githubmock.ExpectGetInfo() s.StatusPullRequests(ctx) - assert.Equal("", output.String()) + assert.Equal("pull request stack is empty\n", output.String()) output.Reset() // 'git spr -u' :: UpdatePullRequest :: commits=[c1, c2] @@ -237,7 +238,7 @@ func TestSPRReorderCommit(t *testing.T) { LocalBranch: "master", } var output bytes.Buffer - s := NewStackedPR(&cfg, githubmock, gitmock, &output, false, false) + s := NewStackedPR(&cfg, githubmock, gitmock, &output) ctx := context.Background() @@ -265,7 +266,7 @@ func TestSPRReorderCommit(t *testing.T) { // 'git spr -s' :: StatusPullRequest githubmock.ExpectGetInfo() s.StatusPullRequests(ctx) - assert.Equal("", output.String()) + assert.Equal("pull request stack is empty\n", output.String()) output.Reset() // 'git spr -u' :: UpdatePullRequest :: commits=[c1, c2, c3, c4] @@ -321,7 +322,7 @@ func TestSPRReorderCommit(t *testing.T) { func TestParseLocalCommitStack(t *testing.T) { var buffer bytes.Buffer - sd := NewStackedPR(&config.Config{}, nil, nil, &buffer, false, false) + sd := NewStackedPR(&config.Config{}, nil, nil, &buffer) tests := []struct { name string inputCommitLog string