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

     1  package git
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/ActiveState/cli/internal/analytics"
    10  	anaConsts "github.com/ActiveState/cli/internal/analytics/constants"
    11  	"github.com/ActiveState/cli/internal/constants"
    12  	"github.com/ActiveState/cli/internal/errs"
    13  	"github.com/ActiveState/cli/internal/fileutils"
    14  	"github.com/ActiveState/cli/internal/locale"
    15  	"github.com/ActiveState/cli/internal/output"
    16  	"github.com/ActiveState/cli/pkg/platform/model"
    17  	"github.com/ActiveState/cli/pkg/project"
    18  	"github.com/ActiveState/cli/pkg/projectfile"
    19  	"gopkg.in/src-d/go-git.v4"
    20  )
    21  
    22  // Repository is the interface used to represent a version control system repository
    23  type Repository interface {
    24  	CloneProject(owner, name, path string, out output.Outputer, an analytics.Dispatcher) error
    25  }
    26  
    27  // NewRepo returns a new repository
    28  func NewRepo() *Repo {
    29  	return &Repo{}
    30  }
    31  
    32  // Repo represents a git repository
    33  type Repo struct {
    34  }
    35  
    36  // CloneProject will attempt to clone the associalted public git repository
    37  // for the project identified by <owner>/<name> to the given directory
    38  func (r *Repo) CloneProject(owner, name, path string, out output.Outputer, an analytics.Dispatcher) error {
    39  	project, err := model.LegacyFetchProjectByName(owner, name)
    40  	if err != nil {
    41  		return locale.WrapError(err, "err_git_fetch_project", "Could not fetch project details")
    42  	}
    43  
    44  	tempDir, err := os.MkdirTemp("", fmt.Sprintf("state-activate-repo-%s-%s", owner, name))
    45  	if err != nil {
    46  		return locale.WrapError(err, "err_git_tempdir", "Could not create temporary directory for git clone operation")
    47  	}
    48  	defer os.RemoveAll(tempDir)
    49  
    50  	if project.RepoURL == nil {
    51  		return locale.NewError("err_nil_repo_url", "Project returned empty repository URL")
    52  	}
    53  
    54  	out.Notice(output.Title(locale.Tr("git_cloning_project_heading")))
    55  	out.Notice(locale.Tr("git_cloning_project", *project.RepoURL))
    56  	_, err = git.PlainClone(tempDir, false, &git.CloneOptions{
    57  		URL:      *project.RepoURL,
    58  		Progress: os.Stdout,
    59  	})
    60  	if err != nil {
    61  		err = locale.WrapError(err, "err_clone_repo", "Could not clone repository with URL: {{.V0}}, error received: {{.V1}}.", *project.RepoURL, err.Error())
    62  		tipMsg := locale.Tl(
    63  			"err_tip_git_ssh-add",
    64  			"If you are using an SSH key please ensure it's configured by running '[ACTIONABLE]ssh-add <path-to-key>[/RESET]'.",
    65  		)
    66  		return errs.AddTips(err, tipMsg)
    67  	}
    68  
    69  	err = EnsureCorrectProject(owner, name, filepath.Join(tempDir, constants.ConfigFileName), *project.RepoURL, out, an)
    70  	if err != nil {
    71  		return locale.WrapError(err, "err_git_ensure_project", "Could not ensure that the activestate.yaml in the cloned repository matches the project you are activating.")
    72  	}
    73  
    74  	err = MoveFiles(tempDir, path)
    75  	if err != nil {
    76  		return locale.WrapError(err, "err_git_move_files", "Could not move cloned files")
    77  	}
    78  
    79  	return nil
    80  }
    81  
    82  func EnsureCorrectProject(owner, name, projectFilePath, repoURL string, out output.Outputer, an analytics.Dispatcher) error {
    83  	if !fileutils.FileExists(projectFilePath) {
    84  		return nil
    85  	}
    86  
    87  	projectFile, err := projectfile.Parse(projectFilePath)
    88  	if err != nil {
    89  		return locale.WrapError(err, "err_git_parse_projectfile", "Could not parse projectfile")
    90  	}
    91  
    92  	proj, err := project.NewLegacy(projectFile)
    93  	if err != nil {
    94  		return locale.WrapError(err, "err_git_project", "Could not create new project from project file at: {{.V0}}", projectFile.Path())
    95  	}
    96  
    97  	if !(strings.EqualFold(proj.Owner(), owner)) || !(strings.EqualFold(proj.Name(), name)) {
    98  		out.Notice(locale.Tr("warning_git_project_mismatch", repoURL, project.NewNamespace(owner, name, "").String(), constants.DocumentationURLMismatch))
    99  		err = proj.Source().SetNamespace(owner, name)
   100  		if err != nil {
   101  			return locale.WrapError(err, "err_git_update_mismatch", "Could not update projectfile namespace")
   102  		}
   103  		an.Event(anaConsts.CatMisc, "git-project-mismatch")
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  func MoveFiles(src, dest string) error {
   110  	err := verifyDestinationDirectory(dest)
   111  	if err != nil {
   112  		return locale.WrapError(err, "err_git_verify_dir", "Could not verify destination directory")
   113  	}
   114  
   115  	err = fileutils.MoveAllFilesCrossDisk(src, dest)
   116  	if err != nil {
   117  		return locale.WrapError(err, "err_git_move_file", "Could not move files from {{.V0}} to {{.V1}}", src, dest)
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  func verifyDestinationDirectory(dest string) error {
   124  	if !fileutils.DirExists(dest) {
   125  		return fileutils.Mkdir(dest)
   126  	}
   127  
   128  	empty, err := fileutils.IsEmptyDir(dest)
   129  	if err != nil {
   130  		return locale.WrapError(err, "err_git_empty_dir", "Could not verify if destination directory is empty")
   131  	}
   132  	if !empty {
   133  		return locale.NewError("err_git_in_use", "Destination directory is not empty")
   134  	}
   135  
   136  	return nil
   137  }