From 12c0039b1dbc2646a1c5b6f570b74eade2e5d287 Mon Sep 17 00:00:00 2001 From: Toma Puljak Date: Thu, 12 Dec 2024 12:40:06 +0000 Subject: [PATCH] refactor: git service move all git operations into the git service Signed-off-by: Toma Puljak --- pkg/agent/toolbox/git/add.go | 27 +- pkg/agent/toolbox/git/clone_repository.go | 57 ++-- pkg/agent/toolbox/git/commit.go | 19 +- pkg/agent/toolbox/git/create_branch.go | 22 +- pkg/agent/toolbox/git/history.go | 35 +-- pkg/agent/toolbox/git/list_branches.go | 21 +- pkg/agent/toolbox/git/pull.go | 30 +- pkg/agent/toolbox/git/push.go | 21 +- pkg/agent/toolbox/git/types.go | 10 - pkg/git/add.go | 27 ++ pkg/git/branch.go | 46 +++ pkg/git/clone.go | 89 ++++++ pkg/git/commit.go | 25 ++ pkg/git/config.go | 173 +++++++++++ pkg/git/log.go | 41 +++ pkg/git/pull.go | 29 ++ pkg/git/push.go | 23 ++ pkg/git/service.go | 346 ---------------------- pkg/git/status.go | 119 ++++++++ pkg/git/types.go | 14 + 20 files changed, 652 insertions(+), 522 deletions(-) create mode 100644 pkg/git/add.go create mode 100644 pkg/git/branch.go create mode 100644 pkg/git/clone.go create mode 100644 pkg/git/commit.go create mode 100644 pkg/git/config.go create mode 100644 pkg/git/log.go create mode 100644 pkg/git/pull.go create mode 100644 pkg/git/push.go create mode 100644 pkg/git/status.go create mode 100644 pkg/git/types.go diff --git a/pkg/agent/toolbox/git/add.go b/pkg/agent/toolbox/git/add.go index ac11b493ff..3ab6eb8134 100644 --- a/pkg/agent/toolbox/git/add.go +++ b/pkg/agent/toolbox/git/add.go @@ -4,8 +4,8 @@ package git import ( + "github.com/daytonaio/daytona/pkg/git" "github.com/gin-gonic/gin" - "github.com/go-git/go-git/v5" ) func AddFiles(c *gin.Context) { @@ -15,33 +15,14 @@ func AddFiles(c *gin.Context) { return } - repo, err := git.PlainOpen(req.Path) - if err != nil { - c.AbortWithError(400, err) - return + gitService := git.Service{ + ProjectDir: req.Path, } - w, err := repo.Worktree() - if err != nil { + if err := gitService.Add(req.Files); err != nil { c.AbortWithError(400, err) return } - if len(req.Files) == 1 && req.Files[0] == "." { - _, err = w.Add(".") - if err != nil { - c.AbortWithError(400, err) - return - } - } else { - for _, file := range req.Files { - _, err = w.Add(file) - if err != nil { - c.AbortWithError(400, err) - return - } - } - } - c.Status(200) } diff --git a/pkg/agent/toolbox/git/clone_repository.go b/pkg/agent/toolbox/git/clone_repository.go index 0b1efcac49..fd5bec42e8 100644 --- a/pkg/agent/toolbox/git/clone_repository.go +++ b/pkg/agent/toolbox/git/clone_repository.go @@ -4,11 +4,9 @@ package git import ( - "fmt" - + "github.com/daytonaio/daytona/pkg/git" + "github.com/daytonaio/daytona/pkg/gitprovider" "github.com/gin-gonic/gin" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport/http" ) @@ -19,49 +17,40 @@ func CloneRepository(c *gin.Context) { return } - options := &git.CloneOptions{ - URL: req.URL, - Progress: nil, + branch := "main" + if req.Branch != nil { + branch = *req.Branch + } + + repo := gitprovider.GitRepository{ + Url: req.URL, + Branch: branch, + } + + if req.CommitID != nil { + repo.Target = gitprovider.CloneTargetCommit + repo.Sha = *req.CommitID } + gitService := git.Service{ + ProjectDir: req.Path, + } + + var auth *http.BasicAuth + // Set authentication if provided if req.Username != nil && req.Password != nil { - options.Auth = &http.BasicAuth{ + auth = &http.BasicAuth{ Username: *req.Username, Password: *req.Password, } } - // Handle branch or commit specification - if req.Branch != nil { - options.ReferenceName = plumbing.NewBranchReferenceName(*req.Branch) - options.SingleBranch = true - } - - // Clone the repository - repo, err := git.PlainClone(req.Path, false, options) + err := gitService.CloneRepository(&repo, auth) if err != nil { c.AbortWithError(400, err) return } - // If a specific commit is requested, checkout that commit - if req.CommitID != nil { - worktree, err := repo.Worktree() - if err != nil { - c.AbortWithError(400, fmt.Errorf("failed to get worktree: %w", err)) - return - } - - hash := plumbing.NewHash(*req.CommitID) - err = worktree.Checkout(&git.CheckoutOptions{ - Hash: hash, - }) - if err != nil { - c.AbortWithError(400, fmt.Errorf("failed to checkout commit: %w", err)) - return - } - } - c.Status(200) } diff --git a/pkg/agent/toolbox/git/commit.go b/pkg/agent/toolbox/git/commit.go index 39ff3a2e99..d03bbbe236 100644 --- a/pkg/agent/toolbox/git/commit.go +++ b/pkg/agent/toolbox/git/commit.go @@ -6,8 +6,9 @@ package git import ( "time" + "github.com/daytonaio/daytona/pkg/git" "github.com/gin-gonic/gin" - "github.com/go-git/go-git/v5" + go_git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" ) @@ -18,19 +19,11 @@ func CommitChanges(c *gin.Context) { return } - repo, err := git.PlainOpen(req.Path) - if err != nil { - c.AbortWithError(400, err) - return - } - - worktree, err := repo.Worktree() - if err != nil { - c.AbortWithError(400, err) - return + gitService := git.Service{ + ProjectDir: req.Path, } - commit, err := worktree.Commit(req.Message, &git.CommitOptions{ + commitSha, err := gitService.Commit(req.Message, &go_git.CommitOptions{ Author: &object.Signature{ Name: req.Author, Email: req.Email, @@ -44,6 +37,6 @@ func CommitChanges(c *gin.Context) { } c.JSON(200, GitCommitResponse{ - Hash: commit.String(), + Hash: commitSha, }) } diff --git a/pkg/agent/toolbox/git/create_branch.go b/pkg/agent/toolbox/git/create_branch.go index 2c1ddd1b54..5c9d05d97d 100644 --- a/pkg/agent/toolbox/git/create_branch.go +++ b/pkg/agent/toolbox/git/create_branch.go @@ -4,9 +4,8 @@ package git import ( + "github.com/daytonaio/daytona/pkg/git" "github.com/gin-gonic/gin" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" ) func CreateBranch(c *gin.Context) { @@ -16,24 +15,11 @@ func CreateBranch(c *gin.Context) { return } - repo, err := git.PlainOpen(req.Path) - if err != nil { - c.AbortWithError(400, err) - return + gitService := git.Service{ + ProjectDir: req.Path, } - worktree, err := repo.Worktree() - if err != nil { - c.AbortWithError(400, err) - return - } - - err = worktree.Checkout(&git.CheckoutOptions{ - Create: true, - Branch: plumbing.NewBranchReferenceName(req.Name), - }) - - if err != nil { + if err := gitService.CreateBranch(req.Name); err != nil { c.AbortWithError(400, err) return } diff --git a/pkg/agent/toolbox/git/history.go b/pkg/agent/toolbox/git/history.go index f18d7fb6f7..1894efe83b 100644 --- a/pkg/agent/toolbox/git/history.go +++ b/pkg/agent/toolbox/git/history.go @@ -6,9 +6,8 @@ package git import ( "errors" + "github.com/daytonaio/daytona/pkg/git" "github.com/gin-gonic/gin" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/object" ) func GetCommitHistory(c *gin.Context) { @@ -18,39 +17,15 @@ func GetCommitHistory(c *gin.Context) { return } - repo, err := git.PlainOpen(path) - if err != nil { - c.AbortWithError(400, err) - return - } - - ref, err := repo.Head() - if err != nil { - c.AbortWithError(400, err) - return - } - - commits, err := repo.Log(&git.LogOptions{From: ref.Hash()}) - if err != nil { - c.AbortWithError(400, err) - return + gitService := git.Service{ + ProjectDir: path, } - var history []GitCommitInfo - err = commits.ForEach(func(commit *object.Commit) error { - history = append(history, GitCommitInfo{ - Hash: commit.Hash.String(), - Author: commit.Author.Name, - Email: commit.Author.Email, - Message: commit.Message, - Timestamp: commit.Author.When, - }) - return nil - }) + log, err := gitService.Log() if err != nil { c.AbortWithError(400, err) return } - c.JSON(200, history) + c.JSON(200, log) } diff --git a/pkg/agent/toolbox/git/list_branches.go b/pkg/agent/toolbox/git/list_branches.go index 8b5ae88e57..18553ebab6 100644 --- a/pkg/agent/toolbox/git/list_branches.go +++ b/pkg/agent/toolbox/git/list_branches.go @@ -6,9 +6,8 @@ package git import ( "errors" + "github.com/daytonaio/daytona/pkg/git" "github.com/gin-gonic/gin" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" ) func ListBranches(c *gin.Context) { @@ -18,23 +17,11 @@ func ListBranches(c *gin.Context) { return } - repo, err := git.PlainOpen(path) - if err != nil { - c.AbortWithError(400, err) - return + gitService := git.Service{ + ProjectDir: path, } - branches, err := repo.Branches() - if err != nil { - c.AbortWithError(400, err) - return - } - - var branchList []string - err = branches.ForEach(func(ref *plumbing.Reference) error { - branchList = append(branchList, ref.Name().Short()) - return nil - }) + branchList, err := gitService.ListBranches() if err != nil { c.AbortWithError(400, err) return diff --git a/pkg/agent/toolbox/git/pull.go b/pkg/agent/toolbox/git/pull.go index 02363f89db..ba7727f361 100644 --- a/pkg/agent/toolbox/git/pull.go +++ b/pkg/agent/toolbox/git/pull.go @@ -4,8 +4,9 @@ package git import ( + "github.com/daytonaio/daytona/pkg/git" "github.com/gin-gonic/gin" - "github.com/go-git/go-git/v5" + go_git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/transport/http" ) @@ -16,31 +17,20 @@ func PullChanges(c *gin.Context) { return } - repo, err := git.PlainOpen(req.Path) - if err != nil { - c.AbortWithError(400, err) - return - } - - w, err := repo.Worktree() - if err != nil { - c.AbortWithError(400, err) - return - } - - options := &git.PullOptions{ - RemoteName: "origin", - } - + var auth *http.BasicAuth if req.Username != nil && req.Password != nil { - options.Auth = &http.BasicAuth{ + auth = &http.BasicAuth{ Username: *req.Username, Password: *req.Password, } } - err = w.Pull(options) - if err != nil && err != git.NoErrAlreadyUpToDate { + gitService := git.Service{ + ProjectDir: req.Path, + } + + err := gitService.Pull(auth) + if err != nil && err != go_git.NoErrAlreadyUpToDate { c.AbortWithError(400, err) return } diff --git a/pkg/agent/toolbox/git/push.go b/pkg/agent/toolbox/git/push.go index e7ce7248b9..94c2459c5f 100644 --- a/pkg/agent/toolbox/git/push.go +++ b/pkg/agent/toolbox/git/push.go @@ -4,8 +4,9 @@ package git import ( + "github.com/daytonaio/daytona/pkg/git" "github.com/gin-gonic/gin" - "github.com/go-git/go-git/v5" + go_git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/transport/http" ) @@ -16,22 +17,20 @@ func PushChanges(c *gin.Context) { return } - repo, err := git.PlainOpen(req.Path) - if err != nil { - c.AbortWithError(400, err) - return - } - - options := &git.PushOptions{} + var auth *http.BasicAuth if req.Username != nil && req.Password != nil { - options.Auth = &http.BasicAuth{ + auth = &http.BasicAuth{ Username: *req.Username, Password: *req.Password, } } - err = repo.Push(options) - if err != nil && err != git.NoErrAlreadyUpToDate { + gitService := git.Service{ + ProjectDir: req.Path, + } + + err := gitService.Push(auth) + if err != nil && err != go_git.NoErrAlreadyUpToDate { c.AbortWithError(400, err) return } diff --git a/pkg/agent/toolbox/git/types.go b/pkg/agent/toolbox/git/types.go index 9f608b41ef..8974f5102c 100644 --- a/pkg/agent/toolbox/git/types.go +++ b/pkg/agent/toolbox/git/types.go @@ -3,8 +3,6 @@ package git -import "time" - type GitAddRequest struct { Path string `json:"path" validate:"required"` // files to add (use . for all files) @@ -36,14 +34,6 @@ type GitBranchRequest struct { Name string `json:"name" validate:"required"` } // @name GitBranchRequest -type GitCommitInfo struct { - Hash string `json:"hash" validate:"required"` - Author string `json:"author" validate:"required"` - Email string `json:"email" validate:"required"` - Message string `json:"message" validate:"required"` - Timestamp time.Time `json:"timestamp" validate:"required"` -} // @name GitCommitInfo - type ListBranchResponse struct { Branches []string `json:"branches" validate:"required"` } // @name ListBranchResponse diff --git a/pkg/git/add.go b/pkg/git/add.go new file mode 100644 index 0000000000..b78aae41d3 --- /dev/null +++ b/pkg/git/add.go @@ -0,0 +1,27 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import "github.com/go-git/go-git/v5" + +func (s *Service) Add(files []string) error { + repo, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return err + } + + w, err := repo.Worktree() + if err != nil { + return err + } + + for _, file := range files { + _, err = w.Add(file) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/git/branch.go b/pkg/git/branch.go new file mode 100644 index 0000000000..f97c799bb2 --- /dev/null +++ b/pkg/git/branch.go @@ -0,0 +1,46 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" +) + +func (s *Service) CreateBranch(name string) error { + repo, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return err + } + + w, err := repo.Worktree() + if err != nil { + return err + } + + return w.Checkout(&git.CheckoutOptions{ + Create: true, + Branch: plumbing.NewBranchReferenceName(name), + }) +} + +func (s *Service) ListBranches() ([]string, error) { + repo, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return []string{}, err + } + + branches, err := repo.Branches() + if err != nil { + return []string{}, err + } + + var branchList []string + err = branches.ForEach(func(ref *plumbing.Reference) error { + branchList = append(branchList, ref.Name().Short()) + return nil + }) + + return branchList, err +} diff --git a/pkg/git/clone.go b/pkg/git/clone.go new file mode 100644 index 0000000000..6e5fae4f6d --- /dev/null +++ b/pkg/git/clone.go @@ -0,0 +1,89 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + "fmt" + "strings" + + "github.com/daytonaio/daytona/pkg/gitprovider" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" +) + +func (s *Service) CloneRepository(repo *gitprovider.GitRepository, auth *http.BasicAuth) error { + cloneOptions := &git.CloneOptions{ + URL: repo.Url, + SingleBranch: true, + InsecureSkipTLS: true, + Auth: auth, + } + + if s.LogWriter != nil { + cloneOptions.Progress = s.LogWriter + } + + // Azure DevOps requires capabilities multi_ack / multi_ack_detailed, + // which are not fully implemented and by default are included in + // transport.UnsupportedCapabilities. + // + // This can be removed once go-git implements the git v2 protocol. + transport.UnsupportedCapabilities = []capability.Capability{ + capability.ThinPack, + } + + cloneOptions.ReferenceName = plumbing.ReferenceName("refs/heads/" + repo.Branch) + + _, err := git.PlainClone(s.ProjectDir, false, cloneOptions) + if err != nil { + return err + } + + if repo.Target == gitprovider.CloneTargetCommit { + r, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return err + } + + w, err := r.Worktree() + if err != nil { + return err + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(repo.Sha), + }) + if err != nil { + return err + } + } + + return err +} + +func (s *Service) CloneRepositoryCmd(repo *gitprovider.GitRepository, auth *http.BasicAuth) []string { + cloneCmd := []string{"git", "clone", "--single-branch", "--branch", fmt.Sprintf("\"%s\"", repo.Branch)} + cloneUrl := repo.Url + + // Default to https protocol if not specified + if !strings.Contains(cloneUrl, "://") { + cloneUrl = fmt.Sprintf("https://%s", cloneUrl) + } + + if auth != nil { + cloneUrl = fmt.Sprintf("%s://%s:%s@%s", strings.Split(cloneUrl, "://")[0], auth.Username, auth.Password, strings.SplitN(cloneUrl, "://", 2)[1]) + } + + cloneCmd = append(cloneCmd, cloneUrl, s.ProjectDir) + + if repo.Target == gitprovider.CloneTargetCommit { + cloneCmd = append(cloneCmd, "&&", "cd", s.ProjectDir) + cloneCmd = append(cloneCmd, "&&", "git", "checkout", repo.Sha) + } + + return cloneCmd +} diff --git a/pkg/git/commit.go b/pkg/git/commit.go new file mode 100644 index 0000000000..6acd3583dd --- /dev/null +++ b/pkg/git/commit.go @@ -0,0 +1,25 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import "github.com/go-git/go-git/v5" + +func (s *Service) Commit(message string, options *git.CommitOptions) (string, error) { + repo, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return "", err + } + + w, err := repo.Worktree() + if err != nil { + return "", err + } + + commit, err := w.Commit(message, options) + if err != nil { + return "", err + } + + return commit.String(), nil +} diff --git a/pkg/git/config.go b/pkg/git/config.go new file mode 100644 index 0000000000..b1a6638e02 --- /dev/null +++ b/pkg/git/config.go @@ -0,0 +1,173 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + + "github.com/daytonaio/daytona/pkg/gitprovider" + "gopkg.in/ini.v1" +) + +func (s *Service) SetGitConfig(userData *gitprovider.GitUser, providerConfig *gitprovider.GitProviderConfig) error { + gitConfigFileName := s.GitConfigFileName + + var gitConfigContent []byte + gitConfigContent, err := os.ReadFile(gitConfigFileName) + if err != nil { + gitConfigContent = []byte{} + } + + cfg, err := ini.Load(gitConfigContent) + if err != nil { + return err + } + + if !cfg.HasSection("credential") { + _, err := cfg.NewSection("credential") + if err != nil { + return err + } + } + + _, err = cfg.Section("credential").NewKey("helper", "/usr/local/bin/daytona git-cred") + if err != nil { + return err + } + + if !cfg.HasSection("safe") { + _, err := cfg.NewSection("safe") + if err != nil { + return err + } + } + _, err = cfg.Section("safe").NewKey("directory", s.ProjectDir) + if err != nil { + return err + } + + if userData != nil { + if !cfg.HasSection("user") { + _, err := cfg.NewSection("user") + if err != nil { + return err + } + } + + _, err := cfg.Section("user").NewKey("name", userData.Name) + if err != nil { + return err + } + + _, err = cfg.Section("user").NewKey("email", userData.Email) + if err != nil { + return err + } + } + + if err := s.setSigningConfig(cfg, providerConfig, userData); err != nil { + return err + } + + var buf bytes.Buffer + _, err = cfg.WriteTo(&buf) + if err != nil { + return err + } + + return os.WriteFile(gitConfigFileName, buf.Bytes(), 0644) +} + +func (s *Service) setSigningConfig(cfg *ini.File, providerConfig *gitprovider.GitProviderConfig, userData *gitprovider.GitUser) error { + if providerConfig == nil || providerConfig.SigningMethod == nil || providerConfig.SigningKey == nil { + return nil + } + + if !cfg.HasSection("user") { + _, err := cfg.NewSection("user") + if err != nil { + return err + } + } + + _, err := cfg.Section("user").NewKey("signingkey", *providerConfig.SigningKey) + if err != nil { + return err + } + + if !cfg.HasSection("commit") { + _, err := cfg.NewSection("commit") + if err != nil { + return err + } + } + + switch *providerConfig.SigningMethod { + case gitprovider.SigningMethodGPG: + _, err := cfg.Section("commit").NewKey("gpgSign", "true") + if err != nil { + return err + } + case gitprovider.SigningMethodSSH: + err := s.configureAllowedSigners(userData.Email, *providerConfig.SigningKey) + if err != nil { + return err + } + + if !cfg.HasSection("gpg") { + _, err := cfg.NewSection("gpg") + if err != nil { + return err + } + } + _, err = cfg.Section("gpg").NewKey("format", "ssh") + if err != nil { + return err + } + + if !cfg.HasSection("gpg \"ssh\"") { + _, err := cfg.NewSection("gpg \"ssh\"") + if err != nil { + return err + } + } + + allowedSignersFile := filepath.Join(os.Getenv("HOME"), ".ssh/allowed_signers") + _, err = cfg.Section("gpg \"ssh\"").NewKey("allowedSignersFile", allowedSignersFile) + if err != nil { + return err + } + } + return nil +} + +func (s *Service) configureAllowedSigners(email, sshKey string) error { + homeDir := os.Getenv("HOME") + sshDir := filepath.Join(homeDir, ".ssh") + allowedSignersFile := filepath.Join(sshDir, "allowed_signers") + + err := os.MkdirAll(sshDir, 0700) + if err != nil { + return fmt.Errorf("failed to create SSH directory: %w", err) + } + + entry := fmt.Sprintf("%s namespaces=\"git\" %s\n", email, sshKey) + + existingContent, err := os.ReadFile(allowedSignersFile) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to read allowed_signers file: %w", err) + } + + newContent := string(existingContent) + entry + + err = os.WriteFile(allowedSignersFile, []byte(newContent), 0600) + if err != nil { + return fmt.Errorf("failed to write to allowed_signers file: %w", err) + } + + return nil +} diff --git a/pkg/git/log.go b/pkg/git/log.go new file mode 100644 index 0000000000..dca50c0b59 --- /dev/null +++ b/pkg/git/log.go @@ -0,0 +1,41 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + "github.com/go-git/go-git/v5/plumbing/object" + + "github.com/go-git/go-git/v5" +) + +func (s *Service) Log() ([]GitCommitInfo, error) { + repo, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return []GitCommitInfo{}, err + } + + ref, err := repo.Head() + if err != nil { + return []GitCommitInfo{}, err + } + + commits, err := repo.Log(&git.LogOptions{From: ref.Hash()}) + if err != nil { + return []GitCommitInfo{}, err + } + + var history []GitCommitInfo + err = commits.ForEach(func(commit *object.Commit) error { + history = append(history, GitCommitInfo{ + Hash: commit.Hash.String(), + Author: commit.Author.Name, + Email: commit.Author.Email, + Message: commit.Message, + Timestamp: commit.Author.When, + }) + return nil + }) + + return history, err +} diff --git a/pkg/git/pull.go b/pkg/git/pull.go new file mode 100644 index 0000000000..bd86ccd188 --- /dev/null +++ b/pkg/git/pull.go @@ -0,0 +1,29 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + "github.com/go-git/go-git/v5/plumbing/transport/http" + + "github.com/go-git/go-git/v5" +) + +func (s *Service) Pull(auth *http.BasicAuth) error { + repo, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return err + } + + w, err := repo.Worktree() + if err != nil { + return err + } + + options := &git.PullOptions{ + RemoteName: "origin", + Auth: auth, + } + + return w.Pull(options) +} diff --git a/pkg/git/push.go b/pkg/git/push.go new file mode 100644 index 0000000000..abbd23d89f --- /dev/null +++ b/pkg/git/push.go @@ -0,0 +1,23 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + "github.com/go-git/go-git/v5/plumbing/transport/http" + + "github.com/go-git/go-git/v5" +) + +func (s *Service) Push(auth *http.BasicAuth) error { + repo, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return err + } + + options := &git.PushOptions{ + Auth: auth, + } + + return repo.Push(options) +} diff --git a/pkg/git/service.go b/pkg/git/service.go index 1c66db81ab..38fb5bf4e5 100644 --- a/pkg/git/service.go +++ b/pkg/git/service.go @@ -4,23 +4,14 @@ package git import ( - "bytes" - "fmt" "io" "os" - "os/exec" "path/filepath" - "strconv" - "strings" "github.com/daytonaio/daytona/pkg/gitprovider" "github.com/daytonaio/daytona/pkg/workspace/project" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" - "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" - "gopkg.in/ini.v1" ) var MapStatus map[git.StatusCode]project.Status = map[git.StatusCode]project.Status{ @@ -49,79 +40,6 @@ type Service struct { OpenRepository *git.Repository } -func (s *Service) CloneRepository(repo *gitprovider.GitRepository, auth *http.BasicAuth) error { - cloneOptions := &git.CloneOptions{ - URL: repo.Url, - SingleBranch: true, - InsecureSkipTLS: true, - Auth: auth, - } - - if s.LogWriter != nil { - cloneOptions.Progress = s.LogWriter - } - - // Azure DevOps requires capabilities multi_ack / multi_ack_detailed, - // which are not fully implemented and by default are included in - // transport.UnsupportedCapabilities. - // - // This can be removed once go-git implements the git v2 protocol. - transport.UnsupportedCapabilities = []capability.Capability{ - capability.ThinPack, - } - - cloneOptions.ReferenceName = plumbing.ReferenceName("refs/heads/" + repo.Branch) - - _, err := git.PlainClone(s.ProjectDir, false, cloneOptions) - if err != nil { - return err - } - - if repo.Target == gitprovider.CloneTargetCommit { - r, err := git.PlainOpen(s.ProjectDir) - if err != nil { - return err - } - - w, err := r.Worktree() - if err != nil { - return err - } - - err = w.Checkout(&git.CheckoutOptions{ - Hash: plumbing.NewHash(repo.Sha), - }) - if err != nil { - return err - } - } - - return err -} - -func (s *Service) CloneRepositoryCmd(repo *gitprovider.GitRepository, auth *http.BasicAuth) []string { - cloneCmd := []string{"git", "clone", "--single-branch", "--branch", fmt.Sprintf("\"%s\"", repo.Branch)} - cloneUrl := repo.Url - - // Default to https protocol if not specified - if !strings.Contains(cloneUrl, "://") { - cloneUrl = fmt.Sprintf("https://%s", cloneUrl) - } - - if auth != nil { - cloneUrl = fmt.Sprintf("%s://%s:%s@%s", strings.Split(cloneUrl, "://")[0], auth.Username, auth.Password, strings.SplitN(cloneUrl, "://", 2)[1]) - } - - cloneCmd = append(cloneCmd, cloneUrl, s.ProjectDir) - - if repo.Target == gitprovider.CloneTargetCommit { - cloneCmd = append(cloneCmd, "&&", "cd", s.ProjectDir) - cloneCmd = append(cloneCmd, "&&", "git", "checkout", repo.Sha) - } - - return cloneCmd -} - func (s *Service) RepositoryExists() (bool, error) { _, err := os.Stat(filepath.Join(s.ProjectDir, ".git")) if os.IsNotExist(err) { @@ -132,267 +50,3 @@ func (s *Service) RepositoryExists() (bool, error) { } return true, nil } - -func (s *Service) SetGitConfig(userData *gitprovider.GitUser, providerConfig *gitprovider.GitProviderConfig) error { - gitConfigFileName := s.GitConfigFileName - - var gitConfigContent []byte - gitConfigContent, err := os.ReadFile(gitConfigFileName) - if err != nil { - gitConfigContent = []byte{} - } - - cfg, err := ini.Load(gitConfigContent) - if err != nil { - return err - } - - if !cfg.HasSection("credential") { - _, err := cfg.NewSection("credential") - if err != nil { - return err - } - } - - _, err = cfg.Section("credential").NewKey("helper", "/usr/local/bin/daytona git-cred") - if err != nil { - return err - } - - if !cfg.HasSection("safe") { - _, err := cfg.NewSection("safe") - if err != nil { - return err - } - } - _, err = cfg.Section("safe").NewKey("directory", s.ProjectDir) - if err != nil { - return err - } - - if userData != nil { - if !cfg.HasSection("user") { - _, err := cfg.NewSection("user") - if err != nil { - return err - } - } - - _, err := cfg.Section("user").NewKey("name", userData.Name) - if err != nil { - return err - } - - _, err = cfg.Section("user").NewKey("email", userData.Email) - if err != nil { - return err - } - } - - if err := s.setSigningConfig(cfg, providerConfig, userData); err != nil { - return err - } - - var buf bytes.Buffer - _, err = cfg.WriteTo(&buf) - if err != nil { - return err - } - - return os.WriteFile(gitConfigFileName, buf.Bytes(), 0644) -} - -func (s *Service) setSigningConfig(cfg *ini.File, providerConfig *gitprovider.GitProviderConfig, userData *gitprovider.GitUser) error { - if providerConfig == nil || providerConfig.SigningMethod == nil || providerConfig.SigningKey == nil { - return nil - } - - if !cfg.HasSection("user") { - _, err := cfg.NewSection("user") - if err != nil { - return err - } - } - - _, err := cfg.Section("user").NewKey("signingkey", *providerConfig.SigningKey) - if err != nil { - return err - } - - if !cfg.HasSection("commit") { - _, err := cfg.NewSection("commit") - if err != nil { - return err - } - } - - switch *providerConfig.SigningMethod { - case gitprovider.SigningMethodGPG: - _, err := cfg.Section("commit").NewKey("gpgSign", "true") - if err != nil { - return err - } - case gitprovider.SigningMethodSSH: - err := s.configureAllowedSigners(userData.Email, *providerConfig.SigningKey) - if err != nil { - return err - } - - if !cfg.HasSection("gpg") { - _, err := cfg.NewSection("gpg") - if err != nil { - return err - } - } - _, err = cfg.Section("gpg").NewKey("format", "ssh") - if err != nil { - return err - } - - if !cfg.HasSection("gpg \"ssh\"") { - _, err := cfg.NewSection("gpg \"ssh\"") - if err != nil { - return err - } - } - - allowedSignersFile := filepath.Join(os.Getenv("HOME"), ".ssh/allowed_signers") - _, err = cfg.Section("gpg \"ssh\"").NewKey("allowedSignersFile", allowedSignersFile) - if err != nil { - return err - } - } - return nil -} - -func (s *Service) configureAllowedSigners(email, sshKey string) error { - homeDir := os.Getenv("HOME") - sshDir := filepath.Join(homeDir, ".ssh") - allowedSignersFile := filepath.Join(sshDir, "allowed_signers") - - err := os.MkdirAll(sshDir, 0700) - if err != nil { - return fmt.Errorf("failed to create SSH directory: %w", err) - } - - entry := fmt.Sprintf("%s namespaces=\"git\" %s\n", email, sshKey) - - existingContent, err := os.ReadFile(allowedSignersFile) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read allowed_signers file: %w", err) - } - - newContent := string(existingContent) + entry - - err = os.WriteFile(allowedSignersFile, []byte(newContent), 0600) - if err != nil { - return fmt.Errorf("failed to write to allowed_signers file: %w", err) - } - - return nil -} - -func (s *Service) isBranchPublished() (bool, error) { - upstream, err := s.getUpstreamBranch() - if err != nil { - return false, err - } - return upstream != "", nil -} - -func (s *Service) getUpstreamBranch() (string, error) { - cmd := exec.Command("git", "-C", s.ProjectDir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}") - out, err := cmd.CombinedOutput() - if err != nil { - return "", nil - } - - return strings.TrimSpace(string(out)), nil -} - -func (s *Service) getAheadBehindInfo() (int, int, error) { - upstream, err := s.getUpstreamBranch() - if err != nil { - return 0, 0, err - } - if upstream == "" { - return 0, 0, nil - } - - cmd := exec.Command("git", "-C", s.ProjectDir, "rev-list", "--left-right", "--count", fmt.Sprintf("%s...HEAD", upstream)) - out, err := cmd.CombinedOutput() - if err != nil { - return 0, 0, nil - } - - return parseAheadBehind(out) -} - -func parseAheadBehind(output []byte) (int, int, error) { - counts := strings.Split(strings.TrimSpace(string(output)), "\t") - if len(counts) != 2 { - return 0, 0, nil - } - - ahead, err := strconv.Atoi(counts[1]) - if err != nil { - return 0, 0, nil - } - - behind, err := strconv.Atoi(counts[0]) - if err != nil { - return 0, 0, nil - } - - return ahead, behind, nil -} - -func (s *Service) GetGitStatus() (*project.GitStatus, error) { - repo, err := git.PlainOpen(s.ProjectDir) - if err != nil { - return nil, err - } - - ref, err := repo.Head() - if err != nil { - return nil, err - } - - worktree, err := repo.Worktree() - if err != nil { - return nil, err - } - - status, err := worktree.Status() - if err != nil { - return nil, err - } - - files := []*project.FileStatus{} - for path, file := range status { - files = append(files, &project.FileStatus{ - Name: path, - Extra: file.Extra, - Staging: MapStatus[file.Staging], - Worktree: MapStatus[file.Worktree], - }) - } - - branchPublished, err := s.isBranchPublished() - if err != nil { - return nil, err - } - - ahead, behind, err := s.getAheadBehindInfo() - if err != nil { - return nil, err - } - - return &project.GitStatus{ - CurrentBranch: ref.Name().Short(), - Files: files, - BranchPublished: branchPublished, - Ahead: ahead, - Behind: behind, - }, nil -} diff --git a/pkg/git/status.go b/pkg/git/status.go new file mode 100644 index 0000000000..f8f202a080 --- /dev/null +++ b/pkg/git/status.go @@ -0,0 +1,119 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/daytonaio/daytona/pkg/workspace/project" + "github.com/go-git/go-git/v5" +) + +func (s *Service) GetGitStatus() (*project.GitStatus, error) { + repo, err := git.PlainOpen(s.ProjectDir) + if err != nil { + return nil, err + } + + ref, err := repo.Head() + if err != nil { + return nil, err + } + + worktree, err := repo.Worktree() + if err != nil { + return nil, err + } + + status, err := worktree.Status() + if err != nil { + return nil, err + } + + files := []*project.FileStatus{} + for path, file := range status { + files = append(files, &project.FileStatus{ + Name: path, + Extra: file.Extra, + Staging: MapStatus[file.Staging], + Worktree: MapStatus[file.Worktree], + }) + } + + branchPublished, err := s.isBranchPublished() + if err != nil { + return nil, err + } + + ahead, behind, err := s.getAheadBehindInfo() + if err != nil { + return nil, err + } + + return &project.GitStatus{ + CurrentBranch: ref.Name().Short(), + Files: files, + BranchPublished: branchPublished, + Ahead: ahead, + Behind: behind, + }, nil +} + +func (s *Service) isBranchPublished() (bool, error) { + upstream, err := s.getUpstreamBranch() + if err != nil { + return false, err + } + return upstream != "", nil +} + +func (s *Service) getUpstreamBranch() (string, error) { + cmd := exec.Command("git", "-C", s.ProjectDir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}") + out, err := cmd.CombinedOutput() + if err != nil { + return "", nil + } + + return strings.TrimSpace(string(out)), nil +} + +func (s *Service) getAheadBehindInfo() (int, int, error) { + upstream, err := s.getUpstreamBranch() + if err != nil { + return 0, 0, err + } + if upstream == "" { + return 0, 0, nil + } + + cmd := exec.Command("git", "-C", s.ProjectDir, "rev-list", "--left-right", "--count", fmt.Sprintf("%s...HEAD", upstream)) + out, err := cmd.CombinedOutput() + if err != nil { + return 0, 0, nil + } + + return parseAheadBehind(out) +} + +func parseAheadBehind(output []byte) (int, int, error) { + counts := strings.Split(strings.TrimSpace(string(output)), "\t") + if len(counts) != 2 { + return 0, 0, nil + } + + ahead, err := strconv.Atoi(counts[1]) + if err != nil { + return 0, 0, nil + } + + behind, err := strconv.Atoi(counts[0]) + if err != nil { + return 0, 0, nil + } + + return ahead, behind, nil +} diff --git a/pkg/git/types.go b/pkg/git/types.go new file mode 100644 index 0000000000..b9cca1b0eb --- /dev/null +++ b/pkg/git/types.go @@ -0,0 +1,14 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package git + +import "time" + +type GitCommitInfo struct { + Hash string `json:"hash" validate:"required"` + Author string `json:"author" validate:"required"` + Email string `json:"email" validate:"required"` + Message string `json:"message" validate:"required"` + Timestamp time.Time `json:"timestamp" validate:"required"` +} // @name GitCommitInfo