From e64b7576e9c75af2e82b6a3d908bd2eebb809bff Mon Sep 17 00:00:00 2001
From: RahulGautamSingh <rahultesnik@gmail.com>
Date: Mon, 28 Mar 2022 13:28:20 +0530
Subject: [PATCH] feat: matchSourceUrls (#14813)

---
 docs/usage/configuration-options.md | 15 ++++++
 lib/config/options/index.ts         | 12 +++++
 lib/config/types.ts                 |  1 +
 lib/config/validation.ts            |  1 +
 lib/util/package-rules.spec.ts      | 83 +++++++++++++++++++++++++++++
 lib/util/package-rules.ts           | 11 ++++
 6 files changed, 123 insertions(+)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index c8eb2d5793..cc5194f678 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1756,6 +1756,21 @@ Here's an example of where you use this to group together all packages from the
 }
 ```
 
+### matchSourceUrls
+
+Here's an example of where you use this to match exact package urls:
+
+```json
+{
+  "packageRules": [
+    {
+      "matchSourceUrls": ["https://github.com/facebook/react"],
+      "groupName": "React"
+    }
+  ]
+}
+```
+
 ### matchUpdateTypes
 
 Use this field to match rules against types of updates.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index cdbb0ae6d4..a28240c12e 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -1022,6 +1022,18 @@ const options: RenovateOptions[] = [
     cli: false,
     env: false,
   },
+  {
+    name: 'matchSourceUrls',
+    description: 'A list of source URLs to exact match against.',
+    type: 'array',
+    subType: 'string',
+    allowString: true,
+    stage: 'package',
+    parent: 'packageRules',
+    mergeable: true,
+    cli: false,
+    env: false,
+  },
   {
     name: 'replacementName',
     description:
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 3bf10bab7e..8c5a068ac3 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -274,6 +274,7 @@ export interface PackageRule
   excludePackagePrefixes?: string[];
   matchCurrentVersion?: string | Range;
   matchSourceUrlPrefixes?: string[];
+  matchSourceUrls?: string[];
   matchUpdateTypes?: UpdateType[];
 }
 
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index 40e40ab33f..383790945c 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -304,6 +304,7 @@ export async function validateConfig(
               'excludePackagePrefixes',
               'matchCurrentVersion',
               'matchSourceUrlPrefixes',
+              'matchSourceUrls',
               'matchUpdateTypes',
             ];
             if (key === 'packageRules') {
diff --git a/lib/util/package-rules.spec.ts b/lib/util/package-rules.spec.ts
index 30816b299a..ffb02321bf 100644
--- a/lib/util/package-rules.spec.ts
+++ b/lib/util/package-rules.spec.ts
@@ -495,6 +495,68 @@ describe('util/package-rules', () => {
     const res = applyPackageRules({ ...config, ...dep });
     expect(res.x).toBeUndefined();
   });
+  it('matches matchSourceUrls', () => {
+    const config: TestConfig = {
+      packageRules: [
+        {
+          matchSourceUrls: [
+            'https://github.com/foo/bar',
+            'https://github.com/renovatebot/presets',
+          ],
+          x: 1,
+        },
+      ],
+    };
+    const dep = {
+      depType: 'dependencies',
+      depName: 'a',
+      updateType: 'patch' as UpdateType,
+      sourceUrl: 'https://github.com/renovatebot/presets',
+    };
+    const res = applyPackageRules({ ...config, ...dep });
+    expect(res.x).toBe(1);
+  });
+  it('non-matches matchSourceUrls', () => {
+    const config: TestConfig = {
+      packageRules: [
+        {
+          matchSourceUrls: [
+            'https://github.com/foo/bar',
+            'https://github.com/facebook/react',
+          ],
+          x: 1,
+        },
+      ],
+    };
+    const dep = {
+      depType: 'dependencies',
+      depName: 'a',
+      updateType: 'patch' as UpdateType,
+      sourceUrl: 'https://github.com/facebook/react-native',
+    };
+    const res = applyPackageRules({ ...config, ...dep });
+    expect(res.x).toBeUndefined();
+  });
+  it('handles matchSourceUrls when missing sourceUrl', () => {
+    const config: TestConfig = {
+      packageRules: [
+        {
+          matchSourceUrls: [
+            'https://github.com/foo/bar',
+            'https://github.com/renovatebot/',
+          ],
+          x: 1,
+        },
+      ],
+    };
+    const dep = {
+      depType: 'dependencies',
+      depName: 'a',
+      updateType: 'patch' as UpdateType,
+    };
+    const res = applyPackageRules({ ...config, ...dep });
+    expect(res.x).toBeUndefined();
+  });
   it('filters naked depType', () => {
     const config: TestConfig = {
       packageRules: [
@@ -791,4 +853,25 @@ describe('util/package-rules', () => {
     const res = applyPackageRules({ ...config, ...dep });
     expect(res.x).toBe(1);
   });
+  it('matches matchSourceUrls(case-insensitive)', () => {
+    const config: TestConfig = {
+      packageRules: [
+        {
+          matchSourceUrls: [
+            'https://github.com/foo/bar',
+            'https://github.com/Renovatebot/renovate',
+          ],
+          x: 1,
+        },
+      ],
+    };
+    const dep = {
+      depType: 'dependencies',
+      depName: 'a',
+      updateType: 'patch' as UpdateType,
+      sourceUrl: 'https://github.com/renovatebot/Renovate',
+    };
+    const res = applyPackageRules({ ...config, ...dep });
+    expect(res.x).toBe(1);
+  });
 });
diff --git a/lib/util/package-rules.ts b/lib/util/package-rules.ts
index 46a62b37cd..ec18e0f909 100644
--- a/lib/util/package-rules.ts
+++ b/lib/util/package-rules.ts
@@ -45,6 +45,7 @@ function matchesRule(
   const excludePackagePatterns = packageRule.excludePackagePatterns || [];
   const excludePackagePrefixes = packageRule.excludePackagePrefixes || [];
   const matchSourceUrlPrefixes = packageRule.matchSourceUrlPrefixes || [];
+  const matchSourceUrls = packageRule.matchSourceUrls || [];
   const matchCurrentVersion = packageRule.matchCurrentVersion || null;
   const matchUpdateTypes = packageRule.matchUpdateTypes || [];
   let positiveMatch = false;
@@ -210,6 +211,16 @@ function matchesRule(
     }
     positiveMatch = true;
   }
+  if (matchSourceUrls.length) {
+    const upperCaseSourceUrl = sourceUrl?.toUpperCase();
+    const isMatch = matchSourceUrls.some(
+      (url) => upperCaseSourceUrl === url.toUpperCase()
+    );
+    if (!isMatch) {
+      return false;
+    }
+    positiveMatch = true;
+  }
   if (matchCurrentVersion) {
     const version = allVersioning.get(versioning);
     const matchCurrentVersionStr = matchCurrentVersion.toString();
-- 
GitLab