From 52037b1b88c00ab5bbcb22b88fa062c19aafa3a9 Mon Sep 17 00:00:00 2001
From: Philipp0205 <36603238+Philipp0205@users.noreply.github.com>
Date: Thu, 3 Sep 2026 13:45:32 +0000
Subject: [PATCH 1/2] Use stock synchronize compare for pull requests
Co-authored-by: Kurrle, Philipp
---
AGENTS.md | 10 +-
README.md | 31 +-
.../internal/model/DiffHunkParserTest.java | 19 ++
.../ui/CompareCommentOverlayBinderTest.java | 46 +++
.../ui/LocalDiffLineCalculatorTest.java | 69 ++++
.../META-INF/MANIFEST.MF | 2 +
.../internal/bitbucket/BitbucketClient.java | 41 ---
.../internal/client/IPullRequestClient.java | 15 -
.../internal/github/GitHubClient.java | 31 --
.../internal/github/GitHubJsonParser.java | 25 --
.../internal/model/DiffHunkParser.java | 5 +
.../internal/ui/CommentViewSynchronizer.java | 12 +-
.../ui/CompareCommentOverlayBinder.java | 158 +++++++++
.../internal/ui/EmptyTypedElement.java | 38 ---
.../internal/ui/LocalDiffLineCalculator.java | 93 +++++
.../ui/PullRequestActionContributor.java | 8 +-
.../internal/ui/PullRequestChangedFile.java | 301 ----------------
.../ui/PullRequestCompareEditorInput.java | 315 -----------------
.../internal/ui/PullRequestContext.java | 51 +++
.../internal/ui/PullRequestListView.java | 4 +-
.../ui/PullRequestOpenInCompareAction.java | 185 ++++------
.../ui/PullRequestSynchronizeLauncher.java | 6 +-
.../internal/ui/RemoteFileEditorInput.java | 96 ------
.../internal/ui/RemoteFileTypedElement.java | 321 ------------------
24 files changed, 554 insertions(+), 1328 deletions(-)
create mode 100644 org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/CompareCommentOverlayBinderTest.java
create mode 100644 org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculatorTest.java
create mode 100644 org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/CompareCommentOverlayBinder.java
delete mode 100644 org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/EmptyTypedElement.java
create mode 100644 org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculator.java
delete mode 100644 org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestChangedFile.java
delete mode 100644 org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestCompareEditorInput.java
delete mode 100644 org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/RemoteFileEditorInput.java
delete mode 100644 org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/RemoteFileTypedElement.java
diff --git a/AGENTS.md b/AGENTS.md
index bf87187..ee49c66 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -158,7 +158,7 @@ eclipse-pullrequest-plugin/
│ │ ├── client/ # Abstraction layer (4 files)
│ │ ├── bitbucket/ # Bitbucket provider (2 files)
│ │ ├── github/ # GitHub provider (3 files)
-│ │ └── ui/ # Eclipse UI (21 files)
+│ │ └── ui/ # Synchronize/compare integration
│ ├── META-INF/MANIFEST.MF # OSGi bundle manifest
│ ├── plugin.xml # Extension points
│ └── plugin.properties # Externalized strings
@@ -173,10 +173,16 @@ eclipse-pullrequest-plugin/
- `client`: Provider abstraction and factory pattern
- `bitbucket`: Bitbucket Data Center API client implementation
- `github`: GitHub API client implementation with OAuth device flow
-- `ui`: Eclipse views, editors, perspectives, and UI components
+- `ui`: Eclipse views plus EGit Synchronize and compare comment integration
## Eclipse/OSGi Patterns
+### Pull Request Comparison
+- Use EGit's `GitModelSynchronizeParticipant` for changed files
+- Let the stock EGit compare action create file comparison inputs
+- Add pull request comments with `CompareCommentOverlayBinder` and
+ `CommentOverlayInstaller`; do not fetch file content through provider APIs
+
### Bundle Manifest (META-INF/MANIFEST.MF)
- Declare all exported packages in `Export-Package`
- Use version ranges for dependencies: `[7.6.0,7.7.0)`
diff --git a/README.md b/README.md
index b2a10c6..b8bd319 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,8 @@ This plugin was extracted from [EGit](https://www.eclipse.org/egit/) to provide
- View Pull Request overview with metadata (title, description, status, reviewers)
- Browse changed files in a Pull Request
-- View inline comments and review threads
+- Browse changes with Eclipse Team Synchronize and EGit compare
+- View and manage inline comments in the stock compare editor
- Navigate between review comments
- Support for both GitHub and Bitbucket APIs
- Integration with Eclipse Compare framework for side-by-side diff viewing
@@ -245,27 +246,21 @@ makes the log noisy.
```
org.eclipse.egit.pullrequest/
-├── internal.model/ # Data models
-│ ├── BitbucketChange.java
-│ ├── PullRequestChangedFile.java
-│ ├── PullRequestComment.java
-│ └── PullRequestMetadata.java
+├── internal.model/ # Provider-agnostic PR models
├── internal.bitbucket/ # Bitbucket API client
-│ ├── BitbucketClient.java
-│ └── BitbucketJsonParser.java
├── internal.github/ # GitHub API client
-│ ├── GitHubClient.java
-│ └── GitHubJsonParser.java
-├── internal.pullrequestclient/ # Abstraction layer
-│ ├── IPullRequestClient.java
-│ └── PullRequestClientFactory.java
-└── internal.ui/ # UI components
- ├── PullRequestOverviewView.java
- ├── PullRequestChangedFilesView.java
- ├── InlineCommentTextMergeViewer.java
- └── ... (15 more UI classes)
+├── internal.client/ # Provider abstraction
+└── internal.ui/ # Eclipse and EGit integration
+ ├── PullRequestSynchronizeLauncher.java
+ ├── PullRequestOpenInCompareAction.java
+ ├── CompareCommentOverlayBinder.java
+ └── CommentOverlayInstaller.java
```
+Changed files and file comparisons use Eclipse Team Synchronize and EGit's
+stock compare inputs. The plugin decorates that flow with pull request comment
+counts, inline comment painting, and review actions.
+
### Provider Architecture
The plugin uses a **provider-agnostic model** approach:
diff --git a/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/model/DiffHunkParserTest.java b/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/model/DiffHunkParserTest.java
index 4046d48..ed79915 100644
--- a/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/model/DiffHunkParserTest.java
+++ b/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/model/DiffHunkParserTest.java
@@ -11,6 +11,7 @@
package org.eclipse.egit.pullrequest.internal.model;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
@@ -45,6 +46,24 @@ public void testParseEmptyPatch() {
assertThat(result.getRightLines(), hasSize(0));
}
+ @Test
+ public void testIgnoresFullGitPatchHeaders() {
+ String patch = "diff --git a/file.txt b/file.txt\n" //$NON-NLS-1$
+ + "index 1111111..2222222 100644\n" //$NON-NLS-1$
+ + "--- a/file.txt\n" //$NON-NLS-1$
+ + "+++ b/file.txt\n" //$NON-NLS-1$
+ + "@@ -1,2 +1,2 @@\n" //$NON-NLS-1$
+ + " context\n" //$NON-NLS-1$
+ + "-old\n" //$NON-NLS-1$
+ + "+new\n" //$NON-NLS-1$
+ + "\\ No newline at end of file"; //$NON-NLS-1$
+
+ DiffLines result = DiffHunkParser.parse(patch);
+
+ assertThat(result.getLeftLines(), containsInAnyOrder(1, 2));
+ assertThat(result.getRightLines(), containsInAnyOrder(1, 2));
+ }
+
@Test
public void testParseSingleHunkAddedLines() {
// Hunk that adds 2 lines at line 5 of the new file
diff --git a/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/CompareCommentOverlayBinderTest.java b/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/CompareCommentOverlayBinderTest.java
new file mode 100644
index 0000000..15c4663
--- /dev/null
+++ b/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/CompareCommentOverlayBinderTest.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Philipp Hoenisch and contributors
+ *
+ * All rights reserved. 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.eclipse.egit.pullrequest.internal.ui;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+
+import java.util.List;
+
+import org.eclipse.egit.pullrequest.internal.model.PullRequestComment;
+import org.junit.Test;
+
+/**
+ * Tests for {@link CompareCommentOverlayBinder}.
+ */
+public class CompareCommentOverlayBinderTest {
+
+ @Test
+ public void testFiltersCommentsByNestedRepositoryPath() {
+ PullRequestComment matching =
+ comment("src/nested/Example.java"); //$NON-NLS-1$
+ PullRequestComment other = comment("src/Other.java"); //$NON-NLS-1$
+ PullRequestComment general = comment(null);
+
+ List result =
+ CompareCommentOverlayBinder.filterComments(
+ List.of(matching, other, general),
+ "src/nested/Example.java"); //$NON-NLS-1$
+
+ assertThat(result, contains(matching));
+ }
+
+ private static PullRequestComment comment(String path) {
+ PullRequestComment comment = new PullRequestComment();
+ comment.setPath(path);
+ return comment;
+ }
+}
diff --git a/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculatorTest.java b/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculatorTest.java
new file mode 100644
index 0000000..4679850
--- /dev/null
+++ b/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculatorTest.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Philipp Hoenisch and contributors
+ *
+ * All rights reserved. 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.eclipse.egit.pullrequest.internal.ui;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+import java.io.File;
+import java.nio.file.Files;
+
+import org.eclipse.egit.pullrequest.internal.model.DiffHunkParser.DiffLines;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Tests for {@link LocalDiffLineCalculator}.
+ */
+public class LocalDiffLineCalculatorTest {
+
+ /**
+ * Temporary repository location.
+ */
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void testCalculatesLinesFromLocalCommitRange() throws Exception {
+ File directory = temporaryFolder.newFolder("repository"); //$NON-NLS-1$
+ try (Git git = Git.init().setDirectory(directory).call()) {
+ File file = new File(directory, "example.txt"); //$NON-NLS-1$
+ Files.writeString(file.toPath(), "one\ntwo\nthree\n"); //$NON-NLS-1$
+ git.add().addFilepattern("example.txt").call(); //$NON-NLS-1$
+ RevCommit base = commit(git, "base"); //$NON-NLS-1$
+
+ Files.writeString(file.toPath(),
+ "one\ntwo\nadded\nthree\n"); //$NON-NLS-1$
+ git.add().addFilepattern("example.txt").call(); //$NON-NLS-1$
+ RevCommit head = commit(git, "head"); //$NON-NLS-1$
+
+ DiffLines lines = LocalDiffLineCalculator.calculate(
+ git.getRepository(), base.getName(), head.getName(),
+ "example.txt"); //$NON-NLS-1$
+
+ assertThat(lines.getLeftLines(),
+ containsInAnyOrder(1, 2, 3));
+ assertThat(lines.getRightLines(),
+ containsInAnyOrder(1, 2, 3, 4));
+ }
+ }
+
+ private static RevCommit commit(Git git, String message)
+ throws Exception {
+ return git.commit().setMessage(message)
+ .setAuthor("Test User", "test@example.com") //$NON-NLS-1$ //$NON-NLS-2$
+ .setCommitter("Test User", "test@example.com") //$NON-NLS-1$ //$NON-NLS-2$
+ .call();
+ }
+}
diff --git a/org.eclipse.egit.pullrequest/META-INF/MANIFEST.MF b/org.eclipse.egit.pullrequest/META-INF/MANIFEST.MF
index 3e4e790..ec80b6b 100644
--- a/org.eclipse.egit.pullrequest/META-INF/MANIFEST.MF
+++ b/org.eclipse.egit.pullrequest/META-INF/MANIFEST.MF
@@ -40,9 +40,11 @@ Import-Package: org.eclipse.core.resources.mapping,
org.eclipse.egit.ui.internal.synchronize.model;version="[6.0.0,8.0.0)",
org.eclipse.jgit.annotations;version="[6.0.0,8.0.0)",
org.eclipse.jgit.api;version="[6.0.0,8.0.0)",
+ org.eclipse.jgit.diff;version="[6.0.0,8.0.0)",
org.eclipse.jgit.lib;version="[6.0.0,8.0.0)",
org.eclipse.jgit.revwalk;version="[6.0.0,8.0.0)",
org.eclipse.jgit.transport;version="[6.0.0,8.0.0)",
+ org.eclipse.jgit.treewalk;version="[6.0.0,8.0.0)",
org.eclipse.team.core.subscribers,
org.eclipse.team.ui.synchronize,
org.eclipse.ui,
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/bitbucket/BitbucketClient.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/bitbucket/BitbucketClient.java
index 0f84932..995d3c1 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/bitbucket/BitbucketClient.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/bitbucket/BitbucketClient.java
@@ -179,17 +179,6 @@ public List getPullRequestComments(long pullRequestId)
return BitbucketJsonParser.parseActivities(jsonResponse, serverUrl);
}
- @Override
- @NonNull
- public byte[] getFileContent(@NonNull String commitId, @NonNull String path)
- throws IOException {
- String url = serverUrl + API_BASE_PATH + "/projects/" + projectKey //$NON-NLS-1$
- + "/repos/" + repositorySlug //$NON-NLS-1$
- + "/raw/" + path + "?at=" + commitId; //$NON-NLS-1$ //$NON-NLS-2$
-
- return executeGetBinary(url);
- }
-
@Override
@NonNull
public PullRequestComment addComment(long pullRequestId,
@@ -1124,36 +1113,6 @@ private String readResponse(InputStream inputStream) throws IOException {
}
}
- private byte[] executeGetBinary(String urlString) throws IOException {
- HttpURLConnection connection = openConnection(urlString, "GET", "*/*"); //$NON-NLS-1$ //$NON-NLS-2$
- try {
- int status = responseCode(connection, "GET", urlString); //$NON-NLS-1$
- if (status == HttpURLConnection.HTTP_OK) {
- return readBinaryResponse(connection.getInputStream());
- }
- if (status == HttpURLConnection.HTTP_NOT_FOUND) {
- // Files that were added or deleted do not exist on one side
- Activator.logDebug("GET " + urlString //$NON-NLS-1$
- + " -> HTTP 404, treated as empty content"); //$NON-NLS-1$
- return new byte[0];
- }
- throw httpFailure("GET", urlString, connection, status); //$NON-NLS-1$
- } finally {
- connection.disconnect();
- }
- }
-
- private byte[] readBinaryResponse(InputStream inputStream)
- throws IOException {
- if (inputStream == null) {
- return new byte[0];
- }
-
- try (InputStream in = inputStream) {
- return in.readAllBytes();
- }
- }
-
@Override
public void submitReview(long pullRequestId, @NonNull String event,
@Nullable String body) throws IOException {
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/client/IPullRequestClient.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/client/IPullRequestClient.java
index 5ea36dd..16edf91 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/client/IPullRequestClient.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/client/IPullRequestClient.java
@@ -91,21 +91,6 @@ List getPullRequestChanges(long pullRequestId)
List getPullRequestComments(long pullRequestId)
throws IOException;
- /**
- * Retrieves raw file content at a specific commit
- *
- * @param commitId
- * the commit SHA or branch name
- * @param path
- * the file path
- * @return raw file content as byte array
- * @throws IOException
- * if the request fails
- */
- @NonNull
- byte[] getFileContent(@NonNull String commitId, @NonNull String path)
- throws IOException;
-
/**
* Adds a comment to a pull request
*
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/github/GitHubClient.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/github/GitHubClient.java
index 66d5b44..bc76017 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/github/GitHubClient.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/github/GitHubClient.java
@@ -446,37 +446,6 @@ private java.util.Map parseThreadResolutionStates(
return result;
}
- @Override
- public byte[] getFileContent(@NonNull String commitId,
- @NonNull String path) throws IOException {
- // GitHub API for getting file content at specific commit
- String apiPath = "/repos/" + owner + "/" + repo + "/contents/" + path //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
- + "?ref=" + commitId; //$NON-NLS-1$
-
- HttpURLConnection conn = null;
- try {
- conn = createConnection(apiPath, "GET"); //$NON-NLS-1$
-
- int responseCode = conn.getResponseCode();
- if (responseCode == 404) {
- // File doesn't exist at this commit (likely deleted)
- return new byte[0];
- }
- if (responseCode != 200) {
- throw new IOException(
- "Failed to get file content: HTTP " + responseCode); //$NON-NLS-1$
- }
-
- String json = readResponse(conn);
- return GitHubJsonParser.parseFileContent(json);
-
- } finally {
- if (conn != null) {
- conn.disconnect();
- }
- }
- }
-
@Override
public @NonNull PullRequestComment addComment(long pullRequestId,
@NonNull String text, long parentCommentId)
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/github/GitHubJsonParser.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/github/GitHubJsonParser.java
index bbcb031..5953650 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/github/GitHubJsonParser.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/github/GitHubJsonParser.java
@@ -3,7 +3,6 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -714,30 +713,6 @@ private static List groupIntoThreads(
return roots;
}
- /**
- * Parses file content from GitHub API JSON response
- *
- * @param json
- * the JSON response
- * @return the decoded file content
- */
- static byte[] parseFileContent(String json) {
- // GitHub returns file content base64-encoded in the "content" field
- String content = extractString(json, "content"); //$NON-NLS-1$
- if (content == null || content.isEmpty()) {
- return new byte[0];
- }
-
- // Remove whitespace/newlines that GitHub adds
- content = content.replaceAll("\\s+", ""); //$NON-NLS-1$ //$NON-NLS-2$
-
- try {
- return Base64.getDecoder().decode(content);
- } catch (IllegalArgumentException e) {
- return new byte[0];
- }
- }
-
/**
* Parses the current user from GitHub API JSON
*
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/model/DiffHunkParser.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/model/DiffHunkParser.java
index 176fe46..0825608 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/model/DiffHunkParser.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/model/DiffHunkParser.java
@@ -106,6 +106,7 @@ public static DiffLines parse(String patch) {
String[] lines = patch.split("\n"); //$NON-NLS-1$
int oldLine = 0;
int newLine = 0;
+ boolean inHunk = false;
for (String line : lines) {
if (line.startsWith("@@")) { //$NON-NLS-1$
@@ -115,7 +116,11 @@ public static DiffLines parse(String patch) {
if (header != null) {
oldLine = header[0];
newLine = header[1];
+ inHunk = true;
}
+ } else if (!inHunk
+ || line.startsWith("\\ No newline")) { //$NON-NLS-1$
+ // Ignore file headers and informational patch lines.
} else if (line.startsWith("-")) { //$NON-NLS-1$
// Removed line: valid on LEFT side only
leftLines.add(Integer.valueOf(oldLine));
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/CommentViewSynchronizer.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/CommentViewSynchronizer.java
index b9de08e..c92104a 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/CommentViewSynchronizer.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/CommentViewSynchronizer.java
@@ -13,14 +13,14 @@
import org.eclipse.swt.widgets.Display;
/**
- * Synchronizes pull request comments across Eclipse views
- * (Changed Files View) and the compare editor.
+ * Synchronizes pull request comments across the Synchronize view and the
+ * compare editor.
*
*
* This class handles:
*
* - Fetching fresh comments from the API after an action
- * - Refreshing the Changed Files View
+ * - Refreshing Synchronize view decorators
* - Filtering comments for the current file
*
*
@@ -68,9 +68,9 @@ void setCurrentComments(List comments) {
}
/**
- * Refreshes all views after a comment action completes. Fetches
- * fresh comments from the API and updates the Changed Files View
- * and compare editor.
+ * Refreshes all views after a comment action completes. Fetches fresh
+ * comments from the API and updates Synchronize decorators and the compare
+ * editor.
*
* @param pr
* the pull request
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/CompareCommentOverlayBinder.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/CompareCommentOverlayBinder.java
new file mode 100644
index 0000000..3c4b350
--- /dev/null
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/CompareCommentOverlayBinder.java
@@ -0,0 +1,158 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Philipp Hoenisch and contributors
+ *
+ * All rights reserved. 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.eclipse.egit.pullrequest.internal.ui;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.compare.CompareEditorInput;
+import org.eclipse.compare.CompareViewerSwitchingPane;
+import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
+import org.eclipse.egit.pullrequest.Activator;
+import org.eclipse.egit.pullrequest.internal.PRPreferences;
+import org.eclipse.egit.pullrequest.internal.model.DiffHunkParser;
+import org.eclipse.egit.pullrequest.internal.model.PullRequestComment;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * Installs pull request comment overlays on the compare viewer opened by the
+ * stock EGit Synchronize action.
+ */
+final class CompareCommentOverlayBinder {
+
+ private static final String BINDING_KEY = Activator.PLUGIN_ID
+ + ".compareCommentOverlay"; //$NON-NLS-1$
+
+ private static final int MAX_RETRIES = 20;
+
+ private CompareCommentOverlayBinder() {
+ // Utility class
+ }
+
+ /**
+ * Binds comments after the stock Synchronize action opens or reuses its
+ * compare editor.
+ *
+ * @param filePath
+ * the repository-relative file path
+ * @param diffLines
+ * the locally calculated commentable lines
+ */
+ static void bindAfterOpen(String filePath,
+ DiffHunkParser.DiffLines diffLines) {
+ Display display = Display.getDefault();
+ if (display != null && !display.isDisposed()) {
+ display.asyncExec(() -> bind(filePath, diffLines, 0));
+ }
+ }
+
+ static List filterComments(
+ List comments, String filePath) {
+ return comments.stream()
+ .filter(comment -> filePath.equals(comment.getPath()))
+ .collect(Collectors.toList());
+ }
+
+ private static void bind(String filePath,
+ DiffHunkParser.DiffLines diffLines, int retryCount) {
+ IWorkbenchWindow window = PlatformUI.getWorkbench()
+ .getActiveWorkbenchWindow();
+ IWorkbenchPage page = window != null ? window.getActivePage() : null;
+ IEditorPart editor = page != null ? page.getActiveEditor() : null;
+ Viewer viewer = editor != null
+ ? findContentViewer(editor.getEditorInput()) : null;
+
+ if (viewer == null) {
+ retry(filePath, diffLines, retryCount);
+ return;
+ }
+ if (!(viewer instanceof TextMergeViewer)
+ || !Activator.getDefault().getPreferenceStore().getBoolean(
+ PRPreferences.PULLREQUEST_SHOW_INLINE_COMMENTS)) {
+ return;
+ }
+
+ Control control = viewer.getControl();
+ if (control == null || control.isDisposed()) {
+ retry(filePath, diffLines, retryCount);
+ return;
+ }
+
+ Object existing = control.getData(BINDING_KEY);
+ if (existing instanceof CommentOverlayInstaller) {
+ ((CommentOverlayInstaller) existing).dispose();
+ }
+
+ CommentOverlayInstaller installer =
+ new CommentOverlayInstaller(viewer);
+ installer.setFilePath(filePath);
+ installer.setDiffLines(diffLines);
+ installer.installComments(filterComments(
+ PullRequestContext.getInstance().getComments(), filePath));
+ control.setData(BINDING_KEY, installer);
+ control.addDisposeListener(event -> installer.dispose());
+ }
+
+ private static Viewer findContentViewer(Object editorInput) {
+ if (!(editorInput instanceof CompareEditorInput)) {
+ return null;
+ }
+ try {
+ Field field = findField(editorInput.getClass(),
+ "fContentInputPane"); //$NON-NLS-1$
+ if (field == null) {
+ return null;
+ }
+ field.setAccessible(true);
+ Object pane = field.get(editorInput);
+ if (pane instanceof CompareViewerSwitchingPane) {
+ return ((CompareViewerSwitchingPane) pane).getViewer();
+ }
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ Activator.logError(
+ "Failed to locate stock compare viewer", e); //$NON-NLS-1$
+ }
+ return null;
+ }
+
+ private static Field findField(Class> type, String name) {
+ Class> current = type;
+ while (current != null) {
+ try {
+ return current.getDeclaredField(name);
+ } catch (NoSuchFieldException e) {
+ current = current.getSuperclass();
+ }
+ }
+ return null;
+ }
+
+ private static void retry(String filePath,
+ DiffHunkParser.DiffLines diffLines, int retryCount) {
+ if (retryCount >= MAX_RETRIES) {
+ Activator.logWarning(
+ "Stock compare viewer was not ready for comments"); //$NON-NLS-1$
+ return;
+ }
+ Display display = Display.getDefault();
+ if (display != null && !display.isDisposed()) {
+ display.timerExec(100,
+ () -> bind(filePath, diffLines, retryCount + 1));
+ }
+ }
+}
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/EmptyTypedElement.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/EmptyTypedElement.java
deleted file mode 100644
index d0c72c7..0000000
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/EmptyTypedElement.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.eclipse.egit.pullrequest.internal.ui;
-
-import org.eclipse.compare.ITypedElement;
-import org.eclipse.swt.graphics.Image;
-
-/**
- * Empty typed element for use in compare editors when a file doesn't exist
- */
-public class EmptyTypedElement implements ITypedElement {
-
- private String name;
-
- /**
- * Creates a new empty typed element
- *
- * @param name
- * the name used for display
- */
- public EmptyTypedElement(String name) {
- this.name = name;
- }
-
- @Override
- public Image getImage() {
- return null;
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public String getType() {
- return ITypedElement.UNKNOWN_TYPE;
- }
-
-}
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculator.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculator.java
new file mode 100644
index 0000000..de89ca2
--- /dev/null
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculator.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Philipp Hoenisch and contributors
+ *
+ * All rights reserved. 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.eclipse.egit.pullrequest.internal.ui;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.eclipse.egit.pullrequest.internal.model.DiffHunkParser;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.EmptyTreeIterator;
+
+/**
+ * Calculates commentable lines from the local Git objects used by a
+ * Synchronize comparison.
+ */
+final class LocalDiffLineCalculator {
+
+ private static final String EMPTY_TREE = "^tree"; //$NON-NLS-1$
+
+ private LocalDiffLineCalculator() {
+ // Utility class
+ }
+
+ /**
+ * Calculates commentable lines for one repository-relative path.
+ *
+ * @param repository
+ * the local repository
+ * @param baseRevision
+ * the base revision, or {@code ^tree} for an empty tree
+ * @param headRevision
+ * the head revision
+ * @param path
+ * the repository-relative path
+ * @return commentable lines, or empty line sets when no entry matches
+ * @throws IOException
+ * if Git objects cannot be read
+ */
+ static DiffHunkParser.DiffLines calculate(Repository repository,
+ String baseRevision, String headRevision, String path)
+ throws IOException {
+ ByteArrayOutputStream patch = new ByteArrayOutputStream();
+ try (ObjectReader reader = repository.newObjectReader();
+ DiffFormatter formatter = new DiffFormatter(patch)) {
+ formatter.setRepository(repository);
+ formatter.setDetectRenames(true);
+ AbstractTreeIterator base = treeIterator(repository, reader,
+ baseRevision);
+ AbstractTreeIterator head = treeIterator(repository, reader,
+ headRevision);
+ List entries = formatter.scan(base, head);
+ for (DiffEntry entry : entries) {
+ if (path.equals(entry.getNewPath())
+ || path.equals(entry.getOldPath())) {
+ formatter.format(entry);
+ break;
+ }
+ }
+ }
+ return DiffHunkParser.parse(
+ patch.toString(StandardCharsets.UTF_8));
+ }
+
+ private static AbstractTreeIterator treeIterator(Repository repository,
+ ObjectReader reader, String revision) throws IOException {
+ if (EMPTY_TREE.equals(revision)) {
+ return new EmptyTreeIterator();
+ }
+ ObjectId tree = repository.resolve(revision + "^{tree}"); //$NON-NLS-1$
+ if (tree == null) {
+ throw new IOException("Cannot resolve revision " + revision); //$NON-NLS-1$
+ }
+ CanonicalTreeParser parser = new CanonicalTreeParser();
+ parser.reset(reader, tree);
+ return parser;
+ }
+}
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestActionContributor.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestActionContributor.java
index 9a0b4ce..1dca9fd 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestActionContributor.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestActionContributor.java
@@ -11,8 +11,8 @@
/**
* Action contributor for the pull request synchronize participant.
*
- * This class overrides the default "open" action in the Synchronize view to
- * provide custom pull request compare editors with inline comment overlays.
+ * This class decorates the default "open" action in the Synchronize view so
+ * the stock EGit compare editor receives inline comment overlays.
*/
@SuppressWarnings("restriction")
public class PullRequestActionContributor extends SynchronizePageActionGroup {
@@ -21,8 +21,8 @@ public class PullRequestActionContributor extends SynchronizePageActionGroup {
public void initialize(ISynchronizePageConfiguration configuration) {
super.initialize(configuration);
- // Override the default open action (double-click/Enter on files)
- // to use our custom PR compare editor with comment overlay
+ // Decorate the default open action (double-click/Enter on files)
+ // with the pull request comment overlay.
IWorkbenchSite ws = configuration.getSite().getWorkbenchSite();
if (ws instanceof IViewSite) {
Object oldAction = configuration.getProperty(P_OPEN_ACTION);
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestChangedFile.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestChangedFile.java
deleted file mode 100644
index 942fe3a..0000000
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestChangedFile.java
+++ /dev/null
@@ -1,301 +0,0 @@
-package org.eclipse.egit.pullrequest.internal.ui;
-
-import org.eclipse.core.resources.IFile;
-import org.eclipse.core.resources.IProject;
-import org.eclipse.core.resources.IResource;
-import org.eclipse.core.resources.ResourcesPlugin;
-import org.eclipse.core.runtime.IPath;
-import org.eclipse.core.runtime.Path;
-import org.eclipse.egit.pullrequest.internal.model.ChangedFile;
-import org.eclipse.egit.pullrequest.internal.model.DiffHunkParser;
-import org.eclipse.egit.core.internal.util.ResourceUtil;
-import org.eclipse.egit.pullrequest.internal.ui.IProblemDecoratable;
-import org.eclipse.jgit.lib.Repository;
-
-/**
- * UI model for a changed file in a pull request
- */
-public class PullRequestChangedFile implements IProblemDecoratable {
-
- /**
- * Change type enum
- */
- public enum ChangeType {
- /** File was added */
- ADDED,
- /** File was modified */
- MODIFIED,
- /** File was deleted */
- DELETED,
- /** File was renamed or moved */
- RENAMED
- }
-
- private final String path;
-
- private final String name;
-
- private final IPath parentPath;
-
- private final ChangeType changeType;
-
- private final String srcPath;
-
- private Repository repository;
-
- private boolean read;
-
- private DiffHunkParser.DiffLines diffLines;
-
- /**
- * Creates a new changed file entry
- *
- * @param path
- * the full repository-relative path
- * @param name
- * the file name
- * @param changeType
- * the type of change
- * @param srcPath
- * the source path for renames, null otherwise
- */
- public PullRequestChangedFile(String path, String name,
- ChangeType changeType, String srcPath) {
- this.path = path;
- this.name = name;
- this.changeType = changeType;
- this.srcPath = srcPath;
-
- IPath fullPath = new Path(path);
- this.parentPath = fullPath.segmentCount() > 1
- ? fullPath.removeLastSegments(1)
- : Path.EMPTY;
- }
-
- /**
- * @return the full repository-relative path
- */
- public String getPath() {
- return path;
- }
-
- /**
- * @return the file name
- */
- public String getName() {
- return name;
- }
-
- /**
- * @return the parent directory path
- */
- public IPath getParentPath() {
- return parentPath;
- }
-
- /**
- * @return the change type
- */
- public ChangeType getChangeType() {
- return changeType;
- }
-
- /**
- * @return the source path for renames, null otherwise
- */
- public String getSrcPath() {
- return srcPath;
- }
-
- /**
- * @return the old path for renames/moves, same as {@link #getSrcPath()}
- */
- public String getOldPath() {
- return srcPath;
- }
-
- /**
- * Sets the Git repository for this changed file.
- *
- * This allows for more accurate workspace file resolution by using the
- * repository's working tree as a base path.
- *
- *
- * @param repository
- * the Git repository, or null
- */
- public void setRepository(Repository repository) {
- this.repository = repository;
- }
-
- /**
- * @return the Git repository, or null if not set
- */
- public Repository getRepository() {
- return repository;
- }
-
- /**
- * Returns whether this file has been read (opened) by the user.
- *
- * @return {@code true} if the file has been read, {@code false}
- * otherwise
- */
- public boolean isRead() {
- return read;
- }
-
- /**
- * Sets the read state of this file.
- *
- * @param read
- * {@code true} to mark as read, {@code false} to mark as
- * unread
- */
- public void setRead(boolean read) {
- this.read = read;
- }
-
- /**
- * Returns the parsed diff lines for this file. The diff lines
- * indicate which 1-based line numbers in the file are part of
- * the pull request diff and are therefore valid targets for
- * inline comments.
- *
- * @return the diff lines, or {@code null} if no patch data
- * was available
- */
- public DiffHunkParser.DiffLines getDiffLines() {
- return diffLines;
- }
-
- /**
- * Sets the parsed diff lines for this file.
- *
- * @param diffLines
- * the diff lines
- */
- public void setDiffLines(DiffHunkParser.DiffLines diffLines) {
- this.diffLines = diffLines;
- }
-
- @Override
- public int getProblemSeverity() {
- // No problem markers for remote files
- return SEVERITY_NONE;
- }
-
- /**
- * Attempts to find a matching workspace file for this changed file.
- *
- * This method searches all workspace projects for a file matching this
- * changed file's path. The file is only returned if it exists and is
- * accessible.
- *
- *
- * @return the workspace {@link IFile} if found and accessible, or
- * {@code null} if the file is not in the workspace (e.g., the PR
- * branch is not checked out locally)
- */
- public IFile getWorkspaceFile() {
- // Use Repository-based resolution if available (preferred)
- if (repository != null) {
- return ResourceUtil.getFileForLocation(repository, path, false);
- }
-
- // Fallback: search all workspace projects
- IProject[] projects = ResourcesPlugin.getWorkspace().getRoot()
- .getProjects();
- for (IProject project : projects) {
- if (!project.isAccessible()) {
- continue;
- }
- // Try to find the file relative to the project
- IResource member = project.findMember(path);
- if (member instanceof IFile && member.isAccessible()) {
- return (IFile) member;
- }
- }
- return null;
- }
-
- /**
- * Gets the absolute file system path for this file, if it can be determined.
- *
- * @return the absolute {@link IPath} to the file, or {@code null} if it
- * cannot be determined
- */
- public IPath getLocation() {
- IFile file = getWorkspaceFile();
- if (file != null) {
- return file.getLocation();
- }
- // Fallback: compute from repository work tree
- if (repository != null) {
- return new Path(repository.getWorkTree().getAbsolutePath())
- .append(path);
- }
- return null;
- }
-
- /**
- * Returns the repository-relative path as an {@link IPath}.
- *
- * @return the repository-relative path
- */
- public IPath getRepoRelativePath() {
- return new Path(path);
- }
-
- /**
- * Converts a ChangedFile API model to UI model
- *
- * @param cf
- * the changed file from API
- * @return the UI model instance
- */
- public static PullRequestChangedFile fromChangedFile(ChangedFile cf) {
- ChangeType type;
- String changeType = cf.getType();
- if (changeType == null) {
- type = ChangeType.MODIFIED;
- } else {
- switch (changeType) {
- case "ADD": //$NON-NLS-1$
- type = ChangeType.ADDED;
- break;
- case "DELETE": //$NON-NLS-1$
- type = ChangeType.DELETED;
- break;
- case "MOVE": //$NON-NLS-1$
- type = ChangeType.RENAMED;
- break;
- default:
- type = ChangeType.MODIFIED;
- break;
- }
- }
-
- String srcPathStr = null;
- if (cf.getSrcPath() != null && cf.getSrcPath().getToString() != null) {
- srcPathStr = cf.getSrcPath().getToString();
- }
-
- PullRequestChangedFile result = new PullRequestChangedFile(
- cf.getPath().getToString(),
- cf.getPath().getName(), type, srcPathStr);
-
- // Parse diff patch to extract valid comment lines
- if (cf.getPatch() != null) {
- result.setDiffLines(
- DiffHunkParser.parse(cf.getPatch()));
- }
-
- return result;
- }
-
- @Override
- public String toString() {
- return "PullRequestChangedFile[" + path + ", " + changeType + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
- }
-}
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestCompareEditorInput.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestCompareEditorInput.java
deleted file mode 100644
index 638e41f..0000000
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestCompareEditorInput.java
+++ /dev/null
@@ -1,315 +0,0 @@
-package org.eclipse.egit.pullrequest.internal.ui;
-
-import java.lang.reflect.InvocationTargetException;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.compare.CompareConfiguration;
-import org.eclipse.compare.CompareEditorInput;
-import org.eclipse.compare.ITypedElement;
-import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
-import org.eclipse.compare.structuremergeviewer.DiffNode;
-import org.eclipse.compare.structuremergeviewer.ICompareInput;
-import org.eclipse.core.runtime.IProgressMonitor;
-import org.eclipse.egit.pullrequest.Activator;
-import org.eclipse.egit.pullrequest.internal.PRPreferences;
-import org.eclipse.egit.pullrequest.internal.client.IPullRequestClient;
-import org.eclipse.egit.pullrequest.internal.model.PullRequest;
-import org.eclipse.egit.pullrequest.internal.model.PullRequestComment;
-import org.eclipse.jface.viewers.Viewer;
-import org.eclipse.swt.widgets.Composite;
-
-/**
- * Compare editor input for comparing files from a pull request
- */
-public class PullRequestCompareEditorInput extends CompareEditorInput {
-
- private final IPullRequestClient client;
-
- private final PullRequest pullRequest;
-
- private final PullRequestChangedFile changedFile;
-
- private Viewer contentViewer;
-
- private CommentOverlayInstaller commentOverlay;
-
- private List comments = new ArrayList<>();
-
- /**
- * Creates a new PullRequestCompareEditorInput
- *
- * @param client
- * the pull request client
- * @param pullRequest
- * the pull request
- * @param changedFile
- * the changed file to compare
- */
- public PullRequestCompareEditorInput(IPullRequestClient client,
- PullRequest pullRequest, PullRequestChangedFile changedFile) {
- super(new CompareConfiguration());
- this.client = client;
- this.pullRequest = pullRequest;
- this.changedFile = changedFile;
-
- // Configure the compare editor
- CompareConfiguration config = getCompareConfiguration();
- config.setLeftEditable(false);
- config.setRightEditable(false);
-
- // Set labels for left and right sides
- String fromBranch = pullRequest.getFromRef().getDisplayId();
- String toBranch = pullRequest.getToRef().getDisplayId();
-
- switch (changedFile.getChangeType()) {
- case ADDED:
- config.setLeftLabel("(non-existent)"); //$NON-NLS-1$
- config.setRightLabel(fromBranch + " (new file)"); //$NON-NLS-1$
- break;
- case DELETED:
- config.setLeftLabel(toBranch + " (deleted)"); //$NON-NLS-1$
- config.setRightLabel("(non-existent)"); //$NON-NLS-1$
- break;
- case RENAMED:
- config.setLeftLabel(toBranch + " (old: " + changedFile.getOldPath() //$NON-NLS-1$
- + ")"); //$NON-NLS-1$
- config.setRightLabel(fromBranch + " (new: " + changedFile.getPath() //$NON-NLS-1$
- + ")"); //$NON-NLS-1$
- break;
- case MODIFIED:
- default:
- config.setLeftLabel(toBranch);
- config.setRightLabel(fromBranch);
- break;
- }
-
- setTitle(MessageFormat.format("Compare {0}", changedFile.getName())); //$NON-NLS-1$
- }
-
- @Override
- protected Object prepareInput(IProgressMonitor monitor)
- throws InvocationTargetException, InterruptedException {
- Object result = createCompareInput(client, pullRequest, changedFile, monitor);
- return result;
- }
-
- @Override
- public Viewer findContentViewer(Viewer oldViewer, ICompareInput input,
- Composite parent) {
- // Always let the framework select the appropriate viewer for
- // the file type (e.g. JavaMergeViewer for .java files, XML
- // compare for .xml, etc.). This ensures proper syntax
- // highlighting and language-specific comparison.
- contentViewer = super.findContentViewer(oldViewer, input, parent);
-
- // After the framework has selected the right viewer, install
- // comment overlays if needed
- boolean useInlineComments = Activator.getDefault()
- .getPreferenceStore()
- .getBoolean(PRPreferences.PULLREQUEST_SHOW_INLINE_COMMENTS);
-
- if (useInlineComments
- && contentViewer instanceof TextMergeViewer) {
- // Dispose any previous overlay before creating a new one.
- // The compare framework may call findContentViewer() more
- // than once (e.g. when the viewer is recycled or the input
- // changes). Without this, every call stacks an additional
- // ruler column and paint listener on the same SourceViewer,
- // which is the root cause of duplicate comment icons.
- if (commentOverlay != null) {
- commentOverlay.dispose();
- commentOverlay = null;
- }
- commentOverlay = new CommentOverlayInstaller(
- contentViewer);
- commentOverlay.setFilePath(changedFile.getPath());
- commentOverlay.setDiffLines(changedFile.getDiffLines());
- commentOverlay.installComments(comments);
- }
-
- return contentViewer;
- }
-
- /**
- * Set the comments to display as inline annotations in the compare editor
- *
- * @param comments
- * the list of comments for this file
- */
- public void setComments(List comments) {
- if (comments != null) {
- this.comments = new ArrayList<>(comments);
- } else {
- this.comments.clear();
- }
- }
-
- /**
- * Updates the inline comment overlays with fresh comments and refreshes
- * the compare editor display.
- *
- * This method is called when comments are added, modified, or deleted
- * to ensure the inline comment overlays in the compare editor are
- * synchronized.
- *
- *
- * @param fileComments
- * the updated list of comments for the file displayed in this
- * compare editor
- */
- public void updateComments(List fileComments) {
- if (commentOverlay != null && fileComments != null) {
- commentOverlay.installComments(fileComments);
- }
- }
-
- /**
- * Creates a compare input (DiffNode) for the given changed file
- *
- * @param client
- * the Bitbucket client
- * @param pullRequest
- * the pull request
- * @param changedFile
- * the changed file to compare
- * @param monitor
- * progress monitor
- * @return the DiffNode for comparison
- * @throws InvocationTargetException
- * if an error occurs
- * @throws InterruptedException
- * if the operation is interrupted
- */
- public static Object createCompareInput(IPullRequestClient client,
- PullRequest pullRequest, PullRequestChangedFile changedFile,
- IProgressMonitor monitor)
- throws InvocationTargetException, InterruptedException {
- monitor.beginTask("Comparing files...", IProgressMonitor.UNKNOWN); //$NON-NLS-1$
-
- try {
- // Get commit IDs - use displayId (branch name) as commit reference
- String fromCommitId = pullRequest.getFromRef().getDisplayId();
- String toCommitId = pullRequest.getToRef().getDisplayId();
-
- Object left = null;
- Object right = null;
-
- switch (changedFile.getChangeType()) {
- case ADDED:
- // File doesn't exist in target branch (left side)
- left = new EmptyTypedElement(
- changedFile.getName());
- // File exists in source branch (right side)
- right = new RemoteFileTypedElement(client, fromCommitId, changedFile.getPath());
- break;
-
- case DELETED:
- // File exists in target branch (left side)
- left = new RemoteFileTypedElement(client, toCommitId, changedFile.getPath());
- // File doesn't exist in source branch (right side)
- right = new EmptyTypedElement(
- changedFile.getName());
- break;
-
- case RENAMED:
- // Old file in target branch (left side)
- left = new RemoteFileTypedElement(client, toCommitId, changedFile.getOldPath());
- // New file in source branch (right side)
- right = new RemoteFileTypedElement(client, fromCommitId, changedFile.getPath());
- break;
-
- case MODIFIED:
- default:
- // File in target branch (left side)
- left = new RemoteFileTypedElement(client, toCommitId, changedFile.getPath());
- // File in source branch (right side)
- right = new RemoteFileTypedElement(client, fromCommitId, changedFile.getPath());
- break;
- }
-
- return new DiffNode((ITypedElement) left, (ITypedElement) right);
-
- } finally {
- monitor.done();
- }
- }
-
- /**
- * Creates a configured CompareConfiguration for the given changed file
- *
- * @param pullRequest
- * the pull request
- * @param changedFile
- * the changed file
- * @return configured CompareConfiguration
- */
- public static CompareConfiguration createCompareConfiguration(
- PullRequest pullRequest, PullRequestChangedFile changedFile) {
- CompareConfiguration config = new CompareConfiguration();
- config.setLeftEditable(false);
- config.setRightEditable(false);
-
- // Set labels for left and right sides
- String fromBranch = pullRequest.getFromRef().getDisplayId();
- String toBranch = pullRequest.getToRef().getDisplayId();
-
- switch (changedFile.getChangeType()) {
- case ADDED:
- config.setLeftLabel("(non-existent)"); //$NON-NLS-1$
- config.setRightLabel(fromBranch + " (new file)"); //$NON-NLS-1$
- break;
- case DELETED:
- config.setLeftLabel(toBranch + " (deleted)"); //$NON-NLS-1$
- config.setRightLabel("(non-existent)"); //$NON-NLS-1$
- break;
- case RENAMED:
- config.setLeftLabel(toBranch + " (old: " + changedFile.getOldPath() //$NON-NLS-1$
- + ")"); //$NON-NLS-1$
- config.setRightLabel(fromBranch + " (new: " + changedFile.getPath() //$NON-NLS-1$
- + ")"); //$NON-NLS-1$
- break;
- case MODIFIED:
- default:
- config.setLeftLabel(toBranch);
- config.setRightLabel(fromBranch);
- break;
- }
-
- return config;
- }
-
- /**
- * Get the changed file being compared
- *
- * @return the changed file
- */
- public PullRequestChangedFile getChangedFile() {
- return changedFile;
- }
-
- /**
- * Get the control for the compare viewer. This allows external code to
- * traverse the widget tree to find specific controls like StyledText.
- *
- * @return the viewer control, or null if not available
- */
- public org.eclipse.swt.widgets.Control getViewerControl() {
- if (contentViewer != null && contentViewer.getControl() != null) {
- return contentViewer.getControl();
- }
- return null;
- }
-
- /**
- * Get the comment overlay installer for this compare editor.
- * This allows external code to expand and navigate to specific comments.
- *
- * @return the comment overlay installer, or {@code null} if inline
- * comments are not enabled or not yet installed
- */
- public CommentOverlayInstaller getCommentOverlay() {
- return commentOverlay;
- }
-}
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestContext.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestContext.java
index ea5f2ab..4e7c545 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestContext.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestContext.java
@@ -7,6 +7,7 @@
import org.eclipse.egit.pullrequest.internal.client.IPullRequestClient;
import org.eclipse.egit.pullrequest.internal.model.PullRequest;
import org.eclipse.egit.pullrequest.internal.model.PullRequestComment;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
@@ -31,6 +32,12 @@ public class PullRequestContext {
private List comments = Collections.emptyList();
+ private Repository repository;
+
+ private String baseRevision;
+
+ private String headRevision;
+
private PullRequestContext() {
// Singleton
}
@@ -89,6 +96,50 @@ public IPullRequestClient getClient() {
return client;
}
+ /**
+ * Sets the local Git comparison backing the active Synchronize view.
+ *
+ * @param repository
+ * the local repository
+ * @param baseRevision
+ * the base revision
+ * @param headRevision
+ * the head revision
+ */
+ public void setComparison(Repository repository, String baseRevision,
+ String headRevision) {
+ this.repository = repository;
+ this.baseRevision = baseRevision;
+ this.headRevision = headRevision;
+ }
+
+ /**
+ * Gets the repository backing the active Synchronize view.
+ *
+ * @return the repository, or {@code null}
+ */
+ public Repository getRepository() {
+ return repository;
+ }
+
+ /**
+ * Gets the base revision backing the active Synchronize view.
+ *
+ * @return the base revision, or {@code null}
+ */
+ public String getBaseRevision() {
+ return baseRevision;
+ }
+
+ /**
+ * Gets the head revision backing the active Synchronize view.
+ *
+ * @return the head revision, or {@code null}
+ */
+ public String getHeadRevision() {
+ return headRevision;
+ }
+
/**
* Sets the comments for the currently active pull request.
*
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestListView.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestListView.java
index f738f14..7b8c3a3 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestListView.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestListView.java
@@ -608,8 +608,8 @@ public void setFocus() {
}
/**
- * Loads a pull request by opening the PR Overview editor and
- * PullRequestChangedFilesView.
+ * Loads a pull request by opening the PR Overview editor and launching the
+ * Synchronize view.
*
* @param pr
* the pull request to load
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestOpenInCompareAction.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestOpenInCompareAction.java
index ff35b88..38063ea 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestOpenInCompareAction.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestOpenInCompareAction.java
@@ -1,33 +1,31 @@
package org.eclipse.egit.pullrequest.internal.ui;
-import java.util.List;
-import java.util.stream.Collectors;
+import java.io.IOException;
-import org.eclipse.compare.CompareUI;
import org.eclipse.compare.IResourceProvider;
-import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Adapters;
-import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.egit.pullrequest.Activator;
-import org.eclipse.egit.pullrequest.internal.client.IPullRequestClient;
+import org.eclipse.egit.pullrequest.internal.model.DiffHunkParser;
import org.eclipse.egit.pullrequest.internal.model.PullRequest;
-import org.eclipse.egit.pullrequest.internal.model.PullRequestComment;
+import org.eclipse.egit.ui.internal.synchronize.model.GitModelBlob;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.swt.widgets.Display;
import org.eclipse.team.ui.synchronize.ISynchronizePageConfiguration;
/**
- * Opens a compare editor with pull request comment overlay when files are
- * double-clicked in the Synchronize view.
+ * Adds pull request comment overlays after the stock Synchronize open action.
*
- * This action is registered as the {@code P_OPEN_ACTION} for the pull request
- * synchronize participant, overriding Eclipse's default compare editor to
- * inject inline comment overlays.
+ * EGit remains responsible for creating and opening its compare input. This
+ * action only resolves the selected path and binds comments to the resulting
+ * text merge viewer.
*/
@SuppressWarnings("restriction")
public class PullRequestOpenInCompareAction extends Action {
@@ -42,14 +40,17 @@ public class PullRequestOpenInCompareAction extends Action {
* @param configuration the synchronize page configuration
* @param defaultOpenAction the default Eclipse open action (fallback)
*/
- public PullRequestOpenInCompareAction(ISynchronizePageConfiguration configuration, Action defaultOpenAction) {
+ public PullRequestOpenInCompareAction(
+ ISynchronizePageConfiguration configuration,
+ Action defaultOpenAction) {
this.configuration = configuration;
this.defaultOpenAction = defaultOpenAction;
}
@Override
public void run() {
- ISelection selection = configuration.getSite().getSelectionProvider().getSelection();
+ ISelection selection = configuration.getSite()
+ .getSelectionProvider().getSelection();
if (!(selection instanceof IStructuredSelection)) {
return;
}
@@ -59,27 +60,35 @@ public void run() {
return;
}
- // Get the first selected element
- Object element = sel.getFirstElement();
+ if (sel.size() != 1) {
+ runDefaultAction();
+ return;
+ }
- // Try to resolve to a file. The Synchronize view uses
- // GitModelBlob elements (which implement IResourceProvider)
- // rather than plain IResource objects in the Git ChangeSet
- // model.
- IFile file = resolveFile(element);
- if (file == null) {
- // Not a file - fall back to default action
- if (defaultOpenAction != null) {
- defaultOpenAction.run();
- }
+ Object element = sel.getFirstElement();
+ PullRequestContext context = PullRequestContext.getInstance();
+ PullRequest activePR = context.getActivePullRequest();
+ String filePath = resolveRepositoryRelativePath(element, context);
+ if (activePR == null || filePath == null) {
+ runDefaultAction();
return;
}
- // Open our custom compare editor with comments
- openPullRequestCompareEditor(file);
+ runDefaultAction();
+ scheduleOverlay(filePath, context);
}
- private IFile resolveFile(Object element) {
+ private String resolveRepositoryRelativePath(Object element,
+ PullRequestContext context) {
+ Repository repository = context.getRepository();
+ if (element instanceof GitModelBlob && repository != null) {
+ GitModelBlob blob = (GitModelBlob) element;
+ if (blob.getLocation() != null) {
+ return Repository.stripWorkDir(repository.getWorkTree(),
+ blob.getLocation().toFile());
+ }
+ }
+
IResource resource = null;
if (element instanceof IResource) {
@@ -90,94 +99,48 @@ private IFile resolveFile(Object element) {
resource = Adapters.adapt(element, IResource.class);
}
- if (resource instanceof IFile) {
- return (IFile) resource;
+ if (resource != null) {
+ RepositoryMapping mapping = RepositoryMapping
+ .getMapping(resource);
+ if (mapping != null) {
+ return mapping.getRepoRelativePath(resource);
+ }
}
return null;
}
- private void openPullRequestCompareEditor(IFile file) {
- PullRequestContext context = PullRequestContext.getInstance();
- PullRequest activePR = context.getActivePullRequest();
-
- if (activePR == null) {
- // No active PR - fall back to default
- if (defaultOpenAction != null) {
- defaultOpenAction.run();
- }
- return;
- }
-
- // Get repository-relative path
- String repoRelativePath = getRepositoryRelativePath(file);
- if (repoRelativePath == null) {
- if (defaultOpenAction != null) {
- defaultOpenAction.run();
- }
- return;
- }
-
- // Get the client from context
- IPullRequestClient client = context.getClient();
- if (client == null) {
- Activator.logWarning("No PR client available for compare editor"); //$NON-NLS-1$
- if (defaultOpenAction != null) {
- defaultOpenAction.run();
+ private void scheduleOverlay(String filePath,
+ PullRequestContext context) {
+ Job job = new Job("Calculating pull request diff lines") { //$NON-NLS-1$
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ DiffHunkParser.DiffLines diffLines =
+ DiffHunkParser.parse(null);
+ try {
+ Repository repository = context.getRepository();
+ String base = context.getBaseRevision();
+ String head = context.getHeadRevision();
+ if (repository != null && base != null && head != null) {
+ diffLines = LocalDiffLineCalculator.calculate(
+ repository, base, head, filePath);
+ }
+ } catch (IOException e) {
+ Activator.logWarning(
+ "Failed to calculate local comment lines: " //$NON-NLS-1$
+ + e.getMessage());
+ }
+ CompareCommentOverlayBinder.bindAfterOpen(filePath,
+ diffLines);
+ return Status.OK_STATUS;
}
- return;
- }
-
- // Create a PullRequestChangedFile model
- PullRequestChangedFile changedFile = createChangedFileFromResource(file, repoRelativePath);
-
- // Filter comments for this file
- List allComments = context.getComments();
- List fileComments = allComments.stream()
- .filter(comment -> repoRelativePath.equals(comment.getPath())).collect(Collectors.toList());
-
- // Open the compare editor on the UI thread
- Display.getDefault().asyncExec(() -> {
- PullRequestCompareEditorInput input = new PullRequestCompareEditorInput(client, activePR, changedFile);
- input.setComments(fileComments);
- CompareUI.openCompareEditor(input, true);
- });
- }
-
- private String getRepositoryRelativePath(IFile file) {
- RepositoryMapping mapping = RepositoryMapping.getMapping(file);
- if (mapping == null) {
- return null;
- }
-
- Repository repository = mapping.getRepository();
- if (repository == null) {
- return null;
- }
-
- IPath workTreePath = new org.eclipse.core.runtime.Path(repository.getWorkTree().getAbsolutePath());
- IPath filePath = file.getLocation();
- if (filePath == null) {
- return null;
- }
-
- if (workTreePath.isPrefixOf(filePath)) {
- IPath relativePath = filePath.makeRelativeTo(workTreePath);
- return relativePath.toPortableString();
- }
-
- return null;
+ };
+ job.setSystem(true);
+ job.schedule();
}
- private PullRequestChangedFile createChangedFileFromResource(IFile file, String repoRelativePath) {
- PullRequestChangedFile changedFile = new PullRequestChangedFile(repoRelativePath, file.getName(),
- PullRequestChangedFile.ChangeType.MODIFIED, null);
-
- // Set the repository
- RepositoryMapping mapping = RepositoryMapping.getMapping(file);
- if (mapping != null) {
- changedFile.setRepository(mapping.getRepository());
+ private void runDefaultAction() {
+ if (defaultOpenAction != null) {
+ defaultOpenAction.run();
}
-
- return changedFile;
}
}
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestSynchronizeLauncher.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestSynchronizeLauncher.java
index 1efe22f..31fa814 100644
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestSynchronizeLauncher.java
+++ b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/PullRequestSynchronizeLauncher.java
@@ -326,8 +326,8 @@ private static RevCommit resolveCommit(Repository repo, String commitId) {
*
* This method replicates the logic from
* {@code GitModelSynchronize.launch()} but adds a custom action
- * contributor to override the default open action with one that opens
- * our {@link PullRequestCompareEditorInput} with inline comment overlays.
+ * contributor that adds pull request comment overlays to the stock
+ * Synchronize compare editor.
*
*
* @param repo
@@ -376,6 +376,8 @@ private static void launchSynchronize(Repository repo,
: "^tree"; //$NON-NLS-1$
String dstRev = headCommit.getName();
+ PullRequestContext.getInstance().setComparison(repo, srcRev, dstRev);
+
GitSynchronizeData data = new GitSynchronizeData(repo, srcRev, dstRev,
false);
GitSynchronizeDataSet dataSet = new GitSynchronizeDataSet(data);
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/RemoteFileEditorInput.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/RemoteFileEditorInput.java
deleted file mode 100644
index 97feed3..0000000
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/RemoteFileEditorInput.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package org.eclipse.egit.pullrequest.internal.ui;
-
-import org.eclipse.core.resources.IStorage;
-import org.eclipse.core.runtime.CoreException;
-import org.eclipse.core.runtime.PlatformObject;
-import org.eclipse.jface.resource.ImageDescriptor;
-import org.eclipse.ui.IPersistableElement;
-import org.eclipse.ui.IStorageEditorInput;
-
-/**
- * Editor input for remote file revisions from pull request providers.
- *
- * This class provides the bridge between the compare framework's
- * {@link org.eclipse.compare.ISharedDocumentAdapter} and Eclipse's editor
- * infrastructure. It allows Eclipse to associate proper document providers
- * (e.g., Java document provider) with the file content, enabling features like
- * Java Source Compare.
- *
- */
-public class RemoteFileEditorInput extends PlatformObject
- implements IStorageEditorInput {
-
- private final RemoteFileTypedElement element;
-
- private final IStorage storage;
-
- /**
- * Creates a new editor input.
- *
- * @param element
- * the typed element this input is for
- * @param storage
- * the storage containing the file contents
- */
- public RemoteFileEditorInput(RemoteFileTypedElement element,
- IStorage storage) {
- this.element = element;
- this.storage = storage;
- }
-
- @Override
- public IStorage getStorage() throws CoreException {
- return storage;
- }
-
- @Override
- public boolean exists() {
- return true;
- }
-
- @Override
- public ImageDescriptor getImageDescriptor() {
- return null;
- }
-
- @Override
- public String getName() {
- return element.getName() + " [" + element.getCommitId() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- @Override
- public IPersistableElement getPersistable() {
- // Cannot persist - this is a remote file
- return null;
- }
-
- @Override
- public String getToolTipText() {
- return element.getPath() + " @ " + element.getCommitId(); //$NON-NLS-1$
- }
-
- @Override
- public T getAdapter(Class adapter) {
- if (adapter == IStorage.class) {
- return adapter.cast(storage);
- }
- return super.getAdapter(adapter);
- }
-
- @Override
- public int hashCode() {
- return element.hashCode();
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj instanceof RemoteFileEditorInput) {
- RemoteFileEditorInput other = (RemoteFileEditorInput) obj;
- return element.equals(other.element);
- }
- return false;
- }
-}
diff --git a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/RemoteFileTypedElement.java b/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/RemoteFileTypedElement.java
deleted file mode 100644
index 4b5969a..0000000
--- a/org.eclipse.egit.pullrequest/src/org/eclipse/egit/pullrequest/internal/ui/RemoteFileTypedElement.java
+++ /dev/null
@@ -1,321 +0,0 @@
-package org.eclipse.egit.pullrequest.internal.ui;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-import org.eclipse.compare.CompareUI;
-import org.eclipse.compare.IEncodedStreamContentAccessor;
-import org.eclipse.compare.ISharedDocumentAdapter;
-import org.eclipse.compare.ITypedElement;
-import org.eclipse.compare.SharedDocumentAdapter;
-import org.eclipse.core.resources.IEncodedStorage;
-import org.eclipse.core.resources.IStorage;
-import org.eclipse.core.runtime.CoreException;
-import org.eclipse.core.runtime.IAdaptable;
-import org.eclipse.core.runtime.IPath;
-import org.eclipse.core.runtime.IProgressMonitor;
-import org.eclipse.core.runtime.NullProgressMonitor;
-import org.eclipse.core.runtime.Path;
-import org.eclipse.core.runtime.Platform;
-import org.eclipse.core.runtime.Status;
-import org.eclipse.egit.pullrequest.internal.client.IPullRequestClient;
-import org.eclipse.jface.text.IDocument;
-import org.eclipse.swt.graphics.Image;
-import org.eclipse.ui.IEditorInput;
-import org.eclipse.ui.texteditor.IDocumentProvider;
-
-/**
- * Represents a remote file from a pull request provider for use in compare editors.
- *
- * This implementation extends the pattern used by EGit's
- * {@code StorageTypedElement} to provide proper integration with Eclipse's
- * compare framework, including support for Java Source Compare and other
- * language-specific compare viewers.
- *
- *
- * Key features:
- *
- *
- * - Implements {@link IAdaptable} to support adapter queries
- * - Provides {@link ISharedDocumentAdapter} for editor integration
- * - Returns proper file type icons via {@link CompareUI#getImage(String)}
- * - Supports charset/encoding detection
- *
- */
-public class RemoteFileTypedElement
- implements ITypedElement, IEncodedStreamContentAccessor, IAdaptable {
-
- private final String commitId;
-
- private final String path;
-
- private final String name;
-
- private final IPullRequestClient client;
-
- private IStorage bufferedStorage;
-
- private ISharedDocumentAdapter sharedDocumentAdapter;
-
- /**
- * Creates a new RemoteFileTypedElement
- *
- * @param client
- * the pull request client to use
- * @param commitId
- * the commit ID to fetch the file from
- * @param path
- * the file path
- */
- public RemoteFileTypedElement(IPullRequestClient client, String commitId, String path) {
- this.client = client;
- this.commitId = commitId;
- this.path = path;
- // Extract file name from path
- int lastSlash = path.lastIndexOf('/');
- this.name = lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public Image getImage() {
- // Return proper file type icon based on extension
- return CompareUI.getImage(getType());
- }
-
- @Override
- public String getType() {
- // Determine type from file extension
- int lastDot = name.lastIndexOf('.');
- if (lastDot >= 0 && lastDot < name.length() - 1) {
- return name.substring(lastDot + 1);
- }
- return ITypedElement.TEXT_TYPE;
- }
-
- @Override
- public InputStream getContents() throws CoreException {
- if (bufferedStorage == null) {
- cacheContents(new NullProgressMonitor());
- }
- if (bufferedStorage != null) {
- return bufferedStorage.getContents();
- }
- return null;
- }
-
- @Override
- public String getCharset() throws CoreException {
- if (bufferedStorage == null) {
- cacheContents(new NullProgressMonitor());
- }
- if (bufferedStorage instanceof IEncodedStorage) {
- return ((IEncodedStorage) bufferedStorage).getCharset();
- }
- // Default to UTF-8 for source files
- return "UTF-8"; //$NON-NLS-1$
- }
-
- /**
- * Cache the contents for the remote resource in a local buffer. This method
- * should be invoked before {@link #getContents()} to ensure that a round
- * trip is not made in that method.
- *
- * @param monitor
- * a progress monitor.
- * @throws CoreException
- * if fetching fails
- */
- public void cacheContents(IProgressMonitor monitor) throws CoreException {
- bufferedStorage = fetchContents(monitor);
- }
-
- /**
- * Fetches the file contents from the pull request provider.
- *
- * @param monitor
- * progress monitor
- * @return storage containing the file contents
- * @throws CoreException
- * if fetching fails
- */
- protected IStorage fetchContents(IProgressMonitor monitor)
- throws CoreException {
- try {
- byte[] content = client.getFileContent(commitId, path);
- return new RemoteFileStorage(name, path, content);
- } catch (IOException e) {
- throw new CoreException(Status.error(
- "Failed to fetch file content: " //$NON-NLS-1$
- + e.getMessage(),
- e));
- }
- }
-
- /**
- * @return the {@link IStorage} that has been buffered for this element
- */
- public IStorage getBufferedStorage() {
- return bufferedStorage;
- }
-
- /**
- * Get the file path
- *
- * @return the file path
- */
- public String getPath() {
- return path;
- }
-
- /**
- * Get the commit ID this file is from
- *
- * @return the commit ID
- */
- public String getCommitId() {
- return commitId;
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public T getAdapter(Class adapter) {
- if (adapter == ISharedDocumentAdapter.class) {
- synchronized (this) {
- if (sharedDocumentAdapter == null) {
- sharedDocumentAdapter = createSharedDocumentAdapter();
- }
- return (T) sharedDocumentAdapter;
- }
- }
- if (adapter == IStorage.class) {
- return (T) bufferedStorage;
- }
- return Platform.getAdapterManager().getAdapter(this, adapter);
- }
-
- /**
- * Creates an {@link ISharedDocumentAdapter} for the element.
- *
- * @return the {@link ISharedDocumentAdapter}
- */
- protected ISharedDocumentAdapter createSharedDocumentAdapter() {
- return new SharedDocumentAdapter() {
-
- @Override
- public IEditorInput getDocumentKey(Object element) {
- return RemoteFileTypedElement.this.getDocumentKey(element);
- }
-
- @Override
- public void flushDocument(IDocumentProvider provider,
- IEditorInput documentKey, IDocument document,
- boolean overwrite) throws CoreException {
- // The document is read-only
- }
- };
- }
-
- /**
- * Method called from the shared document adapter to get the document key.
- *
- * @param element
- * the element
- * @return the document key (editor input)
- */
- protected IEditorInput getDocumentKey(Object element) {
- if (element == this && bufferedStorage != null) {
- return new RemoteFileEditorInput(this, bufferedStorage);
- }
- return null;
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((commitId == null) ? 0 : commitId.hashCode());
- result = prime * result + ((path == null) ? 0 : path.hashCode());
- return result;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null || getClass() != obj.getClass()) {
- return false;
- }
- RemoteFileTypedElement other = (RemoteFileTypedElement) obj;
- if (commitId == null) {
- if (other.commitId != null) {
- return false;
- }
- } else if (!commitId.equals(other.commitId)) {
- return false;
- }
- if (path == null) {
- if (other.path != null) {
- return false;
- }
- } else if (!path.equals(other.path)) {
- return false;
- }
- return true;
- }
-
- /**
- * Internal storage implementation for remote file contents.
- */
- private static class RemoteFileStorage implements IEncodedStorage {
-
- private final String name;
-
- private final String fullPath;
-
- private final byte[] content;
-
- RemoteFileStorage(String name, String fullPath, byte[] content) {
- this.name = name;
- this.fullPath = fullPath;
- this.content = content;
- }
-
- @Override
- public InputStream getContents() throws CoreException {
- return new ByteArrayInputStream(content);
- }
-
- @Override
- public IPath getFullPath() {
- return new Path(fullPath);
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public boolean isReadOnly() {
- return true;
- }
-
- @Override
- public T getAdapter(Class adapter) {
- return null;
- }
-
- @Override
- public String getCharset() throws CoreException {
- // Default to UTF-8 for source files
- return "UTF-8"; //$NON-NLS-1$
- }
- }
-}
From be5fa1d86bd685292a5259e862b7a1ed5e51a2e8 Mon Sep 17 00:00:00 2001
From: Philipp0205 <36603238+Philipp0205@users.noreply.github.com>
Date: Thu, 3 Sep 2026 13:47:30 +0000
Subject: [PATCH 2/2] Isolate local diff test from signing config
Co-authored-by: Kurrle, Philipp
---
.../pullrequest/internal/ui/LocalDiffLineCalculatorTest.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculatorTest.java b/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculatorTest.java
index 4679850..62a6113 100644
--- a/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculatorTest.java
+++ b/org.eclipse.egit.pullrequest.test/src/org/eclipse/egit/pullrequest/internal/ui/LocalDiffLineCalculatorTest.java
@@ -64,6 +64,7 @@ private static RevCommit commit(Git git, String message)
return git.commit().setMessage(message)
.setAuthor("Test User", "test@example.com") //$NON-NLS-1$ //$NON-NLS-2$
.setCommitter("Test User", "test@example.com") //$NON-NLS-1$ //$NON-NLS-2$
+ .setSign(false)
.call();
}
}