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

     1  package packages
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/ActiveState/cli/pkg/buildplan"
    10  	bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner"
    11  	"github.com/go-openapi/strfmt"
    12  
    13  	"github.com/ActiveState/cli/internal/analytics"
    14  	"github.com/ActiveState/cli/internal/config"
    15  	"github.com/ActiveState/cli/internal/errs"
    16  	"github.com/ActiveState/cli/internal/locale"
    17  	"github.com/ActiveState/cli/internal/logging"
    18  	"github.com/ActiveState/cli/internal/output"
    19  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    20  	"github.com/ActiveState/cli/internal/runbits/rationalize"
    21  	"github.com/ActiveState/cli/pkg/localcommit"
    22  	gqlModel "github.com/ActiveState/cli/pkg/platform/api/graphql/model"
    23  	"github.com/ActiveState/cli/pkg/platform/authentication"
    24  	"github.com/ActiveState/cli/pkg/platform/model"
    25  	"github.com/ActiveState/cli/pkg/project"
    26  )
    27  
    28  // ListRunParams tracks the info required for running List.
    29  type ListRunParams struct {
    30  	Commit  string
    31  	Name    string
    32  	Project string
    33  }
    34  
    35  // List manages the listing execution context.
    36  type List struct {
    37  	out       output.Outputer
    38  	project   *project.Project
    39  	analytics analytics.Dispatcher
    40  	svcModel  *model.SvcModel
    41  	auth      *authentication.Auth
    42  	cfg       *config.Instance
    43  }
    44  
    45  // NewList prepares a list execution context for use.
    46  func NewList(prime primeable) *List {
    47  	return &List{
    48  		out:       prime.Output(),
    49  		project:   prime.Project(),
    50  		analytics: prime.Analytics(),
    51  		svcModel:  prime.SvcModel(),
    52  		auth:      prime.Auth(),
    53  		cfg:       prime.Config(),
    54  	}
    55  }
    56  
    57  type requirement struct {
    58  	Name            string `json:"package"`
    59  	Version         string `json:"version" `
    60  	ResolvedVersion string `json:"resolved_version"`
    61  }
    62  
    63  type requirementPlainOutput struct {
    64  	Name    string `locale:"package_name,Name"`
    65  	Version string `locale:"package_version,Version"`
    66  }
    67  
    68  // Run executes the list behavior.
    69  func (l *List) Run(params ListRunParams, nstype model.NamespaceType) error {
    70  	logging.Debug("ExecuteList")
    71  
    72  	l.out.Notice(locale.T("manifest_deprecation_warning"))
    73  
    74  	if l.project != nil && params.Project == "" {
    75  		l.out.Notice(locale.Tr("operating_message", l.project.NamespaceString(), l.project.Dir()))
    76  	}
    77  
    78  	var commitID *strfmt.UUID
    79  	var err error
    80  	switch {
    81  	case params.Commit != "":
    82  		commitID, err = targetFromCommit(params.Commit, l.project)
    83  		if err != nil {
    84  			return locale.WrapError(err, fmt.Sprintf("%s_err_cannot_obtain_commit", nstype))
    85  		}
    86  	case params.Project != "":
    87  		commitID, err = targetFromProject(params.Project)
    88  		if err != nil {
    89  			return locale.WrapError(err, fmt.Sprintf("%s_err_cannot_obtain_commit", nstype))
    90  		}
    91  	default:
    92  		commitID, err = targetFromProjectFile(l.project)
    93  		if err != nil {
    94  			return locale.WrapError(err, fmt.Sprintf("%s_err_cannot_obtain_commit", nstype))
    95  		}
    96  	}
    97  
    98  	checkpoint, err := fetchCheckpoint(commitID, l.auth)
    99  	if err != nil {
   100  		return locale.WrapError(err, fmt.Sprintf("%s_err_cannot_fetch_checkpoint", nstype))
   101  	}
   102  
   103  	language, err := model.LanguageByCommit(*commitID, l.auth)
   104  	if err != nil {
   105  		return locale.WrapError(err, "err_package_list_language", "Unable to get language from project")
   106  	}
   107  	ns := ptr.To(model.NewNamespacePkgOrBundle(language.Name, nstype))
   108  
   109  	// Fetch resolved artifacts list for showing full version numbers, if possible.
   110  	var artifacts buildplan.Artifacts
   111  	if l.project != nil && params.Project == "" {
   112  		bpm := bpModel.NewBuildPlannerModel(l.auth)
   113  		commit, err := bpm.FetchCommit(*commitID, l.project.Owner(), l.project.Name(), nil)
   114  		if err != nil {
   115  			return errs.Wrap(err, "could not fetch commit")
   116  		}
   117  		artifacts = commit.BuildPlan().Artifacts(buildplan.FilterStateArtifacts())
   118  	}
   119  
   120  	requirements := model.FilterCheckpointNamespace(checkpoint, model.NamespacePackage, model.NamespaceBundle)
   121  	sort.SliceStable(requirements, func(i, j int) bool {
   122  		return strings.ToLower(requirements[i].Requirement) < strings.ToLower(requirements[j].Requirement)
   123  	})
   124  
   125  	requirementsPlainOutput := []requirementPlainOutput{}
   126  	requirementsOutput := []requirement{}
   127  
   128  	for _, req := range requirements {
   129  		if !strings.Contains(strings.ToLower(req.Requirement), strings.ToLower(params.Name)) {
   130  			continue
   131  		}
   132  
   133  		if !strings.HasPrefix(req.Namespace, nstype.Prefix()) {
   134  			continue
   135  		}
   136  
   137  		version := req.VersionConstraint
   138  		if version == "" {
   139  			version = model.GqlReqVersionConstraintsString(req)
   140  			if version == "" {
   141  				version = locale.T("constraint_auto")
   142  			}
   143  		}
   144  
   145  		resolvedVersion := ""
   146  		(func() {
   147  			for _, a := range artifacts {
   148  				for _, i := range a.Ingredients {
   149  					if i.Namespace == ns.String() && i.Name == req.Requirement {
   150  						resolvedVersion = i.Version
   151  						return // break outer loop
   152  					}
   153  				}
   154  			}
   155  		})()
   156  
   157  		plainVersion := version
   158  		if resolvedVersion != "" && resolvedVersion != version {
   159  			plainVersion = locale.Tr("constraint_resolved", version, resolvedVersion)
   160  		}
   161  		requirementsPlainOutput = append(requirementsPlainOutput, requirementPlainOutput{
   162  			Name:    req.Requirement,
   163  			Version: plainVersion,
   164  		})
   165  
   166  		requirementsOutput = append(requirementsOutput, requirement{
   167  			Name:            req.Requirement,
   168  			Version:         version,
   169  			ResolvedVersion: resolvedVersion,
   170  		})
   171  	}
   172  
   173  	var plainOutput interface{} = requirementsPlainOutput
   174  	if len(requirementsOutput) == 0 {
   175  		plainOutput = locale.T(fmt.Sprintf("%s_list_no_packages", nstype.String()))
   176  	}
   177  
   178  	l.out.Print(output.Prepare(plainOutput, requirementsOutput))
   179  	return nil
   180  }
   181  
   182  func targetFromCommit(commitOpt string, proj *project.Project) (*strfmt.UUID, error) {
   183  	if commitOpt == "latest" {
   184  		logging.Debug("latest commit selected")
   185  		return model.BranchCommitID(proj.Owner(), proj.Name(), proj.BranchName())
   186  	}
   187  
   188  	return prepareCommit(commitOpt)
   189  }
   190  
   191  func targetFromProject(projectString string) (*strfmt.UUID, error) {
   192  	ns, err := project.ParseNamespace(projectString)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	branch, err := model.DefaultBranchForProjectName(ns.Owner, ns.Project)
   198  	if err != nil {
   199  		return nil, errs.Wrap(err, "Could not grab default branch for project")
   200  	}
   201  
   202  	return branch.CommitID, nil
   203  }
   204  
   205  func targetFromProjectFile(proj *project.Project) (*strfmt.UUID, error) {
   206  	logging.Debug("commit from project file")
   207  	if proj == nil {
   208  		return nil, rationalize.ErrNoProject
   209  	}
   210  	commit, err := localcommit.Get(proj.Dir())
   211  	if err != nil {
   212  		return nil, errs.Wrap(err, "Unable to get local commit")
   213  	}
   214  	if commit == "" {
   215  		logging.Debug("latest commit used as fallback selection")
   216  		return model.BranchCommitID(proj.Owner(), proj.Name(), proj.BranchName())
   217  	}
   218  
   219  	return prepareCommit(commit.String())
   220  }
   221  
   222  func prepareCommit(commit string) (*strfmt.UUID, error) {
   223  	logging.Debug("commit %s selected", commit)
   224  	if ok := strfmt.Default.Validates("uuid", commit); !ok {
   225  		return nil, locale.NewInputError("err_invalid_commit", "Invalid commit: {{.V0}}", commit)
   226  	}
   227  
   228  	var uuid strfmt.UUID
   229  	if err := uuid.UnmarshalText([]byte(commit)); err != nil {
   230  		return nil, errs.Wrap(err, "UnmarshalText %s failed", commit)
   231  	}
   232  
   233  	return &uuid, nil
   234  }
   235  
   236  func fetchCheckpoint(commit *strfmt.UUID, auth *authentication.Auth) ([]*gqlModel.Requirement, error) {
   237  	if commit == nil {
   238  		logging.Debug("commit id is nil")
   239  		return nil, nil
   240  	}
   241  
   242  	checkpoint, _, err := model.FetchCheckpointForCommit(*commit, auth)
   243  	if err != nil && errors.Is(err, model.ErrNoData) {
   244  		return nil, locale.WrapExternalError(err, "package_no_data")
   245  	}
   246  
   247  	return checkpoint, err
   248  }