From dcec25c2916cd75e26fe1d5f4a91bd0107afbef6 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Fri, 21 Sep 2018 09:46:51 +0200
Subject: [PATCH] feat: customisable PR tables (#2544)

Adds ability to both redefine column definitions in PRs as well as add or remove columns.
---
 lib/config/definitions.js                     |  20 +++
 lib/workers/pr/pr-body.js                     | 124 ++++++++++--------
 .../pr/__snapshots__/index.spec.js.snap       |  20 +--
 test/workers/pr/index.spec.js                 |   2 +
 .../__snapshots__/flatten.spec.js.snap        | 120 +++++++++++++++++
 website/docs/configuration-options.md         |  52 ++++++++
 6 files changed, 270 insertions(+), 68 deletions(-)

diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index eb360166c4..886a527d6a 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -1160,6 +1160,26 @@ const options = [
     cli: true,
     mergeable: true,
   },
+  {
+    name: 'prBodyDefinitions',
+    description: 'Table column definitions for use in PR tables',
+    type: 'object',
+    mergeable: true,
+    default: {
+      Package: '{{{depName}}}',
+      Type: '{{{depType}}}',
+      Update: '{{{updateType}}}',
+      'New value': '{{{newValue}}}',
+      References: '{{{references}}}',
+      'Package file': '{{{packageFile}}}',
+    },
+  },
+  {
+    name: 'prBodyColumns',
+    description: 'List of columns to use in PR bodies',
+    type: 'list',
+    default: ['Package', 'Type', 'Update', 'New value', 'References'],
+  },
 ];
 
 function getOptions() {
diff --git a/lib/workers/pr/pr-body.js b/lib/workers/pr/pr-body.js
index a54f982321..7d1fd16fcb 100644
--- a/lib/workers/pr/pr-body.js
+++ b/lib/workers/pr/pr-body.js
@@ -2,57 +2,38 @@ const handlebars = require('handlebars');
 const releaseNotesHbs = require('./changelog/hbs-template');
 
 module.exports = {
-  getUpdateHeaders,
   getPrBody,
 };
 
-function getUpdateHeaders(config) {
-  const updateHeaders = ['Package'];
-  if (config.upgrades.some(upgrade => upgrade.depType)) {
-    updateHeaders.push('Type');
+function getTableDefinition(config) {
+  const res = [];
+  for (const header of config.prBodyColumns) {
+    const value = config.prBodyDefinitions[header];
+    res.push({ header, value });
   }
-  updateHeaders.push('Update');
-  updateHeaders.push('New value');
-  if (
-    config.upgrades.some(
-      upgrade =>
-        upgrade.homepage || upgrade.repositoryUrl || upgrade.changelogUrl
-    )
-  ) {
-    updateHeaders.push('References');
+  return res;
+}
+
+function getNonEmptyColumns(definitions, rows) {
+  const res = [];
+  for (const column of definitions) {
+    const { header } = column;
+    for (const row of rows) {
+      if (row[header] && row[header].length) {
+        if (!res.includes(header)) {
+          res.push(header);
+        }
+      }
+    }
   }
-  return updateHeaders;
+  return res;
 }
 
 async function getPrBody(config) {
-  let prBody = '';
-  // istanbul ignore if
-  if (config.prBanner && !config.isGroup) {
-    prBody += handlebars.compile(config.prBanner)(config) + '\n\n';
-  }
-  prBody += '\n\nThis PR contains the following updates:\n\n';
-  const updateHeaders = getUpdateHeaders(config);
-  prBody += '| ' + updateHeaders.join(' | ') + ' |\n';
-  prBody += '|' + updateHeaders.map(() => '--|').join('') + '\n';
-  const seen = [];
-  for (const upgrade of config.upgrades) {
-    const {
-      depName,
-      depType,
-      updateType,
-      newValue,
-      newDigestShort,
-      homepage,
-      repositoryUrl,
-      changelogUrl,
-    } = upgrade;
-    const key = depName + depType + updateType + newValue;
-    if (seen.includes(key)) {
-      // don't have duplicate rows
-      continue; // eslint-disable-line no-continue
-    }
-    seen.push(key);
-    let references = [];
+  config.upgrades.forEach(upgrade => {
+    /* eslint-disable no-param-reassign */
+    const { homepage, repositoryUrl, changelogUrl } = upgrade;
+    const references = [];
     if (homepage) {
       references.push(`[homepage](${homepage})`);
     }
@@ -62,31 +43,58 @@ async function getPrBody(config) {
     if (changelogUrl) {
       references.push(`[changelog](${changelogUrl})`);
     }
-    references = references.join(', ');
-    let value = '';
+    upgrade.references = references.join(', ');
+    const { newValue, newDigestShort, updateType } = upgrade;
     if (newDigestShort) {
       if (updateType === 'pin') {
-        value = config.newDigestShort;
+        upgrade.newValue = config.newDigestShort;
       }
       if (newValue) {
-        value = newValue + '@' + newDigestShort;
+        upgrade.newVaue = newValue + '@' + newDigestShort;
       } else {
-        value = newDigestShort;
+        upgrade.newValue = newDigestShort;
       }
     } else if (updateType !== 'lockFileMaintenance') {
-      value = newValue;
+      upgrade.newValue = newValue;
+    }
+    /* eslint-enable no-param-reassign */
+  });
+  const tableDefinitions = getTableDefinition(config);
+  const tableValues = config.upgrades.map(upgrade => {
+    const res = {};
+    for (const column of tableDefinitions) {
+      const { header, value } = column;
+      try {
+        res[header] = handlebars
+          .compile(value)(upgrade)
+          .replace(/^``$/, '');
+      } catch (err) /* istanbul ignore next */ {
+        logger.info({ header, value, err }, 'Handlebars compilation error');
+      }
     }
-    const name =
-      upgrade.updateType === 'lockFileMaintenance'
-        ? 'all'
-        : '`' + depName + '`';
-    // prettier-ignore
-    prBody += `| ${name} | ${updateHeaders.includes('Type') ? depType + ' |' : ''} ${updateType} | ${value} |`;
-    if (updateHeaders.includes('References')) {
-      prBody += references + ' |';
+    return res;
+  });
+  const tableColumns = getNonEmptyColumns(tableDefinitions, tableValues);
+  logger.info({ tableDefinitions, tableValues, tableColumns });
+  let prBody = '';
+  // istanbul ignore if
+  if (config.prBanner && !config.isGroup) {
+    prBody += handlebars.compile(config.prBanner)(config) + '\n\n';
+  }
+  prBody += '\n\nThis PR contains the following updates:\n\n';
+  prBody += '| ' + tableColumns.join(' | ') + ' |\n';
+  prBody += '|' + tableColumns.map(() => '--|').join('') + '\n';
+  const rows = [];
+  for (const row of tableValues) {
+    let val = '|';
+    for (const column of tableColumns) {
+      val += ` ${row[column]} |`;
     }
-    prBody += '\n';
+    val += '\n';
+    rows.push(val);
   }
+  const uniqueRows = [...new Set(rows)];
+  prBody += uniqueRows.join('');
   prBody += '\n\n';
 
   if (config.upgrades.some(upgrade => upgrade.gitRef)) {
diff --git a/test/workers/pr/__snapshots__/index.spec.js.snap b/test/workers/pr/__snapshots__/index.spec.js.snap
index a0318deec0..69a8009cf3 100644
--- a/test/workers/pr/__snapshots__/index.spec.js.snap
+++ b/test/workers/pr/__snapshots__/index.spec.js.snap
@@ -32,7 +32,7 @@ Array [
 
 | Package | Type | Update | New value | References |
 |--|--|--|--|--|
-| \`dummy\` | devDependencies | undefined | 1.1.0 |[homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
+| dummy | devDependencies | pin | 1.1.0 | [homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
 
 :pushpin: **Important**: Renovate will wait until you have merged this Pin PR before creating any *upgrade* PRs for the affected packages. Add the preset \`:preserveSemverRanges\` your config if you instead don't wish to pin dependencies.
 
@@ -74,7 +74,7 @@ Array [
 
 | Package | Type | Update | New value | References |
 |--|--|--|--|--|
-| \`dummy\` | devDependencies | undefined | 1.1.0 |[homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
+| dummy | devDependencies | minor | 1.1.0 | [homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
 
 ---
 
@@ -114,11 +114,11 @@ Array [
 
 | Package | Type | Update | New value | References |
 |--|--|--|--|--|
-| all | devDependencies | lockFileMaintenance |  |[homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
-| \`a\` | undefined | undefined | aaaaaaa | |
-| \`b\` | undefined | pin | some_new_value@bbbbbbb | |
-| \`c\` | undefined | undefined | undefined | |
-| all | undefined | lockFileMaintenance |  | |
+| dummy | devDependencies | lockFileMaintenance | 1.1.0 | [homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
+| a |  |  | aaaaaaa |  |
+| b |  | pin |  |  |
+| c |  |  |  |  |
+| d |  | lockFileMaintenance |  |  |
 
 :abcd: If you wish to disable git hash updates, add \`\\":disableDigestUpdates\\"\` to the extends array in your config.
 
@@ -167,7 +167,7 @@ Array [
 
 | Package | Type | Update | New value | References |
 |--|--|--|--|--|
-| \`dummy\` | devDependencies | undefined | 1.1.0 |[homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
+| dummy | devDependencies | minor | 1.1.0 | [homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
 
 ---
 
@@ -205,7 +205,7 @@ Object {
 
 | Package | Type | Update | New value | References |
 |--|--|--|--|--|
-| \`dummy\` | devDependencies | undefined | 1.1.0 |[homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
+| dummy | devDependencies | minor | 1.1.0 | [homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
 
 ---
 
@@ -243,7 +243,7 @@ Object {
 
 | Package | Type | Update | New value | References |
 |--|--|--|--|--|
-| \`dummy\` | devDependencies | undefined | 1.1.0 |[homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
+| dummy | devDependencies | minor | 1.1.0 | [homepage](https://dummy.com), [source](https://github.com/renovateapp/dummy), [changelog](https://github.com/renovateapp/dummy/changelog.md) |
 
 ---
 
diff --git a/test/workers/pr/index.spec.js b/test/workers/pr/index.spec.js
index 1f42eba6fb..c1f947352b 100644
--- a/test/workers/pr/index.spec.js
+++ b/test/workers/pr/index.spec.js
@@ -117,6 +117,7 @@ describe('workers/pr', () => {
       config.privateRepo = true;
       config.currentValue = '1.0.0';
       config.newValue = '1.1.0';
+      config.updateType = 'minor';
       config.homepage = 'https://dummy.com';
       config.repositoryUrl = 'https://github.com/renovateapp/dummy';
       config.changelogUrl = 'https://github.com/renovateapp/dummy/changelog.md';
@@ -183,6 +184,7 @@ describe('workers/pr', () => {
       platform.getBranchStatus.mockReturnValueOnce('success');
       config.prCreation = 'status-success';
       config.isPin = true;
+      config.updateType = 'pin';
       config.schedule = 'before 5am';
       config.timezone = 'some timezone';
       config.rebaseStalePrs = true;
diff --git a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
index 3aaf8a1f59..e85bd437dd 100644
--- a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
+++ b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
@@ -40,6 +40,21 @@ Array [
     "npmrc": null,
     "packageFile": "package.json",
     "platform": "github",
+    "prBodyColumns": Array [
+      "Package",
+      "Type",
+      "Update",
+      "New value",
+      "References",
+    ],
+    "prBodyDefinitions": Object {
+      "New value": "{{{newValue}}}",
+      "Package": "{{{depName}}}",
+      "Package file": "{{{packageFile}}}",
+      "References": "{{{references}}}",
+      "Type": "{{{depType}}}",
+      "Update": "{{{updateType}}}",
+    },
     "prConcurrentLimit": 0,
     "prCreation": "immediate",
     "prHourlyLimit": 0,
@@ -111,6 +126,21 @@ Array [
     "npmrc": null,
     "packageFile": "package.json",
     "platform": "github",
+    "prBodyColumns": Array [
+      "Package",
+      "Type",
+      "Update",
+      "New value",
+      "References",
+    ],
+    "prBodyDefinitions": Object {
+      "New value": "{{{newValue}}}",
+      "Package": "{{{depName}}}",
+      "Package file": "{{{packageFile}}}",
+      "References": "{{{references}}}",
+      "Type": "{{{depType}}}",
+      "Update": "{{{updateType}}}",
+    },
     "prConcurrentLimit": 0,
     "prCreation": "immediate",
     "prHourlyLimit": 0,
@@ -179,6 +209,21 @@ Array [
     "npmrc": null,
     "packageFile": "package.json",
     "platform": "github",
+    "prBodyColumns": Array [
+      "Package",
+      "Type",
+      "Update",
+      "New value",
+      "References",
+    ],
+    "prBodyDefinitions": Object {
+      "New value": "{{{newValue}}}",
+      "Package": "{{{depName}}}",
+      "Package file": "{{{packageFile}}}",
+      "References": "{{{references}}}",
+      "Type": "{{{depType}}}",
+      "Update": "{{{updateType}}}",
+    },
     "prConcurrentLimit": 0,
     "prCreation": "immediate",
     "prHourlyLimit": 0,
@@ -253,6 +298,21 @@ Array [
     "npmrc": null,
     "packageFile": "backend/package.json",
     "platform": "github",
+    "prBodyColumns": Array [
+      "Package",
+      "Type",
+      "Update",
+      "New value",
+      "References",
+    ],
+    "prBodyDefinitions": Object {
+      "New value": "{{{newValue}}}",
+      "Package": "{{{depName}}}",
+      "Package file": "{{{packageFile}}}",
+      "References": "{{{references}}}",
+      "Type": "{{{depType}}}",
+      "Update": "{{{updateType}}}",
+    },
     "prConcurrentLimit": 0,
     "prCreation": "immediate",
     "prHourlyLimit": 0,
@@ -321,6 +381,21 @@ Array [
     "npmrc": null,
     "packageFile": "backend/package.json",
     "platform": "github",
+    "prBodyColumns": Array [
+      "Package",
+      "Type",
+      "Update",
+      "New value",
+      "References",
+    ],
+    "prBodyDefinitions": Object {
+      "New value": "{{{newValue}}}",
+      "Package": "{{{depName}}}",
+      "Package file": "{{{packageFile}}}",
+      "References": "{{{references}}}",
+      "Type": "{{{depType}}}",
+      "Update": "{{{updateType}}}",
+    },
     "prConcurrentLimit": 0,
     "prCreation": "immediate",
     "prHourlyLimit": 0,
@@ -395,6 +470,21 @@ Array [
     "npmrc": null,
     "packageFile": "frontend/package.json",
     "platform": "github",
+    "prBodyColumns": Array [
+      "Package",
+      "Type",
+      "Update",
+      "New value",
+      "References",
+    ],
+    "prBodyDefinitions": Object {
+      "New value": "{{{newValue}}}",
+      "Package": "{{{depName}}}",
+      "Package file": "{{{packageFile}}}",
+      "References": "{{{references}}}",
+      "Type": "{{{depType}}}",
+      "Update": "{{{updateType}}}",
+    },
     "prConcurrentLimit": 0,
     "prCreation": "immediate",
     "prHourlyLimit": 0,
@@ -466,6 +556,21 @@ Array [
     "npmrc": null,
     "packageFile": "Dockerfile",
     "platform": "github",
+    "prBodyColumns": Array [
+      "Package",
+      "Type",
+      "Update",
+      "New value",
+      "References",
+    ],
+    "prBodyDefinitions": Object {
+      "New value": "{{{newValue}}}",
+      "Package": "{{{depName}}}",
+      "Package file": "{{{packageFile}}}",
+      "References": "{{{references}}}",
+      "Type": "{{{depType}}}",
+      "Update": "{{{updateType}}}",
+    },
     "prConcurrentLimit": 0,
     "prCreation": "immediate",
     "prHourlyLimit": 0,
@@ -537,6 +642,21 @@ Array [
     "npmrc": null,
     "packageFile": "Dockerfile",
     "platform": "github",
+    "prBodyColumns": Array [
+      "Package",
+      "Type",
+      "Update",
+      "New value",
+      "References",
+    ],
+    "prBodyDefinitions": Object {
+      "New value": "{{{newValue}}}",
+      "Package": "{{{depName}}}",
+      "Package file": "{{{packageFile}}}",
+      "References": "{{{references}}}",
+      "Type": "{{{depType}}}",
+      "Update": "{{{updateType}}}",
+    },
     "prConcurrentLimit": 0,
     "prCreation": "immediate",
     "prHourlyLimit": 0,
diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md
index fb08523b5c..978ed72ae5 100644
--- a/website/docs/configuration-options.md
+++ b/website/docs/configuration-options.md
@@ -543,6 +543,58 @@ By default, Renovate will add sha256 digests to Docker source images so that the
 
 Add configuration here to specifically override settings for `pip` requirements files. Supports `requirements.txt` and `requirements.pip` files. The default file pattern is fairly flexible in an attempt to catch similarly named ones too but may be extended/changed.
 
+## prBodyColumns
+
+Use this array to provide a list of column names you wish to include in the PR tables.
+
+For example, if you wish to add the package file name to the table, you would add this to your config:
+
+```json
+{
+  "prBodyColumns": [
+    "Package",
+    "Update",
+    "Type",
+    "New value",
+    "Package file",
+    "References"
+  ]
+}
+```
+
+Note: "Package file" is predefined in the default `prBodyDefinitions` object so does not require a definition before it can be used.
+
+## prBodyDefinitions
+
+You can configure this object if you with to either (a) modify the template for an existing table column in PR bodies, or (b) you wish to _add_ a definition for a new/additional column.
+
+Here is an example of modifying the default value for the "Package" column to put it inside a `<code></code>` block:
+
+```json
+  "prBodyDefinitions": {
+    "Package": "`{{{depName}}}`"
+  }
+```
+
+Here is an example of adding a custom "Sourcegraph" column definition:
+
+```json
+{
+  "prBodyDefinitions": {
+    "Sourcegraph": "[![code search for \"{{{depName}}}\"](https://sourcegraph.com/search/badge?q=repo:%5Egithub%5C.com/{{{repository}}}%24+case:yes+-file:package%28-lock%29%3F%5C.json+{{{depName}}}&label=matches)](https://sourcegraph.com/search?q=repo:%5Egithub%5C.com/{{{repository}}}%24+case:yes+-file:package%28-lock%29%3F%5C.json+{{{depName}}})"
+  },
+  "prBodyColumns": [
+    "Package",
+    "Update",
+    "New value",
+    "References",
+    "Sourcegraph"
+  ]
+}
+```
+
+Note: Columns must also be included in the `prBodyColumns` array in order to be used, so that's why it's included above in the example.
+
 ## prConcurrentLimit
 
 This setting - if enabled - limits Renovate to a maximum of x concurrent PRs open at any time.
-- 
GitLab