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  }