/*******************************************************************************
* Copyright (c) 2011 Benjamin Muskalla and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Benjamin Muskalla <benjamin.muskalla@tasktop.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.egit.internal.mylyn.ui;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.Assert;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.RepositoryCache;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.ui.internal.commit.CommitEditor;
import org.eclipse.egit.ui.internal.commit.RepositoryCommit;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
/**
* Detects Git commit ids in task descriptions and allows users to open them in
* the commit editor.
*/
public class CommitHyperlinkDetector extends AbstractHyperlinkDetector {
private static final Pattern PATTERN_COMMIT_ID = Pattern
.compile("(?<!\\w)([0-9a-f]{8}([0-9a-f]{32})?)"); //$NON-NLS-1$
private static class CommitHyperlink implements IHyperlink {
private IRegion region;
private String objectId;
private final Shell shell;
public CommitHyperlink(IRegion region, String objectId, Shell shell) {
this.shell = shell;
Assert.isNotNull(objectId);
Assert.isNotNull(region);
Assert.isNotNull(shell);
this.region = region;
this.objectId = objectId;
}
public IRegion getHyperlinkRegion() {
return region;
}
public String getTypeLabel() {
return null;
}
public String getHyperlinkText() {
return objectId;
}
public void open() {
try {
RepositoryCommit commit;
commit = searchCommit();
if (commit != null)
CommitEditor.openQuiet(commit);
else
informCommitNotFound();
} catch (IOException e) {
// ignore
}
}
private void informCommitNotFound() {
MessageDialog
.openWarning(
shell,
Messages.CommitHyperlinkDetector_CommitNotFound,
NLS.bind(
Messages.CommitHyperlinkDetector_CommitNotFoundInRepositories,
objectId));
}
private RepositoryCommit searchCommit() throws IOException {
RepositoryUtil repositoryUtil = Activator.getDefault()
.getRepositoryUtil();
List<String> configuredRepositories = repositoryUtil
.getConfiguredRepositories();
RepositoryCache repositoryCache = Activator.getDefault()
.getRepositoryCache();
for (String repoDir : configuredRepositories) {
Repository repository = repositoryCache
.lookupRepository(new File(repoDir));
RevCommit commit = getCommit(repository);
if (commit != null)
return new RepositoryCommit(repository, commit);
}
return null;
}
private RevCommit getCommit(Repository repository) throws IOException {
RevWalk revWalk = null;
try {
revWalk = new RevWalk(repository);
return revWalk.parseCommit(ObjectId.fromString(objectId));
} catch (MissingObjectException e) {
// ignore
return null;
} catch (IncorrectObjectTypeException e) {
// ignore
return null;
} finally {
if (revWalk != null)
revWalk.release();
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("CommitHyperlink [region="); //$NON-NLS-1$
builder.append(region);
builder.append(", objectId="); //$NON-NLS-1$
builder.append(objectId);
builder.append("]"); //$NON-NLS-1$
return builder.toString();
}
}
/**
* Detects and returns all available hyperlinks for the given
* {@link TextViewer} which link to a Git commit.
*/
public IHyperlink[] detectHyperlinks(ITextViewer textViewer,
final IRegion region, boolean canShowMultipleHyperlinks) {
IDocument document = textViewer.getDocument();
if (document == null || document.getLength() == 0) {
return null;
}
String content;
int contentOffset;
int index;
try {
if (region.getLength() == 0) {
// expand the region to include the whole line
IRegion lineInfo = document.getLineInformationOfOffset(region
.getOffset());
int lineLength = lineInfo.getLength();
int lineOffset = lineInfo.getOffset();
int lineEnd = lineOffset + lineLength;
int regionEnd = region.getOffset() + region.getLength();
if (lineOffset < region.getOffset()) {
int regionLength = Math.max(regionEnd, lineEnd)
- lineOffset;
contentOffset = lineOffset;
content = document.get(lineOffset, regionLength);
index = region.getOffset() - lineOffset;
} else {
// the line starts after region, may never happen
int regionLength = Math.max(regionEnd, lineEnd)
- region.getOffset();
contentOffset = region.getOffset();
content = document.get(contentOffset, regionLength);
index = 0;
}
} else {
content = document.get(region.getOffset(), region.getLength());
contentOffset = region.getOffset();
index = -1;
}
} catch (BadLocationException ex) {
return null;
}
List<IHyperlink> hyperlinks = detectHyperlinks(textViewer, content,
index, contentOffset);
if (hyperlinks == null) {
return null;
}
// filter hyperlinks that do not match original region
if (region.getLength() == 0) {
for (Iterator<IHyperlink> it = hyperlinks.iterator(); it.hasNext();) {
IHyperlink hyperlink = it.next();
IRegion hyperlinkRegion = hyperlink.getHyperlinkRegion();
if (!isInRegion(region, hyperlinkRegion)) {
it.remove();
}
}
}
if (hyperlinks.isEmpty()) {
return null;
}
return hyperlinks.toArray(new IHyperlink[hyperlinks.size()]);
}
private List<IHyperlink> detectHyperlinks(ITextViewer textViewer,
String content, int index, int contentOffset) {
Shell shell = textViewer.getTextWidget().getShell();
List<IHyperlink> links = null;
Matcher matcher = PATTERN_COMMIT_ID.matcher(content);
while (matcher.find()) {
if (index != -1
&& (index < matcher.start() || index > matcher.end())) {
continue;
}
if (links == null) {
links = new ArrayList<IHyperlink>();
}
int start = matcher.start(1);
Region region = new Region(contentOffset + start, matcher.end(1)
- start);
CommitHyperlink hyperlink = new CommitHyperlink(region,
matcher.group(1), shell);
links.add(hyperlink);
}
return links;
}
private boolean isInRegion(IRegion detectInRegion, IRegion hyperlinkRegion) {
return detectInRegion.getOffset() >= hyperlinkRegion.getOffset()
&& detectInRegion.getOffset() <= hyperlinkRegion.getOffset()
+ hyperlinkRegion.getLength();
}
}