diff --git a/lib/versioning/aws-machine-image/index.spec.ts b/lib/versioning/aws-machine-image/index.spec.ts
index e4aff0a818fdc7b655a0a8e8b877c9b8a4d9f90d..69af0932d73d161e5ef0dc6620a1ca3d0cebc6ce 100644
--- a/lib/versioning/aws-machine-image/index.spec.ts
+++ b/lib/versioning/aws-machine-image/index.spec.ts
@@ -44,7 +44,8 @@ describe('versioning/aws-machine-image/index', () => {
   });
   describe('isGreaterThan(version1, version2)', () => {
     it('should return false', () => {
-      expect(aws.isGreaterThan('', '')).toBeTruthy();
+      expect(aws.isGreaterThan('ami-00', 'ami-99')).toBeFalse();
+      expect(aws.isGreaterThan('ami-99', 'ami-00')).toBeFalse();
     });
   });
 });
diff --git a/lib/versioning/aws-machine-image/index.ts b/lib/versioning/aws-machine-image/index.ts
index 0f7b6fed1b8ec2a3b847fbc5ca14f27468d12b86..736fa54dc1938ccf4d18c8c6a7ddaf4e6adbbd0b 100644
--- a/lib/versioning/aws-machine-image/index.ts
+++ b/lib/versioning/aws-machine-image/index.ts
@@ -1,4 +1,5 @@
-import * as generic from '../loose/generic';
+import { regEx } from '../../util/regex';
+import { GenericVersion, GenericVersioningApi } from '../loose/generic';
 import type { VersioningApi } from '../types';
 
 export const id = 'aws-machine-image';
@@ -8,37 +9,25 @@ export const urls = [];
 
 export const supportsRanges = false;
 
-function parse(version: string): any {
-  return { release: [1, 0, 0] };
+const awsMachineImageRegex = regEx('^ami-(?<suffix>[a-z0-9]{17})$');
+
+class AwsMachineImageVersioningApi extends GenericVersioningApi {
+  protected _parse(version: string): GenericVersion | null {
+    if (version) {
+      const matchGroups = awsMachineImageRegex.exec(version)?.groups;
+      if (matchGroups) {
+        const { suffix } = matchGroups;
+        return { release: [1, 0, 0], suffix };
+      }
+    }
+    return null;
+  }
+
+  protected override _compare(_version: string, _other: string): number {
+    return 0;
+  }
 }
 
-function compare(version1: string, version2: string): number {
-  return 1;
-}
-
-function isValid(input: string): string | boolean | null {
-  return typeof input === 'string' && /^ami-[a-z0-9]{17}$/.test(input);
-}
-
-function isVersion(input: string): string | boolean | null {
-  return isValid(input);
-}
-
-function isCompatible(
-  version: string,
-  _range?: string
-): string | boolean | null {
-  return isValid(version);
-}
-
-export const api: VersioningApi = {
-  ...generic.create({
-    parse,
-    compare,
-  }),
-  isValid,
-  isVersion,
-  isCompatible,
-};
+export const api: VersioningApi = new AwsMachineImageVersioningApi();
 
 export default api;
diff --git a/lib/versioning/docker/index.spec.ts b/lib/versioning/docker/index.spec.ts
index d9284a79c954842823e3633ee698c0ff21567a10..f49a9cb9aedb791af4a6424506f38bdaea0f3dd5 100644
--- a/lib/versioning/docker/index.spec.ts
+++ b/lib/versioning/docker/index.spec.ts
@@ -131,7 +131,7 @@ describe('versioning/docker/index', () => {
         '3.8.0',
       ];
 
-      expect(versions.sort(docker.sortVersions)).toEqual([
+      expect(versions.sort((x, y) => docker.sortVersions(x, y))).toEqual([
         '3.7.0b1',
         '3.7.0b5',
         '3.7.0',
diff --git a/lib/versioning/docker/index.ts b/lib/versioning/docker/index.ts
index 1ec88bb36ed330f993b69f373795d997160e1f12..909ede9a8ec083a00f4feed481f128f70f75506b 100644
--- a/lib/versioning/docker/index.ts
+++ b/lib/versioning/docker/index.ts
@@ -1,5 +1,5 @@
 import { regEx } from '../../util/regex';
-import * as generic from '../loose/generic';
+import { GenericVersion, GenericVersioningApi } from '../loose/generic';
 import type { VersioningApi } from '../types';
 
 export const id = 'docker';
@@ -13,81 +13,79 @@ const versionPattern = regEx(/^(?<version>\d+(?:\.\d+)*)(?<prerelease>.*)$/);
 const commitHashPattern = regEx(/^[a-f0-9]{7,40}$/);
 const numericPattern = regEx(/^[0-9]+$/);
 
-function parse(version: string): generic.GenericVersion {
-  if (commitHashPattern.test(version) && !numericPattern.test(version)) {
-    return null;
-  }
-  const versionPieces = version.replace(regEx(/^v/), '').split('-');
-  const prefix = versionPieces.shift();
-  const suffix = versionPieces.join('-');
-  const m = versionPattern.exec(prefix);
-  if (!m?.groups) {
-    return null;
-  }
-
-  const { version: ver, prerelease } = m.groups;
-  const release = ver.split('.').map(Number);
-  return { release, suffix, prerelease };
-}
-
-function valueToVersion(value: string): string {
-  // Remove any suffix after '-', e.g. '-alpine'
-  return value ? value.split('-')[0] : value;
-}
-
-function compare(version1: string, version2: string): number {
-  const parsed1 = parse(version1);
-  const parsed2 = parse(version2);
-  // istanbul ignore if
-  if (!(parsed1 && parsed2)) {
-    return 1;
-  }
-  const length = Math.max(parsed1.release.length, parsed2.release.length);
-  for (let i = 0; i < length; i += 1) {
-    const part1 = parsed1.release[i];
-    const part2 = parsed2.release[i];
-    // shorter is bigger 2.1 > 2.1.1
-    if (part1 === undefined) {
-      return 1;
+class DockerVersioningApi extends GenericVersioningApi {
+  protected _parse(version: string): GenericVersion | null {
+    if (!version) {
+      return null;
     }
-    if (part2 === undefined) {
-      return -1;
+    if (commitHashPattern.test(version) && !numericPattern.test(version)) {
+      return null;
     }
-    if (part1 !== part2) {
-      return part1 - part2;
+    const versionPieces = version.replace(regEx(/^v/), '').split('-');
+    const prefix = versionPieces.shift();
+    const suffix = versionPieces.join('-');
+    const m = versionPattern.exec(prefix);
+    if (!m?.groups) {
+      return null;
     }
+
+    const { version: ver, prerelease } = m.groups;
+    const release = ver.split('.').map(Number);
+    return { release, suffix, prerelease };
   }
-  if (parsed1.prerelease !== parsed2.prerelease) {
-    // unstable is lower
-    if (!parsed1.prerelease && parsed2.prerelease) {
+
+  protected override _compare(version: string, other: string): number {
+    const parsed1 = this._parse(version);
+    const parsed2 = this._parse(other);
+    // istanbul ignore if
+    if (!(parsed1 && parsed2)) {
       return 1;
     }
-    if (parsed1.prerelease && !parsed2.prerelease) {
-      return -1;
+    const length = Math.max(parsed1.release.length, parsed2.release.length);
+    for (let i = 0; i < length; i += 1) {
+      const part1 = parsed1.release[i];
+      const part2 = parsed2.release[i];
+      // shorter is bigger 2.1 > 2.1.1
+      if (part1 === undefined) {
+        return 1;
+      }
+      if (part2 === undefined) {
+        return -1;
+      }
+      if (part1 !== part2) {
+        return part1 - part2;
+      }
+    }
+    if (parsed1.prerelease !== parsed2.prerelease) {
+      // unstable is lower
+      if (!parsed1.prerelease && parsed2.prerelease) {
+        return 1;
+      }
+      if (parsed1.prerelease && !parsed2.prerelease) {
+        return -1;
+      }
+      // alphabetic order
+      return parsed1.prerelease.localeCompare(parsed2.prerelease);
     }
-    // alphabetic order
-    return parsed1.prerelease.localeCompare(parsed2.prerelease);
+    // equals
+    return parsed2.suffix.localeCompare(parsed1.suffix);
+  }
+
+  override isCompatible(version: string, range: string): boolean {
+    const parsed1 = this._parse(version);
+    const parsed2 = this._parse(range);
+    return (
+      parsed1.suffix === parsed2.suffix &&
+      parsed1.release.length === parsed2.release.length
+    );
   }
-  // equals
-  return parsed2.suffix.localeCompare(parsed1.suffix);
-}
 
-function isCompatible(version: string, range: string): boolean {
-  const parsed1 = parse(version);
-  const parsed2 = parse(range);
-  return (
-    parsed1.suffix === parsed2.suffix &&
-    parsed1.release.length === parsed2.release.length
-  );
+  valueToVersion(value: string): string {
+    // Remove any suffix after '-', e.g. '-alpine'
+    return value ? value.split('-')[0] : value;
+  }
 }
 
-export const api: VersioningApi = {
-  ...generic.create({
-    parse,
-    compare,
-  }),
-  isCompatible,
-  valueToVersion,
-};
+export const api: VersioningApi = new DockerVersioningApi();
 
 export default api;
diff --git a/lib/versioning/git/index.spec.ts b/lib/versioning/git/index.spec.ts
index 53b5b8b35c9e4a5b13e37e850db61a92bcdba988..5230eb7932fa9421e36492bbe8417c98607e395d 100644
--- a/lib/versioning/git/index.spec.ts
+++ b/lib/versioning/git/index.spec.ts
@@ -1,24 +1,40 @@
 import git from '.';
 
 describe('versioning/git/index', () => {
-  describe('isValid(version)', () => {
-    it('should return true', () => {
-      expect(git.isValid('a1')).toBeTruthy();
-    });
+  test.each`
+    input                                         | expected
+    ${''}                                         | ${false}
+    ${'2'}                                        | ${false}
+    ${'29'}                                       | ${false}
+    ${'29c'}                                      | ${false}
+    ${'29c7'}                                     | ${false}
+    ${'29c79'}                                    | ${false}
+    ${'29c792'}                                   | ${false}
+    ${'29c7921'}                                  | ${true}
+    ${'29c792109259545157f4bc3f8d43f47ffcf34e20'} | ${true}
+    ${'foobar'}                                   | ${false}
+  `('isValid("$input") === $expected', ({ input, expected }) => {
+    expect(git.isValid(input)).toBe(expected);
   });
-  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('')).toBe('');
-    });
+
+  test.each`
+    version               | range | expected
+    ${''}                 | ${''} | ${false}
+    ${'1234567890aBcDeF'} | ${''} | ${true}
+  `(
+    'isCompatible("$version") === $expected',
+    ({ version, range, expected }) => {
+      const res = git.isCompatible(version, range);
+      expect(!!res).toBe(expected);
+    }
+  );
+
+  test.each`
+    a        | b        | expected
+    ${''}    | ${''}    | ${false}
+    ${'abc'} | ${'bca'} | ${false}
+    ${'123'} | ${'321'} | ${false}
+  `('isGreaterThan("$a", "$b") === $expected', ({ a, b, expected }) => {
+    expect(git.isGreaterThan(a, b)).toBe(expected);
   });
 });
diff --git a/lib/versioning/git/index.ts b/lib/versioning/git/index.ts
index 3cf9cfcc187cb7fdd5ba1a6e8ab8bd0bcb636867..3c2e50991732d6eb634d17a0145ac5c797227493 100644
--- a/lib/versioning/git/index.ts
+++ b/lib/versioning/git/index.ts
@@ -1,4 +1,5 @@
-import * as generic from '../loose/generic';
+import { regEx } from '../../util/regex';
+import { GenericVersion, GenericVersioningApi } from '../loose/generic';
 import type { VersioningApi } from '../types';
 
 export const id = 'git';
@@ -6,21 +7,21 @@ export const displayName = 'git';
 export const urls = ['https://git-scm.com/'];
 export const supportsRanges = false;
 
-const parse = (version: string): any => ({ release: [parseInt(version, 10)] });
+const regex = regEx('^[0-9a-f]{7,40}$', 'i');
 
-const isCompatible = (version: string, range: string): boolean => true;
+class GitVersioningApi extends GenericVersioningApi {
+  protected _parse(version: string): GenericVersion | null {
+    if (version?.match(regex)) {
+      return { release: [1, 0, 0], suffix: version };
+    }
+    return null;
+  }
 
-const compare = (version1: string, version2: string): number => -1;
+  protected override _compare(_version: string, _other: string): number {
+    return -1;
+  }
+}
 
-const valueToVersion = (value: string): string => value;
-
-export const api: VersioningApi = {
-  ...generic.create({
-    parse,
-    compare,
-  }),
-  isCompatible,
-  valueToVersion,
-};
+export const api: VersioningApi = new GitVersioningApi();
 
 export default api;
diff --git a/lib/versioning/loose/generic.ts b/lib/versioning/loose/generic.ts
index a57acac54ced7bcff823bbc5e512823a8f2b8fd4..cf8d49bbed7fd7246d2630760a224296f9bb2a4c 100644
--- a/lib/versioning/loose/generic.ts
+++ b/lib/versioning/loose/generic.ts
@@ -15,119 +15,6 @@ export interface VersionComparator {
   (version: string, other: string): number;
 }
 
-// since this file was meant for no range support, a range = version
-// parse should return null if version not valid
-// parse should return an object with property release, an array of version sections major.minor.patch
-export const parser = (parse: VersionParser): Partial<VersioningApi> => {
-  function isValid(version: string): string {
-    if (!version) {
-      return null;
-    }
-    const parsed = parse(version);
-    return parsed ? version : null;
-  }
-  function getSection(version: string, index: number): number {
-    const parsed = parse(version);
-    return parsed && parsed.release.length > index
-      ? parsed.release[index]
-      : null;
-  }
-  function getMajor(version: string): number {
-    return getSection(version, 0);
-  }
-  function getMinor(version: string): number {
-    return getSection(version, 1);
-  }
-  function getPatch(version: string): number {
-    return getSection(version, 2);
-  }
-
-  function isStable(version: string): boolean {
-    const parsed = parse(version);
-    return parsed && !parsed.prerelease;
-  }
-
-  return {
-    // validation
-    isCompatible: isValid,
-    isSingleVersion: isValid,
-    isStable,
-    isValid,
-    isVersion: isValid,
-    // digestion of version
-    getMajor,
-    getMinor,
-    getPatch,
-  };
-};
-
-// this is the main reason this file was created
-// most operations below could be derived from a compare function
-export const comparer = (
-  compare: VersionComparator
-): Partial<VersioningApi> => {
-  function equals(version: string, other: string): boolean {
-    return compare(version, other) === 0;
-  }
-
-  function isGreaterThan(version: string, other: string): boolean {
-    return compare(version, other) > 0;
-  }
-  function isLessThanRange(version: string, range: string): boolean {
-    return compare(version, range) < 0;
-  }
-
-  // we don't not have ranges, so versions has to be equal
-  function getSatisfyingVersion(versions: string[], range: string): string {
-    return versions.find((v) => equals(v, range)) || null;
-  }
-  function minSatisfyingVersion(versions: string[], range: string): string {
-    return versions.find((v) => equals(v, range)) || null;
-  }
-  function getNewValue(newValueConfig: NewValueConfig): string {
-    const { newVersion } = newValueConfig || {};
-    return newVersion;
-  }
-  function sortVersions(version: string, other: string): number {
-    return compare(version, other);
-  }
-
-  return {
-    equals,
-    isGreaterThan,
-    isLessThanRange,
-    matches: equals,
-    getSatisfyingVersion,
-    minSatisfyingVersion,
-    getNewValue,
-    sortVersions,
-  };
-};
-
-/**
- * helper functions to ease create other versioning schemas with little code
- * especially if those schemas do not support ranges
- * @deprecated Use `GenericVersioningApi` instead
- * @param param0 object with parse and optional compare function
- * @returns
- */
-export const create = ({
-  parse,
-  compare,
-}: {
-  parse: VersionParser;
-  compare: VersionComparator;
-}): VersioningApi => {
-  let schema: VersioningApi = {} as any;
-  if (parse) {
-    schema = { ...schema, ...parser(parse) };
-  }
-  if (compare) {
-    schema = { ...schema, ...comparer(compare) };
-  }
-  return schema;
-};
-
 export abstract class GenericVersioningApi<
   T extends GenericVersion = GenericVersion
 > implements VersioningApi
diff --git a/lib/versioning/loose/index.ts b/lib/versioning/loose/index.ts
index 32cba9301f3fb086570fdd755e3075d893b2004f..39d27d631b77f7253de73e4cecb87c26fa249bc2 100644
--- a/lib/versioning/loose/index.ts
+++ b/lib/versioning/loose/index.ts
@@ -1,6 +1,6 @@
 import { regEx } from '../../util/regex';
 import type { VersioningApi } from '../types';
-import * as generic from './generic';
+import { GenericVersion, GenericVersioningApi } from './generic';
 
 export const id = 'loose';
 export const displayName = 'Loose';
@@ -11,51 +11,50 @@ const versionPattern = regEx(/^v?(\d+(?:\.\d+)*)(.*)$/);
 const commitHashPattern = regEx(/^[a-f0-9]{7,40}$/);
 const numericPattern = regEx(/^[0-9]+$/);
 
-function parse(version: string): any {
-  if (commitHashPattern.test(version) && !numericPattern.test(version)) {
-    return null;
-  }
-  const matches = versionPattern.exec(version);
-  if (!matches) {
-    return null;
-  }
-  const [, prefix, suffix] = matches;
-  const release = prefix.split('.').map(Number);
-  if (release.length > 6) {
-    return null;
+class LooseVersioningApi extends GenericVersioningApi {
+  protected _parse(version: string): GenericVersion | null {
+    if (commitHashPattern.test(version) && !numericPattern.test(version)) {
+      return null;
+    }
+    const matches = versionPattern.exec(version);
+    if (!matches) {
+      return null;
+    }
+    const [, prefix, suffix] = matches;
+    const release = prefix.split('.').map(Number);
+    if (release.length > 6) {
+      return null;
+    }
+    return { release, suffix: suffix || '' };
   }
-  return { release, suffix: suffix || '' };
-}
 
-function compare(version1: string, version2: string): number {
-  const parsed1 = parse(version1);
-  const parsed2 = parse(version2);
-  // istanbul ignore if
-  if (!(parsed1 && parsed2)) {
-    return 1;
-  }
-  const length = Math.max(parsed1.release.length, parsed2.release.length);
-  for (let i = 0; i < length; i += 1) {
-    const part1 = parsed1.release[i];
-    const part2 = parsed2.release[i];
-    // shorter is smaller 2.1 < 2.1.0
-    if (part1 === undefined) {
-      return -1;
-    }
-    if (part2 === undefined) {
+  protected override _compare(version: string, other: string): number {
+    const parsed1 = this._parse(version);
+    const parsed2 = this._parse(other);
+    // istanbul ignore if
+    if (!(parsed1 && parsed2)) {
       return 1;
     }
-    if (part1 !== part2) {
-      return part1 - part2;
+    const length = Math.max(parsed1.release.length, parsed2.release.length);
+    for (let i = 0; i < length; i += 1) {
+      const part1 = parsed1.release[i];
+      const part2 = parsed2.release[i];
+      // shorter is smaller 2.1 < 2.1.0
+      if (part1 === undefined) {
+        return -1;
+      }
+      if (part2 === undefined) {
+        return 1;
+      }
+      if (part1 !== part2) {
+        return part1 - part2;
+      }
     }
+    // equals
+    return parsed1.suffix.localeCompare(parsed2.suffix);
   }
-  // equals
-  return parsed1.suffix.localeCompare(parsed2.suffix);
 }
 
-export const api: VersioningApi = generic.create({
-  parse,
-  compare,
-});
+export const api: VersioningApi = new LooseVersioningApi();
 
 export default api;
diff --git a/lib/workers/repository/process/lookup/__snapshots__/index.spec.ts.snap b/lib/workers/repository/process/lookup/__snapshots__/index.spec.ts.snap
index dd710b7fc1f2737a6f676d5d201951e1b55212ca..47c4490be5da2cfac8ba7055f0ceb701ca2aa6e8 100644
--- a/lib/workers/repository/process/lookup/__snapshots__/index.spec.ts.snap
+++ b/lib/workers/repository/process/lookup/__snapshots__/index.spec.ts.snap
@@ -175,12 +175,10 @@ Object {
 
 exports[`workers/repository/process/lookup/index .lookupUpdates() handles git submodule update 1`] = `
 Object {
-  "currentVersion": undefined,
   "updates": Array [
     Object {
       "newDigest": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
       "newValue": undefined,
-      "newVersion": undefined,
       "updateType": "digest",
     },
   ],