This is an automated email from the ASF dual-hosted git repository.
tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-tck.git
The following commit(s) were added to refs/heads/main by this push:
new 4d88b55 feat: update snapshot revisions through CLI (#7)
4d88b55 is described below
commit 4d88b55cfa4d7e2e7cf1c8f90372c4078d57c672
Author: tison <[email protected]>
AuthorDate: Tue Sep 1 09:32:26 2026 +0800
feat: update snapshot revisions through CLI (#7)
Signed-off-by: tison <[email protected]>
---
cmd/tck/__snapshots__/root_test.snap | 3 +-
cmd/tck/output.go | 25 ++++++
cmd/tck/snapshots.go | 74 +++++++++++++---
config.toml | 26 ++++++
docs/serialization-snapshots.md | 83 ++++++++++--------
go.mod | 1 +
go.sum | 2 +
internal/snapshots/config.go | 166 +++++++++++++++++++++++++++++++++++
internal/snapshots/generator.go | 22 ++---
internal/snapshots/reconcile.go | 86 ++++++++++++++++--
internal/snapshots/runner.go | 46 ++++++++--
11 files changed, 454 insertions(+), 80 deletions(-)
diff --git a/cmd/tck/__snapshots__/root_test.snap
b/cmd/tck/__snapshots__/root_test.snap
index 101cf9e..7442f13 100644
--- a/cmd/tck/__snapshots__/root_test.snap
+++ b/cmd/tck/__snapshots__/root_test.snap
@@ -8,7 +8,8 @@ Usage:
Available Commands:
check Check the snapshot set and stable snapshot contents
- update Replace committed snapshots with newly generated snapshots
+ sync Synchronize snapshots from config.toml
+ update Adopt one upstream revision and update its snapshots
Flags:
-h, --help help for snapshots
diff --git a/cmd/tck/output.go b/cmd/tck/output.go
index 8676101..401e04b 100644
--- a/cmd/tck/output.go
+++ b/cmd/tck/output.go
@@ -32,6 +32,13 @@ import (
func printResult(output io.Writer, root string, mode snapshots.Mode, result
snapshots.Result) error {
target := displayPath(root, result.Target)
if len(result.Changes) == 0 {
+ if result.RevisionChanged() {
+ if err := printRevisionChange(output, result); err !=
nil {
+ return err
+ }
+ _, err := fmt.Fprintf(output, "✓ Updated %s.\n", target)
+ return err
+ }
_, err := fmt.Fprintf(output, "✓ %s is up to date.\n", target)
return err
}
@@ -71,11 +78,19 @@ func printResult(output io.Writer, root string, mode
snapshots.Mode, result snap
); err != nil {
return err
}
+ if result.RevisionChanged() {
+ if err := printRevisionChange(output, result); err != nil {
+ return err
+ }
+ }
switch {
case mode == snapshots.ModeUpdate:
_, err := fmt.Fprintf(output, "✓ Updated %s.\n", target)
return err
+ case mode == snapshots.ModeSync:
+ _, err := fmt.Fprintf(output, "✓ Synced %s.\n", target)
+ return err
case result.HasBlockingChanges():
count := result.BlockingChangeCount()
_, err := fmt.Fprintf(output, "✗ %s has %d blocking %s.\n",
target, count, plural(count, "change", "changes"))
@@ -91,6 +106,16 @@ func printResult(output io.Writer, root string, mode
snapshots.Mode, result snap
}
}
+func printRevisionChange(output io.Writer, result snapshots.Result) error {
+ _, err := fmt.Fprintf(
+ output,
+ "Source revision: %s -> %s\n",
+ result.PreviousRevision,
+ result.Revision,
+ )
+ return err
+}
+
func changeDetails(change snapshots.Change) string {
switch change.Status {
case snapshots.ChangeAdded:
diff --git a/cmd/tck/snapshots.go b/cmd/tck/snapshots.go
index 0df9b74..be11fc6 100644
--- a/cmd/tck/snapshots.go
+++ b/cmd/tck/snapshots.go
@@ -42,33 +42,72 @@ func newSnapshotsCommand() *cobra.Command {
},
}
command.AddCommand(
- newSnapshotReconcileCommand(snapshots.ModeCheck),
- newSnapshotReconcileCommand(snapshots.ModeUpdate),
+ newSnapshotCheckCommand(),
+ newSnapshotSyncCommand(),
+ newSnapshotUpdateCommand(),
)
return command
}
-func newSnapshotReconcileCommand(mode snapshots.Mode) *cobra.Command {
- verb := string(mode)
+func newSnapshotCheckCommand() *cobra.Command {
validLanguages := append(snapshots.Languages(), "all")
return &cobra.Command{
- Use: verb + " <cpp|go|java|all>",
- Short: reconcileDescription(mode),
- Long: reconcileDetails(mode),
+ Use: "check <cpp|go|java|all>",
+ Short: reconcileDescription(snapshots.ModeCheck),
+ Long: reconcileDetails(snapshots.ModeCheck),
Args: cobra.MatchAll(cobra.ExactArgs(1),
cobra.OnlyValidArgs),
ValidArgs: validLanguages,
RunE: func(command *cobra.Command, args []string) error {
- return reconcileSnapshots(command, mode, args[0])
+ languages := []string{args[0]}
+ if args[0] == "all" {
+ languages = snapshots.Languages()
+ }
+ return reconcileSnapshots(command, snapshots.ModeCheck,
languages, "")
},
}
}
-func reconcileSnapshots(command *cobra.Command, mode snapshots.Mode,
requestedLanguage string) error {
- languages := []string{requestedLanguage}
- if requestedLanguage == "all" {
- languages = snapshots.Languages()
+func newSnapshotSyncCommand() *cobra.Command {
+ return &cobra.Command{
+ Use: "sync",
+ Short: reconcileDescription(snapshots.ModeSync),
+ Long: reconcileDetails(snapshots.ModeSync),
+ Args: cobra.NoArgs,
+ RunE: func(command *cobra.Command, _ []string) error {
+ return reconcileSnapshots(command, snapshots.ModeSync,
snapshots.Languages(), "")
+ },
}
+}
+
+func newSnapshotUpdateCommand() *cobra.Command {
+ return &cobra.Command{
+ Use: "update <cpp|go|java> <revision>",
+ Short: reconcileDescription(snapshots.ModeUpdate),
+ Long: reconcileDetails(snapshots.ModeUpdate),
+ Args: func(command *cobra.Command, args []string) error {
+ if err := cobra.ExactArgs(2)(command, args); err != nil
{
+ return err
+ }
+ for _, language := range snapshots.Languages() {
+ if args[0] == language {
+ return nil
+ }
+ }
+ return fmt.Errorf("unsupported snapshot language %q",
args[0])
+ },
+ ValidArgs: snapshots.Languages(),
+ RunE: func(command *cobra.Command, args []string) error {
+ return reconcileSnapshots(command,
snapshots.ModeUpdate, []string{args[0]}, args[1])
+ },
+ }
+}
+func reconcileSnapshots(
+ command *cobra.Command,
+ mode snapshots.Mode,
+ languages []string,
+ requestedRevision string,
+) error {
root, err := repositoryRoot(command.Context())
if err != nil {
return err
@@ -95,6 +134,7 @@ func reconcileSnapshots(command *cobra.Command, mode
snapshots.Mode, requestedLa
root,
language,
mode,
+ requestedRevision,
command.OutOrStdout(),
command.ErrOrStderr(),
)
@@ -116,8 +156,10 @@ func reconcileDescription(mode snapshots.Mode) string {
switch mode {
case snapshots.ModeCheck:
return "Check the snapshot set and stable snapshot contents"
+ case snapshots.ModeSync:
+ return "Synchronize snapshots from config.toml"
case snapshots.ModeUpdate:
- return "Replace committed snapshots with newly generated
snapshots"
+ return "Adopt one upstream revision and update its snapshots"
default:
panic(fmt.Sprintf("unsupported snapshot mode %q", mode))
}
@@ -127,8 +169,10 @@ func reconcileDetails(mode snapshots.Mode) string {
switch mode {
case snapshots.ModeCheck:
return "Generate snapshots and verify that the file set and
stable contents match. Content changes to existing probabilistic snapshots are
reported but do not fail the check."
+ case snapshots.ModeSync:
+ return "Regenerate all snapshot directories from the
repositories and commits in config.toml without changing the config."
case snapshots.ModeUpdate:
- return "Generate snapshots and atomically replace the selected
committed snapshot directory with the complete generated file set."
+ return "Resolve an upstream commit, branch, or tag, record its
exact commit ID in config.toml, and regenerate that source's snapshots."
default:
panic(fmt.Sprintf("unsupported snapshot mode %q", mode))
}
@@ -138,6 +182,8 @@ func modeHeading(mode snapshots.Mode) string {
switch mode {
case snapshots.ModeCheck:
return "Check"
+ case snapshots.ModeSync:
+ return "Sync"
case snapshots.ModeUpdate:
return "Update"
default:
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..316318c
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[snapshot.cpp]
+repository = "https://github.com/apache/datasketches-cpp.git"
+commit = "401423367055acdf7502e8ed3126730a08039d91"
+
+[snapshot.go]
+repository = "https://github.com/apache/datasketches-go.git"
+commit = "730c0ca31e00b8becf8b70591ae8ca73954912d0"
+
+[snapshot.java]
+repository = "https://github.com/apache/datasketches-java.git"
+commit = "f3b334b380feee9f928500de9adb6bbf763fc104"
diff --git a/docs/serialization-snapshots.md b/docs/serialization-snapshots.md
index d44c415..95b1af9 100644
--- a/docs/serialization-snapshots.md
+++ b/docs/serialization-snapshots.md
@@ -19,69 +19,78 @@
The serialization corpus is a compatibility boundary between DataSketches
implementations. Each directory under `serialization/<language>/snapshots`
contains sketches produced by one implementation and intended to be read and
validated by the others.
-This repository generates snapshots from exact upstream commits instead of
following the latest branch. Pinning makes a checkout reproducible and ensures
that changes to the compatibility corpus receive normal code review.
+This repository generates snapshots from the upstream sources configured in
`config.toml` instead of following the latest branch. Each source records its
repository and exact commit so a checkout remains reproducible.
-## Set up the toolchain
+```toml
+[snapshot.go]
+repository = "https://github.com/apache/datasketches-go.git"
+commit = "730c0ca31e00b8becf8b70591ae8ca73954912d0"
+```
-Install [mise](https://mise.jdx.dev/), then install the pinned toolchain and
inspect the available snapshot commands:
+## Update one source
+
+With the [toolchain installed](#set-up-the-toolchain), adopt a different
upstream branch, tag, or commit by specifying one source language and revision:
```shell
-mise install
-mise run tck -- snapshots --help
+mise run tck -- snapshots update go main
```
-Mise supplies Go, CMake and CTest, Java, and Maven. Git is required for every
source language, a C++ compiler is required for C++, and Make is required for
Go.
+The command resolves the supplied revision to an exact commit ID, writes it to
`[snapshot.go]` in `config.toml`, and regenerates `serialization/go/snapshots`.
Generation happens before either file set is changed, so a generation failure
leaves the repository untouched.
-Commands accept `cpp`, `go`, `java`, or `all` as the source language.
+Updates intentionally handle one source at a time so each upstream change can
be reviewed independently. If the upstream build or output layout changed,
update the corresponding adapter in `internal/snapshots/<language>.go` before
running the command.
-## Update snapshots from upstream
+Review the resolved pin and corpus together:
-Updating the corpus is an intentional maintainer operation because selecting
an upstream revision and accepting compatibility changes require review. There
is no GitHub Actions workflow that discovers newer revisions or opens snapshot
update pull requests.
+```shell
+git diff --stat
+git diff -- config.toml serialization/go/snapshots
+```
-To update one source language:
+Added and deleted files change the set of compatibility cases. Unexpected
changes to deterministic files should be understood from the upstream change
before they are accepted.
-1. Choose the upstream commit to adopt. Prefer a commit on the
implementation's main development branch whose generator represents the
compatibility behavior being adopted.
-2. Change that language's `commit` field in `internal/snapshots/generator.go`.
If the upstream build or output layout changed, update the corresponding
adapter in `internal/snapshots/<language>.go` as well.
-3. Regenerate the complete snapshot directory:
+Verify that generation is reproducible at the new pin and run the repository
checks:
- ```shell
- mise run tck -- snapshots update go
- ```
+```shell
+mise run tck -- snapshots check go
+mise run check
+```
- Update mode generates from the new pin and atomically replaces
`serialization/go/snapshots`; it is not an incremental copy, so removed
upstream outputs become visible as deletions.
+A second generation may report allowed modifications for known probabilistic
snapshots. The file set and deterministic contents must reproduce.
-4. Review the pin and corpus together:
+## Check the pinned corpus
- ```shell
- git diff --stat
- git diff -- internal/snapshots/generator.go
- git status --short serialization/go/snapshots
- ```
+Use check mode to regenerate snapshots from the currently pinned commit and
compare them with the committed corpus:
- Added and deleted files change the set of compatibility cases. Unexpected
changes to deterministic files should be understood from the upstream change
before they are accepted.
+```shell
+mise run tck -- snapshots check go
+```
-5. Verify that generation is reproducible at the new pin and run the
repository checks:
+Check mode does not modify the repository. It fails for added or deleted files
and for content changes to deterministic snapshots. It reports, but allows,
content changes to existing snapshots classified as probabilistic by
`internal/snapshots/stability.go`.
- ```shell
- mise run tck -- snapshots check go
- mise run check
- ```
+This command answers whether the repository matches its pin; it does not
determine whether the pin is the latest upstream commit.
- A second generation may report allowed modifications for known
probabilistic snapshots. The file set and deterministic contents must reproduce.
+## Synchronize configured snapshots
-Use `all` instead of a language only when intentionally refreshing every
source implementation. Updating languages separately usually produces smaller,
easier-to-review pull requests.
+Regenerate every snapshot directory from the repositories and commits
currently recorded in `config.toml`:
-## Check the pinned corpus
+```shell
+mise run tck -- snapshots sync
+```
-Use check mode to regenerate snapshots from the currently pinned commit and
compare them with the committed corpus:
+Sync may change the committed snapshot directories, but it never changes
`config.toml`.
+
+## Set up the toolchain
+
+Install [mise](https://mise.jdx.dev/), then install the pinned toolchain and
inspect the available snapshot commands:
```shell
-mise run tck -- snapshots check go
+mise install
+mise run tck -- snapshots --help
```
-Check mode does not modify the repository. It fails for added or deleted files
and for content changes to deterministic snapshots. It reports, but allows,
content changes to existing snapshots classified as probabilistic by
`internal/snapshots/stability.go`.
+Mise supplies Go, CMake and CTest, Java, and Maven. Git is required for every
source language, a C++ compiler is required for C++, and Make is required for
Go.
-This command answers whether the repository matches its pin; it does not
determine whether the pin is the latest upstream commit.
+Check mode accepts `cpp`, `go`, `java`, or `all`. Synchronization takes no
arguments, while update requires one source language and revision.
## Use the corpus from an implementation
@@ -105,6 +114,6 @@ Snapshot generation is kept out of the required workflow
because it clones and b
## Implementation notes
-For each requested language, the `tck` command reads the repository and commit
from `internal/snapshots/generator.go`, checks out that revision in a temporary
workspace, and invokes the source-specific adapter in
`internal/snapshots/<language>.go`. It then compares the generated output with
`serialization/<language>/snapshots`; update mode atomically replaces that
directory.
+For each requested language, the `tck` command reads the repository and commit
from `config.toml`, checks out that revision in a temporary workspace, and
invokes the source-specific adapter in `internal/snapshots/<language>.go`. It
then compares the generated output with `serialization/<language>/snapshots`.
The command-line interface and change report live in `cmd/tck`. Reconciliation
and file comparison live in `internal/snapshots`, where `stability.go`
classifies deterministic and known probabilistic outputs.
diff --git a/go.mod b/go.mod
index ad8dc28..202eab9 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ toolchain go1.26.5
require (
github.com/gkampitakis/go-snaps v0.5.23
github.com/jedib0t/go-pretty/v6 v6.8.3
+ github.com/pelletier/go-toml/v2 v2.4.3
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
)
diff --git a/go.sum b/go.sum
index 9bb4d11..e33cfbc 100644
--- a/go.sum
+++ b/go.sum
@@ -24,6 +24,8 @@ github.com/maruel/natural v1.3.0
h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg=
github.com/maruel/natural v1.3.0/go.mod
h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mattn/go-runewidth v0.0.16
h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod
h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/pelletier/go-toml/v2 v2.4.3
h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
+github.com/pelletier/go-toml/v2 v2.4.3/go.mod
h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod
h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
diff --git a/internal/snapshots/config.go b/internal/snapshots/config.go
new file mode 100644
index 0000000..2746df4
--- /dev/null
+++ b/internal/snapshots/config.go
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package snapshots
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+
+ "github.com/pelletier/go-toml/v2"
+)
+
+const configFilename = "config.toml"
+
+const configFileHeader = `# Licensed to the Apache Software Foundation (ASF)
under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+`
+
+var commitIDPattern = regexp.MustCompile(`^[0-9a-f]{40}$`)
+
+type repositoryConfig struct {
+ Snapshot snapshotSources `toml:"snapshot"`
+}
+
+type snapshotSources struct {
+ CPP snapshotSource `toml:"cpp"`
+ Go snapshotSource `toml:"go"`
+ Java snapshotSource `toml:"java"`
+}
+
+type snapshotSource struct {
+ Repository string `toml:"repository"`
+ Commit string `toml:"commit"`
+}
+
+func loadConfig(repositoryRoot string) (repositoryConfig, []byte, error) {
+ filename := filepath.Join(repositoryRoot, configFilename)
+ content, err := os.ReadFile(filename)
+ if err != nil {
+ return repositoryConfig{}, nil, fmt.Errorf("read TCK config:
%w", err)
+ }
+
+ var config repositoryConfig
+ decoder :=
toml.NewDecoder(bytes.NewReader(content)).DisallowUnknownFields()
+ if err := decoder.Decode(&config); err != nil {
+ return repositoryConfig{}, nil, fmt.Errorf("decode TCK config:
%w", err)
+ }
+ if err := config.validate(); err != nil {
+ return repositoryConfig{}, nil, err
+ }
+ return config, content, nil
+}
+
+func (config repositoryConfig) validate() error {
+ for _, language := range Languages() {
+ source, found := config.source(language)
+ if !found {
+ return fmt.Errorf("snapshot source for %s is not
configured", language)
+ }
+ if source.Repository == "" {
+ return fmt.Errorf("snapshot repository for %s must not
be empty", language)
+ }
+ if !commitIDPattern.MatchString(source.Commit) {
+ return fmt.Errorf("snapshot commit for %s must be a
40-character lowercase commit ID", language)
+ }
+ }
+ return nil
+}
+
+func (config repositoryConfig) source(language string) (snapshotSource, bool) {
+ switch language {
+ case "cpp":
+ return config.Snapshot.CPP, true
+ case "go":
+ return config.Snapshot.Go, true
+ case "java":
+ return config.Snapshot.Java, true
+ default:
+ return snapshotSource{}, false
+ }
+}
+
+func (config *repositoryConfig) setCommit(language, commit string) error {
+ switch language {
+ case "cpp":
+ config.Snapshot.CPP.Commit = commit
+ case "go":
+ config.Snapshot.Go.Commit = commit
+ case "java":
+ config.Snapshot.Java.Commit = commit
+ default:
+ return fmt.Errorf("unsupported snapshot language %q", language)
+ }
+ return config.validate()
+}
+
+func encodeConfig(config repositoryConfig) ([]byte, error) {
+ if err := config.validate(); err != nil {
+ return nil, err
+ }
+ return []byte(fmt.Sprintf(
+ "%s[snapshot.cpp]\nrepository = %q\ncommit = %q\n\n"+
+ "[snapshot.go]\nrepository = %q\ncommit = %q\n\n"+
+ "[snapshot.java]\nrepository = %q\ncommit = %q\n",
+ configFileHeader,
+ config.Snapshot.CPP.Repository,
+ config.Snapshot.CPP.Commit,
+ config.Snapshot.Go.Repository,
+ config.Snapshot.Go.Commit,
+ config.Snapshot.Java.Repository,
+ config.Snapshot.Java.Commit,
+ )), nil
+}
+
+func replaceConfigFile(repositoryRoot string, content []byte) error {
+ target := filepath.Join(repositoryRoot, configFilename)
+ transaction, err := os.MkdirTemp(repositoryRoot, ".tck-config-")
+ if err != nil {
+ return fmt.Errorf("create config update transaction: %w", err)
+ }
+ defer func() { _ = os.RemoveAll(transaction) }()
+
+ next := filepath.Join(transaction, "next")
+ if err := os.WriteFile(next, content, 0o644); err != nil {
+ return fmt.Errorf("stage TCK config: %w", err)
+ }
+ previous := filepath.Join(transaction, "previous")
+ if err := os.Rename(target, previous); err != nil {
+ return fmt.Errorf("preserve current TCK config: %w", err)
+ }
+ if err := os.Rename(next, target); err != nil {
+ _ = os.Rename(previous, target)
+ return fmt.Errorf("install TCK config: %w", err)
+ }
+ return nil
+}
diff --git a/internal/snapshots/generator.go b/internal/snapshots/generator.go
index 4d6786f..9fccb7f 100644
--- a/internal/snapshots/generator.go
+++ b/internal/snapshots/generator.go
@@ -33,8 +33,6 @@ type generationPaths struct {
type generateFunc func(context.Context, generationPaths, commandRunner) error
type generator struct {
- repository string
- commit string
requirements []string
generate generateFunc
stability func(string) Stability
@@ -42,22 +40,16 @@ type generator struct {
var generators = map[string]generator{
"cpp": {
- repository: "https://github.com/apache/datasketches-cpp.git",
- commit: "401423367055acdf7502e8ed3126730a08039d91",
requirements: []string{"git", "cmake", "ctest"},
generate: generateCPP,
stability: cppStability,
},
"go": {
- repository: "https://github.com/apache/datasketches-go.git",
- commit: "730c0ca31e00b8becf8b70591ae8ca73954912d0",
requirements: []string{"git", "go", "make"},
generate: generateGo,
stability: goStability,
},
"java": {
- repository: "https://github.com/apache/datasketches-java.git",
- commit: "f3b334b380feee9f928500de9adb6bbf763fc104",
requirements: []string{"git", "java", "mvn"},
generate: generateJava,
stability: javaStability,
@@ -75,18 +67,22 @@ func Languages() []string {
func (definition generator) run(
ctx context.Context,
- workspace, destination string,
+ workspace, destination, repository, revision string,
runner commandRunner,
-) error {
+) (string, error) {
paths := generationPaths{
workspace: workspace,
source: filepath.Join(workspace, "source"),
destination: destination,
}
- if err := cloneAtCommit(ctx, runner, definition.repository,
definition.commit, paths.source); err != nil {
- return err
+ resolvedRevision, err := cloneAtRevision(ctx, runner, repository,
revision, paths.source)
+ if err != nil {
+ return "", err
}
- return definition.generate(ctx, paths, runner)
+ if err := definition.generate(ctx, paths, runner); err != nil {
+ return "", err
+ }
+ return resolvedRevision, nil
}
type commandRunner struct {
diff --git a/internal/snapshots/reconcile.go b/internal/snapshots/reconcile.go
index 168e125..f87a6b4 100644
--- a/internal/snapshots/reconcile.go
+++ b/internal/snapshots/reconcile.go
@@ -31,12 +31,19 @@ type Mode string
const (
ModeCheck Mode = "check"
+ ModeSync Mode = "sync"
ModeUpdate Mode = "update"
)
type Result struct {
- Target string
- Changes []Change
+ Target string
+ PreviousRevision string
+ Revision string
+ Changes []Change
+}
+
+func (result Result) RevisionChanged() bool {
+ return result.PreviousRevision != result.Revision
}
func (result Result) HasBlockingChanges() bool {
@@ -57,15 +64,34 @@ func Reconcile(
ctx context.Context,
repositoryRoot, language string,
mode Mode,
+ requestedRevision string,
stdout, stderr io.Writer,
) (Result, error) {
- if mode != ModeCheck && mode != ModeUpdate {
+ if mode != ModeCheck && mode != ModeSync && mode != ModeUpdate {
return Result{}, fmt.Errorf("unsupported reconciliation mode
%q", mode)
}
generator, found := generators[language]
if !found {
return Result{}, fmt.Errorf("unsupported snapshot language %q",
language)
}
+ if mode == ModeUpdate && requestedRevision == "" {
+ return Result{}, fmt.Errorf("a source revision is required in
update mode")
+ }
+ if mode != ModeUpdate && requestedRevision != "" {
+ return Result{}, fmt.Errorf("a source revision can only be
selected in update mode")
+ }
+ config, originalConfig, err := loadConfig(repositoryRoot)
+ if err != nil {
+ return Result{}, err
+ }
+ source, found := config.source(language)
+ if !found {
+ return Result{}, fmt.Errorf("snapshot source for unsupported
language %q", language)
+ }
+ sourceRevision := source.Commit
+ if requestedRevision != "" {
+ sourceRevision = requestedRevision
+ }
for _, requirement := range generator.requirements {
if _, err := exec.LookPath(requirement); err != nil {
return Result{}, fmt.Errorf("required command %q is not
installed or not on PATH", requirement)
@@ -80,7 +106,15 @@ func Reconcile(
generated := filepath.Join(workspace, "generated")
runner := commandRunner{stdout: stdout, stderr: stderr}
- if err := generator.run(ctx, workspace, generated, runner); err != nil {
+ resolvedRevision, err := generator.run(
+ ctx,
+ workspace,
+ generated,
+ source.Repository,
+ sourceRevision,
+ runner,
+ )
+ if err != nil {
return Result{}, fmt.Errorf("generate %s snapshots: %w",
language, err)
}
@@ -90,9 +124,49 @@ func Reconcile(
return Result{}, err
}
- result := Result{Target: target, Changes: changes}
- if mode == ModeUpdate && len(changes) > 0 {
+ result := Result{
+ Target: target,
+ PreviousRevision: source.Commit,
+ Revision: resolvedRevision,
+ Changes: changes,
+ }
+ if mode == ModeCheck {
+ return result, nil
+ }
+ if mode == ModeSync {
+ if len(changes) > 0 {
+ if err := replaceDirectory(target, generated); err !=
nil {
+ return Result{}, err
+ }
+ }
+ return result, nil
+ }
+
+ revisionChanged := result.RevisionChanged()
+ if revisionChanged {
+ if err := config.setCommit(language, resolvedRevision); err !=
nil {
+ return Result{}, err
+ }
+ updatedConfig, err := encodeConfig(config)
+ if err != nil {
+ return Result{}, err
+ }
+ if err := replaceConfigFile(repositoryRoot, updatedConfig); err
!= nil {
+ return Result{}, err
+ }
+ }
+ if len(changes) > 0 {
if err := replaceDirectory(target, generated); err != nil {
+ if !revisionChanged {
+ return Result{}, err
+ }
+ if restoreErr := replaceConfigFile(repositoryRoot,
originalConfig); restoreErr != nil {
+ return Result{}, fmt.Errorf(
+ "%w; restoring the previous config also
failed: %v",
+ err,
+ restoreErr,
+ )
+ }
return Result{}, err
}
}
diff --git a/internal/snapshots/runner.go b/internal/snapshots/runner.go
index 91e3759..f316b5c 100644
--- a/internal/snapshots/runner.go
+++ b/internal/snapshots/runner.go
@@ -18,6 +18,7 @@
package snapshots
import (
+ "bytes"
"context"
"fmt"
"os/exec"
@@ -40,22 +41,49 @@ func (runner commandRunner) run(ctx context.Context,
directory, name string, arg
return nil
}
-func cloneAtCommit(
+func (runner commandRunner) output(ctx context.Context, directory, name
string, arguments ...string) (string, error) {
+ commandLine := strings.Join(append([]string{name}, arguments...), " ")
+ if _, err := fmt.Fprintf(runner.stdout, "$ %s\n", commandLine); err !=
nil {
+ return "", fmt.Errorf("write command trace: %w", err)
+ }
+
+ var output bytes.Buffer
+ command := exec.CommandContext(ctx, name, arguments...)
+ command.Dir = directory
+ command.Stdout = &output
+ command.Stderr = runner.stderr
+ if err := command.Run(); err != nil {
+ return "", fmt.Errorf("run %s: %w", commandLine, err)
+ }
+ return strings.TrimSpace(output.String()), nil
+}
+
+func cloneAtRevision(
ctx context.Context,
runner commandRunner,
- repository, commit, destination string,
-) error {
+ repository, revision, destination string,
+) (string, error) {
+ if revision == "" || strings.TrimSpace(revision) != revision ||
strings.HasPrefix(revision, "-") {
+ return "", fmt.Errorf("invalid source revision %q", revision)
+ }
if err := runner.run(ctx, "", "git", "init", "--quiet", destination);
err != nil {
- return err
+ return "", err
}
if err := runner.run(ctx, "", "git", "-C", destination, "remote",
"add", "origin", repository); err != nil {
- return err
+ return "", err
}
- if err := runner.run(ctx, "", "git", "-C", destination, "fetch",
"--depth", "1", "--no-tags", "origin", commit); err != nil {
- return err
+ if err := runner.run(ctx, "", "git", "-C", destination, "fetch",
"--depth", "1", "--no-tags", "origin", revision); err != nil {
+ return "", err
}
if err := runner.run(ctx, "", "git", "-C", destination, "checkout",
"--quiet", "--detach", "FETCH_HEAD"); err != nil {
- return err
+ return "", err
}
- return nil
+ resolvedRevision, err := runner.output(ctx, "", "git", "-C",
destination, "rev-parse", "HEAD")
+ if err != nil {
+ return "", err
+ }
+ if !commitIDPattern.MatchString(resolvedRevision) {
+ return "", fmt.Errorf("resolved source revision %q is not a
40-character lowercase commit ID", resolvedRevision)
+ }
+ return resolvedRevision, nil
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]