You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@couchdb.apache.org by ja...@apache.org on 2015/02/18 11:17:01 UTC

[1/4] admin commit: updated refs/heads/master to db84dab

Repository: couchdb-admin
Updated Branches:
  refs/heads/master 59aab1212 -> db84dab07


Automate mailinglist message count collection

It seems that there is a lot of manual work needed to generate
the board reports. This is an attempt to start automating the
recurring tasks.

Happy reporting! :)


Project: http://git-wip-us.apache.org/repos/asf/couchdb-admin/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-admin/commit/e5a2458e
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-admin/tree/e5a2458e
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-admin/diff/e5a2458e

Branch: refs/heads/master
Commit: e5a2458e9619d46d495a4a4ff02f29a08691a50e
Parents: 59aab12
Author: Robert Kowalski <ro...@kowalski.gd>
Authored: Sat Nov 15 21:12:15 2014 +0100
Committer: Robert Kowalski <ro...@kowalski.gd>
Committed: Sat Nov 15 21:12:15 2014 +0100

----------------------------------------------------------------------
 .gitignore                   |   1 +
 board-report/README.md       |  10 ++++
 board-report/generate-report | 112 ++++++++++++++++++++++++++++++++++++++
 board-report/lib/index.js    |  60 ++++++++++++++++++++
 board-report/package.json    |  17 ++++++
 5 files changed, 200 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/e5a2458e/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/e5a2458e/board-report/README.md
----------------------------------------------------------------------
diff --git a/board-report/README.md b/board-report/README.md
new file mode 100644
index 0000000..821294a
--- /dev/null
+++ b/board-report/README.md
@@ -0,0 +1,10 @@
+# generate-report
+
+## Usage
+
+```shell
+generate-report <daterange> | --help
+```
+
+If you are reporting for February 2014, for instance,
+set the date range to: `201311-201402`.

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/e5a2458e/board-report/generate-report
----------------------------------------------------------------------
diff --git a/board-report/generate-report b/board-report/generate-report
new file mode 100755
index 0000000..230a859
--- /dev/null
+++ b/board-report/generate-report
@@ -0,0 +1,112 @@
+#!/usr/bin/env node
+// Licensed 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.
+
+const crawler = require('./lib/index.js');
+
+const arg = process.argv[2];
+
+if (arg === '-h' || arg === '--help') {
+  return printUsage();
+}
+
+if (!validArg(arg)) {
+  console.log('Error: format is YYYYMM-YYYYMM - see ' +
+    "'generate-report --help'");
+  return;
+}
+
+const template = '{{count}} {{messages}} since {{month}} ' +
+  '(DIFF change)';
+
+crawler(arg, function (err, data) {
+  if (err) {
+    logLine('Error:');
+    console.log(err);
+    return;
+  }
+
+  logLine('Your message counts:');
+  data.forEach(function (el) {
+    const month = getStartMonthAsWord(arg),
+          messageCount = +el[1].replace(',', ''),
+          message = messageCount > 1 ? 'messages' : 'message',
+          wikitext = template
+            .replace('{{count}}', el[1])
+            .replace('{{month}}', month)
+            .replace('{{messages}}', message);
+
+    console.log(el[0] + ':');
+    logLine(wikitext);
+  });
+  console.log('Happy reporting! :)');
+});
+
+function logLine (line) {
+  console.log(line + '\n');
+}
+
+function getStartMonthAsWord (date) {
+  const i = parseInt(date.substr(4, 2), 10);
+  return getMonthAsWord(i);
+}
+
+function getMonthAsWord (i) {
+  return [
+    'January',
+    'February',
+    'March',
+    'April',
+    'May',
+    'June',
+    'July',
+    'August',
+    'September',
+    'October',
+    'November',
+    'December'
+  ][i - 1];
+}
+
+function validArg (arg) {
+  if (!arg) {
+    return false;
+  }
+
+  const dates = arg.split('-');
+
+  if (dates.length !== 2) {
+    return false;
+  }
+
+  if (dates[0].length !== 6 || dates[1].length !== 6) {
+    return false;
+  }
+
+  if (Number.isNaN(+dates[0]) || Number.isNaN(+dates[1])) {
+    return false;
+  }
+
+  return true;
+}
+
+function printUsage () {
+  logLine('usage: generate-report <daterange> | --help');
+  console.log('If you are reporting for February 2014, for instance,');
+  logLine('set the date range to: 201311-201402');
+  console.log('guide:');
+  console.log('https://cwiki.apache.org/confluence/display' +
+    '/COUCHDB/Guide');
+  console.log('template:');
+  console.log('https://cwiki.apache.org/confluence/display' +
+    '/COUCHDB/Template');
+}

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/e5a2458e/board-report/lib/index.js
----------------------------------------------------------------------
diff --git a/board-report/lib/index.js b/board-report/lib/index.js
new file mode 100644
index 0000000..db2a324
--- /dev/null
+++ b/board-report/lib/index.js
@@ -0,0 +1,60 @@
+// Licensed 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.
+
+const async = require('async'),
+      request = require('request'),
+      cheerio = require('cheerio');
+
+const urlTemplate = 'http://markmail.org/search/?q=list%3A' +
+  'org.apache.{{listname}}%20date%3A{{date}}';
+
+const lists = [
+  'couchdb-announce',
+  'couchdb-user',
+  'couchdb-erlang',
+  'couchdb-dev',
+  'couchdb-commits',
+  'couchdb-l10n',
+  'couchdb-replication',
+  'couchdb-marketing'
+];
+
+function getMessageCounts (date, cb) {
+  const listUrls = lists.map(function (list) {
+    return urlTemplate
+      .replace('{{date}}', date)
+      .replace('{{listname}}', list);
+  });
+
+  async.map(listUrls, request, function (err, results) {
+    if (err) {
+      return cb(err);
+    }
+
+    const bodies = results.reduce(function (acc, cur) {
+      acc.push(cur.request.req.res.body);
+      return acc;
+    }, []);
+
+    const res = bodies.map(function (markup) {
+      const $ = cheerio.load(markup),
+            count = $('#lists .count').text(),
+            list = $('#lists a').text().replace('org.apache.couchdb.', '');
+
+      return [list, count];
+    });
+
+    return cb(null, res);
+  });
+}
+
+module.exports = getMessageCounts;

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/e5a2458e/board-report/package.json
----------------------------------------------------------------------
diff --git a/board-report/package.json b/board-report/package.json
new file mode 100644
index 0000000..6fb1742
--- /dev/null
+++ b/board-report/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "board-report",
+  "version": "1.0.0",
+  "private": true,
+  "description": "I'm helping to prepare board reports",
+  "main": "lib/index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "Robert Kowalski <ro...@kowalski.gd>",
+  "license": "Apache License, Version 2.0",
+  "dependencies": {
+    "async": "0.9.0",
+    "cheerio": "0.18.0",
+    "request": "2.48.0"
+  }
+}


[3/4] admin commit: updated refs/heads/master to db84dab

Posted by ja...@apache.org.
bin mode


Project: http://git-wip-us.apache.org/repos/asf/couchdb-admin/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-admin/commit/b456c034
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-admin/tree/b456c034
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-admin/diff/b456c034

Branch: refs/heads/master
Commit: b456c0343c6a032edc0d3b6a94dbd9047512cbeb
Parents: 0ec747a
Author: Robert Kowalski <ro...@kowalski.gd>
Authored: Mon Feb 16 18:16:23 2015 +0100
Committer: Robert Kowalski <ro...@kowalski.gd>
Committed: Mon Feb 16 18:18:20 2015 +0100

----------------------------------------------------------------------
 board-report/README.md    | 1 +
 board-report/package.json | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/b456c034/board-report/README.md
----------------------------------------------------------------------
diff --git a/board-report/README.md b/board-report/README.md
index 821294a..9709935 100644
--- a/board-report/README.md
+++ b/board-report/README.md
@@ -3,6 +3,7 @@
 ## Usage
 
 ```shell
+npm i -g
 generate-report <daterange> | --help
 ```
 

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/b456c034/board-report/package.json
----------------------------------------------------------------------
diff --git a/board-report/package.json b/board-report/package.json
index 2a1abbd..41c680d 100644
--- a/board-report/package.json
+++ b/board-report/package.json
@@ -1,9 +1,10 @@
 {
-  "name": "board-report",
+  "name": "generate-report",
   "version": "1.0.0",
   "private": true,
   "description": "I'm helping to prepare board reports",
   "main": "lib/index.js",
+  "bin": "./generate-report",
   "scripts": {
     "test": "mocha -R spec"
   },


[4/4] admin commit: updated refs/heads/master to db84dab

Posted by ja...@apache.org.
fix bugs


Project: http://git-wip-us.apache.org/repos/asf/couchdb-admin/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-admin/commit/db84dab0
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-admin/tree/db84dab0
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-admin/diff/db84dab0

Branch: refs/heads/master
Commit: db84dab079108366dcdbe70cbe12e1c1cc97abb7
Parents: b456c03
Author: Robert Kowalski <ro...@kowalski.gd>
Authored: Mon Feb 16 19:12:45 2015 +0100
Committer: Robert Kowalski <ro...@kowalski.gd>
Committed: Mon Feb 16 19:14:21 2015 +0100

----------------------------------------------------------------------
 board-report/generate-report  |  4 ++--
 board-report/lib/argument.js  |  5 +++--
 board-report/lib/index.js     | 27 +++++++++++++++------------
 board-report/test/argument.js |  8 ++++----
 4 files changed, 24 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/db84dab0/board-report/generate-report
----------------------------------------------------------------------
diff --git a/board-report/generate-report b/board-report/generate-report
index 2ec0335..79c9fa8 100755
--- a/board-report/generate-report
+++ b/board-report/generate-report
@@ -15,7 +15,7 @@ const crawler = require('./lib/index.js'),
       argument = require('./lib/argument.js');
 
 const arg = process.argv[2];
-const template = '{{count}} {{messages}} since {{month}} ' +
+const template = '{{count}} {{messages}} since end of {{month}} ' +
   '({{diff}} change)';
 
 if (arg === '-h' || arg === '--help') {
@@ -41,7 +41,7 @@ crawler(queryParams, monthsForDiff, function (err, data) {
 
   logLine('Your message counts:');
   Object.keys(data).forEach(function (el) {
-    const month = argument.getMonthAsWordFromNow(arg, monthsForDiff),
+    const month = argument.getMonthAsWordFromNow(arg, monthsForDiff + 1),
           messageCount = data[el].curr,
           message = messageCount > 1 ? 'messages' : 'message',
           wikitext = template

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/db84dab0/board-report/lib/argument.js
----------------------------------------------------------------------
diff --git a/board-report/lib/argument.js b/board-report/lib/argument.js
index 5fda129..a75e460 100644
--- a/board-report/lib/argument.js
+++ b/board-report/lib/argument.js
@@ -14,6 +14,7 @@ const moment = require('moment');
 
 exports.prepareQueryParams = prepareQueryParams;
 function prepareQueryParams (arg) {
+  console.log(arg)
   const startEnd = getStartEndDates(arg, 3);
         startAsString = startEnd[0].format('YYYYMM'),
         diffStartEnd = getStartEndDates(startAsString, 3);
@@ -50,6 +51,7 @@ function getMonthAsWordFromNow (reportEnd, time) {
   return moment(inter, 'YYYYMM').format('MMMM');
 }
 
+exports.getMonthAsWord = getMonthAsWord;
 function getMonthAsWord (reportEnd) {
   return moment(reportEnd, 'YYYYMM').format('MMMM');
 }
@@ -60,8 +62,7 @@ function formatQuery (startEnd) {
 }
 
 function getStartEndDates (arg, time) {
-  const end = moment(arg, 'YYYYMM'),
+  const end = moment(arg, 'YYYYMM').subtract(1, 'month'),
         start = moment(arg, 'YYYYMM').subtract(time, 'months');
-
   return [start, end];
 }

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/db84dab0/board-report/lib/index.js
----------------------------------------------------------------------
diff --git a/board-report/lib/index.js b/board-report/lib/index.js
index 63ec26c..d1e783c 100644
--- a/board-report/lib/index.js
+++ b/board-report/lib/index.js
@@ -16,8 +16,9 @@ const async = require('async'),
       assert = require('assert'),
       argument = require('./argument.js');
 
-const urlTemplate = 'http://markmail.org/search/?q=list%3A' +
-  'org.apache.{{listname}}%20date%3A{{date}}';
+const urlPrefix = 'http://markmail.org/search/?q=list%3Aorg.apache.';
+const urlTemplate = urlPrefix + '{{listname}}%20date%3A{{date}}';
+
 
 const lists = [
   'couchdb-announce',
@@ -40,9 +41,6 @@ function api (queryParams, timeframe, cb) {
   const listUrlsCurr = getUrls(queryParams.queryCurr),
         listUrlsDiff = getUrls(queryParams.queryDiff);
 
-  console.log(listUrlsCurr);
-  console.log(listUrlsDiff);
-
   async.parallel({
     current: function (cb) {
       getMessageCounts(listUrlsCurr, cb);
@@ -86,7 +84,7 @@ function pick (element, structure) {
 }
 
 function getDiffString (count, countOld) {
-  const result = normalize(countOld) - normalize(count);
+  const result = normalize(count) - normalize(countOld);
 
   if (result >= 0) {
     return '+' + result;
@@ -95,6 +93,9 @@ function getDiffString (count, countOld) {
 }
 
 function normalize (string) {
+  if (!string) {
+    string = '0';
+  }
   return parseInt(string.replace(',', ''), 10);
 }
 
@@ -113,21 +114,23 @@ function requestWithOptions (url, cb) {
       maxSockets: Infinity
     }
   }, function (err, res, body) {
-    cb(err, body);
+    cb(err, [url, body]);
   })
 }
 
+function normalizeListName (list) {
+  return list.replace(urlPrefix, '').split('%20date')[0];
+}
+
 function getMessageCounts (urlList, cb) {
   async.map(urlList, requestWithOptions, function (err, results) {
     if (err) {
       return cb(err);
     }
-
-    const res = results.map(function (markup) {
-      const $ = cheerio.load(markup),
+    const res = results.map(function (element) {
+      const $ = cheerio.load(element[1]),
             count = $('#lists .count').text(),
-            list = $('#lists a').text().replace('org.apache.couchdb.', '');
-
+            list = normalizeListName(element[0]);
       return [list, count];
     });
 

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/db84dab0/board-report/test/argument.js
----------------------------------------------------------------------
diff --git a/board-report/test/argument.js b/board-report/test/argument.js
index 0074425..b3dad7e 100644
--- a/board-report/test/argument.js
+++ b/board-report/test/argument.js
@@ -29,14 +29,14 @@ describe('arguments', function () {
   it('prepares query parameters', function () {
     const queryParams = argument.prepareQueryParams('201411');
 
-    assert.equal(queryParams.queryCurr, '201408-201411');
-    assert.equal(queryParams.queryDiff, '201405-201408');
+    assert.equal(queryParams.queryCurr, '201408-201410');
+    assert.equal(queryParams.queryDiff, '201405-201407');
   });
 
   it('prepares urls for the current and the diff', function () {
     const queryParams = argument.prepareQueryParams('201411');
 
-    assert.equal(queryParams.queryCurr, '201408-201411');
-    assert.equal(queryParams.queryDiff, '201405-201408');
+    assert.equal(queryParams.queryCurr, '201408-201410');
+    assert.equal(queryParams.queryDiff, '201405-201407');
   });
 });


[2/4] admin commit: updated refs/heads/master to db84dab

Posted by ja...@apache.org.
wip


Project: http://git-wip-us.apache.org/repos/asf/couchdb-admin/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-admin/commit/0ec747a1
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-admin/tree/0ec747a1
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-admin/diff/0ec747a1

Branch: refs/heads/master
Commit: 0ec747a14ef0b4b23992379819b20ebd92bb6c3e
Parents: e5a2458
Author: Robert Kowalski <ro...@kowalski.gd>
Authored: Fri Nov 21 02:45:32 2014 +0100
Committer: Robert Kowalski <ro...@kowalski.gd>
Committed: Fri Nov 21 02:48:50 2014 +0100

----------------------------------------------------------------------
 .gitignore                    |   1 +
 board-report/generate-report  |  81 ++++++++----------------------
 board-report/lib/argument.js  |  67 +++++++++++++++++++++++++
 board-report/lib/index.js     | 100 +++++++++++++++++++++++++++++++++----
 board-report/package.json     |   6 ++-
 board-report/test/argument.js |  42 ++++++++++++++++
 6 files changed, 225 insertions(+), 72 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/0ec747a1/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 3c3629e..93f1361 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 node_modules
+npm-debug.log

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/0ec747a1/board-report/generate-report
----------------------------------------------------------------------
diff --git a/board-report/generate-report b/board-report/generate-report
index 230a859..2ec0335 100755
--- a/board-report/generate-report
+++ b/board-report/generate-report
@@ -11,41 +11,46 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-const crawler = require('./lib/index.js');
+const crawler = require('./lib/index.js'),
+      argument = require('./lib/argument.js');
 
 const arg = process.argv[2];
+const template = '{{count}} {{messages}} since {{month}} ' +
+  '({{diff}} change)';
 
 if (arg === '-h' || arg === '--help') {
   return printUsage();
 }
 
-if (!validArg(arg)) {
-  console.log('Error: format is YYYYMM-YYYYMM - see ' +
+if (!argument.validArg(arg)) {
+  console.error('Error: format is YYYYMM - see ' +
     "'generate-report --help'");
+  process.exit(1);
   return;
 }
 
-const template = '{{count}} {{messages}} since {{month}} ' +
-  '(DIFF change)';
-
-crawler(arg, function (err, data) {
+var monthsForDiff = 3
+const queryParams = argument.prepareQueryParams(arg);
+crawler(queryParams, monthsForDiff, function (err, data) {
   if (err) {
     logLine('Error:');
-    console.log(err);
+    console.error(err);
+    process.exit(1);
     return;
   }
 
   logLine('Your message counts:');
-  data.forEach(function (el) {
-    const month = getStartMonthAsWord(arg),
-          messageCount = +el[1].replace(',', ''),
+  Object.keys(data).forEach(function (el) {
+    const month = argument.getMonthAsWordFromNow(arg, monthsForDiff),
+          messageCount = data[el].curr,
           message = messageCount > 1 ? 'messages' : 'message',
           wikitext = template
-            .replace('{{count}}', el[1])
+            .replace('{{count}}', messageCount)
             .replace('{{month}}', month)
-            .replace('{{messages}}', message);
+            .replace('{{messages}}', message)
+            .replace('{{diff}}', data[el].diff);
 
-    console.log(el[0] + ':');
+    console.log(el + ':');
     logLine(wikitext);
   });
   console.log('Happy reporting! :)');
@@ -55,54 +60,10 @@ function logLine (line) {
   console.log(line + '\n');
 }
 
-function getStartMonthAsWord (date) {
-  const i = parseInt(date.substr(4, 2), 10);
-  return getMonthAsWord(i);
-}
-
-function getMonthAsWord (i) {
-  return [
-    'January',
-    'February',
-    'March',
-    'April',
-    'May',
-    'June',
-    'July',
-    'August',
-    'September',
-    'October',
-    'November',
-    'December'
-  ][i - 1];
-}
-
-function validArg (arg) {
-  if (!arg) {
-    return false;
-  }
-
-  const dates = arg.split('-');
-
-  if (dates.length !== 2) {
-    return false;
-  }
-
-  if (dates[0].length !== 6 || dates[1].length !== 6) {
-    return false;
-  }
-
-  if (Number.isNaN(+dates[0]) || Number.isNaN(+dates[1])) {
-    return false;
-  }
-
-  return true;
-}
-
 function printUsage () {
-  logLine('usage: generate-report <daterange> | --help');
+  logLine('usage: generate-report <date> | --help');
   console.log('If you are reporting for February 2014, for instance,');
-  logLine('set the date range to: 201311-201402');
+  logLine('set the date to: 201402');
   console.log('guide:');
   console.log('https://cwiki.apache.org/confluence/display' +
     '/COUCHDB/Guide');

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/0ec747a1/board-report/lib/argument.js
----------------------------------------------------------------------
diff --git a/board-report/lib/argument.js b/board-report/lib/argument.js
new file mode 100644
index 0000000..5fda129
--- /dev/null
+++ b/board-report/lib/argument.js
@@ -0,0 +1,67 @@
+// Licensed 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.
+
+const moment = require('moment');
+
+exports.prepareQueryParams = prepareQueryParams;
+function prepareQueryParams (arg) {
+  const startEnd = getStartEndDates(arg, 3);
+        startAsString = startEnd[0].format('YYYYMM'),
+        diffStartEnd = getStartEndDates(startAsString, 3);
+
+  return {
+    queryCurr: formatQuery(startEnd),
+    queryDiff: formatQuery(diffStartEnd)
+  };
+}
+
+exports.validArg = validArg;
+function validArg (arg) {
+  if (!arg) {
+    return false;
+  }
+
+  if (arg.length !== 6) {
+    return false;
+  }
+
+  if (Number.isNaN(+arg)) {
+    return false;
+  }
+
+  return moment(arg, 'YYYYMM').isValid();
+}
+
+exports.getMonthAsWordFromNow = getMonthAsWordFromNow;
+function getMonthAsWordFromNow (reportEnd, time) {
+  const inter = moment(reportEnd, 'YYYYMM')
+    .subtract(time, 'months')
+    .format('YYYYMM');
+
+  return moment(inter, 'YYYYMM').format('MMMM');
+}
+
+function getMonthAsWord (reportEnd) {
+  return moment(reportEnd, 'YYYYMM').format('MMMM');
+}
+
+function formatQuery (startEnd) {
+  return startEnd[0].format('YYYYMM') + '-' +
+    startEnd[1].format('YYYYMM');
+}
+
+function getStartEndDates (arg, time) {
+  const end = moment(arg, 'YYYYMM'),
+        start = moment(arg, 'YYYYMM').subtract(time, 'months');
+
+  return [start, end];
+}

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/0ec747a1/board-report/lib/index.js
----------------------------------------------------------------------
diff --git a/board-report/lib/index.js b/board-report/lib/index.js
index db2a324..63ec26c 100644
--- a/board-report/lib/index.js
+++ b/board-report/lib/index.js
@@ -12,7 +12,9 @@
 
 const async = require('async'),
       request = require('request'),
-      cheerio = require('cheerio');
+      cheerio = require('cheerio'),
+      assert = require('assert'),
+      argument = require('./argument.js');
 
 const urlTemplate = 'http://markmail.org/search/?q=list%3A' +
   'org.apache.{{listname}}%20date%3A{{date}}';
@@ -28,24 +30,100 @@ const lists = [
   'couchdb-marketing'
 ];
 
-function getMessageCounts (date, cb) {
-  const listUrls = lists.map(function (list) {
+function api (queryParams, timeframe, cb) {
+  assert.ok(queryParams.queryCurr, 'queryParams must be defined');
+  assert.ok(queryParams.queryDiff, 'queryParams must be defined');
+  assert.equal(typeof timeframe, 'number',
+    'timeframe must be a number');
+  assert.equal(typeof cb, 'function', 'callback must a a function');
+
+  const listUrlsCurr = getUrls(queryParams.queryCurr),
+        listUrlsDiff = getUrls(queryParams.queryDiff);
+
+  console.log(listUrlsCurr);
+  console.log(listUrlsDiff);
+
+  async.parallel({
+    current: function (cb) {
+      getMessageCounts(listUrlsCurr, cb);
+    },
+    diff: function (cb) {
+      getMessageCounts(listUrlsDiff, cb);
+    }
+  },
+  function (err, res) {
+    const data = joinDiffWithCurrent(res);
+    cb(null, data);
+  });
+}
+
+function joinDiffWithCurrent (structure) {
+  const curr = structure.current,
+        diff = structure.diff;
+
+  return curr.reduce(function (acc, el) {
+    const name = el[0],
+          count = el[1],
+          countOld = pick(name, diff);
+
+    acc[name] = {
+      curr: normalize(count),
+      old: normalize(countOld),
+      diff: getDiffString(count, countOld)
+    };
+
+    return acc;
+  }, {});
+}
+
+function pick (element, structure) {
+  return structure.reduce(function (acc, row) {
+    if (row[0] === element) {
+      acc = acc + row[1];
+    }
+    return acc;
+  }, 0);
+}
+
+function getDiffString (count, countOld) {
+  const result = normalize(countOld) - normalize(count);
+
+  if (result >= 0) {
+    return '+' + result;
+  }
+  return '' + result;
+}
+
+function normalize (string) {
+  return parseInt(string.replace(',', ''), 10);
+}
+
+function getUrls (date) {
+  return lists.map(function (list) {
     return urlTemplate
       .replace('{{date}}', date)
       .replace('{{listname}}', list);
   });
+}
+
+function requestWithOptions (url, cb) {
+  request({
+    uri: url,
+    pool: {
+      maxSockets: Infinity
+    }
+  }, function (err, res, body) {
+    cb(err, body);
+  })
+}
 
-  async.map(listUrls, request, function (err, results) {
+function getMessageCounts (urlList, cb) {
+  async.map(urlList, requestWithOptions, function (err, results) {
     if (err) {
       return cb(err);
     }
 
-    const bodies = results.reduce(function (acc, cur) {
-      acc.push(cur.request.req.res.body);
-      return acc;
-    }, []);
-
-    const res = bodies.map(function (markup) {
+    const res = results.map(function (markup) {
       const $ = cheerio.load(markup),
             count = $('#lists .count').text(),
             list = $('#lists a').text().replace('org.apache.couchdb.', '');
@@ -57,4 +135,4 @@ function getMessageCounts (date, cb) {
   });
 }
 
-module.exports = getMessageCounts;
+module.exports = api;

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/0ec747a1/board-report/package.json
----------------------------------------------------------------------
diff --git a/board-report/package.json b/board-report/package.json
index 6fb1742..2a1abbd 100644
--- a/board-report/package.json
+++ b/board-report/package.json
@@ -5,13 +5,17 @@
   "description": "I'm helping to prepare board reports",
   "main": "lib/index.js",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "mocha -R spec"
   },
   "author": "Robert Kowalski <ro...@kowalski.gd>",
   "license": "Apache License, Version 2.0",
   "dependencies": {
     "async": "0.9.0",
     "cheerio": "0.18.0",
+    "moment": "2.8.3",
     "request": "2.48.0"
+  },
+  "devDependencies": {
+    "mocha": "~2.0.1"
   }
 }

http://git-wip-us.apache.org/repos/asf/couchdb-admin/blob/0ec747a1/board-report/test/argument.js
----------------------------------------------------------------------
diff --git a/board-report/test/argument.js b/board-report/test/argument.js
new file mode 100644
index 0000000..0074425
--- /dev/null
+++ b/board-report/test/argument.js
@@ -0,0 +1,42 @@
+// Licensed 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.
+
+const assert = require('assert'),
+      argument = require('../lib/argument.js');
+
+describe('arguments', function () {
+
+  it('validates arguments', function () {
+    assert.ok(argument.validArg('201401'));
+    assert.equal(argument.validArg('2ente'), false);
+    assert.equal(argument.validArg('2014123'), false);
+  });
+
+   it('has a method for getting names of months', function () {
+    assert.equal(argument.getMonthAsWord('201401'), 'January');
+    assert.equal(argument.getMonthAsWord('201412'), 'December');
+  });
+
+  it('prepares query parameters', function () {
+    const queryParams = argument.prepareQueryParams('201411');
+
+    assert.equal(queryParams.queryCurr, '201408-201411');
+    assert.equal(queryParams.queryDiff, '201405-201408');
+  });
+
+  it('prepares urls for the current and the diff', function () {
+    const queryParams = argument.prepareQueryParams('201411');
+
+    assert.equal(queryParams.queryCurr, '201408-201411');
+    assert.equal(queryParams.queryDiff, '201405-201408');
+  });
+});