mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
324 lines
10 KiB
Go
324 lines
10 KiB
Go
/*
|
|
Copyright 2022 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package github
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/gravitational/trace"
|
|
|
|
go_github "github.com/google/go-github/v41/github"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
type Client struct {
|
|
Client *go_github.Client
|
|
c Config
|
|
}
|
|
|
|
type Config struct {
|
|
// Token is the Github auth token.
|
|
Token string
|
|
|
|
// Repository is the name of the repository to create
|
|
// the backport pull requests in.
|
|
Repository string
|
|
|
|
// Organization is the organization/owner name of the
|
|
// repository.
|
|
Organization string
|
|
}
|
|
|
|
// New returns a new GitHub client.
|
|
func New(ctx context.Context, c *Config) (*Client, error) {
|
|
if err := c.Check(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
ts := oauth2.StaticTokenSource(
|
|
&oauth2.Token{AccessToken: c.Token},
|
|
)
|
|
return &Client{
|
|
Client: go_github.NewClient(oauth2.NewClient(ctx, ts)),
|
|
c: *c,
|
|
}, nil
|
|
}
|
|
|
|
// Check validates config.
|
|
func (c *Config) Check() error {
|
|
if c.Token == "" {
|
|
return trace.BadParameter("missing parameter Token")
|
|
}
|
|
if c.Organization == "" {
|
|
return trace.BadParameter("missing parameter Organization")
|
|
}
|
|
if c.Repository == "" {
|
|
return trace.BadParameter("missing parameter Repository")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Backport backports changes from backportBranchName to a new branch based
|
|
// off baseBranchName.
|
|
//
|
|
// A new branch is created with the name in the format of
|
|
// auto-backport/[pull number]-to-[base branch], and
|
|
// cherry-picks commits onto the new branch.
|
|
func (c *Client) Backport(ctx context.Context, baseBranchName string, pullNumber int) (string, error) {
|
|
newBranchName := fmt.Sprintf("auto-backport/%v-to-%s", pullNumber, baseBranchName)
|
|
// Create a new branch off of the target branch.
|
|
err := c.createBranchFrom(ctx, baseBranchName, newBranchName)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
fmt.Printf("Created a new branch: %s.\n", newBranchName)
|
|
|
|
commits, err := c.getPullRequestCommits(ctx, pullNumber)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
fmt.Printf("Found %v commits. \n", len(commits))
|
|
|
|
// Cherry pick commits.
|
|
err = c.cherryPickCommitsOnBranch(ctx, newBranchName, commits)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
return newBranchName, nil
|
|
}
|
|
|
|
// CreatePullRequest creates a pull request.
|
|
func (c *Client) CreatePullRequest(ctx context.Context, baseBranch string, headBranch string, originalPrNumber int) error {
|
|
titleAndBody := fmt.Sprintf("Backport #%v to %s", originalPrNumber, baseBranch)
|
|
newPR := &go_github.NewPullRequest{
|
|
Title: go_github.String(titleAndBody),
|
|
Head: go_github.String(headBranch),
|
|
Base: go_github.String(baseBranch),
|
|
Body: go_github.String(titleAndBody),
|
|
}
|
|
_, _, err := c.Client.PullRequests.Create(ctx, c.c.Organization, c.c.Repository, newPR)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getPullRequestCommits gets the commits for a pull request.
|
|
func (c *Client) getPullRequestCommits(ctx context.Context, number int) (commits []string, err error) {
|
|
var commitSHAs []string
|
|
opts := go_github.ListOptions{
|
|
Page: 0,
|
|
PerPage: perPage,
|
|
}
|
|
for {
|
|
currCommits, resp, err := c.Client.PullRequests.ListCommits(ctx,
|
|
c.c.Organization,
|
|
c.c.Repository,
|
|
number, &go_github.ListOptions{})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
for _, commit := range currCommits {
|
|
commitSHAs = append(commitSHAs, commit.GetSHA())
|
|
}
|
|
|
|
if resp.NextPage == 0 {
|
|
break
|
|
}
|
|
|
|
opts.Page = resp.NextPage
|
|
}
|
|
return commitSHAs, nil
|
|
}
|
|
|
|
// cherryPickCommitsOnBranch cherry picks a list of commits onto the given branch.
|
|
func (c *Client) cherryPickCommitsOnBranch(ctx context.Context, branchName string, commits []string) error {
|
|
branch, _, err := c.Client.Repositories.GetBranch(ctx, c.c.Organization, c.c.Repository, branchName, true)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// Get the branch's HEAD.
|
|
headCommit, _, err := c.Client.Git.GetCommit(ctx,
|
|
c.c.Organization,
|
|
c.c.Repository,
|
|
branch.GetCommit().GetSHA())
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
for _, commit := range commits {
|
|
cherryCommit, _, err := c.Client.Git.GetCommit(ctx, c.c.Organization, c.c.Repository, commit)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// Skip merge commits. The commit to cherry-pick MUST have only 1 parent.
|
|
if len(cherryCommit.Parents) != 1 {
|
|
fmt.Printf("Skipping merge commit: %s\n", cherryCommit.GetMessage())
|
|
continue
|
|
}
|
|
fmt.Printf("%s %s\n", cherryCommit.GetSHA(), cherryCommit.GetMessage())
|
|
tree, sha, err := c.cherryPickCommit(ctx, branchName, cherryCommit, headCommit)
|
|
if err != nil {
|
|
fmt.Printf("failed to cherry pick commit: %s %s\n", cherryCommit.GetSHA(), cherryCommit.GetMessage())
|
|
return trace.Errorf("please manually delete branch %s: %v", branchName, err)
|
|
}
|
|
headCommit.SHA = &sha
|
|
headCommit.Tree = tree
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cherryPickCommit cherry picks a single commit on a branch.
|
|
func (c *Client) cherryPickCommit(ctx context.Context, branchName string, cherryCommit, headBranchCommit *go_github.Commit) (*go_github.Tree, string, error) {
|
|
cherryParent := cherryCommit.Parents[0]
|
|
|
|
// Temporarily set the parent of the branch HEAD to the parent of the commit
|
|
// to cherry-pick so they are siblings.
|
|
err := c.createSiblingCommit(ctx, branchName, headBranchCommit, cherryParent)
|
|
if err != nil {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
|
|
// When git performs the merge, it detects that the parent of the branch commit that is
|
|
// being merged onto matches the parent of the cherry pick commit, and merges a tree of size 1.
|
|
// The merge commit will contain the delta between the file tree in target branch and the
|
|
// commit to cherry-pick.
|
|
merge, err := c.merge(ctx, branchName, cherryCommit.GetSHA())
|
|
if err != nil {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
mergeTree := merge.GetTree()
|
|
|
|
updatedCommit, _, err := c.Client.Git.GetCommit(ctx,
|
|
c.c.Organization,
|
|
c.c.Repository,
|
|
headBranchCommit.GetSHA())
|
|
if err != nil {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
// Create the actual cherry-pick commit on the target branch containing the merge commit tree.
|
|
commit, _, err := c.Client.Git.CreateCommit(ctx, c.c.Organization, c.c.Repository, &go_github.Commit{
|
|
Message: cherryCommit.Message,
|
|
Tree: mergeTree,
|
|
Parents: []*go_github.Commit{
|
|
updatedCommit,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
|
|
// Overwrite the merge commit and its parent on the branch by the newly created commit.
|
|
// The result will be equivalent to what would have happened with a fast-forward merge.
|
|
sha := commit.GetSHA()
|
|
refName := fmt.Sprintf("%s%s", branchRefPrefix, branchName)
|
|
_, _, err = c.Client.Git.UpdateRef(ctx, c.c.Organization, c.c.Repository, &go_github.Reference{
|
|
Ref: go_github.String(refName),
|
|
Object: &go_github.GitObject{
|
|
SHA: go_github.String(sha),
|
|
},
|
|
}, true)
|
|
if err != nil {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
return mergeTree, sha, nil
|
|
}
|
|
|
|
// createSiblingCommit creates a commit with the passed in commit's tree and parent
|
|
// and updates the passed in branch to point at that commit.
|
|
func (c *Client) createSiblingCommit(ctx context.Context, branchName string, branchHeadCommit *go_github.Commit, cherryParent *go_github.Commit) error {
|
|
tree := branchHeadCommit.GetTree()
|
|
|
|
// This sibling commit is temporary commit to later merge for a tree size of 1.
|
|
// The commit message does not matter as this commit will not be in the final
|
|
// branch.
|
|
commit, _, err := c.Client.Git.CreateCommit(ctx, c.c.Organization, c.c.Repository, &go_github.Commit{
|
|
Message: go_github.String("field-not-required"),
|
|
Tree: tree,
|
|
Parents: []*go_github.Commit{
|
|
cherryParent,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
sha := commit.GetSHA()
|
|
|
|
refName := fmt.Sprintf("%s%s", branchRefPrefix, branchName)
|
|
_, _, err = c.Client.Git.UpdateRef(ctx, c.c.Organization, c.c.Repository, &go_github.Reference{
|
|
Ref: go_github.String(refName),
|
|
Object: &go_github.GitObject{
|
|
SHA: go_github.String(sha),
|
|
},
|
|
}, true)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createBranchFrom creates a new branch pointing at the same commit as the supplied branch.
|
|
func (c *Client) createBranchFrom(ctx context.Context, branchFromName string, newBranchName string) error {
|
|
baseBranch, _, err := c.Client.Repositories.GetBranch(ctx, c.c.Organization, c.c.Repository, branchFromName, true)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
newRefBranchName := fmt.Sprintf("%s%s", branchRefPrefix, newBranchName)
|
|
baseBranchSHA := baseBranch.GetCommit().GetSHA()
|
|
|
|
ref := &go_github.Reference{
|
|
Ref: go_github.String(newRefBranchName),
|
|
Object: &go_github.GitObject{
|
|
SHA: go_github.String(baseBranchSHA), /* SHA to branch from */
|
|
},
|
|
}
|
|
_, _, err = c.Client.Git.CreateRef(ctx, c.c.Organization, c.c.Repository, ref)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// merge merges a branch at `headCommitSHA` into branch `base`
|
|
func (c *Client) merge(ctx context.Context, base string, headCommitSHA string) (*go_github.Commit, error) {
|
|
merge, _, err := c.Client.Repositories.Merge(ctx, c.c.Organization, c.c.Repository, &go_github.RepositoryMergeRequest{
|
|
Base: go_github.String(base),
|
|
Head: go_github.String(headCommitSHA),
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Errorf("err: %v. failed to merge %s into %s", err, headCommitSHA, base)
|
|
}
|
|
mergeCommit, _, err := c.Client.Git.GetCommit(ctx,
|
|
c.c.Organization,
|
|
c.c.Repository,
|
|
merge.GetSHA())
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return mergeCommit, nil
|
|
}
|
|
|
|
const (
|
|
// perPage is the number of items per page to request.
|
|
perPage = 100
|
|
|
|
// branchRefPrefix is the prefix for a reference that is
|
|
// pointing to a branch.
|
|
branchRefPrefix = "refs/heads/"
|
|
)
|