package git import ( "context" "fmt" "os" "path/filepath" "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/transport/http" ) // GitOperations handles git operations on repositories type GitOperations struct { cacheDir string } // NewGitOperations creates a new GitOperations instance func NewGitOperations(cacheDir string) *GitOperations { return &GitOperations{ cacheDir: cacheDir, } } // getRepoPath returns the local path for a repository func (g *GitOperations) getRepoPath(repoFullName string) string { return filepath.Join(g.cacheDir, repoFullName) } // CloneOrOpen clones a repository if it doesn't exist, or opens it if it does func (g *GitOperations) CloneOrOpen(ctx context.Context, repoFullName, cloneURL, accessToken string) (*git.Repository, error) { repoPath := g.getRepoPath(repoFullName) // Check if repository already exists if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { // Repository exists, open it repo, err := git.PlainOpen(repoPath) if err != nil { return nil, fmt.Errorf("failed to open repository: %w", err) } // Fetch latest changes err = repo.Fetch(&git.FetchOptions{ Auth: &http.BasicAuth{ Username: "oauth2", Password: accessToken, }, Force: true, }) if err != nil && err != git.NoErrAlreadyUpToDate { return nil, fmt.Errorf("failed to fetch: %w", err) } return repo, nil } // Repository doesn't exist, clone it if err := os.MkdirAll(filepath.Dir(repoPath), 0755); err != nil { return nil, fmt.Errorf("failed to create cache directory: %w", err) } repo, err := git.PlainClone(repoPath, false, &git.CloneOptions{ URL: cloneURL, Auth: &http.BasicAuth{ Username: "oauth2", Password: accessToken, }, Progress: nil, }) if err != nil { return nil, fmt.Errorf("failed to clone repository: %w", err) } return repo, nil } // CreateBranch creates a new branch from the default branch func (g *GitOperations) CreateBranch(repo *git.Repository, branchName, baseBranch string) error { // Get the base branch reference baseBranchRef := plumbing.NewRemoteReferenceName("origin", baseBranch) baseRef, err := repo.Reference(baseBranchRef, true) if err != nil { return fmt.Errorf("failed to get base branch reference: %w", err) } // Create new branch reference newBranchRef := plumbing.NewBranchReferenceName(branchName) ref := plumbing.NewHashReference(newBranchRef, baseRef.Hash()) // Store the reference if err := repo.Storer.SetReference(ref); err != nil { return fmt.Errorf("failed to create branch reference: %w", err) } // Checkout the new branch w, err := repo.Worktree() if err != nil { return fmt.Errorf("failed to get worktree: %w", err) } if err := w.Checkout(&git.CheckoutOptions{ Branch: newBranchRef, Force: true, }); err != nil { return fmt.Errorf("failed to checkout branch: %w", err) } return nil } // CheckoutBranch checks out an existing branch func (g *GitOperations) CheckoutBranch(repo *git.Repository, branchName string) error { w, err := repo.Worktree() if err != nil { return fmt.Errorf("failed to get worktree: %w", err) } branchRef := plumbing.NewBranchReferenceName(branchName) if err := w.Checkout(&git.CheckoutOptions{ Branch: branchRef, Force: true, }); err != nil { return fmt.Errorf("failed to checkout branch: %w", err) } return nil } // WriteFile writes content to a file in the repository func (g *GitOperations) WriteFile(repoFullName, filePath, content string) error { repoPath := g.getRepoPath(repoFullName) fullPath := filepath.Join(repoPath, filePath) // Create directory if it doesn't exist if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { return fmt.Errorf("failed to create directory: %w", err) } // Write file if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { return fmt.Errorf("failed to write file: %w", err) } return nil } // CommitChanges commits all changes with a message func (g *GitOperations) CommitChanges(repo *git.Repository, message string, filePaths []string) (string, error) { w, err := repo.Worktree() if err != nil { return "", fmt.Errorf("failed to get worktree: %w", err) } // Add files for _, filePath := range filePaths { if _, err := w.Add(filePath); err != nil { return "", fmt.Errorf("failed to add file %s: %w", filePath, err) } } // Commit commit, err := w.Commit(message, &git.CommitOptions{ Author: &object.Signature{ Name: "MarkEdit", Email: "markedit@users.noreply.github.com", When: time.Now(), }, }) if err != nil { return "", fmt.Errorf("failed to commit: %w", err) } return commit.String(), nil } // Push pushes the current branch to remote func (g *GitOperations) Push(repo *git.Repository, branchName, accessToken string) error { // Get the branch reference branchRef := plumbing.NewBranchReferenceName(branchName) // Push the branch err := repo.Push(&git.PushOptions{ RemoteName: "origin", RefSpecs: []config.RefSpec{ config.RefSpec(branchRef + ":" + branchRef), }, Auth: &http.BasicAuth{ Username: "oauth2", Password: accessToken, }, Force: false, }) if err != nil && err != git.NoErrAlreadyUpToDate { return fmt.Errorf("failed to push: %w", err) } return nil } // DeleteLocalBranch deletes a local branch func (g *GitOperations) DeleteLocalBranch(repo *git.Repository, branchName string) error { branchRef := plumbing.NewBranchReferenceName(branchName) if err := repo.Storer.RemoveReference(branchRef); err != nil { return fmt.Errorf("failed to delete branch: %w", err) } return nil } // GetCurrentBranch returns the name of the current branch func (g *GitOperations) GetCurrentBranch(repo *git.Repository) (string, error) { head, err := repo.Head() if err != nil { return "", fmt.Errorf("failed to get HEAD: %w", err) } if !head.Name().IsBranch() { return "", fmt.Errorf("HEAD is not a branch") } return head.Name().Short(), nil } // DeleteFile removes a file from the repository worktree func (g *GitOperations) DeleteFile(repoFullName, filePath string) error { repoPath := g.getRepoPath(repoFullName) fullPath := filepath.Join(repoPath, filePath) if err := os.Remove(fullPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to delete file: %w", err) } return nil }