Skip to content

Add patch application tools and improve git_add parsing #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ This MCP server provides the following Git operations as tools:
- **git_init**: Initialize a new Git repository
- **git_push**: Pushes local commits to a remote repository (requires `--write-access` flag)
- **git_list_repositories**: Lists all available Git repositories
- **git_apply_patch_string**: Applies a patch from a string to a git repository
- **git_apply_patch_file**: Applies a patch from a file to a git repository

## Installation

Expand Down
57 changes: 52 additions & 5 deletions pkg/gitops/gogit/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/geropl/git-mcp-go/pkg/gitops"
Expand Down Expand Up @@ -266,12 +267,12 @@ func (g *GoGitOperations) PushChanges(repoPath string, remote string, branch str
if err != nil {
return "", fmt.Errorf("failed to open repository: %w", err)
}

// Use "origin" as default remote if not specified
if remote == "" {
remote = "origin"
}

// Determine refspec based on branch
var refspec string
var branchName string
Expand All @@ -290,19 +291,65 @@ func (g *GoGitOperations) PushChanges(repoPath string, remote string, branch str
refspec = plumbing.NewBranchReferenceName(branch).String()
branchName = branch
}

// Push to remote
err = repo.Push(&git.PushOptions{
RemoteName: remote,
RefSpecs: []config.RefSpec{config.RefSpec(refspec + ":" + refspec)},
})

if err != nil {
if err == git.NoErrAlreadyUpToDate {
return "Everything up-to-date", nil
}
return "", fmt.Errorf("failed to push: %w", err)
}

return fmt.Sprintf("Successfully pushed to %s/%s", remote, branchName), nil
}

// ApplyPatchFromFile applies a patch from a file to the repository
func (g *GoGitOperations) ApplyPatchFromFile(repoPath string, patchFilePath string) (string, error) {
// Ensure the patch file exists
if _, err := os.Stat(patchFilePath); os.IsNotExist(err) {
return "", fmt.Errorf("patch file does not exist: %s", patchFilePath)
}

// go-git doesn't have direct support for applying patches
// We'll use git command for this operation
output, err := gitops.RunGitCommand(repoPath, "apply", patchFilePath)
if err != nil {
return "", fmt.Errorf("failed to apply patch: %w", err)
}

return fmt.Sprintf("Patch from file '%s' applied successfully\n%s", patchFilePath, output), nil
}

// ApplyPatchFromString applies a patch from a string to the repository
func (g *GoGitOperations) ApplyPatchFromString(repoPath string, patchString string) (string, error) {
// Create a temporary file to store the patch
tmpFile, err := os.CreateTemp("", "git-mcp-patch-*.patch")
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %w", err)
}
defer os.Remove(tmpFile.Name()) // Clean up the temp file when done

// Write the patch content to the temporary file
if _, err := tmpFile.WriteString(patchString); err != nil {
return "", fmt.Errorf("failed to write patch to temporary file: %w", err)
}

// Close the file to ensure all data is written
if err := tmpFile.Close(); err != nil {
return "", fmt.Errorf("failed to close temporary file: %w", err)
}

// Delegate to the file-based method
result, err := g.ApplyPatchFromFile(repoPath, tmpFile.Name())
if err != nil {
return "", err
}

// Modify the result to remove the file path reference since it's a temporary file
return strings.Replace(result, fmt.Sprintf("from file '%s' ", tmpFile.Name()), "", 1), nil
}
2 changes: 2 additions & 0 deletions pkg/gitops/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ type GitOperations interface {
InitRepo(repoPath string) (string, error)
ShowCommit(repoPath string, revision string) (string, error)
PushChanges(repoPath string, remote string, branch string) (string, error)
ApplyPatchFromString(repoPath string, patchString string) (string, error)
ApplyPatchFromFile(repoPath string, patchFilePath string) (string, error)
}
73 changes: 59 additions & 14 deletions pkg/gitops/shell/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ func (s *ShellGitOperations) GetLog(repoPath string, maxCount int) ([]string, er
if maxCount > 0 {
args = append(args, fmt.Sprintf("-n%d", maxCount))
}

output, err := gitops.RunGitCommand(repoPath, args...)
if err != nil {
return nil, fmt.Errorf("failed to get log: %w", err)
}

// Split the output into individual commit entries
logs := strings.Split(strings.TrimSpace(output), "\n\n")
return logs, nil
Expand All @@ -88,12 +88,12 @@ func (s *ShellGitOperations) CreateBranch(repoPath string, branchName string, ba
if baseBranch != "" {
args = append(args, baseBranch)
}

_, err := gitops.RunGitCommand(repoPath, args...)
if err != nil {
return "", fmt.Errorf("failed to create branch: %w", err)
}

baseRef := baseBranch
if baseRef == "" {
// Get the current branch name
Expand All @@ -104,7 +104,7 @@ func (s *ShellGitOperations) CreateBranch(repoPath string, branchName string, ba
baseRef = strings.TrimSpace(currentBranch)
}
}

return fmt.Sprintf("Created branch '%s' from '%s'", branchName, baseRef), nil
}

Expand All @@ -114,7 +114,7 @@ func (s *ShellGitOperations) CheckoutBranch(repoPath string, branchName string)
if err != nil {
return "", fmt.Errorf("failed to checkout branch: %w", err)
}

return fmt.Sprintf("Switched to branch '%s'", branchName), nil
}

Expand All @@ -125,12 +125,12 @@ func (s *ShellGitOperations) InitRepo(repoPath string) (string, error) {
if err != nil {
return "", fmt.Errorf("failed to create directory: %w", err)
}

_, err = gitops.RunGitCommand(repoPath, "init")
if err != nil {
return "", fmt.Errorf("failed to initialize repository: %w", err)
}

gitDir := filepath.Join(repoPath, ".git")
return fmt.Sprintf("Initialized empty Git repository in %s", gitDir), nil
}
Expand All @@ -149,20 +149,65 @@ func (s *ShellGitOperations) PushChanges(repoPath string, remote string, branch
if branch != "" {
args = append(args, branch)
}

output, err := gitops.RunGitCommand(repoPath, args...)
if err != nil {
return "", fmt.Errorf("failed to push changes: %w", err)
}

// Check if the output indicates that everything is up-to-date
if strings.Contains(output, "up-to-date") {
return output, nil
}

// Format the output to match the expected format
return fmt.Sprintf("Successfully pushed to %s/%s\n%s",
remote,
branch,
return fmt.Sprintf("Successfully pushed to %s/%s\n%s",
remote,
branch,
output), nil
}

// ApplyPatchFromFile applies a patch from a file to the repository
func (s *ShellGitOperations) ApplyPatchFromFile(repoPath string, patchFilePath string) (string, error) {
// Ensure the patch file exists
if _, err := os.Stat(patchFilePath); os.IsNotExist(err) {
return "", fmt.Errorf("patch file does not exist: %s", patchFilePath)
}

// Apply the patch using git apply
output, err := gitops.RunGitCommand(repoPath, "apply", patchFilePath)
if err != nil {
return "", fmt.Errorf("failed to apply patch: %w", err)
}

return fmt.Sprintf("Patch from file '%s' applied successfully\n%s", patchFilePath, output), nil
}

// ApplyPatchFromString applies a patch from a string to the repository
func (s *ShellGitOperations) ApplyPatchFromString(repoPath string, patchString string) (string, error) {
// Create a temporary file to store the patch
tmpFile, err := os.CreateTemp("", "git-mcp-patch-*.patch")
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %w", err)
}
defer os.Remove(tmpFile.Name()) // Clean up the temp file when done

// Write the patch content to the temporary file
if _, err := tmpFile.WriteString(patchString); err != nil {
return "", fmt.Errorf("failed to write patch to temporary file: %w", err)
}

// Close the file to ensure all data is written
if err := tmpFile.Close(); err != nil {
return "", fmt.Errorf("failed to close temporary file: %w", err)
}

// Delegate to the file-based method
result, err := s.ApplyPatchFromFile(repoPath, tmpFile.Name())
if err != nil {
return "", err
}

// Modify the result to remove the file path reference since it's a temporary file
return strings.Replace(result, fmt.Sprintf("from file '%s' ", tmpFile.Name()), "", 1), nil
}
Loading