Index: pom.xml
===================================================================
--- pom.xml	(revision 21318)
+++ pom.xml	(working copy)
@@ -29,6 +29,18 @@
       <artifactId>issuetracker-stats</artifactId>
       <version>1.1</version>
     </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.6</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+        <groupId>org.mockito</groupId>
+        <artifactId>mockito-core</artifactId>
+        <version>1.7</version> 
+        <scope>test</scope>
+    </dependency>
     <!--
       TODO: dependency resolution isn't working correctly.
       I get copies of jars that are already in hudson-core.
Index: src/main/java/hudson/plugins/javanet/JavaNetChangeLogAnnotator.java
===================================================================
--- src/main/java/hudson/plugins/javanet/JavaNetChangeLogAnnotator.java	(revision 0)
+++ src/main/java/hudson/plugins/javanet/JavaNetChangeLogAnnotator.java	(revision 0)
@@ -0,0 +1,57 @@
+package hudson.plugins.javanet;
+
+import java.util.regex.Pattern;
+
+import hudson.Extension;
+import hudson.MarkupText;
+import hudson.MarkupText.SubText;
+import hudson.model.AbstractBuild;
+import hudson.scm.ChangeLogAnnotator;
+import hudson.scm.ChangeLogSet.Entry;
+
+@Extension
+public class JavaNetChangeLogAnnotator extends ChangeLogAnnotator {
+
+    @Override
+    public void annotate(AbstractBuild<?, ?> build, Entry change, MarkupText text) {
+        StatsProperty property = build.getParent().getProperty(StatsProperty.class);
+        if(property==null)
+            return; // not configured
+        
+        String projectName = property.getJavaNetProject(build.getParent());
+        if (projectName != null) {
+            String url = String.format("https://%s.dev.java.net", projectName);
+            new LinkMarkup(
+                    String.format("%s-\\s*(NUM)", projectName),
+                    "/issues/show_bug.cgi?id=$1",
+                    Pattern.CASE_INSENSITIVE).process(text, url);
+        }
+    }
+
+    static final class LinkMarkup {
+        private final Pattern pattern;
+        private final String href;
+        
+        LinkMarkup(String pattern, String href) {
+            this(pattern, href, 0);
+        }
+
+        LinkMarkup(String pattern, String href, int flags) {
+            pattern = NUM_PATTERN.matcher(pattern).replaceAll("(\\\\d+)"); // \\\\d becomes \\d when in the expanded text.
+            pattern = ANYWORD_PATTERN.matcher(pattern).replaceAll("((?:\\\\w|[_-])+)");
+            this.pattern = Pattern.compile(pattern, flags);
+            this.href = href;
+        }
+
+        void process(MarkupText text, String url) {
+            for(SubText st : text.findTokens(pattern)) {
+                st.surroundWith(
+                    "<a href='"+url+href+"'>",
+                    "</a>");
+            }
+        }
+
+        private static final Pattern NUM_PATTERN = Pattern.compile("NUM");
+        private static final Pattern ANYWORD_PATTERN = Pattern.compile("ANYWORD");
+    }
+}
Index: src/main/java/hudson/plugins/javanet/StatsProperty.java
===================================================================
--- src/main/java/hudson/plugins/javanet/StatsProperty.java	(revision 21318)
+++ src/main/java/hudson/plugins/javanet/StatsProperty.java	(working copy)
@@ -34,7 +34,7 @@
         return new JavaNetStatsAction(job,jnp);
     }
 
-    private String getJavaNetProject(AbstractProject<?,?> job) {
+    String getJavaNetProject(AbstractProject<?,?> job) {
         String v = JavaNetStatsAction.readOverrideFile(job);
         if(v!=null)     return v;
         return getJavaNetProject(job.getScm());
Index: src/test/java/hudson/plugins/javanet/JavaNetChangeLogAnnotatorTest.java
===================================================================
--- src/test/java/hudson/plugins/javanet/JavaNetChangeLogAnnotatorTest.java	(revision 0)
+++ src/test/java/hudson/plugins/javanet/JavaNetChangeLogAnnotatorTest.java	(revision 0)
@@ -0,0 +1,43 @@
+package hudson.plugins.javanet;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import hudson.MarkupText;
+import hudson.model.AbstractBuild;
+import hudson.model.AbstractProject;
+
+import org.junit.Test;
+
+@SuppressWarnings("unchecked")
+public class JavaNetChangeLogAnnotatorTest {
+    @Test
+    public void assertIssueInBracketsIsAnnotated() {
+        AbstractBuild build = createBuildWithProperty("hudson");
+
+        JavaNetChangeLogAnnotator annotator = new JavaNetChangeLogAnnotator();
+        MarkupText markupText = new MarkupText("Message with HUDSON-1212. Yes a link.");
+        annotator.annotate(build, null, markupText);
+        assertEquals("Message with <a href='https://hudson.dev.java.net/issues/show_bug.cgi?id=1212'>HUDSON-1212</a>. Yes a link.", markupText.toString() );
+    }
+    
+    @Test
+    public void assertFixedIssueInBracketsIsAnnotated() {
+        AbstractBuild build = createBuildWithProperty("hudson");
+
+        JavaNetChangeLogAnnotator annotator = new JavaNetChangeLogAnnotator();
+        MarkupText markupText = new MarkupText("Message with [FIXED HUDSON-1212]. Yes a link.");
+        annotator.annotate(build, null, markupText);
+        assertEquals("Message with [FIXED <a href='https://hudson.dev.java.net/issues/show_bug.cgi?id=1212'>HUDSON-1212</a>]. Yes a link.", markupText.toString() );
+    }
+
+    private AbstractBuild createBuildWithProperty(String projectName) {
+        AbstractBuild build = mock(AbstractBuild.class);
+        AbstractProject<?, ?> project = mock(AbstractProject.class);
+        StatsProperty statsProperty = mock(StatsProperty.class);
+        when(statsProperty.getJavaNetProject(any(AbstractProject.class))).thenReturn(projectName);
+        when(project.getProperty(StatsProperty.class)).thenReturn(statsProperty);
+        when(build.getProject()).thenReturn(project);
+        return build;
+    }
+}