Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)`
Expand Down
31 changes: 13 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PullRequestComment> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*******************************************************************************
* 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$
.setSign(false)
.call();
}
}
2 changes: 2 additions & 0 deletions org.eclipse.egit.pullrequest/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,6 @@ public List<PullRequestComment> 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,
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,6 @@ List<ChangedFile> getPullRequestChanges(long pullRequestId)
List<PullRequestComment> 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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,37 +446,6 @@ private java.util.Map<Long, Boolean> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -714,30 +713,6 @@ private static List<PullRequestComment> 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
*
Expand Down
Loading