| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package checkout |
| |
| import ( |
| "errors" |
| "fmt" |
| "net/url" |
| "strings" |
| |
| "github.com/golang/protobuf/proto" |
| |
| buildbucketpb "go.chromium.org/luci/buildbucket/proto" |
| "go.fuchsia.dev/infra/cmd/build_init/execution" |
| ) |
| |
| // strategy checks out a git repository according to a Buildbucket build input. |
| type strategy interface { |
| Checkout(*execution.Executor) error |
| Equal(other strategy) bool |
| } |
| |
| // newStrategy selects a strategy for the given git repo URL and build input. input is the |
| // current Buildbucket build input. repoURL is the repository containing test inputs. This |
| // ensures that the patched versions of test inputs are used if they're modified, or |
| // fetched from an expected revision if not. |
| // |
| // If the build input indicates that the repo at repoURL was modified: |
| // * checkout from patchset if the build input has a Gerrit change. |
| // * checkout from a commit if the build input has Gitiles commit. |
| // |
| // If the build input indicates otherwise: |
| // * error if there is no Gitiles commit or Gerrit change, to avoid masking the bug |
| // that there is no build input (nothing to test). |
| // * otherwise checkout from specRef. |
| func newStrategy(input buildbucketpb.Build_Input, repoURL url.URL, specRef string) (strategy, error) { |
| if len(input.GerritChanges) == 0 && input.GitilesCommit == nil { |
| return nil, errors.New("build input has no changes") |
| } |
| |
| if len(input.GerritChanges) > 0 { |
| change := input.GerritChanges[0] |
| changeURL := url.URL{ |
| Scheme: repoURL.Scheme, // gerrit changes have no scheme. |
| Host: removeCodeReviewSuffix(change.Host), |
| Path: change.Project, |
| } |
| if changeURL.String() == repoURL.String() { |
| return checkoutChange{change: change, parent: input.GitilesCommit}, nil |
| } |
| } else if input.GitilesCommit != nil { |
| commit := input.GitilesCommit |
| commitURL := url.URL{ |
| Scheme: repoURL.Scheme, // gitiles commits have no scheme. |
| Host: commit.Host, |
| Path: commit.Project, |
| } |
| if commitURL.String() == repoURL.String() { |
| return checkoutCommit{commit: input.GitilesCommit}, nil |
| } |
| } |
| |
| commit := &buildbucketpb.GitilesCommit{ |
| Host: repoURL.Host, |
| Project: strings.TrimLeft(repoURL.Path, "/"), |
| Id: specRef, |
| } |
| return checkoutCommit{commit}, nil |
| } |
| |
| // checks out from a Gerrit change by rebasing that change on top of a Gitiles commit. |
| type checkoutChange struct { |
| change *buildbucketpb.GerritChange |
| parent *buildbucketpb.GitilesCommit |
| } |
| |
| func (c checkoutChange) Equal(o strategy) bool { |
| switch other := o.(type) { |
| case checkoutChange: |
| return proto.Equal(other.parent, c.parent) && proto.Equal(other.change, c.change) |
| default: |
| return false |
| } |
| } |
| |
| func (c checkoutChange) String() string { |
| return fmt.Sprintf("change: %v parent: %v", c.change.String(), c.parent.String()) |
| } |
| |
| func (c checkoutChange) Checkout(executor *execution.Executor) error { |
| host := removeCodeReviewSuffix(c.change.Host) |
| url := fmt.Sprintf("https://%s/%s", host, c.change.Project) |
| ref := gitilesChangeRef(*c.change) |
| return executor.ExecAllWithRetries([][]string{ |
| // Checkout the patch. |
| {git, "init"}, |
| {git, "remote", "add", "origin", url}, |
| {git, "fetch", "--tags", "origin", ref}, |
| {git, "checkout", "--force", "FETCH_HEAD"}, |
| // Rebase on top of parent. |
| {git, "fetch", "origin", c.parent.Id}, |
| {git, "rebase", "FETCH_HEAD"}, |
| }, 2) |
| } |
| |
| // checks out from a Gitiles commit. |
| type checkoutCommit struct { |
| commit *buildbucketpb.GitilesCommit |
| } |
| |
| func (c checkoutCommit) String() string { |
| return fmt.Sprintf("commit: %v", c.commit.String()) |
| } |
| |
| func (c checkoutCommit) Equal(o strategy) bool { |
| switch other := o.(type) { |
| case checkoutCommit: |
| return proto.Equal(other.commit, c.commit) |
| default: |
| return false |
| } |
| } |
| |
| func (c checkoutCommit) Checkout(executor *execution.Executor) error { |
| url := fmt.Sprintf("https://%s/%s", c.commit.Host, c.commit.Project) |
| return executor.ExecAllWithRetries([][]string{ |
| {git, "init"}, |
| {git, "remote", "add", "origin", url}, |
| {git, "fetch", "--depth=1", "origin", c.commit.Id}, |
| {git, "checkout", "FETCH_HEAD"}, |
| }, 2) |
| } |
| |
| // Converts foo-review.googlesource.com to foo.googlesource.com |
| func removeCodeReviewSuffix(host string) string { |
| return strings.ReplaceAll(host, "-review.googlesource.com", ".googlesource.com") |
| } |
| |
| // Returns the Gitiles ref for a gerrit changeNumber. The ref has the form xx/yyyy/zz |
| // where xx is `yyyy modulo 100` (always 2 digits), and zz is the patchset number. |
| func gitilesChangeRef(change buildbucketpb.GerritChange) string { |
| changeno := change.Change |
| return fmt.Sprintf("refs/changes/%02d/%d/%d", changeno%100, changeno, change.Patchset) |
| } |