From ba9d4177f9129e33cf2b40d29d5d2557a1ccee24 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 15 Aug 2022 04:00:47 +0900 Subject: [PATCH] create rc pull request --- git.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 4 +++ go.sum | 10 ++++++ rcpr.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 git.go diff --git a/git.go b/git.go new file mode 100644 index 0000000..ba7901c --- /dev/null +++ b/git.go @@ -0,0 +1,91 @@ +package rcpr + +import ( + "bytes" + "errors" + "fmt" + "io" + "os/exec" + "regexp" + "strings" +) + +func git(args ...string) (string, string, error) { + var ( + outBuf bytes.Buffer + errBuf bytes.Buffer + ) + cmd := exec.Command("git", args...) + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + err := cmd.Run() + return strings.TrimSpace(outBuf.String()), strings.TrimSpace(errBuf.String()), err +} + +type cmd struct { + outStream, errStream io.Writer + dir string + err error +} + +func (c *cmd) git(args ...string) (string, string) { + return c.run("git", args...) +} + +func (c *cmd) run(prog string, args ...string) (string, string) { + if c.err != nil { + return "", "" + } + var ( + outBuf bytes.Buffer + errBuf bytes.Buffer + ) + cmd := exec.Command(prog, args...) + cmd.Stdout = io.MultiWriter(&outBuf, c.outStream) + cmd.Stderr = io.MultiWriter(&errBuf, c.errStream) + if c.dir != "" { + cmd.Dir = c.dir + } + c.err = cmd.Run() + return outBuf.String(), errBuf.String() +} + +var headBranchReg = regexp.MustCompile(`(?m)^\s*HEAD branch: (.*)$`) + +func defaultBranch(remote string) (string, error) { + if remote == "" { + var err error + remote, err = detectRemote() + if err != nil { + return "", err + } + } + // `git symbolic-ref refs/remotes/origin/HEAD` sometimes doesn't work + // So use `git remote show origin` for detecting default branch + show, _, err := git("remote", "show", remote) + if err != nil { + return "", fmt.Errorf("failed to detect defaut branch: %w", err) + } + m := headBranchReg.FindStringSubmatch(show) + if len(m) < 2 { + return "", fmt.Errorf("failed to detect default branch from remote: %s", remote) + } + return m[1], nil +} + +func detectRemote() (string, error) { + remotesStr, _, err := git("remote") + if err != nil { + return "", fmt.Errorf("failed to detect remote: %s", err) + } + remotes := strings.Fields(remotesStr) + if len(remotes) == 1 { + return remotes[0], nil + } + for _, r := range remotes { + if r == "origin" { + return r, nil + } + } + return "", errors.New("failed to detect remote") +} diff --git a/go.mod b/go.mod index 34b3899..cd8a0f7 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/Songmu/rcpr go 1.19 require ( + github.com/Masterminds/semver/v3 v3.1.1 github.com/Songmu/gitconfig v0.1.0 + github.com/Songmu/gitsemvers v0.0.2 github.com/google/go-github/v45 v45.2.0 golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7 ) @@ -13,9 +15,11 @@ require ( github.com/goccy/go-yaml v1.8.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/jessevdk/go-flags v1.5.0 // indirect github.com/mattn/go-colorable v0.1.7 // indirect github.com/mattn/go-isatty v0.0.12 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/mod v0.5.1 // indirect golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect diff --git a/go.sum b/go.sum index 4f3d38d..cd69e35 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,10 @@ +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Songmu/gitconfig v0.1.0 h1:JsaQ6rh3Lnig0Xvo4t4nj7Xu6pXrMfyCOPJ6iRriuKY= github.com/Songmu/gitconfig v0.1.0/go.mod h1:kVksEBYKkMQuZ5BumlnW7KBDBreiySMbf50dseHYOpQ= +github.com/Songmu/gitmock v0.0.2 h1:KF5GTll60LxGskZbt58QDd29y/GYLgdxqvkvnSU6RlY= +github.com/Songmu/gitsemvers v0.0.2 h1:Qr76LMQCA/in0H8ufK69/cjd5nUkTZeY9IUiP1ZO/1s= +github.com/Songmu/gitsemvers v0.0.2/go.mod h1:WdKXiC8zjNK1N7CoZ9cM9vrw/Bg6/W4AwGrfTkkUPdM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= @@ -21,6 +26,8 @@ github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FC github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= 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/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -37,6 +44,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -50,6 +59,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/rcpr.go b/rcpr.go index 1e33430..8da55a6 100644 --- a/rcpr.go +++ b/rcpr.go @@ -6,10 +6,18 @@ import ( "fmt" "io" "log" + "regexp" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/Songmu/gitsemvers" + "github.com/google/go-github/v45/github" ) const cmdName = "rcpr" +var remoteReg = regexp.MustCompile(`origin\s.*?github\.com[:/]([-a-zA-Z0-9]+)/(\S+)`) + // Run the rcpr func Run(ctx context.Context, argv []string, outStream, errStream io.Writer) error { log.SetOutput(errStream) @@ -23,7 +31,99 @@ func Run(ctx context.Context, argv []string, outStream, errStream io.Writer) err if *ver { return printVersion(outStream) } - return nil + + vers := (&gitsemvers.Semvers{}).VersionStrings() + currVer := "v0.0.0" + if len(vers) > 0 { + currVer = vers[0] + } + defaultBr, _ := defaultBranch("") // TODO: make configable + if defaultBr == "" { + defaultBr = "main" + } + branch, _, err := git("symbolic-ref", "--short", "HEAD") + if err != nil { + return fmt.Errorf("faild to release when git symbolic-ref: %w", err) + } + if branch != defaultBr { + return fmt.Errorf("you are not on releasing branch %q, current branch is %q", + defaultBr, branch) + } + + c := &cmd{outStream: outStream, errStream: errStream, dir: "."} + rcBranch := fmt.Sprintf("rc-%s", currVer) + c.git("branch", "-D", rcBranch) + c.git("checkout", "-b", rcBranch) + + // XXX do some releng related changes before commit + c.git("commit", "--allow-empty", "-am", "release") + + c.git("push", "--force", "origin", rcBranch) + if c.err != nil { + return c.err + } + remote, _, err := git("remote", "-v") + if err != nil { + return err + } + m := remoteReg.FindStringSubmatch(remote) + if len(m) < 3 { + return fmt.Errorf("failed to detect remote") + } + owner := m[1] + repo := m[2] + repo = strings.TrimSuffix(repo, ".git") // XXX + + cli, err := client(ctx, "", "") + if err != nil { + return err + } + + v, err := semver.NewVersion(currVer) + if err != nil { + return err + } + nextVer := "v" + v.IncPatch().String() // XXX proper next version detection + + previousTag := &currVer + if *previousTag == "v0.0.0" { + previousTag = nil + } + releases, _, err := cli.Repositories.GenerateReleaseNotes( + ctx, owner, repo, &github.GenerateNotesOptions{ + TagName: nextVer, + PreviousTagName: previousTag, + TargetCommitish: &defaultBr, + }) + if err != nil { + return err + } + + pulls, _, err := cli.PullRequests.List(ctx, owner, repo, &github.PullRequestListOptions{ + Head: fmt.Sprintf("%s:%s", owner, rcBranch), + Base: defaultBr, + }) + if err != nil { + return err + } + + pstr := func(str string) *string { + return &str + } + if len(pulls) == 0 { + _, _, err := cli.PullRequests.Create(ctx, owner, repo, &github.NewPullRequest{ + Title: pstr(fmt.Sprintf("release %s", nextVer)), + Body: pstr(releases.Body), + Base: &defaultBr, + Head: pstr(fmt.Sprintf("%s:%s", owner, rcBranch)), + }) + return err + } + _, _, err = cli.PullRequests.Edit(ctx, owner, repo, *pulls[0].Number, &github.PullRequest{ + Title: pstr(fmt.Sprintf("release %s", nextVer)), + Body: pstr(releases.Body), + }) + return err } func printVersion(out io.Writer) error {