From 19e839fc5add84356715760e88885de5f0b4ff28 Mon Sep 17 00:00:00 2001
From: Tanuel <tanuel.mategi@gmail.com>
Date: Wed, 17 Jul 2019 16:53:20 +0200
Subject: [PATCH] feat(composer): Add support for custom git repositories
 (#4055)

---
 lib/manager/composer/extract.js               | 130 ++++++++++++------
 .../__snapshots__/extract.spec.js.snap        |  74 ++++++++++
 .../manager/composer/_fixtures/composer4.json |  30 ++++
 .../manager/composer/_fixtures/composer5.json |  28 ++++
 .../manager/composer/_fixtures/composer5.lock |  28 ++++
 test/manager/composer/extract.spec.js         |  23 ++++
 6 files changed, 270 insertions(+), 43 deletions(-)
 create mode 100644 test/manager/composer/_fixtures/composer4.json
 create mode 100644 test/manager/composer/_fixtures/composer5.json
 create mode 100644 test/manager/composer/_fixtures/composer5.lock

diff --git a/lib/manager/composer/extract.js b/lib/manager/composer/extract.js
index b67da65a06..81d320d473 100644
--- a/lib/manager/composer/extract.js
+++ b/lib/manager/composer/extract.js
@@ -5,6 +5,38 @@ const semverComposer = require('../../versioning/composer');
 
 export { extractPackageFile };
 
+/**
+ * Parse the repositories field from a composer.json
+ *
+ * Entries with type vcs or git will be added to repositories,
+ * other entries will be added to registryUrls
+ *
+ * @param repoJson
+ * @param repositories
+ * @param registryUrls
+ */
+function parseRepositories(repoJson, repositories, registryUrls) {
+  try {
+    Object.entries(repoJson).forEach(([key, repo]) => {
+      const name = is.array(repoJson) ? repo.name : key;
+      switch (repo.type) {
+        case 'vcs':
+        case 'git':
+          // eslint-disable-next-line no-param-reassign
+          repositories[name] = repo;
+          break;
+        default:
+          registryUrls.push(repo);
+      }
+    });
+  } catch (e) /* istanbul ignore next */ {
+    logger.info(
+      { repositories: repoJson },
+      'Error parsing composer.json repositories config'
+    );
+  }
+}
+
 async function extractPackageFile(content, fileName) {
   logger.trace(`composer.extractPackageFile(${fileName})`);
   let composerJson;
@@ -14,6 +46,33 @@ async function extractPackageFile(content, fileName) {
     logger.info({ fileName }, 'Invalid JSON');
     return null;
   }
+  const repositories = {};
+  const registryUrls = [];
+  const res = {};
+
+  // handle lockfile
+  const lockfilePath = fileName.replace(/\.json$/, '.lock');
+  const lockContents = await platform.getFile(lockfilePath);
+  let lockParsed;
+  if (lockContents) {
+    logger.debug({ packageFile: fileName }, 'Found composer lock file');
+    res.composerLock = lockfilePath;
+    try {
+      lockParsed = JSON.parse(lockContents);
+    } catch (err) /* istanbul ignore next */ {
+      logger.warn({ err }, 'Error processing composer.lock');
+    }
+  } else {
+    res.composerLock = false;
+  }
+
+  // handle composer.json repositories
+  if (composerJson.repositories) {
+    parseRepositories(composerJson.repositories, repositories, registryUrls);
+  }
+  if (registryUrls.length !== 0) {
+    res.registryUrls = registryUrls;
+  }
   const deps = [];
   const depTypes = ['require', 'require-dev'];
   for (const depType of depTypes) {
@@ -23,12 +82,30 @@ async function extractPackageFile(content, fileName) {
           composerJson[depType]
         )) {
           const currentValue = version.trim();
+          // Default datasource and lookupName
+          let datasource = 'packagist';
+          let lookupName = depName;
+
+          // Check custom repositories by type
+          if (repositories[depName]) {
+            // eslint-disable-next-line default-case
+            switch (repositories[depName].type) {
+              case 'vcs':
+              case 'git':
+                datasource = 'gitTags';
+                lookupName = repositories[depName].url;
+                break;
+            }
+          }
           const dep = {
             depType,
             depName,
             currentValue,
-            datasource: 'packagist',
+            datasource,
           };
+          if (depName !== lookupName) {
+            dep.lookupName = lookupName;
+          }
           if (!depName.includes('/')) {
             dep.skipReason = 'unsupported';
           }
@@ -38,6 +115,14 @@ async function extractPackageFile(content, fileName) {
           if (currentValue === '*') {
             dep.skipReason = 'any-version';
           }
+          if (lockParsed) {
+            const lockedDep = lockParsed.packages.find(
+              item => item.name === dep.depName
+            );
+            if (lockedDep && semverComposer.isVersion(lockedDep.version)) {
+              dep.lockedVersion = lockedDep.version.replace(/^v/i, '');
+            }
+          }
           deps.push(dep);
         }
       } catch (err) /* istanbul ignore next */ {
@@ -49,48 +134,7 @@ async function extractPackageFile(content, fileName) {
   if (!deps.length) {
     return null;
   }
-  const res = { deps };
-  const filePath = fileName.replace(/\.json$/, '.lock');
-  const lockContents = await platform.getFile(filePath);
-  // istanbul ignore if
-  if (lockContents) {
-    logger.debug({ packageFile: fileName }, 'Found composer lock file');
-    res.composerLock = filePath;
-    try {
-      const lockParsed = JSON.parse(lockContents);
-      for (const dep of res.deps) {
-        const lockedDep = lockParsed.packages.find(
-          item => item.name === dep.depName
-        );
-        if (lockedDep && semverComposer.isVersion(lockedDep.version)) {
-          dep.lockedVersion = lockedDep.version.replace(/^v/i, '');
-        }
-      }
-    } catch (err) {
-      logger.warn({ err }, 'Error processing composer.lock');
-    }
-  } else {
-    res.composerLock = false;
-  }
-  if (composerJson.repositories) {
-    if (is.array(composerJson.repositories)) {
-      res.registryUrls = composerJson.repositories;
-    } else if (is.object(composerJson.repositories)) {
-      try {
-        res.registryUrls = [];
-        for (const repository of Object.values(composerJson.repositories)) {
-          res.registryUrls.push(repository);
-        }
-      } catch (err) /* istanbul ignore next */ {
-        logger.warn({ err }, 'Error extracting composer repositories');
-      }
-    } /* istanbul ignore next */ else {
-      logger.info(
-        { repositories: composerJson.repositories },
-        'Unknown composer repositories'
-      );
-    }
-  }
+  res.deps = deps;
   if (composerJson.type) {
     res.composerJsonType = composerJson.type;
   }
diff --git a/test/manager/composer/__snapshots__/extract.spec.js.snap b/test/manager/composer/__snapshots__/extract.spec.js.snap
index b944859152..30cd5c6dfe 100644
--- a/test/manager/composer/__snapshots__/extract.spec.js.snap
+++ b/test/manager/composer/__snapshots__/extract.spec.js.snap
@@ -565,6 +565,44 @@ Object {
 }
 `;
 
+exports[`lib/manager/composer/extract extractPackageFile() extracts object repositories and registryUrls with lock file 1`] = `
+Object {
+  "composerLock": "composer.lock",
+  "deps": Array [
+    Object {
+      "currentValue": "*",
+      "datasource": "packagist",
+      "depName": "aws/aws-sdk-php",
+      "depType": "require",
+      "skipReason": "any-version",
+    },
+    Object {
+      "currentValue": "dev-trunk",
+      "datasource": "gitTags",
+      "depName": "awesome/vcs",
+      "depType": "require",
+      "lockedVersion": "1.1.0",
+      "lookupName": "https://my-vcs.example/my-vcs-repo",
+      "skipReason": "unsupported-constraint",
+    },
+    Object {
+      "currentValue": ">=7.0.2",
+      "datasource": "gitTags",
+      "depName": "awesome/git",
+      "depType": "require",
+      "lockedVersion": "1.2.0",
+      "lookupName": "git@my-git.example:my-git-repo",
+    },
+  ],
+  "registryUrls": Array [
+    Object {
+      "type": "composer",
+      "url": "https://wpackagist.org",
+    },
+  ],
+}
+`;
+
 exports[`lib/manager/composer/extract extractPackageFile() extracts registryUrls 1`] = `
 Object {
   "composerLock": false,
@@ -605,3 +643,39 @@ Object {
   ],
 }
 `;
+
+exports[`lib/manager/composer/extract extractPackageFile() extracts repositories and registryUrls 1`] = `
+Object {
+  "composerLock": false,
+  "deps": Array [
+    Object {
+      "currentValue": "*",
+      "datasource": "packagist",
+      "depName": "aws/aws-sdk-php",
+      "depType": "require",
+      "skipReason": "any-version",
+    },
+    Object {
+      "currentValue": "dev-trunk",
+      "datasource": "gitTags",
+      "depName": "awesome/vcs",
+      "depType": "require",
+      "lookupName": "https://my-vcs.example/my-vcs-repo",
+      "skipReason": "unsupported-constraint",
+    },
+    Object {
+      "currentValue": ">=7.0.2",
+      "datasource": "gitTags",
+      "depName": "awesome/git",
+      "depType": "require",
+      "lookupName": "https://my-git.example/my-git-repo",
+    },
+  ],
+  "registryUrls": Array [
+    Object {
+      "type": "composer",
+      "url": "https://wpackagist.org",
+    },
+  ],
+}
+`;
diff --git a/test/manager/composer/_fixtures/composer4.json b/test/manager/composer/_fixtures/composer4.json
new file mode 100644
index 0000000000..2b9e63280c
--- /dev/null
+++ b/test/manager/composer/_fixtures/composer4.json
@@ -0,0 +1,30 @@
+{
+  "name": "acme/git-sources",
+  "description": "Fetch Packages via git",
+  "repositories":[
+      {
+          "name": "awesome/vcs",
+          "type":"vcs",
+          "url":"https://my-vcs.example/my-vcs-repo"
+      },
+      {
+          "name": "awesome/git",
+          "type":"git",
+          "url":"https://my-git.example/my-git-repo"
+      },
+      {
+        "type": "composer",
+        "url": "https://wpackagist.org"
+      }
+  ],
+  "require": {
+      "aws/aws-sdk-php":"*",
+      "awesome/vcs":"dev-trunk",
+      "awesome/git":">=7.0.2"
+  },
+  "autoload": {
+      "psr-0": {
+          "Acme": "src/"
+      }
+  }
+}
diff --git a/test/manager/composer/_fixtures/composer5.json b/test/manager/composer/_fixtures/composer5.json
new file mode 100644
index 0000000000..17238d84a8
--- /dev/null
+++ b/test/manager/composer/_fixtures/composer5.json
@@ -0,0 +1,28 @@
+{
+  "name": "acme/git-sources",
+  "description": "Fetch Packages via git",
+  "repositories":{
+    "awesome/vcs": {
+          "type":"vcs",
+          "url":"https://my-vcs.example/my-vcs-repo"
+      },
+    "awesome/git": {
+          "type":"git",
+          "url":"git@my-git.example:my-git-repo"
+      },
+      "wpackagist": {
+        "type": "composer",
+        "url": "https://wpackagist.org"
+      }
+  },
+  "require": {
+      "aws/aws-sdk-php":"*",
+      "awesome/vcs":"dev-trunk",
+      "awesome/git":">=7.0.2"
+  },
+  "autoload": {
+      "psr-0": {
+          "Acme": "src/"
+      }
+  }
+}
diff --git a/test/manager/composer/_fixtures/composer5.lock b/test/manager/composer/_fixtures/composer5.lock
new file mode 100644
index 0000000000..53e834ba47
--- /dev/null
+++ b/test/manager/composer/_fixtures/composer5.lock
@@ -0,0 +1,28 @@
+{
+  "_readme": [
+    "This file locks the dependencies of your project to a known state",
+    "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+    "This file is @generated automatically"
+  ],
+  "content-hash": "h34j5h3p3g4ug34u34543j5h34j53h5j",
+  "packages": [
+    {
+      "name": "awesome/vcs",
+      "version": "1.1.0",
+      "source": {
+        "type": "git",
+        "url": "https://my-vcs.example/my-vcs-repo",
+        "reference": "3j5b345j3b45j345j345h34j5h345j34h5j34h5j"
+      }
+    },
+    {
+      "name": "awesome/git",
+      "version": "1.2.0",
+      "source": {
+        "type": "git",
+        "url": "git@my-git.example/awesome.git",
+        "reference": "3j5b345j3b45j345j345h34j5h345j34h5j34h5j"
+      }
+    }
+  ]
+}
diff --git a/test/manager/composer/extract.spec.js b/test/manager/composer/extract.spec.js
index e8420dac96..c01ce51f07 100644
--- a/test/manager/composer/extract.spec.js
+++ b/test/manager/composer/extract.spec.js
@@ -16,6 +16,18 @@ const requirements3 = fs.readFileSync(
   'test/manager/composer/_fixtures/composer3.json',
   'utf8'
 );
+const requirements4 = fs.readFileSync(
+  'test/manager/composer/_fixtures/composer4.json',
+  'utf8'
+);
+const requirements5 = fs.readFileSync(
+  'test/manager/composer/_fixtures/composer5.json',
+  'utf8'
+);
+const requirements5Lock = fs.readFileSync(
+  'test/manager/composer/_fixtures/composer5.lock',
+  'utf8'
+);
 
 describe('lib/manager/composer/extract', () => {
   describe('extractPackageFile()', () => {
@@ -43,6 +55,17 @@ describe('lib/manager/composer/extract', () => {
       expect(res).toMatchSnapshot();
       expect(res.registryUrls).toHaveLength(3);
     });
+    it('extracts repositories and registryUrls', async () => {
+      const res = await extractPackageFile(requirements4, packageFile);
+      expect(res).toMatchSnapshot();
+      expect(res.registryUrls).toHaveLength(1);
+    });
+    it('extracts object repositories and registryUrls with lock file', async () => {
+      platform.getFile.mockReturnValueOnce(requirements5Lock);
+      const res = await extractPackageFile(requirements5, packageFile);
+      expect(res).toMatchSnapshot();
+      expect(res.registryUrls).toHaveLength(1);
+    });
     it('extracts dependencies with lock file', async () => {
       platform.getFile.mockReturnValueOnce('some content');
       const res = await extractPackageFile(requirements1, packageFile);
-- 
GitLab