github.com/google/trillian-examples@v0.0.0-20240520080811-0d40d35cef0e/internal/github/github.go (about) 1 // Copyright 2021 Google LLC. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package github contains libraries for using github repositories that 16 // make serverless operations easy to follow. 17 package github 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "net/http" 25 "os" 26 "strings" 27 28 "github.com/golang/glog" 29 gh_api "github.com/google/go-github/v39/github" 30 "golang.org/x/oauth2" 31 ) 32 33 // RepoID identifies a github project, including owner & repo. 34 type RepoID struct { 35 Owner string 36 RepoName string 37 } 38 39 // String returns "<owner>/<repo>". 40 func (r RepoID) String() string { 41 return fmt.Sprintf("%s/%s", r.Owner, r.RepoName) 42 } 43 44 // NewRepoID creates a new RepoID struct from an owner/repo fragment. 45 func NewRepoID(or string) (RepoID, error) { 46 s := strings.Split(or, "/") 47 if l, want := len(s), 2; l != want { 48 return RepoID{}, fmt.Errorf("can't split owner/repo %q, found %d parts want %d", or, l, want) 49 } 50 return RepoID{ 51 Owner: s[0], 52 RepoName: s[1], 53 }, nil 54 } 55 56 // NewRepository creates a wrapper around a git repository which has a fork owned by 57 // the user, and an upstream repository configured that PRs can be proposed against. 58 func NewRepository(ctx context.Context, c *http.Client, upstream RepoID, upstreamBranch string, fork RepoID, ghUser, ghEmail, ghToken string) (Repository, error) { 59 ctx = context.WithValue(ctx, oauth2.HTTPClient, c) 60 repo := Repository{ 61 upstream: upstream, 62 upstreamBranch: upstreamBranch, 63 fork: fork, 64 user: ghUser, 65 email: ghEmail, 66 } 67 var err error 68 repo.ghCli, err = authWithGithub(ctx, ghToken) 69 return repo, err 70 } 71 72 // authWithGithub returns a github client struct which uses the provided OAuth token. 73 // These tokens can be created using the github -> Settings -> Developers -> Personal Authentication Tokens page. 74 func authWithGithub(ctx context.Context, ghToken string) (*gh_api.Client, error) { 75 ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: ghToken}) 76 tc := oauth2.NewClient(ctx, ts) 77 return gh_api.NewClient(tc), nil 78 } 79 80 // Repository represents a github repository with a working area and an upstream. 81 // The upstream repo must allow PRs. The working fork is usually owned by the user; 82 // commits are pushed to branches here and proposed against the upstream repository. 83 type Repository struct { 84 // upstream is the original repository, and fork is the user's clone of it. 85 // Changes will be made and pushed to fork, and PRs proposed to upstream. 86 upstream, fork RepoID 87 // upstreamBranch is the name of the upstreamBranch/main branch that PRs will be proposed against. 88 upstreamBranch string 89 user, email string 90 ghCli *gh_api.Client 91 } 92 93 func (r Repository) String() string { 94 return fmt.Sprintf("%s → %s", r.fork, r.upstream) 95 } 96 97 // CreateOrUpdateBranch attempts to create a new branch on the fork repo if it doesn't already exist, or 98 // rebase it onto HEAD if it does. 99 func (r *Repository) CreateOrUpdateBranch(ctx context.Context, branchName string) error { 100 baseRef, _, err := r.ghCli.Git.GetRef(ctx, r.upstream.Owner, r.upstream.RepoName, "refs/heads/"+r.upstreamBranch) 101 if err != nil { 102 return fmt.Errorf("failed to get %s ref: %v", r.upstreamBranch, err) 103 } 104 branch := "refs/heads/" + branchName 105 newRef := &gh_api.Reference{Ref: gh_api.String(branch), Object: baseRef.Object} 106 107 if _, rsp, err := r.ghCli.Git.GetRef(ctx, r.fork.Owner, r.fork.RepoName, branch); err != nil { 108 if rsp == nil || rsp.StatusCode != 404 { 109 return fmt.Errorf("failed to check for existing branch %q: %v", branchName, err) 110 } 111 // Branch doesn't exist, so we'll create it: 112 _, _, err = r.ghCli.Git.CreateRef(ctx, r.fork.Owner, r.fork.RepoName, newRef) 113 return err 114 } 115 // The branch already exists, so we'll update it: 116 _, _, err = r.ghCli.Git.UpdateRef(ctx, r.fork.Owner, r.fork.RepoName, newRef, true) 117 return err 118 } 119 120 // ReadFile returns the contents of the specified path from the configured upstream repo. 121 func (r *Repository) ReadFile(ctx context.Context, path string) ([]byte, error) { 122 f, _, resp, err := r.ghCli.Repositories.GetContents(ctx, r.upstream.Owner, r.upstream.RepoName, path, nil) 123 if err != nil { 124 if resp != nil && resp.StatusCode == 404 { 125 return nil, os.ErrNotExist 126 } 127 return nil, fmt.Errorf("failed to GetContents(%q): %v", path, err) 128 } 129 s, err := f.GetContent() 130 return []byte(s), err 131 } 132 133 // CommitFile creates a commit on a repo's worktree which overwrites the specified file path 134 // with the provided bytes. 135 func (r *Repository) CommitFile(ctx context.Context, path string, raw []byte, branch, commitMsg string) error { 136 opts := &gh_api.RepositoryContentFileOptions{ 137 Message: gh_api.String(commitMsg), 138 Content: raw, 139 Branch: gh_api.String(branch), 140 } 141 cRsp, _, err := r.ghCli.Repositories.CreateFile(ctx, r.fork.Owner, r.fork.RepoName, path, opts) 142 if err != nil { 143 return fmt.Errorf("failed to CreateFile(%q): %v", path, err) 144 } 145 glog.V(2).Infof("Commit %s updated %q on %s", cRsp.Commit, path, branch) 146 return nil 147 } 148 149 // CreatePR creates a pull request. 150 // Based on: https://godoc.org/github.com/google/go-github/github#example-PullRequestsService-Create 151 func (r *Repository) CreatePR(ctx context.Context, title, commitBranch string) error { 152 if title == "" { 153 return errors.New("missing `title`, won't create PR") 154 } 155 156 newPR := &gh_api.NewPullRequest{ 157 Title: gh_api.String(title), 158 Head: gh_api.String(r.fork.Owner + ":" + commitBranch), 159 Base: gh_api.String(r.upstreamBranch), 160 MaintainerCanModify: gh_api.Bool(true), 161 } 162 163 prJSON, err := json.Marshal(newPR) 164 if err != nil { 165 return fmt.Errorf("failed to marshal JSON for new PR request: %v", err) 166 } 167 glog.V(2).Infof("Creating PR:\n%s", prJSON) 168 169 pr, _, err := r.ghCli.PullRequests.Create(ctx, r.upstream.Owner, r.upstream.RepoName, newPR) 170 if err != nil { 171 return err 172 } 173 174 glog.V(1).Infof("PR created: %q %s", title, pr.GetHTMLURL()) 175 return nil 176 }