diff --git a/project-clone/internal/git/operations.go b/project-clone/internal/git/operations.go index 2a8e0c58d..59949e021 100644 --- a/project-clone/internal/git/operations.go +++ b/project-clone/internal/git/operations.go @@ -22,9 +22,7 @@ import ( dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/go-git/go-git/v5" gitConfig "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/devfile/devworkspace-operator/project-clone/internal" "github.com/devfile/devworkspace-operator/project-clone/internal/shell" ) @@ -109,44 +107,25 @@ func CheckoutReference(repo *git.Repository, project *dw.Project, projectPath st } else { defaultRemoteName = checkoutFrom.Remote } - remote, err := repo.Remote(defaultRemoteName) - if err != nil { - return fmt.Errorf("could not find remote %s: %s", defaultRemoteName, err) - } - auth, err := internal.GetAuthForHost(remote.Config().URLs[0]) - if err != nil { - log.Printf("Error reading credentials file: %s", err) - } - refs, err := remote.List(&git.ListOptions{ - Auth: auth, - }) + revision := checkoutFrom.Revision + refType, err := shell.GitResolveReference(projectPath, defaultRemoteName, revision) if err != nil { - return fmt.Errorf("failed to read remote %s: %s", defaultRemoteName, err) - } - - branch, err := repo.Branch(checkoutFrom.Revision) - if err == nil { - return checkoutLocalBranch(projectPath, branch.Name, defaultRemoteName) - } - - for _, ref := range refs { - if ref.Name().Short() != checkoutFrom.Revision { - continue - } - if ref.Name().IsBranch() { - return checkoutRemoteBranch(projectPath, defaultRemoteName, ref) - } else if ref.Name().IsTag() { - return checkoutTag(projectPath, defaultRemoteName, ref) - } - } - - log.Printf("No tag or branch named %s found on remote %s; attempting to resolve commit", checkoutFrom.Revision, defaultRemoteName) - if _, err := repo.ResolveRevision(plumbing.Revision(checkoutFrom.Revision)); err == nil { - return checkoutCommit(projectPath, checkoutFrom.Revision) + return fmt.Errorf("failed to resolve git revision %s: %w", revision, err) + } + switch refType { + case shell.GitRefLocalBranch: + return checkoutLocalBranch(projectPath, revision, defaultRemoteName) + case shell.GitRefRemoteBranch: + return checkoutRemoteBranch(projectPath, revision, defaultRemoteName) + case shell.GitRefTag: + return checkoutTag(projectPath, revision) + case shell.GitRefHash: + return checkoutCommit(projectPath, revision) + default: + log.Printf("Could not find revision %s in repository, using default branch", checkoutFrom.Revision) + return nil } - log.Printf("Could not find revision %s in repository, using default branch", checkoutFrom.Revision) - return nil } func checkoutLocalBranch(projectPath, branchName, remote string) error { @@ -163,29 +142,21 @@ func checkoutLocalBranch(projectPath, branchName, remote string) error { return nil } -func checkoutRemoteBranch(projectPath string, remote string, branchRef *plumbing.Reference) error { - // Implement logic of `git checkout `: - // 1. Create tracking info in .git/config to properly track remote branch - // 2. Create local branch to match name of remote branch with hash matching remote branch - // More info: see https://git-scm.com/docs/git-checkout section `git checkout []` - branchName := branchRef.Name().Short() - log.Printf("Creating branch %s to track remote branch %s from %s", branchName, branchName, remote) +func checkoutRemoteBranch(projectPath, branchName, remote string) error { + log.Printf("Checking out remote branch %s", branchName) if err := shell.GitCheckoutBranch(projectPath, branchName, remote); err != nil { return fmt.Errorf("failed to checkout branch %s: %s", branchName, err) } - return nil } -func checkoutTag(projectPath, remote string, tagRef *plumbing.Reference) error { - tagName := tagRef.Name().Short() - log.Printf("Checking out tag %s from remote %s", tagName, remote) +func checkoutTag(projectPath, tagName string) error { + log.Printf("Checking out tag %s", tagName) if err := shell.GitCheckoutRef(projectPath, tagName); err != nil { return fmt.Errorf("failed to checkout tag %s: %s", tagName, err) } - return nil } diff --git a/project-clone/internal/shell/execute.go b/project-clone/internal/shell/execute.go index 579f5989e..ce2d3feb0 100644 --- a/project-clone/internal/shell/execute.go +++ b/project-clone/internal/shell/execute.go @@ -22,6 +22,16 @@ import ( "os/exec" ) +type GitRefType int64 + +const ( + GitRefUnknown GitRefType = iota + GitRefLocalBranch + GitRefRemoteBranch + GitRefTag + GitRefHash +) + // GitCloneProject constructs a command-line string for cloning a git project, and delegates execution // to the os/exec package. func GitCloneProject(repoUrl, defaultRemoteName, destPath string) error { @@ -139,9 +149,46 @@ func GitSetTrackingRemoteBranch(projectPath, branchName, remote string) error { return executeCommand("git", "branch", "--set-upstream-to", fmt.Sprintf("%s/%s", remote, branchName), branchName) } +// GitResolveReference determines if the provided revision is a (local) branch, tag, or hash for use when preparing a +// cloned repository. This is done by using `git show-ref` for branches/tags and `git rev-parse` for checking whether +// a commit hash exists. If the reference type cannot be determined, GitRefUnknown is returned. +func GitResolveReference(projectPath, remote, revision string) (GitRefType, error) { + currDir, err := os.Getwd() + if err != nil { + return GitRefUnknown, fmt.Errorf("failed to get current working directory: %s", err) + } + defer func() { + if err := os.Chdir(currDir); err != nil { + log.Printf("failed to return to original working directory: %s", err) + } + }() + err = os.Chdir(projectPath) + if err != nil { + return GitRefUnknown, fmt.Errorf("failed to move to project directory %s: %s", projectPath, err) + } + if err := executeCommandSilent("git", "show-ref", "-q", "--verify", fmt.Sprintf("refs/heads/%s", revision)); err == nil { + return GitRefLocalBranch, nil + } + if err := executeCommandSilent("git", "show-ref", "-q", "--verify", fmt.Sprintf("refs/remotes/%s/%s", remote, revision)); err == nil { + return GitRefRemoteBranch, nil + } + if err := executeCommandSilent("git", "show-ref", "-q", "--verify", fmt.Sprintf("refs/tags/%s", revision)); err == nil { + return GitRefTag, nil + } + if err := executeCommandSilent("git", "rev-parse", "-q", "--verify", fmt.Sprintf("%s^{commit}", revision)); err == nil { + return GitRefHash, nil + } + return GitRefUnknown, nil +} + func executeCommand(name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout return cmd.Run() } + +func executeCommandSilent(name string, args ...string) error { + cmd := exec.Command(name, args...) + return cmd.Run() +} diff --git a/samples/git-clone-sample.yaml b/samples/git-clone-sample.yaml index 46af77b17..0e6241ef4 100644 --- a/samples/git-clone-sample.yaml +++ b/samples/git-clone-sample.yaml @@ -13,8 +13,8 @@ spec: - name: devworkspace-operator git: checkoutFrom: - remote: amisevsk - revision: clone-projects-on-start + remote: origin + revision: 0.21.x remotes: origin: "https://github.com/devfile/devworkspace-operator.git" amisevsk: "https://github.com/amisevsk/devworkspace-operator.git"