diff --git a/__tests__/unit/lib/adapter/GitAdapter.test.ts b/__tests__/unit/lib/adapter/GitAdapter.test.ts index 5529b45c..c954a0a5 100644 --- a/__tests__/unit/lib/adapter/GitAdapter.test.ts +++ b/__tests__/unit/lib/adapter/GitAdapter.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, jest } from '@jest/globals' import { readFile } from 'fs-extra' +import { EOL } from 'os' import GitAdapter from '../../../../src/adapter/GitAdapter' import { IGNORE_WHITESPACE_PARAMS } from '../../../../src/constant/gitConstants' import type { Config } from '../../../../src/types/config' @@ -142,6 +143,23 @@ describe('GitAdapter', () => { } ) }) + describe('when called multiple times with the same parameters', () => { + it('returns cached value', async () => { + // Arrange + const gitAdapter = GitAdapter.getInstance(config) + mockedCatFile.mockResolvedValue('blob' as never) + + // Act + const result = await gitAdapter.pathExists('path') + const cachedResult = await gitAdapter.pathExists('path') + + // Assert + expect(result).toBe(true) + expect(cachedResult).toStrictEqual(result) + expect(mockedCatFile).toBeCalledTimes(1) + expect(mockedCatFile).toBeCalledWith(['-t', `${config.to}:path`]) + }) + }) describe('when catFile throws', () => { it('returns false', async () => { // Arrange @@ -256,6 +274,60 @@ describe('GitAdapter', () => { config.source, ]) }) + + it('memoize call', async () => { + // Arrange + const gitAdapter = GitAdapter.getInstance(config) + const rawOutput = [ + 'path/from/file', + 'path/to/file', + 'path/to/another/file', + ] + mockedRaw.mockResolvedValue(rawOutput.join(EOL) as never) + + // Act + const result = await gitAdapter.getFilesPath(config.source) + const cachedResult = await gitAdapter.getFilesPath(config.source) + + // Assert + expect(result).toEqual(rawOutput) + expect(cachedResult).toStrictEqual(result) + expect(mockedRaw).toBeCalledTimes(1) + expect(mockedRaw).toBeCalledWith([ + 'ls-tree', + '--name-only', + '-r', + config.to, + config.source, + ]) + }) + + it('memoize sub call', async () => { + // Arrange + const gitAdapter = GitAdapter.getInstance(config) + const rawOutput = [ + 'path/from/file', + 'path/to/file', + 'path/to/another/file', + ] + mockedRaw.mockResolvedValue(rawOutput.join(EOL) as never) + + // Act + const result = await gitAdapter.getFilesPath('path') + const subCachedResult = await gitAdapter.getFilesPath('path/to') + + // Assert + expect(result).toEqual(rawOutput) + expect(subCachedResult).toEqual(rawOutput.slice(1)) + expect(mockedRaw).toBeCalledTimes(1) + expect(mockedRaw).toBeCalledWith([ + 'ls-tree', + '--name-only', + '-r', + config.to, + 'path', + ]) + }) }) describe('getFilesFrom', () => { diff --git a/__tests__/unit/lib/utils/fsHelper.test.ts b/__tests__/unit/lib/utils/fsHelper.test.ts index d56aaaff..34ff6abb 100644 --- a/__tests__/unit/lib/utils/fsHelper.test.ts +++ b/__tests__/unit/lib/utils/fsHelper.test.ts @@ -322,7 +322,7 @@ describe('pathExists', () => { mockPathExists.mockImplementation(() => Promise.resolve(false)) // Act - const result = await pathExists('path', work.config) + const result = await pathExists('not/existing/path', work.config) // Assert expect(result).toBe(false) diff --git a/src/adapter/GitAdapter.ts b/src/adapter/GitAdapter.ts index 35e529b9..a47b5946 100644 --- a/src/adapter/GitAdapter.ts +++ b/src/adapter/GitAdapter.ts @@ -3,7 +3,7 @@ import { join } from 'path/posix' import { readFile } from 'fs-extra' import { SimpleGit, simpleGit } from 'simple-git' -import { UTF8_ENCODING } from '../constant/fsConstants' +import { PATH_SEP, UTF8_ENCODING } from '../constant/fsConstants' import { ADDITION, BLOB_TYPE, @@ -22,7 +22,6 @@ import { getLFSObjectContentPath, isLFS } from '../utils/gitLfsHelper' const EOL = new RegExp(/\r?\n/) const revPath = (pathDef: FileGitRef) => `${pathDef.oid}:${pathDef.path}` - export default class GitAdapter { private static instances: Map = new Map() @@ -36,9 +35,13 @@ export default class GitAdapter { } protected readonly simpleGit: SimpleGit + protected readonly getFilesPathCache: Map> + protected readonly pathExistsCache: Map private constructor(protected readonly config: Config) { this.simpleGit = simpleGit({ baseDir: config.repo, trimmed: true }) + this.getFilesPathCache = new Map>() + this.pathExistsCache = new Map() } public async configureRepository() { @@ -50,16 +53,27 @@ export default class GitAdapter { return await this.simpleGit.revparse([ref]) } - public async pathExists(path: string) { + protected async pathExistsImpl(path: string) { + let doesPathExists = false try { const type = await this.simpleGit.catFile([ '-t', revPath({ path, oid: this.config.to }), ]) - return [TREE_TYPE, BLOB_TYPE].includes(type.trimEnd()) + doesPathExists = [TREE_TYPE, BLOB_TYPE].includes(type.trimEnd()) } catch { - return false + doesPathExists = false } + return doesPathExists + } + + public async pathExists(path: string) { + if (this.pathExistsCache.has(path)) { + return this.pathExistsCache.get(path) + } + const doesPathExists = await this.pathExistsImpl(path) + this.pathExistsCache.set(path, doesPathExists) + return doesPathExists } public async getFirstCommitRef() { @@ -81,7 +95,7 @@ export default class GitAdapter { return content.toString(UTF8_ENCODING) } - public async getFilesPath(path: string): Promise { + protected async getFilesPathImpl(path: string): Promise { return ( await this.simpleGit.raw([ 'ls-tree', @@ -96,6 +110,28 @@ export default class GitAdapter { .map(line => treatPathSep(line)) } + public async getFilesPath(path: string): Promise { + if (this.getFilesPathCache.has(path)) { + return Array.from(this.getFilesPathCache.get(path)!) + } + const filesPath = await this.getFilesPathImpl(path) + // Memoize every containing sub path for each filesPath + for (const filePath of filesPath) { + const currentPath = [] + for (const segment of filePath.split(PATH_SEP)) { + currentPath.push(segment) + const subPath = currentPath.join(PATH_SEP) + + if (!this.getFilesPathCache.has(subPath)) { + this.getFilesPathCache.set(subPath, new Set()) + } + this.getFilesPathCache.get(subPath)!.add(filePath) + } + } + this.getFilesPathCache.set(path, new Set(filesPath)) + return filesPath + } + public async *getFilesFrom(path: string) { const filesPath = await this.getFilesPath(path) for (const filePath of filesPath) {