From e849b902024bbbd8f841fbbbf618f8b3f8248931 Mon Sep 17 00:00:00 2001 From: Bob Glickstein Date: Wed, 29 Mar 2023 06:20:33 -0700 Subject: [PATCH 1/4] Add -pr for inspecting GitHub pull requests (#11) * Checkpoint. * Reorganize main package. Add -pr and -token. Update main package doc comment. Wire up doPR. * Adjust whitespace in comment template. * Upgrade go-git dependency. * Improve (?) isModverComment. * Oops. * Take a stab at adding "modver -pr" to the GitHub Actions workflow. * Wrong version of actions/checkout. * Disable TestGit for now. * Some decoupling courtesy of github.com/bobg/decouple. * Add a test with mocks. * More test coverage. * Unskip TestGit. * Fix (?) broken test. * If it ain't broke? * ??? * Grr * revert revert revert * Flail flail flail * Update go-git. * Skip go-git in GitHub Actions. * Oops. * Oh come on. * Downgrade go-git. * go mod tidy * Formatting. * Add a fetch-master step. * checkout v3, no -v for go test * Add a workaround. * Add a different workaround. * More test coverage. * Oops, pass os.Args[1:] to fs.Parse. * More test coverage. * More test coverage. * More test coverage. --- .github/workflows/go.yml | 32 +++-- .gitignore | 4 +- cmd/modver/comment.md.tmpl | 35 +++++ cmd/modver/compare.go | 43 ++++++ cmd/modver/main.go | 140 +++++++++++++++++++ cmd/modver/main_test.go | 74 ++++++++++ cmd/modver/modver.go | 277 ------------------------------------- cmd/modver/options.go | 89 ++++++++++++ cmd/modver/options_test.go | 123 ++++++++++++++++ cmd/modver/pr.go | 132 ++++++++++++++++++ cmd/modver/pr_test.go | 124 +++++++++++++++++ cmd/modver/tags.go | 101 ++++++++++++++ cmd/modver/tags_test.go | 15 ++ compare.go | 15 +- go.mod | 41 +++--- go.sum | 181 ++++++++++++++++-------- modver_test.go | 7 + 17 files changed, 1066 insertions(+), 367 deletions(-) create mode 100644 cmd/modver/comment.md.tmpl create mode 100644 cmd/modver/compare.go create mode 100644 cmd/modver/main.go create mode 100644 cmd/modver/main_test.go delete mode 100644 cmd/modver/modver.go create mode 100644 cmd/modver/options.go create mode 100644 cmd/modver/options_test.go create mode 100644 cmd/modver/pr.go create mode 100644 cmd/modver/pr_test.go create mode 100644 cmd/modver/tags.go create mode 100644 cmd/modver/tags_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b6fbd73..571b0bc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,22 +10,24 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.18 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18 - - name: Unit tests - run: go test -v -coverprofile=cover.out ./... + - name: Unit tests + run: go test -coverprofile=cover.out ./... - - name: Send coverage - uses: shogo82148/actions-goveralls@v1 - with: - path-to-profile: cover.out + - name: Modver + if: ${{ github.event_name == 'pull_request' }} + run: go run ./cmd/modver -pr https://github.com/${{ github.repository }}/pull/${{ github.event.number }} -token ${{ github.token }} -pretty - - name: Version string check - run: go run ./cmd/modver -git `pwd`/.git -versions HEAD~1 HEAD + - name: Send coverage + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: cover.out diff --git a/.gitignore b/.gitignore index 2849b5a..2f567b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ *~ -cover.out -modver +/cover.out +/modver diff --git a/cmd/modver/comment.md.tmpl b/cmd/modver/comment.md.tmpl new file mode 100644 index 0000000..8148973 --- /dev/null +++ b/cmd/modver/comment.md.tmpl @@ -0,0 +1,35 @@ +# Modver result + +This report was generated by [Modver](https://pkg.go.dev/github.com/bobg/modver/v2), +a Go package and command that helps you obey [semantic versioning rules](https://semver.org/) in your Go module. + +{{ if eq .Code "Major" }} + +This PR requires an increase in your module’s major version number. +If the new major version number is 2 or greater, +you must also add or update the version suffix +on the module path defined in your `go.mod` file. +See [the Go Modules Reference](https://go.dev/ref/mod#major-version-suffixes) for more info. + +{{ else if eq .Code "Minor" }} + +This PR requires (at least) an increase in your module's minor version number. + +{{ else if eq .Code "Patchlevel" }} + +This PR requires (at least) an increase in your module's patchlevel. + +{{ else }} + +This PR does not require a change in your module’s version number. +(You might still consider bumping the patchlevel anyway.) + +{{ end }} + +{{ if ne .Code "None" }} + +``` +{{ .Report -}} +``` + +{{ end }} diff --git a/cmd/modver/compare.go b/cmd/modver/compare.go new file mode 100644 index 0000000..6b0aa7f --- /dev/null +++ b/cmd/modver/compare.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/google/go-github/v50/github" + "github.com/pkg/errors" + + "github.com/bobg/modver/v2" +) + +func doCompare(ctx context.Context, opts options) (modver.Result, error) { + if opts.pr != "" { + owner, reponame, prnum, err := parsePR(opts.pr) + if err != nil { + return modver.None, errors.Wrap(err, "parsing pull-request URL") + } + if opts.ghtoken == "" { + return modver.None, fmt.Errorf("usage: %s -pr URL -token TOKEN [-q | -pretty]", os.Args[0]) + } + gh := github.NewTokenClient(ctx, opts.ghtoken) + return doPR(ctx, gh, owner, reponame, prnum) + } + + if opts.gitRepo != "" { + if len(opts.args) != 2 { + return nil, fmt.Errorf("usage: %s -git REPO [-gitcmd GIT_COMMAND] [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION | -versions] OLDERREV NEWERREV", os.Args[0]) + } + + callback := modver.CompareDirs + if opts.versions { + callback = getTags(&opts.v1, &opts.v2, opts.args[0], opts.args[1]) + } + + return modver.CompareGitWith(ctx, opts.gitRepo, opts.args[0], opts.args[1], callback) + } + if len(opts.args) != 2 { + return nil, fmt.Errorf("usage: %s [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR", os.Args[0]) + } + return modver.CompareDirs(opts.args[0], opts.args[1]) +} diff --git a/cmd/modver/main.go b/cmd/modver/main.go new file mode 100644 index 0000000..54d094e --- /dev/null +++ b/cmd/modver/main.go @@ -0,0 +1,140 @@ +// Command modver compares two versions of the same Go packages +// and tells whether a Major, Minor, or Patchlevel version bump +// (or None) +// is needed to go from one to the other. +// +// Usage: +// +// modver -git REPO [-gitcmd GIT_COMMAND] [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION | -versions] OLDERREV NEWERREV +// modver [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR +// modver -pr URL [-q | -pretty] +// +// With `-git REPO`, +// where REPO is the path to a Git repository, +// OLDER and NEWER are two revisions in the repository +// (e.g. hexadecimal SHA strings or "HEAD", etc) +// containing the older and newer versions of a Go module. +// Without the -git flag, +// OLDER and NEWER are two directories containing the older and newer versions of a Go module. +// +// With `-gitcmd GIT_COMMAND`, +// modver uses the given command for Git operations. +// This is "git" by default. +// If the command does not exist or is not found in your PATH, +// modver falls back to using the go-git library. +// +// With -v1 and -v2, +// modver checks whether the change from OLDERVERSION to NEWERVERSION +// (two version strings) +// is adequate for the differences detected between OLDER and NEWER. +// Output is either "OK" or "ERR" +// (followed by a description) +// and the exit code is 0 for OK and 1 for ERR. +// In quiet mode (-q), +// there is no output. +// With -git REPO and -versions instead of -v1 and -v2, +// the values for -v1 and -v2 are determined by querying the repo at the given revisions. +// +// Without -v1 and -v2 +// (or -versions), +// output is a string describing the minimum version-number change required. +// In quiet mode (-q), +// there is no output, +// and the exit status is 0, 1, 2, 3, or 4 +// for None, Patchlevel, Minor, Major, and error. +package main + +import ( + "context" + "fmt" + "io" + "os" + + "golang.org/x/mod/semver" + + "github.com/bobg/modver/v2" +) + +const errorStatus = 4 + +func main() { + opts, err := parseArgs() + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing args: %s\n", err) + os.Exit(errorStatus) + } + + ctx := context.Background() + if opts.gitCmd != "" { + ctx = modver.WithGit(ctx, opts.gitCmd) + } + + res, err := doCompare(ctx, opts) + if err != nil { + fmt.Fprintf(os.Stderr, "Error in comparing: %s\n", err) + os.Exit(errorStatus) + } + + exitCode := doShowResult(os.Stdout, res, opts) + os.Exit(exitCode) +} + +func doShowResult(out io.Writer, res modver.Result, opts options) int { + if opts.v1 != "" && opts.v2 != "" { + var ok bool + + cmp := semver.Compare(opts.v1, opts.v2) + switch res.Code() { + case modver.None: + ok = cmp <= 0 // v1 <= v2 + + case modver.Patchlevel: + ok = cmp < 0 // v1 < v2 + + case modver.Minor: + var ( + min1 = semver.MajorMinor(opts.v1) + min2 = semver.MajorMinor(opts.v2) + ) + ok = semver.Compare(min1, min2) < 0 // min1 < min2 + + case modver.Major: + var ( + maj1 = semver.Major(opts.v1) + maj2 = semver.Major(opts.v2) + ) + ok = semver.Compare(maj1, maj2) < 0 // maj1 < maj2 + } + + if ok { + if !opts.quiet { + if opts.versions { + fmt.Fprintf(out, "OK using versions %s and %s: %s\n", opts.v1, opts.v2, res) + } else { + fmt.Fprintf(out, "OK %s\n", res) + } + } + return 0 + } + if !opts.quiet { + if opts.versions { + fmt.Fprintf(out, "ERR using versions %s and %s: %s\n", opts.v1, opts.v2, res) + } else { + fmt.Fprintf(out, "ERR %s\n", res) + } + } + return 1 + } + + if opts.quiet { + return int(res.Code()) + } + + if opts.pretty { + modver.Pretty(out, res) + } else { + fmt.Fprintln(out, res) + } + + return 0 +} diff --git a/cmd/modver/main_test.go b/cmd/modver/main_test.go new file mode 100644 index 0000000..b48cd63 --- /dev/null +++ b/cmd/modver/main_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "bytes" + "fmt" + "testing" + + "github.com/bobg/modver/v2" +) + +func TestDoShowResult(t *testing.T) { + cases := []struct { + res modver.Result + opts options + wantExitCode int + want string + }{{ + res: modver.Patchlevel, + opts: options{quiet: true}, + wantExitCode: int(modver.Patchlevel), + }, { + res: modver.Patchlevel, + want: modver.Patchlevel.String() + "\n", + }, { + res: modver.None, + opts: options{v1: "v1.0.0", v2: "v1.0.1"}, + want: "OK None\n", + }, { + res: modver.None, + opts: options{v1: "v1.0.1", v2: "v1.0.0"}, + want: "ERR None\n", + wantExitCode: 1, + }, { + res: modver.Patchlevel, + opts: options{v1: "v1.0.0", v2: "v1.0.1"}, + want: "OK Patchlevel\n", + }, { + res: modver.Patchlevel, + opts: options{v1: "v1.0.0", v2: "v1.0.0"}, + want: "ERR Patchlevel\n", + wantExitCode: 1, + }, { + res: modver.Minor, + opts: options{v1: "v1.0.0", v2: "v1.1.0"}, + want: "OK Minor\n", + }, { + res: modver.Minor, + opts: options{v1: "v1.0.0", v2: "v1.0.1"}, + want: "ERR Minor\n", + wantExitCode: 1, + }, { + res: modver.Major, + opts: options{v1: "v1.0.0", v2: "v2.0.0"}, + want: "OK Major\n", + }, { + res: modver.Major, + opts: options{v1: "v1.0.0", v2: "v1.1.0"}, + want: "ERR Major\n", + wantExitCode: 1, + }} + + for i, tc := range cases { + t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { + buf := new(bytes.Buffer) + exitCode := doShowResult(buf, tc.res, tc.opts) + if exitCode != tc.wantExitCode { + t.Errorf("got exit code %d, want %d", exitCode, tc.wantExitCode) + } + if buf.String() != tc.want { + t.Errorf("got %s, want %s", buf, tc.want) + } + }) + } +} diff --git a/cmd/modver/modver.go b/cmd/modver/modver.go deleted file mode 100644 index 81331ed..0000000 --- a/cmd/modver/modver.go +++ /dev/null @@ -1,277 +0,0 @@ -// Command modver compares two versions of the same Go packages -// and tells whether a Major, Minor, or Patchlevel version bump -// (or None) -// is needed to go from one to the other. -// -// Usage: -// -// modver -git REPO [-gitcmd GIT_COMMAND] [-q] [-v1 OLDERVERSION -v2 NEWERVERSION | -versions] OLDERREV NEWERREV -// modver [-q] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR -// -// With `-git REPO`, -// where REPO is the path to a Git repository, -// OLDER and NEWER are two revisions in the repository -// (e.g. hexadecimal SHA strings or "HEAD", etc) -// containing the older and newer versions of a Go module. -// Without the -git flag, -// OLDER and NEWER are two directories containing the older and newer versions of a Go module. -// -// With `-gitcmd GIT_COMMAND`, -// modver uses the given command for Git operations. -// This is "git" by default. -// If the command does not exist or is not found in your PATH, -// modver falls back to using the go-git library. -// -// With -v1 and -v2, -// modver checks whether the change from OLDERVERSION to NEWERVERSION -// (two version strings) -// is adequate for the differences detected between OLDER and NEWER. -// Output is either "OK" or "ERR" -// (followed by a description) -// and the exit code is 0 for OK and 1 for ERR. -// In quiet mode (-q), -// there is no output. -// With -git REPO and -versions instead of -v1 and -v2, -// the values for -v1 and -v2 are determined by querying the repo at the given revisions. -// -// Without -v1 and -v2 -// (or -versions), -// output is a string describing the minimum version-number change required. -// In quiet mode (-q), -// there is no output, -// and the exit status is 0, 1, 2, 3, or 4 -// for None, Patchlevel, Minor, Major, and error. -package main - -import ( - "context" - "errors" - "flag" - "fmt" - "io" - "os" - "strings" - - "golang.org/x/mod/semver" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/storer" - - "github.com/bobg/modver/v2" -) - -const errorStatus = 4 - -func main() { - gitRepo, v1, v2, gitCmd, quiet, pretty, versions, err := parseArgs() - if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing args: %s\n", err) - os.Exit(errorStatus) - } - - ctx := context.Background() - if gitCmd != "" { - ctx = modver.WithGit(ctx, gitCmd) - } - - res, err := doCompare(ctx, gitRepo, v1, v2, versions) - if err != nil { - fmt.Fprintf(os.Stderr, "Error in comparing: %s\n", err) - os.Exit(errorStatus) - } - - doShowResultExit(res, quiet, pretty, v1, v2, versions) -} - -func parseArgs() (gitRepo, v1, v2, gitCmd string, quiet, pretty, versions bool, err error) { - flag.StringVar(&gitCmd, "gitcmd", "git", "use this command for git operations, if found; otherwise use the go-git library") - flag.StringVar(&gitRepo, "git", "", "Git repo URL") - flag.BoolVar(&quiet, "q", false, "quiet mode: prints no output, exits with status 0, 1, 2, 3, or 4 to mean None, Patchlevel, Minor, Major, or error") - flag.BoolVar(&pretty, "pretty", false, "result is shown in a pretty format with (possibly) multiple lines and indentation") - flag.StringVar(&v1, "v1", "", "version string of older version; with -v2 changes output to OK (exit status 0) for adequate version-number change, ERR (exit status 1) for inadequate") - flag.StringVar(&v2, "v2", "", "version string of newer version") - flag.BoolVar(&versions, "versions", false, "with -git, compute values for -v1 and -v2 from the Git repository") - flag.Parse() - - if v1 != "" && v2 != "" { - if !strings.HasPrefix(v1, "v") { - v1 = "v" + v1 - } - if !strings.HasPrefix(v2, "v") { - v2 = "v" + v2 - } - if !semver.IsValid(v1) { - err = fmt.Errorf("not a valid version string: %s", v1) - return - } - if !semver.IsValid(v2) { - err = fmt.Errorf("not a valid version string: %s", v2) - return - } - } - - return -} - -func doCompare(ctx context.Context, gitRepo, v1, v2 string, versions bool) (modver.Result, error) { - if gitRepo != "" { - if flag.NArg() != 2 { - return nil, fmt.Errorf("usage: %s -git REPO [-gitcmd GIT_COMMAND] [-q] [-v1 OLDERVERSION -v2 NEWERVERSION | -versions] OLDERREV NEWERREV", os.Args[0]) - } - - callback := modver.CompareDirs - if versions { - callback = getTags(&v1, &v2, flag.Arg(0), flag.Arg(1)) - } - - return modver.CompareGitWith(ctx, gitRepo, flag.Arg(0), flag.Arg(1), callback) - } - if flag.NArg() != 2 { - return nil, fmt.Errorf("usage: %s [-q] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR", os.Args[0]) - } - return modver.CompareDirs(flag.Arg(0), flag.Arg(1)) -} - -func doShowResultExit(res modver.Result, quiet, pretty bool, v1, v2 string, versions bool) { - if v1 != "" && v2 != "" { - var ok bool - - cmp := semver.Compare(v1, v2) - switch res.Code() { - case modver.None: - ok = cmp <= 0 // v1 <= v2 - - case modver.Patchlevel: - ok = cmp < 0 // v1 < v2 - - case modver.Minor: - var ( - min1 = semver.MajorMinor(v1) - min2 = semver.MajorMinor(v2) - ) - ok = semver.Compare(min1, min2) < 0 // min1 < min2 - - case modver.Major: - var ( - maj1 = semver.Major(v1) - maj2 = semver.Major(v2) - ) - ok = semver.Compare(maj1, maj2) < 0 // maj1 < maj2 - } - - if ok { - if !quiet { - if versions { - fmt.Printf("OK using versions %s and %s: %s\n", v1, v2, res) - } else { - fmt.Printf("OK %s\n", res) - } - } - os.Exit(0) - } - if !quiet { - if versions { - fmt.Printf("ERR using versions %s and %s: %s\n", v1, v2, res) - } else { - fmt.Printf("ERR %s\n", res) - } - } - os.Exit(1) - } - - if quiet { - os.Exit(int(res.Code())) - } - - if pretty { - modver.Pretty(os.Stdout, res) - } else { - fmt.Println(res) - } -} - -func getTags(v1, v2 *string, olderRev, newerRev string) func(older, newer string) (modver.Result, error) { - return func(older, newer string) (modver.Result, error) { - tag, err := getTag(older, olderRev) - if err != nil { - return modver.None, fmt.Errorf("getting tag from %s: %w", older, err) - } - *v1 = tag - - tag, err = getTag(newer, newerRev) - if err != nil { - return modver.None, fmt.Errorf("getting tag from %s: %w", newer, err) - } - *v2 = tag - - return modver.CompareDirs(older, newer) - } -} - -func getTag(dir, rev string) (string, error) { - repo, err := git.PlainOpen(dir) - if err != nil { - return "", fmt.Errorf("opening %s: %w", dir, err) - } - tags, err := repo.Tags() - if err != nil { - return "", fmt.Errorf("getting tags in %s: %w", dir, err) - } - hash, err := repo.ResolveRevision(plumbing.Revision(rev)) - if err != nil { - return "", fmt.Errorf(`resolving revision "%s" in %s: %w`, rev, dir, err) - } - repoCommit, err := object.GetCommit(repo.Storer, *hash) - if err != nil { - return "", fmt.Errorf("getting commit at %s: %w", rev, err) - } - - return getTagHelper(dir, rev, repo.Storer, tags, hash, repoCommit) -} - -func getTagHelper(dir, rev string, s storer.EncodedObjectStorer, tags storer.ReferenceIter, hash *plumbing.Hash, repoCommit *object.Commit) (string, error) { - var result string - -OUTER: - for { - tref, err := tags.Next() - if errors.Is(err, io.EOF) { - return result, nil - } - if err != nil { - return "", fmt.Errorf("iterating over tags in %s: %w", dir, err) - } - tag := strings.TrimPrefix(string(tref.Name()), "refs/tags/") - if !semver.IsValid(tag) { - continue - } - tagCommit, err := object.GetCommit(s, tref.Hash()) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: getting commit for tag %s: %s", tref.Name(), err) - continue - } - if tagCommit.Hash != *hash { - bases, err := repoCommit.MergeBase(tagCommit) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: getting merge base of %s and %s: %s", rev, tag, err) - continue - } - INNER: - for _, base := range bases { - switch base.Hash { - case *hash: - // This tag comes later than the checked-out commit. - continue OUTER - case tagCommit.Hash: - // The checked-out commit comes later than the tag. - break INNER - } - } - } - if result == "" || semver.Compare(result, tag) < 0 { // result < tag - result = tag - } - } -} diff --git a/cmd/modver/options.go b/cmd/modver/options.go new file mode 100644 index 0000000..30457a3 --- /dev/null +++ b/cmd/modver/options.go @@ -0,0 +1,89 @@ +package main + +import ( + "flag" + "fmt" + "net/url" + "os" + "strconv" + "strings" + + "github.com/pkg/errors" + "golang.org/x/mod/semver" +) + +type options struct { + gitRepo, gitCmd, ghtoken, v1, v2, pr string + quiet, pretty, versions bool + args []string +} + +func parseArgs() (options, error) { + return parseArgsHelper(os.Args[1:]) +} + +func parseArgsHelper(args []string) (opts options, err error) { + var fs flag.FlagSet + + fs.BoolVar(&opts.pretty, "pretty", false, "result is shown in a pretty format with (possibly) multiple lines and indentation") + fs.BoolVar(&opts.quiet, "q", false, "quiet mode: prints no output, exits with status 0, 1, 2, 3, or 4 to mean None, Patchlevel, Minor, Major, or error") + fs.BoolVar(&opts.versions, "versions", false, "with -git, compute values for -v1 and -v2 from the Git repository") + fs.StringVar(&opts.ghtoken, "token", os.Getenv("GITHUB_TOKEN"), "GitHub access token") + fs.StringVar(&opts.gitCmd, "gitcmd", "git", "use this command for git operations, if found; otherwise use the go-git library") + fs.StringVar(&opts.gitRepo, "git", "", "Git repo URL") + fs.StringVar(&opts.pr, "pr", "", "URL of GitHub pull request") + fs.StringVar(&opts.v1, "v1", "", "version string of older version; with -v2 changes output to OK (exit status 0) for adequate version-number change, ERR (exit status 1) for inadequate") + fs.StringVar(&opts.v2, "v2", "", "version string of newer version") + if err := fs.Parse(args); err != nil { + return opts, errors.Wrap(err, "parsing args") + } + opts.args = fs.Args() + + if opts.pr != "" { + if opts.gitRepo != "" { + return opts, fmt.Errorf("do not specify -git with -pr") + } + if opts.v1 != "" || opts.v2 != "" || opts.versions { + return opts, fmt.Errorf("do not specify -v1, -v2, or -versions with -pr") + } + } + + if opts.v1 != "" && opts.v2 != "" { + if !strings.HasPrefix(opts.v1, "v") { + opts.v1 = "v" + opts.v1 + } + if !strings.HasPrefix(opts.v2, "v") { + opts.v2 = "v" + opts.v2 + } + if !semver.IsValid(opts.v1) { + return opts, fmt.Errorf("not a valid version string: %s", opts.v1) + } + if !semver.IsValid(opts.v2) { + return opts, fmt.Errorf("not a valid version string: %s", opts.v2) + } + } + + return opts, nil +} + +func parsePR(pr string) (owner, reponame string, prnum int, err error) { + u, err := url.Parse(pr) + if err != nil { + err = errors.Wrap(err, "parsing GitHub pull-request URL") + return + } + path := strings.TrimLeft(u.Path, "/") + parts := strings.Split(path, "/") + if len(parts) < 4 { + err = fmt.Errorf("too few path elements in pull-request URL (got %d, want 4)", len(parts)) + return + } + if parts[2] != "pull" { + err = fmt.Errorf("pull-request URL not in expected format") + return + } + owner, reponame = parts[0], parts[1] + prnum, err = strconv.Atoi(parts[3]) + err = errors.Wrap(err, "parsing number from GitHub pull-request URL") + return +} diff --git a/cmd/modver/options_test.go b/cmd/modver/options_test.go new file mode 100644 index 0000000..5c25a50 --- /dev/null +++ b/cmd/modver/options_test.go @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "os" + "reflect" + "testing" +) + +func TestParseArgs(t *testing.T) { + ghtok := os.Getenv("GITHUB_TOKEN") + + cases := []struct { + args []string + wantErr bool + want options + }{{ + want: options{ + ghtoken: ghtok, + gitCmd: "git", + }, + }, { + args: []string{"-pr", "foo"}, + want: options{ + pr: "foo", + ghtoken: ghtok, + gitCmd: "git", + }, + }, { + args: []string{"-pr", "foo", "-git", "bar"}, + wantErr: true, + }, { + args: []string{"-pr", "foo", "-v1", "bar"}, + wantErr: true, + }, { + args: []string{"-pr", "foo", "-v2", "bar"}, + wantErr: true, + }, { + args: []string{"-pr", "foo", "-versions"}, + wantErr: true, + }, { + args: []string{"-v1", "1", "-v2", "2"}, + want: options{ + v1: "v1", + v2: "v2", + ghtoken: ghtok, + gitCmd: "git", + }, + }, { + args: []string{"-v1", "1", "-v2", "bar"}, + wantErr: true, + }, { + args: []string{"-v1", "foo", "-v2", "2"}, + wantErr: true, + }} + + for i, tc := range cases { + t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { + got, err := parseArgsHelper(tc.args) + if err != nil { + if !tc.wantErr { + t.Errorf("got error %v, wanted no error", err) + } + return + } + if tc.wantErr { + t.Fatal("got no error but wanted one") + } + if len(got.args) == 0 { + got.args = nil // not []string{} + } + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("got %+v, want %+v", got, tc.want) + } + }) + } +} + +func TestParsePR(t *testing.T) { + cases := []struct { + inp string + wantErr bool + owner, reponame string + prnum int + }{{ + wantErr: true, + }, { + inp: "https://x/y", + wantErr: true, + }, { + inp: "https://github.com/bobg/modver/bleah/17", + wantErr: true, + }, { + inp: "https://github.com/bobg/modver/pull/17", + owner: "bobg", + reponame: "modver", + prnum: 17, + }} + + for i, tc := range cases { + t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { + owner, reponame, prnum, err := parsePR(tc.inp) + if err != nil { + if !tc.wantErr { + t.Errorf("got error %v, wanted no error", err) + } + return + } + if tc.wantErr { + t.Fatal("got no error but wanted one") + } + if owner != tc.owner { + t.Errorf("got owner %s, want %s", owner, tc.owner) + } + if reponame != tc.reponame { + t.Errorf("got repo %s, want %s", reponame, tc.reponame) + } + if prnum != tc.prnum { + t.Errorf("got PR number %d, want %d", prnum, tc.prnum) + } + }) + } +} diff --git a/cmd/modver/pr.go b/cmd/modver/pr.go new file mode 100644 index 0000000..0788ceb --- /dev/null +++ b/cmd/modver/pr.go @@ -0,0 +1,132 @@ +package main + +import ( + "bufio" + "bytes" + "context" + _ "embed" + "io" + "regexp" + "strings" + "text/template" + + "github.com/google/go-github/v50/github" + "github.com/pkg/errors" + + "github.com/bobg/modver/v2" +) + +func doPR(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) { + return prHelper(ctx, gh.Repositories, gh.PullRequests, gh.Issues, modver.CompareGit, owner, reponame, prnum) +} + +type reposIntf interface { + Get(ctx context.Context, owner, reponame string) (*github.Repository, *github.Response, error) +} + +type prsIntf interface { + Get(ctx context.Context, owner, reponame string, number int) (*github.PullRequest, *github.Response, error) +} + +type issuesIntf interface { + createCommenter + editCommenter + ListComments(ctx context.Context, owner, reponame string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) +} + +func prHelper(ctx context.Context, repos reposIntf, prs prsIntf, issues issuesIntf, comparer func(ctx context.Context, cloneURL, baseSHA, headSHA string) (modver.Result, error), owner, reponame string, prnum int) (modver.Result, error) { + repo, _, err := repos.Get(ctx, owner, reponame) + if err != nil { + return modver.None, errors.Wrap(err, "getting repository") + } + pr, _, err := prs.Get(ctx, owner, reponame, prnum) + if err != nil { + return modver.None, errors.Wrap(err, "getting pull request") + } + result, err := comparer(ctx, *repo.CloneURL, *pr.Base.SHA, *pr.Head.SHA) + if err != nil { + return modver.None, errors.Wrap(err, "comparing versions") + } + comments, _, err := issues.ListComments(ctx, owner, reponame, prnum, nil) + if err != nil { + return modver.None, errors.Wrap(err, "listing PR comments") + } + + for _, c := range comments { + if isModverComment(c) { + err = updateComment(ctx, issues, repo, c, result) + return result, errors.Wrap(err, "updating PR comment") + } + } + + err = createComment(ctx, issues, repo, pr, result) + return result, errors.Wrap(err, "creating PR comment") +} + +var modverCommentRegex = regexp.MustCompile(`^# Modver result$`) + +func isModverComment(comment *github.IssueComment) bool { + var r io.Reader = strings.NewReader(*comment.Body) + r = &io.LimitedReader{R: r, N: 1024} + sc := bufio.NewScanner(r) + for sc.Scan() { + if modverCommentRegex.MatchString(sc.Text()) { + return true + } + } + return false +} + +type createCommenter interface { + CreateComment(ctx context.Context, owner, reponame string, num int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) +} + +func createComment(ctx context.Context, issues createCommenter, repo *github.Repository, pr *github.PullRequest, result modver.Result) error { + body, err := commentBody(result) + if err != nil { + return errors.Wrap(err, "rendering comment body") + } + comment := &github.IssueComment{ + Body: &body, + } + _, _, err = issues.CreateComment(ctx, *repo.Owner.Login, *repo.Name, *pr.Number, comment) + return errors.Wrap(err, "creating GitHub comment") +} + +type editCommenter interface { + EditComment(ctx context.Context, owner, reponame string, commentID int64, newComment *github.IssueComment) (*github.IssueComment, *github.Response, error) +} + +func updateComment(ctx context.Context, issues editCommenter, repo *github.Repository, comment *github.IssueComment, result modver.Result) error { + body, err := commentBody(result) + if err != nil { + return errors.Wrap(err, "rendering comment body") + } + newComment := &github.IssueComment{ + Body: &body, + } + _, _, err = issues.EditComment(ctx, *repo.Owner.Login, *repo.Name, *comment.ID, newComment) + return errors.Wrap(err, "editing GitHub comment") +} + +//go:embed comment.md.tmpl +var commentTplStr string + +var commentTpl = template.Must(template.New("").Parse(commentTplStr)) + +func commentBody(result modver.Result) (string, error) { + report := new(bytes.Buffer) + modver.Pretty(report, result) + + s := struct { + Code string + Report string + }{ + Code: result.Code().String(), + Report: report.String(), + } + + out := new(bytes.Buffer) + err := commentTpl.Execute(out, s) + return out.String(), err +} diff --git a/cmd/modver/pr_test.go b/cmd/modver/pr_test.go new file mode 100644 index 0000000..ffa44b4 --- /dev/null +++ b/cmd/modver/pr_test.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "strings" + "testing" + + "github.com/google/go-github/v50/github" + + "github.com/bobg/modver/v2" +) + +func TestPRHelper(t *testing.T) { + var ( + ctx = context.Background() + repos mockReposService + prs mockPRsService + ) + + t.Run("new-comment", func(t *testing.T) { + var issues mockIssuesService + + result, err := prHelper(ctx, repos, prs, &issues, mockComparer(modver.Minor), "owner", "repo", 17) + if err != nil { + t.Fatal(err) + } + if result.Code() != modver.Minor { + t.Fatalf("got result %s, want %s", result, modver.Minor) + } + if !strings.HasPrefix(issues.body, "# Modver result") { + t.Error("issues.body does not start with # Modver result") + } + if issues.commentID != 0 { + t.Errorf("issues.commentID is %d, want 0", issues.commentID) + } + }) + + t.Run("new-comment", func(t *testing.T) { + issues := mockIssuesService{update: true} + + result, err := prHelper(ctx, repos, prs, &issues, mockComparer(modver.Minor), "owner", "repo", 17) + if err != nil { + t.Fatal(err) + } + if result.Code() != modver.Minor { + t.Fatalf("got result %s, want %s", result, modver.Minor) + } + if !strings.HasPrefix(issues.body, "# Modver result") { + t.Error("issues.body does not start with # Modver result") + } + if issues.commentID != 2 { + t.Errorf("issues.commentID is %d, want 0", issues.commentID) + } + }) +} + +type mockReposService struct{} + +func (mockReposService) Get(ctx context.Context, owner, reponame string) (*github.Repository, *github.Response, error) { + return &github.Repository{ + Owner: &github.User{ + Login: &owner, + }, + Name: &reponame, + CloneURL: ptr("cloneURL"), + }, nil, nil +} + +type mockPRsService struct{} + +func (mockPRsService) Get(ctx context.Context, owner, reponame string, number int) (*github.PullRequest, *github.Response, error) { + return &github.PullRequest{ + Base: &github.PullRequestBranch{SHA: ptr("baseSHA")}, + Head: &github.PullRequestBranch{SHA: ptr("headSHA")}, + Number: ptr(17), + }, nil, nil +} + +type mockIssuesService struct { + update bool + owner, repo string + commentID int64 + body string +} + +func (m *mockIssuesService) CreateComment(ctx context.Context, owner, reponame string, num int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { + m.owner = owner + m.repo = reponame + m.commentID = 0 + m.body = *comment.Body + return nil, nil, nil +} + +func (m *mockIssuesService) EditComment(ctx context.Context, owner, reponame string, commentID int64, newComment *github.IssueComment) (*github.IssueComment, *github.Response, error) { + m.owner = owner + m.repo = reponame + m.commentID = commentID + m.body = *newComment.Body + return nil, nil, nil +} + +func (m *mockIssuesService) ListComments(ctx context.Context, owner, reponame string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) { + result := []*github.IssueComment{{ + ID: ptr(int64(1)), + Body: ptr("not a modver comment"), + }} + if m.update { + result = append(result, &github.IssueComment{ + ID: ptr(int64(2)), + Body: ptr("# Modver result\n\nwoop"), + }) + } + return result, nil, nil +} + +func mockComparer(result modver.Result) func(ctx context.Context, cloneURL, baseSHA, headSHA string) (modver.Result, error) { + return func(ctx context.Context, cloneURL, baseSHA, headSHA string) (modver.Result, error) { + return result, nil + } +} + +func ptr[T any](x T) *T { + return &x +} diff --git a/cmd/modver/tags.go b/cmd/modver/tags.go new file mode 100644 index 0000000..2ce613f --- /dev/null +++ b/cmd/modver/tags.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/storer" + "github.com/pkg/errors" + "golang.org/x/mod/semver" + + "github.com/bobg/modver/v2" +) + +func getTags(v1, v2 *string, olderRev, newerRev string) func(older, newer string) (modver.Result, error) { + return func(older, newer string) (modver.Result, error) { + tag, err := getTag(older, olderRev) + if err != nil { + return modver.None, fmt.Errorf("getting tag from %s: %w", older, err) + } + *v1 = tag + + tag, err = getTag(newer, newerRev) + if err != nil { + return modver.None, fmt.Errorf("getting tag from %s: %w", newer, err) + } + *v2 = tag + + return modver.CompareDirs(older, newer) + } +} + +func getTag(dir, rev string) (string, error) { + repo, err := git.PlainOpen(dir) + if err != nil { + return "", fmt.Errorf("opening %s: %w", dir, err) + } + tags, err := repo.Tags() + if err != nil { + return "", fmt.Errorf("getting tags in %s: %w", dir, err) + } + hash, err := repo.ResolveRevision(plumbing.Revision(rev)) + if err != nil { + return "", fmt.Errorf(`resolving revision "%s" in %s: %w`, rev, dir, err) + } + repoCommit, err := object.GetCommit(repo.Storer, *hash) + if err != nil { + return "", fmt.Errorf("getting commit at %s: %w", rev, err) + } + + return getTagHelper(dir, rev, repo.Storer, tags, hash, repoCommit) +} + +func getTagHelper(dir, rev string, s storer.EncodedObjectStorer, tags storer.ReferenceIter, hash *plumbing.Hash, repoCommit *object.Commit) (string, error) { + var result string + +OUTER: + for { + tref, err := tags.Next() + if errors.Is(err, io.EOF) { + return result, nil + } + if err != nil { + return "", fmt.Errorf("iterating over tags in %s: %w", dir, err) + } + tag := strings.TrimPrefix(string(tref.Name()), "refs/tags/") + if !semver.IsValid(tag) { + continue + } + tagCommit, err := object.GetCommit(s, tref.Hash()) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: getting commit for tag %s: %s", tref.Name(), err) + continue + } + if tagCommit.Hash != *hash { + bases, err := repoCommit.MergeBase(tagCommit) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: getting merge base of %s and %s: %s", rev, tag, err) + continue + } + INNER: + for _, base := range bases { + switch base.Hash { + case *hash: + // This tag comes later than the checked-out commit. + continue OUTER + case tagCommit.Hash: + // The checked-out commit comes later than the tag. + break INNER + } + } + } + if result == "" || semver.Compare(result, tag) < 0 { // result < tag + result = tag + } + } +} diff --git a/cmd/modver/tags_test.go b/cmd/modver/tags_test.go new file mode 100644 index 0000000..0ec99b9 --- /dev/null +++ b/cmd/modver/tags_test.go @@ -0,0 +1,15 @@ +package main + +import "testing" + +func TestGetTag(t *testing.T) { + got, err := getTag("../..", "aa470e1b623810ea1434f51b569f37cf9a0782ab") + if err != nil { + t.Fatal(err) + } + + const want = "v1.1.8" + if got != want { + t.Errorf("got %s, want %s", got, want) + } +} diff --git a/compare.go b/compare.go index 894c6ee..58adb32 100644 --- a/compare.go +++ b/compare.go @@ -303,7 +303,7 @@ func gitSetup(ctx context.Context, repoURL, dir, rev string) error { cloneOpts := &git.CloneOptions{URL: repoURL, NoCheckout: true} repo, err := git.PlainCloneContext(ctx, dir, false, cloneOpts) if err != nil { - return fmt.Errorf("cloning %s into %s: %w", repoURL, dir, err) + return cloneBugErr{repoURL: repoURL, dir: dir, err: err} } worktree, err := repo.Worktree() if err != nil { @@ -321,3 +321,16 @@ func gitSetup(ctx context.Context, repoURL, dir, rev string) error { return nil } + +type cloneBugErr struct { + repoURL, dir string + err error +} + +func (cb cloneBugErr) Error() string { + return fmt.Sprintf("cloning %s into %s: %s", cb.repoURL, cb.dir, cb.err) +} + +func (cb cloneBugErr) Unwrap() error { + return cb.err +} diff --git a/go.mod b/go.mod index 6ab0e43..5b91a3e 100644 --- a/go.mod +++ b/go.mod @@ -3,28 +3,35 @@ module github.com/bobg/modver/v2 go 1.18 require ( - github.com/go-git/go-git/v5 v5.4.2 + github.com/go-git/go-git/v5 v5.6.1 + github.com/google/go-github/v50 v50.2.0 github.com/pkg/errors v0.9.1 - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/tools v0.1.12 + golang.org/x/mod v0.9.0 + golang.org/x/tools v0.7.0 ) require ( - github.com/Microsoft/go-winio v0.4.16 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect - github.com/acomagu/bufpipe v1.0.3 // indirect - github.com/emirpasic/gods v1.12.0 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/cloudflare/circl v1.3.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect - github.com/go-git/go-billy/v5 v5.3.1 // indirect - github.com/google/go-cmp v0.4.0 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/sergi/go-diff v1.1.0 // indirect - github.com/xanzy/ssh-agent v0.3.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/skeema/knownhosts v1.1.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/sys v0.6.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 02899ef..d891e17 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,56 @@ -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= -github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310 h1:dGAdTcqheKrQ/TW76sAcmO2IorwXplUw2inPkOzykbw= +github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk= +github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= -github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= -github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= -github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= -github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ= +github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= +github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= +github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v50 v50.2.0 h1:j2FyongEHlO9nxXLc+LP3wuBSVU9mVxfpdYUexMpIfk= +github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -47,54 +60,110 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= +github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= -github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -104,7 +173,9 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/modver_test.go b/modver_test.go index 611d36b..625046f 100644 --- a/modver_test.go +++ b/modver_test.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "errors" "fmt" "io" "log" @@ -193,6 +194,12 @@ func TestGit(t *testing.T) { // Do it once with the go-git library. res, err := CompareGit(ctx, gitDir, "HEAD", "HEAD") + var cberr cloneBugErr + if errors.As(err, &cberr) { + // Workaround for an apparent bug in go-git. See https://github.com/go-git/go-git/issues/726. + t.Logf("Encountered clone bug, trying workaround: %s", cberr) + res, err = CompareGit(ctx, "https://github.com/bobg/modver", "HEAD", "HEAD") + } if err != nil { t.Fatal(err) } From 8247ebb8ce348e2e152f3dbb6c0281586f983047 Mon Sep 17 00:00:00 2001 From: Bob Glickstein Date: Fri, 31 Mar 2023 06:00:37 -0700 Subject: [PATCH 2/4] The main package is not part of the public API (#12) * The main package is not part of the public API. * Beef up isPublic and add test coverage. --- compare.go | 26 +++++++++++++++--- public_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 public_test.go diff --git a/compare.go b/compare.go index 58adb32..502a468 100644 --- a/compare.go +++ b/compare.go @@ -86,10 +86,29 @@ func Compare(olders, newers []*packages.Package) Result { return None } +func isPublic(pkgpath string) bool { + switch pkgpath { + case "internal", "main": + return false + } + if strings.HasSuffix(pkgpath, "/main") { + return false + } + if strings.HasPrefix(pkgpath, "internal/") { + return false + } + if strings.HasSuffix(pkgpath, "/internal") { + return false + } + if strings.Contains(pkgpath, "/internal/") { + return false + } + return true +} + func (c *comparer) compareMajor(older, newer map[string]*packages.Package) Result { for pkgPath, pkg := range older { - if strings.Contains(pkgPath, "/internal/") || strings.HasSuffix(pkgPath, "/internal") { - // Nothing in an internal package or subpackage is part of the public API. + if !isPublic(pkgPath) { continue } @@ -132,8 +151,7 @@ func (c *comparer) compareMajor(older, newer map[string]*packages.Package) Resul func (c *comparer) compareMinor(older, newer map[string]*packages.Package) Result { for pkgPath, pkg := range newer { - if strings.Contains(pkgPath, "/internal/") || strings.HasSuffix(pkgPath, "/internal") { - // Nothing in an internal package or subpackage is part of the public API. + if !isPublic(pkgPath) { continue } diff --git a/public_test.go b/public_test.go new file mode 100644 index 0000000..ad96a22 --- /dev/null +++ b/public_test.go @@ -0,0 +1,72 @@ +package modver + +import ( + "fmt" + "testing" +) + +func TestIsPublic(t *testing.T) { + cases := []struct { + inp string + want bool + }{{ + inp: "main", + want: false, + }, { + inp: "internal", + want: false, + }, { + inp: "mainx", + want: true, + }, { + inp: "internalx", + want: true, + }, { + inp: "foo/main", + want: false, + }, { + inp: "main/foo", + want: true, + }, { + inp: "foo/mainx", + want: true, + }, { + inp: "mainx/foo", + want: true, + }, { + inp: "foo/internal", + want: false, + }, { + inp: "internal/foo", + want: false, + }, { + inp: "foo/internal/bar", + want: false, + }, { + inp: "foo/internalx", + want: true, + }, { + inp: "internalx/foo", + want: true, + }, { + inp: "foo/xinternal/bar", + want: true, + }, { + inp: "foo/xinternal", + want: true, + }, { + inp: "xinternal/foo", + want: true, + }, { + inp: "foo/xinternal/bar", + want: true, + }} + for i, tc := range cases { + t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { + got := isPublic(tc.inp) + if got != tc.want { + t.Errorf("got %v, want %v", got, tc.want) + } + }) + } +} From d6904db897537897aad2a7316086594656e4b851 Mon Sep 17 00:00:00 2001 From: Bob Glickstein Date: Fri, 31 Mar 2023 06:02:32 -0700 Subject: [PATCH 3/4] Fix a couple of usage strings. (#14) --- cmd/modver/compare.go | 2 +- cmd/modver/main.go | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cmd/modver/compare.go b/cmd/modver/compare.go index 6b0aa7f..e86c525 100644 --- a/cmd/modver/compare.go +++ b/cmd/modver/compare.go @@ -18,7 +18,7 @@ func doCompare(ctx context.Context, opts options) (modver.Result, error) { return modver.None, errors.Wrap(err, "parsing pull-request URL") } if opts.ghtoken == "" { - return modver.None, fmt.Errorf("usage: %s -pr URL -token TOKEN [-q | -pretty]", os.Args[0]) + return modver.None, fmt.Errorf("usage: %s -pr URL [-token TOKEN]", os.Args[0]) } gh := github.NewTokenClient(ctx, opts.ghtoken) return doPR(ctx, gh, owner, reponame, prnum) diff --git a/cmd/modver/main.go b/cmd/modver/main.go index 54d094e..e18c357 100644 --- a/cmd/modver/main.go +++ b/cmd/modver/main.go @@ -5,9 +5,18 @@ // // Usage: // +// modver -pr URL [-token GITHUB_TOKEN] // modver -git REPO [-gitcmd GIT_COMMAND] [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION | -versions] OLDERREV NEWERREV // modver [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR -// modver -pr URL [-q | -pretty] +// +// With `-pr URL`, +// the URL must be that of a github.com pull request +// (having the form https://github.com/OWNER/REPO/pull/NUMBER). +// The environment variable GITHUB_TOKEN must contain a valid GitHub access token, +// or else one must be supplied on the command line with -token. +// In this mode, +// modver compares the base of the pull-request branch with the head +// and produces a report that it adds as a comment to the pull request. // // With `-git REPO`, // where REPO is the path to a Git repository, From 9f38d1bb884257c001d5bbd0d950cd94667738a3 Mon Sep 17 00:00:00 2001 From: Bob Glickstein Date: Sat, 1 Apr 2023 08:29:57 -0700 Subject: [PATCH 4/4] Make the bobg/modver repo suitable for use in GitHub Actions (#15) * Checkpoint. * Checkpoint. Refactor some stuff to internal and create cmd/modver-action. * go mod tidy; move a test * More test coverage. * INPUT_GITHUB_TOKEN * Add GOROOT/bin to PATH in modver-action. * Not PATH but GOROOT needs to be set. * Show failure from go env * More test coverage. * Is this a thing? * Will this work? * Iterate. * Iterate. * Iterate. * Iterate. * Iterate. * Iterate. * Iterate. * Iterate. * Update Readme with info about using Modver in GitHub Actions. --- Dockerfile | 9 ++ Readme.md | 72 ++++++++++- action.yml | 13 ++ cmd/modver-action/main.go | 34 +++++ cmd/modver/compare.go | 25 +++- cmd/modver/compare_test.go | 152 +++++++++++++++++++++++ cmd/modver/main.go | 2 +- cmd/modver/options.go | 24 ---- cmd/modver/options_test.go | 46 ------- cmd/modver/tags.go | 6 +- go.mod | 2 +- {cmd/modver => internal}/comment.md.tmpl | 0 internal/github.go | 48 +++++++ internal/github_test.go | 56 +++++++++ {cmd/modver => internal}/pr.go | 5 +- {cmd/modver => internal}/pr_test.go | 2 +- 16 files changed, 413 insertions(+), 83 deletions(-) create mode 100644 Dockerfile create mode 100644 action.yml create mode 100644 cmd/modver-action/main.go create mode 100644 cmd/modver/compare_test.go rename {cmd/modver => internal}/comment.md.tmpl (100%) create mode 100644 internal/github.go create mode 100644 internal/github_test.go rename {cmd/modver => internal}/pr.go (95%) rename {cmd/modver => internal}/pr_test.go (99%) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c1b215c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:latest + +ADD . /app + +WORKDIR /app + +RUN go build ./cmd/modver-action + +ENTRYPOINT ["/app/modver-action"] diff --git a/Readme.md b/Readme.md index c482cbd..8c9f633 100644 --- a/Readme.md +++ b/Readme.md @@ -6,17 +6,24 @@ [![Coverage Status](https://coveralls.io/repos/github/bobg/modver/badge.svg?branch=master)](https://coveralls.io/github/bobg/modver?branch=master) This is modver, -a Go package and command that helps you obey [semantic versioning rules](https://semver.org/) in your Go module. +a tool that helps you obey [semantic versioning rules](https://semver.org/) in your Go module. It can read and compare two different versions of the same module, from two different directories, -or two different Git commits. +or two different Git commits, +or the base and head of a Git pull request. It then reports whether the changes require an increase in the major-version number, the minor-version number, or the patchlevel. ## Installation and usage +Modver can be used from the command line, +or in your Go program, +or with [GitHub Actions](https://github.com/features/actions). + +### Command-line interface + Install the `modver` command like this: ```sh @@ -37,7 +44,68 @@ The arguments `HEAD~1` and `HEAD` specify two Git revisions to compare; in this case, the latest two commits on the current branch. These could also be tags or commit hashes. +### GitHub Action + +You can arrange for Modver to inspect the changes on your pull-request branch +as part of a GitHub Actions-based continuous-integration step. +It will add a comment to the pull request with its findings, +and will update the comment as new commits are pushed to the branch. + +To do this, you’ll need a directory in your GitHub repository named `.github/workflows`, +and a Yaml file containing (at least) the following: + +```yaml +name: Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.19 + + - name: Modver + if: ${{ github.event_name == 'pull_request' }} + uses: bobg/modver@v2.5.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + pull_request_url: https://github.com/${{ github.repository }}/pull/${{ github.event.number }} +``` + +This can be combined with other steps that run unit tests, etc. +You can change `Tests` to whatever name you like, +and should change `main` to the name of your repository’s default branch. +If your pull request is on a GitHub server other than `github.com`, +change the hostname in the `pull_request_url` parameter to match. + +Note the `fetch-depth: 0` parameter for the `Checkout` step. +This causes GitHub Actions to create a clone of your repo with its full history, +as opposed to the default, +which is a shallow clone. +Modver requires enough history to be present in the clone +for it to access the “base” and “head” revisions of your pull-request branch. + +For more information about configuring GitHub Actions, +see [the GitHub Actions documentation](https://docs.github.com/actions). + +### Go library + Modver also has a simple API for use from within Go programs. +Add it to your project with `go get github.com/bobg/modver/v2@latest`. +See [the Go doc page](https://pkg.go.dev/github.com/bobg/modver/v2) for information about how to use it. ## Semantic versioning diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..907d8d1 --- /dev/null +++ b/action.yml @@ -0,0 +1,13 @@ +name: Modver +description: Analyze pull requests for changes in Go code that require updating a module's version number. +author: Bob Glickstein +inputs: + github_token: + description: 'The GitHub token to use for authentication.' + required: true + pull_request_url: + description: 'The full github.com URL of the pull request.' + required: true +runs: + using: 'docker' + image: 'Dockerfile' diff --git a/cmd/modver-action/main.go b/cmd/modver-action/main.go new file mode 100644 index 0000000..7a1d2c1 --- /dev/null +++ b/cmd/modver-action/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "log" + "os" + + "github.com/bobg/modver/v2" + "github.com/bobg/modver/v2/internal" +) + +func main() { + os.Setenv("GOROOT", "/usr/local/go") // Work around some Docker weirdness. + + prURL := os.Getenv("INPUT_PULL_REQUEST_URL") + host, owner, reponame, prnum, err := internal.ParsePR(prURL) + if err != nil { + log.Fatal(err) + } + token := os.Getenv("INPUT_GITHUB_TOKEN") + if token == "" { + log.Fatal("No GitHub token in the environment variable INPUT_GITHUB_TOKEN") + } + ctx := context.Background() + gh, err := internal.NewClient(ctx, host, token) + if err != nil { + log.Fatalf("Creating GitHub client: %s", err) + } + result, err := internal.PR(ctx, gh, owner, reponame, prnum) + if err != nil { + log.Fatalf("Running comparison: %s", err) + } + modver.Pretty(os.Stdout, result) +} diff --git a/cmd/modver/compare.go b/cmd/modver/compare.go index e86c525..d78d719 100644 --- a/cmd/modver/compare.go +++ b/cmd/modver/compare.go @@ -9,19 +9,34 @@ import ( "github.com/pkg/errors" "github.com/bobg/modver/v2" + "github.com/bobg/modver/v2/internal" ) func doCompare(ctx context.Context, opts options) (modver.Result, error) { + return doCompareHelper(ctx, opts, internal.NewClient, internal.PR, modver.CompareGitWith, modver.CompareDirs) +} + +type ( + newClientType = func(ctx context.Context, host, token string) (*github.Client, error) + prType = func(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) + compareGitWithType = func(ctx context.Context, repoURL, olderRev, newerRev string, f func(older, newer string) (modver.Result, error)) (modver.Result, error) + compareDirsType = func(older, newer string) (modver.Result, error) +) + +func doCompareHelper(ctx context.Context, opts options, newClient newClientType, pr prType, compareGitWith compareGitWithType, compareDirs compareDirsType) (modver.Result, error) { if opts.pr != "" { - owner, reponame, prnum, err := parsePR(opts.pr) + host, owner, reponame, prnum, err := internal.ParsePR(opts.pr) if err != nil { return modver.None, errors.Wrap(err, "parsing pull-request URL") } if opts.ghtoken == "" { return modver.None, fmt.Errorf("usage: %s -pr URL [-token TOKEN]", os.Args[0]) } - gh := github.NewTokenClient(ctx, opts.ghtoken) - return doPR(ctx, gh, owner, reponame, prnum) + gh, err := newClient(ctx, host, opts.ghtoken) + if err != nil { + return modver.None, errors.Wrap(err, "creating GitHub client") + } + return pr(ctx, gh, owner, reponame, prnum) } if opts.gitRepo != "" { @@ -34,10 +49,10 @@ func doCompare(ctx context.Context, opts options) (modver.Result, error) { callback = getTags(&opts.v1, &opts.v2, opts.args[0], opts.args[1]) } - return modver.CompareGitWith(ctx, opts.gitRepo, opts.args[0], opts.args[1], callback) + return compareGitWith(ctx, opts.gitRepo, opts.args[0], opts.args[1], callback) } if len(opts.args) != 2 { return nil, fmt.Errorf("usage: %s [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR", os.Args[0]) } - return modver.CompareDirs(opts.args[0], opts.args[1]) + return compareDirs(opts.args[0], opts.args[1]) } diff --git a/cmd/modver/compare_test.go b/cmd/modver/compare_test.go new file mode 100644 index 0000000..a423910 --- /dev/null +++ b/cmd/modver/compare_test.go @@ -0,0 +1,152 @@ +package main + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-github/v50/github" + + "github.com/bobg/modver/v2" +) + +func TestDoCompare(t *testing.T) { + cases := []struct { + opts options + wantErr bool + pr func(*testing.T, *int) prType + compareGitWith func(*testing.T, *int) compareGitWithType + compareDirs func(*testing.T, *int) compareDirsType + }{{ + opts: options{ + pr: "https://github.com/foo/bar/pull/17", + ghtoken: "token", + }, + pr: mockPR("foo", "bar", 17), + }, { + opts: options{ + pr: "https://github.com/foo/bar/baz/pull/17", + ghtoken: "token", + }, + wantErr: true, + }, { + opts: options{ + pr: "https://github.com/foo/bar/pull/17", + }, + wantErr: true, + }, { + opts: options{ + gitRepo: ".git", + args: []string{"older", "newer"}, + }, + compareGitWith: mockCompareGitWith(".git", "older", "newer"), + }, { + opts: options{ + gitRepo: ".git", + args: []string{"older", "newer", "evenmorenewer"}, + }, + wantErr: true, + }, { + opts: options{ + args: []string{"older", "newer"}, + }, + compareDirs: mockCompareDirs("older", "newer"), + }, { + opts: options{ + args: []string{"older"}, + }, + wantErr: true, + }} + + ctx := context.Background() + + for i, tc := range cases { + t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { + var ( + pr prType + compareGitWith compareGitWithType + compareDirs compareDirsType + calls int + ) + if tc.pr != nil { + pr = tc.pr(t, &calls) + } + if tc.compareGitWith != nil { + compareGitWith = tc.compareGitWith(t, &calls) + } + if tc.compareDirs != nil { + compareDirs = tc.compareDirs(t, &calls) + } + + _, err := doCompareHelper(ctx, tc.opts, mockNewClient, pr, compareGitWith, compareDirs) + if err != nil { + if !tc.wantErr { + t.Errorf("got error %s, wanted none", err) + } + return + } + if tc.wantErr { + t.Error("got no error, wanted one") + return + } + if calls != 1 { + t.Errorf("got %d calls, want 1", calls) + } + }) + } +} + +func mockNewClient(ctx context.Context, host, token string) (*github.Client, error) { + return nil, nil +} + +func mockPR(wantOwner, wantRepo string, wantPRNum int) func(*testing.T, *int) prType { + return func(t *testing.T, calls *int) prType { + return func(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) { + *calls++ + if owner != wantOwner { + t.Errorf("got owner %s, want %s", owner, wantOwner) + } + if reponame != wantRepo { + t.Errorf("got repo %s, want %s", reponame, wantRepo) + } + if wantPRNum != prnum { + t.Errorf("got PR number %d, want %d", prnum, wantPRNum) + } + return modver.None, nil + } + } +} + +func mockCompareGitWith(wantGitRepo, wantOlder, wantNewer string) func(*testing.T, *int) compareGitWithType { + return func(t *testing.T, calls *int) compareGitWithType { + return func(ctx context.Context, repoURL, olderRev, newerRev string, f func(older, newer string) (modver.Result, error)) (modver.Result, error) { + *calls++ + if repoURL != wantGitRepo { + t.Errorf("got repo URL %s, want %s", repoURL, wantGitRepo) + } + if olderRev != wantOlder { + t.Errorf("got older rev %s, want %s", olderRev, wantOlder) + } + if newerRev != wantNewer { + t.Errorf("got newer rev %s, want %s", newerRev, wantNewer) + } + return modver.None, nil + } + } +} + +func mockCompareDirs(wantOlder, wantNewer string) func(*testing.T, *int) compareDirsType { + return func(t *testing.T, calls *int) compareDirsType { + return func(older, newer string) (modver.Result, error) { + *calls++ + if older != wantOlder { + t.Errorf("got older dir %s, want %s", older, wantOlder) + } + if newer != wantNewer { + t.Errorf("got newer dir %s, want %s", newer, wantNewer) + } + return modver.None, nil + } + } +} diff --git a/cmd/modver/main.go b/cmd/modver/main.go index e18c357..c1604c9 100644 --- a/cmd/modver/main.go +++ b/cmd/modver/main.go @@ -11,7 +11,7 @@ // // With `-pr URL`, // the URL must be that of a github.com pull request -// (having the form https://github.com/OWNER/REPO/pull/NUMBER). +// (having the form https://HOST/OWNER/REPO/pull/NUMBER). // The environment variable GITHUB_TOKEN must contain a valid GitHub access token, // or else one must be supplied on the command line with -token. // In this mode, diff --git a/cmd/modver/options.go b/cmd/modver/options.go index 30457a3..c69402d 100644 --- a/cmd/modver/options.go +++ b/cmd/modver/options.go @@ -3,9 +3,7 @@ package main import ( "flag" "fmt" - "net/url" "os" - "strconv" "strings" "github.com/pkg/errors" @@ -65,25 +63,3 @@ func parseArgsHelper(args []string) (opts options, err error) { return opts, nil } - -func parsePR(pr string) (owner, reponame string, prnum int, err error) { - u, err := url.Parse(pr) - if err != nil { - err = errors.Wrap(err, "parsing GitHub pull-request URL") - return - } - path := strings.TrimLeft(u.Path, "/") - parts := strings.Split(path, "/") - if len(parts) < 4 { - err = fmt.Errorf("too few path elements in pull-request URL (got %d, want 4)", len(parts)) - return - } - if parts[2] != "pull" { - err = fmt.Errorf("pull-request URL not in expected format") - return - } - owner, reponame = parts[0], parts[1] - prnum, err = strconv.Atoi(parts[3]) - err = errors.Wrap(err, "parsing number from GitHub pull-request URL") - return -} diff --git a/cmd/modver/options_test.go b/cmd/modver/options_test.go index 5c25a50..b222cf9 100644 --- a/cmd/modver/options_test.go +++ b/cmd/modver/options_test.go @@ -75,49 +75,3 @@ func TestParseArgs(t *testing.T) { }) } } - -func TestParsePR(t *testing.T) { - cases := []struct { - inp string - wantErr bool - owner, reponame string - prnum int - }{{ - wantErr: true, - }, { - inp: "https://x/y", - wantErr: true, - }, { - inp: "https://github.com/bobg/modver/bleah/17", - wantErr: true, - }, { - inp: "https://github.com/bobg/modver/pull/17", - owner: "bobg", - reponame: "modver", - prnum: 17, - }} - - for i, tc := range cases { - t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { - owner, reponame, prnum, err := parsePR(tc.inp) - if err != nil { - if !tc.wantErr { - t.Errorf("got error %v, wanted no error", err) - } - return - } - if tc.wantErr { - t.Fatal("got no error but wanted one") - } - if owner != tc.owner { - t.Errorf("got owner %s, want %s", owner, tc.owner) - } - if reponame != tc.reponame { - t.Errorf("got repo %s, want %s", reponame, tc.reponame) - } - if prnum != tc.prnum { - t.Errorf("got PR number %d, want %d", prnum, tc.prnum) - } - }) - } -} diff --git a/cmd/modver/tags.go b/cmd/modver/tags.go index 2ce613f..7190b6b 100644 --- a/cmd/modver/tags.go +++ b/cmd/modver/tags.go @@ -17,6 +17,10 @@ import ( ) func getTags(v1, v2 *string, olderRev, newerRev string) func(older, newer string) (modver.Result, error) { + return getTagsHelper(v1, v2, olderRev, newerRev, modver.CompareDirs) +} + +func getTagsHelper(v1, v2 *string, olderRev, newerRev string, compareDirs compareDirsType) func(older, newer string) (modver.Result, error) { return func(older, newer string) (modver.Result, error) { tag, err := getTag(older, olderRev) if err != nil { @@ -30,7 +34,7 @@ func getTags(v1, v2 *string, olderRev, newerRev string) func(older, newer string } *v2 = tag - return modver.CompareDirs(older, newer) + return compareDirs(older, newer) } } diff --git a/go.mod b/go.mod index 5b91a3e..8660963 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/go-github/v50 v50.2.0 github.com/pkg/errors v0.9.1 golang.org/x/mod v0.9.0 + golang.org/x/oauth2 v0.6.0 golang.org/x/tools v0.7.0 ) @@ -29,7 +30,6 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/net v0.8.0 // indirect - golang.org/x/oauth2 v0.6.0 // indirect golang.org/x/sys v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect diff --git a/cmd/modver/comment.md.tmpl b/internal/comment.md.tmpl similarity index 100% rename from cmd/modver/comment.md.tmpl rename to internal/comment.md.tmpl diff --git a/internal/github.go b/internal/github.go new file mode 100644 index 0000000..f50625c --- /dev/null +++ b/internal/github.go @@ -0,0 +1,48 @@ +package internal + +import ( + "context" + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/google/go-github/v50/github" + "github.com/pkg/errors" + "golang.org/x/oauth2" +) + +// ParsePR parses a GitHub pull-request URL, +// which should have the form http(s)://HOST/OWNER/REPO/pull/NUMBER. +func ParsePR(pr string) (host, owner, reponame string, prnum int, err error) { + u, err := url.Parse(pr) + if err != nil { + err = errors.Wrap(err, "parsing GitHub pull-request URL") + return + } + path := strings.TrimLeft(u.Path, "/") + parts := strings.Split(path, "/") + if len(parts) < 4 { + err = fmt.Errorf("too few path elements in pull-request URL (got %d, want 4)", len(parts)) + return + } + if parts[2] != "pull" { + err = fmt.Errorf("pull-request URL not in expected format") + return + } + host = u.Host + owner, reponame = parts[0], parts[1] + prnum, err = strconv.Atoi(parts[3]) + err = errors.Wrap(err, "parsing number from GitHub pull-request URL") + return +} + +// NewClient creates a new GitHub client talking to the given host and authenticated with the given token. +func NewClient(ctx context.Context, host, token string) (*github.Client, error) { + if strings.ToLower(host) == "github.com" { + return github.NewTokenClient(ctx, token), nil + } + oClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})) + u := "https://" + host + return github.NewEnterpriseClient(u, u, oClient) +} diff --git a/internal/github_test.go b/internal/github_test.go new file mode 100644 index 0000000..d320602 --- /dev/null +++ b/internal/github_test.go @@ -0,0 +1,56 @@ +package internal + +import ( + "fmt" + "testing" +) + +func TestParsePR(t *testing.T) { + cases := []struct { + inp string + wantErr bool + host, owner, reponame string + prnum int + }{{ + wantErr: true, + }, { + inp: "https://x/y", + wantErr: true, + }, { + inp: "https://github.com/bobg/modver/bleah/17", + wantErr: true, + }, { + inp: "https://github.com/bobg/modver/pull/17", + host: "github.com", + owner: "bobg", + reponame: "modver", + prnum: 17, + }} + + for i, tc := range cases { + t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) { + host, owner, reponame, prnum, err := ParsePR(tc.inp) + if err != nil { + if !tc.wantErr { + t.Errorf("got error %v, wanted no error", err) + } + return + } + if tc.wantErr { + t.Fatal("got no error but wanted one") + } + if host != tc.host { + t.Errorf("got host %s, want %s", host, tc.host) + } + if owner != tc.owner { + t.Errorf("got owner %s, want %s", owner, tc.owner) + } + if reponame != tc.reponame { + t.Errorf("got repo %s, want %s", reponame, tc.reponame) + } + if prnum != tc.prnum { + t.Errorf("got PR number %d, want %d", prnum, tc.prnum) + } + }) + } +} diff --git a/cmd/modver/pr.go b/internal/pr.go similarity index 95% rename from cmd/modver/pr.go rename to internal/pr.go index 0788ceb..6de3569 100644 --- a/cmd/modver/pr.go +++ b/internal/pr.go @@ -1,4 +1,4 @@ -package main +package internal import ( "bufio" @@ -16,7 +16,8 @@ import ( "github.com/bobg/modver/v2" ) -func doPR(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) { +// PR performs modver analysis on a GitHub pull request. +func PR(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) { return prHelper(ctx, gh.Repositories, gh.PullRequests, gh.Issues, modver.CompareGit, owner, reponame, prnum) } diff --git a/cmd/modver/pr_test.go b/internal/pr_test.go similarity index 99% rename from cmd/modver/pr_test.go rename to internal/pr_test.go index ffa44b4..b1b34da 100644 --- a/cmd/modver/pr_test.go +++ b/internal/pr_test.go @@ -1,4 +1,4 @@ -package main +package internal import ( "context"