Skip to content

Commit

Permalink
Add default Git Hyperlink Provider.
Browse files Browse the repository at this point in the history
 - matches PR/issue IDs and links them to the fitting web page
 - the repo host is identified by inspecting the registered remotes
 - tested with github and several gitlab instances
  • Loading branch information
mbien committed Mar 19, 2024
1 parent 3be92f1 commit a904045
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@
import org.openide.filesystems.FileUtil;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.ServiceProvider;

/**
* Provides hyperlink functionality on issue reference in VCS artefacts as e.g. log messages in Search History
*
* @author Tomas Stupka
*/
@org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.versioning.util.VCSHyperlinkProvider.class)
@ServiceProvider(service=VCSHyperlinkProvider.class, position = 100)
public class VcsHyperlinkProviderImpl extends VCSHyperlinkProvider {

@Override
Expand All @@ -53,11 +54,8 @@ public void onClick(final File file, final String text, int offsetStart, int off
Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "No issue found for {0}", text.substring(offsetStart, offsetEnd));
return;
}
RequestProcessor.getDefault().post(new Runnable() {
@Override
public void run() {
Util.openIssue(FileUtil.toFileObject(file), issueId);
}
RequestProcessor.getDefault().post(() -> {
Util.openIssue(FileUtil.toFileObject(file), issueId);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.netbeans.modules.git.ui.history;

import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import org.netbeans.libs.git.GitClient;
import org.netbeans.libs.git.GitException;
import org.netbeans.libs.git.GitRemoteConfig;
import org.netbeans.libs.git.GitRepository;
import org.netbeans.libs.git.GitURI;
import org.netbeans.libs.git.progress.ProgressMonitor.DefaultProgressMonitor;
import org.netbeans.modules.versioning.util.VCSHyperlinkProvider;
import org.openide.awt.StatusDisplayer;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle.Messages;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.ServiceProvider;

/**
* Looks for '#123' patterns in text (e.g commit messages) and tries to link
* them to a fitting web page.
*
* <p>The repository host is identified by inspecting the registered remotes
* of the local git repository.
*
* @author mbien
*/
@Messages({
"# {0} - issue ID",
"TT_DefaultGitHyperlinkProvider.link=Click to open {0} in Browser",
"MSG_DefaultGitHyperlinkProvider.link.failed=Could not resolve issue link."
})
@ServiceProvider(service=VCSHyperlinkProvider.class, position = Integer.MAX_VALUE)
public class DefaultGitHyperlinkProvider extends VCSHyperlinkProvider {

private static final Pattern ids = Pattern.compile("#[0-9]+");

@Override
public int[] getSpans(String text) {
return ids.matcher(text).results()
.flatMapToInt(r -> IntStream.of(r.start(), r.end()))
.toArray();
}

@Override
public String getTooltip(String text, int offsetStart, int offsetEnd) {
return Bundle.TT_DefaultGitHyperlinkProvider_link(text.substring(offsetStart, offsetEnd));
}

@Override
public void onClick(File file, String text, int offsetStart, int offsetEnd) {
Path path = file.toPath();
while (!Files.isDirectory(path) || Files.notExists(path.resolve(".git"))) {
path = path.getParent();
if (path == null) {
return;
}
}
Path repo = path;
RequestProcessor.getDefault().post(() -> {
openIssue(repo, text.substring(offsetStart + 1, offsetEnd));
});
}

private static void openIssue(Path repo, String id) {
try (GitClient client = GitRepository.getInstance(repo.toFile()).createClient()) {
Map<String, GitRemoteConfig> remotes = client.getRemotes(new DefaultProgressMonitor());
// probe well known remotes
GitRemoteConfig origin =
remotes.getOrDefault("origin4nb",
remotes.getOrDefault("upstream",
remotes.getOrDefault("origin",
remotes.getOrDefault("origin/HEAD", null))));
// fallback: try any of the 'origin/*' remotes
if (origin == null) {
origin = remotes.values().stream()
.filter(r -> r.getRemoteName().startsWith("origin/"))
.findFirst()
.orElse(null);
}
if (origin != null) {
for (String str : origin.getUris()) {
GitURI uri = new GitURI(str);
String path = uri.getPath().substring(0, uri.getPath().length() - 4); // remove .git postfix
if (!path.startsWith("/")) {
path = "/" + path;
}
String page = null;
if (uri.getHost().equals("github.com")) {
page = "/pull/" + id; // url works for issues too
} else if (uri.getHost().contains("gitlab")) {
page = "/-/issues/" + id; // does gitlab auto link merge_requests ?
}
if (page != null) {
URI link = URI.create("https://" + uri.getHost() + path + page);
Desktop.getDesktop().browse(link);
return;
}
}
}
} catch (IOException | URISyntaxException | GitException ex) {
Exceptions.printStackTrace(ex);
}
StatusDisplayer.getDefault().setStatusText(Bundle.MSG_DefaultGitHyperlinkProvider_link_failed());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public void testIndexReadOnlyMethods () throws Exception {
"checkoutRevision",
"cherryPick",
"clean",
"close",
"commit",
"copyAfter",
"createBranch",
Expand Down Expand Up @@ -213,6 +214,7 @@ public void testMethodsNeedingRepositoryInfoRefresh () throws Exception {
"checkoutRevision",
"cherryPick",
"clean",
"close",
"commit",
"copyAfter",
"createBranch",
Expand Down Expand Up @@ -324,6 +326,7 @@ public void testNetworkMethods () throws Exception {
"checkoutRevision",
"cherryPick",
"clean",
"close",
"commit",
"copyAfter",
"createBranch",
Expand Down Expand Up @@ -505,6 +508,7 @@ public void testExclusiveMethods () throws Exception {
"checkoutRevision",
"cherryPick",
"clean",
"close",
"commit",
"copyAfter",
"createBranch",
Expand Down
18 changes: 13 additions & 5 deletions ide/libs.git/src/org/netbeans/libs/git/GitClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
* </ol>
* @author Ondra Vrabec
*/
public final class GitClient {
public final class GitClient implements AutoCloseable {
private final DelegateListener delegateListener;
private GitClassFactory gitFactory;

Expand Down Expand Up @@ -308,7 +308,7 @@ public String toString () {

GitClient (JGitRepository gitRepository) throws GitException {
this.gitRepository = gitRepository;
listeners = new HashSet<NotificationListener>();
listeners = new HashSet<>();
delegateListener = new DelegateListener();
gitRepository.increaseClientUsage();
}
Expand Down Expand Up @@ -1083,6 +1083,14 @@ public void release () {
gitRepository.decreaseClientUsage();
}

/**
* Calls {@link #release()}.
*/
@Override
public void close() {
release();
}

/**
* Removes given files/folders from the index and/or from the working tree
* @param roots files/folders to remove, can not be empty
Expand Down Expand Up @@ -1387,7 +1395,7 @@ public void notifyRevisionInfo (GitRevisionInfo revisionInfo) {
private void notifyFile (File file, String relativePathToRoot) {
List<NotificationListener> lists;
synchronized (listeners) {
lists = new LinkedList<NotificationListener>(listeners);
lists = new LinkedList<>(listeners);
}
for (NotificationListener list : lists) {
if (list instanceof FileListener) {
Expand All @@ -1399,7 +1407,7 @@ private void notifyFile (File file, String relativePathToRoot) {
private void notifyStatus (GitStatus status) {
List<NotificationListener> lists;
synchronized (listeners) {
lists = new LinkedList<NotificationListener>(listeners);
lists = new LinkedList<>(listeners);
}
for (NotificationListener list : lists) {
if (list instanceof StatusListener) {
Expand All @@ -1411,7 +1419,7 @@ private void notifyStatus (GitStatus status) {
private void notifyRevisionInfo (GitRevisionInfo info) {
List<NotificationListener> lists;
synchronized (listeners) {
lists = new LinkedList<NotificationListener>(listeners);
lists = new LinkedList<>(listeners);
}
for (NotificationListener list : lists) {
if (list instanceof RevisionInfoListener) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ public boolean mouseMoved(Point p, JComponent component) {
for (int i = 0; i < start.length; i++) {
if (bounds != null && bounds[i] != null && bounds[i].contains(p)) {
component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
component.setToolTipText(hp.getTooltip(text, start[i], end[i]));
return true;
}
}
Expand Down

0 comments on commit a904045

Please sign in to comment.