package diff import ( "bytes" "fmt" "io" "net/http" "strings" "testing" "testing/iotest" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_NewCmdDiff(t *testing.T) { tests := []struct { name string args string isTTY bool want DiffOptions wantErr string }{ { name: "name only", args: "--name-only", want: DiffOptions{ NameOnly: true, }, }, { name: "number argument", args: "123", isTTY: true, want: DiffOptions{ SelectorArg: "123", UseColor: true, }, }, { name: "no argument", args: "", isTTY: true, want: DiffOptions{ SelectorArg: "", UseColor: true, }, }, { name: "no color when redirected", args: "", isTTY: false, want: DiffOptions{ SelectorArg: "", UseColor: false, }, }, { name: "force color", args: "--color always", isTTY: false, want: DiffOptions{ SelectorArg: "", UseColor: true, }, }, { name: "disable color", args: "--color never", isTTY: true, want: DiffOptions{ SelectorArg: "", UseColor: false, }, }, { name: "no argument with --repo override", args: "-R owner/repo", isTTY: true, wantErr: "argument required when using the `--repo` flag", }, { name: "exclude single pattern", args: "--exclude '*.yml'", isTTY: true, want: DiffOptions{ SelectorArg: "", UseColor: true, Exclude: []string{"*.yml"}, }, }, { name: "exclude multiple patterns", args: "--exclude '*.yml' --exclude Makefile", isTTY: true, want: DiffOptions{ SelectorArg: "", UseColor: true, Exclude: []string{"*.yml", "Makefile"}, }, }, { name: "invalid --color argument", args: "--color doublerainbow", isTTY: true, wantErr: "invalid argument \"doublerainbow\" for \"--color\" flag: valid values are {always|never|auto}", }, { name: "web mode", args: "123 --web", isTTY: true, want: DiffOptions{ SelectorArg: "123", UseColor: true, BrowserMode: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, _, _ := iostreams.Test() ios.SetStdoutTTY(tt.isTTY) ios.SetStdinTTY(tt.isTTY) ios.SetStderrTTY(tt.isTTY) ios.SetColorEnabled(tt.isTTY) f := &cmdutil.Factory{ IOStreams: ios, } var opts *DiffOptions cmd := NewCmdDiff(f, func(o *DiffOptions) error { opts = o return nil }) cmd.PersistentFlags().StringP("repo", "R", "", "") argv, err := shlex.Split(tt.args) require.NoError(t, err) cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) _, err = cmd.ExecuteC() if tt.wantErr != "" { require.EqualError(t, err, tt.wantErr) return } else { require.NoError(t, err) } assert.Equal(t, tt.want.SelectorArg, opts.SelectorArg) assert.Equal(t, tt.want.UseColor, opts.UseColor) assert.Equal(t, tt.want.BrowserMode, opts.BrowserMode) assert.Equal(t, tt.want.Exclude, opts.Exclude) }) } } func Test_diffRun(t *testing.T) { pr := &api.PullRequest{Number: 123, URL: "https://github.com/OWNER/REPO/pull/123"} tests := []struct { name string opts DiffOptions wantFields []string wantStdout string wantStderr string wantBrowsedURL string httpStubs func(*httpmock.Registry) }{ { name: "no color", opts: DiffOptions{ SelectorArg: "123", UseColor: false, Patch: false, }, wantFields: []string{"number"}, wantStdout: fmt.Sprintf(testDiff, "", "", "", ""), httpStubs: func(reg *httpmock.Registry) { stubDiffRequest(reg, "application/vnd.github.v3.diff", fmt.Sprintf(testDiff, "", "", "", "")) }, }, { name: "with color", opts: DiffOptions{ SelectorArg: "123", UseColor: true, Patch: false, }, wantFields: []string{"number"}, wantStdout: fmt.Sprintf(testDiff, "\x1b[m", "\x1b[1;37m", "\x1b[32m", "\x1b[31m"), httpStubs: func(reg *httpmock.Registry) { stubDiffRequest(reg, "application/vnd.github.v3.diff", fmt.Sprintf(testDiff, "", "", "", "")) }, }, { name: "patch format", opts: DiffOptions{ SelectorArg: "123", UseColor: false, Patch: true, }, wantFields: []string{"number"}, wantStdout: fmt.Sprintf(testDiff, "", "", "", ""), httpStubs: func(reg *httpmock.Registry) { stubDiffRequest(reg, "application/vnd.github.v3.patch", fmt.Sprintf(testDiff, "", "", "", "")) }, }, { name: "name only", opts: DiffOptions{ SelectorArg: "123", UseColor: false, Patch: false, NameOnly: true, }, wantFields: []string{"number"}, wantStdout: ".github/workflows/releases.yml\nMakefile\n", httpStubs: func(reg *httpmock.Registry) { stubDiffRequest(reg, "application/vnd.github.v3.diff", fmt.Sprintf(testDiff, "", "", "", "")) }, }, { name: "exclude yml files", opts: DiffOptions{ SelectorArg: "123", UseColor: false, Exclude: []string{"*.yml"}, }, wantFields: []string{"number"}, wantStdout: `diff --git a/Makefile b/Makefile index f2b4805c..3d7bd0f9 100644 --- a/Makefile +++ b/Makefile @@ -22,8 +22,8 @@ test: go test ./... .PHONY: test -site: - git clone https://github.com/github/cli.github.com.git "$@" +site: bin/gh + bin/gh repo clone github/cli.github.com "$@" site-docs: site git -C site pull `, httpStubs: func(reg *httpmock.Registry) { stubDiffRequest(reg, "application/vnd.github.v3.diff", fmt.Sprintf(testDiff, "", "", "", "")) }, }, { name: "name only with exclude", opts: DiffOptions{ SelectorArg: "123", UseColor: false, NameOnly: true, Exclude: []string{"*.yml"}, }, wantFields: []string{"number"}, wantStdout: "Makefile\n", httpStubs: func(reg *httpmock.Registry) { stubDiffRequest(reg, "application/vnd.github.v3.diff", fmt.Sprintf(testDiff, "", "", "", "")) }, }, { name: "web mode", opts: DiffOptions{ SelectorArg: "123", BrowserMode: true, }, wantFields: []string{"url"}, wantStderr: "Opening https://github.com/OWNER/REPO/pull/123/files in your browser.\n", wantBrowsedURL: "https://github.com/OWNER/REPO/pull/123/files", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { httpReg := &httpmock.Registry{} defer httpReg.Verify(t) if tt.httpStubs != nil { tt.httpStubs(httpReg) } tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: httpReg}, nil } browser := &browser.Stub{} tt.opts.Browser = browser ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(true) tt.opts.IO = ios finder := shared.NewMockFinder("123", pr, ghrepo.New("OWNER", "REPO")) finder.ExpectFields(tt.wantFields) tt.opts.Finder = finder err := diffRun(&tt.opts) assert.NoError(t, err) assert.Equal(t, tt.wantStdout, stdout.String()) assert.Equal(t, tt.wantStderr, stderr.String()) assert.Equal(t, tt.wantBrowsedURL, browser.BrowsedURL()) }) } } const testDiff = `%[2]sdiff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml%[1]s %[2]sindex 73974448..b7fc0154 100644%[1]s %[2]s--- a/.github/workflows/releases.yml%[1]s %[2]s+++ b/.github/workflows/releases.yml%[1]s @@ -44,6 +44,11 @@ jobs: token: ${{secrets.SITE_GITHUB_TOKEN}} - name: Publish documentation site if: "!contains(github.ref, '-')" # skip prereleases %[3]s+ env:%[1]s %[3]s+ GIT_COMMITTER_NAME: cli automation%[1]s %[3]s+ GIT_AUTHOR_NAME: cli automation%[1]s %[3]s+ GIT_COMMITTER_EMAIL: noreply@github.com%[1]s %[3]s+ GIT_AUTHOR_EMAIL: noreply@github.com%[1]s run: make site-publish - name: Move project cards if: "!contains(github.ref, '-')" # skip prereleases %[2]sdiff --git a/Makefile b/Makefile%[1]s %[2]sindex f2b4805c..3d7bd0f9 100644%[1]s %[2]s--- a/Makefile%[1]s %[2]s+++ b/Makefile%[1]s @@ -22,8 +22,8 @@ test: go test ./... .PHONY: test %[4]s-site:%[1]s %[4]s- git clone https://github.com/github/cli.github.com.git "$@"%[1]s %[3]s+site: bin/gh%[1]s %[3]s+ bin/gh repo clone github/cli.github.com "$@"%[1]s site-docs: site git -C site pull ` func Test_colorDiffLines(t *testing.T) { inputs := []struct { input, output string }{ { input: "", output: "", }, { input: "\n", output: "\n", }, { input: "foo\nbar\nbaz\n", output: "foo\nbar\nbaz\n", }, { input: "foo\nbar\nbaz", output: "foo\nbar\nbaz\n", }, { input: fmt.Sprintf("+foo\n-b%sr\n+++ baz\n", strings.Repeat("a", 2*lineBufferSize)), output: fmt.Sprintf( "%[4]s+foo%[2]s\n%[5]s-b%[1]sr%[2]s\n%[3]s+++ baz%[2]s\n", strings.Repeat("a", 2*lineBufferSize), "\x1b[m", "\x1b[1;37m", "\x1b[32m", "\x1b[31m", ), }, } for _, tt := range inputs { buf := bytes.Buffer{} if err := colorDiffLines(&buf, strings.NewReader(tt.input)); err != nil { t.Fatalf("unexpected error: %s", err) } if got := buf.String(); got != tt.output { t.Errorf("expected: %q, got: %q", tt.output, got) } } } func Test_changedFileNames(t *testing.T) { inputs := []struct { input, output string }{ { input: "", output: "", }, { input: "\n", output: "", }, { input: "diff --git a/cmd.go b/cmd.go\n--- /dev/null\n+++ b/cmd.go\n@@ -0,0 +1,313 @@", output: "cmd.go\n", }, { input: "diff --git a/cmd.go b/cmd.go\n--- a/cmd.go\n+++ /dev/null\n@@ -0,0 +1,313 @@", output: "cmd.go\n", }, { input: fmt.Sprintf("diff --git a/baz.go b/rename.go\n--- a/baz.go\n+++ b/rename.go\n+foo\n-b%sr", strings.Repeat("a", 2*lineBufferSize)), output: "rename.go\n", }, { input: fmt.Sprintf("diff --git a/baz.go b/baz.go\n--- a/baz.go\n+++ b/baz.go\n+foo\n-b%sr", strings.Repeat("a", 2*lineBufferSize)), output: "baz.go\n", }, { input: "diff --git \"a/\343\202\212\343\203\274\343\201\251\343\201\277\343\203\274.md\" \"b/\343\202\212\343\203\274\343\201\251\343\201\277\343\203\274.md\"", output: "\"\343\202\212\343\203\274\343\201\251\343\201\277\343\203\274.md\"\n", }, } for _, tt := range inputs { buf := bytes.Buffer{} if err := changedFilesNames(&buf, strings.NewReader(tt.input)); err != nil { t.Fatalf("unexpected error: %s", err) } if got := buf.String(); got != tt.output { t.Errorf("expected: %q, got: %q", tt.output, got) } } } func stubDiffRequest(reg *httpmock.Registry, accept, diff string) { reg.Register( func(req *http.Request) bool { if !strings.EqualFold(req.Method, "GET") { return false } if req.URL.EscapedPath() != "/repos/OWNER/REPO/pulls/123" { return false } return req.Header.Get("Accept") == accept }, func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, Request: req, Body: io.NopCloser(strings.NewReader(diff)), }, nil }) } func Test_filterDiff(t *testing.T) { rawDiff := fmt.Sprintf(testDiff, "", "", "", "") tests := []struct { name string patterns []string want string }{ { name: "exclude yml files", patterns: []string{"*.yml"}, want: `diff --git a/Makefile b/Makefile index f2b4805c..3d7bd0f9 100644 --- a/Makefile +++ b/Makefile @@ -22,8 +22,8 @@ test: go test ./... .PHONY: test -site: - git clone https://github.com/github/cli.github.com.git "$@" +site: bin/gh + bin/gh repo clone github/cli.github.com "$@" site-docs: site git -C site pull `, }, { name: "exclude Makefile", patterns: []string{"Makefile"}, want: `diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 73974448..b7fc0154 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -44,6 +44,11 @@ jobs: token: ${{secrets.SITE_GITHUB_TOKEN}} - name: Publish documentation site if: "!contains(github.ref, '-')" # skip prereleases + env: + GIT_COMMITTER_NAME: cli automation + GIT_AUTHOR_NAME: cli automation + GIT_COMMITTER_EMAIL: noreply@github.com + GIT_AUTHOR_EMAIL: noreply@github.com run: make site-publish - name: Move project cards if: "!contains(github.ref, '-')" # skip prereleases `, }, { name: "exclude all files", patterns: []string{"*.yml", "Makefile"}, want: "", }, { name: "no matches", patterns: []string{"*.go"}, want: rawDiff, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reader, err := filterDiff(strings.NewReader(rawDiff), tt.patterns) require.NoError(t, err) got, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, tt.want, string(got)) }) } } func Test_matchesAny(t *testing.T) { tests := []struct { name string filename string patterns []string want bool }{ { name: "exact match", filename: "Makefile", patterns: []string{"Makefile"}, want: true, }, { name: "glob extension", filename: ".github/workflows/releases.yml", patterns: []string{"*.yml"}, want: true, }, { name: "no match", filename: "main.go", patterns: []string{"*.yml"}, want: false, }, { name: "directory glob", filename: ".github/workflows/releases.yml", patterns: []string{".github/*/*"}, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, matchesAny(tt.filename, tt.patterns)) }) } } func Test_sanitizedReader(t *testing.T) { input := strings.NewReader("\t hello \x1B[m world! ăѣ𝔠ծề\r\n") expected := "\t hello \\u{1b}[m world! ăѣ𝔠ծề\r\n" err := iotest.TestReader(sanitizedReader(input), []byte(expected)) if err != nil { t.Error(err) } }