WorkingTreeDiffProvider.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.mining.git;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
import org.sandbox.jdt.triggerpattern.mining.analysis.DiffHunk;
import org.sandbox.jdt.triggerpattern.mining.analysis.FileDiff;
/**
* Computes Java source changes between {@code HEAD} and the current working tree.
*
* <p>This deliberately compares the committed tree directly with the filesystem
* working tree, so the result represents the source the user currently sees:
* staged and unstaged changes are both included. Added and deleted Java files are
* represented with an empty before/after side respectively.</p>
*/
public final class WorkingTreeDiffProvider {
/** Returns Java-file changes from HEAD to the current working tree. */
public List<FileDiff> getDiffs(Path repositoryPath) {
List<FileDiff> result = new ArrayList<>();
try (Git git = Git.open(repositoryPath.toFile())) {
Repository repository = git.getRepository();
RevCommit head = resolveHead(repository);
AbstractTreeIterator oldTree = head != null
? treeParser(repository, head)
: new EmptyTreeIterator();
FileTreeIterator workingTree = new FileTreeIterator(repository);
List<DiffEntry> entries = git.diff()
.setOldTree(oldTree)
.setNewTree(workingTree)
.setPathFilter(PathSuffixFilter.create(".java")) //$NON-NLS-1$
.call();
Path workTree = repository.getWorkTree().toPath();
for (DiffEntry entry : entries) {
String filePath = entry.getChangeType() == DiffEntry.ChangeType.DELETE
? entry.getOldPath()
: entry.getNewPath();
String before = entry.getChangeType() == DiffEntry.ChangeType.ADD || head == null
? "" //$NON-NLS-1$
: readCommittedFile(repository, head, entry.getOldPath());
String after = entry.getChangeType() == DiffEntry.ChangeType.DELETE
? "" //$NON-NLS-1$
: readWorkingFile(workTree.resolve(entry.getNewPath()));
if (before == null) {
before = ""; //$NON-NLS-1$
}
if (after == null) {
after = ""; //$NON-NLS-1$
}
if (!before.equals(after)) {
DiffHunk completeFile = new DiffHunk(1, lineCount(before), 1, lineCount(after), before, after);
result.add(new FileDiff(filePath, before, after, List.of(completeFile)));
}
}
} catch (IOException | GitAPIException e) {
throw new GitProviderException("Failed to diff HEAD against working tree at " + repositoryPath, e); //$NON-NLS-1$
}
return result;
}
private static RevCommit resolveHead(Repository repository) throws IOException {
ObjectId headId = repository.resolve(Constants.HEAD);
if (headId == null) {
return null;
}
try (RevWalk walk = new RevWalk(repository)) {
return walk.parseCommit(headId);
}
}
private static CanonicalTreeParser treeParser(Repository repository, RevCommit commit) throws IOException {
CanonicalTreeParser parser = new CanonicalTreeParser();
try (ObjectReader reader = repository.newObjectReader()) {
parser.reset(reader, commit.getTree().getId());
}
return parser;
}
private static String readCommittedFile(Repository repository, RevCommit commit, String path) throws IOException {
try (TreeWalk treeWalk = TreeWalk.forPath(repository, path, commit.getTree())) {
if (treeWalk == null) {
return null;
}
ObjectLoader loader = repository.open(treeWalk.getObjectId(0));
return new String(loader.getBytes(), StandardCharsets.UTF_8);
}
}
private static String readWorkingFile(Path path) throws IOException {
return Files.isRegularFile(path) ? Files.readString(path, StandardCharsets.UTF_8) : null;
}
private static int lineCount(String text) {
return text.isEmpty() ? 0 : text.split("\\n", -1).length; //$NON-NLS-1$
}
}