diff --git a/ide/bugtracking.bridge/src/org/netbeans/modules/bugtracking/hyperlink/VcsHyperlinkProviderImpl.java b/ide/bugtracking.bridge/src/org/netbeans/modules/bugtracking/hyperlink/VcsHyperlinkProviderImpl.java index 06c7198e221d..4282eefb246a 100644 --- a/ide/bugtracking.bridge/src/org/netbeans/modules/bugtracking/hyperlink/VcsHyperlinkProviderImpl.java +++ b/ide/bugtracking.bridge/src/org/netbeans/modules/bugtracking/hyperlink/VcsHyperlinkProviderImpl.java @@ -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 @@ -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); }); } diff --git a/ide/git/src/org/netbeans/modules/git/ui/history/DefaultGitHyperlinkProvider.java b/ide/git/src/org/netbeans/modules/git/ui/history/DefaultGitHyperlinkProvider.java new file mode 100644 index 000000000000..f0bdb2887465 --- /dev/null +++ b/ide/git/src/org/netbeans/modules/git/ui/history/DefaultGitHyperlinkProvider.java @@ -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. + * + *

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 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()); + } + +} diff --git a/ide/git/test/unit/src/org/netbeans/modules/git/GitClientTest.java b/ide/git/test/unit/src/org/netbeans/modules/git/GitClientTest.java index 987c81cc4d18..f6decbca3685 100644 --- a/ide/git/test/unit/src/org/netbeans/modules/git/GitClientTest.java +++ b/ide/git/test/unit/src/org/netbeans/modules/git/GitClientTest.java @@ -90,6 +90,7 @@ public void testIndexReadOnlyMethods () throws Exception { "checkoutRevision", "cherryPick", "clean", + "close", "commit", "copyAfter", "createBranch", @@ -213,6 +214,7 @@ public void testMethodsNeedingRepositoryInfoRefresh () throws Exception { "checkoutRevision", "cherryPick", "clean", + "close", "commit", "copyAfter", "createBranch", @@ -324,6 +326,7 @@ public void testNetworkMethods () throws Exception { "checkoutRevision", "cherryPick", "clean", + "close", "commit", "copyAfter", "createBranch", @@ -505,6 +508,7 @@ public void testExclusiveMethods () throws Exception { "checkoutRevision", "cherryPick", "clean", + "close", "commit", "copyAfter", "createBranch", diff --git a/ide/libs.git/src/org/netbeans/libs/git/GitClient.java b/ide/libs.git/src/org/netbeans/libs/git/GitClient.java index 493527435b4a..2a28d89ed1b9 100644 --- a/ide/libs.git/src/org/netbeans/libs/git/GitClient.java +++ b/ide/libs.git/src/org/netbeans/libs/git/GitClient.java @@ -130,7 +130,7 @@ * * @author Ondra Vrabec */ -public final class GitClient { +public final class GitClient implements AutoCloseable { private final DelegateListener delegateListener; private GitClassFactory gitFactory; @@ -308,7 +308,7 @@ public String toString () { GitClient (JGitRepository gitRepository) throws GitException { this.gitRepository = gitRepository; - listeners = new HashSet(); + listeners = new HashSet<>(); delegateListener = new DelegateListener(); gitRepository.increaseClientUsage(); } @@ -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 @@ -1387,7 +1395,7 @@ public void notifyRevisionInfo (GitRevisionInfo revisionInfo) { private void notifyFile (File file, String relativePathToRoot) { List lists; synchronized (listeners) { - lists = new LinkedList(listeners); + lists = new LinkedList<>(listeners); } for (NotificationListener list : lists) { if (list instanceof FileListener) { @@ -1399,7 +1407,7 @@ private void notifyFile (File file, String relativePathToRoot) { private void notifyStatus (GitStatus status) { List lists; synchronized (listeners) { - lists = new LinkedList(listeners); + lists = new LinkedList<>(listeners); } for (NotificationListener list : lists) { if (list instanceof StatusListener) { @@ -1411,7 +1419,7 @@ private void notifyStatus (GitStatus status) { private void notifyRevisionInfo (GitRevisionInfo info) { List lists; synchronized (listeners) { - lists = new LinkedList(listeners); + lists = new LinkedList<>(listeners); } for (NotificationListener list : lists) { if (list instanceof RevisionInfoListener) { diff --git a/ide/versioning.util/src/org/netbeans/modules/versioning/util/VCSHyperlinkSupport.java b/ide/versioning.util/src/org/netbeans/modules/versioning/util/VCSHyperlinkSupport.java index 4d93b0a147d9..a11a4117317d 100644 --- a/ide/versioning.util/src/org/netbeans/modules/versioning/util/VCSHyperlinkSupport.java +++ b/ide/versioning.util/src/org/netbeans/modules/versioning/util/VCSHyperlinkSupport.java @@ -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; } }