diff --git a/checkout.go b/checkout.go index c5aa262..39b1dfb 100644 --- a/checkout.go +++ b/checkout.go @@ -1,6 +1,11 @@ package pipeline -import "encoding/json" +import ( + "encoding/json" + "fmt" + + "github.com/buildkite/go-pipeline/ordered" +) var _ json.Marshaler = (*Checkout)(nil) @@ -12,6 +17,9 @@ type Checkout struct { // the env var explicitly. Submodules *bool `json:"submodules,omitempty" yaml:"submodules,omitempty"` + // Sparse configures sparse checkout. nil = unset (full checkout). + Sparse *Sparse `json:"sparse,omitempty" yaml:"sparse,omitempty"` + // RemainingFields stores any other mapping items so they at least // survive an unmarshal-marshal round-trip. RemainingFields map[string]any `yaml:",inline"` @@ -20,3 +28,38 @@ type Checkout struct { func (c *Checkout) MarshalJSON() ([]byte, error) { return inlineFriendlyMarshalJSON(c) } + +var _ interface { + json.Marshaler + ordered.Unmarshaler +} = (*Sparse)(nil) + +// Sparse models sparse checkout configuration. +type Sparse struct { + // Paths is the list of paths to include in the sparse checkout. + Paths []string `json:"paths,omitempty" yaml:"paths,omitempty"` + + // RemainingFields stores any other mapping items so they at least + // survive an unmarshal-marshal round-trip. + RemainingFields map[string]any `yaml:",inline"` +} + +// MarshalJSON marshals Sparse to JSON. Special handling is needed because +// yaml.v3 has "inline" but encoding/json has no concept of it. +func (s *Sparse) MarshalJSON() ([]byte, error) { + return inlineFriendlyMarshalJSON(s) +} + +// UnmarshalOrdered unmarshals a Sparse from an ordered map. +func (s *Sparse) UnmarshalOrdered(o any) error { + switch o.(type) { + case *ordered.MapSA: + type wrappedSparse Sparse + if err := ordered.Unmarshal(o, (*wrappedSparse)(s)); err != nil { + return fmt.Errorf("unmarshaling sparse: %w", err) + } + return nil + default: + return fmt.Errorf("unmarshaling sparse: unsupported type %T, want a mapping", o) + } +} diff --git a/checkout_test.go b/checkout_test.go index 1a4832e..0f58556 100644 --- a/checkout_test.go +++ b/checkout_test.go @@ -35,6 +35,26 @@ func TestCheckoutUnmarshalYAML(t *testing.T) { `{skip: true}`, Checkout{RemainingFields: map[string]any{"skip": true}}, }, + { + "sparse with paths multi-line", + "sparse:\n paths:\n - .buildkite/\n - src/", + Checkout{Sparse: &Sparse{Paths: []string{".buildkite/", "src/"}}}, + }, + { + "sparse with paths inline", + `{sparse: {paths: [".buildkite/", "src/"]}}`, + Checkout{Sparse: &Sparse{Paths: []string{".buildkite/", "src/"}}}, + }, + { + "sparse omitted", + `{}`, + Checkout{}, + }, + { + "sparse with empty paths", + `{sparse: {paths: []}}`, + Checkout{Sparse: &Sparse{Paths: []string{}}}, + }, } for _, tc := range cases { @@ -84,6 +104,16 @@ func TestCheckoutMarshalYAML(t *testing.T) { wantSubstrs: []string{"skip: true"}, notWant: []string{"submodules"}, }, + { + name: "sparse with paths", + c: Checkout{Sparse: &Sparse{Paths: []string{".buildkite/", "src/"}}}, + wantSubstrs: []string{"sparse:", "paths:", ".buildkite/", "src/"}, + }, + { + name: "sparse nil omitted", + c: Checkout{}, + notWant: []string{"sparse"}, + }, } for _, tc := range cases { @@ -129,6 +159,16 @@ func TestCheckoutMarshalJSON(t *testing.T) { }, `{"depth":1,"gibberish":"x","submodules":true}`, }, + { + "sparse with paths", + &Checkout{Sparse: &Sparse{Paths: []string{".buildkite/", "src/"}}}, + `{"sparse":{"paths":[".buildkite/","src/"]}}`, + }, + { + "sparse nil omitted", + &Checkout{}, + `{}`, + }, } for _, tc := range cases { @@ -159,6 +199,16 @@ func TestCheckoutUnmarshalJSON(t *testing.T) { {"submodules null", `{"submodules":null}`, Checkout{}}, {"submodules true", `{"submodules":true}`, Checkout{Submodules: ptr(true)}}, {"submodules false", `{"submodules":false}`, Checkout{Submodules: ptr(false)}}, + { + "sparse with paths", + `{"sparse":{"paths":[".buildkite/","src/"]}}`, + Checkout{Sparse: &Sparse{Paths: []string{".buildkite/", "src/"}}}, + }, + { + "sparse null", + `{"sparse":null}`, + Checkout{}, + }, } for _, tc := range cases { @@ -177,6 +227,80 @@ func TestCheckoutUnmarshalJSON(t *testing.T) { } } +func TestSparseUnmarshalYAML(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input string + want Sparse + }{ + {"empty", `{}`, Sparse{}}, + { + "paths set", + "paths:\n - .buildkite/\n - src/", + Sparse{Paths: []string{".buildkite/", "src/"}}, + }, + { + "empty paths", + `{paths: []}`, + Sparse{Paths: []string{}}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var node yaml.Node + if err := yaml.Unmarshal([]byte(tc.input), &node); err != nil { + t.Fatalf("yaml.Unmarshal() error = %v", err) + } + + var got Sparse + if err := ordered.Unmarshal(&node, &got); err != nil { + t.Fatalf("ordered.Unmarshal() error = %v", err) + } + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("Sparse diff (-got +want):\n%s", diff) + } + }) + } +} + +func TestSparseMarshalJSON(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + s *Sparse + want string + }{ + {"empty", &Sparse{}, `{}`}, + { + "paths set", + &Sparse{Paths: []string{".buildkite/", "src/"}}, + `{"paths":[".buildkite/","src/"]}`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + b, err := json.Marshal(tc.s) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + + if diff := cmp.Diff(string(b), tc.want); diff != "" { + t.Errorf("Sparse.MarshalJSON() diff (-got +want):\n%s", diff) + } + }) + } +} + func TestPipelineCheckout(t *testing.T) { t.Parallel()