diff --git a/lib/README.md b/lib/README.md index e9bb62031c..ba87609b04 100644 --- a/lib/README.md +++ b/lib/README.md @@ -5,8 +5,8 @@ This directory contains vendored packages from `github.com/sourcegraph/sourcegra ## Source - **Repository**: https://github.com/sourcegraph/sourcegraph -- **Commit**: bdc2f4bb8b59f78f4fa8868b2690b673b41948d4 -- **Date**: 2026-06-01 07:34:50 +0100 +- **Commit**: de6bd07264df18d8dc66e2aebeaad18ac504390f +- **Date**: 2026-06-11 13:59:53 +0000 ## Updating diff --git a/lib/batches/batch_spec.go b/lib/batches/batch_spec.go index 30ef50d4a9..36a730dbed 100644 --- a/lib/batches/batch_spec.go +++ b/lib/batches/batch_spec.go @@ -35,11 +35,39 @@ type BatchSpec struct { Description string `json:"description,omitempty" yaml:"description"` On []OnQueryOrRepository `json:"on,omitempty" yaml:"on"` Workspaces []WorkspaceConfiguration `json:"workspaces,omitempty" yaml:"workspaces"` + Checkout *CheckoutOptions `json:"checkout,omitempty" yaml:"checkout,omitempty"` 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"` - ChangesetHooks *ChangesetHooks `json:"changesetHooks,omitempty" yaml:"hooks,omitempty"` + ChangesetHooks *ChangesetHooks `json:"changesetHooks,omitempty" yaml:"changesetHooks,omitempty"` +} + +// DefaultCheckoutFetchDepth is the git fetch depth used for workspace checkouts +// when the spec does not configure checkout.fetchDepth. It fetches only the +// target commit (a shallow clone), which is sufficient for most batch changes. +const DefaultCheckoutFetchDepth = 1 + +// CheckoutOptions controls how repositories are checked out for workspace +// execution. Only allowed when Version is 3. +type CheckoutOptions struct { + // FetchDepth controls how many commits of git history are fetched into the + // workspace checkout. A value of 0 fetches the full history; any positive + // value fetches that many commits. When nil, DefaultCheckoutFetchDepth is + // used. History is required by tasks that inspect git history (e.g. + // "update codeowners based on git history") or changeset hooks such as + // onMergeConflict. + FetchDepth *int `json:"fetchDepth,omitempty" yaml:"fetchDepth,omitempty"` +} + +// CheckoutFetchDepth returns the configured git fetch depth for workspace +// checkouts, applying DefaultCheckoutFetchDepth when unset. A return value of 0 +// means full history. +func (s *BatchSpec) CheckoutFetchDepth() int { + if s.Checkout == nil || s.Checkout.FetchDepth == nil { + return DefaultCheckoutFetchDepth + } + return *s.Checkout.FetchDepth } // Hooks declares side-effect actions to run at well-defined changeset @@ -54,16 +82,76 @@ type ChangesetHooks struct { // Hook actions reuse the Step shape from the top-level steps block. type ChangesetHookAction struct { Steps []Step `json:"steps,omitempty" yaml:"steps,omitempty"` + // Commit configures the follow-up commits this hook produces. Its message + // and author are resolved independently: when the message is empty a + // per-event default is used, and when the author is nil the + // changesetTemplate's author is inherited (which itself falls back to the + // changeset's author). It may be nil if the hook does not configure a + // commit at all. + Commit *ExpandedGitCommitDescription `json:"commit,omitempty" yaml:"commit,omitempty"` +} + +// HasCommit reports whether the hook action declares its own commit +// information (a message and/or an author). +func (a ChangesetHookAction) HasCommit() bool { + return a.Commit != nil && (a.Commit.Message != "" || a.Commit.Author != nil) +} + +// DefaultCommitMessage returns the commit message used for follow-up commits +// produced by this hook event when the hook action does not provide its own +// message. The returned value may contain changeset template variables (e.g. +// ${{ repository.branch }}) that are rendered when the follow-up commit is +// built. +func (e ChangesetHookEvent) DefaultCommitMessage() string { + switch e { + case ChangesetHookEventOnMergeConflict: + return "Fix for merge conflict on ${{ repository.branch }}" + case ChangesetHookEventOnCIFailure: + return "Fix for CI failure on ${{ repository.branch }}" + default: + return "Changeset hook fix on ${{ repository.branch }}" + } +} + +// ActionForEvent returns the hook action configured for the given event, and +// whether the event is known. +func (h *ChangesetHooks) ActionForEvent(event ChangesetHookEvent) (ChangesetHookAction, bool) { + switch event { + case ChangesetHookEventOnCIFailure: + return h.OnCIFailure, true + case ChangesetHookEventOnMergeConflict: + return h.OnMergeConflict, true + default: + return ChangesetHookAction{}, false + } } -type changesetHookEvent string +type ChangesetHookEvent string // Hook event names. Kept here so callers don't pass typoed strings. const ( - ChangesetHookEventOnCIFailure changesetHookEvent = "onCIFailure" - ChangesetHookEventOnMergeConflict changesetHookEvent = "onMergeConflict" + ChangesetHookEventOnCIFailure ChangesetHookEvent = "onCIFailure" + ChangesetHookEventOnMergeConflict ChangesetHookEvent = "onMergeConflict" ) +// AllChangesetHookEvents is the canonical list of supported changeset hook +// events. Add new events here so callers (validation, filtering, etc.) stay in +// sync from a single source of truth. +var AllChangesetHookEvents = []ChangesetHookEvent{ + ChangesetHookEventOnCIFailure, + ChangesetHookEventOnMergeConflict, +} + +// Valid reports whether e is a known changeset hook event. +func (e ChangesetHookEvent) Valid() bool { + for _, known := range AllChangesetHookEvents { + if e == known { + return true + } + } + return false +} + type ChangesetTemplate struct { Title string `json:"title,omitempty" yaml:"title"` Body string `json:"body,omitempty" yaml:"body"` @@ -127,9 +215,16 @@ type Step struct { If any `json:"if,omitempty" yaml:"if,omitempty"` } +type CodingAgentType string + +const ( + CodingAgentTypeCodex CodingAgentType = "codex" + CodingAgentTypeClaudeCode CodingAgentType = "claude-code" +) + type CodingAgentStep struct { - Type string `json:"type,omitempty" yaml:"type"` - Prompt string `json:"prompt,omitempty" yaml:"prompt"` + Type CodingAgentType `json:"type,omitempty" yaml:"type"` + Prompt string `json:"prompt,omitempty" yaml:"prompt"` } type BuildImageStep struct { @@ -137,6 +232,30 @@ type BuildImageStep struct { BaseImage string `json:"baseImage" yaml:"baseImage"` } +// KanikoImage is the container image used to build new OCI images from a base +// image and a run script (see buildImage steps). It is referenced both when +// desugaring buildImage steps into run steps and by the executor when deciding +// whether a step is a buildImage-derived build container. Can be exchanged for +// other image build tooling, as long as the build script is updated as well. +// +// Kaniko builds run unprivileged under docker's default seccomp/apparmor +// profiles, so no extra docker flags are required for this container. +// +// The -alpine variant is required: the desugared buildImage step runs as an +// ordinary `run` step, so the runner (batch-exec/src-cli) probes the image by +// running ` -c mktemp` and overrides the entrypoint with /bin/sh. The +// plain kaniko image has no shell at all, and the -debug variant is built FROM +// scratch without a /tmp directory, so its mktemp fails and the probe rejects +// the image. -alpine has /bin/sh, /tmp, and the busybox wget/base64 tools used +// by the generated build script. It also presets KANIKO_PRE_CLEANUP=1 and +// KANIKO_CLEANUP=1, which wipe the container filesystem around the build (the +// generated script must not rely on external binaries after invoking kaniko). +// +// Pinned to a digest so the build is reproducible and not subject to the tag +// being re-pushed; the tag is retained for readability. When bumping, update +// both the tag and the @sha256 digest (the multi-arch index digest). +const KanikoImage = "ghcr.io/osscontainertools/kaniko:v1.27.6-alpine@sha256:795a358f6c22a9fcd66bb7e14bd97728155e1c171ca951f3c3ba6501054234ce" + // MarshalJSON canonicalizes the v3 `image:` field into `container:` on the // wire. Both fields exist on Step for ergonomic reasons (v3 specs use // `image:`, v1/v2 specs use `container:`), but src-cli's Step has only @@ -240,6 +359,12 @@ func parseBatchSpec(schema string, data []byte) (*BatchSpec, error) { errs = errors.Append(errs, NewValidationError(errors.New("changesetTemplate.published is not supported in batch spec version 3; drive publication via the publish_changesets tool instead"))) } + // v3 specs do not support importChangesets — batch change agents only + // manage changesets they own. + if spec.Version == 3 && len(spec.ImportChangesets) != 0 { + errs = errors.Append(errs, NewValidationError(errors.New("importChangesets is not supported in batch spec version 3"))) + } + for i, step := range spec.Steps { for _, mount := range step.Mount { if strings.Contains(mount.Path, invalidMountCharacters) { @@ -255,20 +380,39 @@ func parseBatchSpec(schema string, data []byte) (*BatchSpec, error) { if step.BuildImage != nil && step.Run != "" { errs = errors.Append(errs, NewValidationError(errors.Newf("step %d: buildImage and run cannot be combined in the same step", i+1))) } - for name := range step.Files { - if strings.Contains(name, invalidMountCharacters) { - errs = errors.Append(errs, NewValidationError(errors.Newf("step %d files target path contains invalid characters", i+1))) - } - } } if hookErr := validateHooks(&spec); hookErr != nil { errs = errors.Append(errs, hookErr) } + if checkoutErr := validateCheckout(&spec); checkoutErr != nil { + errs = errors.Append(errs, checkoutErr) + } + return &spec, errs } +// validateCheckout performs Go-level validation of spec.Checkout beyond what +// the JSON schema enforces. The schema already gates `checkout:` on `version: 3` +// and constrains `fetchDepth` to a non-negative integer. We re-check the version +// invariant here so non-schema callers (and any future schema drift) still fail +// safely. +func validateCheckout(spec *BatchSpec) error { + if spec.Checkout == nil { + return nil + } + + var errs error + if spec.Version != 3 { + errs = errors.Append(errs, NewValidationError(errors.New("batch spec checkout requires version: 3"))) + } + if spec.Checkout.FetchDepth != nil && *spec.Checkout.FetchDepth < 0 { + errs = errors.Append(errs, NewValidationError(errors.New("checkout.fetchDepth must be greater than or equal to 0"))) + } + return errs +} + // validateHooks performs Go-level validation of spec.Hooks beyond what the // JSON schema enforces. The schema already gates `hooks:` on `version: 3` and // rejects unknown event names. We re-check the version invariant here so @@ -285,8 +429,30 @@ func validateHooks(spec *BatchSpec) error { errs = errors.Append(errs, NewValidationError(errors.New("batch spec hooks require version: 3"))) } - validate := func(event changesetHookEvent, action ChangesetHookAction) { + validate := func(event ChangesetHookEvent, action ChangesetHookAction) { for i, step := range action.Steps { + // Hook steps use the v3 step shape, which requires an image and + // exactly one of a run command or a codingAgent. The JSON schema + // also enforces this (see the version 3 conditional in + // batch_spec.schema.json), so for spec strings parsed through the + // schema these checks are a defense-in-depth backstop; we keep them + // here so non-schema callers (and any future schema drift) still + // fail safely. + if step.Image == "" { + errs = errors.Append(errs, NewValidationError(errors.Newf( + "hooks.%s step %d must specify an image", event, i+1, + ))) + } + if step.Run == "" && step.CodingAgent == nil { + errs = errors.Append(errs, NewValidationError(errors.Newf( + "hooks.%s step %d must specify either run or codingAgent", event, i+1, + ))) + } + if step.CodingAgent != nil && step.Run != "" { + errs = errors.Append(errs, NewValidationError(errors.Newf( + "hooks.%s step %d: codingAgent and run cannot be combined in the same step", event, i+1, + ))) + } for _, mount := range step.Mount { if strings.Contains(mount.Path, invalidMountCharacters) { errs = errors.Append(errs, NewValidationError(errors.Newf( @@ -370,12 +536,14 @@ func SkippedStepsForRepo(spec *BatchSpec, repoName string, fileMatches []string) return skipped, nil } -// RequiredEnvVars inspects all steps for outer environment variables used and -// compiles a deduplicated list from those. -func (s *BatchSpec) RequiredEnvVars() []string { +// RequiredEnvVarsForSteps inspects the given steps for outer environment +// variables used and compiles a deduplicated list from those. Callers pass the +// specific steps they care about (e.g. a spec's top-level steps or its +// changeset hook steps). +func RequiredEnvVarsForSteps(steps []Step) []string { requiredMap := map[string]struct{}{} required := []string{} - for _, step := range s.Steps { + for _, step := range steps { for _, v := range step.Env.OuterVars() { if _, ok := requiredMap[v]; !ok { requiredMap[v] = struct{}{} diff --git a/lib/batches/env/env.go b/lib/batches/env/env.go deleted file mode 100644 index 89e60619f1..0000000000 --- a/lib/batches/env/env.go +++ /dev/null @@ -1,170 +0,0 @@ -// 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 deleted file mode 100644 index 1e63c00662..0000000000 --- a/lib/batches/env/var.go +++ /dev/null @@ -1,99 +0,0 @@ -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 index f77d5ab92d..72ae441c92 100644 --- a/lib/batches/execution/cache/cache.go +++ b/lib/batches/execution/cache/cache.go @@ -9,6 +9,7 @@ import ( "sort" "time" + executortypes "github.com/sourcegraph/sourcegraph/internal/executor/types" "github.com/sourcegraph/sourcegraph/lib/batches" "github.com/sourcegraph/sourcegraph/lib/batches/execution" "github.com/sourcegraph/sourcegraph/lib/batches/template" @@ -49,11 +50,11 @@ func (key CacheKey) mountsMetadata() ([]MountMetadata, error) { // perRunEnvVars resolve to per-job or per-executor values that change on // every dequeue and must be stripped from cache keys. Mirrored in -// sourcegraph/sourcegraph/lib/batches/execution/cache/cache.go. +// src-cli/lib/batches/execution/cache/cache.go. var perRunEnvVars = []string{ - "SRC_EXECUTOR_JOB_TOKEN", - "SRC_EXECUTOR_JOB_ID", - "SRC_EXECUTOR_NAME", + executortypes.JobTokenEnvVar, + executortypes.JobIDEnvVar, + executortypes.ExecutorNameEnvVar, } // resolveStepsEnvironment returns a slice of environments for each of the steps, diff --git a/lib/batches/execution/cache/cache_test.go b/lib/batches/execution/cache/cache_test.go deleted file mode 100644 index c416fe024f..0000000000 --- a/lib/batches/execution/cache/cache_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package cache - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/sourcegraph/sourcegraph/lib/batches" - "github.com/sourcegraph/sourcegraph/lib/batches/env" -) - -func TestKeyer_Key_PerRunEnvVarsIgnored(t *testing.T) { - var stepEnv env.Environment - require.NoError(t, json.Unmarshal( - []byte(`["SRC_EXECUTOR_JOB_TOKEN", "SRC_EXECUTOR_JOB_ID", "SRC_EXECUTOR_NAME"]`), - &stepEnv, - )) - step := batches.Step{Run: "foo", Env: stepEnv} - repo := batches.Repository{ID: "r", Name: "r"} - - unset, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0}).Key() - require.NoError(t, err) - resolved, err := (&CacheKey{Repository: repo, Steps: []batches.Step{step}, StepIndex: 0, GlobalEnv: []string{ - "SRC_EXECUTOR_JOB_TOKEN=tok", - "SRC_EXECUTOR_JOB_ID=42", - "SRC_EXECUTOR_NAME=executor-abc", - }}).Key() - require.NoError(t, err) - require.Equal(t, unset, resolved) -} diff --git a/lib/batches/json/validate.go b/lib/batches/json/validate.go deleted file mode 100644 index 9e38b6f108..0000000000 --- a/lib/batches/json/validate.go +++ /dev/null @@ -1,24 +0,0 @@ -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 index 03477d038c..36070c1746 100644 --- a/lib/batches/json_logs.go +++ b/lib/batches/json_logs.go @@ -32,58 +32,67 @@ func (l *LogEvent) UnmarshalJSON(data []byte) error { l.Timestamp = j.Timestamp l.Status = j.Status - switch l.Operation { + metadata, err := NewLogEventMetadata(l.Operation) + if err != nil { + return err + } + l.Metadata = metadata + + wrapper := struct { + Metadata any `json:"metadata"` + }{ + Metadata: l.Metadata, + } + + return json.Unmarshal(data, &wrapper) +} + +// NewLogEventMetadata returns an empty metadata value for the given operation. +func NewLogEventMetadata(operation LogEventOperation) (any, error) { + switch operation { case LogEventOperationParsingBatchSpec: - l.Metadata = new(ParsingBatchSpecMetadata) + return new(ParsingBatchSpecMetadata), nil case LogEventOperationResolvingNamespace: - l.Metadata = new(ResolvingNamespaceMetadata) + return new(ResolvingNamespaceMetadata), nil case LogEventOperationPreparingDockerImages: - l.Metadata = new(PreparingDockerImagesMetadata) + return new(PreparingDockerImagesMetadata), nil case LogEventOperationDeterminingWorkspaceType: - l.Metadata = new(DeterminingWorkspaceTypeMetadata) + return new(DeterminingWorkspaceTypeMetadata), nil case LogEventOperationDeterminingWorkspaces: - l.Metadata = new(DeterminingWorkspacesMetadata) + return new(DeterminingWorkspacesMetadata), nil case LogEventOperationCheckingCache: - l.Metadata = new(CheckingCacheMetadata) + return new(CheckingCacheMetadata), nil case LogEventOperationExecutingTasks: - l.Metadata = new(ExecutingTasksMetadata) + return new(ExecutingTasksMetadata), nil case LogEventOperationLogFileKept: - l.Metadata = new(LogFileKeptMetadata) + return new(LogFileKeptMetadata), nil case LogEventOperationUploadingChangesetSpecs: - l.Metadata = new(UploadingChangesetSpecsMetadata) + return new(UploadingChangesetSpecsMetadata), nil case LogEventOperationCreatingBatchSpec: - l.Metadata = new(CreatingBatchSpecMetadata) + return new(CreatingBatchSpecMetadata), nil case LogEventOperationApplyingBatchSpec: - l.Metadata = new(ApplyingBatchSpecMetadata) + return new(ApplyingBatchSpecMetadata), nil case LogEventOperationBatchSpecExecution: - l.Metadata = new(BatchSpecExecutionMetadata) + return new(BatchSpecExecutionMetadata), nil case LogEventOperationExecutingTask: - l.Metadata = new(ExecutingTaskMetadata) + return new(ExecutingTaskMetadata), nil case LogEventOperationTaskBuildChangesetSpecs: - l.Metadata = new(TaskBuildChangesetSpecsMetadata) + return new(TaskBuildChangesetSpecsMetadata), nil case LogEventOperationTaskSkippingSteps: - l.Metadata = new(TaskSkippingStepsMetadata) + return new(TaskSkippingStepsMetadata), nil case LogEventOperationTaskStepSkipped: - l.Metadata = new(TaskStepSkippedMetadata) + return new(TaskStepSkippedMetadata), nil case LogEventOperationTaskPreparingStep: - l.Metadata = new(TaskPreparingStepMetadata) + return new(TaskPreparingStepMetadata), nil case LogEventOperationTaskStep: - l.Metadata = new(TaskStepMetadata) + return new(TaskStepMetadata), nil case LogEventOperationCacheAfterStepResult: - l.Metadata = new(CacheAfterStepResultMetadata) + return new(CacheAfterStepResultMetadata), nil case LogEventOperationDockerWatchDog: - l.Metadata = new(DockerWatchDogMetadata) + return new(DockerWatchDogMetadata), nil default: - return errors.Newf("invalid event type %s", l.Operation) - } - - wrapper := struct { - Metadata any `json:"metadata"` - }{ - Metadata: l.Metadata, + return nil, errors.Newf("invalid event type %s", errors.Safe(operation)) } - - return json.Unmarshal(data, &wrapper) } type LogEventOperation string diff --git a/lib/batches/jsonschema/jsonschema.go b/lib/batches/jsonschema/jsonschema.go deleted file mode 100644 index 28d1c10dea..0000000000 --- a/lib/batches/jsonschema/jsonschema.go +++ /dev/null @@ -1,36 +0,0 @@ -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/schema/batch_spec_stringdata.go b/lib/batches/schema/batch_spec_stringdata.go index 8215beae93..2c13f28061 100644 --- a/lib/batches/schema/batch_spec_stringdata.go +++ b/lib/batches/schema/batch_spec_stringdata.go @@ -18,12 +18,44 @@ const BatchSpecJSON = `{ "properties": { "steps": { "items": { - "anyOf": [ + "oneOf": [ { "required": ["run", "image"] }, { "required": ["buildImage"] }, - { "required": ["codingAgent"] } + { "required": ["codingAgent", "image"] } ] } + }, + "changesetHooks": { + "properties": { + "onCIFailure": { + "properties": { + "steps": { + "items": { + "oneOf": [{ "required": ["run", "image"] }, { "required": ["codingAgent", "image"] }] + } + } + } + }, + "onMergeConflict": { + "properties": { + "steps": { + "items": { + "oneOf": [{ "required": ["run", "image"] }, { "required": ["codingAgent", "image"] }] + } + } + } + } + } + }, + "importChangesets": { + "not": {} + }, + "changesetTemplate": { + "properties": { + "published": { + "not": {} + } + } } } }, @@ -51,6 +83,20 @@ const BatchSpecJSON = `{ } } } + }, + { + "$comment": "The ` + "`" + `checkout` + "`" + ` property is only allowed when ` + "`" + `version: 3` + "`" + ` is set.", + "if": { + "required": ["checkout"] + }, + "then": { + "required": ["version"], + "properties": { + "version": { + "const": 3 + } + } + } } ], "definitions": { @@ -92,7 +138,7 @@ const BatchSpecJSON = `{ "CodingAgent": { "title": "CodingAgent", "type": "object", - "description": "An out-of-the-box coding agent step that runs the given prompt inside a managed container. Mutually exclusive with run. Only supported in version 3 batch specs. Use the step's top-level container/image field to override the default agent image.", + "description": "An out-of-the-box coding agent step that runs the given prompt inside the step's top-level image. One option for the image is the Sourcegraph-managed ` + "`" + `batches-coding-agent-base` + "`" + `. Mutually exclusive with run. Only supported in version 3 batch specs.", "additionalProperties": false, "required": ["type", "prompt"], "properties": { @@ -109,7 +155,7 @@ const BatchSpecJSON = `{ }, "BuildImage": { "type": "object", - "description": "A step that creates a local image by running a shell command in a base image using buildah. Later steps can use the generated image via ${{ outputs.imageName }}. The generated image name is deterministic for the exact baseImage and run values.", + "description": "A step that creates a local image by running a shell command in a base image using kaniko. Later steps can use the generated image via ${{ outputs.imageName }}. The generated image name is deterministic for the exact baseImage and run values.", "additionalProperties": false, "required": ["run", "baseImage"], "properties": { @@ -324,8 +370,60 @@ const BatchSpecJSON = `{ }, "maxAttempts": { "type": "integer", - "description": "The maximum number of times this step will be attempted before the hook action is considered failed. Defaults to 1 (no retries).", - "minimum": 1 + "description": "The maximum number of times this step will be attempted before the hook action is considered failed. Defaults to 1 (no retries). Must be between 1 and 10. Retries reuse the same working tree, so any file changes made by a failed attempt remain in place for subsequent attempts.", + "minimum": 1, + "maximum": 10, + "default": 1 + } + } + }, + "GitCommitAuthor": { + "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." + } + } + }, + "ExpandedGitCommitDescription": { + "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": { + "$ref": "#/definitions/GitCommitAuthor" + } + } + }, + "ChangesetHookCommitDescription": { + "title": "ChangesetHookCommitDescription", + "type": "object", + "description": "The Git commit to create for changes produced by a changeset hook's steps. Both fields are optional and are resolved independently.", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "message": { + "type": "string", + "description": "The Git commit message. If omitted, a per-event default message is used (e.g. \"Fix for CI failure on ${{ repository.branch }}\")." + }, + "author": { + "$ref": "#/definitions/GitCommitAuthor" } } }, @@ -342,6 +440,10 @@ const BatchSpecJSON = `{ "items": { "$ref": "#/definitions/HookStep" } + }, + "commit": { + "description": "The Git commit to create for changes produced by the hook's steps. The message and author are resolved independently: if message is omitted, a per-event default is used; if author is omitted, the changesetTemplate's author is inherited, falling back to the changeset's author.", + "$ref": "#/definitions/ChangesetHookCommitDescription" } } } @@ -455,6 +557,19 @@ const BatchSpecJSON = `{ } } }, + "checkout": { + "type": "object", + "description": "Options controlling how repositories are checked out for workspace execution. Only supported in version 3 batch specs.", + "additionalProperties": false, + "properties": { + "fetchDepth": { + "type": "integer", + "minimum": 0, + "default": 1, + "description": "The number of commits of git history to fetch into the workspace checkout. The default, 1, fetches only the target commit (a shallow clone). Set to 0 to fetch the full history, which is useful for history-related tasks (e.g. updating CODEOWNERS based on git history) or changeset hooks such as onMergeConflict." + } + } + }, "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.", @@ -550,35 +665,7 @@ const BatchSpecJSON = `{ "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." - } - } - } - } + "$ref": "#/definitions/ExpandedGitCommitDescription" }, "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.", @@ -633,7 +720,7 @@ const BatchSpecJSON = `{ "$ref": "#/definitions/ChangesetHookAction" }, "onMergeConflict": { - "description": "Action to run when the changeset's external mergeability transitions into ` + "`" + `conflicting` + "`" + ` for a given (base, head) SHA pair.", + "description": "Action to run when the changeset's external mergeability transitions into ` + "`" + `conflicting` + "`" + ` for a given head SHA.", "$ref": "#/definitions/ChangesetHookAction" } } diff --git a/lib/batches/schema/changeset_spec_stringdata.go b/lib/batches/schema/changeset_spec_stringdata.go deleted file mode 100644 index 9ec47ef532..0000000000 --- a/lib/batches/schema/changeset_spec_stringdata.go +++ /dev/null @@ -1,121 +0,0 @@ -// 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 index cdffbc96c6..e38c328e90 100644 --- a/lib/batches/template/partial_eval.go +++ b/lib/batches/template/partial_eval.go @@ -42,26 +42,6 @@ func IsStaticBool(input string, ctx *StepContext) (isStatic bool, boolVal bool, return true, isTrueOutput(t.Tree.Root), nil } -// IsStaticString parses the input as a text/template and attempts to evaluate it -// with only the information currently available in StepContext. If any template -// actions remain after partial evaluation, the first return value is false. -func IsStaticString(input string, ctx *StepContext) (isStatic bool, value string, err error) { - t, err := parseAndPartialEval(input, ctx) - if err != nil { - return false, "", err - } - - var out bytes.Buffer - for _, n := range t.Tree.Root.Nodes { - if n.Type() != parse.NodeText { - return false, "", nil - } - out.WriteString(n.String()) - } - - return true, out.String(), 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 diff --git a/lib/batches/template/templating.go b/lib/batches/template/templating.go index 2ff1d641d4..1845d603c1 100644 --- a/lib/batches/template/templating.go +++ b/lib/batches/template/templating.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "slices" "sort" "strings" "text/template" @@ -11,20 +12,192 @@ import ( "github.com/gobwas/glob" "github.com/grafana/regexp" + "github.com/kballard/go-shellquote" "github.com/sourcegraph/sourcegraph/lib/batches/execution" "github.com/sourcegraph/sourcegraph/lib/batches/git" "github.com/sourcegraph/sourcegraph/lib/errors" ) -const ( - startDelim = "${{" - endDelim = "}}" -) +const startDelim = "${{" +const endDelim = "}}" + +// shellEscapeAll returns a copy of in where every element has been quoted +// with shellquote.Join so that it is safe to splat into a /bin/sh command line. +// Elements without shell metacharacters are returned unmodified, so callers +// that pre-existed this escaping (e.g. `${{ join steps.modified_files " " }}` +// against safe filenames) continue to see the same rendered output. +// +// 🚨 SECURITY: This is used to defang attacker-controlled filenames coming +// out of `git diff` parsing before they reach the rendered step script. See +// VULN-91 / HackerOne report 3767160. +func shellEscapeAll(in ...string) []string { + out := make([]string, len(in)) + for i, s := range in { + out[i] = shellquote.Join(s) + } + return out +} +// isCanonicallyShellQuoted reports whether s is exactly what shellquote.Join +// would produce for some single-element argv: i.e. s parses as one shell +// word whose re-quoted canonical form is identical to s. +// +// This lets `shellquote_join` be idempotent — calling it on a value that +// src-cli already pre-escaped (e.g. each element of +// `previous_step.modified_files`) or on the output of a previous +// `shellquote_join` returns the value unchanged instead of double-escaping +// it into a literal filename that the shell would then look up verbatim. +// +// The check is content-based, so it works regardless of how the value +// reached the template (direct slice access, captured outputs, raw stdio, +// hand-built argv, etc.) — types do not have to survive the trip through +// `outputs` serialization and template rendering. +func isCanonicallyShellQuoted(s string) bool { + parts, err := shellquote.Split(s) + if err != nil || len(parts) != 1 { + return false + } + return shellquote.Join(parts[0]) == s +} + +// Builtin template functions available inside any batch spec template. +// +// Quick reference for what to use when a value flows into a /bin/sh command: +// +// - Splatting `repository.search_result_paths`, `*_files`, or any other +// filename slice that src-cli pre-escapes for you: use plain `join`. +// These slices already have every element wrapped with shellquote.Join +// so they are safe to splat as-is. Reaching for `shellquote_join` here +// is unnecessary but also harmless — see the idempotence note below. +// +// - Splatting a string or slice that you built yourself from raw stdio +// (`previous_step.stdout`), captured `outputs.*` values, search results +// fed back through outputs, or any other source where the bytes are not +// already canonical-quoted: use `shellquote_join`. +// +// - Re-parsing a shell-formatted string back into an argv: use +// `shellquote_split` (optionally piping back through `shellquote_join`). +// +// String helpers (all from the Go standard library): +// +// - join — strings.Join(elems, sep). Concatenates the elements of a +// slice with the given separator. Performs NO shell quoting +// itself, so it is only safe on slices whose elements are +// either already shell-quoted (the src-cli `*_files` and +// `repository.search_result_paths` fields) or trusted not +// to contain shell metacharacters. For anything else, +// reach for `shellquote_join`. +// - split — strings.Split(s, sep). Splits a string around every +// occurrence of sep into a slice. +// - replace — strings.ReplaceAll(s, old, new). Replaces every occurrence +// of old in s with new. +// - join_if — Joins elems with sep, skipping any empty strings. +// - matches — Reports whether the input matches the given glob pattern. +// +// Shell-quoting helpers (from github.com/kballard/go-shellquote): +// +// - shellquote_join — Joins its arguments into a single string with each +// element shell-quoted. Accepts either a single +// []string or any number of individual string args +// (or a mix of both), so it works equally well on +// slice variables and on a hand-built argv. Example: +// +// run: gofmt -w ${{ shellquote_join outputs.argv }} +// run: my-tool ${{ shellquote_join "--name" outputs.userName }} +// +// Element-level calls are idempotent: every string +// that is already in canonical shellquote form (each +// element of `*_files` and +// `repository.search_result_paths`, or anything you +// hand-quoted yourself) is passed through unchanged +// instead of being double-escaped. So if you do reach +// for `shellquote_join` on a pre-escaped slice you +// will get the same output as `join … " "`, not a +// corrupted second layer of quoting. +// +// Note: this idempotence is per-element. If you pass +// a SINGLE string that itself happens to look like a +// multi-word canonical argv (e.g. the output of a +// previous shellquote_join captured into outputs), +// it is treated as one shell word and re-quoted as a +// whole. Keep the slice shape if you intend to feed +// the result through shellquote_join a second time. +// +// - shellquote_split — shellquote.Split(input). The inverse of +// shellquote_join: parses a shell-quoted string back +// into the original slice of arguments, honouring +// quoting, escaping and backslash rules in the same +// way /bin/sh's word-splitting does. Useful when a +// previous step (or a user-provided output) hands you +// a single shell-formatted string that you need to +// iterate over or re-quote. Example: +// +// run: | +// for f in ${{ shellquote_join (shellquote_split outputs.fileList) }}; do +// process "$f" +// done +// +// Returns an error (which surfaces as a template +// execution failure) if the input contains an +// unterminated quote. var builtins = template.FuncMap{ - "join": strings.Join, - "split": strings.Split, - "replace": strings.ReplaceAll, + "join": strings.Join, + // shellquote_join accepts either a single []string or variadic string + // arguments and returns a single shell-quoted string. + // + // We expose it as `func(...any) (string, error)` rather than + // `shellquote.Join` directly for two reasons: + // + // 1. Go's text/template does NOT splat a []string into a variadic + // ...string parameter, so a bare shellquote.Join would reject the + // natural `${{ shellquote_join (shellquote_split foo) }}` round-trip + // and `${{ shellquote_join outputs.argv }}` (where argv is a slice). + // + // 2. Elements that are already in canonical shellquote form are passed + // through unchanged so the function is idempotent. This prevents the + // common footgun of calling `shellquote_join` on a value that + // src-cli already pre-escaped (every element of `*_files` and + // `repository.search_result_paths`, or the result of a previous + // `shellquote_join`) and getting a literal-quoted filename that the + // shell would then look up verbatim. See isCanonicallyShellQuoted. + "shellquote_join": func(args ...any) (string, error) { + var flat []string + appendOne := func(s string) { + if isCanonicallyShellQuoted(s) { + flat = append(flat, s) + return + } + flat = append(flat, shellquote.Join(s)) + } + for _, a := range args { + switch v := a.(type) { + case string: + appendOne(v) + case []string: + for _, s := range v { + appendOne(s) + } + case []any: + for i, e := range v { + s, ok := e.(string) + if !ok { + return "", errors.Newf("shellquote_join: element %d is %T, want string", i, e) + } + appendOne(s) + } + default: + return "", errors.Newf("shellquote_join: unsupported argument type %T", a) + } + } + // flat is already per-element canonical-quoted, so a plain + // strings.Join(" ") would suffice. We use shellquote.Join's + // space-separated concatenation for symmetry and to keep the public + // contract "this returns a canonical shellquote.Join string". + return strings.Join(flat, " "), nil + }, + "shellquote_split": shellquote.Split, + "split": strings.Split, + "replace": strings.ReplaceAll, "join_if": func(sep string, elems ...string) string { var nonBlank []string for _, e := range elems { @@ -76,6 +249,7 @@ func ValidateBatchSpecTemplate(spec string) (bool, error) { // 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. @@ -164,9 +338,22 @@ type Repository struct { FileMatches []string } +// SearchResultPaths returns the repository's matched paths in a form that is +// safe to splat into a /bin/sh command line. +// +// 🚨 SECURITY: paths originate from a Sourcegraph search and ultimately from +// `git`, so they may contain attacker-controlled shell metacharacters (see +// VULN-91). Every element is run through shellquote.Join before it is exposed +// to the step template. Elements without metacharacters are returned +// unmodified, so existing usage like `${{ join repository.search_result_paths +// " " }}` keeps producing the same output for benign filenames. func (r Repository) SearchResultPaths() (list fileMatchPathList) { - sort.Strings(r.FileMatches) - return r.FileMatches + paths := slices.Clone(r.FileMatches) + sort.Strings(paths) + for i, p := range paths { + paths[i] = shellquote.Join(p) + } + return paths } type fileMatchPathList []string @@ -209,10 +396,24 @@ func (stepCtx *StepContext) ToFuncMap() template.FuncMap { 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 + // 🚨 SECURITY: file lists are derived from `git diff` output and can + // contain attacker-controlled filenames with shell metacharacters. + // We shell-escape each element before exposing them to the step + // template to prevent command injection when the rendered template + // is executed by /bin/sh. The slice shape is preserved so + // `${{ join … " " }}` and `${{ range … }}` continue to work as + // before. See VULN-91. + // + // NOTE: stdout/stderr are intentionally NOT pre-escaped. They are + // commonly captured into `outputs` and reused as plain values (e.g. + // a filename written by `echo`), where pre-quoting would change + // semantics. Users that splat stdout/stderr into a shell command + // against untrusted data should pipe through the `shellquote_join` + // builtin: `${{ shellquote_join previous_step.stdout }}`. + m["modified_files"] = shellEscapeAll(res.ChangedFiles.Modified...) + m["added_files"] = shellEscapeAll(res.ChangedFiles.Added...) + m["deleted_files"] = shellEscapeAll(res.ChangedFiles.Deleted...) + m["renamed_files"] = shellEscapeAll(res.ChangedFiles.Renamed...) m["stdout"] = res.Stdout m["stderr"] = res.Stderr @@ -296,11 +497,13 @@ func (tmplCtx *ChangesetTemplateContext) ToFuncMap() template.FuncMap { return tmplCtx.Outputs }, "steps": func() map[string]any { + // 🚨 SECURITY: shell-escape per element to defang attacker + // controlled filenames from `git diff`. See VULN-91. 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, + "modified_files": shellEscapeAll(tmplCtx.Steps.Changes.Modified...), + "added_files": shellEscapeAll(tmplCtx.Steps.Changes.Added...), + "deleted_files": shellEscapeAll(tmplCtx.Steps.Changes.Deleted...), + "renamed_files": shellEscapeAll(tmplCtx.Steps.Changes.Renamed...), "path": tmplCtx.Steps.Path, } }, diff --git a/lib/batches/yaml/validate.go b/lib/batches/yaml/validate.go deleted file mode 100644 index 9d901abed2..0000000000 --- a/lib/batches/yaml/validate.go +++ /dev/null @@ -1,33 +0,0 @@ -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/request_logger.go b/lib/codeintel/upload/request_logger.go index 8df7e559c0..00faaa6aa6 100644 --- a/lib/codeintel/upload/request_logger.go +++ b/lib/codeintel/upload/request_logger.go @@ -25,14 +25,6 @@ const ( 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 diff --git a/lib/docgen/default.go b/lib/docgen/default.go deleted file mode 100644 index de71edeb9d..0000000000 --- a/lib/docgen/default.go +++ /dev/null @@ -1,19 +0,0 @@ -package docgen - -import ( - "bytes" - - "github.com/urfave/cli/v3" -) - -// Default renders help text for the app using urfave/cli's default help format. -func Default(cmd *cli.Command) (string, error) { - tpl := cmd.CustomRootCommandHelpTemplate - if tpl == "" { - tpl = cli.RootCommandHelpTemplate - } - - var w bytes.Buffer - cli.HelpPrinterCustom(&w, tpl, cmd, nil) - return w.String(), nil -} diff --git a/lib/docgen/markdown.go b/lib/docgen/markdown.go deleted file mode 100644 index 1b499ce452..0000000000 --- a/lib/docgen/markdown.go +++ /dev/null @@ -1,270 +0,0 @@ -package docgen - -import ( - "bytes" - "cmp" - "fmt" - "io" - "path" - "slices" - "sort" - "strings" - "text/template" - - "github.com/urfave/cli/v3" - - "github.com/sourcegraph/sourcegraph/lib/errors" -) - -type MarkdownFile struct { - Name string - Content string -} - -// Markdown renders a Markdown reference for the app. -// -// It is adapted from https://sourcegraph.com/github.com/urfave/cli-docs/-/blob/docs.go?L16 -func Markdown(root *cli.Command) ([]MarkdownFile, error) { - files := make([]MarkdownFile, 0, len(root.Commands)) - var errs error - for _, sub := range VisibleCommands(root.Commands) { - subFiles, err := markdownFiles(root.Name, []string{sub.Name}, sub) - if err != nil { - errs = errors.Append(errs, err) - } - files = append(files, subFiles...) - } - return files, errs -} - -type cliTemplate struct { - Title string - Usage string - Description string - UsageText string - Flags []flagRow - Subcommands []subcommand -} - -type subcommand struct { - Name string - Link string -} - -type flagRow struct { - Name string - Desc string - Default string -} - -// markdownFiles recursively walks over cmd commands and sub commands to build a list of MarkdownFiles -// that contain the name and content for a command -func markdownFiles(rootName string, lineage []string, cmd *cli.Command) ([]MarkdownFile, error) { - var w bytes.Buffer - err := writeDocTemplate(rootName, lineage, cmd, &w) - - files := []MarkdownFile{{ - Name: docPath(lineage, hasVisibleCommands(cmd.Commands)), - Content: w.String(), - }} - - for _, sub := range VisibleCommands(cmd.Commands) { - subFiles, subErr := markdownFiles(rootName, append(lineage, sub.Name), sub) - if subErr != nil { - err = errors.Append(err, subErr) - } - files = append(files, subFiles...) - } - - return files, err -} - -func writeDocTemplate(rootName string, lineage []string, cmd *cli.Command, w io.Writer) error { - const name = "cli" - t, err := template.New(name).Parse(markdownDocTemplate) - if err != nil { - return err - } - - title := strings.Join(append([]string{rootName}, lineage...), " ") - return t.ExecuteTemplate(w, name, &cliTemplate{ - Title: title, - Usage: prepareUsage(cmd), - Description: strings.TrimSpace(cmd.Description), - UsageText: prepareUsageText(title, cmd), - Flags: prepareArgsWithValues(cmd.Flags), - Subcommands: prepareSubcommands(cmd.Commands), - }) -} - -func prepareSubcommands(commands []*cli.Command) []subcommand { - links := make([]subcommand, 0, len(commands)) - for _, command := range VisibleCommands(commands) { - links = append(links, subcommand{ - Name: command.Name, - Link: SubcommandDocPath(command), - }) - } - return links -} - -func prepareArgsWithValues(flags []cli.Flag) []flagRow { - return prepareFlags(flags) -} - -func prepareFlags( - flags []cli.Flag, -) []flagRow { - rows := []flagRow{} - for _, f := range flags { - flag, ok := f.(cli.DocGenerationFlag) - if !ok { - continue - } - names := make([]string, 0, len(f.Names())) - for _, s := range f.Names() { - trimmed := strings.TrimSpace(s) - if trimmed == "" { - continue - } - if len(trimmed) > 1 { - names = append(names, fmt.Sprintf("--%s", trimmed)) - } else { - names = append(names, fmt.Sprintf("-%s", trimmed)) - } - } - - name := strings.Join(names, ", ") - if len(name) > 0 { - rows = append(rows, flagRow{ - Name: name, - Desc: flag.GetUsage(), - Default: flag.GetValue(), - }) - } - - } - slices.SortFunc(rows, func(a, b flagRow) int { - return cmp.Compare(a.Name, b.Name) - }) - return rows -} - -func prepareUsageText(lineage string, command *cli.Command) string { - if command.UsageText == "" { - if hasVisibleCommands(command.Commands) { - return renderUsageBlock(lineage + " [command options]") - } - if len(command.Flags) > 0 { - return renderUsageBlock(lineage + " [options]") - } - if strings.TrimSpace(command.ArgsUsage) != "" { - return fmt.Sprintf("Arguments: `%s`\n", command.ArgsUsage) - } - return "" - } - - // Write all usage examples as a big shell code block. - lines := make([]string, 0, strings.Count(command.UsageText, "\n")+1) - for line := range strings.SplitSeq(strings.TrimSpace(command.UsageText), "\n") { - line = strings.TrimSpace(line) - if len(line) == 0 { - continue - } - lines = append(lines, line) - } - return renderUsageBlock(lines...) -} - -func renderUsageBlock(lines ...string) string { - var usageText strings.Builder - usageText.WriteString("```sh") - for _, line := range lines { - usageText.WriteByte('\n') - - if strings.HasPrefix(line, "# ") { - usageText.WriteString(line) - } else if len(line) > 0 { - fmt.Fprintf(&usageText, "$ %s", line) - } - } - usageText.WriteString("\n```\n") - - return usageText.String() -} - -func prepareUsage(command *cli.Command) string { - if command.Usage == "" { - return "" - } - - return command.Usage + "." -} - -// VisibleCommands returns the non-hidden commands sorted by name. -func VisibleCommands(commands []*cli.Command) []*cli.Command { - visible := make([]*cli.Command, 0, len(commands)) - for _, command := range commands { - if command.Hidden { - continue - } - visible = append(visible, command) - } - - sort.Slice(visible, func(i, j int) bool { - return visible[i].Name < visible[j].Name - }) - - return visible -} - -// SubcommandDocPath returns the relative doc path for a direct child command. -func SubcommandDocPath(command *cli.Command) string { - return docPath([]string{command.Name}, hasVisibleCommands(command.Commands)) -} - -func hasVisibleCommands(commands []*cli.Command) bool { - for _, command := range commands { - if !command.Hidden { - return true - } - } - return false -} - -func docPath(lineage []string, isGroup bool) string { - if len(lineage) == 0 { - return "index.md" - } - if isGroup { - return path.Join(path.Join(lineage...), "index.md") - } - if len(lineage) == 1 { - return lineage[0] + ".md" - } - return path.Join(path.Join(lineage[:len(lineage)-1]...), lineage[len(lineage)-1]+".md") -} - -var markdownDocTemplate = `# ` + "`" + `{{ .Title }}` + "`" + ` - -{{ if .Usage }}{{ .Usage }} - -{{ end }}{{ if .Description }}{{ .Description }} -{{- end }} - -{{ if .UsageText }}## Usage - -{{ .UsageText }} -{{- end }} -{{ if .Flags }}## Flags - -| Name | Description | Default Value | -|------|-------------|---------------| -{{- range .Flags -}} -{{- "\n" -}} -| ` + "`" + `{{ .Name }}` + "`" + ` | {{ .Desc }} | ` + "`" + `{{ .Default }}` + "`" + `| -{{- end }}{{- end }} -{{- if .Subcommands }}## Subcommands - -{{ range $v := .Subcommands }}* [` + "`" + `{{ $v.Name }}` + "`" + `]({{ $v.Link }}) -{{ end }}{{ end }}` diff --git a/lib/errors/postgres.go b/lib/errors/postgres.go deleted file mode 100644 index c9a7827c96..0000000000 --- a/lib/errors/postgres.go +++ /dev/null @@ -1,12 +0,0 @@ -package errors - -import ( - "github.com/jackc/pgx/v5/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/go.mod b/lib/go.mod index 650889c7b3..636ed028f7 100644 --- a/lib/go.mod +++ b/lib/go.mod @@ -1,88 +1,3 @@ module github.com/sourcegraph/sourcegraph/lib go 1.26.4 - -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/pgx/v5 v5.9.2 - 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/scip-code/scip/bindings/go/scip v0.7.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/stretchr/testify v1.11.1 - github.com/urfave/cli/v3 v3.8.0 - github.com/xeipuuv/gojsonschema v1.2.0 - github.com/xlab/treeprint v1.2.0 - go.opentelemetry.io/otel v1.43.0 - golang.org/x/sys v0.41.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/alecthomas/chroma/v2 v2.14.0 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/benbjohnson/clock v1.3.5 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // 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/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/getsentry/sentry-go v0.27.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/css v1.0.1 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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.14 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a // 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.uber.org/atomic v1.11.0 // indirect - go.uber.org/goleak v1.3.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.37.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -// See: https://github.com/ghodss/yaml/pull/65 -replace github.com/ghodss/yaml => github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152 diff --git a/lib/go.sum b/lib/go.sum deleted file mode 100644 index 7a5acf0f7e..0000000000 --- a/lib/go.sum +++ /dev/null @@ -1,237 +0,0 @@ -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/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -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/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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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/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/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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -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-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/go-cmp v0.5.2/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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.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.0.3 h1:zyrfTlNfyxLpX/zuk8wjTeTYP5AXaFeeRYFEZfHPwao= -github.com/hexops/autogold/v2 v2.0.3/go.mod h1:cYVc0tJn6v9Uf9xMOHvmH6scuTxsVJSxGcKR/yOVPzY= -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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= -github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -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/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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -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/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/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/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= -github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= -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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/scip-code/scip/bindings/go/scip v0.7.0 h1:on80ynl7eSovvMUy3uxWkJm+kepsVjIwI2byIeMZego= -github.com/scip-code/scip/bindings/go/scip v0.7.0/go.mod h1:XC4dP3um0lKaSDRFIMwYlkBjFxKy1Rw7xFt6KDtRjAk= -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/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/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -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/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= -github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= -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/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -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.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -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/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -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/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/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-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= -mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= -mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= -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/progress.go b/lib/output/progress.go index 6490666656..61de540c98 100644 --- a/lib/output/progress.go +++ b/lib/output/progress.go @@ -42,12 +42,6 @@ type ProgressOpts struct { 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 { diff --git a/lib/output/progress_tty.go b/lib/output/progress_tty.go index 0f7421d1e9..18522596f7 100644 --- a/lib/output/progress_tty.go +++ b/lib/output/progress_tty.go @@ -10,7 +10,7 @@ import ( ) var DefaultProgressTTYOpts = &ProgressOpts{ - SuccessEmoji: "\u2705", + SuccessEmoji: EmojiSuccess, SuccessStyle: StyleSuccess, PendingStyle: StylePending, } diff --git a/lib/output/status_bar.go b/lib/output/status_bar.go index e43868fc86..95f6ebe103 100644 --- a/lib/output/status_bar.go +++ b/lib/output/status_bar.go @@ -64,5 +64,3 @@ func (sb *StatusBar) runtime() time.Duration { 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 index f27a6d6530..c07c5c837c 100644 --- a/lib/output/style.go +++ b/lib/output/style.go @@ -42,29 +42,15 @@ var ( 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) + StyleSearchQuery = Fg256Color(68) + StyleSearchLink = Fg256Color(237) + StyleSearchMatch = CombineStyles(Fg256Color(0), Bg256Color(11)) 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) + StyleSearchAlertTitle = Fg256Color(124) // Colors StyleGrey = Fg256Color(8)