From 97504f4f1f9ed65951088584e3e9f630a3b22134 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 29 Sep 2020 12:12:44 +0200 Subject: [PATCH 1/6] Filter out unwanted stderr output during git push commands --- git/git.go | 5 +- pkg/iostreams/regex_filter_writer.go | 46 +++++++ pkg/iostreams/regex_filter_writer_test.go | 143 ++++++++++++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 pkg/iostreams/regex_filter_writer.go create mode 100644 pkg/iostreams/regex_filter_writer_test.go diff --git a/git/git.go b/git/git.go index aa08c4d6c..ede717a00 100644 --- a/git/git.go +++ b/git/git.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/cli/cli/internal/run" + "github.com/cli/cli/pkg/iostreams" ) // ErrNotOnAnyBranch indicates that the user is in detached HEAD state @@ -164,8 +165,10 @@ func CommitBody(sha string) (string, error) { // Push publishes a git ref to a remote and sets up upstream configuration func Push(remote string, ref string) error { pushCmd := GitCommand("push", "--set-upstream", remote, ref) + regexp := regexp.MustCompile("^remote:.*$") + filterErr := iostreams.NewRegexFilterWriter(os.Stderr, regexp, "") pushCmd.Stdout = os.Stdout - pushCmd.Stderr = os.Stderr + pushCmd.Stderr = filterErr return run.PrepareCmd(pushCmd).Run() } diff --git a/pkg/iostreams/regex_filter_writer.go b/pkg/iostreams/regex_filter_writer.go new file mode 100644 index 000000000..d68ce858e --- /dev/null +++ b/pkg/iostreams/regex_filter_writer.go @@ -0,0 +1,46 @@ +package iostreams + +import ( + "bufio" + "bytes" + "io" + "regexp" +) + +func NewRegexFilterWriter(out io.Writer, regexp *regexp.Regexp, repl string) io.Writer { + return &RegexFilterWriter{out: out, regexp: *regexp, repl: repl} +} + +type RegexFilterWriter struct { + out io.Writer + regexp regexp.Regexp + repl string +} + +func (s RegexFilterWriter) Write(data []byte) (int, error) { + filtered := []byte{} + repl := []byte(s.repl) + scanner := bufio.NewScanner(bytes.NewReader(data)) + + for scanner.Scan() { + b := scanner.Bytes() + f := s.regexp.ReplaceAll(b, repl) + if len(f) > 0 { + filtered = append(filtered, f...) + filtered = append(filtered, []byte("\n")...) + } + } + + if err := scanner.Err(); err != nil { + return 0, err + } + + if len(filtered) != 0 { + _, err := s.out.Write(filtered) + if err != nil { + return 0, err + } + } + + return len(data), nil +} diff --git a/pkg/iostreams/regex_filter_writer_test.go b/pkg/iostreams/regex_filter_writer_test.go new file mode 100644 index 000000000..55386bca9 --- /dev/null +++ b/pkg/iostreams/regex_filter_writer_test.go @@ -0,0 +1,143 @@ +package iostreams + +import ( + "bytes" + "regexp" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/stretchr/testify/assert" +) + +func Test_Write(t *testing.T) { + type input struct { + in string + regexp *regexp.Regexp + repl string + } + type output struct { + wantsErr bool + out string + length int + } + tests := []struct { + name string + input input + output output + }{ + { + name: "single line input", + input: input{ + in: "some input line that has wrong information", + regexp: regexp.MustCompile("wrong"), + repl: "right", + }, + output: output{ + wantsErr: false, + out: "some input line that has right information\n", + length: 42, + }, + }, + { + name: "multiple line input", + input: input{ + in: "multiple lines\nin this\ninput lines", + regexp: regexp.MustCompile("lines"), + repl: "tests", + }, + output: output{ + wantsErr: false, + out: "multiple tests\nin this\ninput tests\n", + length: 34, + }, + }, + { + name: "no matches", + input: input{ + in: "this line has no matches", + regexp: regexp.MustCompile("wrong"), + repl: "right", + }, + output: output{ + wantsErr: false, + out: "this line has no matches\n", + length: 24, + }, + }, + { + name: "no output", + input: input{ + in: "remove this whole line", + regexp: regexp.MustCompile("^remove.*$"), + repl: "", + }, + output: output{ + wantsErr: false, + out: "", + length: 22, + }, + }, + { + name: "no input", + input: input{ + in: "", + regexp: regexp.MustCompile("remove"), + repl: "", + }, + output: output{ + wantsErr: false, + out: "", + length: 0, + }, + }, + { + name: "multiple lines removed", + input: input{ + in: "begining line\nremove this whole line\nremove this one also\nnot this one", + regexp: regexp.MustCompile("^remove.*$"), + repl: "", + }, + output: output{ + wantsErr: false, + out: "begining line\nnot this one\n", + length: 70, + }, + }, + { + name: "removes remote from git push output", + input: input{ + in: heredoc.Doc(` + output: some information + remote: + remote: Create a pull request for 'regex' on GitHub by visiting: + remote: https://github.com/owner/repo/pull/new/regex + remote: + output: more information + `), + regexp: regexp.MustCompile("^remote.*$"), + repl: "", + }, + output: output{ + wantsErr: false, + out: "output: some information\noutput: more information\n", + length: 189, + }, + }, + } + + for _, tt := range tests { + out := &bytes.Buffer{} + writer := NewRegexFilterWriter(out, tt.input.regexp, tt.input.repl) + t.Run(tt.name, func(t *testing.T) { + length, err := writer.Write([]byte(tt.input.in)) + + if tt.output.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.output.out, out.String()) + assert.Equal(t, tt.output.length, length) + }) + } +} From 28b3b2c9e7f22b62c9d79d361a2979a7326c8e8b Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Thu, 1 Oct 2020 10:13:41 +0200 Subject: [PATCH 2/6] Address PR comments --- git/git.go | 10 ++++------ pkg/cmd/pr/create/create.go | 7 ++++++- .../pr/create/regex_writer.go} | 10 +++++----- .../pr/create/regex_writer_test.go} | 14 +++++++------- 4 files changed, 22 insertions(+), 19 deletions(-) rename pkg/{iostreams/regex_filter_writer.go => cmd/pr/create/regex_writer.go} (68%) rename pkg/{iostreams/regex_filter_writer_test.go => cmd/pr/create/regex_writer_test.go} (91%) diff --git a/git/git.go b/git/git.go index ede717a00..2c2d32e0e 100644 --- a/git/git.go +++ b/git/git.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "io" "net/url" "os" "os/exec" @@ -12,7 +13,6 @@ import ( "strings" "github.com/cli/cli/internal/run" - "github.com/cli/cli/pkg/iostreams" ) // ErrNotOnAnyBranch indicates that the user is in detached HEAD state @@ -163,12 +163,10 @@ func CommitBody(sha string) (string, error) { } // Push publishes a git ref to a remote and sets up upstream configuration -func Push(remote string, ref string) error { +func Push(remote string, ref string, cmdOut, cmdErr io.Writer) error { pushCmd := GitCommand("push", "--set-upstream", remote, ref) - regexp := regexp.MustCompile("^remote:.*$") - filterErr := iostreams.NewRegexFilterWriter(os.Stderr, regexp, "") - pushCmd.Stdout = os.Stdout - pushCmd.Stderr = filterErr + pushCmd.Stdout = cmdOut + pushCmd.Stderr = cmdErr return run.PrepareCmd(pushCmd).Run() } diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 38950cb65..e52749e1b 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "net/url" + "os" + "regexp" "strings" "time" @@ -429,7 +431,10 @@ func createRun(opts *CreateOptions) error { pushTries := 0 maxPushTries := 3 for { - if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch)); err != nil { + regexp := regexp.MustCompile("^remote: (|Create a pull request for '.*' on GitHub by visiting:.*|.*https://github.com/.*/pull/new/.*)$") + cmdErr := NewRegexWriter(os.Stderr, regexp, "") + cmdOut := os.Stdout + if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch), cmdOut, cmdErr); err != nil { if didForkRepo && pushTries < maxPushTries { pushTries++ // first wait 2 seconds after forking, then 4s, then 6s diff --git a/pkg/iostreams/regex_filter_writer.go b/pkg/cmd/pr/create/regex_writer.go similarity index 68% rename from pkg/iostreams/regex_filter_writer.go rename to pkg/cmd/pr/create/regex_writer.go index d68ce858e..101a2cea5 100644 --- a/pkg/iostreams/regex_filter_writer.go +++ b/pkg/cmd/pr/create/regex_writer.go @@ -1,4 +1,4 @@ -package iostreams +package create import ( "bufio" @@ -7,17 +7,17 @@ import ( "regexp" ) -func NewRegexFilterWriter(out io.Writer, regexp *regexp.Regexp, repl string) io.Writer { - return &RegexFilterWriter{out: out, regexp: *regexp, repl: repl} +func NewRegexWriter(out io.Writer, regexp *regexp.Regexp, repl string) io.Writer { + return &RegexWriter{out: out, regexp: *regexp, repl: repl} } -type RegexFilterWriter struct { +type RegexWriter struct { out io.Writer regexp regexp.Regexp repl string } -func (s RegexFilterWriter) Write(data []byte) (int, error) { +func (s RegexWriter) Write(data []byte) (int, error) { filtered := []byte{} repl := []byte(s.repl) scanner := bufio.NewScanner(bytes.NewReader(data)) diff --git a/pkg/iostreams/regex_filter_writer_test.go b/pkg/cmd/pr/create/regex_writer_test.go similarity index 91% rename from pkg/iostreams/regex_filter_writer_test.go rename to pkg/cmd/pr/create/regex_writer_test.go index 55386bca9..bf57c3b48 100644 --- a/pkg/iostreams/regex_filter_writer_test.go +++ b/pkg/cmd/pr/create/regex_writer_test.go @@ -1,4 +1,4 @@ -package iostreams +package create import ( "bytes" @@ -108,26 +108,26 @@ func Test_Write(t *testing.T) { input: input{ in: heredoc.Doc(` output: some information - remote: - remote: Create a pull request for 'regex' on GitHub by visiting: + remote: + remote: Create a pull request for 'regex' on GitHub by visiting: remote: https://github.com/owner/repo/pull/new/regex - remote: + remote: output: more information `), - regexp: regexp.MustCompile("^remote.*$"), + regexp: regexp.MustCompile("^remote: (|Create a pull request for '.*' on GitHub by visiting:.*|.*https://github.com/.*/pull/new/.*)$"), repl: "", }, output: output{ wantsErr: false, out: "output: some information\noutput: more information\n", - length: 189, + length: 192, }, }, } for _, tt := range tests { out := &bytes.Buffer{} - writer := NewRegexFilterWriter(out, tt.input.regexp, tt.input.repl) + writer := NewRegexWriter(out, tt.input.regexp, tt.input.repl) t.Run(tt.name, func(t *testing.T) { length, err := writer.Write([]byte(tt.input.in)) From 212993632baaa80c760040b81deb63bbd4cfc47a Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Thu, 1 Oct 2020 12:45:33 +0200 Subject: [PATCH 3/6] Fix up regex to escape dot in URL --- pkg/cmd/pr/create/create.go | 2 +- pkg/cmd/pr/create/regex_writer_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index e52749e1b..12a1a9f2b 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -431,7 +431,7 @@ func createRun(opts *CreateOptions) error { pushTries := 0 maxPushTries := 3 for { - regexp := regexp.MustCompile("^remote: (|Create a pull request for '.*' on GitHub by visiting:.*|.*https://github.com/.*/pull/new/.*)$") + regexp := regexp.MustCompile("^remote: (|Create a pull request for '.*' on GitHub by visiting:.*|.*https://github\\.com/.*/pull/new/.*)$") cmdErr := NewRegexWriter(os.Stderr, regexp, "") cmdOut := os.Stdout if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch), cmdOut, cmdErr); err != nil { diff --git a/pkg/cmd/pr/create/regex_writer_test.go b/pkg/cmd/pr/create/regex_writer_test.go index bf57c3b48..e228c58aa 100644 --- a/pkg/cmd/pr/create/regex_writer_test.go +++ b/pkg/cmd/pr/create/regex_writer_test.go @@ -114,7 +114,7 @@ func Test_Write(t *testing.T) { remote: output: more information `), - regexp: regexp.MustCompile("^remote: (|Create a pull request for '.*' on GitHub by visiting:.*|.*https://github.com/.*/pull/new/.*)$"), + regexp: regexp.MustCompile("^remote: (|Create a pull request for '.*' on GitHub by visiting:.*|.*https://github\\.com/.*/pull/new/.*)$"), repl: "", }, output: output{ From eaa685ac9b2dafe84ad3a051f1ab801ae127bdb3 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Thu, 1 Oct 2020 16:22:11 +0200 Subject: [PATCH 4/6] Use custom scanLines function for RegexWriter --- pkg/cmd/pr/create/create.go | 7 +++---- pkg/cmd/pr/create/regex_writer.go | 18 +++++++++++++++++- pkg/cmd/pr/create/regex_writer_test.go | 14 +++++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 12a1a9f2b..bbb2e39d9 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/url" - "os" "regexp" "strings" "time" @@ -431,9 +430,9 @@ func createRun(opts *CreateOptions) error { pushTries := 0 maxPushTries := 3 for { - regexp := regexp.MustCompile("^remote: (|Create a pull request for '.*' on GitHub by visiting:.*|.*https://github\\.com/.*/pull/new/.*)$") - cmdErr := NewRegexWriter(os.Stderr, regexp, "") - cmdOut := os.Stdout + regexp := regexp.MustCompile("^remote: (Create a pull request.*by visiting|[[:space:]]*https://.*/pull/new/).*\n?$") + cmdErr := NewRegexWriter(opts.IO.ErrOut, regexp, "") + cmdOut := opts.IO.Out if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch), cmdOut, cmdErr); err != nil { if didForkRepo && pushTries < maxPushTries { pushTries++ diff --git a/pkg/cmd/pr/create/regex_writer.go b/pkg/cmd/pr/create/regex_writer.go index 101a2cea5..5c13c71b7 100644 --- a/pkg/cmd/pr/create/regex_writer.go +++ b/pkg/cmd/pr/create/regex_writer.go @@ -21,13 +21,13 @@ func (s RegexWriter) Write(data []byte) (int, error) { filtered := []byte{} repl := []byte(s.repl) scanner := bufio.NewScanner(bytes.NewReader(data)) + scanner.Split(scanLines) for scanner.Scan() { b := scanner.Bytes() f := s.regexp.ReplaceAll(b, repl) if len(f) > 0 { filtered = append(filtered, f...) - filtered = append(filtered, []byte("\n")...) } } @@ -44,3 +44,19 @@ func (s RegexWriter) Write(data []byte) (int, error) { return len(data), nil } + +func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + if i := bytes.IndexByte(data, '\n'); i >= 0 { + return i + 1, data[0 : i+1], nil + } + + if atEOF { + return len(data), data, nil + } + + return 0, nil, nil +} diff --git a/pkg/cmd/pr/create/regex_writer_test.go b/pkg/cmd/pr/create/regex_writer_test.go index e228c58aa..6efda0840 100644 --- a/pkg/cmd/pr/create/regex_writer_test.go +++ b/pkg/cmd/pr/create/regex_writer_test.go @@ -34,7 +34,7 @@ func Test_Write(t *testing.T) { }, output: output{ wantsErr: false, - out: "some input line that has right information\n", + out: "some input line that has right information", length: 42, }, }, @@ -47,7 +47,7 @@ func Test_Write(t *testing.T) { }, output: output{ wantsErr: false, - out: "multiple tests\nin this\ninput tests\n", + out: "multiple tests\nin this\ninput tests", length: 34, }, }, @@ -60,7 +60,7 @@ func Test_Write(t *testing.T) { }, output: output{ wantsErr: false, - out: "this line has no matches\n", + out: "this line has no matches", length: 24, }, }, @@ -94,12 +94,12 @@ func Test_Write(t *testing.T) { name: "multiple lines removed", input: input{ in: "begining line\nremove this whole line\nremove this one also\nnot this one", - regexp: regexp.MustCompile("^remove.*$"), + regexp: regexp.MustCompile("(?s)^remove.*$"), repl: "", }, output: output{ wantsErr: false, - out: "begining line\nnot this one\n", + out: "begining line\nnot this one", length: 70, }, }, @@ -114,12 +114,12 @@ func Test_Write(t *testing.T) { remote: output: more information `), - regexp: regexp.MustCompile("^remote: (|Create a pull request for '.*' on GitHub by visiting:.*|.*https://github\\.com/.*/pull/new/.*)$"), + regexp: regexp.MustCompile("^remote: (Create a pull request.*by visiting|[[:space:]]*https://.*/pull/new/).*\n?$"), repl: "", }, output: output{ wantsErr: false, - out: "output: some information\noutput: more information\n", + out: "output: some information\nremote: \nremote: \noutput: more information\n", length: 192, }, }, From bf46b870cb8b3c892b6d5ca67d7b7a64f0a9477b Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Wed, 7 Oct 2020 14:21:22 +0200 Subject: [PATCH 5/6] Add multiple writes test --- pkg/cmd/pr/create/regex_writer_test.go | 46 +++++++++++++++++--------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/pr/create/regex_writer_test.go b/pkg/cmd/pr/create/regex_writer_test.go index 6efda0840..b5bf8ce24 100644 --- a/pkg/cmd/pr/create/regex_writer_test.go +++ b/pkg/cmd/pr/create/regex_writer_test.go @@ -11,7 +11,7 @@ import ( func Test_Write(t *testing.T) { type input struct { - in string + in []string regexp *regexp.Regexp repl string } @@ -28,7 +28,7 @@ func Test_Write(t *testing.T) { { name: "single line input", input: input{ - in: "some input line that has wrong information", + in: []string{"some input line that has wrong information"}, regexp: regexp.MustCompile("wrong"), repl: "right", }, @@ -41,7 +41,7 @@ func Test_Write(t *testing.T) { { name: "multiple line input", input: input{ - in: "multiple lines\nin this\ninput lines", + in: []string{"multiple lines\nin this\ninput lines"}, regexp: regexp.MustCompile("lines"), repl: "tests", }, @@ -54,7 +54,7 @@ func Test_Write(t *testing.T) { { name: "no matches", input: input{ - in: "this line has no matches", + in: []string{"this line has no matches"}, regexp: regexp.MustCompile("wrong"), repl: "right", }, @@ -67,7 +67,7 @@ func Test_Write(t *testing.T) { { name: "no output", input: input{ - in: "remove this whole line", + in: []string{"remove this whole line"}, regexp: regexp.MustCompile("^remove.*$"), repl: "", }, @@ -80,7 +80,7 @@ func Test_Write(t *testing.T) { { name: "no input", input: input{ - in: "", + in: []string{""}, regexp: regexp.MustCompile("remove"), repl: "", }, @@ -93,7 +93,7 @@ func Test_Write(t *testing.T) { { name: "multiple lines removed", input: input{ - in: "begining line\nremove this whole line\nremove this one also\nnot this one", + in: []string{"begining line\nremove this whole line\nremove this one also\nnot this one"}, regexp: regexp.MustCompile("(?s)^remove.*$"), repl: "", }, @@ -106,14 +106,14 @@ func Test_Write(t *testing.T) { { name: "removes remote from git push output", input: input{ - in: heredoc.Doc(` + in: []string{heredoc.Doc(` output: some information remote: remote: Create a pull request for 'regex' on GitHub by visiting: remote: https://github.com/owner/repo/pull/new/regex remote: output: more information - `), + `)}, regexp: regexp.MustCompile("^remote: (Create a pull request.*by visiting|[[:space:]]*https://.*/pull/new/).*\n?$"), repl: "", }, @@ -123,19 +123,35 @@ func Test_Write(t *testing.T) { length: 192, }, }, + { + name: "multiple writes", + input: input{ + in: []string{"first write\n", "second write ", "third write"}, + regexp: regexp.MustCompile("write"), + repl: "read", + }, + output: output{ + wantsErr: false, + out: "first read\nsecond read third read", + length: 36, + }, + }, } for _, tt := range tests { out := &bytes.Buffer{} writer := NewRegexWriter(out, tt.input.regexp, tt.input.repl) t.Run(tt.name, func(t *testing.T) { - length, err := writer.Write([]byte(tt.input.in)) - - if tt.output.wantsErr { - assert.Error(t, err) - return + length := 0 + for _, in := range tt.input.in { + l, err := writer.Write([]byte(in)) + length = length + l + if tt.output.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) } - assert.NoError(t, err) assert.Equal(t, tt.output.out, out.String()) assert.Equal(t, tt.output.length, length) }) From 9acbf8aae458918383c371354c62a30de24d0264 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 13 Oct 2020 16:25:00 +0200 Subject: [PATCH 6/6] Refactor regexp writer --- pkg/cmd/pr/create/create.go | 43 +++++++----- pkg/cmd/pr/create/regex_writer.go | 62 ------------------ pkg/cmd/pr/create/regexp_writer.go | 64 ++++++++++++++++++ ...x_writer_test.go => regexp_writer_test.go} | 65 ++++++++++--------- 4 files changed, 124 insertions(+), 110 deletions(-) delete mode 100644 pkg/cmd/pr/create/regex_writer.go create mode 100644 pkg/cmd/pr/create/regexp_writer.go rename pkg/cmd/pr/create/{regex_writer_test.go => regexp_writer_test.go} (64%) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index bbb2e39d9..61ea0d799 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -427,24 +427,33 @@ func createRun(opts *CreateOptions) error { // automatically push the branch if it hasn't been pushed anywhere yet if isPushEnabled { - pushTries := 0 - maxPushTries := 3 - for { - regexp := regexp.MustCompile("^remote: (Create a pull request.*by visiting|[[:space:]]*https://.*/pull/new/).*\n?$") - cmdErr := NewRegexWriter(opts.IO.ErrOut, regexp, "") - cmdOut := opts.IO.Out - if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch), cmdOut, cmdErr); err != nil { - if didForkRepo && pushTries < maxPushTries { - pushTries++ - // first wait 2 seconds after forking, then 4s, then 6s - waitSeconds := 2 * pushTries - fmt.Fprintf(opts.IO.ErrOut, "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second")) - time.Sleep(time.Duration(waitSeconds) * time.Second) - continue + pushBranch := func() error { + pushTries := 0 + maxPushTries := 3 + for { + r := NewRegexpWriter(opts.IO.ErrOut, gitPushRegexp, "") + defer r.Flush() + cmdErr := r + cmdOut := opts.IO.Out + if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch), cmdOut, cmdErr); err != nil { + if didForkRepo && pushTries < maxPushTries { + pushTries++ + // first wait 2 seconds after forking, then 4s, then 6s + waitSeconds := 2 * pushTries + fmt.Fprintf(opts.IO.ErrOut, "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second")) + time.Sleep(time.Duration(waitSeconds) * time.Second) + continue + } + return err } - return err + break } - break + return nil + } + + err := pushBranch() + if err != nil { + return err } } @@ -565,3 +574,5 @@ func generateCompareURL(r ghrepo.Interface, base, head, title, body string, assi } return url, nil } + +var gitPushRegexp = regexp.MustCompile("^remote: (Create a pull request.*by visiting|[[:space:]]*https://.*/pull/new/).*\n?$") diff --git a/pkg/cmd/pr/create/regex_writer.go b/pkg/cmd/pr/create/regex_writer.go deleted file mode 100644 index 5c13c71b7..000000000 --- a/pkg/cmd/pr/create/regex_writer.go +++ /dev/null @@ -1,62 +0,0 @@ -package create - -import ( - "bufio" - "bytes" - "io" - "regexp" -) - -func NewRegexWriter(out io.Writer, regexp *regexp.Regexp, repl string) io.Writer { - return &RegexWriter{out: out, regexp: *regexp, repl: repl} -} - -type RegexWriter struct { - out io.Writer - regexp regexp.Regexp - repl string -} - -func (s RegexWriter) Write(data []byte) (int, error) { - filtered := []byte{} - repl := []byte(s.repl) - scanner := bufio.NewScanner(bytes.NewReader(data)) - scanner.Split(scanLines) - - for scanner.Scan() { - b := scanner.Bytes() - f := s.regexp.ReplaceAll(b, repl) - if len(f) > 0 { - filtered = append(filtered, f...) - } - } - - if err := scanner.Err(); err != nil { - return 0, err - } - - if len(filtered) != 0 { - _, err := s.out.Write(filtered) - if err != nil { - return 0, err - } - } - - return len(data), nil -} - -func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - - if i := bytes.IndexByte(data, '\n'); i >= 0 { - return i + 1, data[0 : i+1], nil - } - - if atEOF { - return len(data), data, nil - } - - return 0, nil, nil -} diff --git a/pkg/cmd/pr/create/regexp_writer.go b/pkg/cmd/pr/create/regexp_writer.go new file mode 100644 index 000000000..500637d7c --- /dev/null +++ b/pkg/cmd/pr/create/regexp_writer.go @@ -0,0 +1,64 @@ +package create + +import ( + "bytes" + "io" + "regexp" +) + +func NewRegexpWriter(out io.Writer, re *regexp.Regexp, repl string) *RegexpWriter { + return &RegexpWriter{out: out, re: *re, repl: repl} +} + +type RegexpWriter struct { + out io.Writer + re regexp.Regexp + repl string + buf []byte +} + +func (s *RegexpWriter) Write(data []byte) (int, error) { + if len(data) == 0 { + return 0, nil + } + + filtered := []byte{} + repl := []byte(s.repl) + lines := bytes.SplitAfter(data, []byte("\n")) + + if len(s.buf) > 0 { + lines[0] = append(s.buf, lines[0]...) + } + + for i, line := range lines { + if i == len(lines) { + s.buf = line + } else { + f := s.re.ReplaceAll(line, repl) + if len(f) > 0 { + filtered = append(filtered, f...) + } + } + } + + if len(filtered) != 0 { + _, err := s.out.Write(filtered) + if err != nil { + return 0, err + } + } + + return len(data), nil +} + +func (s *RegexpWriter) Flush() (int, error) { + if len(s.buf) > 0 { + repl := []byte(s.repl) + filtered := s.re.ReplaceAll(s.buf, repl) + if len(filtered) > 0 { + return s.out.Write(filtered) + } + } + + return 0, nil +} diff --git a/pkg/cmd/pr/create/regex_writer_test.go b/pkg/cmd/pr/create/regexp_writer_test.go similarity index 64% rename from pkg/cmd/pr/create/regex_writer_test.go rename to pkg/cmd/pr/create/regexp_writer_test.go index b5bf8ce24..fd772a760 100644 --- a/pkg/cmd/pr/create/regex_writer_test.go +++ b/pkg/cmd/pr/create/regexp_writer_test.go @@ -11,9 +11,9 @@ import ( func Test_Write(t *testing.T) { type input struct { - in []string - regexp *regexp.Regexp - repl string + in []string + re *regexp.Regexp + repl string } type output struct { wantsErr bool @@ -28,9 +28,9 @@ func Test_Write(t *testing.T) { { name: "single line input", input: input{ - in: []string{"some input line that has wrong information"}, - regexp: regexp.MustCompile("wrong"), - repl: "right", + in: []string{"some input line that has wrong information"}, + re: regexp.MustCompile("wrong"), + repl: "right", }, output: output{ wantsErr: false, @@ -41,9 +41,9 @@ func Test_Write(t *testing.T) { { name: "multiple line input", input: input{ - in: []string{"multiple lines\nin this\ninput lines"}, - regexp: regexp.MustCompile("lines"), - repl: "tests", + in: []string{"multiple lines\nin this\ninput lines"}, + re: regexp.MustCompile("lines"), + repl: "tests", }, output: output{ wantsErr: false, @@ -54,9 +54,9 @@ func Test_Write(t *testing.T) { { name: "no matches", input: input{ - in: []string{"this line has no matches"}, - regexp: regexp.MustCompile("wrong"), - repl: "right", + in: []string{"this line has no matches"}, + re: regexp.MustCompile("wrong"), + repl: "right", }, output: output{ wantsErr: false, @@ -67,9 +67,9 @@ func Test_Write(t *testing.T) { { name: "no output", input: input{ - in: []string{"remove this whole line"}, - regexp: regexp.MustCompile("^remove.*$"), - repl: "", + in: []string{"remove this whole line"}, + re: regexp.MustCompile("^remove.*$"), + repl: "", }, output: output{ wantsErr: false, @@ -80,9 +80,9 @@ func Test_Write(t *testing.T) { { name: "no input", input: input{ - in: []string{""}, - regexp: regexp.MustCompile("remove"), - repl: "", + in: []string{""}, + re: regexp.MustCompile("remove"), + repl: "", }, output: output{ wantsErr: false, @@ -93,9 +93,9 @@ func Test_Write(t *testing.T) { { name: "multiple lines removed", input: input{ - in: []string{"begining line\nremove this whole line\nremove this one also\nnot this one"}, - regexp: regexp.MustCompile("(?s)^remove.*$"), - repl: "", + in: []string{"begining line\nremove this whole line\nremove this one also\nnot this one"}, + re: regexp.MustCompile("(?s)^remove.*$"), + repl: "", }, output: output{ wantsErr: false, @@ -108,27 +108,27 @@ func Test_Write(t *testing.T) { input: input{ in: []string{heredoc.Doc(` output: some information - remote: - remote: Create a pull request for 'regex' on GitHub by visiting: + remote: + remote: Create a pull request for 'regex' on GitHub by visiting: remote: https://github.com/owner/repo/pull/new/regex - remote: + remote: output: more information `)}, - regexp: regexp.MustCompile("^remote: (Create a pull request.*by visiting|[[:space:]]*https://.*/pull/new/).*\n?$"), - repl: "", + re: regexp.MustCompile("^remote: (Create a pull request.*by visiting|[[:space:]]*https://.*/pull/new/).*\n?$"), + repl: "", }, output: output{ wantsErr: false, - out: "output: some information\nremote: \nremote: \noutput: more information\n", - length: 192, + out: "output: some information\nremote:\nremote:\noutput: more information\n", + length: 189, }, }, { name: "multiple writes", input: input{ - in: []string{"first write\n", "second write ", "third write"}, - regexp: regexp.MustCompile("write"), - repl: "read", + in: []string{"first write\n", "second write ", "third write"}, + re: regexp.MustCompile("write"), + repl: "read", }, output: output{ wantsErr: false, @@ -140,7 +140,7 @@ func Test_Write(t *testing.T) { for _, tt := range tests { out := &bytes.Buffer{} - writer := NewRegexWriter(out, tt.input.regexp, tt.input.repl) + writer := NewRegexpWriter(out, tt.input.re, tt.input.repl) t.Run(tt.name, func(t *testing.T) { length := 0 for _, in := range tt.input.in { @@ -152,6 +152,7 @@ func Test_Write(t *testing.T) { } assert.NoError(t, err) } + writer.Flush() assert.Equal(t, tt.output.out, out.String()) assert.Equal(t, tt.output.length, length) })