From ae33727eecec4a2eb28b75e2505cedfd096c8eee Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 14 Nov 2025 18:00:28 +0200 Subject: [PATCH 1/3] add dev/vendor-lib.sh --- dev/vendor-lib.sh | 166 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100755 dev/vendor-lib.sh diff --git a/dev/vendor-lib.sh b/dev/vendor-lib.sh new file mode 100755 index 0000000000..ef53800ab7 --- /dev/null +++ b/dev/vendor-lib.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +SRC_REPO="$ROOT_DIR/../sourcegraph" +SRC_LIB="$SRC_REPO/lib" +DEST_LIB="$ROOT_DIR/lib" + +# Validate source repository +echo "Validating source repository..." +cd "$SRC_REPO" + +# Check if on main branch +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$CURRENT_BRANCH" != "main" ]; then + echo "Error: Source repository is not on main branch (currently on: $CURRENT_BRANCH)" + exit 1 +fi + +# Check for uncommitted changes +if ! git diff-index --quiet HEAD -- ; then + echo "Error: Source repository has uncommitted changes" + git status --short + exit 1 +fi + +# Get commit info +COMMIT_HASH=$(git rev-parse HEAD) +COMMIT_DATE=$(git show -s --format=%ci HEAD) + +echo "Source repository validated:" +echo " Branch: $CURRENT_BRANCH" +echo " Commit: $COMMIT_HASH" +echo " Date: $COMMIT_DATE" + +cd "$ROOT_DIR" + +echo "" +echo "Discovering packages to vendor..." + +# Get direct imports (including test imports) +DIRECT_IMPORTS=$(go list -f '{{ join .Imports "\n" }} +{{ join .TestImports "\n" }} +{{ join .XTestImports "\n" }}' ./... | grep 'github.com/sourcegraph/sourcegraph/lib' | sort | uniq || true) + +if [ -z "$DIRECT_IMPORTS" ]; then + echo "Error: No lib imports found" + exit 1 +fi + +echo "Direct imports found (including test imports):" +echo "$DIRECT_IMPORTS" + +# Get transitive dependencies within lib +echo "" +echo "Finding transitive dependencies..." +cd "$SRC_REPO" +TRANSITIVE=$(echo "$DIRECT_IMPORTS" | xargs go list -f '{{ join .Deps "\n" }}' 2>/dev/null | grep 'github.com/sourcegraph/sourcegraph/lib' | sort | uniq || true) +cd "$SCRIPT_DIR" + +# Combine and deduplicate +ALL_PACKAGES=$(echo -e "$DIRECT_IMPORTS\n$TRANSITIVE" | sort | uniq) + +echo "" +echo "All packages to vendor:" +echo "$ALL_PACKAGES" + +# Remove existing lib directory +if [ -d "$DEST_LIB" ]; then + echo "" + echo "Removing existing $DEST_LIB directory..." + rm -rf "$DEST_LIB" +fi + +# Create lib directory +mkdir -p "$DEST_LIB" + +# Copy each package +echo "" +echo "Copying packages..." +for pkg in $ALL_PACKAGES; do + # Extract the path after github.com/sourcegraph/sourcegraph/lib/ + pkg_path=${pkg#github.com/sourcegraph/sourcegraph/lib/} + + src_dir="$SRC_LIB/$pkg_path" + dest_dir="$DEST_LIB/$pkg_path" + + if [ ! -d "$src_dir" ]; then + echo "Warning: Source directory not found: $src_dir" + continue + fi + + echo " $pkg_path" + mkdir -p "$dest_dir" + + # Copy Go files (excluding tests) and other relevant files + for gofile in "$src_dir"/*.go; do + [ -e "$gofile" ] || continue + if [[ ! "$gofile" =~ _test\.go$ ]]; then + cp "$gofile" "$dest_dir/" 2>/dev/null || true + fi + done + cp -r "$src_dir"/*.mod "$dest_dir/" 2>/dev/null || true + cp -r "$src_dir"/*.sum "$dest_dir/" 2>/dev/null || true + + # Copy any subdirectories that might contain embedded files or test data + for item in "$src_dir"/*; do + if [ -d "$item" ] && [ "$(basename "$item")" != "." ] && [ "$(basename "$item")" != ".." ]; then + subdir=$(basename "$item") + # Skip if it's a package we're already copying separately + if ! echo "$ALL_PACKAGES" | grep -q "lib/$pkg_path/$subdir"; then + if ls "$item"/*.go 2>/dev/null | grep -v '_test\.go$' >/dev/null 2>&1; then + # Contains non-test Go files, skip it (it's a separate package) + continue + fi + # Copy non-package directories (e.g., testdata, embedded files) + cp -r "$item" "$dest_dir/" 2>/dev/null || true + fi + fi + done +done + +# Create go.mod file +echo "" +echo "Creating go.mod file..." +cat > "$DEST_LIB/go.mod" < "$DEST_LIB/README.md" < Date: Tue, 18 Nov 2025 09:37:04 +0200 Subject: [PATCH 2/3] vendor in lib from sourcegraph --- lib/README.md | 23 + lib/accesstoken/personal_access_token.go | 29 ++ lib/api/version_check.go | 49 +++ lib/batches/batch_spec.go | 264 +++++++++++ lib/batches/changeset_spec.go | 273 ++++++++++++ lib/batches/changeset_specs.go | 310 +++++++++++++ lib/batches/env/env.go | 170 ++++++++ lib/batches/env/var.go | 99 +++++ lib/batches/execution/cache/cache.go | 174 ++++++++ lib/batches/execution/cache/util.go | 7 + lib/batches/execution/results.go | 98 +++++ lib/batches/git/changes.go | 37 ++ lib/batches/git/refs.go | 10 + lib/batches/json/validate.go | 24 + lib/batches/json_logs.go | 334 ++++++++++++++ lib/batches/jsonschema/jsonschema.go | 36 ++ lib/batches/outputs.go | 48 ++ lib/batches/overridable/bool.go | 69 +++ lib/batches/overridable/bool_or_string.go | 83 ++++ lib/batches/overridable/overridable.go | 143 ++++++ lib/batches/published.go | 110 +++++ lib/batches/schema/batch_spec_stringdata.go | 412 ++++++++++++++++++ .../schema/changeset_spec_stringdata.go | 121 +++++ lib/batches/template/partial_eval.go | 345 +++++++++++++++ lib/batches/template/template.go | 18 + lib/batches/template/templating.go | 334 ++++++++++++++ lib/batches/workspaces_execution_input.go | 39 ++ lib/batches/yaml/validate.go | 33 ++ lib/codeintel/upload/compress.go | 42 ++ lib/codeintel/upload/indexer_name.go | 76 ++++ lib/codeintel/upload/progress_reader.go | 56 +++ lib/codeintel/upload/request.go | 204 +++++++++ lib/codeintel/upload/request_logger.go | 90 ++++ lib/codeintel/upload/retry.go | 37 ++ lib/codeintel/upload/upload.go | 369 ++++++++++++++++ lib/codeintel/upload/upload_options.go | 47 ++ lib/errors/client_error.go | 26 ++ lib/errors/cockroach.go | 186 ++++++++ lib/errors/errors.go | 24 + lib/errors/filter.go | 51 +++ lib/errors/multi_error.go | 113 +++++ lib/errors/postgres.go | 12 + lib/errors/rich_error.go | 171 ++++++++ lib/errors/tree.go | 56 +++ lib/errors/warning.go | 96 ++++ lib/go.mod | 133 ++++++ lib/go.sum | 381 ++++++++++++++++ lib/output/block.go | 58 +++ lib/output/capabilities.go | 124 ++++++ lib/output/emoji.go | 32 ++ lib/output/line.go | 78 ++++ lib/output/logger.go | 31 ++ lib/output/noop_writer.go | 10 + lib/output/output.go | 354 +++++++++++++++ lib/output/output_unix.go | 79 ++++ lib/output/output_windows.go | 61 +++ lib/output/pending.go | 26 ++ lib/output/pending_simple.go | 26 ++ lib/output/pending_tty.go | 160 +++++++ lib/output/progress.go | 60 +++ lib/output/progress_simple.go | 91 ++++ lib/output/progress_tty.go | 295 +++++++++++++ lib/output/progress_with_status_bars.go | 23 + .../progress_with_status_bars_simple.go | 106 +++++ lib/output/progress_with_status_bars_tty.go | 309 +++++++++++++ lib/output/spinner.go | 47 ++ lib/output/status_bar.go | 68 +++ lib/output/style.go | 75 ++++ lib/output/table.go | 52 +++ lib/output/visible_string_width.go | 16 + lib/process/pipe.go | 140 ++++++ 71 files changed, 8183 insertions(+) create mode 100644 lib/README.md create mode 100644 lib/accesstoken/personal_access_token.go create mode 100644 lib/api/version_check.go create mode 100644 lib/batches/batch_spec.go create mode 100644 lib/batches/changeset_spec.go create mode 100644 lib/batches/changeset_specs.go create mode 100644 lib/batches/env/env.go create mode 100644 lib/batches/env/var.go create mode 100644 lib/batches/execution/cache/cache.go create mode 100644 lib/batches/execution/cache/util.go create mode 100644 lib/batches/execution/results.go create mode 100644 lib/batches/git/changes.go create mode 100644 lib/batches/git/refs.go create mode 100644 lib/batches/json/validate.go create mode 100644 lib/batches/json_logs.go create mode 100644 lib/batches/jsonschema/jsonschema.go create mode 100644 lib/batches/outputs.go create mode 100644 lib/batches/overridable/bool.go create mode 100644 lib/batches/overridable/bool_or_string.go create mode 100644 lib/batches/overridable/overridable.go create mode 100644 lib/batches/published.go create mode 100644 lib/batches/schema/batch_spec_stringdata.go create mode 100644 lib/batches/schema/changeset_spec_stringdata.go create mode 100644 lib/batches/template/partial_eval.go create mode 100644 lib/batches/template/template.go create mode 100644 lib/batches/template/templating.go create mode 100644 lib/batches/workspaces_execution_input.go create mode 100644 lib/batches/yaml/validate.go create mode 100644 lib/codeintel/upload/compress.go create mode 100644 lib/codeintel/upload/indexer_name.go create mode 100644 lib/codeintel/upload/progress_reader.go create mode 100644 lib/codeintel/upload/request.go create mode 100644 lib/codeintel/upload/request_logger.go create mode 100644 lib/codeintel/upload/retry.go create mode 100644 lib/codeintel/upload/upload.go create mode 100644 lib/codeintel/upload/upload_options.go create mode 100644 lib/errors/client_error.go create mode 100644 lib/errors/cockroach.go create mode 100644 lib/errors/errors.go create mode 100644 lib/errors/filter.go create mode 100644 lib/errors/multi_error.go create mode 100644 lib/errors/postgres.go create mode 100644 lib/errors/rich_error.go create mode 100644 lib/errors/tree.go create mode 100644 lib/errors/warning.go create mode 100644 lib/go.mod create mode 100644 lib/go.sum create mode 100644 lib/output/block.go create mode 100644 lib/output/capabilities.go create mode 100644 lib/output/emoji.go create mode 100644 lib/output/line.go create mode 100644 lib/output/logger.go create mode 100644 lib/output/noop_writer.go create mode 100644 lib/output/output.go create mode 100644 lib/output/output_unix.go create mode 100644 lib/output/output_windows.go create mode 100644 lib/output/pending.go create mode 100644 lib/output/pending_simple.go create mode 100644 lib/output/pending_tty.go create mode 100644 lib/output/progress.go create mode 100644 lib/output/progress_simple.go create mode 100644 lib/output/progress_tty.go create mode 100644 lib/output/progress_with_status_bars.go create mode 100644 lib/output/progress_with_status_bars_simple.go create mode 100644 lib/output/progress_with_status_bars_tty.go create mode 100644 lib/output/spinner.go create mode 100644 lib/output/status_bar.go create mode 100644 lib/output/style.go create mode 100644 lib/output/table.go create mode 100644 lib/output/visible_string_width.go create mode 100644 lib/process/pipe.go diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000000..0b478e3cd1 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,23 @@ +# Vendored lib packages + +This directory contains vendored packages from `github.com/sourcegraph/sourcegraph/lib`. + +## Source + +- **Repository**: https://github.com/sourcegraph/sourcegraph +- **Commit**: 2ee2b8e77de9663b08ce5f6e5a2c7d2217ce721a +- **Date**: 2025-11-17 19:49:42 -0800 + +## Updating + +To update these vendored packages, run: + +```bash +./dev/vendor-lib.sh +``` + +The script will: +1. Validate that `../sourcegraph` is on the `main` branch with no uncommitted changes +2. Discover all direct and transitive dependencies on `lib` packages +3. Copy only the needed packages +4. Update this README with the new commit information diff --git a/lib/accesstoken/personal_access_token.go b/lib/accesstoken/personal_access_token.go new file mode 100644 index 0000000000..2fe68066eb --- /dev/null +++ b/lib/accesstoken/personal_access_token.go @@ -0,0 +1,29 @@ +// Package accesstoken is exposed in lib/ for usage in src-cli +package accesstoken + +import ( + "sync" + + "github.com/grafana/regexp" // avoid pulling in internal lazyregexp package + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +var makePersonalAccessTokenRegex = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp { + return regexp.MustCompile("^(?:(?:sgp_|sgph_)(?:[a-zA-Z0-9]+_)?)?([a-fA-F0-9]{40})$") +}) + +// ParsePersonalAccessToken parses a personal access token to remove prefixes and extract the that is stored in the database +// Personal access tokens can take several forms: +// - +// - sgp_ +// - sgp__ +func ParsePersonalAccessToken(token string) (string, error) { + tokenMatches := makePersonalAccessTokenRegex().FindStringSubmatch(token) + if len(tokenMatches) <= 1 { + return "", errors.New("invalid token format") + } + tokenValue := tokenMatches[1] + + return tokenValue, nil +} diff --git a/lib/api/version_check.go b/lib/api/version_check.go new file mode 100644 index 0000000000..a5c1000bdc --- /dev/null +++ b/lib/api/version_check.go @@ -0,0 +1,49 @@ +package api + +import ( + "github.com/grafana/regexp" + + "github.com/Masterminds/semver" +) + +// BuildDateRegex matches the build date in a Sourcegraph version string. +var BuildDateRegex = regexp.MustCompile(`\d+_(\d{4}-\d{2}-\d{2})_(\d+\.\d+)?-?[a-z0-9]{7,}(_patch)?$`) + +// CheckSourcegraphVersion checks if the given version satisfies the given constraint. +// NOTE: A version with a prerelease suffix (e.g. the "-rc.3" of "3.35.1-rc.3") is not +// considered by semver to satisfy a constraint without a prerelease suffix, regardless of +// whether or not the major/minor/patch version is greater than or equal to that of the +// constraint. +// +// For example, the version "3.35.1-rc.3" is not considered to satisfy the constraint ">= +// 3.23.0". This is likely not the expected outcome. However, the same version IS +// considered to satisfy the constraint "3.23.0-0". Thus, it is recommended to pass a +// constraint with a minimum prerelease version suffix attached if comparisons to +// prerelease versions are ever expected. See +// https://github.com/Masterminds/semver#working-with-prerelease-versions for more. +func CheckSourcegraphVersion(version, constraint, minDate string) (bool, error) { + if version == "dev" || version == "0.0.0+dev" { + return true, nil + } + + // Since we don't actually care about the abbreviated commit hash at the end of the + // version string, we match on 7 or more characters. Currently, the Sourcegraph version + // is expected to return 12: + // https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/dev/ci/internal/ci/config.go?L96. + matches := BuildDateRegex.FindStringSubmatch(version) + if len(matches) > 1 { + return matches[1] >= minDate, nil + } + + c, err := semver.NewConstraint(constraint) + if err != nil { + return false, nil + } + + v, err := semver.NewVersion(version) + if err != nil { + return false, err + } + + return c.Check(v), nil +} diff --git a/lib/batches/batch_spec.go b/lib/batches/batch_spec.go new file mode 100644 index 0000000000..e558c286f4 --- /dev/null +++ b/lib/batches/batch_spec.go @@ -0,0 +1,264 @@ +package batches + +import ( + "fmt" + "strings" + + "github.com/sourcegraph/sourcegraph/lib/batches/env" + "github.com/sourcegraph/sourcegraph/lib/batches/overridable" + "github.com/sourcegraph/sourcegraph/lib/batches/schema" + "github.com/sourcegraph/sourcegraph/lib/batches/template" + "github.com/sourcegraph/sourcegraph/lib/batches/yaml" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// Some general notes about the struct definitions below. +// +// 1. They map _very_ closely to the batch spec JSON schema. We don't +// auto-generate the types because we need YAML support (more on that in a +// moment) and because no generator can currently handle oneOf fields +// gracefully in Go, but that's a potential future enhancement. +// +// 2. Fields are tagged with _both_ JSON and YAML tags. Internally, the JSON +// schema library needs to be able to marshal the struct to JSON for +// validation, so we need to ensure that we're generating the right JSON to +// represent the YAML that we unmarshalled. +// +// 3. All JSON tags include omitempty so that the schema validation can pick up +// omitted fields. The other option here was to have everything unmarshal to +// pointers, which is ugly and inefficient. + +type BatchSpec struct { + Version int `json:"version,omitempty" yaml:"version"` + Name string `json:"name,omitempty" yaml:"name"` + Description string `json:"description,omitempty" yaml:"description"` + On []OnQueryOrRepository `json:"on,omitempty" yaml:"on"` + Workspaces []WorkspaceConfiguration `json:"workspaces,omitempty" yaml:"workspaces"` + Steps []Step `json:"steps,omitempty" yaml:"steps"` + TransformChanges *TransformChanges `json:"transformChanges,omitempty" yaml:"transformChanges,omitempty"` + ImportChangesets []ImportChangeset `json:"importChangesets,omitempty" yaml:"importChangesets"` + ChangesetTemplate *ChangesetTemplate `json:"changesetTemplate,omitempty" yaml:"changesetTemplate"` +} + +type ChangesetTemplate struct { + Title string `json:"title,omitempty" yaml:"title"` + Body string `json:"body,omitempty" yaml:"body"` + Branch string `json:"branch,omitempty" yaml:"branch"` + Fork *bool `json:"fork,omitempty" yaml:"fork"` + Commit ExpandedGitCommitDescription `json:"commit" yaml:"commit"` + Published *overridable.BoolOrString `json:"published" yaml:"published"` +} + +type GitCommitAuthor struct { + Name string `json:"name" yaml:"name"` + Email string `json:"email" yaml:"email"` +} + +type ExpandedGitCommitDescription struct { + Message string `json:"message,omitempty" yaml:"message"` + Author *GitCommitAuthor `json:"author,omitempty" yaml:"author"` +} + +type ImportChangeset struct { + Repository string `json:"repository" yaml:"repository"` + ExternalIDs []any `json:"externalIDs" yaml:"externalIDs"` +} + +type WorkspaceConfiguration struct { + RootAtLocationOf string `json:"rootAtLocationOf,omitempty" yaml:"rootAtLocationOf"` + In string `json:"in,omitempty" yaml:"in"` + OnlyFetchWorkspace bool `json:"onlyFetchWorkspace,omitempty" yaml:"onlyFetchWorkspace"` +} + +type OnQueryOrRepository struct { + RepositoriesMatchingQuery string `json:"repositoriesMatchingQuery,omitempty" yaml:"repositoriesMatchingQuery"` + Repository string `json:"repository,omitempty" yaml:"repository"` + Branch string `json:"branch,omitempty" yaml:"branch"` + Branches []string `json:"branches,omitempty" yaml:"branches"` +} + +var ErrConflictingBranches = NewValidationError(errors.New("both branch and branches specified")) + +func (oqor *OnQueryOrRepository) GetBranches() ([]string, error) { + if oqor.Branch != "" { + if len(oqor.Branches) > 0 { + return nil, ErrConflictingBranches + } + return []string{oqor.Branch}, nil + } + return oqor.Branches, nil +} + +type Step struct { + Run string `json:"run,omitempty" yaml:"run"` + Container string `json:"container,omitempty" yaml:"container"` + Env env.Environment `json:"env" yaml:"env"` + Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"` + Outputs Outputs `json:"outputs,omitempty" yaml:"outputs,omitempty"` + Mount []Mount `json:"mount,omitempty" yaml:"mount,omitempty"` + If any `json:"if,omitempty" yaml:"if,omitempty"` +} + +func (s *Step) IfCondition() string { + switch v := s.If.(type) { + case bool: + if v { + return "true" + } + return "false" + case string: + return v + default: + return "" + } +} + +type Outputs map[string]Output + +type Output struct { + Value string `json:"value,omitempty" yaml:"value,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` +} + +type TransformChanges struct { + Group []Group `json:"group,omitempty" yaml:"group"` +} + +type Group struct { + Directory string `json:"directory,omitempty" yaml:"directory"` + Branch string `json:"branch,omitempty" yaml:"branch"` + Repository string `json:"repository,omitempty" yaml:"repository"` +} + +type Mount struct { + Mountpoint string `json:"mountpoint" yaml:"mountpoint"` + Path string `json:"path" yaml:"path"` +} + +func ParseBatchSpec(data []byte) (*BatchSpec, error) { + return parseBatchSpec(schema.BatchSpecJSON, data) +} + +func parseBatchSpec(schema string, data []byte) (*BatchSpec, error) { + var spec BatchSpec + if err := yaml.UnmarshalValidate(schema, data, &spec); err != nil { + var multiErr errors.MultiError + if errors.As(err, &multiErr) { + var newMultiError error + + for _, e := range multiErr.Errors() { + // In case of `name` we try to make the error message more user-friendly. + if strings.Contains(e.Error(), "name: Does not match pattern") { + newMultiError = errors.Append(newMultiError, NewValidationError(errors.Newf("The batch change name can only contain word characters, dots and dashes. No whitespace or newlines allowed."))) + } else { + newMultiError = errors.Append(newMultiError, NewValidationError(e)) + } + } + + return nil, newMultiError + } + + return nil, err + } + + var errs error + + if len(spec.Steps) != 0 && spec.ChangesetTemplate == nil { + errs = errors.Append(errs, NewValidationError(errors.New("batch spec includes steps but no changesetTemplate"))) + } + + for i, step := range spec.Steps { + for _, mount := range step.Mount { + if strings.Contains(mount.Path, invalidMountCharacters) { + errs = errors.Append(errs, NewValidationError(errors.Newf("step %d mount path contains invalid characters", i+1))) + } + if strings.Contains(mount.Mountpoint, invalidMountCharacters) { + errs = errors.Append(errs, NewValidationError(errors.Newf("step %d mount mountpoint contains invalid characters", i+1))) + } + } + } + + return &spec, errs +} + +const invalidMountCharacters = "," + +func (on *OnQueryOrRepository) String() string { + if on.RepositoriesMatchingQuery != "" { + return on.RepositoriesMatchingQuery + } else if on.Repository != "" { + return "repository:" + on.Repository + } + + return fmt.Sprintf("%v", *on) +} + +// BatchSpecValidationError is returned when parsing/using values from the batch spec failed. +type BatchSpecValidationError struct { + err error +} + +func NewValidationError(err error) BatchSpecValidationError { + return BatchSpecValidationError{err} +} + +func (e BatchSpecValidationError) Error() string { + return e.err.Error() +} + +func IsValidationError(err error) bool { + return errors.HasType[*BatchSpecValidationError](err) +} + +// SkippedStepsForRepo calculates the steps required to run on the given repo. +func SkippedStepsForRepo(spec *BatchSpec, repoName string, fileMatches []string) (skipped map[int]struct{}, err error) { + skipped = map[int]struct{}{} + + for idx, step := range spec.Steps { + // If no if condition is set the step is always run. + if step.IfCondition() == "" { + continue + } + + batchChange := template.BatchChangeAttributes{ + Name: spec.Name, + Description: spec.Description, + } + // TODO: This step ctx is incomplete, is this allowed? + // We can at least optimize further here and do more static evaluation + // when we have a cached result for the previous step. + stepCtx := &template.StepContext{ + Repository: template.Repository{ + Name: repoName, + FileMatches: fileMatches, + }, + BatchChange: batchChange, + } + static, boolVal, err := template.IsStaticBool(step.IfCondition(), stepCtx) + if err != nil { + return nil, err + } + + if static && !boolVal { + skipped[idx] = struct{}{} + } + } + + return skipped, nil +} + +// RequiredEnvVars inspects all steps for outer environment variables used and +// compiles a deduplicated list from those. +func (s *BatchSpec) RequiredEnvVars() []string { + requiredMap := map[string]struct{}{} + required := []string{} + for _, step := range s.Steps { + for _, v := range step.Env.OuterVars() { + if _, ok := requiredMap[v]; !ok { + requiredMap[v] = struct{}{} + required = append(required, v) + } + } + } + return required +} diff --git a/lib/batches/changeset_spec.go b/lib/batches/changeset_spec.go new file mode 100644 index 0000000000..4b64f670a1 --- /dev/null +++ b/lib/batches/changeset_spec.go @@ -0,0 +1,273 @@ +package batches + +import ( + "encoding/json" + "reflect" + "strconv" + + jsonutil "github.com/sourcegraph/sourcegraph/lib/batches/json" + "github.com/sourcegraph/sourcegraph/lib/batches/schema" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// ErrHeadBaseMismatch is returned by (*ChangesetSpec).UnmarshalValidate() if +// the head and base repositories do not match (a case which we do not support +// yet). +var ErrHeadBaseMismatch = errors.New("headRepository does not match baseRepository") + +// ParseChangesetSpec unmarshals the RawSpec into Spec and validates it against +// the ChangesetSpec schema and does additional semantic validation. +func ParseChangesetSpec(rawSpec []byte) (*ChangesetSpec, error) { + spec := &ChangesetSpec{} + err := jsonutil.UnmarshalValidate(schema.ChangesetSpecJSON, rawSpec, &spec) + if err != nil { + return nil, err + } + + headRepo := spec.HeadRepository + baseRepo := spec.BaseRepository + if headRepo != "" && baseRepo != "" && headRepo != baseRepo { + return nil, ErrHeadBaseMismatch + } + + return spec, nil +} + +// ParseChangesetSpecExternalID attempts to parse the ID of a changeset in the +// batch spec that should be imported. +func ParseChangesetSpecExternalID(id any) (string, error) { + var sid string + + switch tid := id.(type) { + case string: + sid = tid + case int, int8, int16, int32, int64: + sid = strconv.FormatInt(reflect.ValueOf(id).Int(), 10) + case uint, uint8, uint16, uint32, uint64: + sid = strconv.FormatUint(reflect.ValueOf(id).Uint(), 10) + case float32: + sid = strconv.FormatFloat(float64(tid), 'f', -1, 32) + case float64: + sid = strconv.FormatFloat(tid, 'f', -1, 64) + default: + return "", NewValidationError(errors.Newf("cannot convert value of type %T into a valid external ID: expected string or int", id)) + } + + return sid, nil +} + +// Note: When modifying this struct, make sure to reflect the new fields below in +// the customized MarshalJSON method. + +type ChangesetSpec struct { + // BaseRepository is the GraphQL ID of the base repository. + BaseRepository string `json:"baseRepository,omitempty"` + + // If this is not empty, the description is a reference to an existing + // changeset and the rest of these fields are empty. + ExternalID string `json:"externalID,omitempty"` + + BaseRev string `json:"baseRev,omitempty"` + BaseRef string `json:"baseRef,omitempty"` + + // HeadRepository is the GraphQL ID of the head repository. + HeadRepository string `json:"headRepository,omitempty"` + HeadRef string `json:"headRef,omitempty"` + + Title string `json:"title,omitempty"` + Body string `json:"body,omitempty"` + Fork *bool `json:"fork,omitempty"` + + Commits []GitCommitDescription `json:"commits,omitempty"` + + Published PublishedValue `json:"published"` +} + +// MarshalJSON overwrites the default behavior of the json lib while unmarshalling +// a *ChangesetSpec. We explicitly only set Published, when it's non-nil. Due to +// it not being a pointer, omitempty does nothing. That causes it to fail schema +// validation. +// TODO: This is the easiest workaround for now, without risking breaking anything +// right before the release. Ideally, we split up this type into two separate ones +// in the future. +// See https://github.com/sourcegraph/sourcegraph-public-snapshot/issues/25968. +func (c *ChangesetSpec) MarshalJSON() ([]byte, error) { + v := struct { + BaseRepository string `json:"baseRepository,omitempty"` + ExternalID string `json:"externalID,omitempty"` + BaseRev string `json:"baseRev,omitempty"` + BaseRef string `json:"baseRef,omitempty"` + HeadRepository string `json:"headRepository,omitempty"` + HeadRef string `json:"headRef,omitempty"` + Title string `json:"title,omitempty"` + Body string `json:"body,omitempty"` + Commits []GitCommitDescription `json:"commits,omitempty"` + Published *PublishedValue `json:"published,omitempty"` + Fork *bool `json:"fork,omitempty"` + }{ + BaseRepository: c.BaseRepository, + ExternalID: c.ExternalID, + BaseRev: c.BaseRev, + BaseRef: c.BaseRef, + HeadRepository: c.HeadRepository, + HeadRef: c.HeadRef, + Title: c.Title, + Body: c.Body, + Commits: c.Commits, + Fork: c.Fork, + } + if !c.Published.Nil() { + v.Published = &c.Published + } + return json.Marshal(&v) +} + +type GitCommitDescription struct { + Version int `json:"version,omitempty"` + Message string `json:"message,omitempty"` + Diff []byte `json:"diff,omitempty"` + AuthorName string `json:"authorName,omitempty"` + AuthorEmail string `json:"authorEmail,omitempty"` +} + +func (a GitCommitDescription) MarshalJSON() ([]byte, error) { + if a.Version == 2 { + return json.Marshal(v2GitCommitDescription(a)) + } + return json.Marshal(v1GitCommitDescription{ + Message: a.Message, + Diff: string(a.Diff), + AuthorName: a.AuthorName, + AuthorEmail: a.AuthorEmail, + }) +} + +func (a *GitCommitDescription) UnmarshalJSON(data []byte) error { + var version versionGitCommitDescription + if err := json.Unmarshal(data, &version); err != nil { + return err + } + if version.Version == 2 { + var v2 v2GitCommitDescription + if err := json.Unmarshal(data, &v2); err != nil { + return err + } + a.Version = v2.Version + a.Message = v2.Message + a.Diff = v2.Diff + a.AuthorName = v2.AuthorName + a.AuthorEmail = v2.AuthorEmail + return nil + } + var v1 v1GitCommitDescription + if err := json.Unmarshal(data, &v1); err != nil { + return err + } + a.Message = v1.Message + a.Diff = []byte(v1.Diff) + a.AuthorName = v1.AuthorName + a.AuthorEmail = v1.AuthorEmail + return nil +} + +type versionGitCommitDescription struct { + Version int `json:"version,omitempty"` +} + +type v2GitCommitDescription struct { + Version int `json:"version,omitempty"` + Message string `json:"message,omitempty"` + Diff []byte `json:"diff,omitempty"` + AuthorName string `json:"authorName,omitempty"` + AuthorEmail string `json:"authorEmail,omitempty"` +} + +type v1GitCommitDescription struct { + Message string `json:"message,omitempty"` + Diff string `json:"diff,omitempty"` + AuthorName string `json:"authorName,omitempty"` + AuthorEmail string `json:"authorEmail,omitempty"` +} + +// Type returns the ChangesetSpecDescriptionType of the ChangesetSpecDescription. +func (d *ChangesetSpec) Type() ChangesetSpecDescriptionType { + if d.ExternalID != "" { + return ChangesetSpecDescriptionTypeExisting + } + return ChangesetSpecDescriptionTypeBranch +} + +// IsImportingExisting returns whether the description is of type +// ChangesetSpecDescriptionTypeExisting. +func (d *ChangesetSpec) IsImportingExisting() bool { + return d.Type() == ChangesetSpecDescriptionTypeExisting +} + +// IsBranch returns whether the description is of type +// ChangesetSpecDescriptionTypeBranch. +func (d *ChangesetSpec) IsBranch() bool { + return d.Type() == ChangesetSpecDescriptionTypeBranch +} + +// ChangesetSpecDescriptionType tells the consumer what the type of a +// ChangesetSpecDescription is without having to look into the description. +// Useful in the GraphQL when a HiddenChangesetSpec is returned. +type ChangesetSpecDescriptionType string + +// Valid ChangesetSpecDescriptionTypes kinds +const ( + ChangesetSpecDescriptionTypeExisting ChangesetSpecDescriptionType = "EXISTING" + ChangesetSpecDescriptionTypeBranch ChangesetSpecDescriptionType = "BRANCH" +) + +// ErrNoCommits is returned by (*ChangesetSpecDescription).Diff if the +// description doesn't have any commits descriptions. +var ErrNoCommits = errors.New("changeset description doesn't contain commit descriptions") + +// Diff returns the Diff of the first GitCommitDescription in Commits. If the +// ChangesetSpecDescription doesn't have Commits it returns ErrNoCommits. +// +// We currently only support a single commit in Commits. Once we support more, +// this method will need to be revisited. +func (d *ChangesetSpec) Diff() ([]byte, error) { + if len(d.Commits) == 0 { + return nil, ErrNoCommits + } + return d.Commits[0].Diff, nil +} + +// CommitMessage returns the Message of the first GitCommitDescription in Commits. If the +// ChangesetSpecDescription doesn't have Commits it returns ErrNoCommits. +// +// We currently only support a single commit in Commits. Once we support more, +// this method will need to be revisited. +func (d *ChangesetSpec) CommitMessage() (string, error) { + if len(d.Commits) == 0 { + return "", ErrNoCommits + } + return d.Commits[0].Message, nil +} + +// AuthorName returns the author name of the first GitCommitDescription in Commits. If the +// ChangesetSpecDescription doesn't have Commits it returns ErrNoCommits. +// +// We currently only support a single commit in Commits. Once we support more, +// this method will need to be revisited. +func (d *ChangesetSpec) AuthorName() (string, error) { + if len(d.Commits) == 0 { + return "", ErrNoCommits + } + return d.Commits[0].AuthorName, nil +} + +// AuthorEmail returns the author email of the first GitCommitDescription in Commits. If the +// ChangesetSpecDescription doesn't have Commits it returns ErrNoCommits. +// +// We currently only support a single commit in Commits. Once we support more, +// this method will need to be revisited. +func (d *ChangesetSpec) AuthorEmail() (string, error) { + if len(d.Commits) == 0 { + return "", ErrNoCommits + } + return d.Commits[0].AuthorEmail, nil +} diff --git a/lib/batches/changeset_specs.go b/lib/batches/changeset_specs.go new file mode 100644 index 0000000000..a3758d09a6 --- /dev/null +++ b/lib/batches/changeset_specs.go @@ -0,0 +1,310 @@ +package batches + +import ( + "context" + "strings" + + godiff "github.com/sourcegraph/go-diff/diff" + + "github.com/sourcegraph/sourcegraph/lib/batches/execution" + "github.com/sourcegraph/sourcegraph/lib/batches/git" + "github.com/sourcegraph/sourcegraph/lib/batches/template" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// Repository is a repository in which the steps of a batch spec are executed. +// +// It is part of the cache.ExecutionKey, so changes to the names of fields here +// will lead to cache busts. +type Repository struct { + ID string + Name string + BaseRef string + BaseRev string + FileMatches []string +} + +type ChangesetSpecInput struct { + Repository Repository + + BatchChangeAttributes *template.BatchChangeAttributes `json:"-"` + Template *ChangesetTemplate `json:"-"` + TransformChanges *TransformChanges `json:"-"` + Path string + + Result execution.AfterStepResult +} + +type ChangesetSpecAuthor struct { + Name string + Email string +} + +func BuildChangesetSpecs(input *ChangesetSpecInput, binaryDiffs bool, fallbackAuthor *ChangesetSpecAuthor) ([]*ChangesetSpec, error) { + tmplCtx := &template.ChangesetTemplateContext{ + BatchChangeAttributes: *input.BatchChangeAttributes, + Steps: template.StepsContext{ + Changes: input.Result.ChangedFiles, + Path: input.Path, + }, + Outputs: input.Result.Outputs, + Repository: template.Repository{ + Name: input.Repository.Name, + Branch: strings.TrimPrefix(input.Repository.BaseRef, "refs/heads/"), + FileMatches: input.Repository.FileMatches, + }, + } + + var author ChangesetSpecAuthor + + if input.Template.Commit.Author == nil { + if fallbackAuthor != nil { + author = *fallbackAuthor + } else { + // user did not provide author info, so use defaults + author = ChangesetSpecAuthor{ + Name: "Sourcegraph", + Email: "batch-changes@sourcegraph.com", + } + } + } else { + var err error + author.Name, err = template.RenderChangesetTemplateField("authorName", input.Template.Commit.Author.Name, tmplCtx) + if err != nil { + return nil, err + } + author.Email, err = template.RenderChangesetTemplateField("authorEmail", input.Template.Commit.Author.Email, tmplCtx) + if err != nil { + return nil, err + } + } + + title, err := template.RenderChangesetTemplateField("title", input.Template.Title, tmplCtx) + if err != nil { + return nil, err + } + + body, err := template.RenderChangesetTemplateField("body", input.Template.Body, tmplCtx) + if err != nil { + return nil, err + } + + message, err := template.RenderChangesetTemplateField("message", input.Template.Commit.Message, tmplCtx) + if err != nil { + return nil, err + } + + // TODO: As a next step, we should extend the ChangesetTemplateContext to also include + // TransformChanges.Group and then change validateGroups and groupFileDiffs to, for each group, + // render the branch name *before* grouping the diffs. + defaultBranch, err := template.RenderChangesetTemplateField("branch", input.Template.Branch, tmplCtx) + if err != nil { + return nil, err + } + + newSpec := func(branch string, diff []byte) *ChangesetSpec { + var published any = nil + if input.Template.Published != nil { + published = input.Template.Published.ValueWithSuffix(input.Repository.Name, branch) + } + + fork := input.Template.Fork + + version := 1 + if binaryDiffs { + version = 2 + } + + return &ChangesetSpec{ + BaseRepository: input.Repository.ID, + HeadRepository: input.Repository.ID, + BaseRef: input.Repository.BaseRef, + BaseRev: input.Repository.BaseRev, + + HeadRef: git.EnsureRefPrefix(branch), + Title: title, + Body: body, + Fork: fork, + Commits: []GitCommitDescription{ + { + Version: version, + Message: message, + AuthorName: author.Name, + AuthorEmail: author.Email, + Diff: diff, + }, + }, + Published: PublishedValue{Val: published}, + } + } + + var specs []*ChangesetSpec + + groups := groupsForRepository(input.Repository.Name, input.TransformChanges) + if len(groups) != 0 { + err := validateGroups(input.Repository.Name, input.Template.Branch, groups) + if err != nil { + return specs, err + } + + // TODO: Regarding 'defaultBranch', see comment above + diffsByBranch, err := groupFileDiffs(input.Result.Diff, defaultBranch, groups) + if err != nil { + return specs, errors.Wrap(err, "grouping diffs failed") + } + + for branch, diff := range diffsByBranch { + spec := newSpec(branch, diff) + specs = append(specs, spec) + } + } else { + spec := newSpec(defaultBranch, input.Result.Diff) + specs = append(specs, spec) + } + + return specs, nil +} + +type RepoFetcher func(context.Context, []string) (map[string]string, error) + +func BuildImportChangesetSpecs(ctx context.Context, importChangesets []ImportChangeset, repoFetcher RepoFetcher) (specs []*ChangesetSpec, errs error) { + if len(importChangesets) == 0 { + return nil, nil + } + + var repoNames []string + for _, ic := range importChangesets { + repoNames = append(repoNames, ic.Repository) + } + + repoNameIDs, err := repoFetcher(ctx, repoNames) + if err != nil { + return nil, err + } + + for _, ic := range importChangesets { + repoID, ok := repoNameIDs[ic.Repository] + if !ok { + errs = errors.Append(errs, errors.Newf("repository %q not found", ic.Repository)) + continue + } + for _, id := range ic.ExternalIDs { + extID, err := ParseChangesetSpecExternalID(id) + if err != nil { + errs = errors.Append(errs, err) + continue + } + specs = append(specs, &ChangesetSpec{ + BaseRepository: repoID, + ExternalID: extID, + }) + } + } + + return specs, errs +} + +func groupsForRepository(repoName string, transform *TransformChanges) []Group { + groups := []Group{} + + if transform == nil { + return groups + } + + for _, g := range transform.Group { + if g.Repository != "" { + if g.Repository == repoName { + groups = append(groups, g) + } + } else { + groups = append(groups, g) + } + } + + return groups +} + +func validateGroups(repoName, defaultBranch string, groups []Group) error { + uniqueBranches := make(map[string]struct{}, len(groups)) + + for _, g := range groups { + if _, ok := uniqueBranches[g.Branch]; ok { + return NewValidationError(errors.Newf("transformChanges would lead to multiple changesets in repository %s to have the same branch %q", repoName, g.Branch)) + } else { + uniqueBranches[g.Branch] = struct{}{} + } + + if g.Branch == defaultBranch { + return NewValidationError(errors.Newf("transformChanges group branch for repository %s is the same as branch %q in changesetTemplate", repoName, defaultBranch)) + } + } + + return nil +} + +func groupFileDiffs(completeDiff []byte, defaultBranch string, groups []Group) (map[string][]byte, error) { + fileDiffs, err := godiff.ParseMultiFileDiff(completeDiff) + if err != nil { + return nil, err + } + + // Housekeeping: we setup these two datastructures so we can + // - access the group.Branch by the directory for which they should be used + // - check against the given directories, in order. + branchesByDirectory := make(map[string]string, len(groups)) + dirs := make([]string, 0, len(groups)) + for _, g := range groups { + branchesByDirectory[g.Directory] = g.Branch + dirs = append(dirs, g.Directory) + } + + byBranch := make(map[string][]*godiff.FileDiff, len(groups)) + byBranch[defaultBranch] = []*godiff.FileDiff{} + + // For each file diff... + for _, f := range fileDiffs { + // strip git's b/ or a/ prefix off paths + name := strings.TrimPrefix(f.NewName, "b/") + if name == "/dev/null" { + name = strings.TrimPrefix(f.OrigName, "a/") + } + + // .. we check whether it matches one of the given directories in the + // group transformations, with the last match winning: + var matchingDir string + for _, d := range dirs { + // We ignore the ~ . and ./ prefixes so that we can support relative and absolute paths. + dWithoutPrefix := strings.TrimLeft(d, "~./") + // We have to add a trailing suffix so that we don't match files that have the same name as other directories. + dWithDirSuffix := dWithoutPrefix + "/" + if strings.HasPrefix(name, dWithDirSuffix) { + matchingDir = d + } + } + + // If the diff didn't match a rule, it goes into the default branch and + // the default changeset. + if matchingDir == "" { + byBranch[defaultBranch] = append(byBranch[defaultBranch], f) + continue + } + + // If it *did* match a directory, we look up which branch we should use: + branch, ok := branchesByDirectory[matchingDir] + if !ok { + panic("this should not happen: " + matchingDir) + } + + byBranch[branch] = append(byBranch[branch], f) + } + + finalDiffsByBranch := make(map[string][]byte, len(byBranch)) + for branch, diffs := range byBranch { + printed, err := godiff.PrintMultiFileDiff(diffs) + if err != nil { + return nil, errors.Wrap(err, "printing multi file diff failed") + } + finalDiffsByBranch[branch] = printed + } + return finalDiffsByBranch, nil +} diff --git a/lib/batches/env/env.go b/lib/batches/env/env.go new file mode 100644 index 0000000000..89e60619f1 --- /dev/null +++ b/lib/batches/env/env.go @@ -0,0 +1,170 @@ +// Package env provides types to handle step environments in batch specs. +package env + +import ( + "encoding/json" + "strings" + + "github.com/google/go-cmp/cmp" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// Environment represents an environment used for a batch step, which may +// require values to be resolved from the outer environment the executor is +// running within. +type Environment struct { + vars []variable +} + +// MarshalJSON marshals the environment. +func (e Environment) MarshalJSON() ([]byte, error) { + if e.vars == nil { + return []byte(`{}`), nil + } + + // For compatibility with older versions of Sourcegraph, if all environment + // variables have static values defined, we'll encode to the object variant. + if e.IsStatic() { + vars := make(map[string]string, len(e.vars)) + for _, v := range e.vars { + vars[v.name] = *v.value + } + + return json.Marshal(vars) + } + + // Otherwise, we have to return the array variant. + return json.Marshal(e.vars) +} + +// UnmarshalJSON unmarshals an environment from one of the two supported JSON +// forms: an array, or a string→string object. +func (e *Environment) UnmarshalJSON(data []byte) error { + // data is either an array or object. (Or invalid.) Let's start by trying to + // unmarshal it as an array. + if err := json.Unmarshal(data, &e.vars); err == nil { + return nil + } + + // It's an object, then. We need to put it into a map, then convert it into + // an array of variables. + kv := make(map[string]string) + if err := json.Unmarshal(data, &kv); err != nil { + return err + } + + e.vars = make([]variable, len(kv)) + i := 0 + for k, v := range kv { + copy := v + e.vars[i].name = k + e.vars[i].value = © + i++ + } + + return nil +} + +// UnmarshalYAML unmarshals an environment from one of the two supported YAML +// forms: an array, or a string→string object. +func (e *Environment) UnmarshalYAML(unmarshal func(any) error) error { + // data is either an array or object. (Or invalid.) Let's start by trying to + // unmarshal it as an array. + if err := unmarshal(&e.vars); err == nil { + return nil + } + + // It's an object, then. As above, we need to convert this via a map. + kv := make(map[string]string) + if err := unmarshal(&kv); err != nil { + return err + } + + e.vars = make([]variable, len(kv)) + i := 0 + for k, v := range kv { + copy := v + e.vars[i].name = k + e.vars[i].value = © + i++ + } + + return nil +} + +// IsStatic returns true if the environment doesn't depend on any outer +// environment variables. +// +// Put another way: if this function returns true, then Resolve() will always +// return the same map for the environment. +func (e Environment) IsStatic() bool { + for _, v := range e.vars { + if v.value == nil { + return false + } + } + return true +} + +// OuterVars returns the list of environment variables that depend on any +// environment variable defined in the global env. +func (e Environment) OuterVars() []string { + outer := []string{} + for _, v := range e.vars { + if v.value == nil { + outer = append(outer, v.name) + } + } + return outer +} + +// Resolve resolves the environment, using values from the given outer +// environment to fill in environment values as needed. If an environment +// variable doesn't exist in the outer environment, then an empty string will be +// used as the value. +// +// outer must be an array of strings in the form `KEY=VALUE`. Generally +// speaking, this will be the return value from os.Environ(). +func (e Environment) Resolve(outer []string) (map[string]string, error) { + // Convert the given outer environment into a map. + omap := make(map[string]string, len(outer)) + for _, v := range outer { + kv := strings.SplitN(v, "=", 2) + if len(kv) != 2 { + return nil, errors.Errorf("unable to parse environment variable %q", v) + } + omap[kv[0]] = kv[1] + } + + // Now we can iterate over our own environment and fill in the missing + // values. + resolved := make(map[string]string, len(e.vars)) + for _, v := range e.vars { + if v.value == nil { + // We don't bother checking if v.name exists in omap here because + // the default behaviour is what we want anyway: we'll get an empty + // string (since that's the zero value for a string), and that is + // the desired outcome if the environment variable isn't set. + resolved[v.name] = omap[v.name] + } else { + resolved[v.name] = *v.value + } + } + + return resolved, nil +} + +// Equal verifies if two environments are equal. +func (e Environment) Equal(other Environment) bool { + return cmp.Equal(e.mapify(), other.mapify()) +} + +func (e Environment) mapify() map[string]*string { + m := make(map[string]*string, len(e.vars)) + for _, v := range e.vars { + m[v.name] = v.value + } + + return m +} diff --git a/lib/batches/env/var.go b/lib/batches/env/var.go new file mode 100644 index 0000000000..1e63c00662 --- /dev/null +++ b/lib/batches/env/var.go @@ -0,0 +1,99 @@ +package env + +import ( + "encoding/json" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// variable is an individual environment variable within an Environment +// instance. If the value is nil, then it needs to be resolved before being +// used, which occurs in Environment.Resolve(). +type variable struct { + name string + value *string +} + +var errInvalidVariableType = errors.New("invalid environment variable: unknown type") + +type errInvalidVariableObject struct{ n int } + +func (e errInvalidVariableObject) Error() string { + return fmt.Sprintf("invalid environment variable: incorrect number of object elements (expected 1, got %d)", e.n) +} + +func (v variable) MarshalJSON() ([]byte, error) { + if v.value != nil { + return json.Marshal(map[string]string{v.name: *v.value}) + } + + return json.Marshal(v.name) +} + +func (v *variable) UnmarshalJSON(data []byte) error { + // This can be a string or an object with one property. Let's try the string + // case first. + var k string + if err := json.Unmarshal(data, &k); err == nil { + v.name = k + v.value = nil + return nil + } + + // We should have a bouncing baby object, then. + var kv map[string]string + if err := json.Unmarshal(data, &kv); err != nil { + return errInvalidVariableType + } else if len(kv) != 1 { + return errInvalidVariableObject{n: len(kv)} + } + + for k, value := range kv { + v.name = k + v.value = &value + } + + return nil +} + +func (v *variable) UnmarshalYAML(unmarshal func(any) error) error { + // This can be a string or an object with one property. Let's try the string + // case first. + var k string + if err := unmarshal(&k); err == nil { + v.name = k + v.value = nil + return nil + } + + // Object time. + var kv map[string]string + if err := unmarshal(&kv); err != nil { + return errInvalidVariableType + } else if len(kv) != 1 { + return errInvalidVariableObject{n: len(kv)} + } + + for k, value := range kv { + v.name = k + v.value = &value + } + + return nil +} + +// Equal checks if two environment variables are equal. +func (a variable) Equal(b variable) bool { + if a.name != b.name { + return false + } + + if a.value == nil && b.value == nil { + return true + } + if a.value == nil || b.value == nil { + return false + } + return *a.value == *b.value +} diff --git a/lib/batches/execution/cache/cache.go b/lib/batches/execution/cache/cache.go new file mode 100644 index 0000000000..9392cd2c31 --- /dev/null +++ b/lib/batches/execution/cache/cache.go @@ -0,0 +1,174 @@ +package cache + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/sourcegraph/sourcegraph/lib/batches" + "github.com/sourcegraph/sourcegraph/lib/batches/execution" + "github.com/sourcegraph/sourcegraph/lib/batches/template" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type Cache interface { + Get(ctx context.Context, key Keyer) (result execution.AfterStepResult, found bool, err error) + Set(ctx context.Context, key Keyer, result execution.AfterStepResult) error + + Clear(ctx context.Context, key Keyer) error +} + +type Keyer interface { + Key() (string, error) + Slug() string +} + +// MetadataRetriever retrieves mount metadata. +type MetadataRetriever interface { + // Get returns the mount metadata from the provided steps. + Get([]batches.Step) ([]MountMetadata, error) +} + +// MountMetadata is the metadata of a file that is mounted by a Step. +type MountMetadata struct { + Path string + Size int64 + Modified time.Time +} + +func (key CacheKey) mountsMetadata() ([]MountMetadata, error) { + if key.MetadataRetriever != nil { + return key.MetadataRetriever.Get(key.Steps) + } + return nil, nil +} + +// resolveStepsEnvironment returns a slice of environments for each of the steps, +// containing only the env vars that are actually used. +func resolveStepsEnvironment(globalEnv []string, steps []batches.Step) ([]map[string]string, error) { + // We have to resolve the step environments and include them in the cache + // key to ensure that the cache is properly invalidated when an environment + // variable changes. + // + // Note that we don't base the cache key on the entire global environment: + // if an unrelated environment variable changes, that's fine. We're only + // interested in the ones that actually make it into the step container. + envs := make([]map[string]string, len(steps)) + for i, step := range steps { + // TODO: This should also render templates inside env vars. + env, err := step.Env.Resolve(globalEnv) + if err != nil { + return nil, errors.Wrapf(err, "resolving environment for step %d", i) + } + envs[i] = env + } + return envs, nil +} + +func marshalAndHash(key *CacheKey, envs []map[string]string, metadata []MountMetadata) (string, error) { + raw, err := json.Marshal(struct { + *CacheKey + Environments []map[string]string + // Omit if empty to be backwards compatible. + MountsMetadata []MountMetadata `json:"MountsMetadata,omitempty"` + }{ + CacheKey: key, + Environments: envs, + MountsMetadata: metadata, + }) + if err != nil { + return "", err + } + + hash := sha256.Sum256(raw) + return base64.RawURLEncoding.EncodeToString(hash[:16]), nil +} + +// CacheKey implements the Keyer interface for a batch spec execution in a +// repository workspace and a *subset* of its Steps, up to and including the +// step with index StepIndex in Task.Steps. +type CacheKey struct { + Repository batches.Repository + Path string + OnlyFetchWorkspace bool + Steps []batches.Step + BatchChangeAttributes *template.BatchChangeAttributes + + // Ignore from serialization. + MetadataRetriever MetadataRetriever `json:"-"` + // Ignore from serialization. + GlobalEnv []string `json:"-"` + + StepIndex int +} + +// Key converts the key into a string form that can be used to uniquely identify +// the cache key in a more concise form than the entire Task. +func (key CacheKey) Key() (string, error) { + // Setup a copy of the cache key that only includes the Steps up to and + // including key.StepIndex. + clone := key + clone.Steps = key.Steps[0 : key.StepIndex+1] + + // Resolve environment only for the subset of Steps. + envs, err := resolveStepsEnvironment(key.GlobalEnv, clone.Steps) + if err != nil { + return "", err + } + metadata, err := key.mountsMetadata() + if err != nil { + return "", err + } + + hash, err := marshalAndHash(&clone, envs, metadata) + if err != nil { + return "", err + } + return fmt.Sprintf("%s-step-%d", hash, key.StepIndex), err +} + +func (key CacheKey) Slug() string { + return SlugForRepo(key.Repository.Name, key.Repository.BaseRev) +} + +func KeyForWorkspace(batchChangeAttributes *template.BatchChangeAttributes, r batches.Repository, path string, globalEnv []string, onlyFetchWorkspace bool, steps []batches.Step, stepIndex int, retriever MetadataRetriever) Keyer { + sort.Strings(r.FileMatches) + + return CacheKey{ + Repository: r, + Path: path, + OnlyFetchWorkspace: onlyFetchWorkspace, + GlobalEnv: globalEnv, + Steps: steps, + BatchChangeAttributes: batchChangeAttributes, + StepIndex: stepIndex, + MetadataRetriever: retriever, + } +} + +// ChangesetSpecsFromCache takes the execution.Result and generates all changeset specs from it. +func ChangesetSpecsFromCache(spec *batches.BatchSpec, r batches.Repository, result execution.AfterStepResult, path string, binaryDiffs bool, fallbackAuthor *batches.ChangesetSpecAuthor) ([]*batches.ChangesetSpec, error) { + if len(result.Diff) == 0 { + return []*batches.ChangesetSpec{}, nil + } + + sort.Strings(r.FileMatches) + + input := &batches.ChangesetSpecInput{ + Repository: r, + BatchChangeAttributes: &template.BatchChangeAttributes{ + Name: spec.Name, + Description: spec.Description, + }, + Template: spec.ChangesetTemplate, + TransformChanges: spec.TransformChanges, + Result: result, + Path: path, + } + + return batches.BuildChangesetSpecs(input, binaryDiffs, fallbackAuthor) +} diff --git a/lib/batches/execution/cache/util.go b/lib/batches/execution/cache/util.go new file mode 100644 index 0000000000..bc4cf8436e --- /dev/null +++ b/lib/batches/execution/cache/util.go @@ -0,0 +1,7 @@ +package cache + +import "strings" + +func SlugForRepo(repoName, commit string) string { + return strings.ReplaceAll(repoName, "/", "-") + "-" + commit +} diff --git a/lib/batches/execution/results.go b/lib/batches/execution/results.go new file mode 100644 index 0000000000..4426430708 --- /dev/null +++ b/lib/batches/execution/results.go @@ -0,0 +1,98 @@ +package execution + +import ( + "encoding/json" + + "github.com/sourcegraph/sourcegraph/lib/batches/git" +) + +// AfterStepResult is the execution result after executing a step with the given +// index in Steps. +type AfterStepResult struct { + Version int `json:"version"` + // Files are the changes made to Files by the step. + ChangedFiles git.Changes `json:"changedFiles"` + // Stdout is the output produced by the step on standard out. + Stdout string `json:"stdout"` + // Stderr is the output produced by the step on standard error. + Stderr string `json:"stderr"` + // StepIndex is the index of the step in the list of steps. + StepIndex int `json:"stepIndex"` + // Diff is the cumulative `git diff` after executing the Step. + Diff []byte `json:"diff"` + // Outputs is a copy of the Outputs after executing the Step. + Outputs map[string]any `json:"outputs"` + // Skipped determines whether the step was skipped. + Skipped bool `json:"skipped"` +} + +func (a AfterStepResult) MarshalJSON() ([]byte, error) { + if a.Version == 2 { + return json.Marshal(v2AfterStepResult(a)) + } + return json.Marshal(v1AfterStepResult{ + ChangedFiles: a.ChangedFiles, + Stdout: a.Stdout, + Stderr: a.Stderr, + StepIndex: a.StepIndex, + Diff: string(a.Diff), + Outputs: a.Outputs, + }) +} + +func (a *AfterStepResult) UnmarshalJSON(data []byte) error { + var version versionAfterStepResult + if err := json.Unmarshal(data, &version); err != nil { + return err + } + if version.Version == 2 { + var v2 v2AfterStepResult + if err := json.Unmarshal(data, &v2); err != nil { + return err + } + a.Version = v2.Version + a.ChangedFiles = v2.ChangedFiles + a.Stdout = v2.Stdout + a.Stderr = v2.Stderr + a.StepIndex = v2.StepIndex + a.Diff = v2.Diff + a.Outputs = v2.Outputs + a.Skipped = v2.Skipped + return nil + } + var v1 v1AfterStepResult + if err := json.Unmarshal(data, &v1); err != nil { + return err + } + a.ChangedFiles = v1.ChangedFiles + a.Stdout = v1.Stdout + a.Stderr = v1.Stderr + a.StepIndex = v1.StepIndex + a.Diff = []byte(v1.Diff) + a.Outputs = v1.Outputs + return nil +} + +type versionAfterStepResult struct { + Version int `json:"version"` +} + +type v2AfterStepResult struct { + Version int `json:"version"` + ChangedFiles git.Changes `json:"changedFiles"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + StepIndex int `json:"stepIndex"` + Diff []byte `json:"diff"` + Outputs map[string]any `json:"outputs"` + Skipped bool `json:"skipped"` +} + +type v1AfterStepResult struct { + ChangedFiles git.Changes `json:"changedFiles"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + StepIndex int `json:"stepIndex"` + Diff string `json:"diff"` + Outputs map[string]any `json:"outputs"` +} diff --git a/lib/batches/git/changes.go b/lib/batches/git/changes.go new file mode 100644 index 0000000000..7da33db848 --- /dev/null +++ b/lib/batches/git/changes.go @@ -0,0 +1,37 @@ +package git + +import ( + "github.com/sourcegraph/go-diff/diff" +) + +// Changes are the changes made to files in a repository. +type Changes struct { + Modified []string `json:"modified"` + Added []string `json:"added"` + Deleted []string `json:"deleted"` + Renamed []string `json:"renamed"` +} + +func ChangesInDiff(rawDiff []byte) (Changes, error) { + result := Changes{} + + fileDiffs, err := diff.ParseMultiFileDiff(rawDiff) + if err != nil { + return result, err + } + + for _, fd := range fileDiffs { + switch { + case fd.NewName == "/dev/null": + result.Deleted = append(result.Deleted, fd.OrigName) + case fd.OrigName == "/dev/null": + result.Added = append(result.Added, fd.NewName) + case fd.OrigName == fd.NewName: + result.Modified = append(result.Modified, fd.OrigName) + case fd.OrigName != fd.NewName: + result.Renamed = append(result.Renamed, fd.NewName) + } + } + + return result, nil +} diff --git a/lib/batches/git/refs.go b/lib/batches/git/refs.go new file mode 100644 index 0000000000..7e840c4b29 --- /dev/null +++ b/lib/batches/git/refs.go @@ -0,0 +1,10 @@ +package git + +import "strings" + +func EnsureRefPrefix(ref string) string { + if strings.HasPrefix(ref, "refs/heads/") { + return ref + } + return "refs/heads/" + ref +} diff --git a/lib/batches/json/validate.go b/lib/batches/json/validate.go new file mode 100644 index 0000000000..9e38b6f108 --- /dev/null +++ b/lib/batches/json/validate.go @@ -0,0 +1,24 @@ +package json + +import ( + "encoding/json" + + "github.com/sourcegraph/sourcegraph/lib/batches/jsonschema" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// UnmarshalValidate validates the JSON input against the provided JSON schema. +// If the validation is successful the validated input is unmarshalled into the +// target. +func UnmarshalValidate(schema string, input []byte, target any) error { + var errs error + if err := jsonschema.Validate(schema, input); err != nil { + errs = errors.Append(errs, err) + } + + if err := json.Unmarshal(input, target); err != nil { + errs = errors.Append(errs, err) + } + + return errs +} diff --git a/lib/batches/json_logs.go b/lib/batches/json_logs.go new file mode 100644 index 0000000000..03477d038c --- /dev/null +++ b/lib/batches/json_logs.go @@ -0,0 +1,334 @@ +package batches + +import ( + "encoding/json" + "time" + + "github.com/sourcegraph/sourcegraph/lib/batches/execution" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type LogEvent struct { + Operation LogEventOperation `json:"operation"` + + Timestamp time.Time `json:"timestamp"` + + Status LogEventStatus `json:"status"` + Metadata any `json:"metadata,omitempty"` +} + +type logEventJSON struct { + Operation LogEventOperation `json:"operation"` + Timestamp time.Time `json:"timestamp"` + Status LogEventStatus `json:"status"` +} + +func (l *LogEvent) UnmarshalJSON(data []byte) error { + var j *logEventJSON + if err := json.Unmarshal(data, &j); err != nil { + return err + } + l.Operation = j.Operation + l.Timestamp = j.Timestamp + l.Status = j.Status + + switch l.Operation { + case LogEventOperationParsingBatchSpec: + l.Metadata = new(ParsingBatchSpecMetadata) + case LogEventOperationResolvingNamespace: + l.Metadata = new(ResolvingNamespaceMetadata) + case LogEventOperationPreparingDockerImages: + l.Metadata = new(PreparingDockerImagesMetadata) + case LogEventOperationDeterminingWorkspaceType: + l.Metadata = new(DeterminingWorkspaceTypeMetadata) + case LogEventOperationDeterminingWorkspaces: + l.Metadata = new(DeterminingWorkspacesMetadata) + case LogEventOperationCheckingCache: + l.Metadata = new(CheckingCacheMetadata) + case LogEventOperationExecutingTasks: + l.Metadata = new(ExecutingTasksMetadata) + case LogEventOperationLogFileKept: + l.Metadata = new(LogFileKeptMetadata) + case LogEventOperationUploadingChangesetSpecs: + l.Metadata = new(UploadingChangesetSpecsMetadata) + case LogEventOperationCreatingBatchSpec: + l.Metadata = new(CreatingBatchSpecMetadata) + case LogEventOperationApplyingBatchSpec: + l.Metadata = new(ApplyingBatchSpecMetadata) + case LogEventOperationBatchSpecExecution: + l.Metadata = new(BatchSpecExecutionMetadata) + case LogEventOperationExecutingTask: + l.Metadata = new(ExecutingTaskMetadata) + case LogEventOperationTaskBuildChangesetSpecs: + l.Metadata = new(TaskBuildChangesetSpecsMetadata) + case LogEventOperationTaskSkippingSteps: + l.Metadata = new(TaskSkippingStepsMetadata) + case LogEventOperationTaskStepSkipped: + l.Metadata = new(TaskStepSkippedMetadata) + case LogEventOperationTaskPreparingStep: + l.Metadata = new(TaskPreparingStepMetadata) + case LogEventOperationTaskStep: + l.Metadata = new(TaskStepMetadata) + case LogEventOperationCacheAfterStepResult: + l.Metadata = new(CacheAfterStepResultMetadata) + case LogEventOperationDockerWatchDog: + l.Metadata = new(DockerWatchDogMetadata) + default: + return errors.Newf("invalid event type %s", l.Operation) + } + + wrapper := struct { + Metadata any `json:"metadata"` + }{ + Metadata: l.Metadata, + } + + return json.Unmarshal(data, &wrapper) +} + +type LogEventOperation string + +const ( + LogEventOperationParsingBatchSpec LogEventOperation = "PARSING_BATCH_SPEC" + LogEventOperationResolvingNamespace LogEventOperation = "RESOLVING_NAMESPACE" + LogEventOperationPreparingDockerImages LogEventOperation = "PREPARING_DOCKER_IMAGES" + LogEventOperationDeterminingWorkspaceType LogEventOperation = "DETERMINING_WORKSPACE_TYPE" + LogEventOperationDeterminingWorkspaces LogEventOperation = "DETERMINING_WORKSPACES" + LogEventOperationCheckingCache LogEventOperation = "CHECKING_CACHE" + LogEventOperationExecutingTasks LogEventOperation = "EXECUTING_TASKS" + LogEventOperationLogFileKept LogEventOperation = "LOG_FILE_KEPT" + LogEventOperationUploadingChangesetSpecs LogEventOperation = "UPLOADING_CHANGESET_SPECS" + LogEventOperationCreatingBatchSpec LogEventOperation = "CREATING_BATCH_SPEC" + LogEventOperationApplyingBatchSpec LogEventOperation = "APPLYING_BATCH_SPEC" + LogEventOperationBatchSpecExecution LogEventOperation = "BATCH_SPEC_EXECUTION" + LogEventOperationExecutingTask LogEventOperation = "EXECUTING_TASK" + LogEventOperationTaskBuildChangesetSpecs LogEventOperation = "TASK_BUILD_CHANGESET_SPECS" + LogEventOperationTaskSkippingSteps LogEventOperation = "TASK_SKIPPING_STEPS" + LogEventOperationTaskStepSkipped LogEventOperation = "TASK_STEP_SKIPPED" + LogEventOperationTaskPreparingStep LogEventOperation = "TASK_PREPARING_STEP" + LogEventOperationTaskStep LogEventOperation = "TASK_STEP" + LogEventOperationCacheAfterStepResult LogEventOperation = "CACHE_AFTER_STEP_RESULT" + LogEventOperationDockerWatchDog LogEventOperation = "DOCKER_WATCH_DOG" +) + +type LogEventStatus string + +const ( + LogEventStatusStarted LogEventStatus = "STARTED" + LogEventStatusSuccess LogEventStatus = "SUCCESS" + LogEventStatusFailure LogEventStatus = "FAILURE" + LogEventStatusProgress LogEventStatus = "PROGRESS" +) + +type ParsingBatchSpecMetadata struct { + Error string `json:"error,omitempty"` +} + +type ResolvingNamespaceMetadata struct { + NamespaceID string `json:"namespaceID,omitempty"` +} + +type PreparingDockerImagesMetadata struct { + Done int `json:"done,omitempty"` + Total int `json:"total,omitempty"` +} + +type DeterminingWorkspaceTypeMetadata struct { + Type string `json:"type,omitempty"` +} + +type DeterminingWorkspacesMetadata struct { + Unsupported int `json:"unsupported,omitempty"` + Ignored int `json:"ignored,omitempty"` + RepoCount int `json:"repoCount,omitempty"` + WorkspaceCount int `json:"workspaceCount,omitempty"` +} + +type CheckingCacheMetadata struct { + CachedSpecsFound int `json:"cachedSpecsFound,omitempty"` + TasksToExecute int `json:"tasksToExecute,omitempty"` +} + +type JSONLinesTask struct { + ID string `json:"id"` + Repository string `json:"repository"` + Workspace string `json:"workspace"` + Steps []Step `json:"steps"` + CachedStepResultsFound bool `json:"cachedStepResultFound"` + StartStep int `json:"startStep"` +} + +type ExecutingTasksMetadata struct { + Tasks []JSONLinesTask `json:"tasks,omitempty"` + Skipped bool `json:"skipped,omitempty"` + Error string `json:"error,omitempty"` +} + +type LogFileKeptMetadata struct { + Path string `json:"path,omitempty"` +} + +type UploadingChangesetSpecsMetadata struct { + Done int `json:"done,omitempty"` + Total int `json:"total,omitempty"` + // IDs is the slice of GraphQL IDs of the created changeset specs. + IDs []string `json:"ids,omitempty"` +} + +type CreatingBatchSpecMetadata struct { + PreviewURL string `json:"previewURL,omitempty"` +} + +type ApplyingBatchSpecMetadata struct { + BatchChangeURL string `json:"batchChangeURL,omitempty"` +} + +type BatchSpecExecutionMetadata struct { + Error string `json:"error,omitempty"` +} + +type ExecutingTaskMetadata struct { + TaskID string `json:"taskID,omitempty"` + Error string `json:"error,omitempty"` +} + +type TaskBuildChangesetSpecsMetadata struct { + TaskID string `json:"taskID,omitempty"` +} + +type TaskSkippingStepsMetadata struct { + TaskID string `json:"taskID,omitempty"` + StartStep int `json:"startStep,omitempty"` +} + +type TaskStepSkippedMetadata struct { + TaskID string `json:"taskID,omitempty"` + Step int `json:"step,omitempty"` +} + +type TaskPreparingStepMetadata struct { + TaskID string `json:"taskID,omitempty"` + Step int `json:"step,omitempty"` + Error string `json:"error,omitempty"` +} + +type TaskStepMetadata struct { + Version int + TaskID string + Step int + + RunScript string + Env map[string]string + + Out string + + Diff []byte + Outputs map[string]any + + ExitCode int + Error string +} + +func (m TaskStepMetadata) MarshalJSON() ([]byte, error) { + if m.Version == 2 { + return json.Marshal(v2TaskStepMetadata{ + Version: 2, + TaskID: m.TaskID, + Step: m.Step, + RunScript: m.RunScript, + Env: m.Env, + Out: m.Out, + Diff: m.Diff, + Outputs: m.Outputs, + ExitCode: m.ExitCode, + Error: m.Error, + }) + } + return json.Marshal(v1TaskStepMetadata{ + TaskID: m.TaskID, + Step: m.Step, + RunScript: m.RunScript, + Env: m.Env, + Out: m.Out, + Diff: string(m.Diff), + Outputs: m.Outputs, + ExitCode: m.ExitCode, + Error: m.Error, + }) +} + +func (m *TaskStepMetadata) UnmarshalJSON(data []byte) error { + var version versionTaskStepMetadata + if err := json.Unmarshal(data, &version); err != nil { + return err + } + if version.Version == 2 { + var v2 v2TaskStepMetadata + if err := json.Unmarshal(data, &v2); err != nil { + return err + } + m.Version = v2.Version + m.TaskID = v2.TaskID + m.Step = v2.Step + m.RunScript = v2.RunScript + m.Env = v2.Env + m.Out = v2.Out + m.Diff = v2.Diff + m.Outputs = v2.Outputs + m.ExitCode = v2.ExitCode + m.Error = v2.Error + return nil + } + var v1 v1TaskStepMetadata + if err := json.Unmarshal(data, &v1); err != nil { + return errors.Wrap(err, string(data)) + } + m.TaskID = v1.TaskID + m.Step = v1.Step + m.RunScript = v1.RunScript + m.Env = v1.Env + m.Out = v1.Out + m.Diff = []byte(v1.Diff) + m.Outputs = v1.Outputs + m.ExitCode = v1.ExitCode + m.Error = v1.Error + return nil +} + +type versionTaskStepMetadata struct { + Version int `json:"version,omitempty"` +} + +type v2TaskStepMetadata struct { + Version int `json:"version,omitempty"` + TaskID string `json:"taskID,omitempty"` + Step int `json:"step,omitempty"` + RunScript string `json:"runScript,omitempty"` + Env map[string]string `json:"env,omitempty"` + Out string `json:"out,omitempty"` + Diff []byte `json:"diff,omitempty"` + Outputs map[string]any `json:"outputs,omitempty"` + ExitCode int `json:"exitCode,omitempty"` + Error string `json:"error,omitempty"` +} + +type v1TaskStepMetadata struct { + TaskID string `json:"taskID,omitempty"` + Step int `json:"step,omitempty"` + RunScript string `json:"runScript,omitempty"` + Env map[string]string `json:"env,omitempty"` + Out string `json:"out,omitempty"` + Diff string `json:"diff,omitempty"` + Outputs map[string]any `json:"outputs,omitempty"` + ExitCode int `json:"exitCode,omitempty"` + Error string `json:"error,omitempty"` +} + +type CacheAfterStepResultMetadata struct { + Key string `json:"key,omitempty"` + Value execution.AfterStepResult `json:"value"` +} + +type DockerWatchDogMetadata struct { + Error string `json:"error,omitempty"` +} diff --git a/lib/batches/jsonschema/jsonschema.go b/lib/batches/jsonschema/jsonschema.go new file mode 100644 index 0000000000..28d1c10dea --- /dev/null +++ b/lib/batches/jsonschema/jsonschema.go @@ -0,0 +1,36 @@ +package jsonschema + +import ( + "strings" + + "github.com/xeipuuv/gojsonschema" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// Validate validates the given input against the JSON schema. +// +// It returns either nil, in case the input is valid, or an error. +func Validate(schema string, input []byte) error { + sl := gojsonschema.NewSchemaLoader() + sc, err := sl.Compile(gojsonschema.NewStringLoader(schema)) + if err != nil { + return errors.Wrap(err, "failed to compile JSON schema") + } + + res, err := sc.Validate(gojsonschema.NewBytesLoader(input)) + if err != nil { + return errors.Wrap(err, "failed to validate input against schema") + } + + var errs error + for _, err := range res.Errors() { + e := err.String() + // Remove `(root): ` from error formatting since these errors are + // presented to users. + e = strings.TrimPrefix(e, "(root): ") + errs = errors.Append(errs, errors.New(e)) + } + + return errs +} diff --git a/lib/batches/outputs.go b/lib/batches/outputs.go new file mode 100644 index 0000000000..03649b5018 --- /dev/null +++ b/lib/batches/outputs.go @@ -0,0 +1,48 @@ +package batches + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/batches/template" + "github.com/sourcegraph/sourcegraph/lib/errors" + + yamlv3 "gopkg.in/yaml.v3" +) + +// SetOutputs renders the outputs of the current step into the global outputs +// map using templating. +func SetOutputs(stepOutputs Outputs, global map[string]any, stepCtx *template.StepContext) error { + for name, output := range stepOutputs { + var value bytes.Buffer + + if err := template.RenderStepTemplate("outputs-"+name, output.Value, &value, stepCtx); err != nil { + return errors.Wrap(err, "parsing step run") + } + fmt.Printf("Rendering step output %s %s: %q (stdout is %q)\n", name, output.Value, value.String(), stepCtx.Step.Stdout) + + switch output.Format { + case "yaml": + var out any + // We use yamlv3 here, because it unmarshals YAML into + // map[string]interface{} which we need to serialize it back to + // JSON when we cache the results. + // See https://github.com/go-yaml/yaml/issues/139 for context + if err := yamlv3.NewDecoder(&value).Decode(&out); err != nil { + return err + } + global[name] = out + case "json": + var out any + if err := json.NewDecoder(&value).Decode(&out); err != nil { + return err + } + global[name] = out + default: + global[name] = value.String() + } + } + + return nil +} diff --git a/lib/batches/overridable/bool.go b/lib/batches/overridable/bool.go new file mode 100644 index 0000000000..411e466071 --- /dev/null +++ b/lib/batches/overridable/bool.go @@ -0,0 +1,69 @@ +package overridable + +import "encoding/json" + +// Bool represents a bool value that can be modified on a per-repo basis. +type Bool struct { + rules rules +} + +// FromBool creates a Bool representing a static, scalar value. +func FromBool(b bool) Bool { + return Bool{ + rules: rules{simpleRule(b)}, + } +} + +// Value returns the bool value for the given repository. +func (b *Bool) Value(name string) bool { + v := b.rules.Match(name) + if v == nil { + return false + } + return v.(bool) +} + +// MarshalJSON encodes the Bool overridable to a json representation. +func (b Bool) MarshalJSON() ([]byte, error) { + if len(b.rules) == 0 { + return []byte("false"), nil + } + return json.Marshal(b.rules) +} + +// UnmarshalJSON unmarshalls a JSON value into a Bool. +func (b *Bool) UnmarshalJSON(data []byte) error { + var all bool + if err := json.Unmarshal(data, &all); err == nil { + *b = Bool{rules: rules{simpleRule(all)}} + return nil + } + + var c complex + if err := json.Unmarshal(data, &c); err != nil { + return err + } + + return b.rules.hydrateFromComplex(c) +} + +// UnmarshalYAML unmarshalls a YAML value into a Bool. +func (b *Bool) UnmarshalYAML(unmarshal func(any) error) error { + var all bool + if err := unmarshal(&all); err == nil { + *b = Bool{rules: rules{simpleRule(all)}} + return nil + } + + var c complex + if err := unmarshal(&c); err != nil { + return err + } + + return b.rules.hydrateFromComplex(c) +} + +// Equal tests two Bools for equality, used in cmp. +func (b Bool) Equal(other Bool) bool { + return b.rules.Equal(other.rules) +} diff --git a/lib/batches/overridable/bool_or_string.go b/lib/batches/overridable/bool_or_string.go new file mode 100644 index 0000000000..4b910b0571 --- /dev/null +++ b/lib/batches/overridable/bool_or_string.go @@ -0,0 +1,83 @@ +package overridable + +import ( + "encoding/json" +) + +// BoolOrString is a set of rules that either evaluate to a string or a bool. +type BoolOrString struct { + rules rules +} + +// FromBoolOrString creates a BoolOrString representing a static, scalar value. +func FromBoolOrString(v any) BoolOrString { + return BoolOrString{ + rules: rules{simpleRule(v)}, + } +} + +// Value returns the value for the given repository. +func (bs *BoolOrString) Value(name string) any { + return bs.rules.Match(name) +} + +// ValueWithSuffix returns the value for the given repository and branch name. +func (bs *BoolOrString) ValueWithSuffix(name, suffix string) any { + return bs.rules.MatchWithSuffix(name, suffix) +} + +// MarshalJSON encodes the BoolOrString overridable to a json representation. +func (bs BoolOrString) MarshalJSON() ([]byte, error) { + if len(bs.rules) == 0 { + return []byte("false"), nil + } + return json.Marshal(bs.rules) +} + +// UnmarshalJSON unmarshalls a JSON value into a Publish. +func (bs *BoolOrString) UnmarshalJSON(data []byte) error { + var b bool + if err := json.Unmarshal(data, &b); err == nil { + *bs = BoolOrString{rules: rules{simpleRule(b)}} + return nil + } + var s string + if err := json.Unmarshal(data, &s); err == nil { + *bs = BoolOrString{rules: rules{simpleRule(s)}} + return nil + } + + var c complex + if err := json.Unmarshal(data, &c); err != nil { + return err + } + + return bs.rules.hydrateFromComplex(c) +} + +// UnmarshalYAML unmarshalls a YAML value into a Publish. +func (bs *BoolOrString) UnmarshalYAML(unmarshal func(any) error) error { + var b bool + if err := unmarshal(&b); err == nil { + *bs = BoolOrString{rules: rules{simpleRule(b)}} + return nil + } + + var s string + if err := unmarshal(&s); err == nil { + *bs = BoolOrString{rules: rules{simpleRule(s)}} + return nil + } + + var c complex + if err := unmarshal(&c); err != nil { + return err + } + + return bs.rules.hydrateFromComplex(c) +} + +// Equal tests two BoolOrStrings for equality, used in cmp. +func (bs BoolOrString) Equal(other BoolOrString) bool { + return bs.rules.Equal(other.rules) +} diff --git a/lib/batches/overridable/overridable.go b/lib/batches/overridable/overridable.go new file mode 100644 index 0000000000..61cfaadd9b --- /dev/null +++ b/lib/batches/overridable/overridable.go @@ -0,0 +1,143 @@ +// Package overridable provides data types representing values in batch +// specs that can be overridden for specific repositories. +package overridable + +import ( + "encoding/json" + "strings" + + "github.com/gobwas/glob" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// allPattern is used to define default rules for the simple scalar case. +const allPattern = "*" + +// simpleRule creates the simplest of rules for the given value: `"*": value`. +func simpleRule(v any) *rule { + r, err := newRule(allPattern, v) + if err != nil { + // Since we control the pattern being compiled, an error should never + // occur. + panic(err) + } + + return r +} + +type complex []map[string]any + +type rule struct { + // pattern is the glob-syntax pattern, such as "a/b/ceee-*" + pattern string + // patternSuffix is an optional suffix that can be appended to the pattern with "@" + patternSuffix string + + compiled glob.Glob + value any +} + +// newRule builds a new rule instance, ensuring that the glob pattern +// is compiled. +func newRule(pattern string, value any) (*rule, error) { + var suffix string + split := strings.SplitN(pattern, "@", 2) + if len(split) > 1 { + pattern = split[0] + suffix = split[1] + } + + compiled, err := glob.Compile(pattern) + if err != nil { + return nil, err + } + + return &rule{ + pattern: pattern, + patternSuffix: suffix, + compiled: compiled, + value: value, + }, nil +} + +func (a rule) Equal(b rule) bool { + return a.pattern == b.pattern && a.value == b.value +} + +type rules []*rule + +// Match matches the given repository name against all rules, returning the rule value that matches at last, or nil if none match. +func (r rules) Match(name string) any { + // We want the last match to win, so we'll iterate in reverse order. + for i := len(r) - 1; i >= 0; i-- { + if r[i].compiled.Match(name) { + return r[i].value + } + } + return nil +} + +// MatchWithSuffix matches the given repository name against all rules and the +// suffix against provided pattern suffix, returning the rule value that matches +// at last, or nil if none match. +func (r rules) MatchWithSuffix(name, suffix string) any { + // We want the last match to win, so we'll iterate in reverse order. + for i := len(r) - 1; i >= 0; i-- { + if r[i].compiled.Match(name) && (r[i].patternSuffix == "" || r[i].patternSuffix == suffix) { + return r[i].value + } + } + return nil +} + +// MarshalJSON marshalls the bool into its JSON representation, which will +// either be a literal or an array of objects. +func (r rules) MarshalJSON() ([]byte, error) { + if len(r) == 1 && r[0].pattern == allPattern { + return json.Marshal(r[0].value) + } + + rules := []map[string]any{} + for _, rule := range r { + rules = append(rules, map[string]any{ + rule.pattern: rule.value, + }) + } + return json.Marshal(rules) +} + +// hydrateFromComplex builds an array of rules out of a complex value. +func (r *rules) hydrateFromComplex(c []map[string]any) error { + *r = make(rules, len(c)) + for i, rule := range c { + if len(rule) != 1 { + return errors.Errorf("unexpected number of elements in the array at entry %d: %d (must be 1)", i, len(rule)) + } + for pattern, value := range rule { + var err error + (*r)[i], err = newRule(pattern, value) + if err != nil { + return errors.Wrapf(err, "building rule for array entry %d", i) + } + } + } + return nil +} + +// Equal tests two rules for equality. Used in cmp. +func (r rules) Equal(other rules) bool { + if len(r) != len(other) { + return false + } + + for i := range r { + a := r[i] + b := other[i] + if !a.Equal(*b) { + return false + } + } + + return true +} diff --git a/lib/batches/published.go b/lib/batches/published.go new file mode 100644 index 0000000000..7dbd200c34 --- /dev/null +++ b/lib/batches/published.go @@ -0,0 +1,110 @@ +package batches + +import ( + "encoding/json" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// PublishedValue is a wrapper type that supports the quadruple `true`, `false`, +// `"draft"`, `nil`, or `pushed-only`. +type PublishedValue struct { + Val any +} + +// True is true if the enclosed value is a bool being true. +func (p *PublishedValue) True() bool { + if b, ok := p.Val.(bool); ok { + return b + } + return false +} + +// False is true if the enclosed value is a bool being false. +func (p PublishedValue) False() bool { + if b, ok := p.Val.(bool); ok { + return !b + } + return false +} + +// Draft is true if the enclosed value is a string being "draft". +func (p PublishedValue) Draft() bool { + if s, ok := p.Val.(string); ok { + return s == "draft" + } + return false +} + +// PushedOnly is true if the enclosed value is a string being "pushed-only". +func (p PublishedValue) PushedOnly() bool { + if s, ok := p.Val.(string); ok { + return s == "pushed-only" + } + return false +} + +// Nil is true if the enclosed value is a null or omitted. +func (p PublishedValue) Nil() bool { + return p.Val == nil +} + +// Valid returns whether the enclosed value is of any of the permitted types. +func (p *PublishedValue) Valid() bool { + return p.True() || p.False() || p.Draft() || p.PushedOnly() || p.Nil() +} + +// Value returns the underlying value stored in this wrapper. +func (p *PublishedValue) Value() any { + return p.Val +} + +func (p PublishedValue) MarshalJSON() ([]byte, error) { + if p.Nil() { + v := "null" + return []byte(v), nil + } + if p.True() { + v := "true" + return []byte(v), nil + } + if p.False() { + v := "false" + return []byte(v), nil + } + if p.Draft() { + v := `"draft"` + return []byte(v), nil + } + if p.PushedOnly() { + v := `"pushed-only"` + return []byte(v), nil + } + return nil, errors.Errorf("invalid PublishedValue: %s (%T)", p.Val, p.Val) +} + +func (p *PublishedValue) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &p.Val) +} + +// UnmarshalYAML unmarshalls a YAML value into a Publish. +func (p *PublishedValue) UnmarshalYAML(unmarshal func(any) error) error { + if err := unmarshal(&p.Val); err != nil { + return err + } + + return nil +} + +func (p *PublishedValue) UnmarshalGraphQL(input any) error { + p.Val = input + if !p.Valid() { + return errors.Errorf("invalid PublishedValue: %v", input) + } + return nil +} + +// ImplementsGraphQLType lets GraphQL-go tell apart the corresponding GraphQL scalar. +func (p *PublishedValue) ImplementsGraphQLType(name string) bool { + return name == "PublishedValue" +} diff --git a/lib/batches/schema/batch_spec_stringdata.go b/lib/batches/schema/batch_spec_stringdata.go new file mode 100644 index 0000000000..6d412a0a33 --- /dev/null +++ b/lib/batches/schema/batch_spec_stringdata.go @@ -0,0 +1,412 @@ +// Code generated by stringdata. DO NOT EDIT. + +package schema + +// BatchSpecJSON is the content of the file "schema/batch_spec.schema.json". +const BatchSpecJSON = `{ + "$id": "batch_spec.schema.json#", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BatchSpec", + "description": "A batch specification, which describes the batch change and what kinds of changes to make (or what existing changesets to track).", + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "version": { + "type": "number", + "description": "The version of the batch spec schema. Defaults to 1.", + "enum": [1, 2] + }, + "name": { + "type": "string", + "description": "The name of the batch change, which is unique among all batch changes in the namespace. A batch change's name is case-preserving.", + "pattern": "^[\\w.-]+$" + }, + "description": { + "type": "string", + "description": "The description of the batch change." + }, + "on": { + "type": ["array", "null"], + "description": "The set of repositories (and branches) to run the batch change on, specified as a list of search queries (that match repositories) and/or specific repositories.", + "items": { + "title": "OnQueryOrRepository", + "oneOf": [ + { + "title": "OnQuery", + "type": "object", + "description": "A Sourcegraph search query that matches a set of repositories (and branches). Each matched repository branch is added to the list of repositories that the batch change will be run on.", + "additionalProperties": false, + "required": ["repositoriesMatchingQuery"], + "properties": { + "repositoriesMatchingQuery": { + "type": "string", + "description": "A Sourcegraph search query that matches a set of repositories (and branches). If the query matches files, symbols, or some other object inside a repository, the object's repository is included.", + "examples": ["file:README.md"] + } + } + }, + { + "title": "OnRepository", + "type": "object", + "description": "A specific repository (and branch) that is added to the list of repositories that the batch change will be run on.", + "additionalProperties": false, + "required": ["repository"], + "properties": { + "repository": { + "type": "string", + "description": "The name of the repository (as it is known to Sourcegraph).", + "examples": ["github.com/foo/bar"] + }, + "branch": { + "description": "The repository branch to propose changes to. If unset, the repository's default branch is used. If this field is defined, branches cannot be.", + "type": "string" + }, + "branches": { + "description": "The repository branches to propose changes to. If unset, the repository's default branch is used. If this field is defined, branch cannot be.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "$comment": "This is a convoluted way of saying either ` + "`" + `branch` + "`" + ` or ` + "`" + `branches` + "`" + ` can be provided, but not both at once, and neither are required.", + "anyOf": [ + { + "oneOf": [ + { + "required": ["branch"] + }, + { + "required": ["branches"] + } + ] + }, + { + "not": { + "required": ["branch", "branches"] + } + } + ] + } + ] + } + }, + "workspaces": { + "type": ["array", "null"], + "description": "Individual workspace configurations for one or more repositories that define which workspaces to use for the execution of steps in the repositories.", + "items": { + "title": "WorkspaceConfiguration", + "type": "object", + "description": "Configuration for how to setup workspaces in repositories", + "additionalProperties": false, + "required": ["rootAtLocationOf"], + "properties": { + "rootAtLocationOf": { + "type": "string", + "description": "The name of the file that sits at the root of the desired workspace.", + "examples": ["package.json", "go.mod", "Gemfile", "Cargo.toml", "README.md"] + }, + "in": { + "type": "string", + "description": "The repositories in which to apply the workspace configuration. Supports globbing.", + "examples": ["github.com/sourcegraph/src-cli", "github.com/sourcegraph/*"] + }, + "onlyFetchWorkspace": { + "type": "boolean", + "description": "If this is true only the files in the workspace (and additional .gitignore) are downloaded instead of an archive of the full repository.", + "default": false + } + } + } + }, + "steps": { + "type": ["array", "null"], + "description": "The sequence of commands to run (for each repository branch matched in the ` + "`" + `on` + "`" + ` property) to produce the workspace changes that will be included in the batch change.", + "items": { + "title": "Step", + "type": "object", + "description": "A command to run (as part of a sequence) in a repository branch to produce the required changes.", + "additionalProperties": false, + "required": ["run", "container"], + "properties": { + "run": { + "type": "string", + "description": "The shell command to run in the container. It can also be a multi-line shell script. The working directory is the root directory of the repository checkout." + }, + "container": { + "type": "string", + "description": "The Docker image used to launch the Docker container in which the shell command is run.", + "examples": ["alpine:3"] + }, + "outputs": { + "type": ["object", "null"], + "description": "Output variables of this step that can be referenced in the changesetTemplate or other steps via outputs.", + "additionalProperties": { + "title": "OutputVariable", + "type": "object", + "required": ["value"], + "properties": { + "value": { + "type": "string", + "description": "The value of the output, which can be a template string.", + "examples": ["hello world", "${{ step.stdout }}", "${{ repository.name }}"] + }, + "format": { + "type": "string", + "description": "The expected format of the output. If set, the output is being parsed in that format before being stored in the var. If not set, 'text' is assumed to the format.", + "enum": ["json", "yaml", "text"] + } + } + } + }, + "env": { + "description": "Environment variables to set in the step environment.", + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "description": "Environment variables to set in the step environment.", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string", + "description": "An environment variable to set in the step environment: the value will be passed through from the environment src is running within." + }, + { + "type": "object", + "description": "An environment variable to set in the step environment: the key is used as the environment variable name and the value as the value.", + "additionalProperties": { + "type": "string" + }, + "minProperties": 1, + "maxProperties": 1 + } + ] + } + } + ] + }, + "files": { + "type": ["object", "null"], + "description": "Files that should be mounted into or be created inside the Docker container.", + "additionalProperties": { + "type": "string" + } + }, + "if": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "A condition to check before executing steps. Supports templating. The value 'true' is interpreted as true.", + "examples": [ + "true", + "${{ matches repository.name \"github.com/my-org/my-repo*\" }}", + "${{ outputs.goModFileExists }}", + "${{ eq previous_step.stdout \"success\" }}" + ] + }, + "mount": { + "description": "Files that are mounted to the Docker container.", + "type": ["array", "null"], + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path", "mountpoint"], + "properties": { + "path": { + "type": "string", + "description": "The path on the local machine to mount. The path must be in the same directory or a subdirectory of the batch spec.", + "examples": ["local/path/to/file.text", "local/path/to/directory"] + }, + "mountpoint": { + "type": "string", + "description": "The path in the container to mount the path on the local machine to.", + "examples": ["path/to/file.txt", "path/to/directory"] + } + } + } + } + } + } + }, + "transformChanges": { + "type": ["object", "null"], + "description": "Optional transformations to apply to the changes produced in each repository.", + "additionalProperties": false, + "properties": { + "group": { + "type": ["array", "null"], + "description": "A list of groups of changes in a repository that each create a separate, additional changeset for this repository, with all ungrouped changes being in the default changeset.", + "items": { + "title": "TransformChangesGroup", + "type": "object", + "additionalProperties": false, + "required": ["directory", "branch"], + "properties": { + "directory": { + "type": "string", + "description": "The directory path (relative to the repository root) of the changes to include in this group.", + "minLength": 1 + }, + "branch": { + "type": "string", + "description": "The branch on the repository to propose changes to. If unset, the repository's default branch is used.", + "minLength": 1 + }, + "repository": { + "type": "string", + "description": "Only apply this transformation in the repository with this name (as it is known to Sourcegraph).", + "examples": ["github.com/foo/bar"] + } + } + } + } + } + }, + "importChangesets": { + "type": ["array", "null"], + "description": "Import existing changesets on code hosts.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "externalIDs"], + "properties": { + "repository": { + "type": "string", + "description": "The repository name as configured on your Sourcegraph instance." + }, + "externalIDs": { + "type": ["array", "null"], + "description": "The changesets to import from the code host. For GitHub this is the PR number, for GitLab this is the MR number, for Bitbucket Server this is the PR number.", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "examples": [120, "120"] + } + } + } + }, + "changesetTemplate": { + "type": "object", + "description": "A template describing how to create (and update) changesets with the file changes produced by the command steps.", + "additionalProperties": false, + "required": ["title", "branch", "commit"], + "properties": { + "title": { + "type": "string", + "description": "The title of the changeset." + }, + "body": { + "type": "string", + "description": "The body (description) of the changeset." + }, + "branch": { + "type": "string", + "description": "The name of the Git branch to create or update on each repository with the changes." + }, + "fork": { + "type": "boolean", + "description": "Whether to publish the changeset to a fork of the target repository. If omitted, the changeset will be published to a branch directly on the target repository, unless the global ` + "`" + `batches.enforceFork` + "`" + ` setting is enabled. If set, this property will override any global setting." + }, + "commit": { + "title": "ExpandedGitCommitDescription", + "type": "object", + "description": "The Git commit to create with the changes.", + "additionalProperties": false, + "required": ["message"], + "properties": { + "message": { + "type": "string", + "description": "The Git commit message." + }, + "author": { + "title": "GitCommitAuthor", + "type": "object", + "description": "The author of the Git commit.", + "additionalProperties": false, + "required": ["name", "email"], + "properties": { + "name": { + "type": "string", + "description": "The Git commit author name." + }, + "email": { + "type": "string", + "format": "email", + "description": "The Git commit author email." + } + } + } + } + }, + "published": { + "description": "Whether to publish the changeset. An unpublished changeset can be previewed on Sourcegraph by any person who can view the batch change, but its commit, branch, and pull request aren't created on the code host. A published changeset results in a commit, branch, and pull request being created on the code host. If omitted, the publication state is controlled from the Batch Changes UI.", + "oneOf": [ + { + "type": "null" + }, + { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^draft$" + } + ], + "description": "A single flag to control the publishing state for the entire batch change." + }, + { + "type": "array", + "description": "A list of glob patterns to match repository names. In the event multiple patterns match, the last matching pattern in the list will be used.", + "items": { + "type": "object", + "description": "An object with one field: the key is the glob pattern to match against repository names; the value will be used as the published flag for matching repositories.", + "additionalProperties": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^draft$" + } + ] + }, + "minProperties": 1, + "maxProperties": 1 + } + } + ] + } + } + } + } +} +` diff --git a/lib/batches/schema/changeset_spec_stringdata.go b/lib/batches/schema/changeset_spec_stringdata.go new file mode 100644 index 0000000000..9ec47ef532 --- /dev/null +++ b/lib/batches/schema/changeset_spec_stringdata.go @@ -0,0 +1,121 @@ +// Code generated by stringdata. DO NOT EDIT. + +package schema + +// ChangesetSpecJSON is the content of the file "schema/changeset_spec.schema.json". +const ChangesetSpecJSON = `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ChangesetSpec", + "description": "A changeset specification, which describes a changeset to be created or an existing changeset to be tracked.", + "type": "object", + "oneOf": [ + { + "title": "ExistingChangesetSpec", + "type": "object", + "properties": { + "version": { + "type": "integer", + "description": "A field for versioning the payload." + }, + "baseRepository": { + "type": "string", + "description": "The GraphQL ID of the repository that contains the existing changeset on the code host.", + "examples": ["UmVwb3NpdG9yeTo5Cg=="] + }, + "externalID": { + "type": "string", + "description": "The ID that uniquely identifies the existing changeset on the code host", + "examples": ["3912", "12"] + } + }, + "required": ["baseRepository", "externalID"], + "additionalProperties": false + }, + { + "title": "BranchChangesetSpec", + "type": "object", + "properties": { + "version": { + "type": "integer", + "description": "A field for versioning the payload." + }, + "baseRepository": { + "type": "string", + "description": "The GraphQL ID of the repository that this changeset spec is proposing to change.", + "examples": ["UmVwb3NpdG9yeTo5Cg=="] + }, + "baseRef": { + "type": "string", + "description": "The full name of the Git ref in the base repository that this changeset is based on (and is proposing to be merged into). This ref must exist on the base repository.", + "pattern": "^refs\\/heads\\/\\S+$", + "examples": ["refs/heads/master"] + }, + "baseRev": { + "type": "string", + "description": "The base revision this changeset is based on. It is the latest commit in baseRef at the time when the changeset spec was created.", + "examples": ["4095572721c6234cd72013fd49dff4fb48f0f8a4"] + }, + "headRepository": { + "type": "string", + "description": "The GraphQL ID of the repository that contains the branch with this changeset's changes. Fork repositories and cross-repository changesets are not yet supported. Therefore, headRepository must be equal to baseRepository.", + "examples": ["UmVwb3NpdG9yeTo5Cg=="] + }, + "fork": { + "type": "boolean", + "description": "Whether to publish the changeset to a fork of the target repository. If omitted, the changeset will be published to a branch directly on the target repository, unless the global ` + "`" + `batches.enforceFork` + "`" + ` setting is enabled. If set, this property will override any global setting." + }, + "headRef": { + "type": "string", + "description": "The full name of the Git ref that holds the changes proposed by this changeset. This ref will be created or updated with the commits.", + "pattern": "^refs\\/heads\\/\\S+$", + "examples": ["refs/heads/fix-foo"] + }, + "title": { "type": "string", "description": "The title of the changeset on the code host." }, + "body": { "type": "string", "description": "The body (description) of the changeset on the code host." }, + "commits": { + "type": "array", + "description": "The Git commits with the proposed changes. These commits are pushed to the head ref.", + "minItems": 1, + "maxItems": 1, + "items": { + "title": "GitCommitDescription", + "type": "object", + "description": "The Git commit to create with the changes.", + "additionalProperties": false, + "required": ["message", "diff", "authorName", "authorEmail"], + "properties": { + "version": { + "type": "integer", + "description": "A field for versioning the payload." + }, + "message": { + "type": "string", + "description": "The Git commit message." + }, + "diff": { + "type": "string", + "description": "The commit diff (in unified diff format)." + }, + "authorName": { + "type": "string", + "description": "The Git commit author name." + }, + "authorEmail": { + "type": "string", + "format": "email", + "description": "The Git commit author email." + } + } + } + }, + "published": { + "oneOf": [{ "type": "boolean" }, { "type": "string", "pattern": "^draft$" }, { "type": "null" }], + "description": "Whether to publish the changeset. An unpublished changeset can be previewed on Sourcegraph by any person who can view the batch change, but its commit, branch, and pull request aren't created on the code host. A published changeset results in a commit, branch, and pull request being created on the code host." + } + }, + "required": ["baseRepository", "baseRef", "baseRev", "headRepository", "headRef", "title", "body", "commits"], + "additionalProperties": false + } + ] +} +` diff --git a/lib/batches/template/partial_eval.go b/lib/batches/template/partial_eval.go new file mode 100644 index 0000000000..905ece71d5 --- /dev/null +++ b/lib/batches/template/partial_eval.go @@ -0,0 +1,345 @@ +package template + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "text/template" + "text/template/parse" +) + +// IsStaticBool parses the input as a text/template and attempts to evaluate it +// with only the ahead-of-execution information available in StepContext. +// +// To do that it first calls parseAndPartialEval to evaluate the template as +// much as possible. +// +// If, after evaluation, more than text is left (i.e. because the template +// requires information that's only available later) the function returns with +// the first return value being false, because the template is not "static". +// first return value is true. +// +// If only text is left we check whether that text equals "true". The result of +// that check is the second return value. +func IsStaticBool(input string, ctx *StepContext) (isStatic bool, boolVal bool, err error) { + t, err := parseAndPartialEval(input, ctx) + if err != nil { + return false, false, err + } + + isStatic = true + for _, n := range t.Tree.Root.Nodes { + if n.Type() != parse.NodeText { + isStatic = false + break + } + } + if !isStatic { + return isStatic, false, nil + } + + return true, isTrueOutput(t.Tree.Root), nil +} + +// parseAndPartialEval parses input as a text/template and then attempts to +// partially evaluate the parts of the template it can evaluate ahead of time +// (meaning: before we've executed any batch spec steps and have a full +// StepContext available). +// +// If it's possible to evaluate a parse.ActionNode (which is what sits between +// delimiters in a text/template), the node is rewritten into a parse.TextNode, +// to make it look like it's always been text in the template. +// +// Partial evaluation is done in a best effort manner: if it's not possible to +// evaluate a node (because it requires information that we only later get, or +// because it's too complex, etc.) we degrade gracefully and simply abort the +// partial evaluation and leave the node as is. +// +// It also should be noted that we don't do "full" partial evaluation: if we +// come across value that we can't partially evaluate we abort the process *for +// the whole node* without replacing the sub-nodes that we've successfully +// evaluated. Why? Because we can't construct correct `*parse.Node` from +// outside the `parse` package. In other words: we evaluate +// all-parse.ActionNode-or-nothing. +func parseAndPartialEval(input string, ctx *StepContext) (*template.Template, error) { + t, err := template. + New("partial-eval"). + Delims(startDelim, endDelim). + Funcs(builtins). + Funcs(ctx.ToFuncMap()). + Parse(input) + + if err != nil { + return nil, err + } + + for i, n := range t.Tree.Root.Nodes { + t.Tree.Root.Nodes[i] = rewriteNode(n, ctx) + } + + return t, nil +} + +// rewriteNode takes the given parse.Parse and tries to partially evaluate it. +// If that's possible, the output of the evaluation is turned into text and +// instead of the node that was passed in a new parse.TextNode is returned that +// represents the output of the evaluation. +func rewriteNode(n parse.Node, ctx *StepContext) parse.Node { + switch n := n.(type) { + case *parse.ActionNode: + if val, ok := evalPipe(ctx, n.Pipe); ok { + var out bytes.Buffer + fmt.Fprint(&out, val.Interface()) + return &parse.TextNode{ + Text: out.Bytes(), + Pos: n.Pos, + NodeType: parse.NodeText, + } + } + + return n + + default: + return n + } +} + +// noValue is returned by the functions that partially evaluate a parse.Node +// to signify that evaluation was not possible or did not yield a value. +var noValue reflect.Value + +func evalPipe(ctx *StepContext, p *parse.PipeNode) (finalVal reflect.Value, ok bool) { + // If the pipe contains declaration we abort evaluation. + if len(p.Decl) > 0 { + return noValue, false + } + + // TODO: Support finalVal and pass it in to evalCmd + // finalVal is the value of the previous Cmd in a pipe (i.e. `${{ 3 + 3 | eq 6 }}`) + // It needs to be the final (fixed) argument of a call if it's set. + + for _, c := range p.Cmds { + finalVal, ok = evalCmd(ctx, c) + if !ok { + return noValue, false + } + } + + return finalVal, ok +} + +func evalCmd(ctx *StepContext, c *parse.CommandNode) (reflect.Value, bool) { + switch first := c.Args[0].(type) { + case *parse.BoolNode, *parse.NumberNode, *parse.StringNode, *parse.ChainNode: + if len(c.Args) == 1 { + return evalNode(ctx, first) + } + return noValue, false + + case *parse.IdentifierNode: + // A function call always starts with an identifier + return evalFunction(ctx, first.Ident, c.Args) + + default: + // Node type that we don't care about, so we don't even try to evaluate it + return noValue, false + } +} + +func evalNode(ctx *StepContext, n parse.Node) (reflect.Value, bool) { + switch n := n.(type) { + case *parse.BoolNode: + return reflect.ValueOf(n.True), true + + case *parse.NumberNode: + // This case branch is lifted from Go's text/template execution engine: + // https://sourcegraph.com/github.com/golang/go@2c9f5a1da823773c436f8b2c119635797d6db2d3/-/blob/src/text/template/exec.go#L493-530 + // The difference is that we don't do any error handling but simply abort. + switch { + case n.IsComplex: + return reflect.ValueOf(n.Complex128), true + + case n.IsFloat && + !isHexInt(n.Text) && !isRuneInt(n.Text) && + strings.ContainsAny(n.Text, ".eEpP"): + return reflect.ValueOf(n.Float64), true + + case n.IsInt: + num := int(n.Int64) + if int64(num) != n.Int64 { + return noValue, false + } + return reflect.ValueOf(num), true + + case n.IsUint: + return noValue, false + } + + case *parse.StringNode: + return reflect.ValueOf(n.Text), true + + case *parse.ChainNode: + // For now we only support fields that are 1 level deep (see below). + // Should we ever want to support more than one level, we need to + // revise this. + if len(n.Field) != 1 { + return noValue, false + } + + if ident, ok := n.Node.(*parse.IdentifierNode); ok { + switch ident.Ident { + case "repository": + switch n.Field[0] { + case "search_result_paths": + // TODO: We don't eval search_result_paths for now, since it's a + // "complex" value, a slice of strings, and turning that + // into text might not be useful to the user. So we abort. + return noValue, false + case "name": + return reflect.ValueOf(ctx.Repository.Name), true + } + + case "batch_change": + switch n.Field[0] { + case "name": + return reflect.ValueOf(ctx.BatchChange.Name), true + case "description": + return reflect.ValueOf(ctx.BatchChange.Description), true + } + } + } + return noValue, false + + case *parse.PipeNode: + return evalPipe(ctx, n) + } + + return noValue, false +} + +func isRuneInt(s string) bool { + return len(s) > 0 && s[0] == '\'' +} + +func isHexInt(s string) bool { + return len(s) > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') && !strings.ContainsAny(s, "pP") +} + +func evalFunction(ctx *StepContext, name string, args []parse.Node) (val reflect.Value, success bool) { + defer func() { + if r := recover(); r != nil { + val = noValue + success = false + } + }() + + switch name { + case "eq": + return evalEqCall(ctx, args[1:]) + + case "ne": + equal, ok := evalEqCall(ctx, args[1:]) + if !ok { + return noValue, false + } + return reflect.ValueOf(!equal.Bool()), true + + case "not": + return evalNotCall(ctx, args[1:]) + + default: + concreteFn, ok := builtins[name] + if !ok { + return noValue, false + } + + fn := reflect.ValueOf(concreteFn) + + // We can eval only if all args are static: + var evaluatedArgs []reflect.Value + for _, a := range args[1:] { + v, ok := evalNode(ctx, a) + if !ok { + // One of the args is not static, abort + return noValue, false + } + evaluatedArgs = append(evaluatedArgs, v) + + } + + ret := fn.Call(evaluatedArgs) + if len(ret) == 2 && !ret[1].IsNil() { + return noValue, false + } + return ret[0], true + } +} + +func evalNotCall(ctx *StepContext, args []parse.Node) (reflect.Value, bool) { + // We only support 1 arg for now: + if len(args) != 1 { + return noValue, false + } + + arg, ok := evalNode(ctx, args[0]) + if !ok { + return noValue, false + } + + return reflect.ValueOf(!isTrue(arg)), true +} + +func evalEqCall(ctx *StepContext, args []parse.Node) (reflect.Value, bool) { + // We only support 2 args for now: + if len(args) != 2 { + return noValue, false + } + + // We only eval `eq` if all args are static: + var evaluatedArgs []reflect.Value + for _, a := range args { + v, ok := evalNode(ctx, a) + if !ok { + // One of the args is not static, abort + return noValue, false + } + evaluatedArgs = append(evaluatedArgs, v) + } + + if len(evaluatedArgs) != 2 { + // safety check + return noValue, false + } + + isEqual := evaluatedArgs[0].Interface() == evaluatedArgs[1].Interface() + return reflect.ValueOf(isEqual), true +} + +// isTrue is taken from Go's text/template/exec.go and simplified +func isTrue(val reflect.Value) (truth bool) { + if !val.IsValid() { + // Something like var x interface{}, never set. It's a form of nil. + return false + } + switch val.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return val.Len() > 0 + case reflect.Bool: + return val.Bool() + case reflect.Complex64, reflect.Complex128: + return val.Complex() != 0 + case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: + return !val.IsNil() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return val.Int() != 0 + case reflect.Float32, reflect.Float64: + return val.Float() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return val.Uint() != 0 + case reflect.Struct: + return true // Struct values are always true. + default: + return false + } +} diff --git a/lib/batches/template/template.go b/lib/batches/template/template.go new file mode 100644 index 0000000000..0591ee8870 --- /dev/null +++ b/lib/batches/template/template.go @@ -0,0 +1,18 @@ +package template + +import "text/template" + +func New(name, tmpl, option string, ctxs ...template.FuncMap) (*template.Template, error) { + t := template.New(name).Delims(startDelim, endDelim) + if option != "" { + t = t.Option(option) + } + + t = t.Funcs(builtins) + + for _, ctx := range ctxs { + t = t.Funcs(ctx) + } + + return t.Parse(tmpl) +} diff --git a/lib/batches/template/templating.go b/lib/batches/template/templating.go new file mode 100644 index 0000000000..eae6547860 --- /dev/null +++ b/lib/batches/template/templating.go @@ -0,0 +1,334 @@ +package template + +import ( + "bytes" + "fmt" + "io" + "sort" + "strings" + "text/template" + + "github.com/gobwas/glob" + "github.com/grafana/regexp" + + "github.com/sourcegraph/sourcegraph/lib/batches/execution" + "github.com/sourcegraph/sourcegraph/lib/batches/git" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +const startDelim = "${{" +const endDelim = "}}" + +var builtins = template.FuncMap{ + "join": strings.Join, + "split": strings.Split, + "replace": strings.ReplaceAll, + "join_if": func(sep string, elems ...string) string { + var nonBlank []string + for _, e := range elems { + if e != "" { + nonBlank = append(nonBlank, e) + } + } + return strings.Join(nonBlank, sep) + }, + "matches": func(in, pattern string) (bool, error) { + g, err := glob.Compile(pattern) + if err != nil { + return false, err + } + return g.Match(in), nil + }, +} + +// ValidateBatchSpecTemplate attempts to perform a dry run replacement of the whole batch +// spec template for any templating variables which are not dependent on execution +// context. It returns a tuple whose first element is whether or not the batch spec is +// valid and whose second element is an error message if the spec is found to be invalid. +func ValidateBatchSpecTemplate(spec string) (bool, error) { + // We use empty contexts to create "dummy" `template.FuncMap`s -- function mappings + // with all the right keys, but no actual values. We'll use these `FuncMap`s to do a + // dry run on the batch spec to determine if it's valid or not, before we actually + // execute it. + sc := &StepContext{} + sfm := sc.ToFuncMap() + cstc := &ChangesetTemplateContext{} + cstfm := cstc.ToFuncMap() + + // Strip any use of `outputs` fields from the spec template. Without using real + // contexts for the `FuncMap`s, they'll fail to `template.Execute`, and it's difficult + // to statically validate them without deeper inspection of the YAML, so our + // validation is just a best-effort without them. + outputRe := regexp.MustCompile(`(?i)\$\{\{\s*[^}]*\s*outputs\.[^}]*\}\}`) + spec = outputRe.ReplaceAllString(spec, "") + + // Also strip index references. We also can't validate whether or not an index is in + // range without real context. + indexRe := regexp.MustCompile(`(?i)\$\{\{\s*index\s*[^}]*\}\}`) + spec = indexRe.ReplaceAllString(spec, "") + + // By default, text/template will continue even if it encounters a key that is not + // indexed in any of the provided `FuncMap`s. A missing key is an indication of an + // unknown or mistyped template variable which would invalidate the batch spec, so we + // want to fail immediately if we encounter one. We accomplish this by setting the + // option "missingkey=error". See https://pkg.go.dev/text/template#Template.Option for + // more. + t, err := New("validateBatchSpecTemplate", spec, "missingkey=error", sfm, cstfm) + + if err != nil { + // Attempt to extract the specific template variable field that caused the error + // to provide a clearer message. + errorRe := regexp.MustCompile(`(?i)function "(?P[^"]+)" not defined`) + if matches := errorRe.FindStringSubmatch(err.Error()); len(matches) > 0 { + return false, errors.New(fmt.Sprintf("validating batch spec template: unknown templating variable: '%s'", matches[1])) + } + // If we couldn't give a more specific error, fall back on the one from text/template. + return false, errors.Wrap(err, "validating batch spec template") + } + + var out bytes.Buffer + if err = t.Execute(&out, &StepContext{}); err != nil { + // Attempt to extract the specific template variable fields that caused the error + // to provide a clearer message. + errorRe := regexp.MustCompile(`(?i)at <(?P[^>]+)>:.*for key "(?P[^"]+)"`) + if matches := errorRe.FindStringSubmatch(err.Error()); len(matches) > 0 { + return false, errors.New(fmt.Sprintf("validating batch spec template: unknown templating variable: '%s.%s'", matches[1], matches[2])) + } + // If we couldn't give a more specific error, fall back on the one from text/template. + return false, errors.Wrap(err, "validating batch spec template") + } + + return true, nil +} + +func isTrueOutput(output interface{ String() string }) bool { + return strings.TrimSpace(output.String()) == "true" +} + +func EvalStepCondition(condition string, stepCtx *StepContext) (bool, error) { + if condition == "" { + return true, nil + } + + var out bytes.Buffer + if err := RenderStepTemplate("step-condition", condition, &out, stepCtx); err != nil { + return false, errors.Wrap(err, "parsing step if") + } + + return isTrueOutput(&out), nil +} + +func RenderStepTemplate(name, tmpl string, out io.Writer, stepCtx *StepContext) error { + // By default, text/template will continue even if it encounters a key that is not + // indexed in any of the provided `FuncMap`s, replacing the variable with "". This means that a mis-typed variable such as "${{ + // repository.search_resalt_paths }}" would just be evaluated as "", which + // is not a particularly useful substitution and will only indirectly manifest to the + // user as an error during execution. Instead, we prefer to fail immediately if we + // encounter an unknown variable. We accomplish this by setting the option + // "missingkey=error". See https://pkg.go.dev/text/template#Template.Option for more. + t, err := New(name, tmpl, "missingkey=error", stepCtx.ToFuncMap()) + if err != nil { + return errors.Wrap(err, "parsing step run") + } + + return t.Execute(out, stepCtx) +} + +func RenderStepMap(m map[string]string, stepCtx *StepContext) (map[string]string, error) { + rendered := make(map[string]string, len(m)) + + for k, v := range m { + var out bytes.Buffer + + if err := RenderStepTemplate(k, v, &out, stepCtx); err != nil { + return rendered, err + } + + rendered[k] = out.String() + } + + return rendered, nil +} + +// TODO(mrnugget): This is bad and should be (a) removed or (b) moved to batches package +type BatchChangeAttributes struct { + Name string + Description string +} + +type Repository struct { + Name string + Branch string + FileMatches []string +} + +func (r Repository) SearchResultPaths() (list fileMatchPathList) { + sort.Strings(r.FileMatches) + return r.FileMatches +} + +type fileMatchPathList []string + +func (f fileMatchPathList) String() string { return strings.Join(f, " ") } + +// StepContext represents the contextual information available when rendering a +// step's fields, such as "run" or "outputs", as templates. +type StepContext struct { + // BatchChange are the attributes in the BatchSpec that are set on the BatchChange. + BatchChange BatchChangeAttributes + // Outputs are the outputs set by the current and all previous steps. + Outputs map[string]any + // Step is the result of the current step. Empty when evaluating the "run" field + // but filled when evaluating the "outputs" field. + Step execution.AfterStepResult + // Steps contains the path in which the steps are being executed and the + // changes made by all steps that were executed up until the current step. + Steps StepsContext + // PreviousStep is the result of the previous step. Empty when there is no + // previous step. + PreviousStep execution.AfterStepResult + // Repository is the Sourcegraph repository in which the steps are executed. + Repository Repository +} + +// ToFuncMap returns a template.FuncMap to access fields on the StepContext in a +// text/template. +func (stepCtx *StepContext) ToFuncMap() template.FuncMap { + newStepResult := func(res *execution.AfterStepResult) map[string]any { + m := map[string]any{ + "modified_files": "", + "added_files": "", + "deleted_files": "", + "renamed_files": "", + "stdout": "", + "stderr": "", + } + if res == nil { + return m + } + + m["modified_files"] = res.ChangedFiles.Modified + m["added_files"] = res.ChangedFiles.Added + m["deleted_files"] = res.ChangedFiles.Deleted + m["renamed_files"] = res.ChangedFiles.Renamed + m["stdout"] = res.Stdout + m["stderr"] = res.Stderr + + return m + } + + return template.FuncMap{ + "previous_step": func() map[string]any { + return newStepResult(&stepCtx.PreviousStep) + }, + "step": func() map[string]any { + return newStepResult(&stepCtx.Step) + }, + "steps": func() map[string]any { + res := newStepResult(&execution.AfterStepResult{ChangedFiles: stepCtx.Steps.Changes}) + res["path"] = stepCtx.Steps.Path + return res + }, + "outputs": func() map[string]any { + return stepCtx.Outputs + }, + "repository": func() map[string]any { + return map[string]any{ + "search_result_paths": stepCtx.Repository.SearchResultPaths(), + "name": stepCtx.Repository.Name, + "branch": stepCtx.Repository.Branch, + } + }, + "batch_change": func() map[string]any { + return map[string]any{ + "name": stepCtx.BatchChange.Name, + "description": stepCtx.BatchChange.Description, + } + }, + } +} + +type StepsContext struct { + // Changes that have been made by executing all steps. + Changes git.Changes + // Path is the relative-to-root directory in which the steps have been + // executed. Default is "". No leading "/". + Path string +} + +// ChangesetTemplateContext represents the contextual information available +// when rendering a field of the ChangesetTemplate as a template. +type ChangesetTemplateContext struct { + // BatchChangeAttributes are the attributes of the BatchChange that will be + // created/updated. + BatchChangeAttributes BatchChangeAttributes + + // Steps are the changes made by all steps that were executed. + Steps StepsContext + + // Outputs are the outputs defined and initialized by the steps. + Outputs map[string]any + + // Repository is the repository in which the steps were executed. + Repository Repository +} + +// ToFuncMap returns a template.FuncMap to access fields on the StepContext in a +// text/template. +func (tmplCtx *ChangesetTemplateContext) ToFuncMap() template.FuncMap { + return template.FuncMap{ + "repository": func() map[string]any { + return map[string]any{ + "search_result_paths": tmplCtx.Repository.SearchResultPaths(), + "name": tmplCtx.Repository.Name, + "branch": tmplCtx.Repository.Branch, + } + }, + "batch_change": func() map[string]any { + return map[string]any{ + "name": tmplCtx.BatchChangeAttributes.Name, + "description": tmplCtx.BatchChangeAttributes.Description, + } + }, + "outputs": func() map[string]any { + return tmplCtx.Outputs + }, + "steps": func() map[string]any { + return map[string]any{ + "modified_files": tmplCtx.Steps.Changes.Modified, + "added_files": tmplCtx.Steps.Changes.Added, + "deleted_files": tmplCtx.Steps.Changes.Deleted, + "renamed_files": tmplCtx.Steps.Changes.Renamed, + "path": tmplCtx.Steps.Path, + } + }, + // Leave batch_change_link alone; it will be rendered during the reconciler phase instead. + "batch_change_link": func() string { + return "${{ batch_change_link }}" + }, + } +} + +func RenderChangesetTemplateField(name, tmpl string, tmplCtx *ChangesetTemplateContext) (string, error) { + var out bytes.Buffer + + // By default, text/template will continue even if it encounters a key that is not + // indexed in any of the provided `FuncMap`s, replacing the variable with "". This means that a mis-typed variable such as "${{ + // repository.search_resalt_paths }}" would just be evaluated as "", which + // is not a particularly useful substitution and will only indirectly manifest to the + // user as an error during execution. Instead, we prefer to fail immediately if we + // encounter an unknown variable. We accomplish this by setting the option + // "missingkey=error". See https://pkg.go.dev/text/template#Template.Option for more. + t, err := New(name, tmpl, "missingkey=error", tmplCtx.ToFuncMap()) + if err != nil { + return "", err + } + + if err := t.Execute(&out, tmplCtx); err != nil { + return "", err + } + + return strings.TrimSpace(out.String()), nil +} diff --git a/lib/batches/workspaces_execution_input.go b/lib/batches/workspaces_execution_input.go new file mode 100644 index 0000000000..76a5a8cb18 --- /dev/null +++ b/lib/batches/workspaces_execution_input.go @@ -0,0 +1,39 @@ +package batches + +import ( + "github.com/sourcegraph/sourcegraph/lib/batches/execution" + "github.com/sourcegraph/sourcegraph/lib/batches/template" +) + +type WorkspacesExecutionInput struct { + BatchChangeAttributes template.BatchChangeAttributes + Repository WorkspaceRepo `json:"repository"` + Branch WorkspaceBranch `json:"branch"` + Path string `json:"path"` + OnlyFetchWorkspace bool `json:"onlyFetchWorkspace"` + Steps []Step `json:"steps"` + SearchResultPaths []string `json:"searchResultPaths"` + // CachedStepResultFound is only required for V1 executions. + // TODO: Remove me once V2 is the only execution format. + CachedStepResultFound bool `json:"cachedStepResultFound"` + // CachedStepResult is only required for V1 executions. + // TODO: Remove me once V2 is the only execution format. + CachedStepResult execution.AfterStepResult `json:"cachedStepResult"` + // SkippedSteps determines which steps are skipped in the execution. + SkippedSteps map[int]struct{} `json:"skippedSteps"` +} + +type WorkspaceRepo struct { + // ID is the GraphQL ID of the repository. + ID string `json:"id"` + Name string `json:"name"` +} + +type WorkspaceBranch struct { + Name string `json:"name"` + Target Commit `json:"target"` +} + +type Commit struct { + OID string `json:"oid"` +} diff --git a/lib/batches/yaml/validate.go b/lib/batches/yaml/validate.go new file mode 100644 index 0000000000..9d901abed2 --- /dev/null +++ b/lib/batches/yaml/validate.go @@ -0,0 +1,33 @@ +package yaml + +import ( + "encoding/json" + + "github.com/ghodss/yaml" + + yamlv3 "gopkg.in/yaml.v3" + + "github.com/sourcegraph/sourcegraph/lib/batches/jsonschema" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// UnmarshalValidate validates the input, which can be YAML or JSON, against +// the provided JSON schema. If the validation is successful the validated +// input is unmarshalled into the target. +func UnmarshalValidate(schema string, input []byte, target any) error { + normalized, err := yaml.YAMLToJSONCustom(input, yamlv3.Unmarshal) + if err != nil { + return errors.Wrapf(err, "failed to normalize JSON") + } + + var errs error + if err := jsonschema.Validate(schema, normalized); err != nil { + errs = errors.Append(errs, err) + } + + if err := json.Unmarshal(normalized, target); err != nil { + errs = errors.Append(errs, err) + } + + return errs +} diff --git a/lib/codeintel/upload/compress.go b/lib/codeintel/upload/compress.go new file mode 100644 index 0000000000..3c9a6176f7 --- /dev/null +++ b/lib/codeintel/upload/compress.go @@ -0,0 +1,42 @@ +package upload + +import ( + "io" + "os" + + gzip "github.com/klauspost/pgzip" + + "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/sourcegraph/lib/output" +) + +// compressReaderToDisk compresses and writes the content of the given reader to a temporary +// file and returns the file's path. If the given progress object is non-nil, then the progress's +// first bar will be updated with the percentage of bytes read on each read. +func compressReaderToDisk(r io.Reader, readerLen int64, progress output.Progress) (filename string, err error) { + compressedFile, err := os.CreateTemp("", "") + if err != nil { + return "", err + } + defer func() { + if closeErr := compressedFile.Close(); err != nil { + err = errors.Append(err, closeErr) + } + }() + + gzipWriter := gzip.NewWriter(compressedFile) + defer func() { + if closeErr := gzipWriter.Close(); err != nil { + err = errors.Append(err, closeErr) + } + }() + + if progress != nil { + r = newProgressCallbackReader(r, readerLen, progress, 0) + } + if _, err := io.Copy(gzipWriter, r); err != nil { + return "", nil + } + + return compressedFile.Name(), nil +} diff --git a/lib/codeintel/upload/indexer_name.go b/lib/codeintel/upload/indexer_name.go new file mode 100644 index 0000000000..7a1c084765 --- /dev/null +++ b/lib/codeintel/upload/indexer_name.go @@ -0,0 +1,76 @@ +package upload + +import ( + "bufio" + "bytes" + "encoding/json" + "io" + + "github.com/sourcegraph/scip/bindings/go/scip" + "google.golang.org/protobuf/proto" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// MaxBufferSize is the maximum size of the metaData line in the dump. This should be large enough +// to be able to read the output of lsif-tsc for most cases, which will contain all glob-expanded +// file names in the indexing of JavaScript projects. +// +// Data point: lodash's metaData vertex constructed by the args `*.js test/*.js --AllowJs --checkJs` +// is 10639 characters long. +const MaxBufferSize = 128 * 1024 + +// ErrMetadataExceedsBuffer occurs when the first line of an LSIF index is too long to read. +var ErrMetadataExceedsBuffer = errors.New("metaData vertex exceeds buffer") + +// ErrInvalidMetaDataVertex occurs when the first line of an LSIF index is not a valid metadata vertex. +var ErrInvalidMetaDataVertex = errors.New("invalid metaData vertex") + +type metaDataVertex struct { + Label string `json:"label"` + ToolInfo toolInfo `json:"toolInfo"` +} + +type toolInfo struct { + Name string `json:"name"` + Version string `json:"version"` +} + +// ReadIndexerName returns the name of the tool that generated the given index contents. +// This function reads only the first line of the file, where the metadata vertex is +// assumed to be in all valid dumps. +func ReadIndexerName(r io.Reader) (string, error) { + name, _, err := ReadIndexerNameAndVersion(r) + return name, err +} + +// ReadIndexerNameAndVersion returns the name and version of the tool that generated the +// given index contents. This function reads only the first line of the file for LSIF, where +// the metadata vertex is assumed to be in all valid dumps. If its a SCIP index, the name +// and version are read from the contents of the index. +func ReadIndexerNameAndVersion(r io.Reader) (name string, verison string, _ error) { + var buf bytes.Buffer + line, isPrefix, err := bufio.NewReaderSize(io.TeeReader(r, &buf), MaxBufferSize).ReadLine() + if err == nil { + if !isPrefix { + meta := metaDataVertex{} + if err := json.Unmarshal(line, &meta); err == nil { + if meta.Label == "metaData" && meta.ToolInfo.Name != "" { + return meta.ToolInfo.Name, meta.ToolInfo.Version, nil + } + } + } + } + + content, err := io.ReadAll(io.MultiReader(bytes.NewReader(buf.Bytes()), r)) + if err != nil { + return "", "", ErrInvalidMetaDataVertex + } + + var index scip.Index + if err := proto.Unmarshal(content, &index); err != nil { + return "", "", ErrInvalidMetaDataVertex + } + + return index.Metadata.ToolInfo.Name, index.Metadata.ToolInfo.Version, nil +} diff --git a/lib/codeintel/upload/progress_reader.go b/lib/codeintel/upload/progress_reader.go new file mode 100644 index 0000000000..ff62eb6ba1 --- /dev/null +++ b/lib/codeintel/upload/progress_reader.go @@ -0,0 +1,56 @@ +package upload + +import ( + "io" + "time" + + "github.com/sourcegraph/sourcegraph/lib/output" +) + +type progressCallbackReader struct { + reader io.Reader + totalRead int64 + progressCallback func(totalRead int64) +} + +var debounceInterval = time.Millisecond * 50 + +// newProgressCallbackReader returns a modified version of the given reader that +// updates the value of a progress bar on each read. If progress is nil or n is +// zero, then the reader is returned unmodified. +// +// Calls to the progress bar update will be debounced so that two updates do not +// occur within 50ms of each other. This is to reduce flicker on the screen for +// massive writes, which make progress more quickly than the screen can redraw. +func newProgressCallbackReader(r io.Reader, readerLen int64, progress output.Progress, barIndex int) io.Reader { + if progress == nil || readerLen == 0 { + return r + } + + var lastUpdated time.Time + + progressCallback := func(totalRead int64) { + if debounceInterval <= time.Since(lastUpdated) { + // Calculate progress through the reader; do not ever complete + // as we wait for the HTTP request finish the remaining small + // percentage. + + p := float64(totalRead) / float64(readerLen) + if p >= 1 { + p = 1 - 10e-3 + } + + lastUpdated = time.Now() + progress.SetValue(barIndex, p) + } + } + + return &progressCallbackReader{reader: r, progressCallback: progressCallback} +} + +func (r *progressCallbackReader) Read(p []byte) (int, error) { + n, err := r.reader.Read(p) + r.totalRead += int64(n) + r.progressCallback(r.totalRead) + return n, err +} diff --git a/lib/codeintel/upload/request.go b/lib/codeintel/upload/request.go new file mode 100644 index 0000000000..db571435ea --- /dev/null +++ b/lib/codeintel/upload/request.go @@ -0,0 +1,204 @@ +package upload + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type uploadRequestOptions struct { + UploadOptions + + Payload io.Reader // Request payload + Target *int // Pointer to upload id decoded from resp + MultiPart bool // Whether the request is a multipart init + NumParts int // The number of upload parts + UncompressedSize int64 // The uncompressed size of the upload + UploadID int // The multipart upload ID + Index int // The index part being uploaded + Done bool // Whether the request is a multipart finalize +} + +// ErrUnauthorized occurs when the upload endpoint returns a 401 response. +var ErrUnauthorized = errors.New("unauthorized upload") + +// performUploadRequest performs an HTTP POST to the upload endpoint. The query string of the request +// is constructed from the given request options and the body of the request is the unmodified reader. +// If target is a non-nil pointer, it will be assigned the value of the upload identifier present +// in the response body. This function returns an error as well as a boolean flag indicating if the +// function can be retried. +func performUploadRequest(ctx context.Context, httpClient Client, opts uploadRequestOptions) (bool, error) { + req, err := makeUploadRequest(opts) + if err != nil { + return false, err + } + + resp, body, err := performRequest(ctx, req, httpClient, opts.OutputOptions.Logger) + if err != nil { + return false, err + } + + return decodeUploadPayload(resp, body, opts.Target) +} + +// makeUploadRequest creates an HTTP request to the upload endpoint described by the given arguments. +func makeUploadRequest(opts uploadRequestOptions) (*http.Request, error) { + uploadURL, err := makeUploadURL(opts) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", uploadURL.String(), opts.Payload) + if err != nil { + return nil, err + } + if opts.UncompressedSize != 0 { + req.Header.Set("X-Uncompressed-Size", strconv.Itoa(int(opts.UncompressedSize))) + } + if opts.SourcegraphInstanceOptions.AccessToken != "" { + req.Header.Set("Authorization", fmt.Sprintf("token %s", opts.SourcegraphInstanceOptions.AccessToken)) + } + + for k, v := range opts.SourcegraphInstanceOptions.AdditionalHeaders { + req.Header.Set(k, v) + } + + return req, nil +} + +// performRequest performs an HTTP request and returns the HTTP response as well as the entire +// body as a byte slice. If a logger is supplied, the request, response, and response body will +// be logged. +func performRequest(ctx context.Context, req *http.Request, httpClient Client, logger RequestLogger) (*http.Response, []byte, error) { + started := time.Now() + if logger != nil { + logger.LogRequest(req) + } + + resp, err := httpClient.Do(req.WithContext(ctx)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if logger != nil { + logger.LogResponse(req, resp, body, time.Since(started)) + } + if err != nil { + return nil, nil, err + } + + return resp, body, nil +} + +// decodeUploadPayload reads the given response to an upload request. If target is a non-nil pointer, +// it will be assigned the value of the upload identifier present in the response body. This function +// returns a boolean flag indicating if the function can be retried on failure (error-dependent). +func decodeUploadPayload(resp *http.Response, body []byte, target *int) (bool, error) { + if resp.StatusCode >= 300 { + if resp.StatusCode == http.StatusUnauthorized { + return false, ErrUnauthorized + } + + suffix := "" + if !bytes.HasPrefix(bytes.TrimSpace(body), []byte{'<'}) { + suffix = fmt.Sprintf(" (%s)", bytes.TrimSpace(body)) + } + + // Do not retry client errors + return resp.StatusCode >= 500, errors.Errorf("unexpected status code: %d%s", resp.StatusCode, suffix) + } + + if target == nil { + // No target expected, skip decoding body + return false, nil + } + + var respPayload struct { // See UploadAPIResult for the dual of this type + ID string `json:"id"` + } + if err := json.Unmarshal(body, &respPayload); err != nil { + return false, errors.Errorf("unexpected response (%s)", err) + } + + id, err := strconv.Atoi(respPayload.ID) + if err != nil { + return false, errors.Errorf("unexpected response (%s)", err) + } + + *target = id + return false, nil +} + +// makeUploadURL creates a URL pointing to the configured Sourcegraph upload +// endpoint with the query string described by the given request options. +func makeUploadURL(opts uploadRequestOptions) (*url.URL, error) { + qs := url.Values{} + + if opts.SourcegraphInstanceOptions.GitHubToken != "" { + qs.Add("github_token", opts.SourcegraphInstanceOptions.GitHubToken) + } + if opts.SourcegraphInstanceOptions.GitLabToken != "" { + qs.Add("gitlab_token", opts.SourcegraphInstanceOptions.GitLabToken) + } + if opts.UploadRecordOptions.Repo != "" { + qs.Add("repository", opts.UploadRecordOptions.Repo) + } + if opts.UploadRecordOptions.Commit != "" { + qs.Add("commit", opts.UploadRecordOptions.Commit) + } + if opts.UploadRecordOptions.Root != "" { + qs.Add("root", opts.UploadRecordOptions.Root) + } + if opts.UploadRecordOptions.Indexer != "" { + qs.Add("indexerName", opts.UploadRecordOptions.Indexer) + } + if opts.UploadRecordOptions.IndexerVersion != "" { + qs.Add("indexerVersion", opts.UploadRecordOptions.IndexerVersion) + } + if opts.UploadRecordOptions.AssociatedIndexID != nil { + qs.Add("associatedIndexId", formatInt(*opts.UploadRecordOptions.AssociatedIndexID)) + } + if opts.MultiPart { + qs.Add("multiPart", "true") + } + if opts.NumParts != 0 { + qs.Add("numParts", formatInt(opts.NumParts)) + } + if opts.UploadID != 0 { + qs.Add("uploadId", formatInt(opts.UploadID)) + } + if opts.UploadID != 0 && !opts.MultiPart && !opts.Done { + // Do not set an index of zero unless we're uploading a part + qs.Add("index", formatInt(opts.Index)) + } + if opts.Done { + qs.Add("done", "true") + } + + path := opts.SourcegraphInstanceOptions.Path + if path == "" { + path = "/.api/lsif/upload" + } + + parsedUrl, err := url.Parse(opts.SourcegraphInstanceOptions.SourcegraphURL + path) + if err != nil { + return nil, err + } + + parsedUrl.RawQuery = qs.Encode() + return parsedUrl, nil +} + +func formatInt(v int) string { + return strconv.FormatInt(int64(v), 10) +} diff --git a/lib/codeintel/upload/request_logger.go b/lib/codeintel/upload/request_logger.go new file mode 100644 index 0000000000..8df7e559c0 --- /dev/null +++ b/lib/codeintel/upload/request_logger.go @@ -0,0 +1,90 @@ +package upload + +import ( + "fmt" + "io" + "net/http" + "sort" + "time" +) + +type RequestLogger interface { + // LogRequest is invoked with a request directly before it is performed. + LogRequest(req *http.Request) + + // LogResponse is invoked with a request, response pair directly after it is performed. + LogResponse(req *http.Request, resp *http.Response, body []byte, elapsed time.Duration) +} + +type RequestLoggerVerbosity int + +const ( + RequestLoggerVerbosityNone RequestLoggerVerbosity = iota // -trace=0 (default) + RequestLoggerVerbosityTrace // -trace=1 + RequestLoggerVerbosityTraceShowHeaders // -trace=2 + RequestLoggerVerbosityTraceShowResponseBody // -trace=3 +) + +// NewRequestLogger creates a new request logger that writes requests and response pairs +// to the given writer. +func NewRequestLogger(w io.Writer, verbosity RequestLoggerVerbosity) RequestLogger { + return &requestLogger{ + writer: w, + verbosity: verbosity} +} + +func (l *requestLogger) LogRequest(req *http.Request) { + if l.verbosity == RequestLoggerVerbosityNone { + return + } + + if l.verbosity >= RequestLoggerVerbosityTrace { + fmt.Fprintf(l.writer, "> %s %s\n", req.Method, req.URL) + } + + if l.verbosity >= RequestLoggerVerbosityTraceShowHeaders { + fmt.Fprintf(l.writer, "> Request Headers:\n") + for _, k := range sortHeaders(req.Header) { + fmt.Fprintf(l.writer, "> %s: %s\n", k, req.Header[k]) + } + } + + fmt.Fprintf(l.writer, "\n") +} + +type requestLogger struct { + writer io.Writer + verbosity RequestLoggerVerbosity +} + +func (l *requestLogger) LogResponse(req *http.Request, resp *http.Response, body []byte, elapsed time.Duration) { + if l.verbosity == RequestLoggerVerbosityNone { + return + } + + if l.verbosity >= RequestLoggerVerbosityTrace { + fmt.Fprintf(l.writer, "< %s %s %s in %s\n", req.Method, req.URL, resp.Status, elapsed) + } + + if l.verbosity >= RequestLoggerVerbosityTraceShowHeaders { + fmt.Fprintf(l.writer, "< Response Headers:\n") + for _, k := range sortHeaders(resp.Header) { + fmt.Fprintf(l.writer, "< %s: %s\n", k, resp.Header[k]) + } + } + + if l.verbosity >= RequestLoggerVerbosityTraceShowResponseBody { + fmt.Fprintf(l.writer, "< Response Body: %s\n", body) + } + + fmt.Fprintf(l.writer, "\n") +} + +func sortHeaders(header http.Header) []string { + var keys []string + for k := range header { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/lib/codeintel/upload/retry.go b/lib/codeintel/upload/retry.go new file mode 100644 index 0000000000..7c70735827 --- /dev/null +++ b/lib/codeintel/upload/retry.go @@ -0,0 +1,37 @@ +package upload + +import ( + "time" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// RetryableFunc is a function that takes the invocation index and returns an error as well as a +// boolean-value flag indicating whether or not the error is considered retryable. +type RetryableFunc = func(attempt int) (bool, error) + +// makeRetry returns a function that calls retry with the given max attempt and interval values. +func makeRetry(n int, interval time.Duration) func(f RetryableFunc) error { + return func(f RetryableFunc) error { + return retry(f, n, interval) + } +} + +// retry will re-invoke the given function until it returns a nil error value, the function returns +// a non-retryable error (as indicated by its boolean return value), or until the maximum number of +// retries have been attempted. All errors encountered will be returned. +func retry(f RetryableFunc, n int, interval time.Duration) (errs error) { + for i := 0; i <= n; i++ { + retry, err := f(i) + + errs = errors.CombineErrors(errs, err) + + if err == nil || !retry { + break + } + + time.Sleep(interval) + } + + return errs +} diff --git a/lib/codeintel/upload/upload.go b/lib/codeintel/upload/upload.go new file mode 100644 index 0000000000..097990d1b8 --- /dev/null +++ b/lib/codeintel/upload/upload.go @@ -0,0 +1,369 @@ +package upload + +import ( + "context" + "fmt" + "io" + "os" + "sync" + + "github.com/sourcegraph/conc/pool" + + "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/sourcegraph/lib/output" +) + +// UploadIndex uploads the index file described by the given options to a Sourcegraph +// instance. If the upload file is large, it may be split into multiple segments and +// uploaded over multiple requests. The identifier of the upload is returned after a +// successful upload. +func UploadIndex(ctx context.Context, filename string, httpClient Client, opts UploadOptions) (int, error) { + originalReader, originalSize, err := openFileAndGetSize(filename) + if err != nil { + return 0, err + } + defer func() { + _ = originalReader.Close() + }() + + bars := []output.ProgressBar{{Label: "Compressing", Max: 1.0}} + progress, _, cleanup := logProgress( + opts.Output, + bars, + "Index compressed", + "Failed to compress index", + ) + + compressedFile, err := compressReaderToDisk(originalReader, originalSize, progress) + if err != nil { + cleanup(err) + return 0, err + } + defer func() { + _ = os.Remove(compressedFile) + }() + + compressedReader, compressedSize, err := openFileAndGetSize(compressedFile) + if err != nil { + cleanup(err) + return 0, err + } + defer func() { + _ = compressedReader.Close() + }() + + cleanup(nil) + + if opts.Output != nil { + opts.Output.WriteLine(output.Linef( + output.EmojiLightbulb, + output.StyleItalic, + "Indexed compressed (%.2fMB -> %.2fMB).", + float64(originalSize)/1000/1000, + float64(compressedSize)/1000/1000, + )) + } + + if compressedSize <= opts.MaxPayloadSizeBytes { + return uploadIndex(ctx, httpClient, opts, compressedReader, compressedSize, originalSize) + } + + return uploadMultipartIndex(ctx, httpClient, opts, compressedReader, compressedSize, originalSize) +} + +// uploadIndex uploads the index file described by the given options to a Sourcegraph +// instance via a single HTTP POST request. The identifier of the upload is returned +// after a successful upload. +func uploadIndex(ctx context.Context, httpClient Client, opts UploadOptions, r io.ReaderAt, readerLen, uncompressedSize int64) (id int, err error) { + bars := []output.ProgressBar{{Label: "Upload", Max: 1.0}} + progress, retry, complete := logProgress( + opts.Output, + bars, + "Index uploaded", + "Failed to upload index file", + ) + defer func() { complete(err) }() + + // Create a section reader that can reset our reader view for retries + reader := io.NewSectionReader(r, 0, readerLen) + + requestOptions := uploadRequestOptions{ + UploadOptions: opts, + Target: &id, + UncompressedSize: uncompressedSize, + } + err = uploadIndexFile(ctx, httpClient, opts, reader, readerLen, requestOptions, progress, retry, 0, 1) + + if progress != nil { + // Mark complete in case we debounced our last updates + progress.SetValue(0, 1) + } + + return id, err +} + +// uploadIndexFile uploads the contents available via the given reader to a +// Sourcegraph instance with the given request options.i +func uploadIndexFile(ctx context.Context, httpClient Client, uploadOptions UploadOptions, reader io.ReadSeeker, readerLen int64, requestOptions uploadRequestOptions, progress output.Progress, retry onRetryLogFn, barIndex int, numParts int) error { + retrier := makeRetry(uploadOptions.MaxRetries, uploadOptions.RetryInterval) + + return retrier(func(attempt int) (_ bool, err error) { + defer func() { + if err != nil && !errors.Is(err, ctx.Err()) && progress != nil { + progress.SetValue(barIndex, 0) + } + }() + + if attempt != 0 { + suffix := "" + if numParts != 1 { + suffix = fmt.Sprintf(" %d of %d", barIndex+1, numParts) + } + + if progress != nil { + progress.SetValue(barIndex, 0) + } + progress = retry(fmt.Sprintf("Failed to upload index file%s (will retry; attempt #%d)", suffix, attempt)) + } + + // Create fresh reader on each attempt + reader.Seek(0, io.SeekStart) + + // Report upload progress as writes occur + requestOptions.Payload = newProgressCallbackReader(reader, readerLen, progress, barIndex) + + // Perform upload + return performUploadRequest(ctx, httpClient, requestOptions) + }) +} + +// uploadMultipartIndex uploads the index file described by the given options to a +// Sourcegraph instance over multiple HTTP POST requests. The identifier of the upload +// is returned after a successful upload. +func uploadMultipartIndex(ctx context.Context, httpClient Client, opts UploadOptions, r io.ReaderAt, readerLen, uncompressedSize int64) (_ int, err error) { + // Create a slice of section readers for upload part retries. + // This allows us to both read concurrently from the same reader, + // but also retry reads from arbitrary offsets. + readers := splitReader(r, readerLen, opts.MaxPayloadSizeBytes) + + // Perform initial request that gives us our upload identifier + id, err := uploadMultipartIndexInit(ctx, httpClient, opts, len(readers), uncompressedSize) + if err != nil { + return 0, err + } + + // Upload each payload of the multipart index + if err := uploadMultipartIndexParts(ctx, httpClient, opts, readers, id, readerLen); err != nil { + return 0, err + } + + // Finalize the upload and mark it as ready for processing + if err := uploadMultipartIndexFinalize(ctx, httpClient, opts, id); err != nil { + return 0, err + } + + return id, nil +} + +// uploadMultipartIndexInit performs an initial request to prepare the backend to accept upload +// parts via additional HTTP requests. This upload will be in a pending state until all upload +// parts are received and the multipart upload is finalized, or until the record is deleted by +// a background process after an expiry period. +func uploadMultipartIndexInit(ctx context.Context, httpClient Client, opts UploadOptions, numParts int, uncompressedSize int64) (id int, err error) { + retry, complete := logPending( + opts.Output, + "Preparing multipart upload", + "Prepared multipart upload", + "Failed to prepare multipart upload", + ) + defer func() { complete(err) }() + + err = makeRetry(opts.MaxRetries, opts.RetryInterval)(func(attempt int) (bool, error) { + if attempt != 0 { + retry(fmt.Sprintf("Failed to prepare multipart upload (will retry; attempt #%d)", attempt)) + } + + return performUploadRequest(ctx, httpClient, uploadRequestOptions{ + UploadOptions: opts, + Target: &id, + MultiPart: true, + NumParts: numParts, + UncompressedSize: uncompressedSize, + }) + }) + + return id, err +} + +// uploadMultipartIndexParts uploads the contents available via each of the given reader(s) +// to a Sourcegraph instance as part of the same multipart upload as indiciated +// by the given identifier. +func uploadMultipartIndexParts(ctx context.Context, httpClient Client, opts UploadOptions, readers []io.ReadSeeker, id int, readerLen int64) (err error) { + var bars []output.ProgressBar + for i := range readers { + label := fmt.Sprintf("Upload part %d of %d", i+1, len(readers)) + bars = append(bars, output.ProgressBar{Label: label, Max: 1.0}) + } + progress, retry, complete := logProgress( + opts.Output, + bars, + "Index parts uploaded", + "Failed to upload index parts", + ) + defer func() { complete(err) }() + + pool := new(pool.ErrorPool).WithFirstError().WithContext(ctx) + if opts.MaxConcurrency > 0 { + pool.WithMaxGoroutines(opts.MaxConcurrency) + } + + for i, reader := range readers { + i, reader := i, reader + + pool.Go(func(ctx context.Context) error { + // Determine size of this reader. If we're not the last reader in the slice, + // then we're the maximum payload size. Otherwise, we're whatever is left. + partReaderLen := opts.MaxPayloadSizeBytes + if i == len(readers)-1 { + partReaderLen = readerLen - int64(len(readers)-1)*opts.MaxPayloadSizeBytes + } + + requestOptions := uploadRequestOptions{ + UploadOptions: opts, + UploadID: id, + Index: i, + } + + if err := uploadIndexFile(ctx, httpClient, opts, reader, partReaderLen, requestOptions, progress, retry, i, len(readers)); err != nil { + return err + } else if progress != nil { + // Mark complete in case we debounced our last updates + progress.SetValue(i, 1) + } + return nil + }) + } + + return pool.Wait() +} + +// uploadMultipartIndexFinalize performs the request to stitch the uploaded parts together and +// mark it ready as processing in the backend. +func uploadMultipartIndexFinalize(ctx context.Context, httpClient Client, opts UploadOptions, id int) (err error) { + retry, complete := logPending( + opts.Output, + "Finalizing multipart upload", + "Finalized multipart upload", + "Failed to finalize multipart upload", + ) + defer func() { complete(err) }() + + return makeRetry(opts.MaxRetries, opts.RetryInterval)(func(attempt int) (bool, error) { + if attempt != 0 { + retry(fmt.Sprintf("Failed to finalize multipart upload (will retry; attempt #%d)", attempt)) + } + + return performUploadRequest(ctx, httpClient, uploadRequestOptions{ + UploadOptions: opts, + UploadID: id, + Done: true, + }) + }) +} + +// splitReader returns a slice of read-seekers into the input ReaderAt, each of max size maxPayloadSize. +// +// The sequential concatenation of each reader produces the content of the original reader. +// +// Each reader is safe to use concurrently with others. The original reader should be closed when all produced +// readers are no longer active. +func splitReader(r io.ReaderAt, n, maxPayloadSize int64) (readers []io.ReadSeeker) { + for offset := int64(0); offset < n; offset += maxPayloadSize { + readers = append(readers, io.NewSectionReader(r, offset, maxPayloadSize)) + } + + return readers +} + +// openFileAndGetSize returns an open file handle and the size on disk for the given filename. +func openFileAndGetSize(filename string) (*os.File, int64, error) { + fileInfo, err := os.Stat(filename) + if err != nil { + return nil, 0, err + } + + file, err := os.Open(filename) + if err != nil { + return nil, 0, err + } + + return file, fileInfo.Size(), err +} + +// logPending creates a pending object from the given output value and returns a retry function that +// can be called to print a message then reset the pending display, and a complete function that should +// be called once the work attached to this log call has completed. This complete function takes an error +// value that determines whether the success or failure message is displayed. If the given output value is +// nil then a no-op complete function is returned. +func logPending(out *output.Output, pendingMessage, successMessage, failureMessage string) (func(message string), func(error)) { + if out == nil { + return func(message string) {}, func(err error) {} + } + + pending := out.Pending(output.Line("", output.StylePending, pendingMessage)) + + retry := func(message string) { + pending.Destroy() + out.WriteLine(output.Line(output.EmojiFailure, output.StyleReset, message)) + pending = out.Pending(output.Line("", output.StylePending, pendingMessage)) + } + + complete := func(err error) { + if err == nil { + pending.Complete(output.Line(output.EmojiSuccess, output.StyleSuccess, successMessage)) + } else { + pending.Complete(output.Line(output.EmojiFailure, output.StyleBold, failureMessage)) + } + } + + return retry, complete +} + +type onRetryLogFn func(message string) output.Progress + +// logProgress creates and returns a progress from the given output value and bars configuration. +// This function also returns a retry function that can be called to print a message then reset the +// progress bar display, and a complete function that should be called once the work attached to +// this log call has completed. This complete function takes an error value that determines whether +// the success or failure message is displayed. If the given output value is nil then a no-op complete +// function is returned. +func logProgress(out *output.Output, bars []output.ProgressBar, successMessage, failureMessage string) (output.Progress, onRetryLogFn, func(error)) { + if out == nil { + return nil, func(message string) output.Progress { return nil }, func(err error) {} + } + + var mu sync.Mutex + progress := out.Progress(bars, nil) + + retry := func(message string) output.Progress { + mu.Lock() + defer mu.Unlock() + + progress.Destroy() + out.WriteLine(output.Line(output.EmojiFailure, output.StyleReset, message)) + progress = out.Progress(bars, nil) + return progress + } + + complete := func(err error) { + progress.Destroy() + + if err == nil { + out.WriteLine(output.Line(output.EmojiSuccess, output.StyleSuccess, successMessage)) + } else { + out.WriteLine(output.Line(output.EmojiFailure, output.StyleBold, failureMessage)) + } + } + + return progress, retry, complete +} diff --git a/lib/codeintel/upload/upload_options.go b/lib/codeintel/upload/upload_options.go new file mode 100644 index 0000000000..e8f498399e --- /dev/null +++ b/lib/codeintel/upload/upload_options.go @@ -0,0 +1,47 @@ +package upload + +import ( + "net/http" + "time" + + "github.com/sourcegraph/sourcegraph/lib/output" +) + +type Client interface { + // Do runs an http.Request against the Sourcegraph API. + Do(req *http.Request) (*http.Response, error) +} + +type UploadOptions struct { + SourcegraphInstanceOptions + OutputOptions + UploadRecordOptions +} + +type SourcegraphInstanceOptions struct { + SourcegraphURL string // The URL (including scheme) of the target Sourcegraph instance + AccessToken string // The user access token + AdditionalHeaders map[string]string // Additional request headers on each request + Path string // Custom path on the Sourcegraph instance (used internally) + MaxRetries int // The maximum number of retries per request + RetryInterval time.Duration // Sleep duration between retries + MaxPayloadSizeBytes int64 // The maximum number of bytes sent in a single request + MaxConcurrency int // The maximum number of concurrent uploads. Only relevant for multipart uploads + GitHubToken string // GitHub token used for auth when lsif.enforceAuth is true (optional) + GitLabToken string // GitLab token used for auth when lsif.enforceAuth is true (optional) + HTTPClient Client +} + +type OutputOptions struct { + Logger RequestLogger // Logger of all HTTP request/responses (optional) + Output *output.Output // Output instance used for fancy output (optional) +} + +type UploadRecordOptions struct { + Repo string + Commit string + Root string + Indexer string + IndexerVersion string + AssociatedIndexID *int +} diff --git a/lib/errors/client_error.go b/lib/errors/client_error.go new file mode 100644 index 0000000000..505646a1cf --- /dev/null +++ b/lib/errors/client_error.go @@ -0,0 +1,26 @@ +package errors + +// ClientError indicates that the error is due to a mistake in +// the API client's request (4xx in HTTP), it's not a server-internal +// error (5xx in HTTP). +// +// GraphQL doesn't require tracking this separately, but it's useful +// for making sure that we can set SLOs against server errors. +// +// For example, on Sourcegraph.com, people can send us arbitrary GraphQL +// requests; it wouldn't make sense for errors in processing malformed +// requests to count against our (internal) SLO. +type ClientError struct { + Err error +} + +var _ error = ClientError{nil} +var _ Wrapper = ClientError{nil} + +func (e ClientError) Error() string { + return e.Err.Error() +} + +func (e ClientError) Unwrap() error { + return e.Err +} diff --git a/lib/errors/cockroach.go b/lib/errors/cockroach.go new file mode 100644 index 0000000000..7a8632722d --- /dev/null +++ b/lib/errors/cockroach.go @@ -0,0 +1,186 @@ +package errors + +import ( + "fmt" + "os" + "reflect" + "time" + + "github.com/cockroachdb/errors" //nolint:depguard + "github.com/cockroachdb/redact" +) + +func init() { + registerCockroachSafeTypes() +} + +var ( + // Safe is a arg marker for non-PII arguments. + Safe = redact.Safe + + New = errors.New + // Newf assumes all args are unsafe PII, except for types in registerCockroachSafeTypes. + // Use Safe to mark non-PII args. Contents of format are retained. + Newf = errors.Newf + // Errorf is the same as Newf. It assumes all args are unsafe PII, except for types + // in registerCockroachSafeTypes. Use Safe to mark non-PII args. Contents of format + // are retained. + Errorf = errors.Newf + + Wrap = errors.Wrap + // Wrapf assumes all args are unsafe PII, except for types in registerCockroachSafeTypes. + // Use Safe to mark non-PII args. Contents of format are retained. + Wrapf = errors.Wrapf + // WithMessage is the same as Wrap. + WithMessage = errors.Wrap + + // WithStack annotates err with a stack trace at the point WithStack was + // called. Useful for sentinel errors. + WithStack = errors.WithStack + + // WithSafeDetails annotates an error with the given reportable details. + // The format is made available as a PII-free string, alongside + // with a PII-free representation of every additional argument. + // Arguments can be reported as-is (without redaction) by wrapping + // them using the Safe() function. + // + // If the format is empty and there are no arguments, the + // error argument is returned unchanged. + // + // Detail is shown: + // - when formatting with `%+v`. + // - in Sentry reports. + WithSafeDetails = errors.WithSafeDetails + + // Is checks if the error tree err is equal to the value target. + // + // For error types which do not contain any data, Is is equivalent to As. + // + // For error types which contain data, it's possible that Is + // returns true for a value other than the one returned by As, + // since an error tree can contain multiple errors of the same + // concrete type but with different data. + Is = errors.Is + IsAny = errors.IsAny + Cause = errors.Cause + Unwrap = errors.Unwrap + UnwrapAll = errors.UnwrapAll + + BuildSentryReport = errors.BuildSentryReport +) + +// As checks if the error tree err is of type target, and if so, +// sets target to the value of the error. +// +// If looking for an error of concrete type T, then the second +// argument must be a non-nil pointer of type *T. This implies that +// if the error interface is implemented with a pointer receiver, +// then target must be of type **MyConcreteType. +// +// For error types which do not contain any data, As is equivalent to Is. +// +// For error types which contain data, As will return an arbitrary +// error of the target type, in case there are multiple errors of the +// same concrete type in the error tree. +// +// Compared to errors.As, this method uses a generic argument to prevent +// a runtime panic when target is not a pointer to an error type. +// +// Use AsInterface over this function for interface targets. +func As[T error](err error, target *T) bool { + return errors.As(err, target) +} + +// AsInterface checks if the error tree err is of type target (which must be +// an interface type), and if so, sets target to the value of the error. +// +// In general, 'I' may be any interface, not just an error interface. +// See internal/errcode/code.go for some examples. +// +// Use As over this function for concrete types. +func AsInterface[I any](err error, target *I) bool { + if target == nil { + panic("Expected non-nil pointer to interface") + } + if typ := reflect.TypeOf(target); typ.Elem().Kind() != reflect.Interface { + panic("Expected pointer to interface") + } + return errors.As(err, target) +} + +// HasType checks if the error tree err has a node of type T. +// +// CAVEAT: HasType is implemented via As. So strictly speaking, it is +// possible that HasType returns true via some implementation of +// `interface { As(target any) bool }` in the error tree that +// doesn't actually check the type. +func HasType[T error](err error) bool { + // At the moment, the cockroachdb/errors package's implementation + // of HasType does not correctly handle multi-errors, whereas As does, + // so we implement HasType via As. + // (See https://github.com/cockroachdb/errors/issues/145) + var zero T + return As(err, &zero) +} + +// Extend multiError to work with cockroachdb errors. Implement here to keep imports in +// one place. + +var _ fmt.Formatter = (*multiError)(nil) + +func (e *multiError) Format(s fmt.State, verb rune) { errors.FormatError(e, s, verb) } + +var _ errors.Formatter = (*multiError)(nil) + +func (e *multiError) FormatError(p errors.Printer) error { + if len(e.errs) > 1 { + p.Printf("%d errors occurred:", len(e.errs)) + } + + // Simple output + for _, err := range e.errs { + if len(e.errs) > 1 { + p.Print("\n\t* ") + } + p.Printf("%v", err) + } + + // Print additional details + if p.Detail() { + p.Print("-- details follow") + for i, err := range e.errs { + p.Printf("\n(%d) %+v", i+1, err) + } + } + + return nil +} + +// registerSafeTypes registers types that should not be considered PII by +// cockroachdb/errors. +// +// Sourced from https://sourcegraph.com/github.com/cockroachdb/cockroach/-/blob/pkg/util/log/redact.go?L141 +func registerCockroachSafeTypes() { + // We consider booleans and numeric values to be always safe for + // reporting. A log call can opt out by using redact.Unsafe() around + // a value that would be otherwise considered safe. + redact.RegisterSafeType(reflect.TypeOf(true)) // bool + redact.RegisterSafeType(reflect.TypeOf(123)) // int + redact.RegisterSafeType(reflect.TypeOf(int8(0))) + redact.RegisterSafeType(reflect.TypeOf(int16(0))) + redact.RegisterSafeType(reflect.TypeOf(int32(0))) + redact.RegisterSafeType(reflect.TypeOf(int64(0))) + redact.RegisterSafeType(reflect.TypeOf(uint8(0))) + redact.RegisterSafeType(reflect.TypeOf(uint16(0))) + redact.RegisterSafeType(reflect.TypeOf(uint32(0))) + redact.RegisterSafeType(reflect.TypeOf(uint64(0))) + redact.RegisterSafeType(reflect.TypeOf(float32(0))) + redact.RegisterSafeType(reflect.TypeOf(float64(0))) + redact.RegisterSafeType(reflect.TypeOf(complex64(0))) + redact.RegisterSafeType(reflect.TypeOf(complex128(0))) + // Signal names are also safe for reporting. + redact.RegisterSafeType(reflect.TypeOf(os.Interrupt)) + // Times and durations too. + redact.RegisterSafeType(reflect.TypeOf(time.Time{})) + redact.RegisterSafeType(reflect.TypeOf(time.Duration(0))) +} diff --git a/lib/errors/errors.go b/lib/errors/errors.go new file mode 100644 index 0000000000..37ff23f126 --- /dev/null +++ b/lib/errors/errors.go @@ -0,0 +1,24 @@ +package errors + +// Typed is an interface custom error types that want to be checkable with errors.Is, +// errors.As can implement for more predicable behaviour. Learn more about error checking: +// https://pkg.go.dev/errors#pkg-overview +// +// In all implementations, the error should not attempt to unwrap itself or the target. +type Typed interface { + // As sets the target to the error value of this type if target is of the same type as + // this error. + // + // See https://pkg.go.dev/errors#example-As + As(target any) bool + // Is reports whether this error matches the target. + // + // See: https://pkg.go.dev/errors#example-Is + Is(target error) bool +} + +// Wrapper is an interface custom error types that carry errors internally should +// implement. +type Wrapper interface { + Unwrap() error +} diff --git a/lib/errors/filter.go b/lib/errors/filter.go new file mode 100644 index 0000000000..909e72d74f --- /dev/null +++ b/lib/errors/filter.go @@ -0,0 +1,51 @@ +package errors + +import ( + "context" +) + +// Ignore filters out any errors that match pred. This applies +// recursively to MultiErrors, filtering out any child errors +// that match `pred`, or returning `nil` if all of the child +// errors match `pred`. +func Ignore(err error, pred ErrorPredicate) error { + // If the error (or any wrapped error) is a multierror, + // filter its children. + var multi *multiError + if As(err, &multi) { + filtered := multi.errs[:0] + for _, childErr := range multi.errs { + if ignored := Ignore(childErr, pred); ignored != nil { + filtered = append(filtered, ignored) + } + } + if len(filtered) == 0 { + return nil + } + multi.errs = filtered + return err + } + + if pred(err) { + return nil + } + return err +} + +// ErrorPredicate is a function type that returns whether an error matches a given condition +type ErrorPredicate func(error) bool + +// IsPred returns an ErrorPredicate that returns true for errors that uwrap to the target error +func IsPred(target error) ErrorPredicate { + return func(err error) bool { + return Is(err, target) + } +} + +func IsContextCanceled(err error) bool { + return Is(err, context.Canceled) +} + +func IsDeadlineExceeded(err error) bool { + return Is(err, context.DeadlineExceeded) +} diff --git a/lib/errors/multi_error.go b/lib/errors/multi_error.go new file mode 100644 index 0000000000..452061473e --- /dev/null +++ b/lib/errors/multi_error.go @@ -0,0 +1,113 @@ +package errors + +import ( + "fmt" + + "github.com/cockroachdb/errors" //nolint:depguard // needed for implementation of multiError.As +) + +// MultiError is a container for groups of errors. +type MultiError interface { + error + // Errors returns all errors carried by this MultiError, or an empty slice otherwise. + Errors() []error +} + +// multiError is our default underlying implementation for MultiError. It is compatible +// with cockroachdb.Error's formatting, printing, etc. and supports introspecting via +// As, Is, and friends. +// +// Implementation is based on https://github.com/knz/shakespeare/blob/master/pkg/cmd/errors.go +type multiError struct { + errs []error +} + +var _ MultiError = (*multiError)(nil) +var _ Typed = (*multiError)(nil) + +func combineNonNilErrors(err1 error, err2 error) MultiError { + multi1, ok1 := err1.(MultiError) + multi2, ok2 := err2.(MultiError) + // flatten + var errs []error + if ok1 && ok2 { + errs = append(multi1.Errors(), multi2.Errors()...) + } else if ok1 { + errs = append(multi1.Errors(), err2) + } else if ok2 { + errs = append([]error{err1}, multi2.Errors()...) + } else { + errs = []error{err1, err2} + } + return &multiError{errs: errs} +} + +// CombineErrors returns a MultiError from err1 and err2. If both are nil, nil is returned. +func CombineErrors(err1, err2 error) MultiError { + if err1 == nil && err2 == nil { + return nil + } + if err1 == nil { + if multi, ok := err2.(MultiError); ok { + return multi + } + return &multiError{errs: []error{err2}} + } else if err2 == nil { + if multi, ok := err1.(MultiError); ok { + return multi + } + return &multiError{errs: []error{err1}} + } + return combineNonNilErrors(err1, err2) +} + +// Append returns a MultiError created from all given errors, skipping errs that are nil. +// If no non-nil errors are provided, nil is returned. +func Append(err error, errs ...error) MultiError { + multi := CombineErrors(err, nil) + for _, e := range errs { + if e != nil { + multi = CombineErrors(multi, e) + } + } + return multi +} + +func (e *multiError) Error() string { return fmt.Sprintf("%v", e) } +func (e *multiError) Errors() []error { + if e == nil || e.errs == nil { + return nil + } + return e.errs +} + +func (e *multiError) Cause() error { return e.errs[len(e.errs)-1] } +func (e *multiError) Unwrap() error { return e.errs[len(e.errs)-1] } + +func (e *multiError) Is(refError error) bool { + if e == refError { + return true + } + for _, err := range e.errs { + if Is(err, refError) { + return true + } + } + return false +} + +func (e *multiError) As(target any) bool { + if m, ok := target.(*multiError); ok { + *m = *e + return true + } + for _, err := range e.errs { + // To conform to the Typed interface, 'target' has to be of type + // any. This means we cannot use our custom As wrapper which has + // a generic argument, so use cockroachdb's As instead. + if errors.As(err, target) { + return true + } + } + return false +} diff --git a/lib/errors/postgres.go b/lib/errors/postgres.go new file mode 100644 index 0000000000..a9696a2d0d --- /dev/null +++ b/lib/errors/postgres.go @@ -0,0 +1,12 @@ +package errors + +import ( + "github.com/jackc/pgconn" +) + +// HasPostgresCode checks whether any of the errors in the chain +// signify a postgres error with the given error code. +func HasPostgresCode(err error, code string) bool { + var pgerr *pgconn.PgError + return As(err, &pgerr) && pgerr.Code == code +} diff --git a/lib/errors/rich_error.go b/lib/errors/rich_error.go new file mode 100644 index 0000000000..6315f3e7e1 --- /dev/null +++ b/lib/errors/rich_error.go @@ -0,0 +1,171 @@ +package errors + +import ( + "reflect" + "slices" + "sync" + + "go.opentelemetry.io/otel/attribute" +) + +type RichError interface { + // Unpack extracts the underlying error and attributes from a RichError. + // + // Unpacking should only return attributes one-level deep; a caller may + // optionally continue recursively unpacking the underlying error. + Unpack() RichErrorData +} + +type RichErrorData struct { + Err error + Attrs []attribute.KeyValue +} + +type PtrRichError[A any] interface { + *A + RichError +} + +// NewRichError is a helper function for wrapping custom errors +// for the observation package. +// +// Pre-condition: The outermost level of ptr must be non-nil. +// +// Generally, this is easy to satisfy by using & on a named return value. +// +// The type signature is more complicated than usual to avoid an extra +// level of pointer indirection when the type implementing RichError uses +// a pointer receiver for RichError.Unpack. See rich_error_test.go for +// examples. +func NewRichError[A any, T PtrRichError[A]](ptr2 **A) *RichError { + v := RichError(NewErrorPtr[A, T](ptr2)) + return &v +} + +func UnpackRichErrorPtr(richErr *RichError) (error, []attribute.KeyValue) { + if richErr == nil || *richErr == nil { + return nil, nil + } + if val := reflect.ValueOf(*richErr); val.Kind() == reflect.Ptr && val.IsNil() { + return nil, nil + } + data := (*richErr).Unpack() + return data.Err, data.Attrs +} + +type ErrorPtr[A any, T PtrRichError[A]] struct { + ptr2 **A +} + +// nolint:unused +func typeAssertion[A any, T PtrRichError[A]]() { + var _ RichError = (*ErrorPtr[A, T])(nil) +} + +// NewErrorPtr is the only way to create an ErrorPtr. +func NewErrorPtr[A any, T PtrRichError[A]](ptr2 **A) ErrorPtr[A, T] { + if ptr2 == nil { + panic("Expected non-nil pointer to some error type") + } + return ErrorPtr[A, T]{ptr2: ptr2} +} + +func (p ErrorPtr[A, T]) Unpack() RichErrorData { + var empty RichErrorData + if p.ptr2 == nil { + panic("Incorrectly created ErrorPtr without going through NewErrorPtr") + } + ptr := *p.ptr2 + if ptr == nil { + return RichErrorData{Err: nil, Attrs: nil} + } + // We need this dynamic type checks here because Go doesn't allow + // constraints on type parameters in methods, and we want to + // have a method so that we can satisfy the RichError interface. + val, ok := any(ptr).(RichError) + if !ok { + // We need two different type checks here because RichError may be + // implemented with a struct receiver or a pointer receiver. + // See the tests for more details. + if val, ok = any(*ptr).(RichError); !ok { + panic("Dynamic type check for pointer type failed; did you create ErrorPtr without using NewErrorPtr?") + } + } + if val == nil { + return empty + } + if inner := reflect.ValueOf(val); inner.Kind() == reflect.Ptr && inner.IsNil() { + return empty + } + return val.Unpack() +} + +// ErasedErrorPtr stores a type-erased 'error' instead of +// generic **T like ErrorPtr. +type ErasedErrorPtr struct { + Err *error +} + +var _ RichError = ErasedErrorPtr{} + +func (e ErasedErrorPtr) Unpack() RichErrorData { + if e.Err == nil || *e.Err == nil { + return RichErrorData{Err: nil, Attrs: nil} + } + if re := RichError(nil); AsInterface(*e.Err, &re) { + return re.Unpack() + } + return RichErrorData{Err: *e.Err, Attrs: nil} +} + +// Collector represents multiple errors and additional log fields that arose from those errors. +// This type is thread-safe. +type Collector struct { + mu sync.Mutex + errs error + extraAttrs []attribute.KeyValue +} + +var _ RichError = (*Collector)(nil) + +func NewErrorCollector() *Collector { return &Collector{errs: nil} } + +func (e *Collector) Collect(err *error, attrs ...attribute.KeyValue) { + e.mu.Lock() + defer e.mu.Unlock() + + if err != nil && *err != nil { + e.errs = Append(e.errs, *err) + e.extraAttrs = append(e.extraAttrs, attrs...) + } +} + +func (e *Collector) Error() string { + e.mu.Lock() + defer e.mu.Unlock() + + if e.errs == nil { + return "" + } + return e.errs.Error() +} + +func (e *Collector) Unwrap() error { + // Collector wraps collected errors, for compatibility with errors.HasType, + // errors.Is etc it has to implement Unwrap to return the inner errors the + // collector stores. + e.mu.Lock() + defer e.mu.Unlock() + + return e.errs +} + +func (e *Collector) Unpack() RichErrorData { + if e.Error() == "" { + return RichErrorData{Err: nil, Attrs: nil} + } + e.mu.Lock() + defer e.mu.Unlock() + // Explicitly clone slice for 1 level of thread safety. + return RichErrorData{Err: e.errs, Attrs: slices.Clone(e.extraAttrs)} +} diff --git a/lib/errors/tree.go b/lib/errors/tree.go new file mode 100644 index 0000000000..30656973ff --- /dev/null +++ b/lib/errors/tree.go @@ -0,0 +1,56 @@ +package errors + +import ( + "reflect" + + "github.com/xlab/treeprint" +) + +type TreeNode struct { + TypeName string + Children []*TreeNode +} + +func (n *TreeNode) String() string { + return debugTreeRecursive(n).String() +} + +func debugTreeRecursive(n *TreeNode) treeprint.Tree { + if n == nil { + return treeprint.NewWithRoot("") + } + t := treeprint.NewWithRoot(n.TypeName) + for _, child := range n.Children { + t.AddNode(debugTreeRecursive(child)) + } + return t +} + +// UnpackTree returns a tree of error types and their children. +// +// This function is meant for debugging issues when you're trying to write +// a test for error handling, and want to inspect the structure of an error +// tree. +// +// DO NOT use this function for matching errors -- use errors.Is, errors.As etc. +func UnpackTree(err error) *TreeNode { + if err == nil { + return nil + } + typeName := reflect.TypeOf(err).String() + children := []*TreeNode{} + if e, ok := err.(interface{ Unwrap() []error }); ok { + for _, suberr := range e.Unwrap() { + children = append(children, UnpackTree(suberr)) + } + } else if e, ok := err.(MultiError); ok { + for _, suberr := range e.Errors() { + children = append(children, UnpackTree(suberr)) + } + } else if e, ok := err.(Wrapper); ok { + children = append(children, UnpackTree(e.Unwrap())) + } else if e, ok := err.(interface{ Cause() error }); ok { + children = append(children, UnpackTree(e.Cause())) + } + return &TreeNode{typeName, children} +} diff --git a/lib/errors/warning.go b/lib/errors/warning.go new file mode 100644 index 0000000000..b1d4255d75 --- /dev/null +++ b/lib/errors/warning.go @@ -0,0 +1,96 @@ +package errors + +// Warning embeds an error. Its purpose is to indicate that this error is not a critical error and +// may be ignored. Additionally, it **must** be logged only as a warning. If it cannot be logged as a +// warning, then these are not the droids you're looking for. +type Warning interface { + error + // IsWarning should always return true. It exists to differentiate regular errors with Warning + // errors. That is, all Warning type objects are error types, but not all error types are + // Warning types. + IsWarning() bool +} + +// warning is the error that wraps an error that is meant to be handled as a warning and not a +// critical error. +// +// AUTHOR'S NOTE +// +// @indradhanush: This type does not need a method `As(any) bool` and can be "asserted" with +// errors.As (see example below) when the underlying package being used is cockroachdb/errors. The +// `As` method from the cockroachdb/errors library is able to distinguish between warning and native +// error types. +// +// When writing this part of the code, I had implemented an `As(any) bool` method into this struct +// but it never got invoked and the corresponding tests in TestWarningError still pass the +// assertions. However after further deliberations during code review, I'm choosing to keep it as +// part of the method list of this type with an aim for interoperability in the future. But the +// method is a NOOP. The good news is that I've also added a test for this method in +// TestWarningError. +type warning struct { + error error +} + +// Ensure that warning always implements the Warning error interface. +var _ Warning = (*warning)(nil) + +// NewWarningError will return an error of type warning. This should be used to wrap errors where we +// do not intend to return an error, increment an error metric. That is, if an error is returned and +// it is not critical and / or expected to be intermittent and / or nothing we can do about +// (example: 404 errors from upstream code host APIs in repo syncing), we should wrap the error with +// NewWarningError. +// +// Consumers of these errors should then use errors.As to check if the error is of a warning type +// and based on that, should just log it as a warning. For example: +// +// var ref errors.Warning +// err := someFunctionThatReturnsAWarningErrorOrACriticalError() +// if err != nil && errors.As(err, &ref) { +// log.Warnf("failed to do X: %v", err) +// } +// +// if err != nil { +// return err +// } +func NewWarningError(err error) *warning { + return &warning{ + error: err, + } +} + +func (w *warning) Error() string { + return w.error.Error() +} + +// IsWarning always returns true. It exists to differentiate regular errors with Warning +// errors. That is, all Warning type objects are error types, but not all error types are Warning +// types. +func (w *warning) IsWarning() bool { + return true +} + +// Unwrap returns the underlying error of the warning. +func (w *warning) Unwrap() error { + return w.error +} + +// As will return true if the target is of type warning. +// +// However, this method is not invoked when `errors.As` is invoked. See note in the docstring of the +// warning struct for more context. +func (w *warning) As(target any) bool { + if _, ok := target.(*warning); ok { + return true + } + + return false +} + +// IsWarning is a helper to check whether the specified err is a Warning +func IsWarning(err error) bool { + var ref Warning + if As(err, &ref) { + return ref.IsWarning() + } + return false +} diff --git a/lib/go.mod b/lib/go.mod new file mode 100644 index 0000000000..f84eb9e62d --- /dev/null +++ b/lib/go.mod @@ -0,0 +1,133 @@ +module github.com/sourcegraph/sourcegraph/lib + +go 1.24.1 + +require ( + github.com/Masterminds/semver v1.5.0 + github.com/charmbracelet/glamour v0.10.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 + github.com/cockroachdb/errors v1.12.0 + github.com/cockroachdb/redact v1.1.6 + github.com/ghodss/yaml v1.0.0 + github.com/gobwas/glob v0.2.3 + github.com/google/go-cmp v0.7.0 + github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 + github.com/jackc/pgconn v1.14.3 + github.com/klauspost/pgzip v1.2.6 + github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-runewidth v0.0.19 + github.com/moby/term v0.5.2 + github.com/muesli/termenv v0.16.0 + github.com/sourcegraph/conc v0.3.0 + github.com/sourcegraph/go-diff v0.7.0 + github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b + github.com/sourcegraph/scip v0.6.1 + github.com/xeipuuv/gojsonschema v1.2.0 + github.com/xlab/treeprint v1.2.0 + go.opentelemetry.io/otel v1.38.0 + golang.org/x/sys v0.38.0 + golang.org/x/term v0.37.0 + google.golang.org/protobuf v1.36.10 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Masterminds/sprig v2.15.0+incompatible // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/aokoli/goutils v1.0.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bufbuild/buf v1.25.0 // indirect + github.com/bufbuild/connect-go v1.9.0 // indirect + github.com/bufbuild/connect-opentelemetry-go v0.4.0 // indirect + github.com/bufbuild/protocompile v0.5.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/docker/cli v24.0.4+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.4+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/envoyproxy/protoc-gen-validate v0.3.0-java // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/felixge/fgprof v0.9.3 // indirect + github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gofrs/uuid/v5 v5.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-containerregistry v0.15.2 // indirect + github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/huandu/xstrings v1.0.0 // indirect + github.com/imdario/mergo v0.3.4 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc4 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/pseudomuto/protoc-gen-doc v1.5.1 // indirect + github.com/pseudomuto/protokit v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/rs/cors v1.9.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tetratelabs/wazero v1.3.0 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/lib/go.sum b/lib/go.sum new file mode 100644 index 0000000000..53f39986cd --- /dev/null +++ b/lib/go.sum @@ -0,0 +1,381 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.15.0+incompatible h1:0gSxPGWS9PAr7U2NsQ2YQg6juRDINkUyuvbb4b2Xm8w= +github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= +github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bufbuild/buf v1.25.0 h1:HFxKrR8wFcZwrBInN50K/oJX/WOtPVq24rHb/ArjfBA= +github.com/bufbuild/buf v1.25.0/go.mod h1:GCKZ5bAP6Ht4MF7KcfaGVgBEXGumwAz2hXjjLVxx8ZU= +github.com/bufbuild/connect-go v1.9.0 h1:JIgAeNuFpo+SUPfU19Yt5TcWlznsN5Bv10/gI/6Pjoc= +github.com/bufbuild/connect-go v1.9.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= +github.com/bufbuild/connect-opentelemetry-go v0.4.0 h1:6JAn10SNqlQ/URhvRNGrIlczKw1wEXknBUUtmWqOiak= +github.com/bufbuild/connect-opentelemetry-go v0.4.0/go.mod h1:nwPXYoDOoc2DGyKE/6pT1Q9MPSi2Et2e6BieMD0l6WU= +github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg= +github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= +github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v24.0.4+incompatible h1:Y3bYF9ekNTm2VFz5U/0BlMdJy73D+Y1iAAZ8l63Ydzw= +github.com/docker/cli v24.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.4+incompatible h1:s/LVDftw9hjblvqIeTiGYXBCD95nOEEl7qRsRrIOuQI= +github.com/docker/docker v24.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= +github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/protoc-gen-validate v0.3.0-java h1:bV5JGEB1ouEzZa0hgVDFFiClrUEuGWRaAc/3mxR2QK0= +github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= +github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.15.2 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE= +github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= +github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo= +github.com/hexops/autogold/v2 v2.2.1 h1:JPUXuZQGkcQMv7eeDXuNMovjfoRYaa0yVcm+F3voaGY= +github.com/hexops/autogold/v2 v2.2.1/go.mod h1:IJwxtUfj1BGLm0YsR/k+dIxYi6xbeLjqGke2bzcOTMI= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hexops/valast v1.4.4 h1:rETyycw+/L2ZVJHHNxEBgh8KUn+87WugH9MxcEv9PGs= +github.com/hexops/valast v1.4.4/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4= +github.com/huandu/xstrings v1.0.0 h1:pO2K/gKgKaat5LdpAhxhluX2GPQMaI3W5FUz/I/UnWk= +github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/imdario/mergo v0.3.4 h1:mKkfHkZWD8dC7WxKx3N9WCF0Y+dLau45704YQmY6H94= +github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co= +github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007 h1:28i1IjGcx8AofiB4N3q5Yls55VEaitzuEPkFJEVgGkA= +github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= +github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= +github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pseudomuto/protoc-gen-doc v1.5.1 h1:Ah259kcrio7Ix1Rhb6u8FCaOkzf9qRBqXnvAufg061w= +github.com/pseudomuto/protoc-gen-doc v1.5.1/go.mod h1:XpMKYg6zkcpgfpCfQ8GcWBDRtRxOmMR5w7pz4Xo+dYM= +github.com/pseudomuto/protokit v0.2.0 h1:hlnBDcy3YEDXH7kc9gV+NLaN0cDzhDvD1s7Y6FZ8RpM= +github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= +github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a h1:j/CQ27s679M9wRGBRJYyXGrfkYuQA6VMnD7R08mHD9c= +github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a/go.mod h1:JG1sdvGTKWwe/oH3/3UKQ26vfcHIN//7fwEJhoqaBcM= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b h1:2FQ72y5zECMu9e5z5jMnllb5n1jVK7qsvgjkVtdFV+g= +github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b/go.mod h1:IDp09QkoqS8Z3CyN2RW6vXjgABkNpDbyjLIHNQwQ8P8= +github.com/sourcegraph/scip v0.6.1 h1:lcBrTwGjsqpMLQN4pVfJYSB+8oVoc8Lnqb4FAfU8EBM= +github.com/sourcegraph/scip v0.6.1/go.mod h1:c6d9mk1cGT6OXdHutxDakKVEPN1D+iFoYWCsrDxEhus= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tetratelabs/wazero v1.3.0 h1:nqw7zCldxE06B8zSZAY0ACrR9OH5QCcPwYmYlwtcwtE= +github.com/tetratelabs/wazero v1.3.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= +github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= +go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= +mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/lib/output/block.go b/lib/output/block.go new file mode 100644 index 0000000000..00d5d7ecbf --- /dev/null +++ b/lib/output/block.go @@ -0,0 +1,58 @@ +package output + +import ( + "bytes" + "sync" +) + +// Block represents a block of output with one status line, and then zero or +// more lines of output nested under the status line. +type Block struct { + *Output + + indent []byte + unwrapped *Output + writer *indentedWriter +} + +func newBlock(indent int, o *Output) *Block { + w := &indentedWriter{} + + // Block uses Output's implementation, but with a wrapped writer that + // indents all output lines. (Note, however, that o's lock mutex is still + // used.) + return &Block{ + Output: &Output{ + w: w, + caps: o.caps, + }, + indent: bytes.Repeat([]byte(" "), indent), + unwrapped: o, + writer: w, + } +} + +func (b *Block) Close() { + b.unwrapped.Lock() + defer b.unwrapped.Unlock() + + // This is a little tricky: output from Writer methods includes a trailing + // newline, so we need to trim that so we don't output extra blank lines. + for line := range bytes.SplitSeq(bytes.TrimRight(b.writer.buffer.Bytes(), "\n"), []byte("\n")) { + _, _ = b.unwrapped.w.Write(b.indent) + _, _ = b.unwrapped.w.Write(line) + _, _ = b.unwrapped.w.Write([]byte("\n")) + } +} + +type indentedWriter struct { + buffer bytes.Buffer + lock sync.Mutex +} + +func (w *indentedWriter) Write(p []byte) (int, error) { + w.lock.Lock() + defer w.lock.Unlock() + + return w.buffer.Write(p) +} diff --git a/lib/output/capabilities.go b/lib/output/capabilities.go new file mode 100644 index 0000000000..f789104ecd --- /dev/null +++ b/lib/output/capabilities.go @@ -0,0 +1,124 @@ +package output + +import ( + "os" + "strconv" + "sync" + + "github.com/mattn/go-isatty" + "github.com/moby/term" + "github.com/muesli/termenv" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// capabilities configures everything that might require detection of the terminal +// environment to change how data is output. +// +// When adding new capabilities, make sure an option to disable running any detection at +// all is provided via OutputOpts, so that issues with detection can be avoided in edge +// cases by configuring an override. +type capabilities struct { + Color bool + Isatty bool + Height int + Width int + + DarkBackground bool +} + +// These all inspect stdout / the terminal so we hide them behind a sync.Once. +// This is because we may call detectCapabilities many times due to doing +// things like capturing output. We have observed garbled output due to +// concurrent calls to detectCapabilities. +var ( + stdoutIsAtty = sync.OnceValue(func() bool { + return isatty.IsTerminal(os.Stdout.Fd()) + }) + stdoutWinSize = sync.OnceValues(func() (*term.Winsize, error) { + return term.GetWinsize(os.Stdout.Fd()) + }) + isDarkBackground = sync.OnceValue(func() bool { + return termenv.HasDarkBackground() + }) +) + +// detectCapabilities lazily evaluates capabilities using the given options. This means +// that if an override is indicated in opts, no inference of the relevant capabilities +// is done at all. +func detectCapabilities(opts OutputOpts) (caps capabilities, err error) { + // Set atty + if opts.ForceTTY != nil { + caps.Isatty = *opts.ForceTTY + } else { + caps.Isatty = stdoutIsAtty() + } + + // Default width and height + caps.Width, caps.Height = 80, 25 + // If all dimensions are forced, detection is not needed + forceAllDimensions := opts.ForceHeight != 0 && opts.ForceWidth != 0 + if caps.Isatty && !forceAllDimensions { + var size *term.Winsize + size, err = stdoutWinSize() + if err == nil { + if size != nil { + caps.Width, caps.Height = int(size.Width), int(size.Height) + } else { + err = errors.New("unexpected nil size from GetWinsize") + } + } else { + err = errors.Wrap(err, "GetWinsize") + } + } + // Set overrides + if opts.ForceWidth != 0 { + caps.Width = opts.ForceWidth + } + if opts.ForceHeight != 0 { + caps.Height = opts.ForceHeight + } + + // detect color mode + caps.Color = opts.ForceColor + if !opts.ForceColor { + caps.Color = detectColor(caps.Isatty) + } + + // set detected background color + caps.DarkBackground = opts.ForceDarkBackground + if !opts.ForceDarkBackground { + caps.DarkBackground = isDarkBackground() + } + + return +} + +func detectColor(atty bool) bool { + if os.Getenv("NO_COLOR") != "" { + return false + } + + if color := os.Getenv("COLOR"); color != "" { + enabled, _ := strconv.ParseBool(color) + return enabled + } + + if !atty { + return false + } + + return true +} + +func (c *capabilities) formatArgs(args []any) []any { + out := make([]any, len(args)) + for i, arg := range args { + if _, ok := arg.(Style); ok && !c.Color { + out[i] = "" + } else { + out[i] = arg + } + } + return out +} diff --git a/lib/output/emoji.go b/lib/output/emoji.go new file mode 100644 index 0000000000..469e9eccb8 --- /dev/null +++ b/lib/output/emoji.go @@ -0,0 +1,32 @@ +package output + +var allEmojis = [...]string{ + EmojiFailure, + EmojiWarning, + EmojiSuccess, + EmojiInfo, + EmojiLightbulb, + EmojiAsterisk, + EmojiWarningSign, + EmojiFingerPointRight, + EmojiHourglass, + EmojiShrug, + EmojiOk, + EmojiQuestionMark, +} + +// Standard emoji for use in output. +const ( + EmojiFailure = "❌" + EmojiWarning = "❗️" + EmojiSuccess = "✅" + EmojiInfo = "ℹ️" + EmojiLightbulb = "💡" + EmojiAsterisk = "✱" + EmojiWarningSign = "⚠️" + EmojiFingerPointRight = "👉" + EmojiHourglass = "⌛" + EmojiShrug = "🤷" + EmojiOk = "👌" + EmojiQuestionMark = "❔" +) diff --git a/lib/output/line.go b/lib/output/line.go new file mode 100644 index 0000000000..ed566d0d8b --- /dev/null +++ b/lib/output/line.go @@ -0,0 +1,78 @@ +package output + +import ( + "fmt" + "io" +) + +// FancyLine is a formatted output line with an optional emoji and style. +type FancyLine struct { + emoji string + style Style + format string + args []any + + // Prefix can be set to prepend some content to this fancy line. + Prefix string + // Prompt can be set to indicate this line is a prompt (should not be followed by a + // new line). + Prompt bool +} + +// Line creates a new FancyLine without a format string. +func Line(emoji string, style Style, s string) FancyLine { + return FancyLine{ + emoji: emoji, + style: style, + format: "%s", + args: []any{s}, + } +} + +// Line creates a new FancyLine with a format string. As with Writer, the +// arguments may include Style instances with the %s specifier. +func Linef(emoji string, style Style, format string, a ...any) FancyLine { + return FancyLine{ + emoji: emoji, + style: style, + format: format, + args: a, + } +} + +// Emoji creates a new FancyLine with an emoji prefix. +func Emoji(emoji string, s string) FancyLine { + return Line(emoji, StyleReset, s) +} + +// Emoji creates a new FancyLine with an emoji prefix and format string. +func Emojif(emoji string, s string, a ...any) FancyLine { + return Linef(emoji, StyleReset, s, a...) +} + +// Styled creates a new FancyLine with style. +func Styled(style Style, s string) FancyLine { + return Line("", style, s) +} + +// Styledf creates a new FancyLine with style and format string. +func Styledf(style Style, s string, a ...any) FancyLine { + return Linef("", style, s, a...) +} + +func (fl FancyLine) write(w io.Writer, caps capabilities) { + if fl.Prefix != "" { + fmt.Fprint(w, fl.Prefix+" ") + } + if fl.emoji != "" { + fmt.Fprint(w, fl.emoji+" ") + } + + fmt.Fprintf(w, "%s"+fl.format+"%s", caps.formatArgs(append(append([]any{fl.style}, fl.args...), StyleReset))...) + if fl.Prompt { + // Add whitespace for user input + _, _ = w.Write([]byte(" ")) + } else { + _, _ = w.Write([]byte("\n")) + } +} diff --git a/lib/output/logger.go b/lib/output/logger.go new file mode 100644 index 0000000000..76f4cf1111 --- /dev/null +++ b/lib/output/logger.go @@ -0,0 +1,31 @@ +package output + +import ( + "bytes" + + "github.com/sourcegraph/log" +) + +type logFacade struct { + logger log.Logger +} + +func OutputFromLogger(logger log.Logger) *Output { + return NewOutput(&logFacade{logger}, OutputOpts{}) +} + +func (l *logFacade) Write(p []byte) (n int, err error) { + for _, emoji := range allEmojis { + if bytes.HasPrefix(p, []byte(emoji)) { + switch emoji { + case EmojiWarningSign: + l.logger.Warn(string(p[len(emoji):])) + case EmojiFailure, EmojiWarning: + l.logger.Error(string(p[len(emoji):])) + default: + l.logger.Info(string(p[len(emoji):])) + } + } + } + return len(p), nil +} diff --git a/lib/output/noop_writer.go b/lib/output/noop_writer.go new file mode 100644 index 0000000000..784bffb67d --- /dev/null +++ b/lib/output/noop_writer.go @@ -0,0 +1,10 @@ +package output + +type NoopWriter struct{} + +func (NoopWriter) Write(s string) {} +func (NoopWriter) Writef(format string, args ...any) {} +func (NoopWriter) WriteLine(line FancyLine) {} +func (NoopWriter) Verbose(s string) {} +func (NoopWriter) Verbosef(format string, args ...any) {} +func (NoopWriter) VerboseLine(line FancyLine) {} diff --git a/lib/output/output.go b/lib/output/output.go new file mode 100644 index 0000000000..f1d1305277 --- /dev/null +++ b/lib/output/output.go @@ -0,0 +1,354 @@ +// Package output provides types related to formatted terminal output. +package output + +import ( + "bytes" + "fmt" + "io" + "os" + "sync" + + "github.com/charmbracelet/glamour" + glamouransi "github.com/charmbracelet/glamour/ansi" + "github.com/charmbracelet/glamour/styles" + "github.com/mattn/go-runewidth" + "golang.org/x/term" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// Writer defines a common set of methods that can be used to output status +// information. +// +// Note that the *f methods can accept Style instances in their arguments with +// the %s format specifier: if given, the detected colour support will be +// respected when outputting. +type Writer interface { + // These methods only write the given message if verbose mode is enabled. + Verbose(s string) + Verbosef(format string, args ...any) + VerboseLine(line FancyLine) + + // These methods write their messages unconditionally. + Write(s string) + Writef(format string, args ...any) + WriteLine(line FancyLine) +} + +type Context interface { + Writer + + Close() +} + +// Output encapsulates a standard set of functionality for commands that need +// to output human-readable data. +// +// Output is not appropriate for machine-readable data, such as JSON. +type Output struct { + w io.Writer + caps capabilities + verbose bool + + // Unsurprisingly, it would be bad if multiple goroutines wrote at the same + // time, so we have a basic mutex to guard against that. + lock sync.Mutex +} + +var _ sync.Locker = &Output{} + +type OutputOpts struct { + // ForceColor ignores all terminal detection and enabled coloured output. + ForceColor bool + // ForceTTY ignores all terminal detection if non-nil and sets TTY output. + ForceTTY *bool + + // ForceHeight ignores all terminal detection and sets the height to this value. + ForceHeight int + // ForceWidth ignores all terminal detection and sets the width to this value. + ForceWidth int + + // ForceDarkBackground ignores all terminal detection and sets whether the terminal + // background is dark to this value. + ForceDarkBackground bool + + Verbose bool +} + +type MarkdownStyleOpts func(style *glamouransi.StyleConfig) + +var MarkdownNoMargin MarkdownStyleOpts = func(style *glamouransi.StyleConfig) { + z := uint(0) + style.CodeBlock.Margin = &z + style.Document.Margin = &z + style.Document.BlockPrefix = "" + style.Document.BlockSuffix = "" +} + +// newOutputPlatformQuirks provides a way for conditionally compiled code to +// hook into NewOutput to perform any required setup. +var newOutputPlatformQuirks func(o *Output) error + +// newCapabilityWatcher returns a channel that receives a message when +// capabilities are updated. By default, no watching functionality is +// available. +var newCapabilityWatcher = func(opts OutputOpts) chan capabilities { return nil } + +func NewOutput(w io.Writer, opts OutputOpts) *Output { + // Not being able to detect capabilities is alright. It might mean output will look + // weird but that should not prevent us from running. + // Before, we logged an error + // "An error was returned when detecting the terminal size and capabilities" + // but it was super noisy and confused people into thinking something would be broken. + caps, _ := detectCapabilities(opts) + + o := &Output{caps: caps, verbose: opts.Verbose, w: w} + if newOutputPlatformQuirks != nil { + if err := newOutputPlatformQuirks(o); err != nil { + o.Verbosef("Error handling platform quirks: %v", err) + } + } + + // Set up a watcher so we can adjust the size of the output if the terminal + // is resized. + if c := newCapabilityWatcher(opts); c != nil { + go func() { + for caps := range c { + o.caps = caps + } + }() + } + + return o +} + +func (o *Output) Lock() { + o.lock.Lock() + + if o.caps.Isatty { + // Hide the cursor while we update: this reduces the jitteriness of the + // whole thing, and some terminals are smart enough to make the update we're + // about to render atomic if the cursor is hidden for a short length of + // time. + o.w.Write([]byte("\033[?25l")) + } +} + +func (o *Output) SetVerbose() { + o.lock.Lock() + defer o.lock.Unlock() + o.verbose = true +} + +func (o *Output) UnsetVerbose() { + o.lock.Lock() + defer o.lock.Unlock() + o.verbose = false +} + +func (o *Output) Unlock() { + if o.caps.Isatty { + // Show the cursor once more. + o.w.Write([]byte("\033[?25h")) + } + + o.lock.Unlock() +} + +func (o *Output) Verbose(s string) { + if o.verbose { + o.Write(s) + } +} + +func (o *Output) Verbosef(format string, args ...any) { + if o.verbose { + o.Writef(format, args...) + } +} + +func (o *Output) VerboseLine(line FancyLine) { + if o.verbose { + o.WriteLine(line) + } +} + +func (o *Output) Write(s string) { + o.Lock() + defer o.Unlock() + fmt.Fprintln(o.w, s) +} + +func (o *Output) Writef(format string, args ...any) { + o.Lock() + defer o.Unlock() + fmt.Fprintf(o.w, format, o.caps.formatArgs(args)...) + fmt.Fprint(o.w, "\n") +} + +func (o *Output) WriteLine(line FancyLine) { + o.Lock() + defer o.Unlock() + line.write(o.w, o.caps) +} + +// Block starts a new block context. This should not be invoked if there is an +// active Pending or Progress context. +func (o *Output) Block(summary FancyLine) *Block { + o.WriteLine(summary) + return newBlock(runewidth.StringWidth(summary.emoji)+1, o) +} + +// Pending sets up a new pending context. This should not be invoked if there +// is an active Block or Progress context. The emoji in the message will be +// ignored, as Pending will render its own spinner. +// +// A Pending instance must be disposed of via the Complete or Destroy methods. +func (o *Output) Pending(message FancyLine) Pending { + return newPending(message, o) +} + +// Progress sets up a new progress bar context. This should not be invoked if +// there is an active Block or Pending context. +// +// A Progress instance must be disposed of via the Complete or Destroy methods. +func (o *Output) Progress(bars []ProgressBar, opts *ProgressOpts) Progress { + return newProgress(bars, o, opts) +} + +// ProgressWithStatusBars sets up a new progress bar context with StatusBar +// contexts. This should not be invoked if there is an active Block or Pending +// context. +// +// A Progress instance must be disposed of via the Complete or Destroy methods. +func (o *Output) ProgressWithStatusBars(bars []ProgressBar, statusBars []*StatusBar, opts *ProgressOpts) ProgressWithStatusBars { + return newProgressWithStatusBars(bars, statusBars, o, opts) +} + +type readWriter struct { + io.Reader + io.Writer +} + +// PromptPassword tries to securely prompt a user for sensitive input. +func (o *Output) PromptPassword(input io.Reader, prompt FancyLine) (string, error) { + o.lock.Lock() + defer o.lock.Unlock() + + // Render the prompt + prompt.Prompt = true + var promptText bytes.Buffer + prompt.write(&promptText, o.caps) + + // If input is a file and terminal, read from it directly + if f, ok := input.(*os.File); ok { + fd := int(f.Fd()) + if term.IsTerminal(fd) { + _, _ = o.w.Write(promptText.Bytes()) + val, err := term.ReadPassword(fd) + _, _ = o.w.Write([]byte("\n")) // once we've read an input + return string(val), err + } + } + + // Otherwise, create a terminal + t := term.NewTerminal(&readWriter{Reader: input, Writer: o.w}, "") + _ = t.SetSize(o.caps.Width, o.caps.Height) + return t.ReadPassword(promptText.String()) +} + +func MarkdownIndent(n uint) MarkdownStyleOpts { + return func(style *glamouransi.StyleConfig) { + style.Document.Indent = &n + } +} + +// WriteCode renders the given code snippet as Markdown, unless color is disabled. +func (o *Output) WriteCode(languageName, str string) error { + return o.WriteMarkdown(fmt.Sprintf("```%s\n%s\n```", languageName, str), MarkdownNoMargin) +} + +func (o *Output) WriteMarkdown(str string, opts ...MarkdownStyleOpts) error { + o.Lock() + defer o.Unlock() + return o.RenderMarkdown(o.w, str) +} + +func (o *Output) RenderMarkdown(w io.Writer, str string, opts ...MarkdownStyleOpts) error { + if !o.caps.Color { + _, err := fmt.Fprintln(w, str) + return err + } + + style := glamourGetDefaultStyle(o.caps) + for _, opt := range opts { + opt(&style) + } + + r, err := glamour.NewTermRenderer( + // detect background color and pick either the default dark or light theme + glamour.WithStyles(style), + // wrap output at slightly less than terminal width + glamour.WithWordWrap(o.caps.Width*4/5), + glamour.WithEmoji(), + glamour.WithPreservedNewLines(), + ) + if err != nil { + return errors.Wrap(err, "renderer") + } + + rendered, err := r.Render(str) + if err != nil { + return errors.Wrap(err, "render") + } + + _, err = fmt.Fprintln(w, rendered) + return err +} + +// The utility functions below do not make checks for whether the terminal is a +// TTY, and should only be invoked from behind appropriate guards. + +func (o *Output) clearCurrentLine() { + fmt.Fprint(o.w, "\033[2K") +} + +func (o *Output) moveDown(lines int) { + fmt.Fprintf(o.w, "\033[%dB", lines) + + // Move the cursor to the leftmost column. + fmt.Fprintf(o.w, "\033[%dD", o.caps.Width+1) +} + +func (o *Output) moveUp(lines int) { + fmt.Fprintf(o.w, "\033[%dA", lines) + + // Move the cursor to the leftmost column. + fmt.Fprintf(o.w, "\033[%dD", o.caps.Width+1) +} + +func (o *Output) MoveUpLines(lines int) { + o.moveUp(lines) +} + +// writeStyle is a helper to write a style while respecting the terminal +// capabilities. +func (o *Output) writeStyle(style Style) { + fmt.Fprintf(o.w, "%s", o.caps.formatArgs([]any{style})...) +} + +func (o *Output) ClearScreen() { + fmt.Fprintf(o.w, "\033c") +} + +// from unexported method glamour.getDefaultStyle but specialized to style == +// "auto" and using our detection +func glamourGetDefaultStyle(caps capabilities) glamouransi.StyleConfig { + if !caps.Isatty { + return styles.NoTTYStyleConfig + } + if caps.DarkBackground { + return styles.DarkStyleConfig + } + return styles.LightStyleConfig +} diff --git a/lib/output/output_unix.go b/lib/output/output_unix.go new file mode 100644 index 0000000000..f55b2003b0 --- /dev/null +++ b/lib/output/output_unix.go @@ -0,0 +1,79 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package output + +import ( + "os" + "os/signal" + "sync" + "syscall" +) + +func init() { + // The platforms this file builds on support the SIGWINCH signal, which + // indicates that the terminal has been resized. When we receive that + // signal, we can use this to re-detect the terminal capabilities. + // + // We won't do any setup until the first time newCapabilityWatcher is + // invoked, but we do need some shared state to be ready. + var ( + // chans contains the listening channels that should be notified when + // capabilities are updated. + chans []chan capabilities + + // mu guards the chans variable. + mu sync.RWMutex + + // once guards the lazy initialisation, including installing the signal + // handler. + once sync.Once + ) + + newCapabilityWatcher = func(opts OutputOpts) chan capabilities { + // Lazily initialise the required global state if we haven't already. + once.Do(func() { + mu.Lock() + chans = make([]chan capabilities, 0, 1) + mu.Unlock() + + // Install the signal handler. To avoid race conditions, we should + // do this synchronously before spawning the goroutine that will + // actually listen to the channel. + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGWINCH) + + go func() { + for { + <-c + caps, err := detectCapabilities(opts) + // We won't bother reporting an error here; there's no harm + // in the previous capabilities being used besides possibly + // being ugly. + if err == nil { + mu.RLock() + for _, out := range chans { + go func() { + select { + case out <- caps: + // success + default: + // welp + } + }() + } + mu.RUnlock() + } + } + }() + }) + + // Now we can create and return the actual output channel. + out := make(chan capabilities) + mu.Lock() + defer mu.Unlock() + chans = append(chans, out) + + return out + } +} diff --git a/lib/output/output_windows.go b/lib/output/output_windows.go new file mode 100644 index 0000000000..794c4444d8 --- /dev/null +++ b/lib/output/output_windows.go @@ -0,0 +1,61 @@ +package output + +import ( + "time" + + "golang.org/x/sys/windows" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +func init() { + newOutputPlatformQuirks = func(o *Output) error { + var errs error + + if err := setConsoleMode(windows.Stdout, windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { + errs = errors.Append(errs, err) + } + if err := setConsoleMode(windows.Stderr, windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { + errs = errors.Append(errs, err) + } + + return errs + } + + // Windows doesn't have a particularly good way of notifying console + // applications that a resize has occurred. (Historically, you could hook + // the console window, but it turns out that's a security nightmare.) So + // we'll just poll every five seconds and update the capabilities from + // there. + newCapabilityWatcher = func(opts OutputOpts) chan capabilities { + c := make(chan capabilities) + + go func() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + for { + <-ticker.C + if caps, err := detectCapabilities(opts); err == nil { + c <- caps + } + } + }() + + return c + } +} + +func setConsoleMode(handle windows.Handle, flags uint32) error { + // This is shamelessly lifted from gitlab-runner, specifically + // https://gitlab.com/gitlab-org/gitlab-runner/blob/f8d87f1e3e3af1cc8aadcea3e40bbb069eee72ef/helpers/cli/init_cli_windows.go + + // First we have to get the current console mode so we can add the desired + // flags. + var mode uint32 + if err := windows.GetConsoleMode(handle, &mode); err != nil { + return err + } + + // Now we can set the console mode. + return windows.SetConsoleMode(handle, mode|flags) +} diff --git a/lib/output/pending.go b/lib/output/pending.go new file mode 100644 index 0000000000..7e33ed9c39 --- /dev/null +++ b/lib/output/pending.go @@ -0,0 +1,26 @@ +package output + +type Pending interface { + // Anything sent to the Writer methods will be displayed as a log message + // above the pending line. + Context + + // Update and Updatef change the message shown after the spinner. + Update(s string) + Updatef(format string, args ...any) + + // Complete stops the spinner and replaces the pending line with the given + // message. + Complete(message FancyLine) + + // Destroy stops the spinner and removes the pending line. + Destroy() +} + +func newPending(message FancyLine, o *Output) Pending { + if !o.caps.Isatty { + return newPendingSimple(message, o) + } + + return newPendingTTY(message, o) +} diff --git a/lib/output/pending_simple.go b/lib/output/pending_simple.go new file mode 100644 index 0000000000..7a25451095 --- /dev/null +++ b/lib/output/pending_simple.go @@ -0,0 +1,26 @@ +package output + +type pendingSimple struct { + *Output +} + +func (p *pendingSimple) Update(s string) { + p.Write(s + "...") +} + +func (p *pendingSimple) Updatef(format string, args ...any) { + p.Writef(format+"...", args...) +} + +func (p *pendingSimple) Complete(message FancyLine) { + p.WriteLine(message) +} + +func (p *pendingSimple) Close() {} +func (p *pendingSimple) Destroy() {} + +func newPendingSimple(message FancyLine, o *Output) *pendingSimple { + message.format += "..." + o.WriteLine(message) + return &pendingSimple{o} +} diff --git a/lib/output/pending_tty.go b/lib/output/pending_tty.go new file mode 100644 index 0000000000..86853edc4a --- /dev/null +++ b/lib/output/pending_tty.go @@ -0,0 +1,160 @@ +package output + +import ( + "bytes" + "fmt" + "time" + + "github.com/mattn/go-runewidth" +) + +type pendingTTY struct { + o *Output + line FancyLine + spinner *spinner +} + +func (p *pendingTTY) Verbose(s string) { + if p.o.verbose { + p.Write(s) + } +} + +func (p *pendingTTY) Verbosef(format string, args ...any) { + if p.o.verbose { + p.Writef(format, args...) + } +} + +func (p *pendingTTY) VerboseLine(line FancyLine) { + if p.o.verbose { + p.WriteLine(line) + } +} + +func (p *pendingTTY) Write(s string) { + p.o.Lock() + defer p.o.Unlock() + + p.o.moveUp(1) + p.o.clearCurrentLine() + fmt.Fprintln(p.o.w, s) + p.write(p.line) +} + +func (p *pendingTTY) Writef(format string, args ...any) { + p.o.Lock() + defer p.o.Unlock() + + p.o.moveUp(1) + p.o.clearCurrentLine() + fmt.Fprintf(p.o.w, format, p.o.caps.formatArgs(args)...) + fmt.Fprint(p.o.w, "\n") + p.write(p.line) +} + +func (p *pendingTTY) WriteLine(line FancyLine) { + p.o.Lock() + defer p.o.Unlock() + + p.o.moveUp(1) + p.o.clearCurrentLine() + line.write(p.o.w, p.o.caps) + p.write(p.line) +} + +func (p *pendingTTY) Update(s string) { + p.o.Lock() + defer p.o.Unlock() + + p.line.format = "%s" + p.line.args = []any{s} + + p.o.moveUp(1) + p.o.clearCurrentLine() + p.write(p.line) +} + +func (p *pendingTTY) Updatef(format string, args ...any) { + p.o.Lock() + defer p.o.Unlock() + + p.line.format = format + p.line.args = args + + p.o.moveUp(1) + p.o.clearCurrentLine() + p.write(p.line) +} + +func (p *pendingTTY) Complete(message FancyLine) { + p.spinner.stop() + + p.o.Lock() + defer p.o.Unlock() + + p.o.moveUp(1) + p.o.clearCurrentLine() + p.write(message) +} + +func (p *pendingTTY) Close() { p.Destroy() } + +func (p *pendingTTY) Destroy() { + p.spinner.stop() + + p.o.Lock() + defer p.o.Unlock() + + p.o.moveUp(1) + p.o.clearCurrentLine() +} + +func newPendingTTY(message FancyLine, o *Output) *pendingTTY { + p := &pendingTTY{ + o: o, + line: message, + spinner: newSpinner(100 * time.Millisecond), + } + p.updateEmoji(spinnerStrings[0]) + fmt.Fprintln(p.o.w, "") + + go func() { + for s := range p.spinner.C { + func() { + p.o.Lock() + defer p.o.Unlock() + + p.updateEmoji(s) + + p.o.moveUp(1) + p.o.clearCurrentLine() + p.write(p.line) + }() + } + }() + + return p +} + +func (p *pendingTTY) updateEmoji(emoji string) { + // We add an extra space because the Braille characters are single width, + // but emoji are generally double width and that's what will most likely be + // used in the completion message, if any. + p.line.emoji = fmt.Sprintf("%s%s ", p.o.caps.formatArgs([]any{ + p.line.style, + emoji, + })...) +} + +func (p *pendingTTY) write(message FancyLine) { + var buf bytes.Buffer + + // This appends a newline to buf, so we have to be careful to ensure that + // we also add a newline if the line is truncated. + message.write(&buf, p.o.caps) + + // FIXME: This doesn't account for escape codes right now, so we may + // truncate shorter than we mean to. + fmt.Fprint(p.o.w, runewidth.Truncate(buf.String(), p.o.caps.Width, "...\n")) +} diff --git a/lib/output/progress.go b/lib/output/progress.go new file mode 100644 index 0000000000..2b7d9688a4 --- /dev/null +++ b/lib/output/progress.go @@ -0,0 +1,60 @@ +package output + +type Progress interface { + Context + + // Complete stops the set of progress bars and marks them all as completed. + Complete() + + // Destroy stops the set of progress bars and clears them from the + // terminal. + Destroy() + + // SetLabel updates the label for the given bar. + SetLabel(i int, label string) + + // SetLabelAndRecalc updates the label for the given bar and recalculates + // the maximum width of the labels. + SetLabelAndRecalc(i int, label string) + + // SetValue updates the value for the given bar. + SetValue(i int, v float64) +} + +type ProgressBar struct { + Label string + Max float64 + Value float64 + + labelWidth int +} + +type ProgressOpts struct { + PendingStyle Style + SuccessEmoji string + SuccessStyle Style + + // NoSpinner turns of the automatic updating of the progress bar and + // spinner in a background goroutine. + // Used for testing only! + NoSpinner bool +} + +func (opt *ProgressOpts) WithNoSpinner(noSpinner bool) *ProgressOpts { + c := *opt + c.NoSpinner = noSpinner + return &c +} + +func newProgress(bars []ProgressBar, o *Output, opts *ProgressOpts) Progress { + barPtrs := make([]*ProgressBar, len(bars)) + for i := range bars { + barPtrs[i] = &bars[i] + } + + if !o.caps.Isatty { + return newProgressSimple(barPtrs, o, opts) + } + + return newProgressTTY(barPtrs, o, opts) +} diff --git a/lib/output/progress_simple.go b/lib/output/progress_simple.go new file mode 100644 index 0000000000..dadfc3086c --- /dev/null +++ b/lib/output/progress_simple.go @@ -0,0 +1,91 @@ +package output + +import ( + "math" + "time" +) + +type progressSimple struct { + *Output + + bars []*ProgressBar + done chan chan struct{} +} + +func (p *progressSimple) Complete() { + p.stop() + writeBars(p.Output, p.bars) +} + +func (p *progressSimple) Close() { p.Destroy() } +func (p *progressSimple) Destroy() { p.stop() } + +func (p *progressSimple) SetLabel(i int, label string) { + p.bars[i].Label = label +} + +func (p *progressSimple) SetLabelAndRecalc(i int, label string) { + p.bars[i].Label = label +} + +func (p *progressSimple) SetValue(i int, v float64) { + p.bars[i].Value = v +} + +func (p *progressSimple) stop() { + c := make(chan struct{}) + p.done <- c + <-c +} + +func newProgressSimple(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progressSimple { + p := &progressSimple{ + Output: o, + bars: bars, + done: make(chan chan struct{}), + } + + if opts != nil && opts.NoSpinner { + if p.Output.verbose { + writeBars(p.Output, p.bars) + } + return p + } + + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if p.Output.verbose { + writeBars(p.Output, p.bars) + } + + case c := <-p.done: + c <- struct{}{} + return + } + } + }() + + return p +} + +func writeBar(w Writer, bar *ProgressBar) { + w.Writef("%s: %d%%", bar.Label, int64(math.Round((100.0*bar.Value)/bar.Max))) +} + +func writeBars(o *Output, bars []*ProgressBar) { + if len(bars) > 1 { + block := o.Block(Line("", StyleReset, "Progress:")) + defer block.Close() + + for _, bar := range bars { + writeBar(block, bar) + } + } else if len(bars) == 1 { + writeBar(o, bars[0]) + } +} diff --git a/lib/output/progress_tty.go b/lib/output/progress_tty.go new file mode 100644 index 0000000000..f10dc18921 --- /dev/null +++ b/lib/output/progress_tty.go @@ -0,0 +1,295 @@ +package output + +import ( + "fmt" + "math" + "strings" + "time" + + "github.com/mattn/go-runewidth" +) + +var DefaultProgressTTYOpts = &ProgressOpts{ + SuccessEmoji: "\u2705", + SuccessStyle: StyleSuccess, + PendingStyle: StylePending, +} + +type progressTTY struct { + bars []*ProgressBar + + o *Output + opts ProgressOpts + + emojiWidth int + labelWidth int + pendingEmoji string + spinner *spinner +} + +func (p *progressTTY) Complete() { + p.spinner.stop() + + p.o.Lock() + defer p.o.Unlock() + + for _, bar := range p.bars { + bar.Value = bar.Max + } + p.drawInSitu() +} + +func (p *progressTTY) Close() { p.Destroy() } + +func (p *progressTTY) Destroy() { + p.spinner.stop() + + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + for range len(p.bars) { + p.o.clearCurrentLine() + p.o.moveDown(1) + } + + p.moveToOrigin() +} + +func (p *progressTTY) SetLabel(i int, label string) { + p.o.Lock() + defer p.o.Unlock() + + p.bars[i].Label = label + p.bars[i].labelWidth = runewidth.StringWidth(label) + p.drawInSitu() +} + +func (p *progressTTY) SetLabelAndRecalc(i int, label string) { + p.o.Lock() + defer p.o.Unlock() + + p.bars[i].Label = label + p.bars[i].labelWidth = runewidth.StringWidth(label) + + p.labelWidth = calcLabelWidth(p.bars, p.emojiWidth, p.o.caps.Width) + p.drawInSitu() +} + +func (p *progressTTY) SetValue(i int, v float64) { + p.o.Lock() + defer p.o.Unlock() + + p.bars[i].Value = v + p.drawInSitu() +} + +func (p *progressTTY) Verbose(s string) { + if p.o.verbose { + p.Write(s) + } +} + +func (p *progressTTY) Verbosef(format string, args ...any) { + if p.o.verbose { + p.Writef(format, args...) + } +} + +func (p *progressTTY) VerboseLine(line FancyLine) { + if p.o.verbose { + p.WriteLine(line) + } +} + +func (p *progressTTY) Write(s string) { + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + p.o.clearCurrentLine() + fmt.Fprintln(p.o.w, s) + p.draw() +} + +func (p *progressTTY) Writef(format string, args ...any) { + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + p.o.clearCurrentLine() + fmt.Fprintf(p.o.w, format, p.o.caps.formatArgs(args)...) + fmt.Fprint(p.o.w, "\n") + p.draw() +} + +func (p *progressTTY) WriteLine(line FancyLine) { + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + p.o.clearCurrentLine() + line.write(p.o.w, p.o.caps) + p.draw() +} + +func newProgressTTY(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progressTTY { + p := &progressTTY{ + bars: bars, + o: o, + emojiWidth: 3, + pendingEmoji: spinnerStrings[0], + spinner: newSpinner(100 * time.Millisecond), + } + + if opts != nil { + p.opts = *opts + } else { + p.opts = *DefaultProgressTTYOpts + } + + if w := runewidth.StringWidth(p.opts.SuccessEmoji); w > p.emojiWidth { + p.emojiWidth = w + 1 + } + + p.labelWidth = calcLabelWidth(p.bars, p.emojiWidth, p.o.caps.Width) + + p.o.Lock() + defer p.o.Unlock() + + p.draw() + + if opts != nil && opts.NoSpinner { + return p + } + + go func() { + for s := range p.spinner.C { + func() { + p.pendingEmoji = s + + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + p.draw() + }() + } + }() + + return p +} + +func (p *progressTTY) draw() { + for _, bar := range p.bars { + p.writeBar(bar) + } +} + +// We think this means "draw in position"? +func (p *progressTTY) drawInSitu() { + p.moveToOrigin() + p.draw() +} + +func (p *progressTTY) moveToOrigin() { + p.o.moveUp(len(p.bars)) +} + +// This is the core render function +func (p *progressTTY) writeBar(bar *ProgressBar) { + p.o.clearCurrentLine() + + value := bar.Value + if bar.Value >= bar.Max { + p.o.writeStyle(p.opts.SuccessStyle) + fmt.Fprint(p.o.w, runewidth.FillRight(p.opts.SuccessEmoji, p.emojiWidth)) + value = bar.Max + } else { + p.o.writeStyle(p.opts.PendingStyle) + fmt.Fprint(p.o.w, runewidth.FillRight(p.pendingEmoji, p.emojiWidth)) + } + + fmt.Fprint(p.o.w, runewidth.FillRight(runewidth.Truncate(bar.Label, p.labelWidth, "..."), p.labelWidth)) + + // Create a status label that represents percentage completion + statusLabel := fmt.Sprintf("%d", int(math.Floor(bar.Value/bar.Max*100))) + "%" + statusLabelWidth := len(statusLabel) + + // The bar width is the space remaining after we write the label and some emoji space... + remainingSpaceAfterLabel := floorZero(p.o.caps.Width - p.labelWidth - p.emojiWidth) + barWidth := floorZero(remainingSpaceAfterLabel - + // minus a overall status indicator... + statusLabelWidth - + // minus two spaces after the label, 2 spaces before the status label + 2 - 2) + + // Unicode box drawing gives us eight possible bar widths, so we need to + // calculate both the bar width and then the final character, if any. + var segments int + if bar.Max > 0 { + segments = int(math.Round((float64(8*barWidth) * value) / bar.Max)) + } + + fillWidth := segments / 8 + remainder := segments % 8 + if remainder == 0 { + if fillWidth > barWidth { + fillWidth = barWidth + } + } else { + if fillWidth+1 > barWidth { + fillWidth = floorZero(barWidth - 1) + } + } + + fmt.Fprintf(p.o.w, " ") + fmt.Fprint(p.o.w, strings.Repeat("█", fillWidth)) + + // The final bar character - if the remainder of the segment division is 0, we write + // no space. Otherwise we write a *single* character that represents that remainder. + fmt.Fprint(p.o.w, []string{ + "", // no remainder case + "▏", + "▎", + "▍", + "▌", + "▋", + "▊", + "▉", + }[remainder]) + + p.o.writeStyle(StyleReset) + + barSize := fillWidth + if remainder > 0 { + barSize += 1 // only a single character gets written if there is a remainder + } + consumedSpace := remainingSpaceAfterLabel - barSize - 2 // leave space for the label + fmt.Fprint(p.o.w, StyleBold, runewidth.FillLeft(statusLabel, consumedSpace), StyleReset) + + fmt.Fprintln(p.o.w) // end the line +} + +func floorZero(v int) int { + if v < 0 { + return 0 + } + return v +} + +func calcLabelWidth(bars []*ProgressBar, emojiWidth, termWidth int) int { + labelWidth := 0 + for _, bar := range bars { + bar.labelWidth = runewidth.StringWidth(bar.Label) + if bar.labelWidth > labelWidth { + labelWidth = bar.labelWidth + } + } + + if maxWidth := termWidth/2 - emojiWidth; (labelWidth + 2) > maxWidth { + labelWidth = maxWidth - 2 + } + + return labelWidth +} diff --git a/lib/output/progress_with_status_bars.go b/lib/output/progress_with_status_bars.go new file mode 100644 index 0000000000..aa8c6a5248 --- /dev/null +++ b/lib/output/progress_with_status_bars.go @@ -0,0 +1,23 @@ +package output + +type ProgressWithStatusBars interface { + Progress + + StatusBarUpdatef(i int, format string, args ...any) + StatusBarCompletef(i int, format string, args ...any) + StatusBarFailf(i int, format string, args ...any) + StatusBarResetf(i int, label, format string, args ...any) +} + +func newProgressWithStatusBars(bars []ProgressBar, statusBars []*StatusBar, o *Output, opts *ProgressOpts) ProgressWithStatusBars { + barPtrs := make([]*ProgressBar, len(bars)) + for i := range bars { + barPtrs[i] = &bars[i] + } + + if !o.caps.Isatty { + return newProgressWithStatusBarsSimple(barPtrs, statusBars, o, opts) + } + + return newProgressWithStatusBarsTTY(barPtrs, statusBars, o, opts) +} diff --git a/lib/output/progress_with_status_bars_simple.go b/lib/output/progress_with_status_bars_simple.go new file mode 100644 index 0000000000..e985c2e952 --- /dev/null +++ b/lib/output/progress_with_status_bars_simple.go @@ -0,0 +1,106 @@ +package output + +import ( + "time" +) + +type progressWithStatusBarsSimple struct { + *progressSimple + + statusBars []*StatusBar +} + +func (p *progressWithStatusBarsSimple) Complete() { + p.stop() + writeBars(p.Output, p.bars) + writeStatusBars(p.Output, p.statusBars) +} + +func (p *progressWithStatusBarsSimple) StatusBarUpdatef(i int, format string, args ...any) { + if p.statusBars[i] != nil { + p.statusBars[i].Updatef(format, args...) + } +} + +func (p *progressWithStatusBarsSimple) StatusBarCompletef(i int, format string, args ...any) { + if p.statusBars[i] != nil { + wasComplete := p.statusBars[i].completed + p.statusBars[i].Completef(format, args...) + if !wasComplete { + writeStatusBar(p.Output, p.statusBars[i]) + } + } +} + +func (p *progressWithStatusBarsSimple) StatusBarFailf(i int, format string, args ...any) { + if p.statusBars[i] != nil { + wasCompleted := p.statusBars[i].completed + p.statusBars[i].Failf(format, args...) + if !wasCompleted { + writeStatusBar(p.Output, p.statusBars[i]) + } + } +} + +func (p *progressWithStatusBarsSimple) StatusBarResetf(i int, label, format string, args ...any) { + if p.statusBars[i] != nil { + p.statusBars[i].Resetf(label, format, args...) + } +} + +func newProgressWithStatusBarsSimple(bars []*ProgressBar, statusBars []*StatusBar, o *Output, opts *ProgressOpts) *progressWithStatusBarsSimple { + p := &progressWithStatusBarsSimple{ + progressSimple: &progressSimple{ + Output: o, + bars: bars, + done: make(chan chan struct{}), + }, + statusBars: statusBars, + } + + if opts != nil && opts.NoSpinner { + if p.Output.verbose { + writeBars(p.Output, p.bars) + writeStatusBars(p.Output, p.statusBars) + } + return p + } + + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if p.Output.verbose { + writeBars(p.Output, p.bars) + writeStatusBars(p.Output, p.statusBars) + } + + case c := <-p.done: + c <- struct{}{} + return + } + } + }() + + return p +} + +func writeStatusBar(w Writer, bar *StatusBar) { + w.Writef("%s: "+bar.format, append([]any{bar.label}, bar.args...)...) +} + +func writeStatusBars(o *Output, bars []*StatusBar) { + if len(bars) > 1 { + block := o.Block(Line("", StyleReset, "Status:")) + defer block.Close() + + for _, bar := range bars { + writeStatusBar(block, bar) + } + } else if len(bars) == 1 { + writeStatusBar(o, bars[0]) + } +} diff --git a/lib/output/progress_with_status_bars_tty.go b/lib/output/progress_with_status_bars_tty.go new file mode 100644 index 0000000000..3cfcf5834f --- /dev/null +++ b/lib/output/progress_with_status_bars_tty.go @@ -0,0 +1,309 @@ +package output + +import ( + "fmt" + "time" + + "github.com/mattn/go-runewidth" +) + +func newProgressWithStatusBarsTTY(bars []*ProgressBar, statusBars []*StatusBar, o *Output, opts *ProgressOpts) *progressWithStatusBarsTTY { + p := &progressWithStatusBarsTTY{ + progressTTY: &progressTTY{ + bars: bars, + o: o, + emojiWidth: 3, + pendingEmoji: spinnerStrings[0], + spinner: newSpinner(100 * time.Millisecond), + }, + statusBars: statusBars, + } + + if opts != nil { + p.opts = *opts + } else { + p.opts = *DefaultProgressTTYOpts + } + + if w := runewidth.StringWidth(p.opts.SuccessEmoji); w > p.emojiWidth { + p.emojiWidth = w + 1 + } + + p.labelWidth = calcLabelWidth(p.bars, p.emojiWidth, p.o.caps.Width) + p.statusBarLabelWidth = calcStatusBarLabelWidth(p.statusBars, p.o.caps.Width) + + p.o.Lock() + defer p.o.Unlock() + + p.draw() + + if opts != nil && opts.NoSpinner { + return p + } + + go func() { + for s := range p.spinner.C { + func() { + p.pendingEmoji = s + + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + p.draw() + }() + } + }() + + return p +} + +type progressWithStatusBarsTTY struct { + *progressTTY + + statusBars []*StatusBar + statusBarLabelWidth int + numPrintedStatusBars int +} + +func (p *progressWithStatusBarsTTY) Close() { p.Destroy() } + +func (p *progressWithStatusBarsTTY) Destroy() { + p.spinner.stop() + + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + + for range p.lines() { + p.o.clearCurrentLine() + p.o.moveDown(1) + } + + p.moveToOrigin() +} + +func (p *progressWithStatusBarsTTY) Complete() { + p.spinner.stop() + + p.o.Lock() + defer p.o.Unlock() + + // +1 because of the line between progress and status bars + for range p.numPrintedStatusBars + 1 { + p.o.moveUp(1) + p.o.clearCurrentLine() + } + + for _, bar := range p.bars { + bar.Value = bar.Max + } + + p.o.moveUp(len(p.bars)) + p.draw() +} + +func (p *progressWithStatusBarsTTY) lines() int { + return len(p.bars) + p.numPrintedStatusBars + 1 +} + +func (p *progressWithStatusBarsTTY) SetLabel(i int, label string) { + p.o.Lock() + defer p.o.Unlock() + + p.bars[i].Label = label + p.bars[i].labelWidth = runewidth.StringWidth(label) + p.drawInSitu() +} + +func (p *progressWithStatusBarsTTY) SetValue(i int, v float64) { + p.o.Lock() + defer p.o.Unlock() + + p.bars[i].Value = v + p.drawInSitu() +} + +func (p *progressWithStatusBarsTTY) StatusBarResetf(i int, label, format string, args ...any) { + p.o.Lock() + defer p.o.Unlock() + + if p.statusBars[i] != nil { + p.statusBars[i].Resetf(label, format, args...) + } + + p.statusBarLabelWidth = calcStatusBarLabelWidth(p.statusBars, p.o.caps.Width) + p.drawInSitu() +} + +func (p *progressWithStatusBarsTTY) StatusBarUpdatef(i int, format string, args ...any) { + p.o.Lock() + defer p.o.Unlock() + + if p.statusBars[i] != nil { + p.statusBars[i].Updatef(format, args...) + } + + p.drawInSitu() +} + +func (p *progressWithStatusBarsTTY) StatusBarCompletef(i int, format string, args ...any) { + p.o.Lock() + defer p.o.Unlock() + + if p.statusBars[i] != nil { + p.statusBars[i].Completef(format, args...) + } + + p.drawInSitu() +} + +func (p *progressWithStatusBarsTTY) StatusBarFailf(i int, format string, args ...any) { + p.o.Lock() + defer p.o.Unlock() + + if p.statusBars[i] != nil { + p.statusBars[i].Failf(format, args...) + } + + p.drawInSitu() +} + +func (p *progressWithStatusBarsTTY) draw() { + for _, bar := range p.bars { + p.writeBar(bar) + } + + if len(p.statusBars) > 0 { + p.o.clearCurrentLine() + fmt.Fprint(p.o.w, StylePending, "│", runewidth.FillLeft("\n", p.o.caps.Width-1)) + + } + + p.numPrintedStatusBars = 0 + for i, statusBar := range p.statusBars { + if statusBar == nil { + continue + } + if !statusBar.initialized { + continue + } + + last := i == len(p.statusBars)-1 + p.writeStatusBar(last, statusBar) + p.numPrintedStatusBars += 1 + } +} + +func (p *progressWithStatusBarsTTY) moveToOrigin() { + p.o.moveUp(p.lines()) +} + +func (p *progressWithStatusBarsTTY) drawInSitu() { + p.moveToOrigin() + p.draw() +} + +func calcStatusBarLabelWidth(statusBars []*StatusBar, termWidth int) int { + statusBarLabelWidth := 0 + for _, bar := range statusBars { + labelWidth := runewidth.StringWidth(bar.label) + if labelWidth > statusBarLabelWidth { + statusBarLabelWidth = labelWidth + } + } + + statusBarPrefixWidth := 4 // statusBars have box char and space + if maxWidth := termWidth/2 - statusBarPrefixWidth; (statusBarLabelWidth + 2) > maxWidth { + statusBarLabelWidth = maxWidth - 2 + } + + return statusBarLabelWidth + +} + +func (p *progressWithStatusBarsTTY) writeStatusBar(last bool, statusBar *StatusBar) { + style := StylePending + if statusBar.completed { + if statusBar.failed { + style = StyleWarning + } else { + style = StyleSuccess + } + } + + box := "├── " + if last { + box = "└── " + } + const boxWidth = 4 + + labelFillWidth := p.statusBarLabelWidth + 2 + label := runewidth.FillRight(runewidth.Truncate(statusBar.label, p.statusBarLabelWidth, "..."), labelFillWidth) + + duration := statusBar.runtime().String() + durationLength := runewidth.StringWidth(duration) + + textMaxLength := p.o.caps.Width - boxWidth - labelFillWidth - (durationLength + 2) + text := runewidth.Truncate(fmt.Sprintf(statusBar.format, p.o.caps.formatArgs(statusBar.args)...), textMaxLength, "...") + + // The text might contain invisible control characters, so we need to + // exclude them when counting length + textLength := visibleStringWidth(text) + + durationMaxWidth := textMaxLength - textLength + (durationLength + 2) + durationText := runewidth.FillLeft(duration, durationMaxWidth) + + p.o.clearCurrentLine() + fmt.Fprint(p.o.w, style, box, label, StyleReset, text, StyleBold, durationText, StyleReset, "\n") +} + +func (p *progressWithStatusBarsTTY) Verbose(s string) { + if p.o.verbose { + p.Write(s) + } +} + +func (p *progressWithStatusBarsTTY) Verbosef(format string, args ...any) { + if p.o.verbose { + p.Writef(format, args...) + } +} + +func (p *progressWithStatusBarsTTY) VerboseLine(line FancyLine) { + if p.o.verbose { + p.WriteLine(line) + } +} + +func (p *progressWithStatusBarsTTY) Write(s string) { + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + p.o.clearCurrentLine() + fmt.Fprintln(p.o.w, s) + p.draw() +} + +func (p *progressWithStatusBarsTTY) Writef(format string, args ...any) { + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + p.o.clearCurrentLine() + fmt.Fprintf(p.o.w, format, p.o.caps.formatArgs(args)...) + fmt.Fprint(p.o.w, "\n") + p.draw() +} + +func (p *progressWithStatusBarsTTY) WriteLine(line FancyLine) { + p.o.Lock() + defer p.o.Unlock() + + p.moveToOrigin() + p.o.clearCurrentLine() + line.write(p.o.w, p.o.caps) + p.draw() +} diff --git a/lib/output/spinner.go b/lib/output/spinner.go new file mode 100644 index 0000000000..b0b5b86de2 --- /dev/null +++ b/lib/output/spinner.go @@ -0,0 +1,47 @@ +package output + +import "time" + +type spinner struct { + C chan string + + done chan chan struct{} +} + +var spinnerStrings = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + +func newSpinner(interval time.Duration) *spinner { + c := make(chan string) + done := make(chan chan struct{}) + s := &spinner{ + C: c, + done: done, + } + + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + defer close(s.C) + + i := 0 + for { + select { + case <-ticker.C: + i = (i + 1) % len(spinnerStrings) + s.C <- spinnerStrings[i] + + case c := <-done: + c <- struct{}{} + return + } + } + }() + + return s +} + +func (s *spinner) stop() { + c := make(chan struct{}) + s.done <- c + <-c +} diff --git a/lib/output/status_bar.go b/lib/output/status_bar.go new file mode 100644 index 0000000000..e43868fc86 --- /dev/null +++ b/lib/output/status_bar.go @@ -0,0 +1,68 @@ +package output + +import "time" + +// StatusBar is a sub-element of a progress bar that displays the current status +// of a process. +type StatusBar struct { + completed bool + failed bool + + label string + format string + args []any + + initialized bool + startedAt time.Time + finishedAt time.Time +} + +// Completef sets the StatusBar to completed and updates its text. +func (sb *StatusBar) Completef(format string, args ...any) { + sb.completed = true + sb.format = format + sb.args = args + sb.finishedAt = time.Now() +} + +// Failf sets the StatusBar to completed and failed and updates its text. +func (sb *StatusBar) Failf(format string, args ...any) { + sb.Completef(format, args...) + sb.failed = true +} + +// Resetf sets the status of the StatusBar to incomplete and updates its label and text. +func (sb *StatusBar) Resetf(label, format string, args ...any) { + sb.initialized = true + sb.completed = false + sb.failed = false + sb.label = label + sb.format = format + sb.args = args + sb.startedAt = time.Now() + sb.finishedAt = time.Time{} +} + +// Updatef updates the StatusBar's text. +func (sb *StatusBar) Updatef(format string, args ...any) { + sb.initialized = true + sb.format = format + sb.args = args +} + +func (sb *StatusBar) runtime() time.Duration { + if sb.startedAt.IsZero() { + return 0 + } + if sb.finishedAt.IsZero() { + return time.Since(sb.startedAt).Truncate(time.Second) + } + + return sb.finishedAt.Sub(sb.startedAt).Truncate(time.Second) +} + +func NewStatusBarWithLabel(label string) *StatusBar { + return &StatusBar{label: label, startedAt: time.Now()} +} + +func NewStatusBar() *StatusBar { return &StatusBar{} } diff --git a/lib/output/style.go b/lib/output/style.go new file mode 100644 index 0000000000..f27a6d6530 --- /dev/null +++ b/lib/output/style.go @@ -0,0 +1,75 @@ +package output + +import ( + "fmt" + "strings" +) + +type Style struct{ code string } + +func (s Style) String() string { return s.code } + +// Line returns a FancyLine using this style as an alias for using output.Styledf(...) +func (s Style) Line(format string) FancyLine { return Styled(s, format) } + +// Linef returns a FancyLine using this style as an alias for using output.Styledf(...) +func (s Style) Linef(format string, args ...any) FancyLine { + return Styledf(s, format, args...) +} + +func CombineStyles(styles ...Style) Style { + sb := strings.Builder{} + for _, s := range styles { + fmt.Fprint(&sb, s) + } + return Style{sb.String()} +} + +func Fg256Color(code int) Style { return Style{fmt.Sprintf("\033[38;5;%dm", code)} } +func Bg256Color(code int) Style { return Style{fmt.Sprintf("\033[48;5;%dm", code)} } + +var ( + StyleReset = Style{"\033[0m"} + StyleLogo = Fg256Color(57) + StylePending = Fg256Color(4) + StyleWarning = Fg256Color(124) + StyleFailure = CombineStyles(StyleBold, Fg256Color(196)) + StyleSuccess = Fg256Color(2) + StyleSuggestion = Fg256Color(244) + + StyleBold = Style{"\033[1m"} + StyleItalic = Style{"\033[3m"} + StyleUnderline = Style{"\033[4m"} + + // Search-specific colors. + StyleSearchQuery = Fg256Color(68) + StyleSearchBorder = Fg256Color(239) + StyleSearchLink = Fg256Color(237) + StyleSearchRepository = Fg256Color(23) + StyleSearchFilename = Fg256Color(69) + StyleSearchMatch = CombineStyles(Fg256Color(0), Bg256Color(11)) + StyleSearchLineNumbers = Fg256Color(69) + StyleSearchCommitAuthor = Fg256Color(2) + StyleSearchCommitSubject = Fg256Color(68) + StyleSearchCommitDate = Fg256Color(23) + + StyleWhiteOnPurple = CombineStyles(Fg256Color(255), Bg256Color(55)) + StyleGreyBackground = CombineStyles(Fg256Color(0), Bg256Color(242)) + + // Search alert specific colors. + StyleSearchAlertTitle = Fg256Color(124) + StyleSearchAlertDescription = Fg256Color(124) + StyleSearchAlertProposedTitle = Style{""} + StyleSearchAlertProposedQuery = Fg256Color(69) + StyleSearchAlertProposedDescription = Style{""} + + StyleLinesDeleted = Fg256Color(196) + StyleLinesAdded = Fg256Color(2) + + // Colors + StyleGrey = Fg256Color(8) + StyleYellow = Fg256Color(220) + StyleOrange = Fg256Color(202) + StyleRed = Fg256Color(196) + StyleGreen = Fg256Color(2) +) diff --git a/lib/output/table.go b/lib/output/table.go new file mode 100644 index 0000000000..6736b30756 --- /dev/null +++ b/lib/output/table.go @@ -0,0 +1,52 @@ +package output + +import ( + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" +) + +const ( + purple = lipgloss.Color("99") + gray = lipgloss.Color("245") + lightGray = lipgloss.Color("241") +) + +type Table struct { + tbl *table.Table + + headers []string +} + +func NewTable(headers []string) *Table { + t := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(purple)). + BorderRow(true). + BorderColumn(true). + StyleFunc(func(row, col int) lipgloss.Style { + s := lipgloss.NewStyle().Width(30) + + // this is the stle to use for the header row + if row == 0 { + return s.Foreground(purple).Bold(true).Align(lipgloss.Center) + } + return s + }). + Headers(headers...) + return &Table{ + tbl: t, + headers: headers, + } +} + +func (t *Table) AddRow(elems ...string) { + t.tbl.Row(elems...) +} + +func (t *Table) AddRows(elems ...[]string) { + t.tbl.Rows(elems...) +} + +func (t *Table) Render() string { + return t.tbl.Render() +} diff --git a/lib/output/visible_string_width.go b/lib/output/visible_string_width.go new file mode 100644 index 0000000000..08cb8c8e33 --- /dev/null +++ b/lib/output/visible_string_width.go @@ -0,0 +1,16 @@ +package output + +import ( + "github.com/grafana/regexp" + "github.com/mattn/go-runewidth" +) + +// This regex is taken from here: +// https://github.com/acarl005/stripansi/blob/5a71ef0e047df0427e87a79f27009029921f1f9b/stripansi.go +const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" + +var ansiRegex = regexp.MustCompile(ansi) + +func visibleStringWidth(str string) int { + return runewidth.StringWidth(ansiRegex.ReplaceAllString(str, "")) +} diff --git a/lib/process/pipe.go b/lib/process/pipe.go new file mode 100644 index 0000000000..b7a6f9a40a --- /dev/null +++ b/lib/process/pipe.go @@ -0,0 +1,140 @@ +package process + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "io/fs" + + "github.com/sourcegraph/conc/pool" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// initialBufSize is the initial size of the buffer that PipeOutput uses to +// read lines. +const initialBufSize = 4 * 1024 // 4k +// maxTokenSize is the max size of a token that PipeOutput reads. +const maxTokenSize = 100 * 1024 * 1024 // 100mb + +type pipe func(w io.Writer, r io.Reader) error + +type cmdPiper interface { + StdoutPipe() (io.ReadCloser, error) + StderrPipe() (io.ReadCloser, error) +} + +// NewOutputScannerWithSplit creates a new bufio.Scanner using the given split +// function with well-working defaults for the initial and max buf sizes. +func NewOutputScannerWithSplit(r io.Reader, split bufio.SplitFunc) *bufio.Scanner { + scanner := bufio.NewScanner(r) + scanner.Split(split) + buf := make([]byte, initialBufSize) + scanner.Buffer(buf, maxTokenSize) + return scanner +} + +// PipeOutput reads stdout/stderr output of the given command into the two +// io.Writers. +// +// It returns a errgroup.Group. The caller *must* call the Wait() method of the +// errgroup.Group **before** waiting for the *exec.Cmd to finish. +// +// The passed in context should be canceled when done. +// +// See this issue for more details: https://github.com/golang/go/issues/21922 +func PipeOutput(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer) (*pool.ErrorPool, error) { + pipe := func(w io.Writer, r io.Reader) error { + scanner := NewOutputScannerWithSplit(r, scanLinesWithNewline) + + for scanner.Scan() { + if _, err := fmt.Fprint(w, scanner.Text()); err != nil { + return err + } + } + + return scanner.Err() + } + + return PipeProcessOutput(ctx, c, stdoutWriter, stderrWriter, pipe) +} + +// PipeOutputUnbuffered is the unbuffered version of PipeOutput and uses +// io.Copy instead of piping output line-based to the output. +func PipeOutputUnbuffered(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer) (*pool.ErrorPool, error) { + pipe := func(w io.Writer, r io.Reader) error { + _, err := io.Copy(w, r) + // We can ignore ErrClosed because we get that if a process crashes + if err != nil && !errors.Is(err, fs.ErrClosed) { + return err + } + return nil + } + + return PipeProcessOutput(ctx, c, stdoutWriter, stderrWriter, pipe) +} + +func PipeProcessOutput(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer, fn pipe) (*pool.ErrorPool, error) { + stdoutPipe, err := c.StdoutPipe() + if err != nil { + return nil, errors.Wrap(err, "failed to attach stdout pipe") + } + + stderrPipe, err := c.StderrPipe() + if err != nil { + return nil, errors.Wrap(err, "failed to attach stderr pipe") + } + + context.AfterFunc(ctx, func() { + // There is a deadlock condition due the following strange decisions: + // + // 1. The pipes attached to a command are not closed if the context + // attached to the command is canceled. The pipes are only closed + // after Wait has been called. + // 2. According to the docs, we are not meant to call cmd.Wait() until + // we have complete read the pipes attached to the command. + // + // Since we're following the expected usage, we block on a wait group + // tracking the consumption of stdout and stderr pipes in two separate + // goroutines between calls to Start and Wait. This means that if there + // is a reason the command is abandoned but the pipes are not closed + // (such as context cancellation), we will hang indefinitely. + // + // To be defensive, we'll forcibly close both pipes when the context has + // finished. These may return an ErrClosed condition, but we don't really + // care: the command package doesn't surface errors when closing the pipes + // either. + stdoutPipe.Close() + stderrPipe.Close() + }) + + eg := pool.New().WithErrors() + + eg.Go(func() error { return fn(stdoutWriter, stdoutPipe) }) + eg.Go(func() error { return fn(stderrWriter, stderrPipe) }) + + return eg, nil +} + +// scanLinesWithNewline is a modified version of bufio.ScanLines that retains +// the trailing newline byte(s) in the returned token. +func scanLinesWithNewline(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + if i := bytes.IndexByte(data, '\n'); i >= 0 { + // We have a full newline-terminated line. + return i + 1, data[0 : i+1], nil + } + + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + + // Request more data. + return 0, nil, nil +} From 3517e7f48a58007ecf7f06535a0091daeea9c284 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Tue, 18 Nov 2025 09:37:25 +0200 Subject: [PATCH 3/3] use vendored in lib Requires one fix due to an API change in lib --- cmd/src/code_intel_upload_flags.go | 6 +- go.mod | 129 +++++++----- go.sum | 302 +++++++++++++++++++---------- 3 files changed, 282 insertions(+), 155 deletions(-) diff --git a/cmd/src/code_intel_upload_flags.go b/cmd/src/code_intel_upload_flags.go index 2a1b31c289..6a1f3031a0 100644 --- a/cmd/src/code_intel_upload_flags.go +++ b/cmd/src/code_intel_upload_flags.go @@ -2,6 +2,7 @@ package main import ( "compress/gzip" + "context" "flag" "fmt" "io" @@ -330,13 +331,14 @@ func readIndexerNameAndVersion(indexFile string) (string, string, error) { var metadata *scip.Metadata visitor := scip.IndexVisitor{ - VisitMetadata: func(m *scip.Metadata) { + VisitMetadata: func(ctx context.Context, m *scip.Metadata) error { metadata = m + return nil }, } // convert file to io.Reader - if err := visitor.ParseStreaming(indexReader); err != nil { + if err := visitor.ParseStreaming(context.Background(), indexReader); err != nil { return "", "", err } diff --git a/go.mod b/go.mod index c1028d1880..ea881257ac 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/sourcegraph/src-cli -go 1.23.12 +go 1.24.1 require ( cloud.google.com/go/storage v1.30.1 @@ -13,26 +13,26 @@ require ( github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663 github.com/dustin/go-humanize v1.0.1 github.com/gobwas/glob v0.2.3 - github.com/google/go-cmp v0.6.0 - github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db + github.com/google/go-cmp v0.7.0 + github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 github.com/hexops/autogold v1.3.1 - github.com/jedib0t/go-pretty/v6 v6.3.7 + github.com/jedib0t/go-pretty/v6 v6.6.3 github.com/jig/teereadcloser v0.0.0-20181016160506-953720c48e05 github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/mattn/go-isatty v0.0.19 + github.com/mattn/go-isatty v0.0.20 github.com/neelance/parallel v0.0.0-20160708114440-4de9ce63d14c github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/sourcegraph/conc v0.3.1-0.20240108182409-4afefce20f9b - github.com/sourcegraph/go-diff v0.6.2-0.20221123165719-f8cd299c40f3 + github.com/sourcegraph/go-diff v0.7.0 github.com/sourcegraph/jsonx v0.0.0-20200629203448-1a936bd500cf - github.com/sourcegraph/scip v0.3.1-0.20230627154934-45df7f6d33fc + github.com/sourcegraph/scip v0.6.1 github.com/sourcegraph/sourcegraph/lib v0.0.0-20240709083501-1af563b61442 - github.com/stretchr/testify v1.9.0 - golang.org/x/net v0.38.0 - golang.org/x/sync v0.12.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/net v0.44.0 + golang.org/x/sync v0.17.0 google.golang.org/api v0.132.0 - google.golang.org/protobuf v1.35.1 + google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7 k8s.io/api v0.32.3 @@ -41,8 +41,33 @@ require ( ) require ( + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/bufbuild/connect-go v1.9.0 // indirect + github.com/bufbuild/connect-opentelemetry-go v0.4.0 // indirect + github.com/bufbuild/protocompile v0.5.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/docker/cli v24.0.4+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.4+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/fgprof v0.9.3 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gofrs/uuid/v5 v5.0.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-containerregistry v0.15.2 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.3 // indirect @@ -50,7 +75,24 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc4 // indirect + github.com/rs/cors v1.9.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a // indirect + github.com/tetratelabs/wazero v1.3.0 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -60,11 +102,10 @@ require ( cloud.google.com/go v0.110.9 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.4 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect - github.com/alecthomas/chroma v0.10.0 // indirect github.com/aws/aws-sdk-go-v2 v1.17.5 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.13.13 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 // indirect @@ -78,25 +119,23 @@ require ( github.com/aws/smithy-go v1.13.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/bufbuild/buf v1.4.0 // indirect - github.com/charmbracelet/glamour v0.5.0 // indirect - github.com/cockroachdb/errors v1.11.1 // indirect + github.com/bufbuild/buf v1.25.0 // indirect + github.com/charmbracelet/glamour v0.10.0 // indirect + github.com/cockroachdb/errors v1.12.0 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect - github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/redact v1.1.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dlclark/regexp2 v1.8.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect github.com/fatih/color v1.15.0 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/gofrs/flock v0.8.1 // indirect - github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -104,46 +143,44 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/gorilla/css v1.0.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/hexops/valast v1.4.3 // indirect + github.com/hexops/valast v1.4.4 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a // indirect - github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f // indirect - github.com/jhump/protoreflect v1.12.1-0.20220417024638-438db461d753 // indirect + github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.16.7 // indirect - github.com/klauspost/pgzip v1.2.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/microcosm-cc/bluemonday v1.0.23 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect + github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.1 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-proto-validators v0.3.2 // indirect github.com/nightlyone/lockfile v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/profile v1.6.0 // indirect + github.com/pkg/profile v1.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pseudomuto/protoc-gen-doc v1.5.1 // indirect github.com/pseudomuto/protokit v0.2.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sourcegraph/log v0.0.0-20231018134238-fbadff7458bb // indirect + github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect @@ -151,20 +188,20 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/yuin/goldmark v1.5.2 // indirect - github.com/yuin/goldmark-emoji v1.0.1 // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/mod v0.21.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/mod v0.28.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.26.0 // indirect + golang.org/x/tools v0.37.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect @@ -174,7 +211,7 @@ require ( k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - mvdan.cc/gofumpt v0.4.0 // indirect + mvdan.cc/gofumpt v0.5.0 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect @@ -183,4 +220,4 @@ require ( // See: https://github.com/ghodss/yaml/pull/65 replace github.com/ghodss/yaml => github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152 -replace github.com/sourcegraph/sourcegraph/lib => github.com/sourcegraph/sourcegraph-public-snapshot/lib v0.0.0-20240709083501-1af563b61442 +replace github.com/sourcegraph/sourcegraph/lib => ./lib diff --git a/go.sum b/go.sum index d1f4a1f59c..4a873f16a1 100644 --- a/go.sum +++ b/go.sum @@ -8,17 +8,24 @@ cloud.google.com/go/iam v1.1.4 h1:K6n/GZHFTtEoKT5aUG3l9diPi0VduZNQ1PfdnpkkIFk= cloud.google.com/go/iam v1.1.4/go.mod h1:l/rg8l1AaA+VFMho/HYx2Vv6xinPSLMF8qfhRPIZ0L8= cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= -github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4= @@ -55,29 +62,58 @@ github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/bufbuild/buf v1.4.0 h1:GqE3a8CMmcFvWPzuY3Mahf9Kf3S9XgZ/ORpfYFzO+90= -github.com/bufbuild/buf v1.4.0/go.mod h1:mwHG7klTHnX+rM/ym8LXGl7vYpVmnwT96xWoRB4H5QI= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bufbuild/buf v1.25.0 h1:HFxKrR8wFcZwrBInN50K/oJX/WOtPVq24rHb/ArjfBA= +github.com/bufbuild/buf v1.25.0/go.mod h1:GCKZ5bAP6Ht4MF7KcfaGVgBEXGumwAz2hXjjLVxx8ZU= +github.com/bufbuild/connect-go v1.9.0 h1:JIgAeNuFpo+SUPfU19Yt5TcWlznsN5Bv10/gI/6Pjoc= +github.com/bufbuild/connect-go v1.9.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= +github.com/bufbuild/connect-opentelemetry-go v0.4.0 h1:6JAn10SNqlQ/URhvRNGrIlczKw1wEXknBUUtmWqOiak= +github.com/bufbuild/connect-opentelemetry-go v0.4.0/go.mod h1:nwPXYoDOoc2DGyKE/6pT1Q9MPSi2Et2e6BieMD0l6WU= +github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg= +github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g= -github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= -github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= +github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= -github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= @@ -93,9 +129,20 @@ github.com/derision-test/glock v1.0.0 h1:b6sViZG+Cm6QtdpqbfWEjaBVbzNPntIS4GzsxpS github.com/derision-test/glock v1.0.0/go.mod h1:jKtLdBMrF+XQatqvg46wiWdDfDSSDjdhO4dOM2FX9H4= github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663 h1:fctNkSsavbXpt8geFWZb8n+noCqS8MrOXRJ/YfdZ2dQ= github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663/go.mod h1:Kad2hux31v/IyD4Rf4wAwIyK48995rs3qAl9IUAhc2k= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.8.0 h1:rJD5HeGIT/2b5CDk63FVCwZA3qgYElfg+oQK7uH5pfE= -github.com/dlclark/regexp2 v1.8.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v24.0.4+incompatible h1:Y3bYF9ekNTm2VFz5U/0BlMdJy73D+Y1iAAZ8l63Ydzw= +github.com/docker/cli v24.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.4+incompatible h1:s/LVDftw9hjblvqIeTiGYXBCD95nOEEl7qRsRrIOuQI= +github.com/docker/docker v24.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= +github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -104,7 +151,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= @@ -112,16 +158,24 @@ github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= @@ -136,8 +190,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= +github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -176,13 +230,16 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.15.2 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE= +github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= @@ -194,20 +251,24 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvki github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA= -github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY= github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo= github.com/hexops/autogold v1.3.1/go.mod h1:sQO+mQUCVfxOKPht+ipDSkJ2SCJ7BNJVHZexsXqWMx4= +github.com/hexops/autogold/v2 v2.2.1 h1:JPUXuZQGkcQMv7eeDXuNMovjfoRYaa0yVcm+F3voaGY= +github.com/hexops/autogold/v2 v2.2.1/go.mod h1:IJwxtUfj1BGLm0YsR/k+dIxYi6xbeLjqGke2bzcOTMI= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hexops/valast v1.4.3 h1:oBoGERMJh6UZdRc6cduE1CTPK+VAdXA59Y1HFgu3sm0= github.com/hexops/valast v1.4.3/go.mod h1:Iqx2kLj3Jn47wuXpj3wX40xn6F93QNFBHuiKBerkTGA= +github.com/hexops/valast v1.4.4 h1:rETyycw+/L2ZVJHHNxEBgh8KUn+87WugH9MxcEv9PGs= +github.com/hexops/valast v1.4.4/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -227,18 +288,12 @@ github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUO github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a h1:d4+I1YEKVmWZrgkt6jpXBnLgV2ZjO0YxEtLDdfIZfH4= -github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= -github.com/jedib0t/go-pretty/v6 v6.3.7 h1:H3Ulkf7h6A+p0HgKBGzgDn0bZIupRbKKWF4pO4Bs7iA= -github.com/jedib0t/go-pretty/v6 v6.3.7/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= -github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= -github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= -github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= -github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f h1:BNuUg9k2EiJmlMwjoef3e8vZLHplbVw6DrjGFjLL+Yo= -github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f/go.mod h1:qr2b5kx4HbFS7/g4uYO5qv9ei8303JMsC7ESbYiqr2Q= -github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= -github.com/jhump/protoreflect v1.12.1-0.20220417024638-438db461d753 h1:uFlcJKZPLQd7rmOY/RrvBuUaYmAFnlFHKLivhO6cOy8= -github.com/jhump/protoreflect v1.12.1-0.20220417024638-438db461d753/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= +github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co= +github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= +github.com/jedib0t/go-pretty/v6 v6.6.3 h1:nGqgS0tgIO1Hto47HSaaK4ac/I/Bu7usmdD3qvs0WvM= +github.com/jedib0t/go-pretty/v6 v6.6.3/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jig/teereadcloser v0.0.0-20181016160506-953720c48e05 h1:dSwwtWuwMyarzsbVWOq4QJ8xVy9wgcNomvWyGtrKe+E= github.com/jig/teereadcloser v0.0.0-20181016160506-953720c48e05/go.mod h1:sRUFlj+HCejvoCRpuhU0EYnNw5FG+YJpz8UFfCf0F2U= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -254,10 +309,10 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= -github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -275,35 +330,35 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= -github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= -github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-proto-validators v0.3.2 h1:qRlmpTzm2pstMKKzTdvwPCF5QfBNURSlAgN/R+qbKos= @@ -319,6 +374,10 @@ github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= +github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -326,8 +385,8 @@ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzL github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= -github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -338,29 +397,34 @@ github.com/pseudomuto/protokit v0.2.0 h1:hlnBDcy3YEDXH7kc9gV+NLaN0cDzhDvD1s7Y6FZ github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= +github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a h1:j/CQ27s679M9wRGBRJYyXGrfkYuQA6VMnD7R08mHD9c= +github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a/go.mod h1:JG1sdvGTKWwe/oH3/3UKQ26vfcHIN//7fwEJhoqaBcM= github.com/sourcegraph/conc v0.3.1-0.20240108182409-4afefce20f9b h1:0FsjN+u9fNXTS2C1JnJ6G/gkgcUg8dscyyfu3PwHWUQ= github.com/sourcegraph/conc v0.3.1-0.20240108182409-4afefce20f9b/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/sourcegraph/go-diff v0.6.2-0.20221123165719-f8cd299c40f3 h1:11miag7hlORpW7ici5mL7T9PyiEsmVmf+8PFOvJ/ZrA= -github.com/sourcegraph/go-diff v0.6.2-0.20221123165719-f8cd299c40f3/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/sourcegraph/jsonx v0.0.0-20200629203448-1a936bd500cf h1:oAdWFqhStsWiiMP/vkkHiMXqFXzl1XfUNOdxKJbd6bI= github.com/sourcegraph/jsonx v0.0.0-20200629203448-1a936bd500cf/go.mod h1:ppFaPm6kpcHnZGqQTFhUIAQRIEhdQDWP1PCv4/ON354= -github.com/sourcegraph/log v0.0.0-20231018134238-fbadff7458bb h1:tHKdC+bXxxGJ0cy/R06kg6Z0zqwVGOWMx8uWsIwsaoY= -github.com/sourcegraph/log v0.0.0-20231018134238-fbadff7458bb/go.mod h1:IDp09QkoqS8Z3CyN2RW6vXjgABkNpDbyjLIHNQwQ8P8= -github.com/sourcegraph/scip v0.3.1-0.20230627154934-45df7f6d33fc h1:o+eq0cjVV3B5ngIBF04Lv3GwttKOuYFF5NTcfXWXzfA= -github.com/sourcegraph/scip v0.3.1-0.20230627154934-45df7f6d33fc/go.mod h1:7ZKAtLIUmiMvOIgG5LMcBxdtBXVa0v2GWC4Hm1ASYQ0= -github.com/sourcegraph/sourcegraph-public-snapshot/lib v0.0.0-20240709083501-1af563b61442 h1:M/jSgKSzeYbfrE/C6H8b8zu07c59taz4fIQcOGh1vqY= -github.com/sourcegraph/sourcegraph-public-snapshot/lib v0.0.0-20240709083501-1af563b61442/go.mod h1:RBdAauod1tGL33L1Izr2YymwN8uWQV++fL6orhD4aqc= +github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b h1:2FQ72y5zECMu9e5z5jMnllb5n1jVK7qsvgjkVtdFV+g= +github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b/go.mod h1:IDp09QkoqS8Z3CyN2RW6vXjgABkNpDbyjLIHNQwQ8P8= +github.com/sourcegraph/scip v0.6.1 h1:lcBrTwGjsqpMLQN4pVfJYSB+8oVoc8Lnqb4FAfU8EBM= +github.com/sourcegraph/scip v0.6.1/go.mod h1:c6d9mk1cGT6OXdHutxDakKVEPN1D+iFoYWCsrDxEhus= github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152 h1:z/MpntplPaW6QW95pzcAR/72Z5TWDyDnSo0EOcyij9o= github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= @@ -380,11 +444,15 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tetratelabs/wazero v1.3.0 h1:nqw7zCldxE06B8zSZAY0ACrR9OH5QCcPwYmYlwtcwtE= +github.com/tetratelabs/wazero v1.3.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -394,16 +462,32 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= -github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= -github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= +github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= +go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -418,9 +502,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -428,8 +514,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -439,17 +525,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= @@ -460,12 +544,11 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -479,19 +562,24 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -499,8 +587,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -515,8 +603,8 @@ golang.org/x/tools v0.0.0-20200624163319-25775e59acb7/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -546,7 +634,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= @@ -561,12 +648,10 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -583,6 +668,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:mub0MmFLOn8XLikZOAhgLD1kXJq8jgftSrrv7m00xFo= @@ -599,8 +686,9 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= +mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= +mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=