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

     1  package model
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/ActiveState/cli/internal/errs"
    10  	"github.com/ActiveState/cli/internal/locale"
    11  	"github.com/ActiveState/cli/internal/logging"
    12  	"github.com/ActiveState/cli/internal/multilog"
    13  	"github.com/ActiveState/cli/pkg/platform/api"
    14  	"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
    15  	gqlModel "github.com/ActiveState/cli/pkg/platform/api/graphql/model"
    16  	"github.com/ActiveState/cli/pkg/platform/api/mediator/model"
    17  	"github.com/ActiveState/cli/pkg/platform/api/mono"
    18  	"github.com/ActiveState/cli/pkg/platform/api/mono/mono_client/version_control"
    19  	vcsClient "github.com/ActiveState/cli/pkg/platform/api/mono/mono_client/version_control"
    20  	"github.com/ActiveState/cli/pkg/platform/api/mono/mono_models"
    21  	"github.com/ActiveState/cli/pkg/platform/authentication"
    22  	bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner"
    23  	"github.com/go-openapi/strfmt"
    24  )
    25  
    26  var (
    27  	ErrCommitCountUnknowable = errs.New("Commit count is unknowable")
    28  
    29  	ErrMergeFastForward = errs.New("No merge required")
    30  
    31  	ErrMergeCommitInHistory = errs.New("Can't merge commit thats already in target commits history")
    32  
    33  	ErrCommitNotInHistory = errs.New("Target commit is not in history")
    34  )
    35  
    36  var ErrOrderForbidden = errs.New("no permission to retrieve order")
    37  
    38  type ErrUpdateBranchAuth struct{ *locale.LocalizedError }
    39  
    40  type ProjectInfo interface {
    41  	Owner() string
    42  	Name() string
    43  	CommitUUID() strfmt.UUID
    44  	BranchName() string
    45  }
    46  
    47  // Operation is the action to be taken in a commit
    48  type Operation string
    49  
    50  const (
    51  	// OperationAdded is for adding a new requirement
    52  	OperationAdded = Operation(mono_models.CommitChangeEditableOperationAdded)
    53  	// OperationUpdated is for updating an existing requirement
    54  	OperationUpdated = Operation(mono_models.CommitChangeEditableOperationUpdated)
    55  	// OperationRemoved is for removing an existing requirement
    56  	OperationRemoved = Operation(mono_models.CommitChangeEditableOperationRemoved)
    57  )
    58  
    59  // NamespaceMatchable represents regular expression strings used for defining matchable
    60  // requirements.
    61  type NamespaceMatchable string
    62  
    63  const (
    64  	// NamespacePlatformMatch is the namespace used for platform requirements
    65  	NamespacePlatformMatch NamespaceMatchable = `^platform$`
    66  
    67  	// NamespaceLanguageMatch is the namespace used for language requirements
    68  	NamespaceLanguageMatch = `^language$`
    69  
    70  	// NamespacePackageMatch is the namespace used for language package requirements
    71  	NamespacePackageMatch = `^language\/(\w+)$`
    72  
    73  	// NamespacePackageMatch is the namespace used for language package requirements
    74  	NamespaceBuilderMatch = `^builder(-lib){0,1}$`
    75  
    76  	// NamespaceBundlesMatch is the namespace used for bundle package requirements
    77  	NamespaceBundlesMatch = `^bundles\/(\w+)$`
    78  
    79  	// NamespacePrePlatformMatch is the namespace used for pre-platform bits
    80  	NamespacePrePlatformMatch = `^pre-platform-installer$`
    81  
    82  	// NamespaceCamelFlagsMatch is the namespace used for passing camel flags
    83  	NamespaceCamelFlagsMatch = `^camel-flags$`
    84  
    85  	// NamespaceOrgMatch is the namespace used for org specific requirements
    86  	NamespaceOrgMatch = `^org\/`
    87  
    88  	// NamespaceBuildFlagsMatch is the namespace used for passing build flags
    89  	NamespaceBuildFlagsMatch = `^build-flags$`
    90  )
    91  
    92  type TrackingType string
    93  
    94  const (
    95  	// TrackingNotify represents the notify tracking type for branches and will
    96  	// notify the project owner of upstream changes
    97  	TrackingNotify TrackingType = TrackingType(mono_models.BranchEditableTrackingTypeNotify)
    98  	// TrackingIgnore represents the ignore tracking type for branches and will
    99  	// ignore upstream changes
   100  	TrackingIgnore = TrackingType(mono_models.BranchEditableTrackingTypeIgnore)
   101  	// TrackingAutoUpdate represents the auto update tracking type for branches and will
   102  	// auto update the branch with any upstream changes
   103  	TrackingAutoUpdate = TrackingType(mono_models.BranchEditableTrackingTypeAutoUpdate)
   104  )
   105  
   106  func (t TrackingType) String() string {
   107  	switch t {
   108  	case TrackingNotify:
   109  		return mono_models.BranchEditableTrackingTypeNotify
   110  	case TrackingAutoUpdate:
   111  		return mono_models.BranchEditableTrackingTypeAutoUpdate
   112  	default:
   113  		return mono_models.BranchEditableTrackingTypeIgnore
   114  	}
   115  }
   116  
   117  // NamespaceMatch Checks if the given namespace query matches the given namespace
   118  func NamespaceMatch(query string, namespace NamespaceMatchable) bool {
   119  	match, err := regexp.Match(string(namespace), []byte(query))
   120  	if err != nil {
   121  		multilog.Error("Could not match regex for %v, query: %s, error: %v", namespace, query, err)
   122  	}
   123  	return match
   124  }
   125  
   126  type NamespaceType struct {
   127  	name      string
   128  	prefix    string
   129  	matchable NamespaceMatchable
   130  }
   131  
   132  var (
   133  	NamespacePackage  = NamespaceType{"package", "language", NamespacePackageMatch} // these values should match the namespace prefix
   134  	NamespaceBundle   = NamespaceType{"bundle", "bundles", NamespaceBundlesMatch}
   135  	NamespaceLanguage = NamespaceType{"language", "", NamespaceLanguageMatch}
   136  	NamespacePlatform = NamespaceType{"platform", "", NamespacePlatformMatch}
   137  	NamespaceOrg      = NamespaceType{"org", "org", NamespaceOrgMatch}
   138  	NamespaceRaw      = NamespaceType{"raw", "", ""}
   139  	NamespaceBlank    = NamespaceType{"", "", ""}
   140  )
   141  
   142  func (t NamespaceType) String() string {
   143  	return t.name
   144  }
   145  
   146  func (t NamespaceType) Prefix() string {
   147  	return t.prefix
   148  }
   149  
   150  func (t NamespaceType) Matchable() NamespaceMatchable {
   151  	return t.matchable
   152  }
   153  
   154  // Namespace is the type used for communicating namespaces, mainly just allows for self documenting code
   155  type Namespace struct {
   156  	nsType NamespaceType
   157  	value  string
   158  }
   159  
   160  func (n Namespace) IsValid() bool {
   161  	return n.nsType.name != "" && n.nsType != NamespaceBlank && n.value != ""
   162  }
   163  
   164  func (n Namespace) Type() NamespaceType {
   165  	return n.nsType
   166  }
   167  
   168  func (n Namespace) String() string {
   169  	return n.value
   170  }
   171  
   172  func NewNamespacePkgOrBundle(language string, nstype NamespaceType) Namespace {
   173  	if nstype == NamespaceBundle {
   174  		return NewNamespaceBundle(language)
   175  	}
   176  	return NewNamespacePackage(language)
   177  }
   178  
   179  // NewNamespacePackage creates a new package namespace
   180  func NewNamespacePackage(language string) Namespace {
   181  	return Namespace{NamespacePackage, fmt.Sprintf("language/%s", language)}
   182  }
   183  
   184  func NewRawNamespace(value string) Namespace {
   185  	return Namespace{NamespaceRaw, value}
   186  }
   187  
   188  func NewBlankNamespace() Namespace {
   189  	return Namespace{NamespaceBlank, ""}
   190  }
   191  
   192  // NewNamespaceBundle creates a new bundles namespace
   193  func NewNamespaceBundle(language string) Namespace {
   194  	return Namespace{NamespaceBundle, fmt.Sprintf("bundles/%s", language)}
   195  }
   196  
   197  // NewNamespaceLanguage provides the base language namespace.
   198  func NewNamespaceLanguage() Namespace {
   199  	return Namespace{NamespaceLanguage, "language"}
   200  }
   201  
   202  // NewNamespacePlatform provides the base platform namespace.
   203  func NewNamespacePlatform() Namespace {
   204  	return Namespace{NamespacePlatform, "platform"}
   205  }
   206  
   207  func NewOrgNamespace(orgName string) Namespace {
   208  	return Namespace{
   209  		nsType: NamespaceOrg,
   210  		value:  fmt.Sprintf("private/%s", orgName),
   211  	}
   212  }
   213  
   214  func LanguageFromNamespace(ns string) string {
   215  	values := strings.Split(ns, "/")
   216  	if len(values) != 2 {
   217  		return ""
   218  	}
   219  	return values[1]
   220  }
   221  
   222  // FilterSupportedIngredients filters a list of ingredients, returning only those that are currently supported (such that they can be built) by the Platform
   223  func FilterSupportedIngredients(supported []model.SupportedLanguage, ingredients []*IngredientAndVersion) ([]*IngredientAndVersion, error) {
   224  	var res []*IngredientAndVersion
   225  
   226  	for _, i := range ingredients {
   227  		language := LanguageFromNamespace(*i.Ingredient.PrimaryNamespace)
   228  
   229  		for _, l := range supported {
   230  			if l.Name != language {
   231  				continue
   232  			}
   233  			res = append(res, i)
   234  			break
   235  		}
   236  	}
   237  
   238  	return res, nil
   239  }
   240  
   241  // BranchCommitID returns the latest commit id by owner and project names. It
   242  // is possible for a nil commit id to be returned without failure.
   243  func BranchCommitID(ownerName, projectName, branchName string) (*strfmt.UUID, error) {
   244  	proj, err := LegacyFetchProjectByName(ownerName, projectName)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	branch, err := BranchForProjectByName(proj, branchName)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	if branch.CommitID == nil {
   255  		return nil, locale.NewInputError(
   256  			"err_project_no_commit",
   257  			"Your project does not have any commits yet, head over to {{.V0}} to set up your project.", api.GetPlatformURL(fmt.Sprintf("%s/%s", ownerName, projectName)).String())
   258  	}
   259  
   260  	return branch.CommitID, nil
   261  }
   262  
   263  func CommitBelongsToBranch(ownerName, projectName, branchName string, commitID strfmt.UUID, auth *authentication.Auth) (bool, error) {
   264  	latestCID, err := BranchCommitID(ownerName, projectName, branchName)
   265  	if err != nil {
   266  		return false, errs.Wrap(err, "Could not get latest commit ID of branch")
   267  	}
   268  
   269  	return CommitWithinCommitHistory(*latestCID, commitID, auth)
   270  }
   271  
   272  func CommitWithinCommitHistory(latestCommitID, searchCommitID strfmt.UUID, auth *authentication.Auth) (bool, error) {
   273  	history, err := CommitHistoryFromID(latestCommitID, auth)
   274  	if err != nil {
   275  		return false, errs.Wrap(err, "Could not get commit history from commit ID")
   276  	}
   277  
   278  	for _, commit := range history {
   279  		if commit.CommitID == searchCommitID {
   280  			return true, nil
   281  		}
   282  	}
   283  
   284  	return false, nil
   285  }
   286  
   287  // CommitHistory will return the commit history for the given owner / project
   288  func CommitHistory(ownerName, projectName, branchName string, auth *authentication.Auth) ([]*mono_models.Commit, error) {
   289  	latestCID, err := BranchCommitID(ownerName, projectName, branchName)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	return commitHistory(*latestCID, auth)
   294  }
   295  
   296  // CommitHistoryFromID will return the commit history from the given commitID
   297  func CommitHistoryFromID(commitID strfmt.UUID, auth *authentication.Auth) ([]*mono_models.Commit, error) {
   298  	return commitHistory(commitID, auth)
   299  }
   300  
   301  func commitHistory(commitID strfmt.UUID, auth *authentication.Auth) ([]*mono_models.Commit, error) {
   302  	offset := int64(0)
   303  	limit := int64(100)
   304  	var commits []*mono_models.Commit
   305  
   306  	cont := true
   307  	for cont {
   308  		payload, err := CommitHistoryPaged(commitID, offset, limit, auth)
   309  		if err != nil {
   310  			return commits, err
   311  		}
   312  		commits = append(commits, payload.Commits...)
   313  		offset += limit
   314  		cont = payload.TotalCommits > offset
   315  	}
   316  
   317  	return commits, nil
   318  }
   319  
   320  // CommitHistoryPaged will return the commit history for the given owner / project
   321  func CommitHistoryPaged(commitID strfmt.UUID, offset, limit int64, auth *authentication.Auth) (*mono_models.CommitHistoryInfo, error) {
   322  	params := vcsClient.NewGetCommitHistoryParams()
   323  	params.SetCommitID(commitID)
   324  	params.Limit = &limit
   325  	params.Offset = &offset
   326  
   327  	var res *vcsClient.GetCommitHistoryOK
   328  	var err error
   329  	if auth.Authenticated() {
   330  		authClient, err := auth.Client()
   331  		if err != nil {
   332  			return nil, errs.Wrap(err, "Could not get auth client")
   333  		}
   334  		res, err = authClient.VersionControl.GetCommitHistory(params, auth.ClientAuth())
   335  		if err != nil {
   336  			return nil, locale.WrapError(err, "err_get_commit_history", "", api.ErrorMessageFromPayload(err))
   337  		}
   338  	} else {
   339  		res, err = mono.New().VersionControl.GetCommitHistory(params, nil)
   340  		if err != nil {
   341  			return nil, locale.WrapError(err, "err_get_commit_history", "", api.ErrorMessageFromPayload(err))
   342  		}
   343  	}
   344  
   345  	return res.Payload, nil
   346  }
   347  
   348  // CommonParent returns the first commit id which both provided commit id
   349  // histories have in common.
   350  func CommonParent(commit1, commit2 *strfmt.UUID, auth *authentication.Auth) (*strfmt.UUID, error) {
   351  	if commit1 == nil || commit2 == nil {
   352  		return nil, nil
   353  	}
   354  
   355  	if *commit1 == *commit2 {
   356  		return commit1, nil
   357  	}
   358  
   359  	history1, err := CommitHistoryFromID(*commit1, auth)
   360  	if err != nil {
   361  		return nil, errs.Wrap(err, "Could not get commit history for %s", commit1.String())
   362  	}
   363  
   364  	history2, err := CommitHistoryFromID(*commit2, auth)
   365  	if err != nil {
   366  		return nil, errs.Wrap(err, "Could not get commit history for %s", commit2.String())
   367  	}
   368  
   369  	return commonParentWithHistory(commit1, commit2, history1, history2), nil
   370  }
   371  
   372  func commonParentWithHistory(commit1, commit2 *strfmt.UUID, history1, history2 []*mono_models.Commit) *strfmt.UUID {
   373  	if commit1 == nil || commit2 == nil {
   374  		return nil
   375  	}
   376  
   377  	if *commit1 == *commit2 {
   378  		return commit1
   379  	}
   380  
   381  	for _, c := range history1 {
   382  		if c.CommitID == *commit2 {
   383  			return commit2 // commit1 history contains commit2
   384  		}
   385  		for _, c2 := range history2 {
   386  			if c.CommitID == c2.CommitID {
   387  				return &c.CommitID // commit1 and commit2 have a common parent
   388  			}
   389  		}
   390  	}
   391  
   392  	for _, c2 := range history2 {
   393  		if c2.CommitID == *commit1 {
   394  			return commit1 // commit2 history contains commit1
   395  		}
   396  	}
   397  
   398  	return nil
   399  }
   400  
   401  // CommitsBehind compares the provided commit id with the latest commit
   402  // id and returns the count of commits it is behind. A negative return value
   403  // indicates the provided commit id is ahead of the latest commit id (that is,
   404  // there are local commits).
   405  func CommitsBehind(latestCID, currentCommitID strfmt.UUID, auth *authentication.Auth) (int, error) {
   406  	if latestCID == "" {
   407  		if currentCommitID == "" {
   408  			return 0, nil // ok, nothing to do
   409  		}
   410  		return 0, locale.NewError("err_commit_count_no_latest_with_commit")
   411  	}
   412  
   413  	if latestCID.String() == currentCommitID.String() {
   414  		return 0, nil
   415  	}
   416  
   417  	// Assume current is behind or equal to latest.
   418  	commits, err := CommitHistoryFromID(latestCID, auth)
   419  	if err != nil {
   420  		return 0, locale.WrapError(err, "err_get_commit_history", "", err.Error())
   421  	}
   422  
   423  	indexed := makeIndexedCommits(commits)
   424  	if behind, err := indexed.countBetween(currentCommitID.String(), latestCID.String()); err == nil {
   425  		return behind, nil
   426  	}
   427  
   428  	// Assume current is ahead of latest.
   429  	commits, err = CommitHistoryFromID(currentCommitID, auth)
   430  	if err != nil {
   431  		return 0, locale.WrapError(err, "err_get_commit_history", "", err.Error())
   432  	}
   433  
   434  	indexed = makeIndexedCommits(commits)
   435  	ahead, err := indexed.countBetween(latestCID.String(), currentCommitID.String())
   436  	return -ahead, err
   437  }
   438  
   439  // Changeset aliases for eased usage and to act as a disconnect from the underlying dep.
   440  type Changeset = []*mono_models.CommitChangeEditable
   441  
   442  func UpdateBranchForProject(pj ProjectInfo, commitID strfmt.UUID, auth *authentication.Auth) error {
   443  	pjm, err := LegacyFetchProjectByName(pj.Owner(), pj.Name())
   444  	if err != nil {
   445  		return errs.Wrap(err, "Could not fetch project")
   446  	}
   447  
   448  	branch, err := BranchForProjectByName(pjm, pj.BranchName())
   449  	if err != nil {
   450  		return errs.Wrap(err, "Could not fetch branch: %s", pj.BranchName())
   451  	}
   452  
   453  	err = UpdateBranchCommit(branch.BranchID, commitID, auth)
   454  	if err != nil {
   455  		return errs.Wrap(err, "Could no update branch to commit %s", commitID.String())
   456  	}
   457  
   458  	return nil
   459  }
   460  
   461  // UpdateBranchCommit updates the commit that a branch is pointed at
   462  func UpdateBranchCommit(branchID strfmt.UUID, commitID strfmt.UUID, auth *authentication.Auth) error {
   463  	changeset := &mono_models.BranchEditable{
   464  		CommitID: &commitID,
   465  	}
   466  
   467  	return updateBranch(branchID, changeset, auth)
   468  }
   469  
   470  // UpdateBranchTracking updates the tracking information for the given branch
   471  func UpdateBranchTracking(branchID, commitID, trackingBranchID strfmt.UUID, trackingType TrackingType, auth *authentication.Auth) error {
   472  	tracking := trackingType.String()
   473  	changeset := &mono_models.BranchEditable{
   474  		CommitID:     &commitID,
   475  		TrackingType: &tracking,
   476  		Tracks:       &trackingBranchID,
   477  	}
   478  
   479  	return updateBranch(branchID, changeset, auth)
   480  }
   481  
   482  func updateBranch(branchID strfmt.UUID, changeset *mono_models.BranchEditable, auth *authentication.Auth) error {
   483  	authClient, err := auth.Client()
   484  	if err != nil {
   485  		return errs.Wrap(err, "Could not get auth client")
   486  	}
   487  
   488  	params := vcsClient.NewUpdateBranchParams()
   489  	params.SetBranchID(branchID)
   490  	params.SetBranch(changeset)
   491  
   492  	_, err = authClient.VersionControl.UpdateBranch(params, auth.ClientAuth())
   493  	if err != nil {
   494  		if _, ok := err.(*version_control.UpdateBranchForbidden); ok {
   495  			return &ErrUpdateBranchAuth{locale.NewExternalError("err_branch_update_auth", "Branch update failed with authentication error")}
   496  		}
   497  		return locale.NewError("err_update_branch", "", api.ErrorMessageFromPayload(err))
   498  	}
   499  	return nil
   500  }
   501  
   502  func DeleteBranch(branchID strfmt.UUID, auth *authentication.Auth) error {
   503  	authClient, err := auth.Client()
   504  	if err != nil {
   505  		return errs.Wrap(err, "Could not get auth client")
   506  	}
   507  
   508  	params := vcsClient.NewDeleteBranchParams()
   509  	params.SetBranchID(branchID)
   510  
   511  	_, err = authClient.VersionControl.DeleteBranch(params, auth.ClientAuth())
   512  	if err != nil {
   513  		return locale.WrapError(err, "err_delete_branch", "Could not delete branch")
   514  	}
   515  
   516  	return nil
   517  }
   518  
   519  // UpdateProjectBranchCommitByName updates the vcs branch for a project given by its namespace with a new commitID
   520  func UpdateProjectBranchCommit(pj ProjectInfo, commitID strfmt.UUID, auth *authentication.Auth) error {
   521  	pjm, err := LegacyFetchProjectByName(pj.Owner(), pj.Name())
   522  	if err != nil {
   523  		return errs.Wrap(err, "Could not fetch project")
   524  	}
   525  
   526  	return UpdateProjectBranchCommitWithModel(pjm, pj.BranchName(), commitID, auth)
   527  }
   528  
   529  // UpdateProjectBranchCommitByName updates the vcs branch for a project given by its namespace with a new commitID
   530  func UpdateProjectBranchCommitWithModel(pjm *mono_models.Project, branchName string, commitID strfmt.UUID, auth *authentication.Auth) error {
   531  	branch, err := BranchForProjectByName(pjm, branchName)
   532  	if err != nil {
   533  		return errs.Wrap(err, "Could not fetch branch: %s", branchName)
   534  	}
   535  
   536  	err = UpdateBranchCommit(branch.BranchID, commitID, auth)
   537  	if err != nil {
   538  		return errs.Wrap(err, "Could update branch %s to commitID %s", branchName, commitID.String())
   539  	}
   540  	return nil
   541  }
   542  
   543  // CommitInitial creates a root commit for a new branch
   544  func CommitInitial(hostPlatform string, langName, langVersion string, auth *authentication.Auth) (strfmt.UUID, error) {
   545  	platformID, err := hostPlatformToPlatformID(hostPlatform)
   546  	if err != nil {
   547  		return "", err
   548  	}
   549  
   550  	var changes []*mono_models.CommitChangeEditable
   551  
   552  	if langName != "" {
   553  		versionConstraints, err := versionStringToConstraints(langVersion)
   554  		if err != nil {
   555  			return "", errs.Wrap(err, "Could not process version string into constraints")
   556  		}
   557  		c := &mono_models.CommitChangeEditable{
   558  			Operation:          string(OperationAdded),
   559  			Namespace:          NewNamespaceLanguage().String(),
   560  			Requirement:        langName,
   561  			VersionConstraints: versionConstraints,
   562  		}
   563  		changes = append(changes, c)
   564  	}
   565  
   566  	c := &mono_models.CommitChangeEditable{
   567  		Operation:   string(OperationAdded),
   568  		Namespace:   NewNamespacePlatform().String(),
   569  		Requirement: platformID,
   570  	}
   571  	changes = append(changes, c)
   572  
   573  	commit := &mono_models.CommitEditable{
   574  		Changeset: changes,
   575  		Message:   locale.T("commit_message_add_initial"),
   576  	}
   577  	params := vcsClient.NewAddCommitParams()
   578  	params.SetCommit(commit)
   579  
   580  	res, err := mono.New().VersionControl.AddCommit(params, auth.ClientAuth())
   581  	if err != nil {
   582  		return "", locale.WrapError(err, "err_add_commit", "", api.ErrorMessageFromPayload(err))
   583  	}
   584  
   585  	return res.Payload.CommitID, nil
   586  }
   587  
   588  func versionStringToConstraints(version string) ([]*mono_models.Constraint, error) {
   589  	requirements, err := bpModel.VersionStringToRequirements(version)
   590  	if err != nil {
   591  		return nil, errs.Wrap(err, "Unable to process version string into requirements")
   592  	}
   593  
   594  	constraints := make([]*mono_models.Constraint, len(requirements))
   595  	for i, constraint := range requirements {
   596  		constraints[i] = &mono_models.Constraint{
   597  			Comparator: constraint[types.VersionRequirementComparatorKey],
   598  			Version:    constraint[types.VersionRequirementVersionKey],
   599  		}
   600  	}
   601  	return constraints, nil
   602  }
   603  
   604  type indexedCommits map[string]string // key == commit id / val == parent id
   605  
   606  func makeIndexedCommits(cs []*mono_models.Commit) indexedCommits {
   607  	m := make(indexedCommits)
   608  
   609  	for _, c := range cs {
   610  		m[string(c.CommitID)] = string(c.ParentCommitID)
   611  	}
   612  
   613  	return m
   614  }
   615  
   616  // countBetween returns 0 if same or if unable to determine the count.
   617  // Caution: Currently, the logic does not verify that the first commit is "before" the last commit.
   618  func (cs indexedCommits) countBetween(first, last string) (int, error) {
   619  	if first == last {
   620  		return 0, nil
   621  	}
   622  
   623  	if last == "" {
   624  		return 0, locale.NewError("err_commit_count_missing_last")
   625  	}
   626  
   627  	if first != "" {
   628  		if _, ok := cs[first]; !ok {
   629  			return 0, locale.WrapError(ErrCommitCountUnknowable, "err_commit_count_cannot_find_first", "", first)
   630  		}
   631  	}
   632  
   633  	next := last
   634  	var ct int
   635  	for ct <= len(cs) {
   636  		if next == first {
   637  			return ct, nil
   638  		}
   639  
   640  		ct++
   641  
   642  		var ok bool
   643  		next, ok = cs[next]
   644  		if !ok {
   645  			return 0, locale.WrapError(ErrCommitCountUnknowable, "err_commit_count_cannot_find", next)
   646  		}
   647  	}
   648  
   649  	return ct, nil
   650  }
   651  
   652  func ResolveRequirementNameAndVersion(name, version string, word int, namespace Namespace, auth *authentication.Auth) (string, string, error) {
   653  	if namespace.Type() == NamespacePlatform {
   654  		platform, err := FetchPlatformByDetails(name, version, word, auth)
   655  		if err != nil {
   656  			return "", "", errs.Wrap(err, "Could not fetch platform")
   657  		}
   658  		name = platform.PlatformID.String()
   659  		version = ""
   660  	}
   661  
   662  	return name, version, nil
   663  }
   664  
   665  func ChangesetFromRequirements(op Operation, reqs []*gqlModel.Requirement) Changeset {
   666  	var changeset Changeset
   667  
   668  	for _, req := range reqs {
   669  		change := &mono_models.CommitChangeEditable{
   670  			Operation:         string(op),
   671  			Namespace:         req.Namespace,
   672  			Requirement:       req.Requirement,
   673  			VersionConstraint: req.VersionConstraint,
   674  		}
   675  
   676  		changeset = append(changeset, change)
   677  	}
   678  
   679  	return changeset
   680  }
   681  
   682  func TrackBranch(source, target *mono_models.Project, auth *authentication.Auth) error {
   683  	authClient, err := auth.Client()
   684  	if err != nil {
   685  		return errs.Wrap(err, "Could not get auth client")
   686  	}
   687  
   688  	sourceBranch, err := DefaultBranchForProject(source)
   689  	if err != nil {
   690  		return err
   691  	}
   692  
   693  	targetBranch, err := DefaultBranchForProject(target)
   694  	if err != nil {
   695  		return err
   696  	}
   697  
   698  	trackingType := mono_models.BranchEditableTrackingTypeNotify
   699  
   700  	updateParams := vcsClient.NewUpdateBranchParams()
   701  	branch := &mono_models.BranchEditable{
   702  		TrackingType: &trackingType,
   703  		Tracks:       &sourceBranch.BranchID,
   704  	}
   705  	updateParams.SetBranch(branch)
   706  	updateParams.SetBranchID(targetBranch.BranchID)
   707  
   708  	_, err = authClient.VersionControl.UpdateBranch(updateParams, auth.ClientAuth())
   709  	if err != nil {
   710  		msg := api.ErrorMessageFromPayload(err)
   711  		return locale.WrapError(err, msg)
   712  	}
   713  	return nil
   714  }
   715  
   716  func GetRootBranches(branches mono_models.Branches) mono_models.Branches {
   717  	var rootBranches mono_models.Branches
   718  	for _, branch := range branches {
   719  		// Account for forked projects where the root branches contain
   720  		// a tracking ID that is not in the current project's branches
   721  		if branch.Tracks != nil && containsBranch(branch.Tracks, branches) {
   722  			continue
   723  		}
   724  		rootBranches = append(rootBranches, branch)
   725  	}
   726  	return rootBranches
   727  }
   728  
   729  func containsBranch(id *strfmt.UUID, branches mono_models.Branches) bool {
   730  	for _, branch := range branches {
   731  		if branch.BranchID.String() == id.String() {
   732  			return true
   733  		}
   734  	}
   735  	return false
   736  }
   737  
   738  // GetBranchChildren returns the direct children of the given branch
   739  func GetBranchChildren(branch *mono_models.Branch, branches mono_models.Branches) mono_models.Branches {
   740  	var children mono_models.Branches
   741  	if branch == nil {
   742  		return children
   743  	}
   744  
   745  	for _, b := range branches {
   746  		if b.Tracks != nil && b.Tracks.String() == branch.BranchID.String() {
   747  			children = append(children, b)
   748  		}
   749  	}
   750  	return children
   751  }
   752  
   753  func GetRevertCommit(from, to strfmt.UUID, auth *authentication.Auth) (*mono_models.Commit, error) {
   754  	params := vcsClient.NewGetRevertCommitParams()
   755  	params.SetCommitFromID(from)
   756  	params.SetCommitToID(to)
   757  
   758  	client := mono.New()
   759  	var err error
   760  	if auth.Authenticated() {
   761  		client, err = auth.Client()
   762  		if err != nil {
   763  			return nil, errs.Wrap(err, "Could not get auth client")
   764  		}
   765  	}
   766  	res, err := client.VersionControl.GetRevertCommit(params, auth.ClientAuth())
   767  	if err != nil {
   768  		return nil, locale.WrapError(err, "err_get_revert_commit", "Could not revert from commit ID {{.V0}} to {{.V1}}", from.String(), to.String())
   769  	}
   770  
   771  	return res.Payload, nil
   772  }
   773  
   774  func RevertCommitWithinHistory(from, to, latest strfmt.UUID, auth *authentication.Auth) (*mono_models.Commit, error) {
   775  	targetCommit := from
   776  	preposition := ""
   777  	if from == latest { // reverting to
   778  		targetCommit = to
   779  		preposition = " to" // need leading whitespace
   780  	}
   781  	ok, err := CommitWithinCommitHistory(latest, targetCommit, auth)
   782  	if err != nil {
   783  		return nil, errs.Wrap(err, "API communication failed.")
   784  	}
   785  	if !ok {
   786  		return nil, locale.WrapError(err, "err_revert_commit_within_history_not_in", "The commit being reverted{{.V0}} is not within the current commit's history.", preposition)
   787  	}
   788  
   789  	return RevertCommit(from, to, latest, auth)
   790  }
   791  
   792  func RevertCommit(from, to, latest strfmt.UUID, auth *authentication.Auth) (*mono_models.Commit, error) {
   793  	revertCommit, err := GetRevertCommit(from, to, auth)
   794  	if err != nil {
   795  		return nil, err
   796  	}
   797  	if from != latest {
   798  		// The platform assumes revert commits are reverting to a particular commit, rather than
   799  		// reverting the changes in a commit. As a result, commit messages are of the form "Revert to
   800  		// commit X" and parent commit IDs are X. Change the message to reflect the fact we're
   801  		// reverting changes from X and change the parent to be the latest commit so that the revert
   802  		// commit applies to the latest project commit.
   803  		revertCommit.Message = locale.Tl("revert_commit", "Revert commit {{.V0}}", from.String())
   804  		revertCommit.ParentCommitID = latest
   805  	}
   806  
   807  	addCommit, err := AddRevertCommit(revertCommit, auth)
   808  	if err != nil {
   809  		return nil, err
   810  	}
   811  	return addCommit, nil
   812  }
   813  
   814  func MergeCommit(commitReceiving, commitWithChanges strfmt.UUID) (*mono_models.MergeStrategies, error) {
   815  	params := vcsClient.NewMergeCommitsParams()
   816  	params.SetCommitReceivingChanges(commitReceiving)
   817  	params.SetCommitWithChanges(commitWithChanges)
   818  	params.SetHTTPClient(api.NewHTTPClient())
   819  
   820  	res, noContent, err := mono.New().VersionControl.MergeCommits(params)
   821  	if err != nil {
   822  		if api.ErrorCodeFromPayload(err) == 409 {
   823  			logging.Debug("Received 409 from MergeCommit: %s", err.Error())
   824  			return nil, ErrMergeCommitInHistory
   825  		}
   826  		return nil, locale.WrapError(err, "err_api_mergecommit", api.ErrorMessageFromPayload(err))
   827  	}
   828  	if noContent != nil {
   829  		return nil, ErrMergeFastForward
   830  	}
   831  
   832  	return res.Payload, nil
   833  }
   834  
   835  func MergeRequired(commitReceiving, commitWithChanges strfmt.UUID) (bool, error) {
   836  	_, err := MergeCommit(commitReceiving, commitWithChanges)
   837  	if err != nil {
   838  		if errors.Is(err, ErrMergeFastForward) || errors.Is(err, ErrMergeCommitInHistory) {
   839  			return false, nil
   840  		}
   841  		return false, err
   842  	}
   843  	return true, nil
   844  }
   845  
   846  func GetCommit(commitID strfmt.UUID, auth *authentication.Auth) (*mono_models.Commit, error) {
   847  	params := vcsClient.NewGetCommitParams()
   848  	params.SetCommitID(commitID)
   849  	params.SetHTTPClient(api.NewHTTPClient())
   850  
   851  	client := mono.New()
   852  	var err error
   853  	if auth.Authenticated() {
   854  		client, err = auth.Client()
   855  		if err != nil {
   856  			return nil, errs.Wrap(err, "Could not get auth client")
   857  		}
   858  	}
   859  	res, err := client.VersionControl.GetCommit(params, auth.ClientAuth())
   860  	if err != nil {
   861  		return nil, locale.WrapError(err, "err_get_commit", "Could not get commit from ID: {{.V0}}", commitID.String())
   862  	}
   863  	return res.Payload, nil
   864  }
   865  
   866  func GetCommitWithinCommitHistory(currentCommitID, targetCommitID strfmt.UUID, auth *authentication.Auth) (*mono_models.Commit, error) {
   867  	commit, err := GetCommit(targetCommitID, auth)
   868  	if err != nil {
   869  		return nil, err
   870  	}
   871  
   872  	ok, err := CommitWithinCommitHistory(currentCommitID, targetCommitID, auth)
   873  	if err != nil {
   874  		return nil, errs.Wrap(err, "API communication failed.")
   875  	}
   876  	if !ok {
   877  		return nil, ErrCommitNotInHistory
   878  	}
   879  
   880  	return commit, nil
   881  }
   882  
   883  func AddRevertCommit(commit *mono_models.Commit, auth *authentication.Auth) (*mono_models.Commit, error) {
   884  	params := vcsClient.NewAddCommitParams()
   885  
   886  	editableCommit, err := commitToCommitEditable(commit)
   887  	if err != nil {
   888  		return nil, locale.WrapError(err, "err_convert_commit", "Could not convert commit data")
   889  	}
   890  	params.SetCommit(editableCommit)
   891  
   892  	res, err := mono.New().VersionControl.AddCommit(params, auth.ClientAuth())
   893  	if err != nil {
   894  		return nil, locale.WrapError(err, "err_add_revert_commit", "Could not add revert commit")
   895  	}
   896  	return res.Payload, nil
   897  }
   898  
   899  func commitToCommitEditable(from *mono_models.Commit) (*mono_models.CommitEditable, error) {
   900  	editableData, err := from.MarshalBinary()
   901  	if err != nil {
   902  		return nil, locale.WrapError(err, "err_commit_marshal", "Could not marshall commit data")
   903  	}
   904  
   905  	commit := &mono_models.CommitEditable{}
   906  	err = commit.UnmarshalBinary(editableData)
   907  	if err != nil {
   908  		return nil, locale.WrapError(err, "err_commit_unmarshal", "Could not unmarshal commit data")
   909  	}
   910  	return commit, nil
   911  }