Rework gitutils package to make it testable

This commit is contained in:
2024-11-19 22:43:24 +01:00
parent 53e52bfdb5
commit 7396b57a5d
4 changed files with 183 additions and 140 deletions

View File

@@ -0,0 +1,176 @@
// Package git provides functionalities to interact with Git repositories, including cloning, pulling, committing, and pushing changes.
package git
import (
"fmt"
"os"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/http"
)
// Config holds the configuration for a Git client
type Config struct {
URL string
Username string
Token string
WorkDir string
}
// Client defines the interface for Git operations
type Client interface {
Clone() error
Pull() error
Commit(message string) error
Push() error
EnsureRepo() error
}
// client implements the Client interface
type client struct {
Config
repo *git.Repository
}
// New creates a new Client instance
// Parameters:
// - url: the URL of the Git repository
// - username: the username for the Git repository
// - token: the access token for the Git repository
// - workDir: the local directory to clone the repository to
// Returns:
// - Client: the Git client
func New(url, username, token, workDir string) Client {
return &client{
Config: Config{
URL: url,
Username: username,
Token: token,
WorkDir: workDir,
},
}
}
// Clone clones the Git repository to the local directory
// Returns:
// - error: any error that occurred during cloning
func (c *client) Clone() error {
auth := &http.BasicAuth{
Username: c.Username,
Password: c.Token,
}
var err error
c.repo, err = git.PlainClone(c.WorkDir, false, &git.CloneOptions{
URL: c.URL,
Auth: auth,
Progress: os.Stdout,
})
if err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
return nil
}
// Pull pulls the latest changes from the remote repository
// Returns:
// - error: any error that occurred during pulling
func (c *client) Pull() error {
if c.repo == nil {
return fmt.Errorf("repository not initialized")
}
w, err := c.repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
auth := &http.BasicAuth{
Username: c.Username,
Password: c.Token,
}
err = w.Pull(&git.PullOptions{
Auth: auth,
Progress: os.Stdout,
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to pull changes: %w", err)
}
return nil
}
// Commit commits the changes in the repository
// Parameters:
// - message: the commit message
// Returns:
// - error: any error that occurred during committing
func (c *client) Commit(message string) error {
if c.repo == nil {
return fmt.Errorf("repository not initialized")
}
w, err := c.repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
_, err = w.Add(".")
if err != nil {
return fmt.Errorf("failed to add changes: %w", err)
}
_, err = w.Commit(message, &git.CommitOptions{})
if err != nil {
return fmt.Errorf("failed to commit changes: %w", err)
}
return nil
}
// Push pushes the changes to the remote repository
// Returns:
// - error: any error that occurred during pushing
func (c *client) Push() error {
if c.repo == nil {
return fmt.Errorf("repository not initialized")
}
auth := &http.BasicAuth{
Username: c.Username,
Password: c.Token,
}
err := c.repo.Push(&git.PushOptions{
Auth: auth,
Progress: os.Stdout,
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to push changes: %w", err)
}
return nil
}
// EnsureRepo ensures the local repository is up-to-date
// Returns:
// - error: any error that occurred during the operation
func (c *client) EnsureRepo() error {
if _, err := os.Stat(filepath.Join(c.WorkDir, ".git")); os.IsNotExist(err) {
return c.Clone()
}
var err error
c.repo, err = git.PlainOpen(c.WorkDir)
if err != nil {
return fmt.Errorf("failed to open existing repository: %w", err)
}
return c.Pull()
}

View File

@@ -1,133 +0,0 @@
package gitutils
import (
"fmt"
"os"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/http"
)
type GitRepo struct {
URL string
Username string
Token string
WorkDir string
repo *git.Repository
}
func New(url, username, token, workDir string) *GitRepo {
return &GitRepo{
URL: url,
Username: username,
Token: token,
WorkDir: workDir,
}
}
func (g *GitRepo) Clone() error {
auth := &http.BasicAuth{
Username: g.Username,
Password: g.Token,
}
var err error
g.repo, err = git.PlainClone(g.WorkDir, false, &git.CloneOptions{
URL: g.URL,
Auth: auth,
Progress: os.Stdout,
})
if err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
return nil
}
func (g *GitRepo) Pull() error {
if g.repo == nil {
return fmt.Errorf("repository not initialized")
}
w, err := g.repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
auth := &http.BasicAuth{
Username: g.Username,
Password: g.Token,
}
err = w.Pull(&git.PullOptions{
Auth: auth,
Progress: os.Stdout,
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to pull changes: %w", err)
}
return nil
}
func (g *GitRepo) Commit(message string) error {
if g.repo == nil {
return fmt.Errorf("repository not initialized")
}
w, err := g.repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
_, err = w.Add(".")
if err != nil {
return fmt.Errorf("failed to add changes: %w", err)
}
_, err = w.Commit(message, &git.CommitOptions{})
if err != nil {
return fmt.Errorf("failed to commit changes: %w", err)
}
return nil
}
func (g *GitRepo) Push() error {
if g.repo == nil {
return fmt.Errorf("repository not initialized")
}
auth := &http.BasicAuth{
Username: g.Username,
Password: g.Token,
}
err := g.repo.Push(&git.PushOptions{
Auth: auth,
Progress: os.Stdout,
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to push changes: %w", err)
}
return nil
}
func (g *GitRepo) EnsureRepo() error {
if _, err := os.Stat(filepath.Join(g.WorkDir, ".git")); os.IsNotExist(err) {
return g.Clone()
}
var err error
g.repo, err = git.PlainOpen(g.WorkDir)
if err != nil {
return fmt.Errorf("failed to open existing repository: %w", err)
}
return g.Pull()
}

View File

@@ -2,7 +2,7 @@ package storage
import (
"fmt"
"novamd/internal/gitutils"
"novamd/internal/git"
)
// RepositoryManager defines the interface for managing Git repositories.
@@ -25,9 +25,9 @@ type RepositoryManager interface {
func (s *Service) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken string) error {
workspacePath := s.GetWorkspacePath(userID, workspaceID)
if _, ok := s.GitRepos[userID]; !ok {
s.GitRepos[userID] = make(map[int]*gitutils.GitRepo)
s.GitRepos[userID] = make(map[int]git.Client)
}
s.GitRepos[userID][workspaceID] = gitutils.New(gitURL, gitUser, gitToken, workspacePath)
s.GitRepos[userID][workspaceID] = git.New(gitURL, gitUser, gitToken, workspacePath)
return s.GitRepos[userID][workspaceID].EnsureRepo()
}
@@ -80,7 +80,7 @@ func (s *Service) Pull(userID, workspaceID int) error {
}
// getGitRepo returns the Git repository for the given user and workspace IDs.
func (s *Service) getGitRepo(userID, workspaceID int) (*gitutils.GitRepo, bool) {
func (s *Service) getGitRepo(userID, workspaceID int) (git.Client, bool) {
userRepos, ok := s.GitRepos[userID]
if !ok {
return nil, false

View File

@@ -1,7 +1,7 @@
package storage
import (
"novamd/internal/gitutils"
"novamd/internal/git"
)
// Manager interface combines all storage interfaces.
@@ -15,7 +15,7 @@ type Manager interface {
type Service struct {
fs fileSystem
RootDir string
GitRepos map[int]map[int]*gitutils.GitRepo // map[userID]map[workspaceID]*gitutils.GitRepo
GitRepos map[int]map[int]git.Client // map[userID]map[workspaceID]*git.Client
}
// NewService creates a new Storage instance.
@@ -37,6 +37,6 @@ func NewServiceWithFS(rootDir string, fs fileSystem) *Service {
return &Service{
fs: fs,
RootDir: rootDir,
GitRepos: make(map[int]map[int]*gitutils.GitRepo),
GitRepos: make(map[int]map[int]git.Client),
}
}