Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 20 additions & 49 deletions project-clone/internal/git/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand All @@ -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 <remote-branch-name>`:
// 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 [<branch>]`
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
}

Expand Down
47 changes: 47 additions & 0 deletions project-clone/internal/shell/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
4 changes: 2 additions & 2 deletions samples/git-clone-sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down