github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runners/packages/import.go (about)

     1  package packages
     2  
     3  import (
     4  	"os"
     5  
     6  	"github.com/ActiveState/cli/internal/analytics"
     7  	"github.com/ActiveState/cli/internal/errs"
     8  	"github.com/ActiveState/cli/internal/keypairs"
     9  	"github.com/ActiveState/cli/internal/locale"
    10  	"github.com/ActiveState/cli/internal/logging"
    11  	"github.com/ActiveState/cli/internal/output"
    12  	"github.com/ActiveState/cli/internal/primer"
    13  	"github.com/ActiveState/cli/internal/prompt"
    14  	"github.com/ActiveState/cli/internal/runbits/rationalize"
    15  	"github.com/ActiveState/cli/internal/runbits/runtime"
    16  	"github.com/ActiveState/cli/pkg/localcommit"
    17  	"github.com/ActiveState/cli/pkg/platform/api"
    18  	"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
    19  	"github.com/ActiveState/cli/pkg/platform/api/reqsimport"
    20  	"github.com/ActiveState/cli/pkg/platform/authentication"
    21  	"github.com/ActiveState/cli/pkg/platform/model"
    22  	"github.com/ActiveState/cli/pkg/platform/model/buildplanner"
    23  	"github.com/ActiveState/cli/pkg/platform/runtime/buildexpression"
    24  	"github.com/ActiveState/cli/pkg/platform/runtime/target"
    25  	"github.com/ActiveState/cli/pkg/project"
    26  )
    27  
    28  const (
    29  	defaultImportFile = "requirements.txt"
    30  )
    31  
    32  type configurable interface {
    33  	keypairs.Configurable
    34  }
    35  
    36  // Confirmer describes the behavior required to prompt a user for confirmation.
    37  type Confirmer interface {
    38  	Confirm(title, msg string, defaultOpt *bool) (bool, error)
    39  }
    40  
    41  // ChangesetProvider describes the behavior required to convert some file data
    42  // into a changeset.
    43  type ChangesetProvider interface {
    44  	Changeset(contents []byte, lang string) (model.Changeset, error)
    45  }
    46  
    47  // ImportRunParams tracks the info required for running Import.
    48  type ImportRunParams struct {
    49  	FileName       string
    50  	Language       string
    51  	NonInteractive bool
    52  }
    53  
    54  // NewImportRunParams prepares the info required for running Import with default
    55  // values.
    56  func NewImportRunParams() *ImportRunParams {
    57  	return &ImportRunParams{
    58  		FileName: defaultImportFile,
    59  	}
    60  }
    61  
    62  // Import manages the importing execution context.
    63  type Import struct {
    64  	auth *authentication.Auth
    65  	out  output.Outputer
    66  	prompt.Prompter
    67  	proj      *project.Project
    68  	cfg       configurable
    69  	analytics analytics.Dispatcher
    70  	svcModel  *model.SvcModel
    71  }
    72  
    73  type primeable interface {
    74  	primer.Outputer
    75  	primer.Prompter
    76  	primer.Projecter
    77  	primer.Auther
    78  	primer.Configurer
    79  	primer.Analyticer
    80  	primer.SvcModeler
    81  }
    82  
    83  // NewImport prepares an importation execution context for use.
    84  func NewImport(prime primeable) *Import {
    85  	return &Import{
    86  		prime.Auth(),
    87  		prime.Output(),
    88  		prime.Prompt(),
    89  		prime.Project(),
    90  		prime.Config(),
    91  		prime.Analytics(),
    92  		prime.SvcModel(),
    93  	}
    94  }
    95  
    96  // Run executes the import behavior.
    97  func (i *Import) Run(params *ImportRunParams) error {
    98  	logging.Debug("ExecuteImport")
    99  
   100  	if i.proj == nil {
   101  		return rationalize.ErrNoProject
   102  	}
   103  
   104  	i.out.Notice(locale.Tr("operating_message", i.proj.NamespaceString(), i.proj.Dir()))
   105  
   106  	if params.FileName == "" {
   107  		params.FileName = defaultImportFile
   108  	}
   109  
   110  	latestCommit, err := localcommit.Get(i.proj.Dir())
   111  	if err != nil {
   112  		return locale.WrapError(err, "package_err_cannot_obtain_commit")
   113  	}
   114  
   115  	reqs, err := fetchCheckpoint(&latestCommit, i.auth)
   116  	if err != nil {
   117  		return locale.WrapError(err, "package_err_cannot_fetch_checkpoint")
   118  	}
   119  
   120  	lang, err := model.CheckpointToLanguage(reqs, i.auth)
   121  	if err != nil {
   122  		return locale.WrapExternalError(err, "err_import_language", "Your project does not have a language associated with it, please add a language first.")
   123  	}
   124  
   125  	changeset, err := fetchImportChangeset(reqsimport.Init(), params.FileName, lang.Name)
   126  	if err != nil {
   127  		return errs.Wrap(err, "Could not import changeset")
   128  	}
   129  
   130  	bp := buildplanner.NewBuildPlannerModel(i.auth)
   131  	be, err := bp.GetBuildExpression(latestCommit.String())
   132  	if err != nil {
   133  		return locale.WrapError(err, "err_cannot_get_build_expression", "Could not get build expression")
   134  	}
   135  
   136  	if err := applyChangeset(changeset, be); err != nil {
   137  		return locale.WrapError(err, "err_cannot_apply_changeset", "Could not apply changeset")
   138  	}
   139  
   140  	if err := be.SetDefaultTimestamp(); err != nil {
   141  		return locale.WrapError(err, "err_cannot_set_timestamp", "Could not set timestamp")
   142  	}
   143  
   144  	msg := locale.T("commit_reqstext_message")
   145  	commitID, err := bp.StageCommit(buildplanner.StageCommitParams{
   146  		Owner:        i.proj.Owner(),
   147  		Project:      i.proj.Name(),
   148  		ParentCommit: latestCommit.String(),
   149  		Description:  msg,
   150  		Expression:   be,
   151  	})
   152  	if err != nil {
   153  		return locale.WrapError(err, "err_commit_changeset", "Could not commit import changes")
   154  	}
   155  
   156  	if err := localcommit.Set(i.proj.Dir(), commitID.String()); err != nil {
   157  		return locale.WrapError(err, "err_package_update_commit_id")
   158  	}
   159  
   160  	_, err = runtime.SolveAndUpdate(i.auth, i.out, i.analytics, i.proj, &commitID, target.TriggerImport, i.svcModel, i.cfg, runtime.OptOrderChanged)
   161  	return err
   162  }
   163  
   164  func fetchImportChangeset(cp ChangesetProvider, file string, lang string) (model.Changeset, error) {
   165  	data, err := os.ReadFile(file)
   166  	if err != nil {
   167  		return nil, locale.WrapExternalError(err, "err_reading_changeset_file", "Cannot read import file: {{.V0}}", err.Error())
   168  	}
   169  
   170  	changeset, err := cp.Changeset(data, lang)
   171  	if err != nil {
   172  		return nil, locale.WrapError(err, "err_obtaining_change_request", "Could not process change set: {{.V0}}.", api.ErrorMessageFromPayload(err))
   173  	}
   174  
   175  	return changeset, err
   176  }
   177  
   178  func applyChangeset(changeset model.Changeset, be *buildexpression.BuildExpression) error {
   179  	for _, change := range changeset {
   180  		var expressionOperation types.Operation
   181  		switch change.Operation {
   182  		case string(model.OperationAdded):
   183  			expressionOperation = types.OperationAdded
   184  		case string(model.OperationRemoved):
   185  			expressionOperation = types.OperationRemoved
   186  		case string(model.OperationUpdated):
   187  			expressionOperation = types.OperationUpdated
   188  		}
   189  
   190  		req := types.Requirement{
   191  			Name:      change.Requirement,
   192  			Namespace: change.Namespace,
   193  		}
   194  
   195  		for _, constraint := range change.VersionConstraints {
   196  			req.VersionRequirement = append(req.VersionRequirement, types.VersionRequirement{
   197  				types.VersionRequirementComparatorKey: constraint.Comparator,
   198  				types.VersionRequirementVersionKey:    constraint.Version,
   199  			})
   200  		}
   201  
   202  		if err := be.UpdateRequirement(expressionOperation, req); err != nil {
   203  			return errs.Wrap(err, "Could not update build expression")
   204  		}
   205  	}
   206  
   207  	return nil
   208  }