Skip to content

Commit e5beee4

Browse files
committed
Add gitStagePatch tool for selective staging and GitServiceTest
- Add stagePatch() to GitService: applies a unified diff to the index only, leaving the working tree unchanged. Enables partial file staging for selective commits. - Expose as gitStagePatch MCP tool in EclipseGitMcpServer - Add GitServiceTest with 17 tests covering: stagePatch, status, log, diff, branches, checkout, reset, stash, add, commit - Test creates a temporary Eclipse project with a local JGit repo and connects via EGit for realistic integration testing
1 parent 3c5ec64 commit e5beee4

3 files changed

Lines changed: 500 additions & 0 deletions

File tree

  • plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp
  • tests/com.github.gradusnikov.eclipse.plugin.assistai.main.tests/src/com/github/gradusnikov/eclipse/plugin/assistai/mcp/services

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/servers/EclipseGitMcpServer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,12 @@ public String gitStashList(
124124
{
125125
return gitService.stashList(projectName);
126126
}
127+
128+
@Tool(name = "gitStagePatch", description = "Stages specific changes from a unified diff patch into the index without modifying the working tree. Use this to stage partial file changes for selective commits. The patch must be in standard unified diff format with file headers (--- a/path and +++ b/path) and @@ hunk headers.", type = "object")
129+
public String gitStagePatch(
130+
@ToolParam(name = "projectName", description = "The Eclipse project name", required = true) String projectName,
131+
@ToolParam(name = "patch", description = "A unified diff patch string to stage. Must include file headers (--- a/path, +++ b/path) and @@ hunk headers.", required = true) String patch)
132+
{
133+
return gitService.stagePatch(projectName, patch);
134+
}
127135
}

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/services/GitService.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import java.io.ByteArrayOutputStream;
44
import java.io.IOException;
5+
import java.io.ByteArrayInputStream;
6+
import java.nio.charset.StandardCharsets;
7+
import java.nio.file.Files;
58
import java.text.SimpleDateFormat;
69
import java.util.List;
710
import java.util.Objects;
@@ -21,6 +24,11 @@
2124
import org.eclipse.jgit.diff.RawTextComparator;
2225
import org.eclipse.jgit.lib.BranchTrackingStatus;
2326
import org.eclipse.jgit.lib.ObjectId;
27+
import org.eclipse.jgit.lib.ObjectInserter;
28+
import org.eclipse.jgit.lib.Constants;
29+
import org.eclipse.jgit.dircache.DirCache;
30+
import org.eclipse.jgit.dircache.DirCacheEntry;
31+
import org.eclipse.jgit.dircache.DirCacheEditor;
2432
import org.eclipse.jgit.lib.PersonIdent;
2533
import org.eclipse.jgit.lib.Repository;
2634
import org.eclipse.jgit.revwalk.RevCommit;
@@ -204,6 +212,99 @@ public String addFiles(String projectName, String filePattern)
204212
}
205213
}
206214

215+
public String stagePatch(String projectName, String patch)
216+
{
217+
Repository repository = getRepository(projectName);
218+
try (Git gitCmd = new Git(repository))
219+
{
220+
java.io.File workTree = repository.getWorkTree();
221+
222+
// Parse the patch to find affected files
223+
org.eclipse.jgit.patch.Patch parsedPatch = new org.eclipse.jgit.patch.Patch();
224+
parsedPatch.parse(new ByteArrayInputStream(patch.getBytes(StandardCharsets.UTF_8)));
225+
226+
if (parsedPatch.getFiles().isEmpty())
227+
{
228+
return "No files affected by patch.";
229+
}
230+
231+
// For each file in the patch: save working tree, restore HEAD, apply patch, stage, restore working tree
232+
java.util.Map<java.io.File, byte[]> savedWorkingTree = new java.util.HashMap<>();
233+
234+
for (var fileHeader : parsedPatch.getFiles())
235+
{
236+
String filePath = fileHeader.getNewPath();
237+
java.io.File file = new java.io.File(workTree, filePath);
238+
if (file.exists())
239+
{
240+
savedWorkingTree.put(file, Files.readAllBytes(file.toPath()));
241+
}
242+
}
243+
244+
// Checkout affected files from HEAD to restore original content
245+
var checkoutCmd = gitCmd.checkout();
246+
for (var fileHeader : parsedPatch.getFiles())
247+
{
248+
checkoutCmd.addPath(fileHeader.getNewPath());
249+
}
250+
checkoutCmd.call();
251+
252+
// Apply the patch to the now-clean working tree files
253+
org.eclipse.jgit.api.ApplyResult result = gitCmd.apply()
254+
.setPatch(new ByteArrayInputStream(patch.getBytes(StandardCharsets.UTF_8)))
255+
.call();
256+
257+
// Stage the patched files
258+
DirCache dirCache = repository.lockDirCache();
259+
try
260+
{
261+
DirCacheEditor editor = dirCache.editor();
262+
ObjectInserter inserter = repository.newObjectInserter();
263+
264+
for (var file : result.getUpdatedFiles())
265+
{
266+
byte[] content = Files.readAllBytes(file.toPath());
267+
ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, content);
268+
269+
String repoRelativePath = workTree.toPath()
270+
.relativize(file.toPath()).toString().replace('\\', '/');
271+
272+
editor.add(new DirCacheEditor.PathEdit(repoRelativePath)
273+
{
274+
@Override
275+
public void apply(DirCacheEntry ent)
276+
{
277+
ent.setObjectId(blobId);
278+
ent.setFileMode(org.eclipse.jgit.lib.FileMode.REGULAR_FILE);
279+
ent.setLength(content.length);
280+
ent.setLastModified(java.time.Instant.now());
281+
}
282+
});
283+
}
284+
285+
inserter.flush();
286+
editor.commit();
287+
}
288+
finally
289+
{
290+
dirCache.unlock();
291+
}
292+
293+
// Restore working tree to the original modified state
294+
for (var entry : savedWorkingTree.entrySet())
295+
{
296+
Files.write(entry.getKey().toPath(), entry.getValue());
297+
}
298+
299+
refreshProject(projectName);
300+
return "Patch staged successfully. Files: " + result.getUpdatedFiles().size();
301+
}
302+
catch (Exception e)
303+
{
304+
throw new RuntimeException("Failed to stage patch: " + e.getMessage(), e);
305+
}
306+
}
307+
207308
public String commit(String projectName, String message)
208309
{
209310
Repository repository = getRepository(projectName);

0 commit comments

Comments
 (0)