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

     1  package commit
     2  
     3  import (
     4  	"errors"
     5  	"time"
     6  
     7  	"github.com/ActiveState/cli/internal/analytics"
     8  	"github.com/ActiveState/cli/internal/config"
     9  	"github.com/ActiveState/cli/internal/constants"
    10  	"github.com/ActiveState/cli/internal/errs"
    11  	"github.com/ActiveState/cli/internal/locale"
    12  	"github.com/ActiveState/cli/internal/output"
    13  	"github.com/ActiveState/cli/internal/primer"
    14  	"github.com/ActiveState/cli/internal/runbits/rationalize"
    15  	"github.com/ActiveState/cli/pkg/localcommit"
    16  	bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response"
    17  	"github.com/ActiveState/cli/pkg/platform/authentication"
    18  	"github.com/ActiveState/cli/pkg/platform/model"
    19  	"github.com/ActiveState/cli/pkg/platform/model/buildplanner"
    20  	"github.com/ActiveState/cli/pkg/platform/runtime/buildscript"
    21  	"github.com/ActiveState/cli/pkg/project"
    22  )
    23  
    24  type primeable interface {
    25  	primer.Outputer
    26  	primer.Projecter
    27  	primer.Auther
    28  	primer.Analyticer
    29  	primer.SvcModeler
    30  	primer.Configurer
    31  }
    32  
    33  type Commit struct {
    34  	out       output.Outputer
    35  	proj      *project.Project
    36  	auth      *authentication.Auth
    37  	analytics analytics.Dispatcher
    38  	svcModel  *model.SvcModel
    39  	cfg       *config.Instance
    40  }
    41  
    42  func New(p primeable) *Commit {
    43  	return &Commit{
    44  		out:       p.Output(),
    45  		proj:      p.Project(),
    46  		auth:      p.Auth(),
    47  		analytics: p.Analytics(),
    48  		svcModel:  p.SvcModel(),
    49  		cfg:       p.Config(),
    50  	}
    51  }
    52  
    53  var ErrNoChanges = errors.New("buildscript has no changes")
    54  
    55  func rationalizeError(err *error) {
    56  	var buildPlannerErr *bpResp.BuildPlannerError
    57  
    58  	switch {
    59  	case err == nil:
    60  		return
    61  
    62  	case errors.Is(*err, ErrNoChanges):
    63  		*err = errs.WrapUserFacing(*err, locale.Tl(
    64  			"commit_notice_no_change",
    65  			"No change to the buildscript was found.",
    66  		), errs.SetInput())
    67  
    68  	case errs.Matches(*err, buildscript.ErrBuildscriptNotExist):
    69  		*err = errs.WrapUserFacing(*err, locale.T("err_buildscript_not_exist"))
    70  
    71  	// We communicate buildplanner errors verbatim as the intend is that these are curated by the buildplanner
    72  	case errors.As(*err, &buildPlannerErr):
    73  		*err = errs.WrapUserFacing(*err,
    74  			buildPlannerErr.LocalizedError(),
    75  			errs.SetIf(buildPlannerErr.InputError(), errs.SetInput()))
    76  	}
    77  }
    78  
    79  func (c *Commit) Run() (rerr error) {
    80  	defer rationalizeError(&rerr)
    81  
    82  	if c.proj == nil {
    83  		return rationalize.ErrNoProject
    84  	}
    85  
    86  	pg := output.StartSpinner(c.out, locale.T("progress_commit"), constants.TerminalAnimationInterval)
    87  	defer func() {
    88  		if pg != nil {
    89  			pg.Stop(locale.T("progress_fail") + "\n")
    90  		}
    91  	}()
    92  
    93  	// Get buildscript.as representation
    94  	script, err := buildscript.ScriptFromProject(c.proj)
    95  	if err != nil {
    96  		return errs.Wrap(err, "Could not get local build script")
    97  	}
    98  
    99  	// Get equivalent build script for current state of the project
   100  	localCommitID, err := localcommit.Get(c.proj.Dir())
   101  	if err != nil {
   102  		return errs.Wrap(err, "Unable to get local commit ID")
   103  	}
   104  	bp := buildplanner.NewBuildPlannerModel(c.auth)
   105  	exprProject, atTime, err := bp.GetBuildExpressionAndTime(localCommitID.String())
   106  	if err != nil {
   107  		return errs.Wrap(err, "Could not get remote build expr and time for provided commit")
   108  	}
   109  	remoteScript, err := buildscript.NewFromBuildExpression(atTime, exprProject)
   110  	if err != nil {
   111  		return errs.Wrap(err, "Could not convert build expression to build script")
   112  	}
   113  
   114  	// Check if there is anything to commit
   115  	if script.Equals(remoteScript) {
   116  		return ErrNoChanges
   117  	}
   118  
   119  	var exprAtTime *time.Time
   120  	if atTime := script.AtTime; atTime != nil {
   121  		atTimeTime := time.Time(*atTime)
   122  		exprAtTime = &atTimeTime
   123  	}
   124  
   125  	stagedCommitID, err := bp.StageCommit(buildplanner.StageCommitParams{
   126  		Owner:        c.proj.Owner(),
   127  		Project:      c.proj.Name(),
   128  		ParentCommit: localCommitID.String(),
   129  		Expression:   script.Expr,
   130  		TimeStamp:    exprAtTime,
   131  	})
   132  	if err != nil {
   133  		return errs.Wrap(err, "Could not update project to reflect build script changes.")
   134  	}
   135  
   136  	// Update local commit ID
   137  	if err := localcommit.Set(c.proj.Dir(), stagedCommitID.String()); err != nil {
   138  		return errs.Wrap(err, "Could not set local commit ID")
   139  	}
   140  
   141  	// Update our local build expression to match the committed one. This allows our API a way to ensure forward compatibility.
   142  	newBuildExpr, newAtTime, err := bp.GetBuildExpressionAndTime(stagedCommitID.String())
   143  	if err != nil {
   144  		return errs.Wrap(err, "Unable to get the remote build expression and time")
   145  	}
   146  	if err := buildscript.Update(c.proj, newAtTime, newBuildExpr); err != nil {
   147  		return errs.Wrap(err, "Could not update local build script")
   148  	}
   149  
   150  	pg.Stop(locale.T("progress_success") + "\n")
   151  	pg = nil
   152  
   153  	c.out.Print(output.Prepare(
   154  		locale.Tl(
   155  			"commit_success",
   156  			"", stagedCommitID.String(), c.proj.NamespaceString(),
   157  		),
   158  		&struct {
   159  			Namespace string `json:"namespace"`
   160  			Path      string `json:"path"`
   161  			CommitID  string `json:"commit_id"`
   162  		}{
   163  			c.proj.NamespaceString(),
   164  			c.proj.Dir(),
   165  			stagedCommitID.String(),
   166  		},
   167  	))
   168  
   169  	return nil
   170  }