go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/checkout/strategy.go (about) 1 // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package checkout 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "net/url" 12 "strings" 13 14 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 15 "google.golang.org/protobuf/proto" 16 17 "go.fuchsia.dev/infra/execution" 18 ) 19 20 // strategy checks out a git repository according to a Buildbucket build input. 21 type strategy interface { 22 Checkout(ctx context.Context, e *execution.Executor) error 23 Equal(other strategy) bool 24 } 25 26 // newStrategy selects a strategy for the given git repo URL and build input. input is the 27 // current Buildbucket build input. repoURL is the repository containing test inputs. This 28 // ensures that the patched versions of test inputs are used if they're modified, or 29 // fetched from an expected revision if not. 30 // 31 // If the build input indicates that the repo at repoURL was modified: 32 // * checkout from patchset if the build input has a Gerrit change. 33 // * checkout from a commit if the build input has Gitiles commit. 34 // 35 // If the build input indicates otherwise: 36 // * error if there is no Gitiles commit or Gerrit change, to avoid masking the bug 37 // that there is no build input (nothing to test). 38 // * otherwise checkout at defaultRevision, which can be either a ref (e.g. 39 // refs/heads/main) or a commit hash. 40 func newStrategy(input *buildbucketpb.Build_Input, repoURL url.URL, defaultRevision string) (strategy, error) { 41 if len(input.GerritChanges) == 0 && input.GitilesCommit == nil { 42 return nil, errors.New("build input has no changes") 43 } 44 45 if len(input.GerritChanges) > 0 { 46 change := input.GerritChanges[0] 47 changeURL := url.URL{ 48 Scheme: repoURL.Scheme, // gerrit changes have no scheme. 49 Host: RemoveCodeReviewSuffix(change.Host), 50 Path: change.Project, 51 } 52 if changeURL.String() == repoURL.String() { 53 return checkoutChange{change: change, parent: input.GitilesCommit}, nil 54 } 55 } else if input.GitilesCommit != nil { 56 commit := input.GitilesCommit 57 commitURL := url.URL{ 58 Scheme: repoURL.Scheme, // gitiles commits have no scheme. 59 Host: commit.Host, 60 Path: commit.Project, 61 } 62 if commitURL.String() == repoURL.String() { 63 return checkoutCommit{commit: input.GitilesCommit}, nil 64 } 65 } 66 67 // The current build was not triggered by a change to the specified 68 // repository, so we'll checkout at the default revision. 69 commit := &buildbucketpb.GitilesCommit{ 70 Host: repoURL.Host, 71 Project: strings.TrimLeft(repoURL.Path, "/"), 72 Id: defaultRevision, 73 } 74 return checkoutCommit{commit}, nil 75 } 76 77 // checks out from a Gerrit change by rebasing that change on top of a Gitiles commit. 78 type checkoutChange struct { 79 change *buildbucketpb.GerritChange 80 parent *buildbucketpb.GitilesCommit 81 } 82 83 func (c checkoutChange) Equal(o strategy) bool { 84 switch other := o.(type) { 85 case checkoutChange: 86 return proto.Equal(other.parent, c.parent) && proto.Equal(other.change, c.change) 87 default: 88 return false 89 } 90 } 91 92 func (c checkoutChange) String() string { 93 return fmt.Sprintf("change: %s parent: %s", c.change, c.parent) 94 } 95 96 func (c checkoutChange) Checkout(ctx context.Context, executor *execution.Executor) error { 97 host := RemoveCodeReviewSuffix(c.change.Host) 98 url := fmt.Sprintf("https://%s/%s", host, c.change.Project) 99 ref := GitilesChangeRef(c.change) 100 101 // If rebase is not necessary, we only need to fetch depth 1. 102 var fetchOpt string 103 if c.parent == nil { 104 fetchOpt = "--depth=1" 105 } else { 106 fetchOpt = "--tags" 107 } 108 109 cmds := []execution.Command{ 110 // Checkout the patch. 111 {Args: []string{git, "init", "--quiet"}}, 112 {Args: []string{git, "remote", "add", "origin", url}}, 113 {Args: []string{git, "fetch", fetchOpt, "origin", ref}}, 114 {Args: []string{git, "checkout", "--force", "FETCH_HEAD"}}, 115 } 116 if c.parent != nil { 117 cmds = append(cmds, []execution.Command{ 118 // Rebase on top of parent. 119 {Args: []string{git, "fetch", "origin", c.parent.Id}}, 120 // Rebase failures are generally user-caused, e.g. because the user 121 // is trying to rebase a change that has a merge conflict with the 122 // parent that needs to be manually fixed. 123 {Args: []string{git, "rebase", "FETCH_HEAD"}, UserCausedError: true}, 124 }...) 125 } 126 return executor.ExecAll(ctx, cmds) 127 } 128 129 // checks out from a Gitiles commit. 130 type checkoutCommit struct { 131 commit *buildbucketpb.GitilesCommit 132 } 133 134 func (c checkoutCommit) String() string { 135 return fmt.Sprintf("commit: %s", c.commit) 136 } 137 138 func (c checkoutCommit) Equal(o strategy) bool { 139 switch other := o.(type) { 140 case checkoutCommit: 141 return proto.Equal(other.commit, c.commit) 142 default: 143 return false 144 } 145 } 146 147 func (c checkoutCommit) Checkout(ctx context.Context, executor *execution.Executor) error { 148 url := fmt.Sprintf("https://%s/%s", c.commit.Host, c.commit.Project) 149 return executor.ExecAll(ctx, []execution.Command{ 150 {Args: []string{git, "init", "--quiet"}}, 151 {Args: []string{git, "remote", "add", "origin", url}}, 152 {Args: []string{git, "fetch", "--depth=1", "origin", c.commit.Id}}, 153 {Args: []string{git, "checkout", "FETCH_HEAD"}}, 154 }) 155 } 156 157 // Converts foo-review.googlesource.com to foo.googlesource.com 158 func RemoveCodeReviewSuffix(host string) string { 159 return strings.ReplaceAll(host, "-review.googlesource.com", ".googlesource.com") 160 } 161 162 // Returns the Gitiles ref for a gerrit changeNumber. The ref has the form xx/yyyy/zz 163 // where xx is `yyyy modulo 100` (always 2 digits), and zz is the patchset number. 164 func GitilesChangeRef(change *buildbucketpb.GerritChange) string { 165 changeno := change.Change 166 return fmt.Sprintf("refs/changes/%02d/%d/%d", changeno%100, changeno, change.Patchset) 167 }