578 lines
14 KiB
Go
578 lines
14 KiB
Go
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)
|
|
}
|
|
}
|