diff --git a/cmd/state/internal/cmdtree/packages.go b/cmd/state/internal/cmdtree/packages.go index 636dc02dc3..0f0f230b83 100644 --- a/cmd/state/internal/cmdtree/packages.go +++ b/cmd/state/internal/cmdtree/packages.go @@ -140,7 +140,18 @@ func newImportCommand(prime *primer.Values, globals *globalOptions) *captain.Com locale.Tl("package_import_title", "Importing Packages"), locale.T("package_import_cmd_description"), prime, - []*captain.Flag{}, + []*captain.Flag{ + { + Name: "language", + Description: locale.T("package_import_flag_language_description"), + Value: ¶ms.Language, + }, + { + Name: "namespace", + Description: locale.T("package_import_flag_namespace_description"), + Value: ¶ms.Namespace, + }, + }, []*captain.Argument{ { Name: locale.Tl("import_file", "File"), diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 062a75715c..0f9bb51a5e 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -597,6 +597,10 @@ package_search_flag_exact-term_description: other: Ensures that search results match search term exactly package_import_flag_filename_description: other: The file to import +package_import_flag_namespace_description: + other: The namespace targeted for the import. Leave blank to auto detect based on filename. +package_import_flag_language_description: + other: The language we're importing data from, this determines the format of the import. Leave blank to auto-detect based on filename. commit_message_added: other: "Added: {{.V0}}" commit_message_removed: diff --git a/internal/runners/packages/import.go b/internal/runners/packages/import.go index ef7f4436ea..b3833368ff 100644 --- a/internal/runners/packages/import.go +++ b/internal/runners/packages/import.go @@ -34,16 +34,11 @@ type Confirmer interface { Confirm(title, msg string, defaultOpt *bool) (bool, error) } -// ChangesetProvider describes the behavior required to convert some file data -// into a changeset. -type ChangesetProvider interface { - Changeset(contents []byte, lang string) (model.Changeset, error) -} - // ImportRunParams tracks the info required for running Import. type ImportRunParams struct { FileName string Language string + Namespace string NonInteractive bool } @@ -88,8 +83,9 @@ func (i *Import) Run(params *ImportRunParams) (rerr error) { out := i.prime.Output() out.Notice(locale.Tr("operating_message", proj.NamespaceString(), proj.Dir())) - if params.FileName == "" { - params.FileName = defaultImportFile + filename := params.FileName + if filename == "" { + filename = defaultImportFile } localCommitId, err := localcommit.Get(proj.Dir()) @@ -110,7 +106,7 @@ func (i *Import) Run(params *ImportRunParams) (rerr error) { } }() - changeset, err := fetchImportChangeset(reqsimport.Init(), params.FileName, language.Name) + changeset, err := fetchImportChangeset(reqsimport.Init(), filename, language.Name, params.Namespace) if err != nil { return errs.Wrap(err, "Could not import changeset") } @@ -170,13 +166,13 @@ func (i *Import) Run(params *ImportRunParams) (rerr error) { return nil } -func fetchImportChangeset(cp ChangesetProvider, file string, lang string) (model.Changeset, error) { +func fetchImportChangeset(reqImport *reqsimport.ReqsImport, file string, lang string, namespace string) (model.Changeset, error) { data, err := os.ReadFile(file) if err != nil { return nil, locale.WrapExternalError(err, "err_reading_changeset_file", "Cannot read import file: {{.V0}}", err.Error()) } - changeset, err := cp.Changeset(data, lang) + changeset, err := reqImport.Changeset(data, lang, file, namespace) if err != nil { return nil, locale.WrapError(err, "err_obtaining_change_request", "Could not process change set: {{.V0}}.", api.ErrorMessageFromPayload(err)) } diff --git a/pkg/platform/api/reqsimport/reqsimport.go b/pkg/platform/api/reqsimport/reqsimport.go index edd9550342..d9a5527dde 100644 --- a/pkg/platform/api/reqsimport/reqsimport.go +++ b/pkg/platform/api/reqsimport/reqsimport.go @@ -3,11 +3,15 @@ package reqsimport import ( "bytes" "encoding/json" + "errors" "fmt" + "io" "net/http" "path" + "path/filepath" "time" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/pkg/platform/api" @@ -63,16 +67,23 @@ func Init() *ReqsImport { // Changeset posts requirements data to a backend service and returns a // Changeset that can be committed to a project. -func (ri *ReqsImport) Changeset(data []byte, lang string) ([]*mono_models.CommitChangeEditable, error) { +func (ri *ReqsImport) Changeset(data []byte, lang, filename, namespace string) ([]*mono_models.CommitChangeEditable, error) { reqPayload := &TranslationReqMsg{ - Data: string(data), - Language: lang, + Data: string(data), + Language: lang, + IncludeLanguageCore: false, + NamespaceOverride: namespace, + Filename: filepath.Base(filename), + Unformatted: false, } respPayload := &TranslationRespMsg{} err := postJSON(ri.client, ri.opts.ReqsvcURL, reqPayload, respPayload) if err != nil { - return nil, err + if errors.Is(err, ErrBadInput) && respPayload.Message != "" { + return nil, locale.NewInputError("API responded with error: " + respPayload.Message) + } + return nil, errs.Wrap(err, "postJSON failed") } if len(respPayload.LineErrs) > 0 { @@ -85,9 +96,12 @@ func (ri *ReqsImport) Changeset(data []byte, lang string) ([]*mono_models.Commit // TranslationReqMsg represents the message sent to the requirements // translation service. type TranslationReqMsg struct { - Data string `json:"requirements"` - Language string `json:"language"` - Unformatted bool `json:"unformatted"` + Data string `json:"requirements"` + Language string `json:"language"` + IncludeLanguageCore bool `json:"includeLanguageCore"` + NamespaceOverride string `json:"namespaceOverride,omitempty"` + Filename string `json:"filename"` + Unformatted bool `json:"unformatted"` } // TranslationRespMsg represents the message returned by the requirements @@ -95,6 +109,7 @@ type TranslationReqMsg struct { type TranslationRespMsg struct { Changeset []*mono_models.CommitChangeEditable `json:"changeset,omitempty"` LineErrs []TranslationLineError `json:"errors,omitempty"` + Message string `json:"message,omitempty"` } // TranslationLineError represents an error reported by the requirements @@ -129,6 +144,8 @@ func (e *TranslationResponseError) Error() string { return locale.Tr("reqsvc_err_line_errors", msgs) } +var ErrBadInput = errs.New("Bad input") + func postJSON(client *http.Client, url string, reqPayload, respPayload interface{}) error { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(reqPayload); err != nil { @@ -137,10 +154,22 @@ func postJSON(client *http.Client, url string, reqPayload, respPayload interface logging.Debug("POSTing JSON") resp, err := client.Post(url, jsonContentType, &buf) + defer resp.Body.Close() // nolint if err != nil { - return err + body, _ := io.ReadAll(resp.Body) + return errs.Wrap(err, "Could not post JSON: %s", body) + } + + if resp.StatusCode == 400 { + err2 := json.NewDecoder(resp.Body).Decode(&respPayload) + if err2 != nil { + return errs.Wrap(ErrBadInput, "Could not decode response body") + } + return ErrBadInput + } + if resp.StatusCode > 299 { + return errs.New("HTTP error: %s", resp.Status) } - defer resp.Body.Close() //nolint return json.NewDecoder(resp.Body).Decode(&respPayload) } diff --git a/pkg/platform/model/buildplanner/build.go b/pkg/platform/model/buildplanner/build.go index 36b0e5b1c5..ab83aba6ae 100644 --- a/pkg/platform/model/buildplanner/build.go +++ b/pkg/platform/model/buildplanner/build.go @@ -190,7 +190,7 @@ func VersionStringToRequirements(version string) ([]types.VersionRequirement, er // Ask the Platform to translate a string like ">=1.2,<1.3" into a list of requirements. // Note that: // - The given requirement name does not matter; it is not looked up. - changeset, err := reqsimport.Init().Changeset([]byte("name "+version), "") + changeset, err := reqsimport.Init().Changeset([]byte("name "+version), "", "", "") if err != nil { return nil, locale.WrapInputError(err, "err_invalid_version_string", "Invalid version string") } diff --git a/test/integration/import_int_test.go b/test/integration/import_int_test.go index 8b4b5eba5f..a842ce70c7 100644 --- a/test/integration/import_int_test.go +++ b/test/integration/import_int_test.go @@ -1,7 +1,6 @@ package integration import ( - "fmt" "os" "path/filepath" "strings" @@ -11,7 +10,6 @@ import ( "github.com/ActiveState/termtest" "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/strutils" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/osutil" "github.com/ActiveState/cli/internal/testhelpers/suite" @@ -87,21 +85,16 @@ func (suite *ImportIntegrationTestSuite) TestImport() { defer ts.Close() ts.LoginAsPersistentUser() - pname := strutils.UUID() - namespace := fmt.Sprintf("%s/%s", e2e.PersistentUsername, pname.String()) - cp := ts.Spawn("init", "--language", "python", namespace, ts.Dirs.Work) - cp.Expect("successfully initialized", e2e.RuntimeSourcingTimeoutOpt) - cp.ExpectExitCode(0) - ts.NotifyProjectCreated(e2e.PersistentUsername, pname.String()) + ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") - reqsFilePath := filepath.Join(cp.WorkDirectory(), reqsFileName) + reqsFilePath := filepath.Join(ts.Dirs.Work, reqsFileName) suite.Run("invalid requirements.txt", func() { ts.SetT(suite.T()) ts.PrepareFile(reqsFilePath, badReqsData) - cp = ts.Spawn("import", "requirements.txt") + cp := ts.Spawn("import", "requirements.txt") cp.ExpectNotExitCode(0) }) @@ -137,14 +130,36 @@ func (suite *ImportIntegrationTestSuite) TestImport() { cp.Expect(">=0.6.1 →") cp.Expect("Mopidy-Dirble") cp.Expect("requests") - cp.Expect(">=2.2,<2.31.0 → 2.30.0") + cp.Expect(">=2.2,<2.31.0 → ") cp.Expect("urllib3") - cp.Expect(">=1.21.1,<=1.26.5 → 1.26.5") + cp.Expect(">=1.21.1,<=1.26.5 → ") cp.ExpectExitCode(0) }) ts.IgnoreLogErrors() } +func (suite *ImportIntegrationTestSuite) TestImportOverrideNamespace() { + suite.OnlyRunForTags(tagsuite.Import) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") + + reqsFilePath := filepath.Join(ts.Dirs.Work, reqsFileName) + + ts.PrepareFile(reqsFilePath, reqsData) + + cp := ts.Spawn("import", "requirements.txt", "--namespace", "language/perl") + // Error handling on this is poor atm, but this is currently the expected behavior + // Partial solves and better error handling will smooth this out in the long term + cp.Expect("Because root depends on Feature|language/perl") + cp.ExpectNotExitCode(0, e2e.RuntimeSolvingTimeoutOpt) + + cp = ts.Spawn("history") + cp.Expect("namespace: language/perl") + cp.ExpectExitCode(0) +} + func (suite *ImportIntegrationTestSuite) TestImportCycloneDx() { suite.OnlyRunForTags(tagsuite.Import) ts := e2e.New(suite.T(), false)