diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 8692ad6155aec7029c97dd68c0f28eb37ee08416..da92af6b2318b2745e5685051a9f356711410304 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -309,6 +309,8 @@ See [shareable config presets](https://docs.renovatebot.com/config-presets) for
 
 The primary use case for this option is if you are following a pre-release tag of a certain dependency, e.g. `typescript` "insiders" build. When it's configured, Renovate bypasses its normal major/minor/patch logic and stable/unstable logic and simply raises a PR if the tag does not match your current version.
 
+## git-submodules
+
 ## gitLabAutomerge
 
 Please note that when this option is enabled it is possible that MRs with failing pipelines are getting merged. This is caused by a race condition in GitLab's Merge Request API - [read the corresponding issue](https://gitlab.com/gitlab-org/gitlab/issues/26293) for details.
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index b7268838480ca513c5943d5ab3887e1008705271..d3929a5aac5cd9f41426812c07043c4a0d6df875 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -555,6 +555,7 @@ const options: RenovateOptions[] = [
       'cargo',
       'composer',
       'docker',
+      'git',
       'hashicorp',
       'hex',
       'ivy',
@@ -1688,6 +1689,18 @@ const options: RenovateOptions[] = [
     mergeable: true,
     cli: false,
   },
+  {
+    name: 'git-submodules',
+    description: 'Configuration object for git submodule files',
+    stage: 'package',
+    type: 'object',
+    default: {
+      versionScheme: 'git',
+      fileMatch: ['(^|/).gitmodules$'],
+    },
+    mergeable: true,
+    cli: false,
+  },
   {
     name: 'php',
     description: 'Configuration object for php',
diff --git a/lib/datasource/git-submodules/index.ts b/lib/datasource/git-submodules/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..446313077924f8259f87602ddd8a8f6f2facff61
--- /dev/null
+++ b/lib/datasource/git-submodules/index.ts
@@ -0,0 +1,55 @@
+import Git from 'simple-git/promise';
+import { URL } from 'url';
+
+import { ReleaseResult, PkgReleaseConfig, DigestConfig } from '../common';
+import { logger } from '../../logger';
+
+export async function getPkgReleases({
+  lookupName,
+  registryUrls,
+}: PkgReleaseConfig): Promise<ReleaseResult | null> {
+  const cacheNamespace = 'datasource-git-submodules';
+  const cacheKey = `${registryUrls[0]}-${registryUrls[1]}`;
+  const cachedResult = await renovateCache.get<ReleaseResult>(
+    cacheNamespace,
+    cacheKey
+  );
+  // istanbul ignore if
+  if (cachedResult) {
+    return cachedResult;
+  }
+
+  const git = Git();
+  try {
+    const newHash = (await git.listRemote([
+      '--refs',
+      registryUrls[0],
+      registryUrls[1],
+    ]))
+      .trim()
+      .split(/\t/)[0];
+
+    const sourceUrl = new URL(registryUrls[0]);
+    sourceUrl.username = '';
+
+    const result = {
+      sourceUrl: sourceUrl.href,
+      releases: [
+        {
+          version: newHash,
+        },
+      ],
+    };
+    const cacheMinutes = 60;
+    await renovateCache.set(cacheNamespace, cacheKey, result, cacheMinutes);
+    return result;
+  } catch (err) {
+    logger.debug(`Error looking up tags in ${lookupName}`);
+  }
+  return null;
+}
+
+export const getDigest = (
+  config: DigestConfig,
+  newValue?: string
+): Promise<string> => new Promise(resolve => resolve(newValue));
diff --git a/lib/datasource/index.ts b/lib/datasource/index.ts
index 1f9604aad0a2e75461224d9109808ffede25e238..4079e1f2fd508a56d6eed51359f28a3ec9da88df 100644
--- a/lib/datasource/index.ts
+++ b/lib/datasource/index.ts
@@ -9,6 +9,7 @@ import * as hex from './hex';
 import * as github from './github';
 import * as gitlab from './gitlab';
 import * as gitTags from './git-tags';
+import * as gitSubmodules from './git-submodules';
 import * as go from './go';
 import * as gradleVersion from './gradle-version';
 import * as helm from './helm';
@@ -41,6 +42,7 @@ const datasources: Record<string, Datasource> = {
   github,
   gitlab,
   gitTags,
+  gitSubmodules,
   go,
   gradleVersion,
   maven,
diff --git a/lib/manager/common.ts b/lib/manager/common.ts
index 71040809bb6ee4ae1a15a57dd1cf75994f0d7425..dfea00da9f22401d6187c48754dd6154c4bf6028 100644
--- a/lib/manager/common.ts
+++ b/lib/manager/common.ts
@@ -145,6 +145,7 @@ export interface Upgrade<T = Record<string, any>>
   currentVersion?: string;
   depGroup?: string;
   downloadUrl?: string;
+  localDir?: string;
   name?: string;
   newDigest?: string;
   newFrom?: string;
diff --git a/lib/manager/git-submodules/artifacts.ts b/lib/manager/git-submodules/artifacts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..81555ef443c66ef0cb85417453de55df5d3c4e7a
--- /dev/null
+++ b/lib/manager/git-submodules/artifacts.ts
@@ -0,0 +1,17 @@
+import { UpdateArtifactsConfig, UpdateArtifactsResult } from '../common';
+
+export default function updateArtifacts(
+  packageFileName: string,
+  updatedDeps: string[],
+  newPackageFileContent: string,
+  config: UpdateArtifactsConfig
+): UpdateArtifactsResult[] | null {
+  return [
+    {
+      file: {
+        name: updatedDeps[0],
+        contents: '',
+      },
+    },
+  ];
+}
diff --git a/lib/manager/git-submodules/extract.ts b/lib/manager/git-submodules/extract.ts
new file mode 100644
index 0000000000000000000000000000000000000000..83a780dea17cbafccac73cb7075c340085518461
--- /dev/null
+++ b/lib/manager/git-submodules/extract.ts
@@ -0,0 +1,87 @@
+import Git from 'simple-git/promise';
+import upath from 'upath';
+import URL from 'url';
+
+import { ManagerConfig, PackageFile } from '../common';
+
+async function getUrl(
+  git: Git.SimpleGit,
+  gitModulesPath: string,
+  submoduleName: string
+): Promise<string> {
+  const path = (await Git().raw([
+    'config',
+    '--file',
+    gitModulesPath,
+    '--get',
+    `submodule.${submoduleName}.url`,
+  ])).trim();
+  if (!path.startsWith('../')) {
+    return path;
+  }
+  const remoteUrl = (await git.raw([
+    'config',
+    '--get',
+    'remote.origin.url',
+  ])).trim();
+  return URL.resolve(`${remoteUrl}/`, path);
+}
+
+async function getBranch(
+  gitModulesPath: string,
+  submoduleName: string
+): Promise<string> {
+  return (
+    (await Git().raw([
+      'config',
+      '--file',
+      gitModulesPath,
+      '--get',
+      `submodule.${submoduleName}.branch`,
+    ])) || 'master'
+  ).trim();
+}
+
+export default async function extractPackageFile(
+  content: string,
+  fileName: string,
+  config: ManagerConfig
+): Promise<PackageFile | null> {
+  const git = Git(config.localDir);
+  const gitModulesPath = upath.join(config.localDir, fileName);
+
+  const depNames = (
+    (await git.raw([
+      'config',
+      '--file',
+      gitModulesPath,
+      '--get-regexp',
+      'path',
+    ])) || ''
+  )
+    .trim()
+    .split(/[\n\s]/)
+    .filter((_e: string, i: number) => i % 2);
+
+  if (!depNames.length) {
+    return null;
+  }
+
+  const deps = await Promise.all(
+    depNames.map(async depName => {
+      const currentValue = (await git.subModule(['status', depName]))
+        .trim()
+        .split(/[+\s]/)[0];
+      const submoduleBranch = await getBranch(gitModulesPath, depName);
+      const subModuleUrl = await getUrl(git, gitModulesPath, depName);
+      return {
+        depName,
+        registryUrls: [subModuleUrl, submoduleBranch],
+        currentValue,
+        currentDigest: currentValue,
+      };
+    })
+  );
+
+  return { deps, datasource: 'gitSubmodules' };
+}
diff --git a/lib/manager/git-submodules/index.ts b/lib/manager/git-submodules/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6b14bdeab34dc9d9be1e9e614e5e845a5ed387bb
--- /dev/null
+++ b/lib/manager/git-submodules/index.ts
@@ -0,0 +1,3 @@
+export { default as extractPackageFile } from './extract';
+export { default as updateDependency } from './update';
+export { default as updateArtifacts } from './artifacts';
diff --git a/lib/manager/git-submodules/update.ts b/lib/manager/git-submodules/update.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e33367dd6a44bc4ade56a4eec34b72e791bf831f
--- /dev/null
+++ b/lib/manager/git-submodules/update.ts
@@ -0,0 +1,23 @@
+import Git from 'simple-git/promise';
+
+import { Upgrade } from '../common';
+
+export default async function updateDependency(
+  fileContent: string,
+  upgrade: Upgrade
+): Promise<string | null> {
+  const git = Git(upgrade.localDir);
+
+  try {
+    await git.raw([
+      'submodule',
+      'update',
+      '--init',
+      '--remote',
+      upgrade.depName,
+    ]);
+    return fileContent;
+  } catch (err) {
+    return null;
+  }
+}
diff --git a/lib/manager/index.ts b/lib/manager/index.ts
index ca8391611760d3d70a3296d8af60da3e0f2fb959..20fbb055664fd1b9dcfc4894c938378542824f75 100644
--- a/lib/manager/index.ts
+++ b/lib/manager/index.ts
@@ -21,6 +21,7 @@ const managerList = [
   'docker-compose',
   'dockerfile',
   'droneci',
+  'git-submodules',
   'github-actions',
   'gitlabci',
   'gitlabci-include',
diff --git a/lib/platform/git/storage.ts b/lib/platform/git/storage.ts
index cdb22fbaddec4b726f17824282e9f83bbf25d370..29b7ee7551eb79c200ebead15544b6ffe60d7e1f 100644
--- a/lib/platform/git/storage.ts
+++ b/lib/platform/git/storage.ts
@@ -76,6 +76,14 @@ function throwBaseBranchValidationError(branchName: string): never {
   throw error;
 }
 
+async function isDirectory(dir: string): Promise<boolean> {
+  try {
+    return (await fs.stat(dir)).isDirectory();
+  } catch (err) {
+    return false;
+  }
+}
+
 export class Storage {
   private _config: LocalConfig = {} as any;
 
@@ -175,6 +183,7 @@ export class Storage {
     const submodules = await this.getSubmodules();
     for (const submodule of submodules) {
       try {
+        logger.debug(`Cloning git submodule at ${submodule}`);
         await this._git.submoduleUpdate(['--init', '--', submodule]);
       } catch (err) {
         logger.warn(`Unable to initialise git submodule at ${submodule}`);
@@ -470,6 +479,9 @@ export class Storage {
         // istanbul ignore if
         if (file.name === '|delete|') {
           deleted.push(file.contents);
+        } else if (await isDirectory(join(this._cwd!, file.name))) {
+          fileNames.push(file.name);
+          await this._git!.add(file.name);
         } else {
           fileNames.push(file.name);
           await fs.outputFile(
diff --git a/lib/versioning/git/index.ts b/lib/versioning/git/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b545fc8a4427c02f7602f2bd2d94caa9e9fff939
--- /dev/null
+++ b/lib/versioning/git/index.ts
@@ -0,0 +1,21 @@
+import * as generic from '../loose/generic';
+import { VersioningApi } from '../common';
+
+const parse = (version: string): any => ({ release: [parseInt(version, 10)] });
+
+const isCompatible = (version: string, range: string): boolean => true;
+
+const compare = (version1: string, version2: string): number => -1;
+
+const valueToVersion = (value: string): string => value;
+
+export const api: VersioningApi = {
+  ...generic.create({
+    parse,
+    compare,
+  }),
+  isCompatible,
+  valueToVersion,
+};
+
+export default api;
diff --git a/lib/workers/branch/get-updated.ts b/lib/workers/branch/get-updated.ts
index 8946e5ead69cfb58d6c4233f6b5f421125b3463b..72f6784990bd51a196afe88c161865b896fcd56d 100644
--- a/lib/workers/branch/get-updated.ts
+++ b/lib/workers/branch/get-updated.ts
@@ -70,6 +70,12 @@ export async function getUpdatedPackageFiles(
         logger.debug('Updating packageFile content');
         updatedFileContents[packageFile] = newContent;
       }
+      if (
+        newContent === existingContent &&
+        upgrade.datasource === 'gitSubmodules'
+      ) {
+        updatedFileContents[packageFile] = newContent;
+      }
     }
   }
   const updatedPackageFiles = Object.keys(updatedFileContents).map(name => ({
diff --git a/lib/workers/repository/process/lookup/index.ts b/lib/workers/repository/process/lookup/index.ts
index 32c070a95c73291586a599764fe76a1ee2d2e87f..0f5775407ca4030d8d75786889aa2e4296d9b939 100644
--- a/lib/workers/repository/process/lookup/index.ts
+++ b/lib/workers/repository/process/lookup/index.ts
@@ -344,7 +344,7 @@ export async function lookupUpdates(
   }
   // Add digests if necessary
   if (supportsDigests(config)) {
-    if (config.currentDigest) {
+    if (config.currentDigest && config.datasource !== 'gitSubmodules') {
       if (!config.digestOneAndOnly || !res.updates.length) {
         // digest update
         res.updates.push({
@@ -361,6 +361,12 @@ export async function lookupUpdates(
           newValue: config.currentValue,
         });
       }
+    } else if (config.datasource === 'gitSubmodules') {
+      const dependency = clone(await getPkgReleases(config));
+      res.updates.push({
+        updateType: 'digest',
+        newValue: dependency.releases[0].version,
+      });
     }
     if (version.valueToVersion) {
       for (const update of res.updates || []) {
diff --git a/renovate-schema.json b/renovate-schema.json
index d59a344cab120d6c981bc9c5d4e4aaa10d8e067e..810042884ac68fa2705b91d37773ba74e152fb36 100644
--- a/renovate-schema.json
+++ b/renovate-schema.json
@@ -315,6 +315,7 @@
         "cargo",
         "composer",
         "docker",
+        "git",
         "hashicorp",
         "hex",
         "ivy",
@@ -1121,6 +1122,15 @@
       },
       "$ref": "#"
     },
+    "git-submodules": {
+      "description": "Configuration object for git submodule files",
+      "type": "object",
+      "default": {
+        "versionScheme": "git",
+        "fileMatch": ["(^|/).gitmodules$"]
+      },
+      "$ref": "#"
+    },
     "php": {
       "description": "Configuration object for php",
       "type": "object",
diff --git a/test/config/__snapshots__/validation.spec.ts.snap b/test/config/__snapshots__/validation.spec.ts.snap
index 478ea19788835a07c67898f7184f618211aaf82e..783f7c42b88c23aea78497e05be98996e5a19379 100644
--- a/test/config/__snapshots__/validation.spec.ts.snap
+++ b/test/config/__snapshots__/validation.spec.ts.snap
@@ -87,7 +87,7 @@ Array [
     "depName": "Configuration Error",
     "message": "packageRules:
         You have included an unsupported manager in a package rule. Your list: foo.
-        Supported managers are: (ansible, bazel, buildkite, bundler, cargo, circleci, composer, deps-edn, docker-compose, dockerfile, droneci, github-actions, gitlabci, gitlabci-include, gomod, gradle, gradle-wrapper, helm-requirements, homebrew, kubernetes, leiningen, maven, meteor, mix, npm, nuget, nvm, pip_requirements, pip_setup, pipenv, poetry, pub, sbt, swift, terraform, travis, ruby-version).",
+        Supported managers are: (ansible, bazel, buildkite, bundler, cargo, circleci, composer, deps-edn, docker-compose, dockerfile, droneci, git-submodules, github-actions, gitlabci, gitlabci-include, gomod, gradle, gradle-wrapper, helm-requirements, homebrew, kubernetes, leiningen, maven, meteor, mix, npm, nuget, nvm, pip_requirements, pip_setup, pipenv, poetry, pub, sbt, swift, terraform, travis, ruby-version).",
   },
 ]
 `;
diff --git a/test/datasource/git-submodules.spec.ts b/test/datasource/git-submodules.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..65d070c2d1993f26bd913cc3f6ab9bc77e239039
--- /dev/null
+++ b/test/datasource/git-submodules.spec.ts
@@ -0,0 +1,57 @@
+import _simpleGit from 'simple-git/promise';
+import { getPkgReleases, getDigest } from '../../lib/datasource/git-submodules';
+
+jest.mock('simple-git/promise.js');
+const simpleGit: any = _simpleGit;
+
+const lookupName = 'https://github.com/example/example.git';
+const registryUrls = [lookupName, 'master'];
+
+describe('datasource/git-submoduless', () => {
+  beforeEach(() => global.renovateCache.rmAll());
+  describe('getPkgReleases', () => {
+    it('returns null if response is wrong', async () => {
+      simpleGit.mockReturnValue({
+        listRemote() {
+          return Promise.resolve(null);
+        },
+      });
+      const versions = await getPkgReleases({ lookupName, registryUrls });
+      expect(versions).toEqual(null);
+    });
+    it('returns null if remote call throws exception', async () => {
+      simpleGit.mockReturnValue({
+        listRemote() {
+          throw new Error();
+        },
+      });
+      const versions = await getPkgReleases({ lookupName, registryUrls });
+      expect(versions).toEqual(null);
+    });
+    it('returns versions filtered from tags', async () => {
+      simpleGit.mockReturnValue({
+        listRemote() {
+          return Promise.resolve('commithash1\trefs/heads/master');
+        },
+      });
+
+      const versions = await getPkgReleases({
+        lookupName,
+        registryUrls,
+      });
+      const result = versions.releases.map(x => x.version).sort();
+      expect(result).toEqual(['commithash1']);
+    });
+  });
+  describe('getDigest', () => {
+    it('returns null if passed null', async () => {
+      const digest = await getDigest({}, null);
+      expect(digest).toBeNull();
+    });
+    it('returns value if passed value', async () => {
+      const commitHash = 'commithash1';
+      const digest = await getDigest({}, commitHash);
+      expect(digest).toEqual(commitHash);
+    });
+  });
+});
diff --git a/test/manager/git-submodules/__snapshots__/artifact.spec.ts.snap b/test/manager/git-submodules/__snapshots__/artifact.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..e1858ffeaf52ba3a4e7383e0f0b62f8863d68cd1
--- /dev/null
+++ b/test/manager/git-submodules/__snapshots__/artifact.spec.ts.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib/manager/gitsubmodules/artifacts updateArtifacts() returns empty content 1`] = `
+Array [
+  Object {
+    "file": Object {
+      "contents": "",
+      "name": "",
+    },
+  },
+]
+`;
diff --git a/test/manager/git-submodules/__snapshots__/extract.spec.ts.snap b/test/manager/git-submodules/__snapshots__/extract.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..208537bf1dea79b0be8c836728e7f1abce407d08
--- /dev/null
+++ b/test/manager/git-submodules/__snapshots__/extract.spec.ts.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib/manager/gitsubmodules/extract extractPackageFile() default to master branch 1`] = `
+Array [
+  Object {
+    "currentDigest": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+    "currentValue": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+    "depName": "PowerShell-Docs",
+    "registryUrls": Array [
+      "git@github.com:PowerShell/PowerShell-Docs",
+      "master",
+    ],
+  },
+]
+`;
+
+exports[`lib/manager/gitsubmodules/extract extractPackageFile() extract branch 1`] = `
+Array [
+  Object {
+    "currentDigest": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+    "currentValue": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+    "depName": "PowerShell-Docs",
+    "registryUrls": Array [
+      "git@github.com:PowerShell/PowerShell-Docs",
+      "staging",
+    ],
+  },
+]
+`;
+
+exports[`lib/manager/gitsubmodules/extract extractPackageFile() extract relative URL 1`] = `
+Array [
+  Object {
+    "currentDigest": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+    "currentValue": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+    "depName": "PowerShell-Docs",
+    "registryUrls": Array [
+      "https://github.com/PowerShell/PowerShell-Docs",
+      "staging",
+    ],
+  },
+]
+`;
diff --git a/test/manager/git-submodules/_fixtures/.gitmodules.1 b/test/manager/git-submodules/_fixtures/.gitmodules.1
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/manager/git-submodules/_fixtures/.gitmodules.2 b/test/manager/git-submodules/_fixtures/.gitmodules.2
new file mode 100644
index 0000000000000000000000000000000000000000..8ef96ca17da5b3ac2d90013bc8b912979f01921c
--- /dev/null
+++ b/test/manager/git-submodules/_fixtures/.gitmodules.2
@@ -0,0 +1,3 @@
+[submodule "PowerShell-Docs"]
+	path = PowerShell-Docs
+	url = git@github.com:PowerShell/PowerShell-Docs
\ No newline at end of file
diff --git a/test/manager/git-submodules/_fixtures/.gitmodules.3 b/test/manager/git-submodules/_fixtures/.gitmodules.3
new file mode 100644
index 0000000000000000000000000000000000000000..3af0ee1449e1c70c5fbc1e121083e96cc5a69552
--- /dev/null
+++ b/test/manager/git-submodules/_fixtures/.gitmodules.3
@@ -0,0 +1,4 @@
+[submodule "PowerShell-Docs"]
+	path = PowerShell-Docs
+	url = git@github.com:PowerShell/PowerShell-Docs
+	branch = staging
\ No newline at end of file
diff --git a/test/manager/git-submodules/_fixtures/.gitmodules.4 b/test/manager/git-submodules/_fixtures/.gitmodules.4
new file mode 100644
index 0000000000000000000000000000000000000000..6ad8493e12e9680bc0a217079e5b7460a21138a7
--- /dev/null
+++ b/test/manager/git-submodules/_fixtures/.gitmodules.4
@@ -0,0 +1,4 @@
+[submodule "PowerShell-Docs"]
+	path = PowerShell-Docs
+	url = ../../PowerShell/PowerShell-Docs
+	branch = staging
diff --git a/test/manager/git-submodules/artifact.spec.ts b/test/manager/git-submodules/artifact.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4c845d40ff56553748771379086d2019d14f89df
--- /dev/null
+++ b/test/manager/git-submodules/artifact.spec.ts
@@ -0,0 +1,9 @@
+import updateArtifacts from '../../../lib/manager/git-submodules/artifacts';
+
+describe('lib/manager/gitsubmodules/artifacts', () => {
+  describe('updateArtifacts()', () => {
+    it('returns empty content', () => {
+      expect(updateArtifacts('', [''], '', {})).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/manager/git-submodules/extract.spec.ts b/test/manager/git-submodules/extract.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..acc882241d7081f5af021f54195b361e1c010c9e
--- /dev/null
+++ b/test/manager/git-submodules/extract.spec.ts
@@ -0,0 +1,46 @@
+import _simpleGit from 'simple-git/promise';
+import extractPackageFile from '../../../lib/manager/git-submodules/extract';
+
+jest.mock('simple-git/promise.js');
+const simpleGit: any = _simpleGit;
+const Git = jest.requireActual('simple-git/promise');
+
+const localDir = `${__dirname}/_fixtures`;
+
+describe('lib/manager/gitsubmodules/extract', () => {
+  beforeAll(() => {
+    simpleGit.mockReturnValue({
+      subModule() {
+        return Promise.resolve('4b825dc642cb6eb9a060e54bf8d69288fbee4904');
+      },
+      raw(options: string[]) {
+        if (options.includes('remote.origin.url')) {
+          return 'https://github.com/renovatebot/renovate.git';
+        }
+        return Git().raw(options);
+      },
+    });
+  });
+  describe('extractPackageFile()', () => {
+    it('handles empty gitmodules file', async () => {
+      expect(
+        await extractPackageFile('', '.gitmodules.1', { localDir })
+      ).toBeNull();
+    });
+    it('default to master branch', async () => {
+      const res = await extractPackageFile('', '.gitmodules.2', { localDir });
+      expect(res.deps).toMatchSnapshot();
+      expect(res.deps).toHaveLength(1);
+    });
+    it('extract branch', async () => {
+      const res = await extractPackageFile('', '.gitmodules.3', { localDir });
+      expect(res.deps).toMatchSnapshot();
+      expect(res.deps).toHaveLength(1);
+    });
+    it('extract relative URL', async () => {
+      const res = await extractPackageFile('', '.gitmodules.4', { localDir });
+      expect(res.deps).toMatchSnapshot();
+      expect(res.deps).toHaveLength(1);
+    });
+  });
+});
diff --git a/test/manager/git-submodules/update.spec.ts b/test/manager/git-submodules/update.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..60eee16fc0c13c309a281740b543317c1cb55a11
--- /dev/null
+++ b/test/manager/git-submodules/update.spec.ts
@@ -0,0 +1,31 @@
+import _simpleGit from 'simple-git/promise';
+import { dir } from 'tmp-promise';
+
+import updateDependency from '../../../lib/manager/git-submodules/update';
+
+jest.mock('simple-git/promise.js');
+const simpleGit: any = _simpleGit;
+
+describe('manager/git-submodules/update', () => {
+  describe('updateDependency', () => {
+    it('returns null on error', async () => {
+      simpleGit.mockReturnValue({
+        raw() {
+          throw new Error();
+        },
+      });
+      const update = await updateDependency('', {});
+      expect(update).toBeNull();
+    });
+    it('returns content on update', async () => {
+      const tmpDir = await dir();
+      simpleGit.mockReturnValue({
+        raw() {
+          return Promise.resolve();
+        },
+      });
+      const update = await updateDependency('', { localDir: tmpDir.path });
+      expect(update).toEqual('');
+    });
+  });
+});
diff --git a/test/platform/git/storage.spec.ts b/test/platform/git/storage.spec.ts
index d8ad8dde3b9eee63278fd22ae920fc2efaea32f0..018b73f13c64a0ee8f21409847bb40b17125b187 100644
--- a/test/platform/git/storage.spec.ts
+++ b/test/platform/git/storage.spec.ts
@@ -250,6 +250,19 @@ describe('platform/git/storage', () => {
         'Update something'
       );
     });
+    it('updates git submodules', async () => {
+      const files = [
+        {
+          name: '.',
+          contents: 'some content',
+        },
+      ];
+      await git.commitFilesToBranch(
+        'renovate/something',
+        files,
+        'Update something'
+      );
+    });
   });
 
   describe('getCommitMessages()', () => {
diff --git a/test/versioning/git.spec.ts b/test/versioning/git.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..21c4318b01ca4b452c311fd6992bafd5c280c6bf
--- /dev/null
+++ b/test/versioning/git.spec.ts
@@ -0,0 +1,24 @@
+import git from '../../lib/versioning/git';
+
+describe('git.', () => {
+  describe('isValid(version)', () => {
+    it('should return true', () => {
+      expect(git.isValid('a1')).toBeTruthy();
+    });
+  });
+  describe('isCompatible(version)', () => {
+    it('should return true', () => {
+      expect(git.isCompatible('')).toBeTruthy();
+    });
+  });
+  describe('isGreaterThan(version1, version2)', () => {
+    it('should return false', () => {
+      expect(git.isGreaterThan('', '')).toBeFalsy();
+    });
+  });
+  describe('valueToVersion(version)', () => {
+    it('should return same as input', () => {
+      expect(git.valueToVersion('')).toEqual('');
+    });
+  });
+});
diff --git a/test/workers/branch/__snapshots__/get-updated.spec.js.snap b/test/workers/branch/__snapshots__/get-updated.spec.js.snap
index 0b5271543dc9f668eef5634cb50ddeb30f6fe829..c61fc9a70a7f55ea73ab7fe3624755231028efd2 100644
--- a/test/workers/branch/__snapshots__/get-updated.spec.js.snap
+++ b/test/workers/branch/__snapshots__/get-updated.spec.js.snap
@@ -23,6 +23,20 @@ Object {
 }
 `;
 
+exports[`workers/branch/get-updated getUpdatedPackageFiles() handles git submodules 1`] = `
+Object {
+  "artifactErrors": Array [],
+  "parentBranch": undefined,
+  "updatedArtifacts": Array [],
+  "updatedPackageFiles": Array [
+    Object {
+      "contents": "existing content",
+      "name": "undefined",
+    },
+  ],
+}
+`;
+
 exports[`workers/branch/get-updated getUpdatedPackageFiles() handles lock file errors 1`] = `
 Object {
   "artifactErrors": Array [
diff --git a/test/workers/branch/get-updated.spec.js b/test/workers/branch/get-updated.spec.js
index eb7fccbf3a6dc7b3eb585f25837f32d3e7735139..953c711b0a1b5e292aeb3e3d20514e74f0c6c3e2 100644
--- a/test/workers/branch/get-updated.spec.js
+++ b/test/workers/branch/get-updated.spec.js
@@ -2,6 +2,8 @@
 const composer = require('../../../lib/manager/composer');
 /** @type any */
 const npm = require('../../../lib/manager/npm');
+/** @type any */
+const gitSubmodules = require('../../../lib/manager/git-submodules');
 const {
   getUpdatedPackageFiles,
 } = require('../../../lib/workers/branch/get-updated');
@@ -11,6 +13,7 @@ const { platform } = require('../../../lib/platform');
 
 jest.mock('../../../lib/manager/composer');
 jest.mock('../../../lib/manager/npm');
+jest.mock('../../../lib/manager/git-submodules');
 
 describe('workers/branch/get-updated', () => {
   describe('getUpdatedPackageFiles()', () => {
@@ -111,5 +114,14 @@ describe('workers/branch/get-updated', () => {
       const res = await getUpdatedPackageFiles(config);
       expect(res).toMatchSnapshot();
     });
+    it('handles git submodules', async () => {
+      config.upgrades.push({
+        manager: 'git-submodules',
+        datasource: 'gitSubmodules',
+      });
+      gitSubmodules.updateDependency.mockReturnValue('existing content');
+      const res = await getUpdatedPackageFiles(config);
+      expect(res).toMatchSnapshot();
+    });
   });
 });
diff --git a/test/workers/repository/extract/__snapshots__/index.spec.js.snap b/test/workers/repository/extract/__snapshots__/index.spec.js.snap
index 1208fdc092d78f36d23ee0f3829654a4ff22b11d..ae5aa2627b22607a9a07bd238b9ee61cec0f1789 100644
--- a/test/workers/repository/extract/__snapshots__/index.spec.js.snap
+++ b/test/workers/repository/extract/__snapshots__/index.spec.js.snap
@@ -35,6 +35,9 @@ Object {
   "droneci": Array [
     Object {},
   ],
+  "git-submodules": Array [
+    Object {},
+  ],
   "github-actions": Array [
     Object {},
   ],
diff --git a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap
index 8f74d40bb37a94bb78bedd916ad35624bce066c1..f719f0e95368b264bdd5cf0e20c4eedc22df806e 100644
--- a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap
+++ b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap
@@ -178,6 +178,22 @@ Object {
 }
 `;
 
+exports[`workers/repository/process/lookup .lookupUpdates() handles git submodule update 1`] = `
+Object {
+  "skipReason": "unsupported-value",
+  "updates": Array [
+    Object {
+      "fromVersion": undefined,
+      "newValue": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+      "newVersion": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+      "toVersion": undefined,
+      "updateType": "digest",
+    },
+  ],
+  "warnings": Array [],
+}
+`;
+
 exports[`workers/repository/process/lookup .lookupUpdates() handles github 404 1`] = `Array []`;
 
 exports[`workers/repository/process/lookup .lookupUpdates() handles packagist 1`] = `Array []`;
diff --git a/test/workers/repository/process/lookup/index.spec.js b/test/workers/repository/process/lookup/index.spec.js
index 4ea1b5b86e1c59188d32c44c59668380e5e19e1c..a215651fd1983de6393d9b0e126eccaa61c737c4 100644
--- a/test/workers/repository/process/lookup/index.spec.js
+++ b/test/workers/repository/process/lookup/index.spec.js
@@ -9,9 +9,12 @@ const vueJson = require('../../../../config/npm/_fixtures/vue.json');
 const typescriptJson = require('../../../../config/npm/_fixtures/typescript.json');
 /** @type any */
 const docker = require('../../../../../lib/datasource/docker');
+/** @type any */
+const gitSubmodules = require('../../../../../lib/datasource/git-submodules');
 const defaults = require('../../../../../lib/config/defaults');
 
 jest.mock('../../../../../lib/datasource/docker');
+jest.mock('../../../../../lib/datasource/git-submodules');
 
 qJson.latestVersion = '1.4.1';
 
@@ -1227,5 +1230,18 @@ describe('workers/repository/process/lookup', () => {
       const res = await lookup.lookupUpdates(config);
       expect(res).toMatchSnapshot();
     });
+    it('handles git submodule update', async () => {
+      config.datasource = 'gitSubmodules';
+      config.versionScheme = 'git';
+      gitSubmodules.getPkgReleases.mockReturnValueOnce({
+        releases: [
+          {
+            version: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+          },
+        ],
+      });
+      const res = await lookup.lookupUpdates(config);
+      expect(res).toMatchSnapshot();
+    });
   });
 });