From a35c535bcb1e596cd24feea8c14595f1530965a7 Mon Sep 17 00:00:00 2001 From: Dan Allen Date: Wed, 20 Nov 2019 03:30:51 -0700 Subject: [PATCH] add support for git repositories that use multiple worktrees --- CHANGELOG.adoc | 1 + .../lib/aggregate-content.js | 59 ++++++++++- .../test/aggregate-content-test.js | 100 +++++++++++++++++- test/repository-builder.js | 6 +- 4 files changed, 156 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 0594320db..6d6002a43 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,6 +9,7 @@ This project utilizes semantic versioning. === Added +* *content-aggregator*: Add support for git repositories that use multiple worktrees (#535) * *content-classifier*: Add `getPages()` method to content catalog to retrieve all pages (#537) * *page-composer*: Expose public API of content catalog to UI model as `site.contentCatalog` (#328) * *page-composer*: Add built-in helpers resolvePage and resolvePageUrl to resolve arbitrary pages and their publish URLs (#328) diff --git a/packages/content-aggregator/lib/aggregate-content.js b/packages/content-aggregator/lib/aggregate-content.js index 48b326fd7..63f70da0f 100644 --- a/packages/content-aggregator/lib/aggregate-content.js +++ b/packages/content-aggregator/lib/aggregate-content.js @@ -32,6 +32,7 @@ const { GIT_OPERATION_LABEL_LENGTH, GIT_PROGRESS_PHASES, } = require('./constants') +const { S_IFREG, S_IFDIR } = fs.constants const ABBREVIATE_REF_RX = /^refs\/(?:heads|remotes\/[^/]+|tags)\// const ANY_SEPARATOR_RX = /[:/]/ @@ -140,9 +141,29 @@ async function loadRepository (url, opts) { repo = { core: GIT_CORE, dir, gitdir: dir, url, noGitSuffix: !opts.ensureGitSuffix, noCheckout: true } credentialManager = opts.credentialManager } else if (await isLocalDirectory((dir = expandPath(url, '~+', opts.startDir)))) { - repo = (await isLocalDirectory(ospath.join(dir, '.git'))) - ? { core: GIT_CORE, dir } - : { core: GIT_CORE, dir, gitdir: dir, noCheckout: true } + const dotgit = ospath.join(dir, '.git') + const dotgitType = await getFileType(dotgit) + if (dotgitType === S_IFREG) { + const worktreeGitdir = (await fs.readFile(dotgit, 'utf-8')) + .trimRight() + .split('\n') + .reduce((accum, it) => { + const rsIdx = it.indexOf(': ') + return ~rsIdx ? Object.assign({}, accum, { [it.substr(0, rsIdx)]: it.substr(rsIdx + 2) }) : accum + }, {}).gitdir + const commondirConfig = worktreeGitdir && ospath.join(worktreeGitdir, 'commondir') + if (commondirConfig && (await isLocalFile(commondirConfig))) { + const currentBranch = await git.currentBranch({ core: GIT_CORE, gitdir: worktreeGitdir }) + const commondir = (await fs.readFile(commondirConfig, 'utf-8')).trimRight() + repo = { core: GIT_CORE, dir, gitdir: ospath.join(worktreeGitdir, commondir), currentBranch } + } else { + repo = { core: GIT_CORE, dir, gitdir: dotgit } + } + } else if (dotgitType === S_IFDIR) { + repo = { core: GIT_CORE, dir, gitdir: dotgit } + } else { + repo = { core: GIT_CORE, dir, gitdir: dir, noCheckout: true } + } } else { throw new Error( `Local content source does not exist: ${dir}${url !== dir ? ' (resolved from url: ' + url + ')' : ''}` @@ -288,7 +309,7 @@ async function selectReferences (source, repo, remote) { if (!isBare) { const localBranches = await git.listBranches(repo) if (localBranches.length) { - const currentBranchName = await git.currentBranch(repo) + const currentBranchName = await getCurrentBranchName(repo) for (const name of matcher(localBranches, branchPatterns)) { refs.set(name, { name, qname: name, type: 'branch', isHead: name === currentBranchName }) } @@ -305,7 +326,9 @@ async function selectReferences (source, repo, remote) { return Array.from(refs.values()) } -function getCurrentBranchName (repo, remote) { +function getCurrentBranchName (repo, remote = undefined) { + if ('currentBranch' in repo) return repo.currentBranch + if (!remote) return git.currentBranch(repo) let refPromise if (repo.noCheckout) { refPromise = git @@ -695,6 +718,32 @@ async function resolveRemoteUrl (repo, remoteName) { }) } +/** + * Gets the file type for the specified URL if it exists on the local file system. + * + * @param {String} url - The URL to check. + * @return {int} The file type int, which can be S_IFREG, S_IFDIR, or undefined if the file does not exist. + */ +function getFileType (url) { + return fs + .stat(url) + .then((stat) => (stat.isFile() ? S_IFREG : stat.isDirectory() ? S_IFDIR : undefined)) + .catch(() => undefined) +} + +/** + * Checks whether the specified URL matches a file on the local filesystem. + * + * @param {String} url - The URL to check. + * @return {Boolean} A flag indicating whether the URL matches a file on the local filesystem. + */ +function isLocalFile (url) { + return fs + .stat(url) + .then((stat) => stat.isFile()) + .catch(() => false) +} + /** * Checks whether the specified URL matches a directory on the local filesystem. * diff --git a/packages/content-aggregator/test/aggregate-content-test.js b/packages/content-aggregator/test/aggregate-content-test.js index 727ff376e..17705720b 100644 --- a/packages/content-aggregator/test/aggregate-content-test.js +++ b/packages/content-aggregator/test/aggregate-content-test.js @@ -1092,7 +1092,7 @@ describe('aggregateContent()', function () { expect(aggregate[1]).to.include({ name: 'the-component', version: 'v3.0' }) }) - it('should use worktree for HEAD if not on branch', async () => { + it('should resolve HEAD to worktree if not on branch', async () => { const repoBuilder = new RepositoryBuilder(CONTENT_REPOS_DIR, FIXTURES_DIR) await initRepoWithBranches(repoBuilder) .then(() => repoBuilder.open()) @@ -2216,10 +2216,14 @@ describe('aggregateContent()', function () { }) describe('aggregate files from worktree', () => { - const initRepoWithFilesAndWorktree = async (repoBuilder) => { - const componentDesc = { name: 'the-component', version: 'v1.2.3' } + const initRepoWithFilesAndWorktree = async (repoBuilder, componentDesc, beforeClose) => { + if (!componentDesc) componentDesc = {} + if (!componentDesc.name) componentDesc.name = 'the-component' + if (!componentDesc.version) componentDesc.version = 'v1.2.3' + const repoName = componentDesc.repoName || componentDesc.name + delete componentDesc.repoName return repoBuilder - .init(componentDesc.name) + .init(repoName) .then(() => repoBuilder.addComponentDescriptorToWorktree(componentDesc)) .then(() => repoBuilder.addFilesFromFixture([ @@ -2230,6 +2234,7 @@ describe('aggregateContent()', function () { ]) ) .then(() => repoBuilder.copyToWorktree(['modules/ROOT/pages/page-two.adoc'], repoBuilder.fixtureBase)) + .then(() => beforeClose && beforeClose()) .then(() => repoBuilder.close()) } @@ -2257,6 +2262,93 @@ describe('aggregateContent()', function () { expect(relatives).to.have.members(expectedPaths) }) + it('on branch of alternate worktree', async () => { + const repoBuilder = new RepositoryBuilder(CONTENT_REPOS_DIR, FIXTURES_DIR) + const repoName = 'the-component' + const dir = ospath.join(repoBuilder.repoBase, repoName) + const altWorktreeRepoBuilder = new RepositoryBuilder(CONTENT_REPOS_DIR, FIXTURES_DIR) + const altWorktreeRepoName = 'the-component-alt-worktree' + const altWorktreeDir = ospath.join(altWorktreeRepoBuilder.repoBase, altWorktreeRepoName) + const altWorktreeDotgit = ospath.join(altWorktreeDir, '.git') + const altWorktreeGitdir = ospath.join(dir, '.git/worktrees/v1.2.3') + await initRepoWithFilesAndWorktree(repoBuilder, undefined, () => + repoBuilder + .addToWorktree('.git/worktrees/v1.2.3/HEAD', 'ref: refs/heads/v1.2.3\n') + .then(() => repoBuilder.addToWorktree('.git/worktrees/v1.2.3/commondir', '../..\n')) + .then(() => repoBuilder.addToWorktree('.git/worktrees/v1.2.3/gitdir', altWorktreeDotgit + '\n')) + .then(() => repoBuilder.checkoutBranch('v1.2.3')) + .then(() => repoBuilder.checkoutBranch('master')) + ) + await initRepoWithFilesAndWorktree(altWorktreeRepoBuilder, { repoName: altWorktreeRepoName }, () => + altWorktreeRepoBuilder + .addToWorktree('modules/ROOT/pages/page-three.adoc', '= Page Three\n\ncontent\n') + .then(() => fs.remove(altWorktreeRepoBuilder.repository.gitdir)) + .then(() => altWorktreeRepoBuilder.addToWorktree('.git', 'gitdir: ' + altWorktreeGitdir + '\n')) + ) + playbookSpec.content.sources.push({ url: altWorktreeRepoBuilder.url, branches: 'HEAD' }) + const aggregate = await aggregateContent(playbookSpec) + expect(aggregate).to.have.lengthOf(1) + const componentVersion = aggregate[0] + expect(componentVersion).to.include({ name: 'the-component', version: 'v1.2.3' }) + const expectedPaths = [ + 'README.adoc', + 'modules/ROOT/_attributes.adoc', + 'modules/ROOT/pages/_attributes.adoc', + 'modules/ROOT/pages/page-one.adoc', + 'modules/ROOT/pages/page-two.adoc', + 'modules/ROOT/pages/page-three.adoc', + ] + const files = aggregate[0].files + expect(files).to.have.lengthOf(expectedPaths.length) + expect(files.map((file) => file.relative)).to.have.members(expectedPaths) + const pageThree = files.find((it) => it.relative === 'modules/ROOT/pages/page-three.adoc') + expect(pageThree.src.abspath).to.equal(ospath.join(altWorktreeRepoBuilder.url, pageThree.relative)) + }) + + it('on detached HEAD of alternate worktree', async () => { + const repoBuilder = new RepositoryBuilder(CONTENT_REPOS_DIR, FIXTURES_DIR) + const repoName = 'the-component' + const dir = ospath.join(repoBuilder.repoBase, repoName) + const altWorktreeRepoBuilder = new RepositoryBuilder(CONTENT_REPOS_DIR, FIXTURES_DIR) + const altWorktreeRepoName = 'the-component-alt-worktree' + const altWorktreeDir = ospath.join(altWorktreeRepoBuilder.repoBase, altWorktreeRepoName) + const altWorktreeDotgit = ospath.join(altWorktreeDir, '.git') + const altWorktreeGitdir = ospath.join(dir, '.git/worktrees/v1.2.3') + await initRepoWithFilesAndWorktree(repoBuilder, undefined, () => + repoBuilder + .checkoutBranch('v1.2.3') + .then(() => repoBuilder.getHeadCommit()) + .then((oid) => repoBuilder.addToWorktree('.git/worktrees/v1.2.3/HEAD', oid + '\n')) + .then(() => repoBuilder.addToWorktree('.git/worktrees/v1.2.3/commondir', '../..\n')) + .then(() => repoBuilder.addToWorktree('.git/worktrees/v1.2.3/gitdir', altWorktreeDotgit + '\n')) + .then(() => repoBuilder.checkoutBranch('master')) + ) + await initRepoWithFilesAndWorktree(altWorktreeRepoBuilder, { repoName: altWorktreeRepoName }, () => + altWorktreeRepoBuilder + .addToWorktree('modules/ROOT/pages/page-three.adoc', '= Page Three\n\ncontent\n') + .then(() => fs.remove(altWorktreeRepoBuilder.repository.gitdir)) + .then(() => altWorktreeRepoBuilder.addToWorktree('.git', 'gitdir: ' + altWorktreeGitdir + '\n')) + ) + playbookSpec.content.sources.push({ url: altWorktreeRepoBuilder.url, branches: 'HEAD' }) + const aggregate = await aggregateContent(playbookSpec) + expect(aggregate).to.have.lengthOf(1) + const componentVersion = aggregate[0] + expect(componentVersion).to.include({ name: 'the-component', version: 'v1.2.3' }) + const expectedPaths = [ + 'README.adoc', + 'modules/ROOT/_attributes.adoc', + 'modules/ROOT/pages/_attributes.adoc', + 'modules/ROOT/pages/page-one.adoc', + 'modules/ROOT/pages/page-two.adoc', + 'modules/ROOT/pages/page-three.adoc', + ] + const files = aggregate[0].files + expect(files).to.have.lengthOf(expectedPaths.length) + expect(files.map((file) => file.relative)).to.have.members(expectedPaths) + const pageThree = files.find((it) => it.relative === 'modules/ROOT/pages/page-three.adoc') + expect(pageThree.src.abspath).to.equal(ospath.join(altWorktreeRepoBuilder.url, pageThree.relative)) + }) + it('should set src.abspath and src.origin.worktree properties on files taken from worktree', async () => { const repoBuilder = new RepositoryBuilder(CONTENT_REPOS_DIR, FIXTURES_DIR) await initRepoWithFilesAndWorktree(repoBuilder) diff --git a/test/repository-builder.js b/test/repository-builder.js index 2cebcc3db..2f7ffd75d 100644 --- a/test/repository-builder.js +++ b/test/repository-builder.js @@ -200,8 +200,12 @@ class RepositoryBuilder { return this } + async getHeadCommit () { + return git.resolveRef({ ...this.repository, ref: 'HEAD' }) + } + async detachHead (oid = undefined) { - if (!oid) oid = await git.resolveRef({ ...this.repository, ref: 'HEAD' }) + if (!oid) oid = await this.getHeadCommit() await git.checkout({ ...this.repository, ref: oid }) return this } -- GitLab