diff --git a/changelog.md b/changelog.md index 34aeb6166b..da34c115e2 100644 --- a/changelog.md +++ b/changelog.md @@ -26,16 +26,26 @@ and this project adheres to it to a meta file to edit and publish later with `--meta`, instead of only cancelling. - When `state publish --build` cannot determine the ingredient name or version, the error now says which is missing, why `pyproject.toml` did not provide it (for example, a version listed in `[project].dynamic`), and how to set it. +- Without `--namespace`, `state publish --build` now defaults to `private//python` when the source has a + `pyproject.toml` (otherwise `private/`, as before). +- The namespace of a new ingredient can now be changed in the `state publish` editor. The new namespace is checked like + one passed with `--namespace`; if it can't be used, the previous one is kept and the reason is shown. The confirmation + and the editor now say up front which fields can't be changed and why, and when one of them is edited anyway (the + name or version with `--build`, or the namespace, name or version with `--edit`), the error names it. +- The ingredient meta data `state publish` shows for confirmation and opens in the editor no longer has a help comment + on each field. The fields are described in the publish guide instead. ### Fixed - `state publish --edit` no longer overwrites the meta file's dependencies and authors with the previous revision's, - keeps inherited dependency types and conditions, and rejects author or description changes on an existing version - up front. + keeps inherited dependency types and conditions, and rejects author changes on an existing version up front. - `state publish --edit --meta` with an exported meta file now edits the version named in the file rather than the latest version. - `state export ingredient-meta` now writes plain YAML without help comments or line wrapping, so its output can be passed back with `--meta` as is. +- `state export ingredient-meta` now includes the ingredient's description instead of leaving it empty. When + `state publish --edit` is given a different description for an existing version, it no longer refuses to publish: + the confirmation says the description can't be changed, shows the current one, and the change is ignored. - `state publish` of a new ingredient without a namespace (such as with `--editor` outside a project) no longer searches every namespace for its name, which could time out with "Could not search for ingredient" before the editor opened. - `state publish --editor` no longer fails with "Could not publish ingredient. Error received: " when no editor diff --git a/go.mod b/go.mod index f1db3695bc..5f71898ed4 100644 --- a/go.mod +++ b/go.mod @@ -76,7 +76,6 @@ require ( github.com/gowebpki/jcs v1.0.1 github.com/mark3labs/mcp-go v0.33.0 github.com/mholt/archives v0.1.5 - github.com/zijiren233/yaml-comment v0.2.1 ) require ( @@ -118,7 +117,6 @@ require ( github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/pty v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/maruel/natural v1.1.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect diff --git a/go.sum b/go.sum index 4497782e6a..20b3ce1428 100644 --- a/go.sum +++ b/go.sum @@ -520,8 +520,6 @@ github.com/mark3labs/mcp-go v0.33.0 h1:naxhjnTIs/tyPZmWUZFuG0lDmdA6sUyYGGf3gsHvT github.com/mark3labs/mcp-go v0.33.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ= -github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ= github.com/mash/go-tempfile-suffix v0.0.0-20150731093933-48f0f8a3a5ab h1:2lWb5W+cZTlTIrJUuzVMrs6vcEvY0ydkkMH66EdPaqc= github.com/mash/go-tempfile-suffix v0.0.0-20150731093933-48f0f8a3a5ab/go.mod h1:oBJAJTenVXse2frk3J4anZ7funxla2ZdJHFQShkD43k= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= @@ -745,8 +743,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zijiren233/yaml-comment v0.2.1 h1:/ymMfauuR6zPme+c59FvGNmvxmjOS+BRZSU9YEM82g4= -github.com/zijiren233/yaml-comment v0.2.1/go.mod h1:Cps/tIAJVNoG6TpNN1Aaht/O1QGthZzOgUMQXXTxlBs= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= diff --git a/internal/runners/export/ingredientmeta.go b/internal/runners/export/ingredientmeta.go index 7213ab0bb1..8f1db78d82 100644 --- a/internal/runners/export/ingredientmeta.go +++ b/internal/runners/export/ingredientmeta.go @@ -8,7 +8,6 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/runners/publish" "github.com/ActiveState/cli/pkg/platform/api/graphql/request" - "gopkg.in/yaml.v3" ) type IngredientMetaParams struct { @@ -37,11 +36,10 @@ func (i *IngredientMeta) Run(params *IngredientMetaParams) error { return writeIngredientMeta(i.prime.Output().Config().OutWriter, meta) } -// writeIngredientMeta writes meta in the format --meta reads. It is plain yaml rather than meta.MarshalYaml, whose field -// help comments would end up in the file, and written as is: the outputer's Print would word-wrap long lines and -// interpret [TAGS]. +// writeIngredientMeta writes meta in the format --meta reads. It is written as is: the outputer's Print would word-wrap +// long lines and interpret [TAGS]. func writeIngredientMeta(w io.Writer, meta *request.PublishVariables) error { - b, err := yaml.Marshal(meta) + b, err := meta.MarshalYaml(false) if err != nil { return errs.Wrap(err, "Could not marshal ingredient meta data") } diff --git a/internal/runners/publish/confirm_test.go b/internal/runners/publish/confirm_test.go index c6d097fa8d..42fdea8c53 100644 --- a/internal/runners/publish/confirm_test.go +++ b/internal/runners/publish/confirm_test.go @@ -93,7 +93,7 @@ func TestConfirmRequestFillsVersionRequirements(t *testing.T) { r := &Runner{out: outputhelper.NewCatcher(), prompt: p} reqVars := newRequest() - publish, err := r.confirmRequest(reqVars, &Params{}, false) + publish, err := r.confirmRequest(reqVars, &Params{}, false, "") if err != nil || !publish { t.Fatalf("publish = %v, err = %v", publish, err) } @@ -104,6 +104,9 @@ func TestConfirmRequestFillsVersionRequirements(t *testing.T) { if !strings.Contains(p.messages[0], "versionRequirements: '>=0'") { t.Errorf("confirmation does not show the filled in requirements:\n%s", p.messages[0]) } + if strings.Contains(p.messages[0], "#") { + t.Errorf("confirmation shows comments:\n%s", p.messages[0]) + } } func TestOpenInEditorWithoutEditor(t *testing.T) { @@ -130,7 +133,7 @@ func TestConfirmRequestEditor(t *testing.T) { r := &Runner{out: outputhelper.NewCatcher(), prompt: p} reqVars := newRequest() - publish, err := r.confirmRequest(reqVars, &Params{}, false) + publish, err := r.confirmRequest(reqVars, &Params{}, false, "") if err != nil || !publish { t.Fatalf("publish = %v, err = %v", publish, err) } @@ -139,25 +142,110 @@ func TestConfirmRequestEditor(t *testing.T) { } }) - t.Run("cleared namespace is rejected", func(t *testing.T) { - useTestEditor(t, "name: mypkg\nversion: 1.0.0\n") + t.Run("namespace change is kept", func(t *testing.T) { + useTestEditor(t, "name: mypkg\nnamespace: private/org/other\nversion: 1.0.0\n") + p := &fakePrompter{confirms: []bool{false, true}, selects: []int{choiceEdit}} + r := &Runner{out: outputhelper.NewCatcher(), prompt: p, versionExists: noVersions} + reqVars := newRequest() + + publish, err := r.confirmRequest(reqVars, &Params{}, false, "") + if err != nil || !publish { + t.Fatalf("publish = %v, err = %v", publish, err) + } + if reqVars.Namespace != "private/org/other" { + t.Errorf("namespace = %q, want the edited one", reqVars.Namespace) + } + }) + + for name, tt := range map[string]struct { + namespace string + params Params + exists bool + reason string + }{ + "cleared namespace is put back": {"", Params{}, false, "namespace is required"}, + "namespace with the version is put back": {"private/org/other", Params{}, true, "already exists"}, + "namespace outside the org is put back": {"private/other/python", Params{Build: "src"}, false, "requires a namespace under"}, + } { + t.Run(name, func(t *testing.T) { + useTestEditor(t, "name: mypkg\nnamespace: "+tt.namespace+"\nversion: 1.0.0\n") + out := outputhelper.NewCatcher() + p := &fakePrompter{confirms: []bool{false, true}, selects: []int{choiceEdit}} + r := &Runner{out: out, prompt: p, versionExists: func(_, _, _ string) (bool, error) { return tt.exists, nil }} + reqVars := newRequest() + + publish, err := r.confirmRequest(reqVars, &tt.params, false, "") + if err != nil || !publish { + t.Fatalf("publish = %v, err = %v", publish, err) + } + if reqVars.Namespace != "private/org/python" { + t.Errorf("namespace = %q, want the original one put back", reqVars.Namespace) + } + if !strings.Contains(out.CombinedOutput(), tt.reason) { + t.Errorf("output does not explain why (%q):\n%s", tt.reason, out.CombinedOutput()) + } + }) + } + + t.Run("name change with --build is saved instead", func(t *testing.T) { + t.Chdir(t.TempDir()) + useTestEditor(t, "name: otherpkg\nnamespace: private/org/python\nversion: 1.0.0\n") p := &fakePrompter{confirms: []bool{false}, selects: []int{choiceEdit}} r := &Runner{out: outputhelper.NewCatcher(), prompt: p} - _, err := r.confirmRequest(newRequest(), &Params{}, false) - if err == nil || !strings.Contains(err.Error(), "namespace") { - t.Errorf("err = %v, want the namespace error", err) + _, err := r.confirmRequest(newRequest(), &Params{Build: "src"}, false, "") + if err == nil || !strings.Contains(err.Error(), "The name can't be changed") { + t.Fatalf("err = %v, want the name named as fixed", err) + } + if !strings.Contains(p.messages[0], "name and version are built into the package") { + t.Errorf("confirmation does not say up front what is fixed:\n%s", p.messages[0]) + } + if _, err := os.Stat("otherpkg-1.0.0.meta.yaml"); err != nil { + t.Errorf("edits were not saved: %v", err) } }) - t.Run("description added on a revision is rejected", func(t *testing.T) { - useTestEditor(t, "name: mypkg\nnamespace: private/org/python\nversion: 1.0.0\ndescription: changed\n") + t.Run("namespace change with --edit is saved instead", func(t *testing.T) { + t.Chdir(t.TempDir()) + useTestEditor(t, "name: mypkg\nnamespace: private/org/other\nversion: 1.0.0\n") p := &fakePrompter{confirms: []bool{false}, selects: []int{choiceEdit}} r := &Runner{out: outputhelper.NewCatcher(), prompt: p} - _, err := r.confirmRequest(newRequest(), &Params{Edit: true}, true) - if err == nil || !strings.Contains(err.Error(), "cannot be changed") { - t.Errorf("err = %v, want the identity error", err) + _, err := r.confirmRequest(newRequest(), &Params{Edit: true}, true, "") + if err == nil || !strings.Contains(err.Error(), "The namespace can't be changed") { + t.Errorf("err = %v, want the namespace named as fixed", err) + } + }) + + t.Run("changed description on a revision is ignored", func(t *testing.T) { + useTestEditor(t, "name: mypkg\nnamespace: private/org/python\nversion: 1.0.0\ndescription: changed\n") + p := &fakePrompter{confirms: []bool{false, true}, selects: []int{choiceEdit}} + r := &Runner{out: outputhelper.NewCatcher(), prompt: p} + reqVars := newRequest() + + publish, err := r.confirmRequest(reqVars, &Params{Edit: true}, true, "current") + if err != nil || !publish { + t.Fatalf("publish = %v, err = %v, want it still published", publish, err) + } + if reqVars.Description != "current" { + t.Errorf("description = %q, want the current one kept", reqVars.Description) + } + if !strings.Contains(p.messages[1], "changed[/RESET]' is ignored and it stays '[ACTIONABLE]current") { + t.Errorf("confirmation does not say the change is ignored:\n%s", p.messages[1]) + } + }) + + t.Run("unchanged description on a revision is accepted", func(t *testing.T) { + useTestEditor(t, "name: mypkg\nnamespace: private/org/python\nversion: 1.0.0\ndescription: current\n") + p := &fakePrompter{confirms: []bool{false, true}, selects: []int{choiceEdit}} + r := &Runner{out: outputhelper.NewCatcher(), prompt: p} + + publish, err := r.confirmRequest(newRequest(), &Params{Edit: true}, true, "current") + if err != nil || !publish { + t.Errorf("publish = %v, err = %v, want the unchanged description accepted", publish, err) + } + if strings.Contains(p.messages[1], "is ignored") { + t.Errorf("confirmation notes an ignored description that wasn't changed:\n%s", p.messages[1]) } }) @@ -167,7 +255,7 @@ func TestConfirmRequestEditor(t *testing.T) { r := &Runner{out: outputhelper.NewCatcher(), prompt: p} reqVars := newRequest() - if _, err := r.confirmRequest(reqVars, &Params{Build: "src"}, false); err != nil { + if _, err := r.confirmRequest(reqVars, &Params{Build: "src"}, false, ""); err != nil { t.Fatal(err) } if len(reqVars.Dependencies) != 1 || reqVars.Dependencies[0].Name != privateBuilderName { @@ -176,12 +264,31 @@ func TestConfirmRequestEditor(t *testing.T) { }) } +// noVersions is a versionExists for a platform where nothing is published yet. +func noVersions(_, _, _ string) (bool, error) { return false, nil } + +func TestDefaultNamespace(t *testing.T) { + withPyproject := t.TempDir() + if err := os.WriteFile(filepath.Join(withPyproject, "pyproject.toml"), nil, 0644); err != nil { + t.Fatal(err) + } + for _, tt := range []struct{ buildDir, want string }{ + {"", "private/org"}, + {t.TempDir(), "private/org"}, + {withPyproject, "private/org/python"}, + } { + if got := defaultNamespace("org", tt.buildDir); got != tt.want { + t.Errorf("defaultNamespace(%q) = %q, want %q", tt.buildDir, got, tt.want) + } + } +} + func TestConfirmRequestSave(t *testing.T) { t.Chdir(t.TempDir()) p := &fakePrompter{confirms: []bool{false}, selects: []int{choiceSave}} r := &Runner{out: outputhelper.NewCatcher(), prompt: p} - publish, err := r.confirmRequest(newRequest(), &Params{Build: "src"}, false) + publish, err := r.confirmRequest(newRequest(), &Params{Build: "src"}, false, "") if err != nil || publish { t.Fatalf("publish = %v, err = %v, want saved", publish, err) } @@ -207,7 +314,7 @@ func TestConfirmRequestCancel(t *testing.T) { p := &fakePrompter{confirms: []bool{false}, selects: []int{choiceCancel}} r := &Runner{out: outputhelper.NewCatcher(), prompt: p} - if _, err := r.confirmRequest(newRequest(), &Params{}, false); err == nil || !strings.Contains(err.Error(), "cancelled") { + if _, err := r.confirmRequest(newRequest(), &Params{}, false, ""); err == nil || !strings.Contains(err.Error(), "cancelled") { t.Errorf("err = %v, want cancelled", err) } } @@ -222,7 +329,7 @@ func TestPublishFailedError(t *testing.T) { func TestMetaFromIngredient(t *testing.T) { ingredient := &ParentIngredient{Version: "1.0.0", Dependencies: windowsOnlyDependency()} - meta, err := metaFromIngredient("private/org/python", "mypkg", ingredient) + meta, err := metaFromIngredient("private/org/python", "mypkg", ingredient, "Internal helpers") if err != nil { t.Fatal(err) } @@ -232,6 +339,9 @@ func TestMetaFromIngredient(t *testing.T) { if got := meta.Dependencies[0].Conditions[0].VersionRequirements; got != ">=0" { t.Errorf("condition without requirements exported as %q, want >=0", got) } + if meta.Description != "Internal helpers" { + t.Errorf("description exported as %q", meta.Description) + } } // windowsOnlyDependency is a revision's dependency on requests, conditioned on Windows without a version requirement. diff --git a/internal/runners/publish/meta.go b/internal/runners/publish/meta.go index da43b54d5d..2d831779ef 100644 --- a/internal/runners/publish/meta.go +++ b/internal/runners/publish/meta.go @@ -5,16 +5,18 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/pkg/platform/api/graphql/request" "github.com/ActiveState/cli/pkg/platform/api/inventory/inventory_client/inventory_operations" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" + "github.com/go-openapi/strfmt" ) // FetchMeta returns the meta file contents of an existing ingredient version (the latest one when version is empty), // in the format the --meta flag accepts. Its dependencies are the complete set, so the file can be modified and -// passed back with --edit to replace them. Authors and description are left out because the platform rejects them -// on a revision of an existing version. +// passed back with --edit to replace them. Authors are left out because the platform rejects them on a revision of an +// existing version; the description is included, and --edit accepts it as long as it is unchanged. func FetchMeta(namespace, name, version string, auth *authentication.Auth) (*request.PublishVariables, error) { latestRevisionTime, err := model.FetchLatestRevisionTimeStamp(auth) if err != nil { @@ -48,13 +50,26 @@ func FetchMeta(namespace, name, version string, auth *authentication.Auth) (*req ingredient = &ParentIngredient{*i.IngredientID, *i.IngredientVersionID, *i.Version, i.Dependencies} } - return metaFromIngredient(namespace, name, ingredient) + description, err := fetchDescription(ingredient.IngredientID, auth) + if err != nil { + return nil, err + } + return metaFromIngredient(namespace, name, ingredient, description) +} + +// fetchDescription returns an ingredient's description, which belongs to the ingredient rather than to its versions. +func fetchDescription(ingredientID strfmt.UUID, auth *authentication.Auth) (string, error) { + ingredient, err := model.FetchIngredient(&ingredientID, auth) + if err != nil { + return "", locale.WrapError(err, "err_ingredient_meta_fetch", "Could not fetch the ingredient's description") + } + return ptr.From(ingredient.Description, ""), nil } // metaFromIngredient builds the meta file contents of an existing ingredient version, complete enough to be published // back as is: dependencies and conditions without a version requirement get the any-version one. -func metaFromIngredient(namespace, name string, ingredient *ParentIngredient) (*request.PublishVariables, error) { - meta := &request.PublishVariables{Name: name, Namespace: namespace} +func metaFromIngredient(namespace, name string, ingredient *ParentIngredient, description string) (*request.PublishVariables, error) { + meta := &request.PublishVariables{Name: name, Namespace: namespace, Description: description} // As a revision, prepareEditRequest doesn't fetch authors, so it needs no auth. if err := prepareEditRequest(ingredient, meta, true, nil); err != nil { return nil, errs.Wrap(err, "Could not read ingredient meta data") diff --git a/internal/runners/publish/publish.go b/internal/runners/publish/publish.go index 12c5ce6990..f13fd25055 100644 --- a/internal/runners/publish/publish.go +++ b/internal/runners/publish/publish.go @@ -58,6 +58,8 @@ type Runner struct { project *project.Project cfg *config.Instance bp *buildplanner.BuildPlanner + // versionExists reports whether a version of an ingredient is published; tests replace it. + versionExists func(namespace, name, version string) (bool, error) } type primeable interface { @@ -70,7 +72,7 @@ type primeable interface { } func New(prime primeable) *Runner { - return &Runner{ + r := &Runner{ auth: prime.Auth(), out: prime.Output(), prompt: prime.Prompt(), @@ -78,6 +80,8 @@ func New(prime primeable) *Runner { cfg: prime.Config(), bp: buildplanner.NewBuildPlannerModel(prime.Auth(), prime.SvcModel()), } + r.versionExists = r.ingredientVersionExists + return r } type ParentIngredient struct { @@ -144,7 +148,7 @@ func (r *Runner) Run(params *Params) error { if params.Namespace != "" { reqVars.Namespace = params.Namespace } else if reqVars.Namespace == "" && r.project != nil && r.project.Owner() != "" { - reqVars.Namespace = model.NewNamespaceOrg(r.project.Owner(), "").String() + reqVars.Namespace = defaultNamespace(r.project.Owner(), params.Build) } // A --build publish is encrypted under the project org's key, so its resolved @@ -213,6 +217,8 @@ func (r *Runner) Run(params *Params) error { } } + // The description of the ingredient a revision belongs to, which the revision can't change. + var currentDescription string if params.Edit { if ingredient == nil { return locale.NewInputError("err_uploadingredient_edit_not_found", @@ -222,6 +228,11 @@ func (r *Runner) Run(params *Params) error { if err := prepareEditRequest(ingredient, &reqVars, isRevision, r.auth); err != nil { return errs.Wrap(err, "Could not prepare edit request") } + if isRevision { + if currentDescription, err = fetchDescription(ingredient.IngredientID, r.auth); err != nil { + return err + } + } } else { if isRevision { return locale.NewInputError("err_uploadingredient_exists", @@ -252,13 +263,16 @@ func (r *Runner) Run(params *Params) error { } } - publish, err := r.confirmRequest(&reqVars, params, isRevision) + publish, err := r.confirmRequest(&reqVars, params, isRevision, currentDescription) if err != nil { return err } if !publish { return nil // saved to a meta file instead } + if params.Edit && isRevision { + reqVars.Description = "" // kept as it was, and the platform rejects a description on a revision + } r.out.Notice(locale.Tl("uploadingredient_uploading", "Publishing ingredient...")) @@ -317,16 +331,56 @@ func (r *Runner) Run(params *Params) error { var versionRegexp = regexp.MustCompile(`\d+\.\d+(\.\d+)?`) -// checkEditKeepsIdentity rejects author and description changes on a revision of an existing version, which the -// platform does not allow; it would otherwise fail the publish with an unclear API error. +// defaultNamespace is the namespace used when neither --namespace nor the meta file gives one: the owner's private +// namespace, under python when a pyproject.toml in the --build directory makes the package a Python one. +func defaultNamespace(owner, buildDir string) string { + if buildDir != "" && fileutils.FileExists(filepath.Join(buildDir, "pyproject.toml")) { + return model.NewNamespaceOrg(owner, "python").String() + } + return model.NewNamespaceOrg(owner, "").String() +} + +// ingredientVersionExists reports whether the given version of an ingredient is published. +func (r *Runner) ingredientVersionExists(namespace, name, version string) (bool, error) { + latestRevisionTime, err := model.FetchLatestRevisionTimeStamp(r.auth) + if err != nil { + return false, errs.Wrap(err, "Unable to determine latest revision time") + } + _, err = model.GetIngredientByNameAndVersion(namespace, name, version, &latestRevisionTime, r.auth) + var notFound *inventory_operations.GetNamespaceIngredientVersionNotFound + if errors.As(err, ¬Found) { + return false, nil + } + if err != nil { + return false, errs.Wrap(err, "could not get ingredient version") + } + return true, nil +} + +// checkEditKeepsIdentity rejects authors on a revision of an existing version, which the platform does not allow; it +// would otherwise fail the publish with an unclear API error. func checkEditKeepsIdentity(r *request.PublishVariables, edit, isRevision bool) error { - if edit && isRevision && (len(r.Authors) > 0 || r.Description != "") { + if edit && isRevision && len(r.Authors) > 0 { return locale.NewInputError("err_uploadingredient_edit_identity", - "Authors and description cannot be changed when editing an existing version. Remove them from the meta file and flags.") + "Authors cannot be changed when editing an existing version. Remove them from the meta file and flags.") } return nil } +// keepDescription shows a revision with its ingredient's current description, which can't be changed on an existing +// version. A different description is replaced rather than refused, and the returned note, ending in a newline, tells +// the user it is ignored; it is empty when there was nothing to ignore. +func keepDescription(r *request.PublishVariables, currentDescription string) string { + ignored := r.Description + r.Description = currentDescription + if ignored == "" || ignored == currentDescription { + return "" + } + return locale.Tl("uploadingredient_description_ignored", + "The description can't be changed on an existing version, so '[ACTIONABLE]{{.V0}}[/RESET]' is ignored and it stays '[ACTIONABLE]{{.V1}}[/RESET]'.", + ignored, currentDescription) + "\n" +} + func prepareRequestFromParams(r *request.PublishVariables, params *Params, isRevision bool) error { if params.Version != "" { r.Version = params.Version @@ -474,7 +528,7 @@ func prepareEditRequest(ingredient *ParentIngredient, r *request.PublishVariable // confirmRequest shows the prepared request and asks the user to publish it. Until they confirm, they can edit it or // save it to a meta file and start over with it. Each pass completes and validates the request again, since an edit // can undo what was done to it before. It reports whether to publish: false means the request was saved instead. -func (r *Runner) confirmRequest(reqVars *request.PublishVariables, params *Params, isRevision bool) (bool, error) { +func (r *Runner) confirmRequest(reqVars *request.PublishVariables, params *Params, isRevision bool, currentDescription string) (bool, error) { for { if params.Build != "" { addPrivateBuilderDependency(reqVars) @@ -486,6 +540,10 @@ func (r *Runner) confirmRequest(reqVars *request.PublishVariables, params *Param if err := checkEditKeepsIdentity(reqVars, params.Edit, isRevision); err != nil { return false, err } + var descriptionNote string + if params.Edit && isRevision { + descriptionNote = keepDescription(reqVars, currentDescription) + } b, err := reqVars.MarshalYaml(false) if err != nil { @@ -497,9 +555,9 @@ func (r *Runner) confirmRequest(reqVars *request.PublishVariables, params *Param locale.Tl("uploadingredient_confirm", `Prepared the following ingredient: {{.V0}} - +{{.V1}} Do you want to publish this ingredient? Answer 'n' to edit it or save it to a file instead. -`, string(b)), ptr.To(true), nil) +`, string(b), fixedFieldsNote(params)+descriptionNote), ptr.To(true), nil) if err != nil { return false, errs.Wrap(err, "Not confirmed") } @@ -555,18 +613,44 @@ func (r *Runner) reviseRequest(reqVars *request.PublishVariables, params *Params return false, locale.NewInputError("uploadingredient_cancel", "Publish cancelled") } -// editRequest opens the request in the user's editor. With --build or --edit, the name, namespace and version were -// already used to build the package or find the ingredient, so changing them here would publish something -// inconsistent: the edits are saved to a meta file instead, and the user is told to start over with it. +// editRequest opens the request in the user's editor. A new namespace is checked again and put back if it can't be +// used. Other fields that were already used can't be changed here: --build packed the name and version into the +// package, and --edit found the ingredient to revise by its namespace, name and version. Changing them would publish +// something inconsistent, so the edits are saved to a meta file instead, and the user is told to start over with it. func (r *Runner) editRequest(reqVars *request.PublishVariables, params *Params) error { + if note := fixedFieldsNote(params); note != "" { + r.out.Notice(note + locale.Tl("uploadingredient_edit_fixed_saved", + "If you change them in the editor, your edits are saved to a meta file to publish from instead.")) + } + before := *reqVars if err := r.OpenInEditor(reqVars); err != nil { return err } + + if !params.Edit && reqVars.Namespace != before.Namespace { + if err := r.checkNamespaceEdit(reqVars, params); err != nil { + r.out.Notice(locale.Tl("uploadingredient_namespace_kept", + "Kept the namespace '[ACTIONABLE]{{.V0}}[/RESET]', because '[ACTIONABLE]{{.V1}}[/RESET]' can't be used: {{.V2}}", + before.Namespace, reqVars.Namespace, errs.JoinMessage(err))) + reqVars.Namespace = before.Namespace + } + } + if params.Build == "" && !params.Edit { return nil } - if reqVars.Name == before.Name && reqVars.Namespace == before.Namespace && reqVars.Version == before.Version { + var changed []string + if params.Edit && reqVars.Namespace != before.Namespace { + changed = append(changed, "namespace") + } + if reqVars.Name != before.Name { + changed = append(changed, "name") + } + if reqVars.Version != before.Version { + changed = append(changed, "version") + } + if len(changed) == 0 { return nil } @@ -574,10 +658,54 @@ func (r *Runner) editRequest(reqVars *request.PublishVariables, params *Params) if err != nil { return err } + reason := locale.Tl("uploadingredient_fixed_reason_build", "it was already built into the package") + if params.Edit { + reason = locale.Tl("uploadingredient_fixed_reason_edit", "it identifies the ingredient you are editing") + } return locale.NewInputError("err_uploadingredient_edit_identity_changed", - "The name, namespace or version can't be changed at this point, because they were already used to build the package "+ - "or find the ingredient. Your changes were saved to [ACTIONABLE]{{.V0}}[/RESET]. Publish them with:\n\n [ACTIONABLE]{{.V1}}[/RESET]", - path, rerunCommand(params, path)) + "The {{.V0}} can't be changed at this point, because {{.V1}}. Your changes were saved to [ACTIONABLE]{{.V2}}[/RESET]. "+ + "Publish them with:\n\n [ACTIONABLE]{{.V3}}[/RESET]", + strings.Join(changed, " and "), reason, path, rerunCommand(params, path)) +} + +// fixedFieldsNote tells the user up front which fields can't be changed before publishing, and why, ending in a +// newline; it is empty when every field can be. +func fixedFieldsNote(params *Params) string { + switch { + case params.Edit: + return locale.Tl("uploadingredient_fixed_edit", + "The namespace, name and version identify the ingredient you are editing, so they can't be changed.") + "\n" + case params.Build != "": + return locale.Tl("uploadingredient_fixed_build", + "The name and version are built into the package, so they can't be changed. To change them, publish again with '[ACTIONABLE]--name[/RESET]' or '[ACTIONABLE]--version[/RESET]'.") + "\n" + } + return "" +} + +// checkNamespaceEdit checks a namespace changed in the editor the way Run checked the original one: a --build package +// must stay in the organization whose key encrypted it, and a new ingredient's version must not already exist in it. +func (r *Runner) checkNamespaceEdit(reqVars *request.PublishVariables, params *Params) error { + if reqVars.Namespace == "" { + return locale.NewInputError("err_uploadingredient_namespace_empty", "a namespace is required") + } + if params.Build != "" { + owner := "" + if r.project != nil { + owner = r.project.Owner() + } + if err := requireOrgNamespace(reqVars.Namespace, owner); err != nil { + return err + } + } + exists, err := r.versionExists(reqVars.Namespace, reqVars.Name, reqVars.Version) + if err != nil { + return err + } + if exists { + return locale.NewInputError("err_uploadingredient_namespace_version_exists", + "version {{.V0}} of '{{.V1}}' already exists in it", reqVars.Version, reqVars.Name) + } + return nil } // saveMetaFile writes the request to -.meta.yaml in the working directory, in the format --meta reads, @@ -598,8 +726,7 @@ func (r *Runner) saveMetaFile(reqVars *request.PublishVariables) (string, error) } } - // Plain yaml rather than MarshalYaml, whose field help comments don't belong in a file the user keeps. - b, err := yaml.Marshal(reqVars) + b, err := reqVars.MarshalYaml(false) if err != nil { return "", errs.Wrap(err, "Could not marshal publish variables") } diff --git a/internal/runners/publish/publish_test.go b/internal/runners/publish/publish_test.go index f188293fae..ee3f7deb5c 100644 --- a/internal/runners/publish/publish_test.go +++ b/internal/runners/publish/publish_test.go @@ -94,10 +94,9 @@ func TestCheckEditKeepsIdentity(t *testing.T) { wantErr bool }{ {"author on revision", withAuthor, true, true, true}, - {"description on revision", withDescription, true, true, true}, + {"description on revision is left to keepDescription", withDescription, true, true, false}, {"no identity change on revision", plain, true, true, false}, {"author on new version via edit", withAuthor, true, false, false}, - {"description on new ingredient", withDescription, false, false, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/runners/publish/readme.md b/internal/runners/publish/readme.md index 6b6e0e97b4..ed336d0e23 100644 --- a/internal/runners/publish/readme.md +++ b/internal/runners/publish/readme.md @@ -78,7 +78,8 @@ From the project checkout: state publish --build ./mypkg-src --namespace private//python ``` -The namespace must be under `private/`, the organization that owns the project. Before uploading, the State Tool +The namespace must be under `private/`, the organization that owns the project. Without `--namespace`, it is +`private//python` when the source directory has a `pyproject.toml`, and `private/` otherwise. Before uploading, the State Tool shows the ingredient it is about to publish and asks for confirmation: ``` @@ -112,6 +113,9 @@ dependencies: namespace: builder versionRequirements: '>=0' +The name and version are built into the package, so they can't be changed. To change them, publish again with +'--name' or '--version'. + Do you want to publish this ingredient? Answer 'n' to edit it or save it to a file instead. (Y/n) ``` @@ -121,8 +125,10 @@ it is what decrypts and installs the package, so leave it in place. If something is wrong, answer `n` and choose one of: - **Edit it in my editor:** opens the meta data in `$EDITOR` (or the default program for `.yaml` files), then shows - the result for confirmation again. Name, namespace and version can't be changed this way, because the package was - already built with them: if you change them, your edits are saved to a meta file as below. + the result for confirmation again. You can change the namespace here: it is checked the same way as `--namespace`, + and if it can't be used (it is outside your organization, or the version already exists there), the previous one is + kept and the reason is shown. The name and version can't be changed this way, because the package was already built + with them: if you change them, your edits are saved to a meta file as below. - **Save it to a meta file to edit and publish later:** writes `mypkg-1.0.0.meta.yaml` to the current directory and prints the command to publish it, e.g. `state publish --build ./mypkg-src --meta mypkg-1.0.0.meta.yaml`. Edit the file, then run that command. The meta file takes precedence over `pyproject.toml`, so its name, version and dependency list @@ -165,7 +171,7 @@ complete dependency list: name: mypkg namespace: private//python version: 1.0.0 -description: "" +description: Internal helpers for the data team dependencies: - dependency: name: private-builder @@ -182,6 +188,18 @@ dependencies: ... ``` +The meta file has these fields: + +| Field | Meaning | +|----------------|------------------------------------------------------------------------------------------------------| +| `name` | The name of the ingredient. | +| `namespace` | Where the ingredient is published, under `private/`, e.g. `private//python`. | +| `version` | The version, following semantic versioning. It must match the version in the source file name, if any. | +| `description` | A short description of the ingredient. It can't be changed on an existing version. | +| `authors` | Optional. The people who contributed to it, each with `name`, `email` and `websites`. They can't be changed on an existing version. | +| `dependencies` | Optional. The ingredients it requires, described below. | +| `features` | Optional. The features it provides, each with `name`, `namespace` and `version`. | + **Edit the `dependencies` list.** Change version ranges, remove entries, or add new ones. Each entry nests its fields under `dependency:`, with optional `conditions:` beside it: @@ -215,9 +233,10 @@ Things to know about `--edit`: `pyproject.toml` lists any, the previous revision's dependencies are kept. - **Name, namespace and version identify what you are editing; they cannot be changed.** To publish under a new version, see step 6. -- **Authors and description cannot be changed** on an existing version. The exported file leaves authors out and the - description empty. If you fill the description in, the publish is refused with "Authors and description cannot be - changed when editing an existing version." +- **Authors and description cannot be changed** on an existing version. The exported file leaves authors out, and + adding any is refused with "Authors cannot be changed when editing an existing version." It includes the current + description. If you change it, the change is ignored: the confirmation shows the current description and says so, + and you can still publish the rest of your edits. - The `language/python` dependencies the platform adds by itself come back automatically, so it does no harm if they are missing from your file. diff --git a/pkg/platform/api/graphql/request/publish.go b/pkg/platform/api/graphql/request/publish.go index 45b7313dd2..6a1cb6a81c 100644 --- a/pkg/platform/api/graphql/request/publish.go +++ b/pkg/platform/api/graphql/request/publish.go @@ -11,7 +11,6 @@ import ( "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/gqlclient" "github.com/ActiveState/cli/internal/locale" - yamlcomment "github.com/zijiren233/yaml-comment" "gopkg.in/yaml.v3" ) @@ -54,15 +53,15 @@ func Publish(vars PublishVariables, filepath string) (*PublishInput, error) { // which is done with yaml. As such the yaml tags are used for representing data to the user, and the json is used for // inputs to graphql. type PublishVariables struct { - Name string `yaml:"name" json:"-" hc:"The name of the ingredient"` // User representation only - Namespace string `yaml:"namespace" json:"-" hc:"The namespace field should be in the format org/folder. Org can simply be your username or any organization you're a member of."` // User representation only - Version string `yaml:"version" json:"version" hc:"The version field should follow semantic versioning and match the version in the filename (if any)."` - Description string `yaml:"description" json:"description" hc:"The description field should be a short description of the ingredient."` + Name string `yaml:"name" json:"-"` // User representation only + Namespace string `yaml:"namespace" json:"-"` // User representation only + Version string `yaml:"version" json:"version"` + Description string `yaml:"description" json:"description"` // Optional - Authors []PublishVariableAuthor `yaml:"authors,omitempty" json:"authors,omitempty" hc:"A list of authors who contributed to the ingredient."` - Dependencies []PublishVariableDep `yaml:"dependencies,omitempty" json:"dependencies,omitempty" hc:"A list of dependencies that the ingredient requires."` - Features []PublishVariableFeature `yaml:"features,omitempty" json:"features,omitempty" hc:"A list of features that the ingredient provides."` + Authors []PublishVariableAuthor `yaml:"authors,omitempty" json:"authors,omitempty"` + Dependencies []PublishVariableDep `yaml:"dependencies,omitempty" json:"dependencies,omitempty"` + Features []PublishVariableFeature `yaml:"features,omitempty" json:"features,omitempty"` // GraphQL input only Path string `yaml:"-" json:"path"` @@ -123,14 +122,14 @@ func (p PublishVariables) MarshalJSON() ([]byte, error) { } func (p PublishVariables) MarshalYaml(includeExample bool) ([]byte, error) { - v, err := yamlcomment.Marshal(p) + v, err := yaml.Marshal(p) if err != nil { return nil, errs.Wrap(err, "Could not marshal publish request") } if includeExample { if len(p.Authors) == 0 { - exampleAuthorYaml, err := yamlcomment.Marshal(exampleAuthor) + exampleAuthorYaml, err := yaml.Marshal(exampleAuthor) if err != nil { return nil, errs.Wrap(err, "Could not marshal example author") } @@ -140,7 +139,7 @@ func (p PublishVariables) MarshalYaml(includeExample bool) ([]byte, error) { } if len(p.Dependencies) == 0 { - exampleDepYaml, err := yamlcomment.Marshal(exampleDep) + exampleDepYaml, err := yaml.Marshal(exampleDep) if err != nil { return nil, errs.Wrap(err, "Could not marshal example deps") } diff --git a/vendor/github.com/maruel/natural/LICENSE b/vendor/github.com/maruel/natural/LICENSE deleted file mode 100644 index 8e539f6924..0000000000 --- a/vendor/github.com/maruel/natural/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2018 Marc-Antoine Ruel - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/vendor/github.com/maruel/natural/README.md b/vendor/github.com/maruel/natural/README.md deleted file mode 100644 index 7d1a18a264..0000000000 --- a/vendor/github.com/maruel/natural/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# natural - -Yet another natural sort, with 100% test coverage and a benchmark. It **does not -allocate memory**, doesn't depend on package `sort` and hence doesn't depend on -`reflect`. It is optimized for speed. - -[![Go -Reference](https://pkg.go.dev/badge/github.com/maruel/natural.svg)](https://pkg.go.dev/github.com/maruel/natural) -[![codecov](https://codecov.io/gh/maruel/natural/branch/main/graph/badge.svg?token=iQg8Y62BBg)](https://codecov.io/gh/maruel/natural) - - -## Benchmarks - -On Go 1.18.3. - -``` -$ go test -bench=. -cpu 1 -goos: linux -goarch: amd64 -pkg: github.com/maruel/natural -cpu: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz -BenchmarkLessDigitsTwoGroupsNative 331287298 3.597 ns/op 0 B/op 0 allocs/op -BenchmarkLessDigitsTwoGroups 32479050 36.55 ns/op 0 B/op 0 allocs/op -BenchmarkLessStringOnly 157775884 7.603 ns/op 0 B/op 0 allocs/op -BenchmarkLessDigitsOnly 69210796 17.52 ns/op 0 B/op 0 allocs/op -BenchmarkLess10Blocks 6331066 190.8 ns/op 0 B/op 0 allocs/op -``` - -On a Raspberry Pi 3: - -``` -$ go test -bench=. -cpu 1 -goos: linux -goarch: arm -pkg: github.com/maruel/natural -BenchmarkLessDigitsTwoGroupsNative 14181789 86.57 ns/op 0 B/op 0 allocs/op -BenchmarkLessDigitsTwoGroups 1600195 748.9 ns/op 0 B/op 0 allocs/op -BenchmarkLessStringOnly 8286034 142.3 ns/op 0 B/op 0 allocs/op -BenchmarkLessDigitsOnly 3653055 331.4 ns/op 0 B/op 0 allocs/op -BenchmarkLess10Blocks 310687 3838 ns/op 0 B/op 0 allocs/op -``` - -Coverage: - -``` -$ go test -cover -PASS -coverage: 100.0% of statements -ok github.com/maruel/natural 0.012s -``` diff --git a/vendor/github.com/maruel/natural/natsort.go b/vendor/github.com/maruel/natural/natsort.go deleted file mode 100644 index cf743a76d2..0000000000 --- a/vendor/github.com/maruel/natural/natsort.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2018 Marc-Antoine Ruel. All rights reserved. -// Use of this source code is governed under the Apache License, Version 2.0 -// that can be found in the LICENSE file. - -// Package natural defines a natural "less" to compare two strings while -// interpreting natural numbers. -// -// This is occasionally nicknamed 'natsort'. -// -// It does so with no memory allocation. -package natural - -import ( - "strconv" -) - -// Less does a 'natural' comparison on the two strings. -// -// It treats digits as decimal numbers, so that Less("10", "2") return true. -// -// This function does no memory allocation. -func Less(a, b string) bool { - for { - if p := commonPrefix(a, b); p != 0 { - a = a[p:] - b = b[p:] - } - if len(a) == 0 { - return len(b) != 0 - } - if ia := digits(a); ia > 0 { - if ib := digits(b); ib > 0 { - // Both sides have digits. - an, aerr := strconv.ParseUint(a[:ia], 10, 64) - bn, berr := strconv.ParseUint(b[:ib], 10, 64) - if aerr == nil && berr == nil { - if an != bn { - return an < bn - } - // Semantically the same digits, e.g. "00" == "0", "01" == "1". In - // this case, only continue processing if there's trailing data on - // both sides, otherwise do lexical comparison. - if ia != len(a) && ib != len(b) { - a = a[ia:] - b = b[ib:] - continue - } - } - } - } - return a < b - } -} - -// StringSlice attaches the methods of Interface to []string, sorting in -// increasing order using natural order. -type StringSlice []string - -func (p StringSlice) Len() int { return len(p) } -func (p StringSlice) Less(i, j int) bool { return Less(p[i], p[j]) } -func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } - -// - -// commonPrefix returns the common prefix except for digits. -func commonPrefix(a, b string) int { - m := len(a) - if n := len(b); n < m { - m = n - } - if m == 0 { - return 0 - } - _ = a[m-1] - _ = b[m-1] - for i := 0; i < m; i++ { - ca := a[i] - cb := b[i] - if (ca >= '0' && ca <= '9') || (cb >= '0' && cb <= '9') || ca != cb { - return i - } - } - return m -} - -func digits(s string) int { - for i := 0; i < len(s); i++ { - c := s[i] - if c < '0' || c > '9' { - return i - } - } - return len(s) -} diff --git a/vendor/github.com/zijiren233/yaml-comment/.gitignore b/vendor/github.com/zijiren233/yaml-comment/.gitignore deleted file mode 100644 index 3b735ec4a8..0000000000 --- a/vendor/github.com/zijiren233/yaml-comment/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work diff --git a/vendor/github.com/zijiren233/yaml-comment/LICENSE b/vendor/github.com/zijiren233/yaml-comment/LICENSE deleted file mode 100644 index dbf44df17a..0000000000 --- a/vendor/github.com/zijiren233/yaml-comment/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 zijiren - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/zijiren233/yaml-comment/README.md b/vendor/github.com/zijiren233/yaml-comment/README.md deleted file mode 100644 index 27426ac6a7..0000000000 --- a/vendor/github.com/zijiren233/yaml-comment/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# yaml-comment -golang yaml comment encoder - -# How to use -```go -package main - -import ( - "fmt" - "github.com/zijiren233/yaml-comment" -) - -type Config struct { - Name string `yaml:"name,omitempty" hc:"this is head comment"` - Age int `yaml:"age,omitempty" lc:"this is line comment" fc:"this is foot comment"` -} - -func main() { - c := Config{ - Name: "comment", - Age: 18, - } - data, err := yamlcomment.Marshal(c) - if err != nil { - panic(err) - } - fmt.Println(string(data)) -} -``` - -# Output -```yaml -# this is head comment -name: comment -age: 18 # this is line comment -# this is foot comment -``` \ No newline at end of file diff --git a/vendor/github.com/zijiren233/yaml-comment/comment.go b/vendor/github.com/zijiren233/yaml-comment/comment.go deleted file mode 100644 index 19acc35a72..0000000000 --- a/vendor/github.com/zijiren233/yaml-comment/comment.go +++ /dev/null @@ -1,275 +0,0 @@ -package yamlcomment - -import ( - "reflect" - "sort" - "strings" - - "github.com/maruel/natural" - yaml "gopkg.in/yaml.v3" -) - -const ( - HeadCommentTag = "hc" - LineCommentTag = "lc" - FootCommentTag = "fc" - OmitemptyTag = "omitempty" - InlineTag = "inline" - FlowTag = "flow" -) - -type option struct { - fieldName string - omitempty bool - skip bool - inline bool - flow bool -} - -type comment struct { - HeadComment string - LineComment string - FootComment string -} - -func setComment(cm *comment, tag reflect.StructTag) { - if cm == nil { - return - } - cm.HeadComment = tag.Get(HeadCommentTag) - cm.LineComment = tag.Get(LineCommentTag) - cm.FootComment = tag.Get(FootCommentTag) -} - -func newComment(tag reflect.StructTag) *comment { - cm := new(comment) - setComment(cm, tag) - return cm -} - -type CommentEncoder struct { - encoder *yaml.Encoder -} - -func NewEncoder(encoder *yaml.Encoder) *CommentEncoder { - return &CommentEncoder{ - encoder: encoder, - } -} - -func (e *CommentEncoder) Encode(v any) error { - node, err := anyToYamlNode(v, false) - if err != nil { - return err - } - return e.encoder.Encode(node) -} - -func Marshal(v any) ([]byte, error) { - node, err := anyToYamlNode(v, false) - if err != nil { - return nil, err - } - return yaml.Marshal(node) -} - -func isZero(value reflect.Value) bool { - if !value.IsValid() { - return true - } - - return value.IsZero() -} - -func isNil(value reflect.Value) bool { - if !value.IsValid() { - return true - } - - switch value.Kind() { - case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice: - return value.IsNil() - default: - return false - } -} - -func parseTags(tag reflect.StructTag) (*option, *comment) { - yamlTags := strings.Split(tag.Get("yaml"), ",") - - var op = &option{ - fieldName: yamlTags[0], - } - - if op.fieldName == "-" { - op.skip = true - return op, nil - } - - for _, part := range yamlTags[1:] { - switch part { - case OmitemptyTag: - op.omitempty = true - case InlineTag: - op.inline = true - case FlowTag: - op.flow = true - } - } - - return op, newComment(tag) -} - -func AnyToYamlNode(model any) (*yaml.Node, error) { - return anyToYamlNode(model, false) -} - -func anyToYamlNode(model any, skip bool) (*yaml.Node, error) { - if n, ok := model.(*yaml.Node); ok { - return n, nil - } - - if m, ok := model.(yaml.Marshaler); ok && !isNil(reflect.ValueOf(model)) { - res, err := m.MarshalYAML() - if err != nil { - return nil, err - } - - if n, ok := res.(*yaml.Node); ok { - return n, nil - } - - model = res - } - - v := reflect.ValueOf(model) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - - node := new(yaml.Node) - - switch v.Kind() { - case reflect.Struct: - node.Kind = yaml.MappingNode - - t := v.Type() - for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - if !field.CanInterface() { - continue - } - - op, cm := parseTags(t.Field(i).Tag) - - if op.skip || (op.omitempty && isZero(field)) { - continue - } - - if op.fieldName == "" { - op.fieldName = strings.ToLower(t.Field(i).Name) - } - - var value any - if field.CanInterface() { - value = field.Interface() - } - - var style yaml.Style - if op.flow { - style |= yaml.FlowStyle - } - - if op.inline { - child, err := anyToYamlNode(value, skip) - if err != nil { - return nil, err - } - - if child.Kind == yaml.MappingNode || child.Kind == yaml.SequenceNode { - appendNodes(node, child.Content...) - } - } else if err := addToMap(node, op.fieldName, value, cm, style, skip); err != nil { - return nil, err - } - } - case reflect.Map: - node.Kind = yaml.MappingNode - keys := v.MapKeys() - sort.SliceStable(keys, func(i, j int) bool { - return natural.Less(keys[i].String(), keys[j].String()) - }) - - for i, k := range keys { - if i != 0 { - skip = true - } - if err := addToMap(node, k.Interface(), v.MapIndex(k).Interface(), nil, 0, skip); err != nil { - return nil, err - } - } - case reflect.Slice: - node.Kind = yaml.SequenceNode - nodes := make([]*yaml.Node, v.Len()) - - for i := 0; i < v.Len(); i++ { - if i != 0 { - skip = true - } - element := v.Index(i) - - var err error - - nodes[i], err = anyToYamlNode(element.Interface(), skip) - if err != nil { - return nil, err - } - } - appendNodes(node, nodes...) - default: - if err := node.Encode(model); err != nil { - return nil, err - } - } - - return node, nil -} - -func appendNodes(dest *yaml.Node, nodes ...*yaml.Node) { - if dest.Content == nil { - dest.Content = nodes - return - } - - dest.Content = append(dest.Content, nodes...) -} - -func addToMap(dest *yaml.Node, fieldName, in any, cm *comment, style yaml.Style, skip bool) error { - key, err := anyToYamlNode(fieldName, skip) - if err != nil { - return err - } - - value, err := anyToYamlNode(in, skip) - if err != nil { - return err - } - value.Style = style - - if !skip { - addComment(key, cm) - } - appendNodes(dest, key, value) - - return nil -} - -func addComment(node *yaml.Node, cm *comment) { - if cm == nil { - return - } - - node.HeadComment = cm.HeadComment - node.LineComment = cm.LineComment - node.FootComment = cm.FootComment -} diff --git a/vendor/modules.txt b/vendor/modules.txt index c62537d5f0..2622c5a9a5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -556,9 +556,6 @@ github.com/mailru/easyjson/jwriter github.com/mark3labs/mcp-go/mcp github.com/mark3labs/mcp-go/server github.com/mark3labs/mcp-go/util -# github.com/maruel/natural v1.1.0 -## explicit; go 1.11 -github.com/maruel/natural # github.com/mash/go-tempfile-suffix v0.0.0-20150731093933-48f0f8a3a5ab ## explicit github.com/mash/go-tempfile-suffix @@ -765,9 +762,6 @@ github.com/yosida95/uritemplate/v3 # github.com/yusufpapurcu/wmi v1.2.4 ## explicit; go 1.16 github.com/yusufpapurcu/wmi -# github.com/zijiren233/yaml-comment v0.2.1 -## explicit; go 1.18 -github.com/zijiren233/yaml-comment # go.mongodb.org/mongo-driver v1.17.7 ## explicit; go 1.18 go.mongodb.org/mongo-driver/bson