github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/projectfile/projectfile.go (about)

     1  package projectfile
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/url"
     7  	"os"
     8  	"os/user"
     9  	"path/filepath"
    10  	"regexp"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/ActiveState/cli/internal/assets"
    17  	"github.com/ActiveState/cli/internal/condition"
    18  	"github.com/ActiveState/cli/internal/config"
    19  	"github.com/ActiveState/cli/internal/constants"
    20  	"github.com/ActiveState/cli/internal/errs"
    21  	"github.com/ActiveState/cli/internal/fileutils"
    22  	"github.com/ActiveState/cli/internal/hash"
    23  	"github.com/ActiveState/cli/internal/language"
    24  	"github.com/ActiveState/cli/internal/locale"
    25  	"github.com/ActiveState/cli/internal/logging"
    26  	"github.com/ActiveState/cli/internal/multilog"
    27  	"github.com/ActiveState/cli/internal/osutils"
    28  	"github.com/ActiveState/cli/internal/profile"
    29  	"github.com/ActiveState/cli/internal/rollbar"
    30  	"github.com/ActiveState/cli/internal/rtutils"
    31  	"github.com/ActiveState/cli/internal/sliceutils"
    32  	"github.com/ActiveState/cli/internal/strutils"
    33  	"github.com/ActiveState/cli/pkg/sysinfo"
    34  	"github.com/go-openapi/strfmt"
    35  	"github.com/google/uuid"
    36  	"github.com/imdario/mergo"
    37  	"github.com/spf13/cast"
    38  	"github.com/thoas/go-funk"
    39  	"gopkg.in/yaml.v2"
    40  )
    41  
    42  var (
    43  	urlProjectRegexStr = `https:\/\/[\w\.]+\/([\w_.-]*)\/([\w_.-]*)(?:\?commitID=)*([^&]*)(?:\&branch=)*(.*)`
    44  	urlCommitRegexStr  = `https:\/\/[\w\.]+\/commit\/(.*)`
    45  
    46  	// ProjectURLRe Regex used to validate project fields /orgname/projectname[?commitID=someUUID]
    47  	ProjectURLRe = regexp.MustCompile(urlProjectRegexStr)
    48  	// CommitURLRe Regex used to validate commit info /commit/someUUID
    49  	CommitURLRe = regexp.MustCompile(urlCommitRegexStr)
    50  	// deprecatedRegex covers the deprecated fields in the project file
    51  	deprecatedRegex = regexp.MustCompile(`(?m)^\s*(?:constraints|platforms|languages):`)
    52  	// nonAlphanumericRegex covers all non alphanumeric characters
    53  	nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9 ]+`)
    54  )
    55  
    56  const ConfigVersion = 1
    57  
    58  type MigratorFunc func(project *Project, configVersion int) (int, error)
    59  
    60  var migrationRunning bool
    61  
    62  var migrator MigratorFunc
    63  
    64  func RegisterMigrator(m MigratorFunc) {
    65  	migrator = m
    66  }
    67  
    68  type ErrorParseProject struct{ *locale.LocalizedError }
    69  
    70  type ErrorNoProject struct{ *locale.LocalizedError }
    71  
    72  type ErrorNoProjectFromEnv struct{ *locale.LocalizedError }
    73  
    74  type ErrorNoDefaultProject struct{ *locale.LocalizedError }
    75  
    76  // projectURL comprises all fields of a parsed project URL
    77  type projectURL struct {
    78  	Owner          string
    79  	Name           string
    80  	LegacyCommitID string
    81  	BranchName     string
    82  }
    83  
    84  const LocalProjectsConfigKey = "projects"
    85  
    86  // VersionInfo is used in cases where we only care about parsing the version and channel fields.
    87  // In all other cases the version is parsed via the Project struct
    88  type VersionInfo struct {
    89  	Channel string `yaml:"branch"` // branch for backward compatibility
    90  	Version string
    91  	Lock    string `yaml:"lock"`
    92  }
    93  
    94  // ProjectSimple reflects a bare basic project structure
    95  type ProjectSimple struct {
    96  	Project string `yaml:"project"`
    97  }
    98  
    99  // Project covers the top level project structure of our yaml
   100  type Project struct {
   101  	Project       string        `yaml:"project"`
   102  	ConfigVersion int           `yaml:"config_version"`
   103  	Lock          string        `yaml:"lock,omitempty"`
   104  	Environments  string        `yaml:"environments,omitempty"`
   105  	Constants     Constants     `yaml:"constants,omitempty"`
   106  	Secrets       *SecretScopes `yaml:"secrets,omitempty"`
   107  	Events        Events        `yaml:"events,omitempty"`
   108  	Scripts       Scripts       `yaml:"scripts,omitempty"`
   109  	Jobs          Jobs          `yaml:"jobs,omitempty"`
   110  	Private       bool          `yaml:"private,omitempty"`
   111  	Cache         string        `yaml:"cache,omitempty"`
   112  	path          string        // "private"
   113  	parsedURL     projectURL    // parsed url data
   114  	parsedChannel string
   115  	parsedVersion string
   116  }
   117  
   118  // Build covers the build map, which can go under languages or packages
   119  // Build can hold variable keys, so we cannot predict what they are, hence why it is a map
   120  type Build map[string]string
   121  
   122  // ConstantFields are the common fields for the Constant type. This is required
   123  // for type composition related to its yaml.Unmarshaler implementation.
   124  type ConstantFields struct {
   125  	Conditional Conditional `yaml:"if,omitempty"`
   126  }
   127  
   128  // Constant covers the constant structure, which goes under Project
   129  type Constant struct {
   130  	NameVal        `yaml:",inline"`
   131  	ConstantFields `yaml:",inline"`
   132  }
   133  
   134  func (c *Constant) UnmarshalYAML(unmarshal func(interface{}) error) error {
   135  	if err := unmarshal(&c.NameVal); err != nil {
   136  		return err
   137  	}
   138  	if err := unmarshal(&c.ConstantFields); err != nil {
   139  		return err
   140  	}
   141  	return nil
   142  }
   143  
   144  var _ ConstrainedEntity = &Constant{}
   145  
   146  // ID returns the constant name
   147  func (c *Constant) ID() string {
   148  	return c.Name
   149  }
   150  
   151  func (c *Constant) ConditionalFilter() Conditional {
   152  	return c.Conditional
   153  }
   154  
   155  // Constants is a slice of constant values
   156  type Constants []*Constant
   157  
   158  // AsConstrainedEntities boxes constants as a slice ConstrainedEntities
   159  func (constants Constants) AsConstrainedEntities() (items []ConstrainedEntity) {
   160  	for _, c := range constants {
   161  		items = append(items, c)
   162  	}
   163  	return items
   164  }
   165  
   166  // MakeConstantsFromConstrainedEntities unboxes ConstraintedEntities as Constants
   167  func MakeConstantsFromConstrainedEntities(items []ConstrainedEntity) (constants []*Constant) {
   168  	constants = make([]*Constant, 0, len(items))
   169  	for _, v := range items {
   170  		if o, ok := v.(*Constant); ok {
   171  			constants = append(constants, o)
   172  		}
   173  	}
   174  	return constants
   175  }
   176  
   177  // SecretScopes holds secret scopes, scopes define what the secrets belong to
   178  type SecretScopes struct {
   179  	User    Secrets `yaml:"user,omitempty"`
   180  	Project Secrets `yaml:"project,omitempty"`
   181  }
   182  
   183  // Secret covers the variable structure, which goes under Project
   184  type Secret struct {
   185  	Name        string      `yaml:"name"`
   186  	Description string      `yaml:"description"`
   187  	Conditional Conditional `yaml:"if,omitempty"`
   188  }
   189  
   190  var _ ConstrainedEntity = &Secret{}
   191  
   192  // ID returns the secret name
   193  func (s *Secret) ID() string {
   194  	return s.Name
   195  }
   196  
   197  func (s *Secret) ConditionalFilter() Conditional {
   198  	return s.Conditional
   199  }
   200  
   201  // Secrets is a slice of Secret definitions
   202  type Secrets []*Secret
   203  
   204  // AsConstrainedEntities box Secrets as a slice of ConstrainedEntities
   205  func (secrets Secrets) AsConstrainedEntities() (items []ConstrainedEntity) {
   206  	for _, s := range secrets {
   207  		items = append(items, s)
   208  	}
   209  	return items
   210  }
   211  
   212  // MakeSecretsFromConstrainedEntities unboxes ConstraintedEntities as Secrets
   213  func MakeSecretsFromConstrainedEntities(items []ConstrainedEntity) (secrets []*Secret) {
   214  	secrets = make([]*Secret, 0, len(items))
   215  	for _, v := range items {
   216  		if o, ok := v.(*Secret); ok {
   217  			secrets = append(secrets, o)
   218  		}
   219  	}
   220  	return secrets
   221  }
   222  
   223  // Conditional is an `if` conditional that when evalutes to true enables the entity its under
   224  // it is meant to replace Constraints
   225  type Conditional string
   226  
   227  // ConstrainedEntity is an entity in a project file that can be filtered with constraints
   228  type ConstrainedEntity interface {
   229  	// ID returns the name of the entity
   230  	ID() string
   231  
   232  	ConditionalFilter() Conditional
   233  }
   234  
   235  // Package covers the package structure, which goes under the language struct
   236  type Package struct {
   237  	Name        string      `yaml:"name"`
   238  	Version     string      `yaml:"version"`
   239  	Conditional Conditional `yaml:"if,omitempty"`
   240  	Build       Build       `yaml:"build,omitempty"`
   241  }
   242  
   243  var _ ConstrainedEntity = Package{}
   244  
   245  // ID returns the package name
   246  func (p Package) ID() string {
   247  	return p.Name
   248  }
   249  
   250  func (p Package) ConditionalFilter() Conditional {
   251  	return p.Conditional
   252  }
   253  
   254  // Packages is a slice of Package configurations
   255  type Packages []Package
   256  
   257  // AsConstrainedEntities boxes Packages as a slice of ConstrainedEntities
   258  func (packages Packages) AsConstrainedEntities() (items []ConstrainedEntity) {
   259  	for i := range packages {
   260  		items = append(items, &packages[i])
   261  	}
   262  	return items
   263  }
   264  
   265  // MakePackagesFromConstrainedEntities unboxes ConstraintedEntities as Packages
   266  func MakePackagesFromConstrainedEntities(items []ConstrainedEntity) (packages []*Package) {
   267  	packages = make([]*Package, 0, len(items))
   268  	for _, v := range items {
   269  		if o, ok := v.(*Package); ok {
   270  			packages = append(packages, o)
   271  		}
   272  	}
   273  	return packages
   274  }
   275  
   276  // EventFields are the common fields for the Event type. This is required
   277  // for type composition related to its yaml.Unmarshaler implementation.
   278  type EventFields struct {
   279  	Scope       []string    `yaml:"scope"`
   280  	Conditional Conditional `yaml:"if,omitempty"`
   281  	id          string
   282  }
   283  
   284  // Event covers the event structure, which goes under Project
   285  type Event struct {
   286  	NameVal     `yaml:",inline"`
   287  	EventFields `yaml:",inline"`
   288  }
   289  
   290  func (e *Event) UnmarshalYAML(unmarshal func(interface{}) error) error {
   291  	if err := unmarshal(&e.NameVal); err != nil {
   292  		return err
   293  	}
   294  	if err := unmarshal(&e.EventFields); err != nil {
   295  		return err
   296  	}
   297  	return nil
   298  }
   299  
   300  var _ ConstrainedEntity = Event{}
   301  
   302  // ID returns the event name
   303  func (e Event) ID() string {
   304  	if e.id == "" {
   305  		id, err := uuid.NewUUID()
   306  		if err != nil {
   307  			multilog.Error("UUID generation failed, defaulting to serialization")
   308  			e.id = hash.ShortHash(e.Name, e.Value, strings.Join(e.Scope, ""))
   309  		} else {
   310  			e.id = id.String()
   311  		}
   312  	}
   313  	return e.id
   314  }
   315  
   316  func (e Event) ConditionalFilter() Conditional {
   317  	return e.Conditional
   318  }
   319  
   320  // Events is a slice of Event definitions
   321  type Events []Event
   322  
   323  // AsConstrainedEntities boxes events as a slice of ConstrainedEntities
   324  func (events Events) AsConstrainedEntities() (items []ConstrainedEntity) {
   325  	for i := range events {
   326  		items = append(items, &events[i])
   327  	}
   328  	return items
   329  }
   330  
   331  // MakeEventsFromConstrainedEntities unboxes ConstraintedEntities as Events
   332  func MakeEventsFromConstrainedEntities(items []ConstrainedEntity) (events []*Event) {
   333  	events = make([]*Event, 0, len(items))
   334  	for _, v := range items {
   335  		if o, ok := v.(*Event); ok {
   336  			events = append(events, o)
   337  		}
   338  	}
   339  	return events
   340  }
   341  
   342  // ScriptFields are the common fields for the Script type. This is required
   343  // for type composition related to its yaml.Unmarshaler implementation.
   344  type ScriptFields struct {
   345  	Description string      `yaml:"description,omitempty"`
   346  	Filename    string      `yaml:"filename,omitempty"`
   347  	Standalone  bool        `yaml:"standalone,omitempty"`
   348  	Language    string      `yaml:"language,omitempty"`
   349  	Conditional Conditional `yaml:"if,omitempty"`
   350  }
   351  
   352  // Script covers the script structure, which goes under Project
   353  type Script struct {
   354  	NameVal      `yaml:",inline"`
   355  	ScriptFields `yaml:",inline"`
   356  }
   357  
   358  func (s *Script) UnmarshalYAML(unmarshal func(interface{}) error) error {
   359  	if err := unmarshal(&s.NameVal); err != nil {
   360  		return err
   361  	}
   362  	if err := unmarshal(&s.ScriptFields); err != nil {
   363  		return err
   364  	}
   365  	return nil
   366  }
   367  
   368  var _ ConstrainedEntity = Script{}
   369  
   370  // ID returns the script name
   371  func (s Script) ID() string {
   372  	return s.Name
   373  }
   374  
   375  func (s Script) ConditionalFilter() Conditional {
   376  	return s.Conditional
   377  }
   378  
   379  // Scripts is a slice of scripts
   380  type Scripts []Script
   381  
   382  // AsConstrainedEntities boxes scripts as a slice of ConstrainedEntities
   383  func (scripts Scripts) AsConstrainedEntities() (items []ConstrainedEntity) {
   384  	for i := range scripts {
   385  		items = append(items, &scripts[i])
   386  	}
   387  	return items
   388  }
   389  
   390  // MakeScriptsFromConstrainedEntities unboxes ConstraintedEntities as Scripts
   391  func MakeScriptsFromConstrainedEntities(items []ConstrainedEntity) (scripts []*Script) {
   392  	scripts = make([]*Script, 0, len(items))
   393  	for _, v := range items {
   394  		if o, ok := v.(*Script); ok {
   395  			scripts = append(scripts, o)
   396  		}
   397  	}
   398  	return scripts
   399  }
   400  
   401  // Job covers the job structure, which goes under Project
   402  type Job struct {
   403  	Name      string   `yaml:"name"`
   404  	Constants []string `yaml:"constants"`
   405  	Scripts   []string `yaml:"scripts"`
   406  }
   407  
   408  // Jobs is a slice of jobs
   409  type Jobs []Job
   410  
   411  var persistentProject *Project
   412  
   413  // Parse the given filepath, which should be the full path to an activestate.yaml file
   414  func Parse(configFilepath string) (_ *Project, rerr error) {
   415  	projectDir := filepath.Dir(configFilepath)
   416  	files, err := os.ReadDir(projectDir)
   417  	if err != nil {
   418  		return nil, locale.WrapError(err, "err_project_readdir", "Could not read project directory: {{.V0}}.", projectDir)
   419  	}
   420  
   421  	project, err := parse(configFilepath)
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  
   426  	re, _ := regexp.Compile(`activestate[._-](\w+)\.yaml`)
   427  	for _, file := range files {
   428  		match := re.FindStringSubmatch(file.Name())
   429  		if len(match) == 0 {
   430  			continue
   431  		}
   432  
   433  		// If an OS keyword was used ensure it matches our runtime
   434  		l := strings.ToLower
   435  		keyword := l(match[1])
   436  		if (keyword == l(sysinfo.Linux.String()) || keyword == l(sysinfo.Mac.String()) || keyword == l(sysinfo.Windows.String())) &&
   437  			keyword != l(sysinfo.OS().String()) {
   438  			continue
   439  		}
   440  
   441  		secondaryProject, err := parse(filepath.Join(projectDir, file.Name()))
   442  		if err != nil {
   443  			return nil, err
   444  		}
   445  		if err := mergo.Merge(secondaryProject, *project, mergo.WithAppendSlice); err != nil {
   446  			return nil, errs.Wrap(err, "Could not merge %s into your activestate.yaml", file.Name())
   447  		}
   448  		secondaryProject.path = project.path // keep original project path, not secondary path
   449  		project = secondaryProject
   450  	}
   451  
   452  	if err = project.Init(); err != nil {
   453  		return nil, errs.Wrap(err, "project.Init failed")
   454  	}
   455  
   456  	cfg, err := config.New()
   457  	if err != nil {
   458  		return nil, errs.Wrap(err, "Could not read configuration required by projectfile parser.")
   459  	}
   460  	defer rtutils.Closer(cfg.Close, &rerr)
   461  
   462  	namespace := fmt.Sprintf("%s/%s", project.parsedURL.Owner, project.parsedURL.Name)
   463  	StoreProjectMapping(cfg, namespace, filepath.Dir(project.Path()))
   464  
   465  	// Migrate project file if needed
   466  	if !migrationRunning && project.ConfigVersion != ConfigVersion && migrator != nil {
   467  		// Migrations may themselves utilize the projectfile package, so we have to ensure we don't start an infinite loop
   468  		migrationRunning = true
   469  		defer func() { migrationRunning = false }()
   470  
   471  		if project.ConfigVersion > ConfigVersion {
   472  			return nil, locale.NewInputError("err_projectfile_version_too_high")
   473  		}
   474  		updatedConfigVersion, errMigrate := migrator(project, ConfigVersion)
   475  
   476  		// Ensure we update the config version regardless of any error that occurred, because we don't want to repeat
   477  		// the same version migrations
   478  		project.ConfigVersion = updatedConfigVersion
   479  		if err := NewYamlField("config_version", ConfigVersion).Save(project.Path()); err != nil {
   480  			return nil, errs.Pack(errMigrate, errs.Wrap(err, "Could not save config_version"))
   481  		}
   482  
   483  		if errMigrate != nil {
   484  			return nil, errs.Wrap(errMigrate, "Migrator failed")
   485  		}
   486  	}
   487  
   488  	return project, nil
   489  }
   490  
   491  // Init initializes the parsedURL field from the project url string
   492  func (p *Project) Init() error {
   493  	parsedURL, err := p.parseURL()
   494  	if err != nil {
   495  		return locale.WrapInputError(err, "parse_project_file_url_err", "Could not parse project url: {{.V0}}.", p.Project)
   496  	}
   497  	p.parsedURL = parsedURL
   498  
   499  	// Ensure branch name is set
   500  	if p.parsedURL.Owner != "" && p.parsedURL.BranchName == "" {
   501  		logging.Debug("Appending default branch as none is set")
   502  		if err := p.SetBranch(constants.DefaultBranchName); err != nil {
   503  			return locale.WrapError(err, "err_set_default_branch", "", constants.DefaultBranchName)
   504  		}
   505  	}
   506  
   507  	if p.Lock != "" {
   508  		parsedLock, err := ParseLock(p.Lock)
   509  		if err != nil {
   510  			return errs.Wrap(err, "ParseLock %s failed", p.Lock)
   511  		}
   512  
   513  		p.parsedChannel = parsedLock.Channel
   514  		p.parsedVersion = parsedLock.Version
   515  	}
   516  
   517  	return nil
   518  }
   519  
   520  func parse(configFilepath string) (*Project, error) {
   521  	if !fileutils.FileExists(configFilepath) {
   522  		return nil, &ErrorNoProject{locale.NewInputError("err_no_projectfile")}
   523  	}
   524  
   525  	dat, err := os.ReadFile(configFilepath)
   526  	if err != nil {
   527  		return nil, errs.Wrap(err, "os.ReadFile %s failure", configFilepath)
   528  	}
   529  
   530  	return parseData(dat, configFilepath)
   531  }
   532  
   533  func parseData(dat []byte, configFilepath string) (*Project, error) {
   534  	if err := detectDeprecations(dat, configFilepath); err != nil {
   535  		return nil, errs.Wrap(err, "deprecations found")
   536  	}
   537  
   538  	project := Project{}
   539  	err2 := yaml.Unmarshal(dat, &project)
   540  	project.path = configFilepath
   541  
   542  	if err2 != nil {
   543  		return nil, &ErrorParseProject{locale.NewExternalError(
   544  			"err_project_parsed",
   545  			"Project file `{{.V1}}` could not be parsed, the parser produced the following error: {{.V0}}", err2.Error(), configFilepath),
   546  		}
   547  	}
   548  
   549  	return &project, nil
   550  }
   551  
   552  func detectDeprecations(dat []byte, configFilepath string) error {
   553  	deprecations := deprecatedRegex.FindAllIndex(dat, -1)
   554  	if len(deprecations) == 0 {
   555  		return nil
   556  	}
   557  	deplist := []string{}
   558  	for _, depIdxs := range deprecations {
   559  		dep := strings.TrimSpace(strings.TrimSuffix(string(dat[depIdxs[0]:depIdxs[1]]), ":"))
   560  		deplist = append(deplist, locale.Tr("pjfile_deprecation_entry", dep, strconv.Itoa(depIdxs[0])))
   561  	}
   562  	return &ErrorParseProject{locale.NewExternalError(
   563  		"pjfile_deprecation_msg",
   564  		"", configFilepath, strings.Join(deplist, "\n"), constants.DocumentationURL+"config/#deprecation"),
   565  	}
   566  }
   567  
   568  // URL returns the project namespace's string URL from activestate.yaml.
   569  func (p *Project) URL() string {
   570  	return p.Project
   571  }
   572  
   573  // Owner returns the project namespace's organization
   574  func (p *Project) Owner() string {
   575  	return p.parsedURL.Owner
   576  }
   577  
   578  // Name returns the project namespace's name
   579  func (p *Project) Name() string {
   580  	return p.parsedURL.Name
   581  }
   582  
   583  // BranchName returns the branch name specified in the project
   584  func (p *Project) BranchName() string {
   585  	return p.parsedURL.BranchName
   586  }
   587  
   588  // Path returns the project's activestate.yaml file path.
   589  func (p *Project) Path() string {
   590  	return p.path
   591  }
   592  
   593  // LegacyCommitID is for use by legacy mechanics ONLY
   594  // It returns a pre-migrated project's commit ID from activestate.yaml.
   595  func (p *Project) LegacyCommitID() string {
   596  	return p.parsedURL.LegacyCommitID
   597  }
   598  
   599  // SetLegacyCommit sets the commit id within the current project file. This is done
   600  // in-place so that line order is preserved.
   601  func (p *Project) SetLegacyCommit(commitID string) error {
   602  	pf := NewProjectField()
   603  	if err := pf.LoadProject(p.Project); err != nil {
   604  		return errs.Wrap(err, "Could not load activestate.yaml")
   605  	}
   606  	pf.SetLegacyCommitID(commitID)
   607  	if err := pf.Save(p.path); err != nil {
   608  		return errs.Wrap(err, "Could not save activestate.yaml")
   609  	}
   610  
   611  	p.parsedURL.LegacyCommitID = commitID
   612  	p.Project = pf.String()
   613  	return nil
   614  }
   615  
   616  func (p *Project) Dir() string {
   617  	return filepath.Dir(p.path)
   618  }
   619  
   620  // SetPath sets the path of the project file and should generally only be used by tests
   621  func (p *Project) SetPath(path string) {
   622  	p.path = path
   623  }
   624  
   625  // Channel returns the channel as it was interpreted from the lock
   626  func (p *Project) Channel() string {
   627  	return p.parsedChannel
   628  }
   629  
   630  // Version returns the version as it was interpreted from the lock
   631  func (p *Project) Version() string {
   632  	return p.parsedVersion
   633  }
   634  
   635  // ValidateProjectURL validates the configured project URL
   636  func ValidateProjectURL(url string) error {
   637  	// Note: This line also matches headless commit URLs: match == {'commit', '<commit_id>'}
   638  	match := ProjectURLRe.FindStringSubmatch(url)
   639  	if len(match) < 3 {
   640  		return &ErrorParseProject{locale.NewError("err_bad_project_url")}
   641  	}
   642  	return nil
   643  }
   644  
   645  // Reload the project file from disk
   646  func (p *Project) Reload() error {
   647  	pj, err := Parse(p.path)
   648  	if err != nil {
   649  		return err
   650  	}
   651  	*p = *pj
   652  	return nil
   653  }
   654  
   655  // Save the project to its activestate.yaml file
   656  func (p *Project) Save(cfg ConfigGetter) error {
   657  	return p.save(cfg, p.Path())
   658  }
   659  
   660  // parseURL returns the parsed fields of a Project URL
   661  func (p *Project) parseURL() (projectURL, error) {
   662  	return parseURL(p.Project)
   663  }
   664  
   665  func validateUUID(uuidStr string) error {
   666  	if ok := strfmt.Default.Validates("uuid", uuidStr); !ok {
   667  		return locale.NewError("err_commit_id_invalid", "", uuidStr)
   668  	}
   669  
   670  	var uuid strfmt.UUID
   671  	if err := uuid.UnmarshalText([]byte(uuidStr)); err != nil {
   672  		return locale.WrapError(err, "err_commit_id_unmarshal", "Failed to unmarshal the commit id {{.V0}} read from activestate.yaml.", uuidStr)
   673  	}
   674  
   675  	return nil
   676  }
   677  
   678  func parseURL(rawURL string) (projectURL, error) {
   679  	p := projectURL{}
   680  
   681  	err := ValidateProjectURL(rawURL)
   682  	if err != nil {
   683  		return p, err
   684  	}
   685  
   686  	u, err := url.Parse(rawURL)
   687  	if err != nil {
   688  		return p, errs.Wrap(err, "Could not parse URL")
   689  	}
   690  
   691  	path := strings.Split(u.Path, "/")
   692  	if len(path) > 2 {
   693  		if path[1] == "commit" {
   694  			p.LegacyCommitID = path[2]
   695  		} else {
   696  			p.Owner = path[1]
   697  			p.Name = path[2]
   698  		}
   699  	}
   700  
   701  	q := u.Query()
   702  	if c := q.Get("commitID"); c != "" {
   703  		p.LegacyCommitID = c
   704  	}
   705  
   706  	if p.LegacyCommitID != "" {
   707  		if err := validateUUID(p.LegacyCommitID); err != nil {
   708  			return p, err
   709  		}
   710  	}
   711  
   712  	if b := q.Get("branch"); b != "" {
   713  		p.BranchName = b
   714  	}
   715  
   716  	return p, nil
   717  }
   718  
   719  // Save the project to its activestate.yaml file
   720  func (p *Project) save(cfg ConfigGetter, path string) error {
   721  	dat, err := yaml.Marshal(p)
   722  	if err != nil {
   723  		return errs.Wrap(err, "yaml.Marshal failed")
   724  	}
   725  
   726  	err = ValidateProjectURL(p.Project)
   727  	if err != nil {
   728  		return errs.Wrap(err, "ValidateProjectURL failed")
   729  	}
   730  
   731  	logging.Debug("Saving %s", path)
   732  
   733  	f, err := os.Create(path)
   734  	if err != nil {
   735  		return errs.Wrap(err, "os.Create %s failed", path)
   736  	}
   737  	defer f.Close()
   738  
   739  	_, err = f.Write([]byte(dat))
   740  	if err != nil {
   741  		return errs.Wrap(err, "f.Write %s failed", path)
   742  	}
   743  
   744  	if cfg != nil {
   745  		StoreProjectMapping(cfg, fmt.Sprintf("%s/%s", p.parsedURL.Owner, p.parsedURL.Name), filepath.Dir(p.Path()))
   746  	}
   747  
   748  	return nil
   749  }
   750  
   751  // SetNamespace updates the namespace in the project file
   752  func (p *Project) SetNamespace(owner, project string) error {
   753  	pf := NewProjectField()
   754  	if err := pf.LoadProject(p.Project); err != nil {
   755  		return errs.Wrap(err, "Could not load activestate.yaml")
   756  	}
   757  	pf.SetNamespace(owner, project)
   758  	if err := pf.Save(p.path); err != nil {
   759  		return errs.Wrap(err, "Could not save activestate.yaml")
   760  	}
   761  
   762  	// keep parsed url components in sync
   763  	p.parsedURL.Owner = owner
   764  	p.parsedURL.Name = project
   765  	p.Project = pf.String()
   766  
   767  	return nil
   768  }
   769  
   770  // SetBranch sets the branch within the current project file. This is done
   771  // in-place so that line order is preserved.
   772  func (p *Project) SetBranch(branch string) error {
   773  	pf := NewProjectField()
   774  
   775  	if err := pf.LoadProject(p.Project); err != nil {
   776  		return errs.Wrap(err, "Could not load activestate.yaml")
   777  	}
   778  
   779  	pf.SetBranch(branch)
   780  
   781  	if !condition.InUnitTest() || p.path != "" {
   782  		if err := pf.Save(p.path); err != nil {
   783  			return errs.Wrap(err, "Could not save activestate.yaml")
   784  		}
   785  	}
   786  
   787  	p.parsedURL.BranchName = branch
   788  	p.Project = pf.String()
   789  	return nil
   790  }
   791  
   792  // GetProjectFilePath returns the path to the project activestate.yaml
   793  // It considers projects in the following order:
   794  // 1. Environment variable (e.g. `state shell` sets one)
   795  // 2. Working directory (i.e. walk up directory tree looking for activestate.yaml)
   796  // 3. Fall back on default project
   797  func GetProjectFilePath() (string, error) {
   798  	defer profile.Measure("GetProjectFilePath", time.Now())
   799  	lookup := []func() (string, error){
   800  		getProjectFilePathFromEnv,
   801  		getProjectFilePathFromWd,
   802  		getProjectFilePathFromDefault,
   803  	}
   804  	for _, getProjectFilePath := range lookup {
   805  		path, err := getProjectFilePath()
   806  		if err != nil {
   807  			return "", errs.Wrap(err, "getProjectFilePath failed")
   808  		}
   809  		if path != "" {
   810  			return path, nil
   811  		}
   812  	}
   813  
   814  	return "", &ErrorNoProject{locale.NewInputError("err_no_projectfile")}
   815  }
   816  
   817  func getProjectFilePathFromEnv() (string, error) {
   818  	var projectFilePath string
   819  
   820  	if activatedProjectDirPath := os.Getenv(constants.ActivatedStateEnvVarName); activatedProjectDirPath != "" {
   821  		projectFilePath = filepath.Join(activatedProjectDirPath, constants.ConfigFileName)
   822  	} else {
   823  		projectFilePath = os.Getenv(constants.ProjectEnvVarName)
   824  	}
   825  
   826  	if projectFilePath != "" {
   827  		if fileutils.FileExists(projectFilePath) {
   828  			return projectFilePath, nil
   829  		}
   830  		return "", &ErrorNoProjectFromEnv{locale.NewInputError("err_project_env_file_not_exist", "", projectFilePath)}
   831  	}
   832  
   833  	return "", nil
   834  }
   835  
   836  func getProjectFilePathFromWd() (string, error) {
   837  	root, err := osutils.Getwd()
   838  	if err != nil {
   839  		return "", errs.Wrap(err, "osutils.Getwd failed")
   840  	}
   841  
   842  	path, err := fileutils.FindFileInPath(root, constants.ConfigFileName)
   843  	if err != nil && !errors.Is(err, fileutils.ErrorFileNotFound) {
   844  		return "", errs.Wrap(err, "fileutils.FindFileInPath %s failed", root)
   845  	}
   846  
   847  	return path, nil
   848  }
   849  
   850  func getProjectFilePathFromDefault() (_ string, rerr error) {
   851  	cfg, err := config.New()
   852  	if err != nil {
   853  		return "", errs.Wrap(err, "Could not read configuration required to determine which project to use")
   854  	}
   855  	defer rtutils.Closer(cfg.Close, &rerr)
   856  
   857  	defaultProjectPath := cfg.GetString(constants.GlobalDefaultPrefname)
   858  	if defaultProjectPath == "" {
   859  		return "", nil
   860  	}
   861  
   862  	path, err := fileutils.FindFileInPath(defaultProjectPath, constants.ConfigFileName)
   863  	if err != nil {
   864  		if !errors.Is(err, fileutils.ErrorFileNotFound) {
   865  			return "", errs.Wrap(err, "fileutils.FindFileInPath %s failed", defaultProjectPath)
   866  		}
   867  		return "", &ErrorNoDefaultProject{locale.NewInputError("err_no_default_project", "Could not find your project at: [ACTIONABLE]{{.V0}}[/RESET]", defaultProjectPath)}
   868  	}
   869  	return path, nil
   870  }
   871  
   872  // GetPersisted gets the persisted project, if any
   873  func GetPersisted() *Project {
   874  	return persistentProject
   875  }
   876  
   877  func Get() (*Project, error) {
   878  	if persistentProject != nil {
   879  		return persistentProject, nil
   880  	}
   881  
   882  	project, err := GetOnce()
   883  	if err != nil {
   884  		return nil, err
   885  	}
   886  
   887  	err = project.Persist()
   888  	if err != nil {
   889  		return nil, err
   890  	}
   891  	return project, nil
   892  }
   893  
   894  // GetOnce returns the project configuration in a safe manner (returns error), the same as GetSafe, but it avoids persisting the project
   895  func GetOnce() (*Project, error) {
   896  	// we do not want to use a path provided by state if we're running tests
   897  	projectFilePath, err := GetProjectFilePath()
   898  	if err != nil {
   899  		if errors.Is(err, fileutils.ErrorFileNotFound) {
   900  			return nil, &ErrorNoProject{locale.WrapError(err, "err_project_file_notfound", "Could not detect project file path.")}
   901  		}
   902  		return nil, err
   903  	}
   904  
   905  	project, err := Parse(projectFilePath)
   906  	if err != nil {
   907  		return nil, errs.Wrap(err, "Could not parse projectfile")
   908  	}
   909  
   910  	return project, nil
   911  }
   912  
   913  // FromPath will return the projectfile that's located at the given path (this will walk up the directory tree until it finds the project)
   914  func FromPath(path string) (*Project, error) {
   915  	defer profile.Measure("projectfile:FromPath", time.Now())
   916  	// we do not want to use a path provided by state if we're running tests
   917  	projectFilePath, err := fileutils.FindFileInPath(path, constants.ConfigFileName)
   918  	if err != nil {
   919  		return nil, &ErrorNoProject{locale.WrapInputError(err, "err_project_not_found", "", path)}
   920  	}
   921  
   922  	_, err = os.ReadFile(projectFilePath)
   923  	if err != nil {
   924  		logging.Warning("Cannot load config file: %v", err)
   925  		return nil, &ErrorNoProject{locale.WrapInputError(err, "err_no_projectfile")}
   926  	}
   927  	project, err := Parse(projectFilePath)
   928  	if err != nil {
   929  		return nil, errs.Wrap(err, "Could not parse projectfile")
   930  	}
   931  
   932  	return project, nil
   933  }
   934  
   935  // FromExactPath will return the projectfile that's located at the given path without walking up the directory tree
   936  func FromExactPath(path string) (*Project, error) {
   937  	// we do not want to use a path provided by state if we're running tests
   938  	projectFilePath := filepath.Join(path, constants.ConfigFileName)
   939  
   940  	if !fileutils.FileExists(projectFilePath) {
   941  		return nil, &ErrorNoProject{locale.NewInputError("err_no_projectfile")}
   942  	}
   943  
   944  	_, err := os.ReadFile(projectFilePath)
   945  	if err != nil {
   946  		logging.Warning("Cannot load config file: %v", err)
   947  		return nil, &ErrorNoProject{locale.WrapInputError(err, "err_no_projectfile")}
   948  	}
   949  	project, err := Parse(projectFilePath)
   950  	if err != nil {
   951  		return nil, errs.Wrap(err, "Could not parse projectfile")
   952  	}
   953  
   954  	return project, nil
   955  }
   956  
   957  // CreateParams are parameters that we create a custom activestate.yaml file from
   958  type CreateParams struct {
   959  	Owner      string
   960  	Project    string
   961  	BranchName string
   962  	Directory  string
   963  	Content    string
   964  	Language   string
   965  	Private    bool
   966  	path       string
   967  	ProjectURL string
   968  	Cache      string
   969  }
   970  
   971  // Create will create a new activestate.yaml with a projectURL for the given details
   972  func Create(params *CreateParams) (*Project, error) {
   973  	lang := language.MakeByName(params.Language)
   974  	err := validateCreateParams(params)
   975  	if err != nil {
   976  		return nil, err
   977  	}
   978  
   979  	return createCustom(params, lang)
   980  }
   981  
   982  func createCustom(params *CreateParams, lang language.Language) (*Project, error) {
   983  	err := fileutils.MkdirUnlessExists(params.Directory)
   984  	if err != nil {
   985  		return nil, err
   986  	}
   987  
   988  	if params.ProjectURL == "" {
   989  		// Note: cannot use api.GetPlatformURL() due to import cycle.
   990  		host := constants.DefaultAPIHost
   991  		if hostOverride := os.Getenv(constants.APIHostEnvVarName); hostOverride != "" {
   992  			host = hostOverride
   993  		}
   994  		u, err := url.Parse(fmt.Sprintf("https://%s/%s/%s", host, params.Owner, params.Project))
   995  		if err != nil {
   996  			return nil, errs.Wrap(err, "url parse new project url failed")
   997  		}
   998  		q := u.Query()
   999  
  1000  		if params.BranchName != "" {
  1001  			q.Set("branch", params.BranchName)
  1002  		}
  1003  
  1004  		u.RawQuery = q.Encode()
  1005  		params.ProjectURL = u.String()
  1006  	}
  1007  
  1008  	params.path = filepath.Join(params.Directory, constants.ConfigFileName)
  1009  	if fileutils.FileExists(params.path) {
  1010  		return nil, locale.NewInputError("err_projectfile_exists")
  1011  	}
  1012  
  1013  	err = ValidateProjectURL(params.ProjectURL)
  1014  	if err != nil {
  1015  		return nil, err
  1016  	}
  1017  	match := ProjectURLRe.FindStringSubmatch(params.ProjectURL)
  1018  	if len(match) < 3 {
  1019  		return nil, locale.NewInputError("err_projectfile_invalid_url")
  1020  	}
  1021  	owner, project := match[1], match[2]
  1022  
  1023  	shell := "bash"
  1024  	if runtime.GOOS == "windows" {
  1025  		shell = "batch"
  1026  	}
  1027  
  1028  	languageDisabled := os.Getenv(constants.DisableLanguageTemplates) == "true"
  1029  	content := params.Content
  1030  	if !languageDisabled && content == "" && lang != language.Unset && lang != language.Unknown {
  1031  		tplName := "activestate.yaml." + strings.TrimRight(lang.String(), "23") + ".tpl"
  1032  		template, err := assets.ReadFileBytes(tplName)
  1033  		if err != nil {
  1034  			return nil, errs.Wrap(err, "Could not read asset")
  1035  		}
  1036  		content, err = strutils.ParseTemplate(
  1037  			string(template),
  1038  			map[string]interface{}{"Owner": owner, "Project": project, "Shell": shell, "Language": lang.String(), "LangExe": lang.Executable().Filename()},
  1039  			nil)
  1040  		if err != nil {
  1041  			return nil, errs.Wrap(err, "Could not parse %s", tplName)
  1042  		}
  1043  	}
  1044  
  1045  	data := map[string]interface{}{
  1046  		"Project":       params.ProjectURL,
  1047  		"Content":       content,
  1048  		"Private":       params.Private,
  1049  		"ConfigVersion": ConfigVersion,
  1050  	}
  1051  
  1052  	tplName := "activestate.yaml.tpl"
  1053  	tplContents, err := assets.ReadFileBytes(tplName)
  1054  	if err != nil {
  1055  		return nil, errs.Wrap(err, "Could not read asset")
  1056  	}
  1057  	fileContents, err := strutils.ParseTemplate(string(tplContents), data, nil)
  1058  	if err != nil {
  1059  		return nil, errs.Wrap(err, "Could not parse %s", tplName)
  1060  	}
  1061  
  1062  	err = fileutils.WriteFile(params.path, []byte(fileContents))
  1063  	if err != nil {
  1064  		return nil, err
  1065  	}
  1066  
  1067  	if params.Cache != "" {
  1068  		createErr := createHostFile(params.Directory, params.Cache)
  1069  		if createErr != nil {
  1070  			return nil, errs.Wrap(createErr, "Could not create cache file")
  1071  		}
  1072  	}
  1073  
  1074  	return Parse(params.path)
  1075  }
  1076  
  1077  func createHostFile(filePath, cachePath string) error {
  1078  	user, err := user.Current()
  1079  	if err != nil {
  1080  		return errs.Wrap(err, "Could not get current user")
  1081  	}
  1082  
  1083  	data := map[string]interface{}{
  1084  		"Cache": cachePath,
  1085  	}
  1086  
  1087  	tplName := "activestate.yaml.cache.tpl"
  1088  	tplContents, err := assets.ReadFileBytes(tplName)
  1089  	if err != nil {
  1090  		return errs.Wrap(err, "Could not read asset")
  1091  	}
  1092  
  1093  	fileContents, err := strutils.ParseTemplate(string(tplContents), data, nil)
  1094  	if err != nil {
  1095  		return errs.Wrap(err, "Could not parse %s", tplName)
  1096  	}
  1097  
  1098  	// Trim any non-alphanumeric characters from the username
  1099  	if err := fileutils.WriteFile(filepath.Join(filePath, fmt.Sprintf("activestate.%s.yaml", nonAlphanumericRegex.ReplaceAllString(user.Username, ""))), []byte(fileContents)); err != nil {
  1100  		return errs.Wrap(err, "Could not write cache file")
  1101  	}
  1102  
  1103  	return nil
  1104  }
  1105  
  1106  func validateCreateParams(params *CreateParams) error {
  1107  	switch {
  1108  	case params.Directory == "":
  1109  		return locale.NewInputError("err_project_require_path")
  1110  	case params.ProjectURL != "":
  1111  		return nil // Owner and Project not required when projectURL is set
  1112  	case params.Owner == "":
  1113  		return locale.NewInputError("err_project_require_owner")
  1114  	case params.Project == "":
  1115  		return locale.NewInputError("err_project_require_name")
  1116  	default:
  1117  		return nil
  1118  	}
  1119  }
  1120  
  1121  // ParseVersionInfo parses the lock field from the projectfile and updates
  1122  // the activestate.yaml if an older version representation is present
  1123  func ParseVersionInfo(projectFilePath string) (*VersionInfo, error) {
  1124  	if !fileutils.FileExists(projectFilePath) {
  1125  		return nil, nil
  1126  	}
  1127  
  1128  	dat, err := os.ReadFile(projectFilePath)
  1129  	if err != nil {
  1130  		return nil, errs.Wrap(err, "os.ReadFile %s failed", projectFilePath)
  1131  	}
  1132  
  1133  	versionStruct := VersionInfo{}
  1134  	err = yaml.Unmarshal(dat, &versionStruct)
  1135  	if err != nil {
  1136  		return nil, &ErrorParseProject{locale.WrapError(err, "Could not unmarshal activestate.yaml")}
  1137  	}
  1138  
  1139  	if versionStruct.Lock == "" {
  1140  		return nil, nil
  1141  	}
  1142  
  1143  	return ParseLock(versionStruct.Lock)
  1144  }
  1145  
  1146  func ParseLock(lock string) (*VersionInfo, error) {
  1147  	split := strings.Split(lock, "@")
  1148  	if len(split) != 2 {
  1149  		return nil, locale.NewInputError("err_invalid_lock", "", lock)
  1150  	}
  1151  
  1152  	return &VersionInfo{
  1153  		Channel: split[0],
  1154  		Version: split[1],
  1155  		Lock:    lock,
  1156  	}, nil
  1157  }
  1158  
  1159  // AddLockInfo adds the lock field to activestate.yaml
  1160  func AddLockInfo(projectFilePath, branch, version string) error {
  1161  	data, err := cleanVersionInfo(projectFilePath)
  1162  	if err != nil {
  1163  		return locale.WrapError(err, "err_clean_projectfile", "Could not remove old version information from projectfile", projectFilePath)
  1164  	}
  1165  
  1166  	lockRegex := regexp.MustCompile(`(?m)^lock:.*`)
  1167  	if lockRegex.Match(data) {
  1168  		versionUpdate := []byte(fmt.Sprintf("lock: %s@%s", branch, version))
  1169  		replaced := lockRegex.ReplaceAll(data, versionUpdate)
  1170  		return os.WriteFile(projectFilePath, replaced, 0644)
  1171  	}
  1172  
  1173  	projectRegex := regexp.MustCompile(fmt.Sprintf("(?m:(^project:\\s*%s))", ProjectURLRe))
  1174  	lockString := fmt.Sprintf("%s@%s", branch, version)
  1175  	lockUpdate := []byte(fmt.Sprintf("${1}\nlock: %s", lockString))
  1176  
  1177  	data, err = os.ReadFile(projectFilePath)
  1178  	if err != nil {
  1179  		return err
  1180  	}
  1181  
  1182  	updated := projectRegex.ReplaceAll(data, lockUpdate)
  1183  
  1184  	return os.WriteFile(projectFilePath, updated, 0644)
  1185  }
  1186  
  1187  func RemoveLockInfo(projectFilePath string) error {
  1188  	data, err := os.ReadFile(projectFilePath)
  1189  	if err != nil {
  1190  		return locale.WrapError(err, "err_read_projectfile", "", projectFilePath)
  1191  	}
  1192  
  1193  	lockRegex := regexp.MustCompile(`(?m)^lock:.*`)
  1194  	clean := lockRegex.ReplaceAll(data, []byte(""))
  1195  
  1196  	err = os.WriteFile(projectFilePath, clean, 0644)
  1197  	if err != nil {
  1198  		return locale.WrapError(err, "err_write_unlocked_projectfile", "Could not remove lock from projectfile")
  1199  	}
  1200  
  1201  	return nil
  1202  }
  1203  
  1204  func cleanVersionInfo(projectFilePath string) ([]byte, error) {
  1205  	data, err := os.ReadFile(projectFilePath)
  1206  	if err != nil {
  1207  		return nil, locale.WrapError(err, "err_read_projectfile", "", projectFilePath)
  1208  	}
  1209  
  1210  	branchRegex := regexp.MustCompile(`(?m:^branch:\s*\w+\n)`)
  1211  	clean := branchRegex.ReplaceAll(data, []byte(""))
  1212  
  1213  	versionRegex := regexp.MustCompile(`(?m:^version:\s*\d+.\d+.\d+-[A-Za-z0-9]+\n)`)
  1214  	clean = versionRegex.ReplaceAll(clean, []byte(""))
  1215  
  1216  	err = os.WriteFile(projectFilePath, clean, 0644)
  1217  	if err != nil {
  1218  		return nil, locale.WrapError(err, "err_write_clean_projectfile", "Could not write cleaned projectfile information")
  1219  	}
  1220  
  1221  	return clean, nil
  1222  }
  1223  
  1224  // Reset the current state, which unsets the persistent project
  1225  func Reset() {
  1226  	persistentProject = nil
  1227  	os.Unsetenv(constants.ProjectEnvVarName)
  1228  }
  1229  
  1230  // Persist "activates" the given project and makes it such that subsequent calls
  1231  // to Get() return this project.
  1232  // Only one project can persist at a time.
  1233  func (p *Project) Persist() error {
  1234  	if p.Project == "" {
  1235  		return locale.NewError(locale.T("err_invalid_project"))
  1236  	}
  1237  	persistentProject = p
  1238  	os.Setenv(constants.ProjectEnvVarName, p.Path())
  1239  	return nil
  1240  }
  1241  
  1242  type ConfigGetter interface {
  1243  	GetStringMapStringSlice(key string) map[string][]string
  1244  	AllKeys() []string
  1245  	GetStringSlice(string) []string
  1246  	GetString(string) string
  1247  	Set(string, interface{}) error
  1248  	GetThenSet(string, func(interface{}) (interface{}, error)) error
  1249  	Close() error
  1250  }
  1251  
  1252  func GetProjectMapping(config ConfigGetter) map[string][]string {
  1253  	addDeprecatedProjectMappings(config)
  1254  	CleanProjectMapping(config)
  1255  	projects := config.GetStringMapStringSlice(LocalProjectsConfigKey)
  1256  	if projects == nil {
  1257  		return map[string][]string{}
  1258  	}
  1259  	return projects
  1260  }
  1261  
  1262  // GetStaleProjectMapping returns a project mapping from the last time the
  1263  // state tool was run. This mapping could include projects that are no longer
  1264  // on the system.
  1265  func GetStaleProjectMapping(config ConfigGetter) map[string][]string {
  1266  	addDeprecatedProjectMappings(config)
  1267  	projects := config.GetStringMapStringSlice(LocalProjectsConfigKey)
  1268  	if projects == nil {
  1269  		return map[string][]string{}
  1270  	}
  1271  	return projects
  1272  }
  1273  
  1274  func GetProjectFileMapping(config ConfigGetter) map[string][]*Project {
  1275  	projects := GetProjectMapping(config)
  1276  
  1277  	res := make(map[string][]*Project)
  1278  	for name, paths := range projects {
  1279  		if name == "/" {
  1280  			continue
  1281  		}
  1282  		var pFiles []*Project
  1283  		for _, path := range paths {
  1284  			prj, err := FromExactPath(path)
  1285  			if err != nil {
  1286  				multilog.Error("Could not read project file at %s: %v", path, err)
  1287  				continue
  1288  			}
  1289  			pFiles = append(pFiles, prj)
  1290  		}
  1291  		if len(pFiles) > 0 {
  1292  			res[name] = pFiles
  1293  		}
  1294  	}
  1295  	return res
  1296  }
  1297  
  1298  func GetCachedProjectNameForPath(config ConfigGetter, projectPath string) string {
  1299  	projects := GetProjectMapping(config)
  1300  
  1301  	for name, paths := range projects {
  1302  		if name == "/" {
  1303  			continue
  1304  		}
  1305  		for _, path := range paths {
  1306  			if isEqual, err := fileutils.PathsEqual(projectPath, path); isEqual {
  1307  				if err != nil {
  1308  					logging.Debug("Failed to compare paths %s and %s", projectPath, path)
  1309  				}
  1310  				return name
  1311  			}
  1312  		}
  1313  	}
  1314  	return ""
  1315  }
  1316  
  1317  func addDeprecatedProjectMappings(cfg ConfigGetter) {
  1318  	var unsets []string
  1319  
  1320  	err := cfg.GetThenSet(
  1321  		LocalProjectsConfigKey,
  1322  		func(v interface{}) (interface{}, error) {
  1323  			projects, err := cast.ToStringMapStringSliceE(v)
  1324  			if err != nil && v != nil { // don't report if error due to nil input
  1325  				multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Projects data in config is abnormal (type: %T)", v)
  1326  			}
  1327  
  1328  			keys := funk.FilterString(cfg.AllKeys(), func(v string) bool {
  1329  				return strings.HasPrefix(v, "project_")
  1330  			})
  1331  
  1332  			for _, key := range keys {
  1333  				namespace := strings.TrimPrefix(key, "project_")
  1334  				newPaths := projects[namespace]
  1335  				paths := cfg.GetStringSlice(key)
  1336  				projects[namespace] = funk.UniqString(append(newPaths, paths...))
  1337  				unsets = append(unsets, key)
  1338  			}
  1339  
  1340  			return projects, nil
  1341  		},
  1342  	)
  1343  	if err != nil {
  1344  		multilog.Error("Could not update project mapping in config, error: %v", err)
  1345  	}
  1346  	for _, unset := range unsets {
  1347  		if err := cfg.Set(unset, nil); err != nil {
  1348  			multilog.Error("Could not clear config entry for key %s, error: %v", unset, err)
  1349  		}
  1350  	}
  1351  
  1352  }
  1353  
  1354  // GetProjectPaths returns the paths of all projects associated with the namespace
  1355  func GetProjectPaths(cfg ConfigGetter, namespace string) []string {
  1356  	projects := GetProjectMapping(cfg)
  1357  
  1358  	// match case-insensitively
  1359  	var paths []string
  1360  	for key, value := range projects {
  1361  		if strings.EqualFold(key, namespace) {
  1362  			paths = append(paths, value...)
  1363  		}
  1364  	}
  1365  
  1366  	return paths
  1367  }
  1368  
  1369  // StoreProjectMapping associates the namespace with the project
  1370  // path in the config
  1371  func StoreProjectMapping(cfg ConfigGetter, namespace, projectPath string) {
  1372  	SetRecentlyUsedNamespace(cfg, namespace)
  1373  	err := cfg.GetThenSet(
  1374  		LocalProjectsConfigKey,
  1375  		func(v interface{}) (interface{}, error) {
  1376  			projects, err := cast.ToStringMapStringSliceE(v)
  1377  			if err != nil && v != nil { // don't report if error due to nil input
  1378  				multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Projects data in config is abnormal (type: %T)", v)
  1379  			}
  1380  
  1381  			projectPath, err = fileutils.ResolveUniquePath(projectPath)
  1382  			if err != nil {
  1383  				multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Could not resolve uniqe project path, %v", err)
  1384  				projectPath = filepath.Clean(projectPath)
  1385  			}
  1386  
  1387  			for name, paths := range projects {
  1388  				for i, path := range paths {
  1389  					path, err = fileutils.ResolveUniquePath(path)
  1390  					if err != nil {
  1391  						multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Could not resolve unique path, :%v", err)
  1392  						path = filepath.Clean(path)
  1393  					}
  1394  
  1395  					if path == projectPath {
  1396  						projects[name] = sliceutils.RemoveFromStrings(projects[name], i)
  1397  					}
  1398  
  1399  					if len(projects[name]) == 0 {
  1400  						delete(projects, name)
  1401  					}
  1402  				}
  1403  			}
  1404  
  1405  			paths := projects[namespace]
  1406  			if paths == nil {
  1407  				paths = make([]string, 0)
  1408  			}
  1409  
  1410  			if !funk.Contains(paths, projectPath) {
  1411  				paths = append(paths, projectPath)
  1412  			}
  1413  
  1414  			projects[namespace] = paths
  1415  
  1416  			return projects, nil
  1417  		},
  1418  	)
  1419  	if err != nil {
  1420  		multilog.Error("Could not set project mapping in config, error: %v", errs.JoinMessage(err))
  1421  	}
  1422  }
  1423  
  1424  // CleanProjectMapping removes projects that no longer exist
  1425  // on a user's filesystem from the projects config entry
  1426  func CleanProjectMapping(cfg ConfigGetter) {
  1427  	err := cfg.GetThenSet(
  1428  		LocalProjectsConfigKey,
  1429  		func(v interface{}) (interface{}, error) {
  1430  			projects, err := cast.ToStringMapStringSliceE(v)
  1431  			if err != nil && v != nil { // don't report if error due to nil input
  1432  				multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Projects data in config is abnormal (type: %T)", v)
  1433  			}
  1434  
  1435  			seen := make(map[string]struct{})
  1436  
  1437  			for namespace, paths := range projects {
  1438  				var removals []int
  1439  				for i, path := range paths {
  1440  					configFile := filepath.Join(path, constants.ConfigFileName)
  1441  					if !fileutils.DirExists(path) || !fileutils.FileExists(configFile) {
  1442  						removals = append(removals, i)
  1443  						continue
  1444  					}
  1445  					// Only remove the project if the activestate.yaml is parseable and there is a namespace
  1446  					// mismatch.
  1447  					// (We do not want to punish anyone for a syntax error when manually editing the file.)
  1448  					if proj, err := parse(configFile); err == nil && proj.Init() == nil {
  1449  						projNamespace := fmt.Sprintf("%s/%s", proj.Owner(), proj.Name())
  1450  						if namespace != projNamespace {
  1451  							removals = append(removals, i)
  1452  						}
  1453  					}
  1454  				}
  1455  
  1456  				projects[namespace] = sliceutils.RemoveFromStrings(projects[namespace], removals...)
  1457  				if _, ok := seen[strings.ToLower(namespace)]; ok || len(projects[namespace]) == 0 {
  1458  					delete(projects, namespace)
  1459  					continue
  1460  				}
  1461  				seen[strings.ToLower(namespace)] = struct{}{}
  1462  			}
  1463  
  1464  			return projects, nil
  1465  		},
  1466  	)
  1467  	if err != nil {
  1468  		logging.Debug("Could not clean project mapping in config, error: %v", err)
  1469  	}
  1470  }
  1471  
  1472  func SetRecentlyUsedNamespace(cfg ConfigGetter, namespace string) {
  1473  	err := cfg.Set(constants.LastUsedNamespacePrefname, namespace)
  1474  	if err != nil {
  1475  		logging.Debug("Could not set recently used namespace in config, error: %v", err)
  1476  	}
  1477  }