go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/git/client.go (about) 1 // Copyright 2018 The LUCI Authors. 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 git provides high level API for Git/Gerrit data. 16 // 17 // Features: 18 // 19 // It enforces read ACLs on Git/Gerrit hosts/projects configured in 20 // settings.cfg in source_acls blocks. 21 // Transparently caches results respecting ACLs above. 22 // That's why no caching of returned data should be done by callers. 23 // 24 // Limitations: 25 // 26 // currently, only works with *.googlesource.com hosted Git/Gerrit 27 // repositories, but could be extended to work with other providers. 28 package git 29 30 import ( 31 "context" 32 "net/http" 33 34 "go.chromium.org/luci/common/api/gerrit" 35 "go.chromium.org/luci/common/api/gitiles" 36 "go.chromium.org/luci/common/errors" 37 gerritpb "go.chromium.org/luci/common/proto/gerrit" 38 gitpb "go.chromium.org/luci/common/proto/git" 39 gitilespb "go.chromium.org/luci/common/proto/gitiles" 40 "go.chromium.org/luci/milo/internal/git/gitacls" 41 "go.chromium.org/luci/server/auth" 42 ) 43 44 // Client provides high level API for Git/Gerrit data. 45 // 46 // Methods may return grpc errors returned by the underlying Gitiles service. 47 // These errors will be annotated with Milo's error tags whenever reasonable. 48 type Client interface { 49 50 // Log returns ancestors commits of the given repository host 51 // (e.g. "chromium.googlesource.com"), project (e.g. "chromium/src") 52 // and descendant committish (e.g. "refs/heads/main" or commit hash). 53 // 54 // Limit specifies the maximum number of commits to return. 55 // If limit<=0, 50 is used. 56 // Setting a lower value increases cache hit probability. 57 // 58 // May return gRPC errors returned by the underlying Gitiles service. 59 Log(c context.Context, host, project, commitish string, inputOptions *LogOptions) ([]*gitpb.Commit, error) 60 61 // CombinedLogs returns latest commits reachable from several refs. 62 // 63 // Returns a slice of up to a limit (defaults to 50 when <= 0) commits that: 64 // * for each source ref, subsequence of returned slice formed by 65 // its corresponding commits will be in the same order, 66 // * across refs, commits will be ordered by timestamp, and 67 // * identical commits from multiple commits will be deduped. 68 // 69 // For example, for two refs with commits (C_5 means C was committed at time 70 // 5) and limit 5, the function will first resolve each ref to sequence of 71 // commits: 72 // 73 // ref1: A_1 -> B_5 -> C_9 74 // ref2: X_2 -> Y_7 -> Z_4 (note timestamp inversion) 75 // 76 // and then return combined list of [C_9, B_5, Z_4, Y_7, X_2]. 77 // 78 // refs must be a list of ref specs as described in the proto config (see 79 // doc for refs field in the Console message in api/config/project.proto) 80 // 81 // excludeRef can be set to non-empty value to exclude commits from a specific 82 // ref, e.g. this is useful when requesting commits from branches that branch 83 // off a single main branch, commits from which should not be returned even 84 // though they are present in the history of each requested branch 85 CombinedLogs(c context.Context, host, project, excludeRef string, 86 refs []string, limit int) (commits []*gitpb.Commit, err error) 87 88 // CLEmail fetches the CL owner email. 89 // 90 // Returns empty string if either: 91 // CL doesn't exist, or 92 // current user has no access to CL's project. 93 // 94 // May return gRPC errors returned by the underlying Gerrit service. 95 CLEmail(c context.Context, host string, changeNumber int64) (string, error) 96 } 97 98 // UseACLs returns context with production implementation installed. 99 func UseACLs(c context.Context, acls *gitacls.ACLs) context.Context { 100 return Use(c, NewClient(acls)) 101 } 102 103 // NewClient returns a new production Client. 104 func NewClient(acls *gitacls.ACLs) Client { 105 return &implementation{acls: acls} 106 } 107 108 // Use returns context with provided Client implementation. 109 func Use(c context.Context, s Client) context.Context { 110 return context.WithValue(c, &contextKey, s) 111 } 112 113 // Get returns Client set in supplied context. 114 // 115 // panics if not set. 116 func Get(c context.Context) Client { 117 s, ok := c.Value(&contextKey).(Client) 118 if !ok { 119 panic(errors.New("git.Client not installed in context")) 120 } 121 return s 122 } 123 124 // private implementation 125 126 var contextKey = "client factory key" 127 128 // implementation implements Client. 129 type implementation struct { 130 acls *gitacls.ACLs 131 // If a mockGerrit or mockGitiles is provided, then the mocks will be used. 132 // Otherwise, the production client will be used. 133 mockGitiles gitilespb.GitilesClient 134 mockGerrit gerritpb.GerritClient 135 } 136 137 var _ Client = (*implementation)(nil) 138 139 // transport returns an authenticated RoundTripper for Gerrit or Gitiles RPCs. 140 func (p *implementation) transport(c context.Context) (transport http.RoundTripper, err error) { 141 luciProject, ok := ProjectFromContext(c) 142 if ok { 143 return auth.GetRPCTransport(c, auth.AsProject, auth.WithProject(luciProject), auth.WithScopes(gitiles.OAuthScope)) 144 } 145 return auth.GetRPCTransport(c, auth.AsSelf, auth.WithScopes(gitiles.OAuthScope)) 146 } 147 148 func (p *implementation) gitilesClient(c context.Context, host string) (gitilespb.GitilesClient, error) { 149 if p.mockGitiles != nil { 150 return p.mockGitiles, nil 151 } 152 t, err := p.transport(c) 153 if err != nil { 154 return nil, err 155 } 156 return gitiles.NewRESTClient(&http.Client{Transport: t}, host, true) 157 } 158 159 func (p *implementation) gerritClient(c context.Context, host string) (gerritpb.GerritClient, error) { 160 if p.mockGerrit != nil { 161 return p.mockGerrit, nil 162 } 163 t, err := p.transport(c) 164 if err != nil { 165 return nil, err 166 } 167 return gerrit.NewRESTClient(&http.Client{Transport: t}, host, true) 168 }