From 305f094bd463e47c78a2733ecd5536d9a4ff25e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C8=9Aurcanu=20Dragomir?= <dragomirt22@gmail.com>
Date: Fri, 27 Apr 2018 06:45:22 +0300
Subject: [PATCH] Added matchCurrentVersion selector to packageRules (#1835)

The matchCurrentVersion option sets a range of versions that a package update can be in. If the package's current version doesn't satisfy the matchCurrentVersion range, it won't match the rule.

Closes #1771
---
 lib/config/definitions.js                     | 10 ++++
 lib/config/validation.js                      | 13 ++++-
 lib/util/semver.js                            |  2 +
 lib/workers/package-file/dep-type.js          |  7 +++
 test/config/__snapshots__/index.spec.js.snap  |  1 +
 .../__snapshots__/validation.spec.js.snap     |  9 ++++
 test/config/validation.spec.js                | 16 +++++++
 test/workers/package-file/dep-type.spec.js    | 48 +++++++++++++++++++
 .../2017-10-05-configuration-options.md       | 10 ++++
 9 files changed, 115 insertions(+), 1 deletion(-)

diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 22564f7dce..27c8195373 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -418,6 +418,16 @@ const options = [
     cli: false,
     env: false,
   },
+  {
+    name: 'matchCurrentVersion',
+    description:
+      'A version or version range to match against the current version of a package. Valid only within `packageRules` object',
+    type: 'string',
+    stage: 'depType',
+    mergeable: true,
+    cli: false,
+    env: false,
+  },
   // Version behaviour
   {
     name: 'allowedVersions',
diff --git a/lib/config/validation.js b/lib/config/validation.js
index 3b7d31db5d..cea56a8f1a 100644
--- a/lib/config/validation.js
+++ b/lib/config/validation.js
@@ -169,6 +169,17 @@ async function validateConfig(config, isPreset, parentPath) {
                       message,
                     });
                   }
+                  if (
+                    resolvedRule.matchCurrentVersion &&
+                    !isValidSemver(resolvedRule.matchCurrentVersion)
+                  ) {
+                    errors.push({
+                      depName: 'Configuration Error',
+                      message: `${currentPath}: ${
+                        resolvedRule.matchCurrentVersion
+                      } isn't a valid semver`,
+                    });
+                  }
                 } else {
                   errors.push({
                     depName: 'Configuration Error',
@@ -197,7 +208,7 @@ async function validateConfig(config, isPreset, parentPath) {
               }
             }
             if (
-              selectors.includes(key) &&
+              (selectors.includes(key) || key === 'matchCurrentVersion') &&
               !(parentPath && parentPath.match(/packageRules\[\d+\]$/)) && // Inside a packageRule
               (parentPath || !isPreset) // top level in a preset
             ) {
diff --git a/lib/util/semver.js b/lib/util/semver.js
index b4e054df13..7c0f41224c 100644
--- a/lib/util/semver.js
+++ b/lib/util/semver.js
@@ -10,6 +10,7 @@ const { parseRange, parse: parseVersion, stringifyRange } = semverUtils;
 const {
   compare: semverSort,
   gt: isGreaterThan,
+  intersects: intersectsSemver,
   maxSatisfying: maxSatisfyingVersion,
   minSatisfying: minSatisfyingVersion,
   minor: getMinor,
@@ -34,6 +35,7 @@ module.exports = {
   getMajor,
   getMinor,
   getPatch,
+  intersectsSemver,
   isGreaterThan,
   isRange,
   isStable,
diff --git a/lib/workers/package-file/dep-type.js b/lib/workers/package-file/dep-type.js
index 3e88cf5374..45d22cea6e 100644
--- a/lib/workers/package-file/dep-type.js
+++ b/lib/workers/package-file/dep-type.js
@@ -1,6 +1,7 @@
 const configParser = require('../../config');
 const pkgWorker = require('./package');
 const { extractDependencies } = require('../../manager');
+const { intersectsSemver } = require('../../util/semver');
 
 module.exports = {
   renovateDepType,
@@ -62,6 +63,7 @@ function getDepConfig(depTypeConfig, dep) {
         excludePackagePatterns,
         packageNames,
         packagePatterns,
+        matchCurrentVersion,
       } = packageRule;
       let applyRule;
       if (
@@ -104,6 +106,9 @@ function getDepConfig(depTypeConfig, dep) {
           }
         }
       }
+      if (applyRule !== false && matchCurrentVersion) {
+        applyRule = intersectsSemver(dep.currentVersion, matchCurrentVersion);
+      }
       if (applyRule !== false && depTypeList && depTypeList.length) {
         applyRule = depTypeList.includes(dep.depType);
       }
@@ -114,6 +119,8 @@ function getDepConfig(depTypeConfig, dep) {
         delete depConfig.packagePatterns;
         delete depConfig.excludePackageNames;
         delete depConfig.excludePackagePatterns;
+        delete depConfig.depTypeList;
+        delete depConfig.matchCurrentVersion;
       }
     });
   }
diff --git a/test/config/__snapshots__/index.spec.js.snap b/test/config/__snapshots__/index.spec.js.snap
index 99616eb51a..15bad95a23 100644
--- a/test/config/__snapshots__/index.spec.js.snap
+++ b/test/config/__snapshots__/index.spec.js.snap
@@ -114,6 +114,7 @@ Object {
   "logLevel": "error",
   "major": Object {},
   "managerBranchPrefix": "",
+  "matchCurrentVersion": null,
   "meteor": Object {},
   "minor": Object {},
   "mirrorMode": false,
diff --git a/test/config/__snapshots__/validation.spec.js.snap b/test/config/__snapshots__/validation.spec.js.snap
index ec7d7a889a..f5b8df6ac4 100644
--- a/test/config/__snapshots__/validation.spec.js.snap
+++ b/test/config/__snapshots__/validation.spec.js.snap
@@ -68,6 +68,15 @@ Array [
 ]
 `;
 
+exports[`config/validation validateConfig(config) invalid matchCurrentVersion triggers an error 1`] = `
+Array [
+  Object {
+    "depName": "Configuration Error",
+    "message": "packageRules: >= 2.-1.4 isn't a valid semver",
+  },
+]
+`;
+
 exports[`config/validation validateConfig(config) returns nested errors 1`] = `
 Array [
   Object {
diff --git a/test/config/validation.spec.js b/test/config/validation.spec.js
index 136f1ed8d5..d010aaf597 100644
--- a/test/config/validation.spec.js
+++ b/test/config/validation.spec.js
@@ -107,5 +107,21 @@ describe('config/validation', () => {
       expect(errors).toMatchSnapshot();
       expect(errors).toHaveLength(0);
     });
+    it('invalid matchCurrentVersion triggers an error', async () => {
+      const config = {
+        packageRules: [
+          {
+            packageNames: ['angular'],
+            matchCurrentVersion: '>= 2.-1.4',
+          },
+        ],
+      };
+      const { warnings, errors } = await configValidation.validateConfig(
+        config
+      );
+      expect(warnings).toHaveLength(0);
+      expect(errors).toMatchSnapshot();
+      expect(errors).toHaveLength(1);
+    });
   });
 });
diff --git a/test/workers/package-file/dep-type.spec.js b/test/workers/package-file/dep-type.spec.js
index e010816452..9bcfb3be43 100644
--- a/test/workers/package-file/dep-type.spec.js
+++ b/test/workers/package-file/dep-type.spec.js
@@ -260,5 +260,53 @@ describe('lib/workers/package-file/dep-type', () => {
       expect(res.x).toBeUndefined();
       expect(res.packageRules).toBeUndefined();
     });
+    it('checks if matchCurrentVersion selector is valid and satisfies the condition on range overlap', () => {
+      const config = {
+        packageRules: [
+          {
+            packageNames: ['test'],
+            matchCurrentVersion: '<= 2.0.0',
+            x: 1,
+          },
+        ],
+      };
+      const res1 = depTypeWorker.getDepConfig(config, {
+        depName: 'test',
+        currentVersion: '^1.0.0',
+      });
+      expect(res1.x).toBeDefined();
+    });
+    it('checks if matchCurrentVersion selector is valid and satisfies the condition on pinned to range overlap', () => {
+      const config = {
+        packageRules: [
+          {
+            packageNames: ['test'],
+            matchCurrentVersion: '>= 2.0.0',
+            x: 1,
+          },
+        ],
+      };
+      const res1 = depTypeWorker.getDepConfig(config, {
+        depName: 'test',
+        currentVersion: '2.4.6',
+      });
+      expect(res1.x).toBeDefined();
+    });
+    it('checks if matchCurrentVersion selector works with static values', () => {
+      const config = {
+        packageRules: [
+          {
+            packageNames: ['test'],
+            matchCurrentVersion: '4.6.0',
+            x: 1,
+          },
+        ],
+      };
+      const res1 = depTypeWorker.getDepConfig(config, {
+        depName: 'test',
+        currentVersion: '4.6.0',
+      });
+      expect(res1.x).toBeDefined();
+    });
   });
 });
diff --git a/website/docs/_posts/2017-10-05-configuration-options.md b/website/docs/_posts/2017-10-05-configuration-options.md
index 303b02ff45..fe1c5afc2e 100644
--- a/website/docs/_posts/2017-10-05-configuration-options.md
+++ b/website/docs/_posts/2017-10-05-configuration-options.md
@@ -618,6 +618,16 @@ Prefix to be added after `branchPrefix` for distinguishing between branches for
 
 This value defaults to empty string, as historically no prefix was necessary for when Renovate was JS-only. Now - for example - we use `docker-` for Docker branches, so they may look like `renovate/docker-ubuntu-16.x`.
 
+## matchCurrentVersion
+
+If set in a packageRule, the rule will be applied only if the current version of the package matches against this version or range.
+
+| name | value  |
+| ---- | ------ |
+| type | string |
+
+`matchCurrentVersion` can be an exact semver version or a semver range.
+
 ## meteor
 
 Configuration specific for meteor updates.
-- 
GitLab