SLING-5099 - Create 'New and Noteworthy' page for Sling 8 

Enhance the launchpad-comparator to list fixed Jira issues for Sling
artifacts with version changes.

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1708567 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89540d8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/dependency-reduced-pom.xml
diff --git a/pom.xml b/pom.xml
index 2cc217e..528f4d1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -106,6 +106,20 @@
             <version>3.0.15</version>
         </dependency>
 
+        <!-- Retrieve changelog data from SVN -->
+        <dependency>
+            <groupId>org.tmatesoft.svnkit</groupId>
+            <artifactId>svnkit</artifactId>
+            <version>1.8.11</version>
+        </dependency>
+
+        <!-- Parse Jira JSON responses -->
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.2.4</version>
+        </dependency>
+
     </dependencies>
 
     <build>
@@ -116,7 +130,8 @@
                 <version>2.4.1</version>
                 <configuration>
                     <transformers>
-                        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                        <transformer
+                            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                             <mainClass>org.apache.sling.tooling.lc.Main</mainClass>
                         </transformer>
                     </transformers>
@@ -130,6 +145,15 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>dependency-reduced-pom.xml</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 
diff --git a/src/main/java/org/apache/sling/tooling/lc/LaunchpadComparer.java b/src/main/java/org/apache/sling/tooling/lc/LaunchpadComparer.java
new file mode 100644
index 0000000..df4c931
--- /dev/null
+++ b/src/main/java/org/apache/sling/tooling/lc/LaunchpadComparer.java
@@ -0,0 +1,171 @@
+/*
+ * 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.apache.sling.tooling.lc;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.apache.sling.maven.projectsupport.BundleListUtils;
+import org.apache.sling.maven.projectsupport.bundlelist.v1_0_0.Bundle;
+import org.apache.sling.maven.projectsupport.bundlelist.v1_0_0.BundleList;
+import org.apache.sling.provisioning.model.Artifact;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.ModelUtility;
+import org.apache.sling.provisioning.model.io.ModelReader;
+import org.apache.sling.tooling.lc.aether.AetherSetup;
+import org.apache.sling.tooling.lc.aether.ArtifactKey;
+import org.apache.sling.tooling.lc.aether.Artifacts;
+import org.apache.sling.tooling.lc.aether.VersionChange;
+import org.apache.sling.tooling.lc.jira.IssueFinder;
+import org.apache.sling.tooling.lc.svn.SvnChangeLogFinder;
+import org.tmatesoft.svn.core.SVNException;
+
+import com.google.common.collect.Sets;
+
+public class LaunchpadComparer {
+    
+    private static final Pattern JIRA_KEY_PATTERN = Pattern.compile("^(SLING-\\d+).*");
+
+    private final String firstVersion;
+    private final String secondVersion;
+
+    public LaunchpadComparer(String firstVersion, String secondVersion) {
+        this.firstVersion = firstVersion;
+        this.secondVersion = secondVersion;
+    }
+
+    public void run() throws Exception {
+        
+        System.out.format("Computing differences between Launchpad versions %s and %s...%n", 
+                firstVersion, secondVersion);
+        
+        // 1. download artifacts
+        AetherSetup aether = new AetherSetup();
+
+        File fromFile = aether.download(Artifacts.launchpadCoordinates(firstVersion));
+        File toFile = aether.download(Artifacts.launchpadCoordinates(secondVersion));
+
+        // 2. parse artifact definitions
+        Model model;
+        try (BufferedReader reader = Files.newBufferedReader(toFile.toPath())) {
+            model = ModelUtility.getEffectiveModel(ModelReader.read(reader, null));
+        }
+        
+        Map<ArtifactKey, Artifact> to = model.getFeatures().stream()
+            .flatMap( f -> f.getRunModes().stream())
+            .flatMap( r -> r.getArtifactGroups().stream())
+            .flatMap( g -> StreamSupport.stream(g.spliterator(), false))
+            .collect(Collectors.toMap( a -> new ArtifactKey(a), Function.identity()));
+        
+        BundleList readBundleList = BundleListUtils.readBundleList(fromFile);
+        
+        Map<ArtifactKey, Artifact> from = readBundleList.getStartLevels().stream()
+            .flatMap( sl -> sl.getBundles().stream() )
+            .collect(Collectors.toMap( b -> new ArtifactKey(b), LaunchpadComparer::newArtifact));
+
+        // 3. generate added / removed / changed
+        Set<Artifact> removed = Sets.difference(from.keySet(), to.keySet()).stream()
+            .map( k -> from.get(k))
+            .collect(Collectors.toSet());
+
+        Set<Artifact> added = Sets.difference(to.keySet(), from.keySet()).stream()
+                .map( k -> to.get(k))
+                .collect(Collectors.toSet());
+
+        Map<ArtifactKey, VersionChange> changed = to.values().stream()
+                .filter( k -> !added.contains(k) && !removed.contains(k))
+                .map( k -> new ArtifactKey(k))
+                .filter( k -> !Objects.equals(to.get(k).getVersion(), from.get(k).getVersion()))
+                .collect(Collectors.toMap( Function.identity(), k -> new VersionChange(from.get(k).getVersion(), to.get(k).getVersion())));
+
+        // 4. output changes
+        
+        System.out.println("Added ");
+        added.stream().sorted().forEach(LaunchpadComparer::outputFormatted);
+        
+        System.out.println("Removed ");
+        removed.stream().sorted().forEach(LaunchpadComparer::outputFormatted);
+        
+        System.out.println("Changed");
+        changed.entrySet().stream()
+            .sorted( (a, b) -> a.getKey().compareTo(b.getKey()) )
+            .forEach(LaunchpadComparer::outputFormatted);        
+        
+    }
+    
+    private static Artifact newArtifact(Bundle bundle) {
+        
+        return new Artifact(bundle.getGroupId(), bundle.getArtifactId(), bundle.getVersion(), bundle.getClassifier(), bundle.getType());
+    }
+    
+    private static void outputFormatted(Artifact a) {
+        
+        System.out.format("    %-30s : %-55s : %s%n", a.getGroupId(), a.getArtifactId(), a.getVersion());
+        
+    }
+
+    private static void outputFormatted(Map.Entry<ArtifactKey, VersionChange> e) {
+        
+        ArtifactKey artifact = e.getKey();
+        VersionChange versionChange = e.getValue();
+        
+        System.out.format("    %-30s : %-55s : %s -> %s%n", artifact.getGroupId(), artifact.getArtifactId(), versionChange.getFrom(), versionChange.getTo());
+        
+        if ( !artifact.getGroupId().equals("org.apache.sling"))  {
+            return;
+        }
+        
+        SvnChangeLogFinder svn = new SvnChangeLogFinder();
+        
+        String fromTag = artifact.getArtifactId()+"-"+versionChange.getFrom();
+        String toTag = artifact.getArtifactId()+"-"+ versionChange.getTo();
+        try {
+            List<String> issues = svn.getChanges(fromTag, toTag)
+                .stream()
+                .map(LaunchpadComparer::toJiraKey)
+                .filter( k -> k != null)
+                .collect(Collectors.toList());
+            
+            IssueFinder issueFinder = new IssueFinder();
+            issueFinder.findIssues(issues).
+                forEach( i -> System.out.format("        %-10s - %s%n", i.getKey(), i.getSummary()));
+            
+        } catch (SVNException | IOException e1) {
+            System.err.println("Failed retrieving changes : " + e1.getMessage());
+        }
+    }
+    
+    private static String toJiraKey(String message) {
+        Matcher matcher = JIRA_KEY_PATTERN.matcher(message);
+        if ( !matcher.matches() ) {
+            return null;
+        }
+        
+        return matcher.group(1);
+    }    
+}
diff --git a/src/main/java/org/apache/sling/tooling/lc/Main.java b/src/main/java/org/apache/sling/tooling/lc/Main.java
index 7b7837b..91da961 100644
--- a/src/main/java/org/apache/sling/tooling/lc/Main.java
+++ b/src/main/java/org/apache/sling/tooling/lc/Main.java
@@ -16,26 +16,6 @@
  */
 package org.apache.sling.tooling.lc;
 
-import java.io.BufferedReader;
-import java.io.File;
-import java.nio.file.Files;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
-
-import org.apache.sling.maven.projectsupport.BundleListUtils;
-import org.apache.sling.maven.projectsupport.bundlelist.v1_0_0.Bundle;
-import org.apache.sling.maven.projectsupport.bundlelist.v1_0_0.BundleList;
-import org.apache.sling.provisioning.model.Artifact;
-import org.apache.sling.provisioning.model.Model;
-import org.apache.sling.provisioning.model.ModelUtility;
-import org.apache.sling.provisioning.model.io.ModelReader;
-
-import com.google.common.collect.Sets;
-
 public class Main {
 
     public static void main(String[] args) throws Exception {
@@ -48,77 +28,9 @@
             secondVersion = args[1];
         }
         
-        System.out.format("Computing differences between Launchpad versions %s and %s...%n", 
-                firstVersion, secondVersion);
+        LaunchpadComparer comparer = new LaunchpadComparer(firstVersion, secondVersion);
+        comparer.run();
         
-        // 1. download artifacts
-        AetherSetup aether = new AetherSetup();
 
-        File fromFile = aether.download(Artifacts.launchpadCoordinates(firstVersion));
-        File toFile = aether.download(Artifacts.launchpadCoordinates(secondVersion));
-
-        // 2. parse artifact definitions
-        Model model;
-        try (BufferedReader reader = Files.newBufferedReader(toFile.toPath())) {
-            model = ModelUtility.getEffectiveModel(ModelReader.read(reader, null));
-        }
-        
-        // TODO - versions are not interpolated
-        Map<ArtifactKey, Artifact> to = model.getFeatures().stream()
-            .flatMap( f -> f.getRunModes().stream())
-            .flatMap( r -> r.getArtifactGroups().stream())
-            .flatMap( g -> StreamSupport.stream(g.spliterator(), false))
-            .collect(Collectors.toMap( a -> new ArtifactKey(a), Function.identity()));
-        
-        BundleList readBundleList = BundleListUtils.readBundleList(fromFile);
-        
-        // 3. generate added / removed / changed
-        Map<ArtifactKey, Artifact> from = readBundleList.getStartLevels().stream()
-            .flatMap( sl -> sl.getBundles().stream() )
-            .collect(Collectors.toMap( b -> new ArtifactKey(b), Main::newArtifact));
-        
-        Set<Artifact> removed = Sets.difference(from.keySet(), to.keySet()).stream()
-            .map( k -> from.get(k))
-            .collect(Collectors.toSet());
-
-        Set<Artifact> added = Sets.difference(to.keySet(), from.keySet()).stream()
-                .map( k -> to.get(k))
-                .collect(Collectors.toSet());
-
-        Map<ArtifactKey, VersionChange> changed = to.values().stream()
-                .filter( k -> !added.contains(k) && !removed.contains(k))
-                .map( k -> new ArtifactKey(k))
-                .filter( k -> !Objects.equals(to.get(k).getVersion(), from.get(k).getVersion()))
-                .collect(Collectors.toMap( Function.identity(), k -> new VersionChange(from.get(k).getVersion(), to.get(k).getVersion())));
-
-        // 4. output changes
-        
-        System.out.println("Added ");
-        added.stream().sorted().forEach(Main::outputFormatted);
-        
-        System.out.println("Removed ");
-        removed.stream().sorted().forEach(Main::outputFormatted);
-        
-        System.out.println("Changed");
-        changed.entrySet().stream()
-            .sorted( (a, b) -> a.getKey().compareTo(b.getKey()) )
-            .forEach(Main::outputFormatted);
-    }
-
-    private static Artifact newArtifact(Bundle bundle) {
-        
-        return new Artifact(bundle.getGroupId(), bundle.getArtifactId(), bundle.getVersion(), bundle.getClassifier(), bundle.getType());
-    }
-    
-    private static void outputFormatted(Artifact a) {
-        
-        System.out.format("    %-30s : %-55s : %s%n", a.getGroupId(), a.getArtifactId(), a.getVersion());
-        
-    }
-
-    private static void outputFormatted(Map.Entry<ArtifactKey, VersionChange> e) {
-        
-        System.out.format("    %-30s : %-55s : %s -> %s%n", e.getKey().getGroupId(), e.getKey().getArtifactId(), e.getValue().getFrom(), e.getValue().getTo());
-        
     }
 }
diff --git a/src/main/java/org/apache/sling/tooling/lc/AetherSetup.java b/src/main/java/org/apache/sling/tooling/lc/aether/AetherSetup.java
similarity index 97%
rename from src/main/java/org/apache/sling/tooling/lc/AetherSetup.java
rename to src/main/java/org/apache/sling/tooling/lc/aether/AetherSetup.java
index d529f3e..8d4679b 100644
--- a/src/main/java/org/apache/sling/tooling/lc/AetherSetup.java
+++ b/src/main/java/org/apache/sling/tooling/lc/aether/AetherSetup.java
@@ -14,7 +14,7 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.sling.tooling.lc;
+package org.apache.sling.tooling.lc.aether;
 
 import java.io.File;
 import java.util.Arrays;
@@ -36,7 +36,7 @@
 import org.eclipse.aether.transport.file.FileTransporterFactory;
 import org.eclipse.aether.transport.http.HttpTransporterFactory;
 
-class AetherSetup {
+public class AetherSetup {
     
     private final List<RemoteRepository> repos = Arrays.asList(
         new RemoteRepository.Builder("central", "default", "http://central.maven.org/maven2/").build(),
diff --git a/src/main/java/org/apache/sling/tooling/lc/ArtifactKey.java b/src/main/java/org/apache/sling/tooling/lc/aether/ArtifactKey.java
similarity index 96%
rename from src/main/java/org/apache/sling/tooling/lc/ArtifactKey.java
rename to src/main/java/org/apache/sling/tooling/lc/aether/ArtifactKey.java
index 047a1d2..112e10a 100644
--- a/src/main/java/org/apache/sling/tooling/lc/ArtifactKey.java
+++ b/src/main/java/org/apache/sling/tooling/lc/aether/ArtifactKey.java
@@ -14,14 +14,14 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.sling.tooling.lc;
+package org.apache.sling.tooling.lc.aether;
 
 import java.util.Objects;
 
 import org.apache.sling.maven.projectsupport.bundlelist.v1_0_0.Bundle;
 import org.apache.sling.provisioning.model.Artifact;
 
-class ArtifactKey implements Comparable<ArtifactKey> {
+public class ArtifactKey implements Comparable<ArtifactKey> {
     
     private String groupId;
     private String artifactId;
diff --git a/src/main/java/org/apache/sling/tooling/lc/Artifacts.java b/src/main/java/org/apache/sling/tooling/lc/aether/Artifacts.java
similarity index 95%
rename from src/main/java/org/apache/sling/tooling/lc/Artifacts.java
rename to src/main/java/org/apache/sling/tooling/lc/aether/Artifacts.java
index 0ffc42a..8bbed53 100644
--- a/src/main/java/org/apache/sling/tooling/lc/Artifacts.java
+++ b/src/main/java/org/apache/sling/tooling/lc/aether/Artifacts.java
@@ -14,12 +14,12 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.sling.tooling.lc;
+package org.apache.sling.tooling.lc.aether;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-class Artifacts {
+public class Artifacts {
 
     private static final Pattern VERSION_NUMBER = Pattern.compile("^(\\d+)(-SNAPSHOT)?");
     
diff --git a/src/main/java/org/apache/sling/tooling/lc/VersionChange.java b/src/main/java/org/apache/sling/tooling/lc/aether/VersionChange.java
similarity index 93%
rename from src/main/java/org/apache/sling/tooling/lc/VersionChange.java
rename to src/main/java/org/apache/sling/tooling/lc/aether/VersionChange.java
index 7c69d0b..671ff2e 100644
--- a/src/main/java/org/apache/sling/tooling/lc/VersionChange.java
+++ b/src/main/java/org/apache/sling/tooling/lc/aether/VersionChange.java
@@ -14,9 +14,9 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.sling.tooling.lc;
+package org.apache.sling.tooling.lc.aether;
 
-class VersionChange {
+public class VersionChange {
     
     private String from;
     private String to;
diff --git a/src/main/java/org/apache/sling/tooling/lc/VersionChange.java b/src/main/java/org/apache/sling/tooling/lc/jira/Fields.java
similarity index 65%
copy from src/main/java/org/apache/sling/tooling/lc/VersionChange.java
copy to src/main/java/org/apache/sling/tooling/lc/jira/Fields.java
index 7c69d0b..2215d06 100644
--- a/src/main/java/org/apache/sling/tooling/lc/VersionChange.java
+++ b/src/main/java/org/apache/sling/tooling/lc/jira/Fields.java
@@ -14,28 +14,18 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.sling.tooling.lc;
+package org.apache.sling.tooling.lc.jira;
 
-class VersionChange {
+public class Fields {
     
-    private String from;
-    private String to;
+    private final String summary;
+    
+    public Fields(String summary) {
+        this.summary = summary;
+    }
 
-    public VersionChange(String from, String to) {
-        this.from = from;
-        this.to = to;
+    public String getSummary() {
+        return summary;
     }
     
-    public String getFrom() {
-        return from;
-    }
-    
-    public String getTo() {
-        return to;
-    }
-    
-    @Override
-    public String toString() {
-        return "VersionChange [" +from + " -> " + to +"]";
-    }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/apache/sling/tooling/lc/jira/Issue.java b/src/main/java/org/apache/sling/tooling/lc/jira/Issue.java
new file mode 100644
index 0000000..12e54b1
--- /dev/null
+++ b/src/main/java/org/apache/sling/tooling/lc/jira/Issue.java
@@ -0,0 +1,79 @@
+/*
+ * 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.apache.sling.tooling.lc.jira;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.inject.internal.util.Objects;
+
+public class Issue implements Comparable<Issue> {
+    
+    private static final Pattern KEY_PATTERN = Pattern.compile("^([A-Z]+)-(\\d+)$"); 
+
+    private final String key;
+    private final Fields fields;
+
+    public Issue(String key, Fields fields) {
+        this.key = key;
+        this.fields = fields;
+    }
+
+    public String getKey() {
+        return key;
+    }
+    
+    public Fields getFields() {
+        return fields;
+    }
+    
+    public String getSummary() {
+        return fields.getSummary();
+    }
+    
+    @Override
+    public String toString() {
+        return key + " - " + getSummary();
+    }
+
+    @Override
+    public int compareTo(Issue o) {
+        
+        Matcher ourMatcher = KEY_PATTERN.matcher(key);
+        Matcher theirMatcher = KEY_PATTERN.matcher(o.key);
+        
+        if ( !ourMatcher.matches()) {
+            throw new IllegalArgumentException("No match found for " + key);
+        }
+
+        if ( !theirMatcher.matches()) {
+            throw new IllegalArgumentException("No match found for " + o.key);
+        }
+        
+        String ourProject = ourMatcher.group(1);
+        String theirProject = theirMatcher.group(1);
+        
+        if ( !Objects.equal(ourProject, theirProject)) {
+            return ourProject.compareTo(theirProject);
+        }
+        
+        int ourId = Integer.parseInt(ourMatcher.group(2));
+        int theirId = Integer.parseInt(theirMatcher.group(2));
+
+        return Integer.valueOf(ourId).compareTo(theirId);
+    }
+}
diff --git a/src/main/java/org/apache/sling/tooling/lc/jira/IssueFinder.java b/src/main/java/org/apache/sling/tooling/lc/jira/IssueFinder.java
new file mode 100644
index 0000000..69005b9
--- /dev/null
+++ b/src/main/java/org/apache/sling/tooling/lc/jira/IssueFinder.java
@@ -0,0 +1,79 @@
+/*
+ * 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.apache.sling.tooling.lc.jira;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.HttpClientUtils;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.impl.client.DefaultHttpClient;
+
+import com.google.gson.Gson;
+
+public class IssueFinder {
+    
+    public static void main(String[] args) throws IOException {
+        
+        new IssueFinder().findIssues(Arrays.asList("SLING-1", "SLING-2"))
+            .stream()
+            .forEach(System.out::println);
+    }
+    
+    public List<Issue> findIssues(List<String> issueKeys) throws IOException{
+        
+        HttpClient client = new DefaultHttpClient();
+        
+        HttpGet get;
+        try {
+            URIBuilder builder = new URIBuilder("https://issues.apache.org/jira/rest/api/2/search")
+                    .addParameter("jql", "key in (" + String.join(",", issueKeys) + ")")
+                    .addParameter("fields", "key,summary");
+            
+            get = new HttpGet(builder.build());
+        } catch (URISyntaxException e) {
+            // never happens
+            throw new RuntimeException(e);
+        }
+        
+        HttpResponse response = client.execute(get);
+        try {
+            if ( response.getStatusLine().getStatusCode() != 200 ) { 
+                throw new IOException("Search call returned status " + response.getStatusLine().getStatusCode());
+            }
+            
+            try ( Reader reader = new InputStreamReader(response.getEntity().getContent(), "UTF-8") ) {
+                Response apiResponse = new Gson().fromJson(reader, Response.class);
+                List<Issue> issues = apiResponse.getIssues();
+                Collections.sort(issues);
+                return issues;
+                
+            }
+        } finally {
+            HttpClientUtils.closeQuietly(client);
+        }
+        
+    }
+}
diff --git a/src/main/java/org/apache/sling/tooling/lc/VersionChange.java b/src/main/java/org/apache/sling/tooling/lc/jira/Response.java
similarity index 64%
copy from src/main/java/org/apache/sling/tooling/lc/VersionChange.java
copy to src/main/java/org/apache/sling/tooling/lc/jira/Response.java
index 7c69d0b..e77407f 100644
--- a/src/main/java/org/apache/sling/tooling/lc/VersionChange.java
+++ b/src/main/java/org/apache/sling/tooling/lc/jira/Response.java
@@ -14,28 +14,19 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.sling.tooling.lc;
+package org.apache.sling.tooling.lc.jira;
 
-class VersionChange {
-    
-    private String from;
-    private String to;
+import java.util.List;
 
-    public VersionChange(String from, String to) {
-        this.from = from;
-        this.to = to;
+public class Response {
+
+    private final List<Issue> issues;
+
+    public Response(List<Issue> issues) {
+        this.issues = issues;
     }
-    
-    public String getFrom() {
-        return from;
+
+    public List<Issue> getIssues() {
+        return issues;
     }
-    
-    public String getTo() {
-        return to;
-    }
-    
-    @Override
-    public String toString() {
-        return "VersionChange [" +from + " -> " + to +"]";
-    }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/apache/sling/tooling/lc/svn/SvnChangeLogFinder.java b/src/main/java/org/apache/sling/tooling/lc/svn/SvnChangeLogFinder.java
new file mode 100644
index 0000000..dd1fc9c
--- /dev/null
+++ b/src/main/java/org/apache/sling/tooling/lc/svn/SvnChangeLogFinder.java
@@ -0,0 +1,62 @@
+/*
+ * 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.apache.sling.tooling.lc.svn;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.SVNURL;
+import org.tmatesoft.svn.core.io.SVNRepository;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.wc.SVNRevision;
+
+public class SvnChangeLogFinder {
+    
+    private static final String SLING_SVN_REPO_BASE = "https://svn.apache.org/repos/asf/sling";
+    
+    public static void main(String[] args) throws SVNException {
+        
+        new SvnChangeLogFinder().getChanges("org.apache.sling.adapter-2.1.2", "org.apache.sling.adapter-2.1.6")
+            .stream().forEach(System.out::println);
+    }
+    
+    public List<String> getChanges(String first, String second) throws SVNException {
+        
+        SVNURL svnUrl = SVNURL.parseURIEncoded(SLING_SVN_REPO_BASE);
+        
+        List<String> changes = new ArrayList<>();
+        
+        SVNClientManager manager  = SVNClientManager.newInstance();
+        
+        SVNRepository repo = manager.getRepositoryPool().createRepository(svnUrl, true);
+        
+        SVNRevision from = SVNRevision.create(getRevision(first, repo));
+        SVNRevision to = SVNRevision.create(getRevision(second, repo));
+        
+        repo.log(new String[] { "tags/" + second } ,from.getNumber(), to.getNumber(), false, false, (e) -> changes.add(e.getMessage()));
+        
+        return changes;
+    }
+
+
+    private long getRevision(String tagName, SVNRepository repo) throws SVNException {
+        
+        return repo.info("tags/" + tagName, -1).getRevision();
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/tooling/lc/ArtifactsTest.java b/src/test/java/org/apache/sling/tooling/lc/ArtifactsTest.java
index 9d41298..8b3b396 100644
--- a/src/test/java/org/apache/sling/tooling/lc/ArtifactsTest.java
+++ b/src/test/java/org/apache/sling/tooling/lc/ArtifactsTest.java
@@ -1,6 +1,22 @@
+/*
+ * 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.apache.sling.tooling.lc;
 
-import static org.apache.sling.tooling.lc.Artifacts.launchpadCoordinates;
+import static org.apache.sling.tooling.lc.aether.Artifacts.launchpadCoordinates;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.junit.Assert.assertThat;