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(); } }