From c6f3d01c29bb67156b8154bfe5780537b0ef43ac Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 03 Dec 2013 10:36:19 -0500
Subject: [PATCH] Add support for per-repository bugtraq configuration

---
 src/main/java/com/gitblit/wicket/pages/RepositoryPage.java     |    2 
 src/test/bugtraq/com/syntevo/bugtraq/BugtraqParserTest.java    |  121 ++++++
 src/main/bugtraq/com/syntevo/bugtraq/BugtraqEntry.java         |   61 +++
 src/test/bugtraq/com/syntevo/bugtraq/BugtraqFormatterTest.java |  170 ++++++++
 src/main/bugtraq/LICENSE                                       |   43 ++
 .classpath                                                     |    3 
 src/main/java/com/gitblit/utils/MessageProcessor.java          |   59 ++
 src/main/bugtraq/com/syntevo/bugtraq/BugtraqFormatter.java     |  118 ++++++
 src/main/bugtraq/com/syntevo/bugtraq/BugtraqParserIssueId.java |   61 +++
 src/main/bugtraq/com/syntevo/bugtraq/BugtraqException.java     |   49 ++
 src/main/bugtraq/com/syntevo/bugtraq/BugtraqParser.java        |  151 +++++++
 src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java     |    2 
 src/main/bugtraq/com/syntevo/bugtraq/BugtraqConfig.java        |  240 ++++++++++++
 src/main/java/com/gitblit/servlet/SyndicationServlet.java      |    2 
 .gitbugtraq                                                    |    7 
 src/main/resources/gitblit.css                                 |    4 
 releases.moxie                                                 |    2 
 src/main/java/com/gitblit/wicket/pages/CommitPage.java         |    6 
 build.moxie                                                    |    3 
 gitblit.iml                                                    |   13 
 20 files changed, 1,104 insertions(+), 13 deletions(-)

diff --git a/.classpath b/.classpath
index 9dfa193..b9367bf 100644
--- a/.classpath
+++ b/.classpath
@@ -1,12 +1,15 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
 	<classpathentry kind="src" path="src/main/java" />
+	<classpathentry kind="src" path="src/main/bugtraq" />
 	<classpathentry kind="src" path="src/test/java" output="bin/test-classes" />
+	<classpathentry kind="src" path="src/test/bugtraq" output="bin/test-classes" />
 	<classpathentry kind="src" path="src/main/resources" />
 	<classpathentry kind="lib" path="ext/dagger-1.1.0.jar" sourcepath="ext/src/dagger-1.1.0.jar" />
 	<classpathentry kind="lib" path="ext/javax.inject-1.jar" sourcepath="ext/src/javax.inject-1.jar" />
 	<classpathentry kind="lib" path="ext/dagger-compiler-1.1.0.jar" sourcepath="ext/src/dagger-compiler-1.1.0.jar" />
 	<classpathentry kind="lib" path="ext/javawriter-2.1.1.jar" sourcepath="ext/src/javawriter-2.1.1.jar" />
+	<classpathentry kind="lib" path="ext/annotations-12.0.jar" sourcepath="ext/src/annotations-12.0.jar" />
 	<classpathentry kind="lib" path="ext/jcommander-1.17.jar" sourcepath="ext/src/jcommander-1.17.jar" />
 	<classpathentry kind="lib" path="ext/log4j-1.2.17.jar" sourcepath="ext/src/log4j-1.2.17.jar" />
 	<classpathentry kind="lib" path="ext/slf4j-api-1.6.6.jar" sourcepath="ext/src/slf4j-api-1.6.6.jar" />
diff --git a/.gitbugtraq b/.gitbugtraq
new file mode 100644
index 0000000..9a2670c
--- /dev/null
+++ b/.gitbugtraq
@@ -0,0 +1,7 @@
+[bugtraq "issues"]
+  url = http://code.google.com/p/gitblit/issues/detail?id=%BUGID%
+  logRegex = "[Ii]ssue[-#:\\s]{1}\\d+"
+  logRegex1 = "\\d+"
+[bugtraq "[pullrequests"]
+  url = "https://github.com/gitblit/gitblit/pull/%BUGID%"
+  logRegex = "(?:pull request|pull|pr)\\s*[-#]?([0-9]+)"
diff --git a/build.moxie b/build.moxie
index 44f4cf8..22fdfea 100644
--- a/build.moxie
+++ b/build.moxie
@@ -57,8 +57,10 @@
 # a scope to each directory.
 sourceDirectories:
 - compile 'src/main/java'
+- compile 'src/main/bugtraq'
 - compile 'src/main/dagger' apt
 - test 'src/test/java'
+- test 'src/test/bugtraq'
 # Moxie supports one site-scoped directory for mx:doc
 - site 'src/site'
 
@@ -125,6 +127,7 @@
 - compile 'com.squareup.dagger:dagger:1.1.0' :war apt
 - compile 'com.squareup.dagger:dagger-compiler:1.1.0' :war optional apt
 # Standard dependencies
+- compile 'com.intellij:annotations:12.0' :war
 - compile 'com.beust:jcommander:1.17' :fedclient :authority
 - compile 'log4j:log4j:1.2.17' :war :fedclient :authority
 - compile 'org.slf4j:slf4j-api:1.6.6' :war :fedclient :authority
diff --git a/gitblit.iml b/gitblit.iml
index 8c95ac6..f1daa9b 100644
--- a/gitblit.iml
+++ b/gitblit.iml
@@ -6,8 +6,10 @@
     <exclude-output />
     <content url="file://$MODULE_DIR$">
       <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/bugtraq" isTestSource="false" />
       <sourceFolder url="file://$MODULE_DIR$/src/main/dagger" isTestSource="false" />
       <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/bugtraq" isTestSource="true" />
       <sourceFolder url="file://$MODULE_DIR$/src/main/resources" isTestSource="false" />
     </content>
     <orderEntry type="sourceFolder" forTests="false" />
@@ -56,6 +58,17 @@
       </library>
     </orderEntry>
     <orderEntry type="module-library">
+      <library name="annotations-12.0.jar">
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/ext/annotations-12.0.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES>
+          <root url="jar://$MODULE_DIR$/ext/src/annotations-12.0.jar!/" />
+        </SOURCES>
+      </library>
+    </orderEntry>
+    <orderEntry type="module-library">
       <library name="jcommander-1.17.jar">
         <CLASSES>
           <root url="jar://$MODULE_DIR$/ext/jcommander-1.17.jar!/" />
diff --git a/releases.moxie b/releases.moxie
index 3310cb1..9f559af 100644
--- a/releases.moxie
+++ b/releases.moxie
@@ -53,6 +53,7 @@
 	- Added a normalized diffstat display to the commit, commitdiff, and compare pages
 	- Added GO setting to automatically redirect all http requests to the secure https connector
 	- Automatically display common repository root documents as tabs on the docs page
+	- Support bugtraq configuration in collaboration with syntevo
     dependencyChanges:
 	- updated to Jetty 8.1.13
 	- updated to JGit 3.1.0
@@ -85,6 +86,7 @@
 	- fpeters.fae
 	- David Ostrovsky
 	- Alex Lewis
+	- Marc Strapetz
 }
 
 #
diff --git a/src/main/bugtraq/LICENSE b/src/main/bugtraq/LICENSE
new file mode 100644
index 0000000..550aacc
--- /dev/null
+++ b/src/main/bugtraq/LICENSE
@@ -0,0 +1,43 @@
+Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ o Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+ o Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+ o Neither the name of syntevo GmbH nor the names of
+   its contributors may be used to endorse or promote products derived
+   from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Third Parties
+-------------
+
+The following third parties have rights on parts of the SOFTWARE: 
+
+- IDEA annotations.jar, copyright by JetBrains, Inc.
+
+- JGit, copyright by various authors (http://repo.or.cz/w/jgit.git and
+  http://jgit.org).
+  The corresponding license agreement can be found at
+  http://repo.or.cz/w/jgit.git/blob/HEAD:/LICENSE. 
+
+- JUnit, copyright by www.hamcrest.org.
+  The corresponding license agreement can be found at
+  in subdirectory lib/JUNIT-LICENSE. 
diff --git a/src/main/bugtraq/com/syntevo/bugtraq/BugtraqConfig.java b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqConfig.java
new file mode 100644
index 0000000..1512a9b
--- /dev/null
+++ b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqConfig.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  o Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  o Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  o Neither the name of syntevo GmbH nor the names of
+ *    its contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.syntevo.bugtraq;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class BugtraqConfig {
+
+	// Constants ==============================================================
+
+	private static final String DOT_GIT_BUGTRAQ = ".gitbugtraq";
+
+	private static final String BUGTRAQ = "bugtraq";
+
+	private static final String URL = "url";
+	private static final String ENABLED = "enabled";
+	private static final String LOG_REGEX = "logRegex";
+
+	// Static =================================================================
+
+	@Nullable
+	public static BugtraqConfig read(@NotNull Repository repository) throws IOException, ConfigInvalidException {
+		final Config baseConfig = getBaseConfig(repository);
+		final Set<String> allNames = new HashSet<String>();
+		final Config config = repository.getConfig();
+		allNames.addAll(config.getSubsections(BUGTRAQ));
+		if (baseConfig != null) {
+			allNames.addAll(baseConfig.getSubsections(BUGTRAQ));
+		}
+
+		final List<BugtraqEntry> entries = new ArrayList<BugtraqEntry>();
+		for (String name : allNames) {
+			final String url = getString(name, URL, config, baseConfig);
+			final String enabled = getString(name, ENABLED, config, baseConfig);
+			if (enabled != null && !"true".equals(enabled)) {
+				continue;
+			}
+
+			final String logIdRegex = getString(name, LOG_REGEX, config, baseConfig);
+			if (url == null || logIdRegex == null) {
+				return null;
+			}
+
+			final List<String> logIdRegexs = new ArrayList<String>();
+			logIdRegexs.add(logIdRegex);
+
+			for (int index = 1; index < Integer.MAX_VALUE; index++) {
+				final String logIdRegexN = getString(name, LOG_REGEX + index, config, baseConfig);
+				if (logIdRegexN == null) {
+					break;
+				}
+
+				logIdRegexs.add(logIdRegexN);
+			}
+
+			entries.add(new BugtraqEntry(url, logIdRegexs));
+		}
+
+		if (entries.isEmpty()) {
+			return null;
+		}
+
+		return new BugtraqConfig(entries);
+	}
+
+	// Fields =================================================================
+
+	@NotNull
+	private final List<BugtraqEntry> entries;
+
+	// Setup ==================================================================
+
+	BugtraqConfig(@NotNull List<BugtraqEntry> entries) {
+		this.entries = entries;
+	}
+
+	// Accessing ==============================================================
+
+	@NotNull
+	public List<BugtraqEntry> getEntries() {
+		return Collections.unmodifiableList(entries);
+	}
+
+	// Utils ==================================================================
+
+	@Nullable
+	private static Config getBaseConfig(Repository repository) throws IOException, ConfigInvalidException {
+		final Config baseConfig;
+		if (repository.isBare()) {
+			// read bugtraq config directly from the repository
+			String content = null;
+			String head = repository.getFullBranch();
+			RevWalk rw = new RevWalk(repository);
+			TreeWalk tw = new TreeWalk(repository);
+			tw.setFilter(PathFilterGroup.createFromStrings(DOT_GIT_BUGTRAQ));
+			try {
+				ObjectId headId = repository.resolve(head);
+				RevCommit commit = rw.parseCommit(headId);
+				RevTree tree = commit.getTree();
+				tw.reset(tree);
+				while (tw.next()) {
+					if (tw.isSubtree()) {
+						tw.enterSubtree();
+						continue;
+					}
+					ObjectId entid = tw.getObjectId(0);
+					FileMode entmode = tw.getFileMode(0);
+					if (entmode == FileMode.REGULAR_FILE) {
+						RevObject ro = rw.lookupAny(entid, entmode.getObjectType());
+						rw.parseBody(ro);
+						ByteArrayOutputStream os = new ByteArrayOutputStream();
+						ObjectLoader ldr = repository.open(ro.getId(), Constants.OBJ_BLOB);
+						byte[] tmp = new byte[4096];
+						InputStream in = ldr.openStream();
+						int n;
+						while ((n = in.read(tmp)) > 0) {
+							os.write(tmp, 0, n);
+						}
+						in.close();
+						content = new String(os.toByteArray(), commit.getEncoding());
+					}
+				}
+			} finally {
+				rw.dispose();
+				tw.release();
+			}
+
+			if (content == null) {
+				// config not found
+				baseConfig = null;
+			} else {
+				// parse the config
+				Config cfg = new Config();
+				cfg.fromText(content);
+				baseConfig = new StoredConfig(cfg) {
+					@Override
+					public void save() throws IOException {
+					}
+
+					@Override
+					public void load() throws IOException, ConfigInvalidException {
+					}
+				};
+			}
+		} else {
+			// read bugtraq config from work tree
+			final File baseFile = new File(repository.getWorkTree(), DOT_GIT_BUGTRAQ);
+			if (baseFile.isFile()) {
+				FileBasedConfig fileConfig = new FileBasedConfig(baseFile, repository.getFS());
+				fileConfig.load();
+				baseConfig = fileConfig;
+			}
+			else {
+				baseConfig = null;
+			}
+		}
+		return baseConfig;
+	}
+
+	@Nullable
+	private static String getString(@NotNull String subsection, @NotNull String key, @NotNull Config config, @Nullable Config baseConfig) {
+		final String value = config.getString(BUGTRAQ, subsection, key);
+		if (value != null) {
+			return trimMaybeNull(value);
+		}
+
+		if (baseConfig != null) {
+			return trimMaybeNull(baseConfig.getString(BUGTRAQ, subsection, key));
+		}
+
+		return value;
+	}
+
+	@Nullable
+	private static String trimMaybeNull(@Nullable String string) {
+		if (string == null) {
+			return null;
+		}
+
+		string = string.trim();
+		if (string.length() == 0) {
+			return null;
+		}
+
+		return string;
+	}
+}
\ No newline at end of file
diff --git a/src/main/bugtraq/com/syntevo/bugtraq/BugtraqEntry.java b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqEntry.java
new file mode 100644
index 0000000..7ecd8bf
--- /dev/null
+++ b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqEntry.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  o Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  o Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  o Neither the name of syntevo GmbH nor the names of
+ *    its contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.syntevo.bugtraq;
+
+import java.util.*;
+
+import org.jetbrains.annotations.*;
+
+final class BugtraqEntry {
+
+	// Fields =================================================================
+
+	private final String url;
+	private final BugtraqParser parser;
+
+	// Setup ==================================================================
+
+	public BugtraqEntry(@NotNull String url, @NotNull List<String> logIdRegexs) throws BugtraqException {
+		this.url = url;
+		this.parser = BugtraqParser.createInstance(logIdRegexs);
+	}
+
+	// Accessing ==============================================================
+
+	@NotNull
+	public String getUrl() {
+		return url;
+	}
+
+	@NotNull
+	public BugtraqParser getParser() {
+		return parser;
+	}
+}
\ No newline at end of file
diff --git a/src/main/bugtraq/com/syntevo/bugtraq/BugtraqException.java b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqException.java
new file mode 100644
index 0000000..fdc8dae
--- /dev/null
+++ b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  o Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  o Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  o Neither the name of syntevo GmbH nor the names of
+ *    its contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.syntevo.bugtraq;
+
+import java.io.*;
+
+public class BugtraqException extends IOException {
+
+	// Setup ==================================================================
+
+	public BugtraqException(String message) {
+		super(message);
+	}
+
+	public BugtraqException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public BugtraqException(Throwable cause) {
+		super(cause);
+	}
+}
\ No newline at end of file
diff --git a/src/main/bugtraq/com/syntevo/bugtraq/BugtraqFormatter.java b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqFormatter.java
new file mode 100644
index 0000000..978cd8c
--- /dev/null
+++ b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqFormatter.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  o Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  o Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  o Neither the name of syntevo GmbH nor the names of
+ *    its contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.syntevo.bugtraq;
+
+import java.util.*;
+
+import org.jetbrains.annotations.*;
+
+public final class BugtraqFormatter {
+
+	// Fields =================================================================
+
+	private final BugtraqConfig config;
+
+	// Setup ==================================================================
+
+	public BugtraqFormatter(@NotNull BugtraqConfig config) {
+		this.config = config;
+	}
+
+	// Accessing ==============================================================
+
+	public void formatLogMessage(@NotNull String message, @NotNull OutputHandler outputHandler) {
+		final SortedSet<IssueId> allIds = new TreeSet<IssueId>(new Comparator<IssueId>() {
+			@Override
+			public int compare(IssueId o1, IssueId o2) {
+				final int from1 = o1.id.getFrom();
+				final int from2 = o2.id.getFrom();
+				return from1 > from2 ? +1 : from1 < from2 ? -1 : 0;
+			}
+		});
+
+		for (BugtraqEntry entry : config.getEntries()) {
+			final List<BugtraqParserIssueId> ids = entry.getParser().parse(message);
+			if (ids == null) {
+				continue;
+			}
+
+			for (BugtraqParserIssueId id : ids) {
+				allIds.add(new IssueId(entry, id));
+			}
+		}
+
+		int lastIdEnd = -1;
+		for (IssueId issueId : allIds) {
+			final BugtraqParserIssueId id = issueId.id;
+			if (id.getFrom() <= lastIdEnd) {
+				continue;
+			}
+
+			appendText(message.substring(lastIdEnd + 1, id.getFrom()), outputHandler);
+
+			final String linkText = message.substring(id.getFrom(), id.getTo() + 1);
+			final String target = issueId.entry.getUrl().replace("%BUGID%", id.getId());
+			outputHandler.appendLink(linkText, target);
+			lastIdEnd = id.getTo();
+		}
+
+		if (lastIdEnd - 1 < message.length()) {
+			appendText(message.substring(lastIdEnd + 1, message.length()), outputHandler);
+		}
+	}
+
+	// Utils ==================================================================
+
+	private static void appendText(@NotNull String message, @NotNull OutputHandler outputHandler) {
+		if (message.length() == 0) {
+			return;
+		}
+
+		outputHandler.appendText(message);
+	}
+
+	// Inner Classes ==========================================================
+
+	public interface OutputHandler {
+		void appendText(@NotNull String text);
+
+		void appendLink(@NotNull String name, @NotNull String target);
+	}
+	
+	private static class IssueId {
+		private final BugtraqEntry entry;
+		private final BugtraqParserIssueId id;
+
+		private IssueId(BugtraqEntry entry, BugtraqParserIssueId id) {
+			this.entry = entry;
+			this.id = id;
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/bugtraq/com/syntevo/bugtraq/BugtraqParser.java b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqParser.java
new file mode 100644
index 0000000..8619c4c
--- /dev/null
+++ b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqParser.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  o Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  o Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  o Neither the name of syntevo GmbH nor the names of
+ *    its contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.syntevo.bugtraq;
+
+import java.util.*;
+import java.util.regex.*;
+
+import org.jetbrains.annotations.*;
+
+final class BugtraqParser {
+
+	// Static =================================================================
+
+	@NotNull
+	public static BugtraqParser createInstance(@NotNull List<String> regexs) throws BugtraqException {
+		try {
+			return new BugtraqParser(regexs);
+		}
+		catch (PatternSyntaxException ex) {
+			throw new BugtraqException(ex);
+		}
+	}
+
+	// Fields =================================================================
+
+	private final List<Pattern> patterns;
+
+	// Setup ==================================================================
+
+	private BugtraqParser(List<String> regexs) {
+		this.patterns = new ArrayList<Pattern>();
+
+		for (String regex : regexs) {
+			patterns.add(compilePatternSafe(regex));
+		}
+	}
+
+	// Accessing ==============================================================
+
+	@Nullable
+	public List<BugtraqParserIssueId> parse(@NotNull String message) {
+		List<Part> parts = new ArrayList<Part>();
+		parts.add(new Part(message, 0, message.length() - 1));
+
+		boolean firstMatch = false;
+
+		for (Pattern pattern : patterns) {
+			final List<Part> newParts = new ArrayList<Part>();
+			for (Part part : parts) {
+				final Matcher matcher = pattern.matcher(part.text);
+				while (matcher.find()) {
+					firstMatch = true;
+					if (matcher.groupCount() == 0) {
+						addNewPart(part, matcher, 0, newParts);
+					}
+					else {
+						addNewPart(part, matcher, 1, newParts);
+					}
+				}
+			}
+
+			parts = newParts;
+			if (parts.isEmpty()) {
+				parts = null;
+				break;
+			}
+		}
+
+		if (!firstMatch) {
+			return null;
+		}
+
+		if (parts == null) {
+			parts = new ArrayList<Part>();
+		}
+
+		final List<BugtraqParserIssueId> ids = new ArrayList<BugtraqParserIssueId>();
+		for (Part part : parts) {
+			final BugtraqParserIssueId id = new BugtraqParserIssueId(part.from, part.to, part.text);
+			if (ids.size() > 0) {
+				final BugtraqParserIssueId lastId = ids.get(ids.size() - 1);
+				if (id.getFrom() <= lastId.getTo()) {
+					continue;
+				}
+			}
+
+			ids.add(id);
+		}
+
+		return ids;
+	}
+
+	// Utils ==================================================================
+
+	private static void addNewPart(Part part, Matcher matcher, int group, List<Part> newParts) {
+		final int textStart = matcher.start(group) + part.from;
+		final int textEnd = matcher.end(group) - 1 + part.from;
+		if (textEnd < 0) {
+			return;
+		}
+
+		final Part newPart = new Part(matcher.group(group), textStart, textEnd);
+		newParts.add(newPart);
+	}
+
+	private static Pattern compilePatternSafe(String pattern) throws PatternSyntaxException {
+		return Pattern.compile(pattern);
+	}
+
+	// Inner Classes ==========================================================
+
+	private static class Part {
+
+		private final int from;
+		private final int to;
+		private final String text;
+
+		public Part(String text, int from, int to) {
+			this.text = text;
+			this.from = from;
+			this.to = to;
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/bugtraq/com/syntevo/bugtraq/BugtraqParserIssueId.java b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqParserIssueId.java
new file mode 100644
index 0000000..fddb4df
--- /dev/null
+++ b/src/main/bugtraq/com/syntevo/bugtraq/BugtraqParserIssueId.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  o Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  o Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  o Neither the name of syntevo GmbH nor the names of
+ *    its contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.syntevo.bugtraq;
+
+final class BugtraqParserIssueId {
+
+	// Fields =================================================================
+
+	private final int from;
+	private final int to;
+	private final String id;
+
+	// Setup ==================================================================
+
+	public BugtraqParserIssueId(int from, int to, String id) {
+		this.id = id;
+		this.from = from;
+		this.to = to;
+	}
+
+	// Accessing ==============================================================
+
+	public int getFrom() {
+		return from;
+	}
+
+	public int getTo() {
+		return to;
+	}
+
+	public String getId() {
+		return id;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/servlet/SyndicationServlet.java b/src/main/java/com/gitblit/servlet/SyndicationServlet.java
index 739ee2d..a35efa8 100644
--- a/src/main/java/com/gitblit/servlet/SyndicationServlet.java
+++ b/src/main/java/com/gitblit/servlet/SyndicationServlet.java
@@ -273,7 +273,7 @@
 						StringUtils.encodeURL(model.name.replace('/', fsc)), commit.getName());
 				entry.published = commit.getCommitterIdent().getWhen();
 				entry.contentType = "text/html";
-				String message = processor.processCommitMessage(model, commit.getFullMessage());
+				String message = processor.processCommitMessage(repository, model, commit.getFullMessage());
 				entry.content = message;
 				entry.repository = model.name;
 				entry.branch = objectId;
diff --git a/src/main/java/com/gitblit/utils/MessageProcessor.java b/src/main/java/com/gitblit/utils/MessageProcessor.java
index 58493de..087187f 100644
--- a/src/main/java/com/gitblit/utils/MessageProcessor.java
+++ b/src/main/java/com/gitblit/utils/MessageProcessor.java
@@ -15,17 +15,24 @@
  */
 package com.gitblit.utils;
 
+import java.io.IOException;
+import java.text.MessageFormat;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
 import com.gitblit.models.RepositoryModel;
+import com.syntevo.bugtraq.BugtraqConfig;
+import com.syntevo.bugtraq.BugtraqFormatter;
+import com.syntevo.bugtraq.BugtraqFormatter.OutputHandler;
 
 public class MessageProcessor {
 
@@ -44,14 +51,15 @@
 	 * This method uses the preferred renderer to transform the commit message.
 	 *
 	 * @param repository
+	 * @param model
 	 * @param text
 	 * @return html version of the commit message
 	 */
-	public String processCommitMessage(RepositoryModel repository, String text) {
-		switch (repository.commitMessageRenderer) {
+	public String processCommitMessage(Repository repository, RepositoryModel model, String text) {
+		switch (model.commitMessageRenderer) {
 		case MARKDOWN:
 			try {
-				String prepared = processCommitMessageRegex(repository.name, text);
+				String prepared = processCommitMessageRegex(repository, model.name, text);
 				return MarkdownUtils.transformMarkdown(prepared);
 			} catch (Exception e) {
 				logger.error("Failed to render commit message as markdown", e);
@@ -62,7 +70,7 @@
 			break;
 		}
 
-		return processPlainCommitMessage(repository.name, text);
+		return processPlainCommitMessage(repository, model.name, text);
 	}
 
 	/**
@@ -71,13 +79,14 @@
 	 *
 	 * This method assumes the commit message is plain text.
 	 *
+	 * @param repository
 	 * @param repositoryName
 	 * @param text
 	 * @return html version of the commit message
 	 */
-	public String processPlainCommitMessage(String repositoryName, String text) {
+	public String processPlainCommitMessage(Repository repository, String repositoryName, String text) {
 		String html = StringUtils.escapeForHtml(text, false);
-		html = processCommitMessageRegex(repositoryName, html);
+		html = processCommitMessageRegex(repository, repositoryName, html);
 		return StringUtils.breakLinesForHtml(html);
 
 	}
@@ -86,11 +95,12 @@
 	 * Apply globally or per-repository specified regex substitutions to the
 	 * commit message.
 	 *
+	 * @param repository
 	 * @param repositoryName
 	 * @param text
 	 * @return the processed commit message
 	 */
-	protected String processCommitMessageRegex(String repositoryName, String text) {
+	protected String processCommitMessageRegex(Repository repository, String repositoryName, String text) {
 		Map<String, String> map = new HashMap<String, String>();
 		// global regex keys
 		if (settings.getBoolean(Keys.regex.global, false)) {
@@ -121,6 +131,41 @@
 						+ definition);
 			}
 		}
+
+		try {
+			// parse bugtraq repo config
+			BugtraqConfig config = BugtraqConfig.read(repository);
+			if (config != null) {
+				BugtraqFormatter formatter = new BugtraqFormatter(config);
+				StringBuilder sb = new StringBuilder();
+				formatter.formatLogMessage(text, new BugtraqOutputHandler(sb));
+				text = sb.toString();
+			}
+		} catch (IOException e) {
+			logger.error(MessageFormat.format("Bugtraq config for {0} is invalid!", repositoryName), e);
+		} catch (ConfigInvalidException e) {
+			logger.error(MessageFormat.format("Bugtraq config for {0} is invalid!", repositoryName), e);
+		}
+
 		return text;
 	}
+
+	private class BugtraqOutputHandler implements OutputHandler {
+
+		final StringBuilder sb;
+
+		BugtraqOutputHandler(StringBuilder sb) {
+			this.sb = sb;
+		}
+
+		@Override
+		public void appendText(String text) {
+			sb.append(text);
+		}
+
+		@Override
+		public void appendLink(String name, String target) {
+			sb.append(MessageFormat.format("<a class=\"bugtraq\" href=\"{1}\">{0}</a>", name, target));
+		}
+	}
 }
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
index 5d14bfe..f3f6175 100644
--- a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -107,7 +107,7 @@
 				item.add(new GravatarImage("noteAuthorAvatar", entry.notesRef.getAuthorIdent()));
 				item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
 						.getAuthorIdent().getWhen(), getTimeZone(), getTimeUtils()));
-				item.add(new Label("noteContent", messageProcessor().processPlainCommitMessage(repositoryName,
+				item.add(new Label("noteContent", messageProcessor().processPlainCommitMessage(getRepository(), repositoryName,
 						entry.content)).setEscapeModelStrings(false));
 			}
 		};
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.java b/src/main/java/com/gitblit/wicket/pages/CommitPage.java
index 5942233..0998c71 100644
--- a/src/main/java/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.java
@@ -135,7 +135,7 @@
 				item.add(new GravatarImage("noteAuthorAvatar", entry.notesRef.getAuthorIdent()));
 				item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
 						.getAuthorIdent().getWhen(), getTimeZone(), getTimeUtils()));
-				item.add(new Label("noteContent", messageProcessor().processPlainCommitMessage(repositoryName,
+				item.add(new Label("noteContent", messageProcessor().processPlainCommitMessage(getRepository(), repositoryName,
 						entry.content)).setEscapeModelStrings(false));
 			}
 		};
@@ -198,11 +198,11 @@
 									.newPathParameter(repositoryName, entry.commitId, path)));
 				}
 
-				
+
 				// quick links
 				if (entry.isSubmodule()) {
 					item.add(new ExternalLink("raw", "").setEnabled(false));
-					
+
 					// submodule
 					item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
 							.newPathParameter(repositoryName, entry.commitId, entry.path))
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
index a108f92..dc0233b 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -516,7 +516,7 @@
 
 	protected void addFullText(String wicketId, String text) {
 		RepositoryModel model = getRepositoryModel();
-		String content = messageProcessor().processCommitMessage(model, text);
+		String content = messageProcessor().processCommitMessage(r, model, text);
 		String html;
 		switch (model.commitMessageRenderer) {
 		case MARKDOWN:
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index c07f8bf..58c0aed 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -34,6 +34,10 @@
 	vertical-align: text-bottom;
 }
 
+a.bugtraq {
+    font-weight: bold;
+}
+
 [class^="icon-"], [class*=" icon-"] i {
 	/* override for a links that look like bootstrap buttons */
 	vertical-align: text-bottom;
diff --git a/src/test/bugtraq/com/syntevo/bugtraq/BugtraqFormatterTest.java b/src/test/bugtraq/com/syntevo/bugtraq/BugtraqFormatterTest.java
new file mode 100644
index 0000000..54f0e42
--- /dev/null
+++ b/src/test/bugtraq/com/syntevo/bugtraq/BugtraqFormatterTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  o Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  o Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  o Neither the name of syntevo GmbH nor the names of
+ *    its contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.syntevo.bugtraq;
+
+import junit.framework.*;
+
+import java.util.*;
+
+import org.jetbrains.annotations.*;
+
+public class BugtraqFormatterTest extends TestCase {
+
+	// Accessing ==============================================================
+
+	public void testSimple() throws BugtraqException {
+		final BugtraqFormatter formatter = createFormatter(createEntry("https://jira.atlassian.com/browse/%BUGID%", "JRA-\\d+"));
+		doTest(formatter, "JRA-7399: Email subject formatting", l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), t(": Email subject formatting"));
+		doTest(formatter, " JRA-7399, JRA-7398: Email subject formatting", t(" "), l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), t(", "), l("JRA-7398", "https://jira.atlassian.com/browse/JRA-7398"), t(": Email subject formatting"));
+		doTest(formatter, "Fixed JRA-7399", t("Fixed "), l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"));
+	}
+
+	public void testTwoNonIntersectingConfigurations() throws BugtraqException {
+		final BugtraqFormatter formatter = createFormatter(createEntry("https://jira.atlassian.com/browse/%BUGID%", "JRA-\\d+"),
+		                                                   createEntry("https://issues.apache.org/jira/browse/%BUGID%", "VELOCITY-\\d+"));
+		doTest(formatter, "JRA-7399, VELOCITY-847: fix", l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), t(", "), l("VELOCITY-847", "https://issues.apache.org/jira/browse/VELOCITY-847"), t(": fix"));
+		doTest(formatter, " JRA-7399: fix/VELOCITY-847", t(" "), l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), t(": fix/"), l("VELOCITY-847", "https://issues.apache.org/jira/browse/VELOCITY-847"));
+		doTest(formatter, "JRA-7399VELOCITY-847", l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), l("VELOCITY-847", "https://issues.apache.org/jira/browse/VELOCITY-847"));
+	}
+
+	public void testTwoIntersectingConfigurations() throws BugtraqException {
+		final BugtraqFormatter formatter = createFormatter(createEntry("https://host1/%BUGID%", "A[AB]"),
+		                                                   createEntry("https://host2/%BUGID%", "BA[A]?"));
+		doTest(formatter, "AA: fix", l("AA", "https://host1/AA"), t(": fix"));
+		doTest(formatter, "AB: fix", l("AB", "https://host1/AB"), t(": fix"));
+		doTest(formatter, "BA: fix", l("BA", "https://host2/BA"), t(": fix"));
+		doTest(formatter, "BAA: fix", l("BAA", "https://host2/BAA"), t(": fix"));
+		doTest(formatter, "BAAA: fix", l("BAA", "https://host2/BAA"), t("A: fix"));
+		doTest(formatter, "BAAAA: fix", l("BAA", "https://host2/BAA"), l("AA", "https://host1/AA"), t(": fix"));
+		doTest(formatter, "BAAAAA: fix", l("BAA", "https://host2/BAA"), l("AA", "https://host1/AA"), t("A: fix"));
+		doTest(formatter, "BAAABA: fix", l("BAA", "https://host2/BAA"), l("AB", "https://host1/AB"), t("A: fix"));
+		doTest(formatter, "BAAABAA: fix", l("BAA", "https://host2/BAA"), l("AB", "https://host1/AB"), l("AA", "https://host1/AA"), t(": fix"));
+		doTest(formatter, "BAAB: fix", l("BAA", "https://host2/BAA"), t("B: fix"));
+		doTest(formatter, "BAAAB: fix", l("BAA", "https://host2/BAA"), l("AB", "https://host1/AB"), t(": fix"));
+		doTest(formatter, "BAABBA: fix", l("BAA", "https://host2/BAA"), t("B"), l("BA", "https://host2/BA"), t(": fix"));
+	}
+
+	// Utils ==================================================================
+
+	private BugtraqFormatter createFormatter(BugtraqEntry ... entries) {
+		return new BugtraqFormatter(new BugtraqConfig(Arrays.asList(entries)));
+	}
+	
+	private BugtraqEntry createEntry(String url, String ... logRegexs) throws BugtraqException {
+		return new BugtraqEntry(url, Arrays.asList(logRegexs));
+	}
+	
+	private Text t(String text) {
+		return new Text(text);
+	}
+	
+	private Link l(String text, String url) {
+		return new Link(text, url);
+	}
+	
+	private void doTest(BugtraqFormatter formatter, String message, Atom ... expectedAtoms) {
+		final List<Atom> actualAtoms = new ArrayList<Atom>();
+		formatter.formatLogMessage(message, new BugtraqFormatter.OutputHandler() {
+			@Override
+			public void appendText(@NotNull String text) {
+				actualAtoms.add(t(text));
+			}
+
+			@Override
+			public void appendLink(@NotNull String name, @NotNull String target) {
+				actualAtoms.add(l(name, target));
+			}
+		});
+		
+		assertEquals(Arrays.asList(expectedAtoms), actualAtoms);
+	}
+
+	// Inner Classes ==========================================================
+
+	private static interface Atom {
+	}
+	
+	private static class Text implements Atom {
+		private final String text;
+
+		private Text(String text) {
+			this.text = text;
+		}
+
+		@Override
+		public String toString() {
+			return text;
+		}
+
+		@Override
+		public int hashCode() {
+			return text.hashCode();
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (obj == null || obj.getClass() != getClass()) {
+				return false;
+			}
+			
+			return text.equals(((Text)obj).text);
+		}
+	}
+	
+	private static class Link implements Atom {
+		private final String text;
+		private final String url;
+
+		private Link(String text, String url) {
+			this.text = text;
+			this.url = url;
+		}
+
+		@Override
+		public String toString() {
+			return "(" + text + "," + url + ")";
+		}
+
+		@Override
+		public int hashCode() {
+			return text.hashCode() ^ url.hashCode();
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (obj == null || obj.getClass() != getClass()) {
+				return false;
+			}
+			
+			return text.equals(((Link)obj).text)
+					&& url.equals(((Link)obj).url);
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/test/bugtraq/com/syntevo/bugtraq/BugtraqParserTest.java b/src/test/bugtraq/com/syntevo/bugtraq/BugtraqParserTest.java
new file mode 100644
index 0000000..424fd1c
--- /dev/null
+++ b/src/test/bugtraq/com/syntevo/bugtraq/BugtraqParserTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  o Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  o Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  o Neither the name of syntevo GmbH nor the names of
+ *    its contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.syntevo.bugtraq;
+
+import junit.framework.*;
+
+import java.util.*;
+
+public class BugtraqParserTest extends TestCase {
+
+	// Accessing ==============================================================
+
+	public void testSimple1() throws BugtraqException {
+		final BugtraqParser parser = createParser("\\d");
+		assertNull(parser.parse(""));
+		doTest("1", parser, id(0, 0, "1"));
+		doTest("1 2 3", parser, id(0, 0, "1"), id(2, 2, "2"), id(4, 4, "3"));
+	}
+
+	public void testSimple2() throws BugtraqException {
+		final BugtraqParser parser = createParser("(\\d)");
+		assertNull(parser.parse(""));
+		doTest("1", parser, id(0, 0, "1"));
+		doTest("1 2 3", parser, id(0, 0, "1"), id(2, 2, "2"), id(4, 4, "3"));
+	}
+
+	public void testSimple3() throws BugtraqException {
+		final BugtraqParser parser = createParser("(SG-\\d)");
+		assertNull(parser.parse(""));
+		doTest("SG-1", parser, id(0, 3, "SG-1"));
+		doTest("SG-1 SG-2 SG-3", parser, id(0, 3, "SG-1"), id(5, 8, "SG-2"), id(10, 13, "SG-3"));
+	}
+
+	public void testSimple4() throws BugtraqException {
+		final BugtraqParser parser = createParser("SG-(\\d)");
+		assertNull(parser.parse(""));
+		doTest("SG-1", parser, id(3, 3, "1"));
+		doTest("SG-1 SG-2 SG-3", parser, id(3, 3, "1"), id(8, 8, "2"), id(13, 13, "3"));
+	}
+
+	public void testTwoLevel1() throws BugtraqException {
+		final BugtraqParser parser = createParser("(SG-\\d)", "\\d");
+		doTest("SG-1", parser, id(3, 3, "1"));
+		doTest("SG-1 SG-2 SG-3", parser, id(3, 3, "1"), id(8, 8, "2"), id(13, 13, "3"));
+	}
+
+	public void testTwoLevel2() throws BugtraqException {
+		final BugtraqParser parser = createParser("xSG-\\dx", "\\d");
+		doTest("SG-1 xSG-2x SG-3", parser, id(9, 9, "2"));
+	}
+
+	public void testTwoLevel3() throws BugtraqException {
+		final BugtraqParser parser = createParser("[Ii]ssues?:?((\\s*(,|and)?\\s*#\\d+)+)", "\\d");
+		doTest("Issues #3, #4 and #5: Git Bugtraq Configuration options (see #12)", parser, id(8, 8, "3"), id(12, 12, "4"), id(19, 19, "5"));
+	}
+
+	public void testThreeLevel() throws BugtraqException {
+		final BugtraqParser parser = createParser("[ab]\\d[cd]", "a\\dc|b\\dd", "\\d");
+		doTest("a1c a2d b3c b4d", parser, id(1, 1, "1"), id(13, 13, "4"));
+	}
+
+	public void testFogBugz() throws BugtraqException {
+		final BugtraqParser parser = createParser("(?:Bug[zs]?\\s*IDs?\\s*|Cases?)[#:; ]+((\\d+[ ,:;#]*)+)", "\\d");
+		doTest("Bug IDs: 3, 4, 5", parser, id(9, 9, "3"), id(12, 12, "4"), id(15, 15, "5"));
+	}
+
+	public void testFogBugzInvalid() throws BugtraqException {
+		final BugtraqParser parser = createParser("Bug[zs]?\\s*IDs?\\s*|Cases?[#:; ]+((\\d+[ ,:;#]*)+)", "\\d");
+		doTest("Bug IDs: 3, 4, 5", parser);
+	}
+
+	// Utils ==================================================================
+
+	private BugtraqParser createParser(String ... regexs) throws BugtraqException {
+		return BugtraqParser.createInstance(Arrays.asList(regexs));
+	}
+	
+	private BugtraqParserIssueId id(int from, int to, String id) {
+		return new BugtraqParserIssueId(from, to, id);
+	} 
+	
+	private void doTest(String message, BugtraqParser parser, BugtraqParserIssueId... expectedIds) {
+		final List<BugtraqParserIssueId> actualIds = parser.parse(message);
+		assertEquals(expectedIds.length, actualIds.size());
+		
+		for (int index = 0; index < expectedIds.length; index++) {
+			final BugtraqParserIssueId expectedId = expectedIds[index];
+			final BugtraqParserIssueId actualId = actualIds.get(index);
+			assertEquals(expectedId.getFrom(), actualId.getFrom());
+			assertEquals(expectedId.getTo(), actualId.getTo());
+			assertEquals(expectedId.getId(), actualId.getId());
+		}
+	}
+}
\ No newline at end of file

--
Gitblit v1.9.1