Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cmd/state/internal/cmdtree/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: &params.Language,
},
{
Name: "namespace",
Description: locale.T("package_import_flag_namespace_description"),
Value: &params.Namespace,
},
},
[]*captain.Argument{
{
Name: locale.Tl("import_file", "File"),
Expand Down
4 changes: 4 additions & 0 deletions internal/locale/locales/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 7 additions & 11 deletions internal/runners/packages/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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())
Expand All @@ -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")
}
Expand Down Expand Up @@ -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))
}
Expand Down
47 changes: 38 additions & 9 deletions pkg/platform/api/reqsimport/reqsimport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -85,16 +96,20 @@ 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
// translation service.
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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
2 changes: 1 addition & 1 deletion pkg/platform/model/buildplanner/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
39 changes: 27 additions & 12 deletions test/integration/import_int_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package integration

import (
"fmt"
"os"
"path/filepath"
"strings"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
})

Expand Down Expand Up @@ -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)
Expand Down
Loading