diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 71068cb093c8222d8d975ef4b2606deb583284d5..fed906bbcb2691d0d02461a2f938584632e4e5fe 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -12,6 +12,8 @@ For a detailed view of what's changed, refer to the {url-repo}/commits[commit hi === Fixed +* prepare clean worktree when no previous worktree exists or when switching from another reference (#7) +* don't follow symlinks when removing untracked files from existing worktree (#7) * always redirect stderr from command to stderr of console == 1.0.0-alpha.2 (2022-09-02) diff --git a/packages/collector-extension/lib/index.js b/packages/collector-extension/lib/index.js index a0374cc818d30a123b6d6ef8f8a8c11c2c5b5e74..c6bddafef249fa24ce2d892e5eb58a85706b8c81 100644 --- a/packages/collector-extension/lib/index.js +++ b/packages/collector-extension/lib/index.js @@ -22,7 +22,7 @@ module.exports.register = function () { this.once('contentAggregated', async ({ playbook, contentAggregate }) => { const quiet = playbook.runtime?.quiet const cacheDir = ospath.join(getBaseCacheDir(playbook), 'collector') - //await fsp.rm(cacheDir, { recursive: true, force: true }) // Q: should we try to reuse existing cache? + //await fsp.rm(cacheDir, { force: true, recursive: true, force: true }) // Q: should we try to reuse existing cache? await fsp.mkdir(cacheDir, { recursive: true }) const gitCache = {} for (const componentVersionBucket of contentAggregate) { @@ -52,7 +52,7 @@ module.exports.register = function () { } }) if (worktree) { - for (const scanDir of scanDirs) await fsp.rm(scanDir, { force: true, recursive: true }) + for (const scanDir of scanDirs) await fsp.rm(scanDir, { recursive: true, force: true }) } else { const cache = gitCache[gitdir] || (gitCache[gitdir] = {}) const ref = `refs/${reftype === 'branch' ? 'head' : reftype}s/${refname}` @@ -116,14 +116,15 @@ async function prepareWorktree (repo) { const worktreeGitdir = ospath.join(dir, '.git') const worktreeIndexPath = ospath.join(worktreeGitdir, 'index') try { - let force - if (await isDirectory(worktreeGitdir)) { - force = false - await fsp.cp(worktreeIndexPath, currentIndexPath) + let force = true + try { + await fsp.cp(worktreeIndexPath, currentIndexPath, { force }) await removeUntrackedFiles(repo) - } else { - force = true - await fsp.access(dir).then(() => fsp.rm(dir, { recursive: true, force: true }), invariably.void) + } catch { + force = false + if (currentIndex) await fsp.unlink(currentIndexPath) + await fsp.rm(dir, { recursive: true, force: true }) + await fsp.mkdir(worktreeGitdir, { recursive: true }) } let head if (ref.startsWith('refs/heads')) { @@ -136,12 +137,11 @@ async function prepareWorktree (repo) { head = await git.resolveRef(repo) } await git.checkout({ ...repo, force, noUpdateHead: true, track: false }) - await fsp.mkdir(worktreeGitdir, { recursive: true }) await fsp.writeFile(ospath.join(worktreeGitdir, 'commondir'), `${gitdir}\n`, 'utf8') await fsp.writeFile(ospath.join(worktreeGitdir, 'HEAD'), `${head}\n`, 'utf8') await fsp.cp(currentIndexPath, worktreeIndexPath) } finally { - if (currentIndex) await fsp.writeFile(currentIndexPath, currentIndex) + currentIndex ? await fsp.writeFile(currentIndexPath, currentIndex) : fsp.rm(currentIndexPath, { force: true }) } } @@ -150,10 +150,6 @@ function generateWorktreeFolderName ({ url, gitdir, worktree }) { return `${url.substr(url.lastIndexOf('/') + 1)}-${createHash('sha1').update(url).digest('hex')}` } -function isDirectory (path_) { - return fsp.stat(path_).then((stat) => stat.isDirectory(), invariably.false) -} - function getBaseCacheDir ({ dir: dot, runtime: { cacheDir: preferredDir } }) { return preferredDir == null ? getUserCacheDir(`antora${process.env.NODE_ENV === 'test' ? '-test' : ''}`) || ospath.join(dot, '.cache/antora') @@ -199,10 +195,13 @@ function srcFs (cwd, globs = '**/*', base) { async function removeUntrackedFiles (repo) { const trees = [git.STAGE({}), git.WORKDIR()] const map = async (path_, [sEntry, wEntry]) => { + if (path_ === '.') return if (path_ === '.git') return null - if (path_ !== '.' && sEntry == null) { + if (sEntry == null) { await fsp.rm(ospath.join(repo.dir, path_), { recursive: true }) return null + } else if ((await sEntry.mode()) === 0o120000) { + return null } } await git.walk(Object.assign({ trees, map }, repo)) diff --git a/packages/collector-extension/test/collector-extension-test.js b/packages/collector-extension/test/collector-extension-test.js index 106c54e23d82e05e0726834c662846de24332bcb..295da436bfb3a5a1281855582c46fc1ca3b8ee5a 100644 --- a/packages/collector-extension/test/collector-extension-test.js +++ b/packages/collector-extension/test/collector-extension-test.js @@ -84,7 +84,20 @@ describe('collector extension', () => { const createRepository = async ({ repoName, fixture = repoName, branches, tags, startPath, collectorConfig }) => { const repo = { dir: ospath.join(REPOS_DIR, repoName), fs } - await fsp.cp(ospath.join(FIXTURES_DIR, fixture), repo.dir, { recursive: true }) + const links = [] + const captureLinks = function (src, dest) { + if (!src.endsWith('-link')) return true + return fsp.readFile(src, 'utf8').then((link) => { + const [from, to] = link.trim().split(' -> ') + this.push([ospath.join(ospath.dirname(dest), from), to]) + return false + }) + } + await fsp.cp(ospath.join(FIXTURES_DIR, fixture), repo.dir, { recursive: true, filter: captureLinks.bind(links) }) + for (const [from, to] of links) { + const type = to.endsWith('/') ? 'dir' : 'file' + await fsp.symlink(type === 'dir' ? to.slice(0, -1) : to, from, type) + } if (collectorConfig) { const antoraYmlPath = ospath.join(repo.dir, startPath || '', 'antora.yml') await updateYamlFile(antoraYmlPath, { ext: { collector: collectorConfig } }) @@ -901,7 +914,7 @@ describe('collector extension', () => { }) }) - it('should remove temporary worktree left behind by previous run', async () => { + it('should remove invalid temporary worktree left behind by previous run', async () => { const collectorConfig = { run: { command: 'node .gen-component-desc.js' }, scan: { dir: 'build' }, @@ -928,6 +941,24 @@ describe('collector extension', () => { }) }) + it('should not leave behind index file in managed repository', async () => { + const collectorConfig = { + run: { command: 'node .check-index-file.js' }, + scan: { dir: 'build' }, + } + await runScenario({ + repoName: 'test-at-root', + collectorConfig, + branches: ['v1.0.x'], + after: (contentAggregate) => { + expect(contentAggregate).to.have.lengthOf(1) + const bucket = contentAggregate[0] + expect(bucket.files).to.have.lengthOf(1) + expect(ospath.basename(bucket.files[0].path)).to.equal('does-not-exist.adoc') + }, + }) + }) + it('should remove untracked changes when switching worktrees for same repository', async () => { const collectorConfig = { run: { command: 'node .gen-dirty-worktree.js' }, @@ -956,6 +987,50 @@ describe('collector extension', () => { }) }) + it('should not follow symlinks when removing untracked files in worktree', async () => { + const collectorConfig = { + run: { command: 'node .check-dirty-worktree.js' }, + scan: { dir: 'build' }, + } + await runScenario({ + repoName: 'test-with-symlink', + collectorConfig, + branches: ['v1.0.x', 'v2.0.x'], + after: (contentAggregate) => { + expect(contentAggregate).to.have.lengthOf(2) + const [bucket1, bucket2] = contentAggregate + expect(bucket1.files).to.have.lengthOf(3) + expect(bucket1.files.filter((it) => it.path.endsWith('/Hello.java'))).to.have.lengthOf(2) + expect(bucket1.files.filter((it) => it.path.endsWith('/dirty-worktree.adoc'))).to.be.empty() + expect(bucket2.files).to.have.lengthOf(3) + expect(bucket2.files.filter((it) => it.path.endsWith('/Hello.java'))).to.have.lengthOf(2) + expect(bucket2.files.filter((it) => it.path.endsWith('/dirty-worktree.adoc'))).to.be.empty() + }, + }) + }) + + it('should force checkout if worktree has changes when switching worktrees for same repository', async () => { + const collectorConfig = { + run: { command: 'node .gen-dirty-worktree.js delete' }, + scan: { dir: 'build' }, + } + await runScenario({ + repoName: 'test-at-root', + collectorConfig, + branches: ['v1.0.x', 'v2.0.x'], + after: (contentAggregate) => { + expect(contentAggregate).to.have.lengthOf(2) + const [bucket1, bucket2] = contentAggregate + expect(bucket1).to.have.property('version', '1.0') + expect(bucket1.files).to.have.lengthOf(1) + expect(bucket1.files[0].path).to.equal('modules/ROOT/pages/v1.0.x-release.adoc') + expect(bucket2).to.have.property('version', '2.0') + expect(bucket2.files).to.have.lengthOf(1) + expect(bucket2.files[0].path).to.equal('modules/ROOT/pages/v2.0.x-release.adoc') + }, + }) + }) + it('should send output from command to stdout by default', async () => { const collectorConfig = { run: { command: 'node .gen-output.js' } } const stdout = await captureStdout(() => diff --git a/packages/collector-extension/test/fixtures/test-at-root/.check-index-file.js b/packages/collector-extension/test/fixtures/test-at-root/.check-index-file.js new file mode 100644 index 0000000000000000000000000000000000000000..37bc474a434495e368b8c4973ee498a41873ecdf --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-at-root/.check-index-file.js @@ -0,0 +1,18 @@ +'use strict' + +const { promises: fsp } = require('fs') +const ospath = require('path') + +;(async () => { + const gitdir = await fsp.readFile('.git/commondir', 'utf8').then((contents) => contents.trim()) + const exists = await fsp.access(ospath.join(gitdir, 'index')).then( + () => true, + () => false + ) + await fsp.mkdir('build/modules/ROOT/pages', { recursive: true }) + if (exists) { + await fsp.writeFile('build/modules/ROOT/pages/exists.adoc', '= Index Exists', 'utf8') + } else { + await fsp.writeFile('build/modules/ROOT/pages/does-not-exist.adoc', '= Index Does Not Exist', 'utf8') + } +})() diff --git a/packages/collector-extension/test/fixtures/test-at-root/.gen-dirty-worktree.js b/packages/collector-extension/test/fixtures/test-at-root/.gen-dirty-worktree.js index 80dcac4454c9a6e8f3062f02a870139835fb8f64..7d8ec90cf5260f2310831d3e35ad277563d129c3 100644 --- a/packages/collector-extension/test/fixtures/test-at-root/.gen-dirty-worktree.js +++ b/packages/collector-extension/test/fixtures/test-at-root/.gen-dirty-worktree.js @@ -4,10 +4,15 @@ const { promises: fsp } = require('fs') const { spawnSync } = require('child_process') ;(async () => { + const dirty = spawnSync('git', ['status', '--short']).stdout.toString().trimRight() const branch = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).stdout.toString().trimRight() await fsp.writeFile('antora.yml', `name: test\nversion: '${branch.substr(1).replace('.x', '')}'\n`, 'utf8') await fsp.writeFile(`untracked-${branch}.txt`, 'untracked', 'utf8') + if (process.argv[2] === 'delete') await fsp.rm(process.argv[1]) await fsp.mkdir('build/modules/ROOT/pages', { recursive: true }) await fsp.writeFile('build/antora.yml', `name: test\nversion: '${branch.substr(1).replace('.x', '')}'\n`, 'utf8') await fsp.writeFile(`build/modules/ROOT/pages/${branch}-release.adoc`, '= Branch-Specific Page', 'utf8') + if (dirty) { + fsp.writeFile('build/modules/ROOT/pages/dirty-worktree.adoc', `= Dirty Worktree\n\n....\n${dirty}\n....`, 'utf8') + } })() diff --git a/packages/collector-extension/test/fixtures/test-with-symlink/.check-dirty-worktree.js b/packages/collector-extension/test/fixtures/test-with-symlink/.check-dirty-worktree.js new file mode 100644 index 0000000000000000000000000000000000000000..f90a8b007a09cb6430b040dcbf7d204c6c160964 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-with-symlink/.check-dirty-worktree.js @@ -0,0 +1,19 @@ +'use strict' + +const { promises: fsp } = require('fs') +const { spawnSync } = require('child_process') + +;(async () => { + const untracked = spawnSync('git', ['status', '--short']) + .stdout.toString() + .split('\n') + .filter((it) => it.trimStart().startsWith('?')) + .join('\n') + .trimEnd() + const modified = spawnSync('git', ['diff', '--name-status']).stdout.toString().trimEnd() + const dirty = untracked + modified + await fsp.mkdir('build/modules/ROOT/pages', { recursive: true }) + if (dirty) { + fsp.writeFile('build/modules/ROOT/pages/dirty-worktree.adoc', `= Dirty Worktree\n\n....\n${dirty}\n....`, 'utf8') + } +})() diff --git a/packages/collector-extension/test/fixtures/test-with-symlink/.gitattributes b/packages/collector-extension/test/fixtures/test-with-symlink/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..6313b56c57848efce05faa7aa7e901ccfc2886ea --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-with-symlink/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/packages/collector-extension/test/fixtures/test-with-symlink/antora.yml b/packages/collector-extension/test/fixtures/test-with-symlink/antora.yml new file mode 100644 index 0000000000000000000000000000000000000000..e35bef6465e7726643768ed70bef8a27ce60b044 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-with-symlink/antora.yml @@ -0,0 +1,2 @@ +name: test +version: true diff --git a/packages/collector-extension/test/fixtures/test-with-symlink/modules/ROOT/examples/java-link b/packages/collector-extension/test/fixtures/test-with-symlink/modules/ROOT/examples/java-link new file mode 100644 index 0000000000000000000000000000000000000000..98528bf9c5df321e447beee70f6ec2bc085ed48f --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-with-symlink/modules/ROOT/examples/java-link @@ -0,0 +1 @@ +java -> ../../../src/main/java/ diff --git a/packages/collector-extension/test/fixtures/test-with-symlink/modules/ROOT/pages/index.adoc b/packages/collector-extension/test/fixtures/test-with-symlink/modules/ROOT/pages/index.adoc new file mode 100644 index 0000000000000000000000000000000000000000..cada0695c920233fad52ea9d7b2453d54f8f8848 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-with-symlink/modules/ROOT/pages/index.adoc @@ -0,0 +1,6 @@ += Hello + +[,java] +---- +include::example$java/Hello.java[] +---- diff --git a/packages/collector-extension/test/fixtures/test-with-symlink/src/main/java/Hello.java b/packages/collector-extension/test/fixtures/test-with-symlink/src/main/java/Hello.java new file mode 100644 index 0000000000000000000000000000000000000000..d9e2b35d6faffd2e83a7681c26cf835eddbb5796 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-with-symlink/src/main/java/Hello.java @@ -0,0 +1,5 @@ +public class Hello { + public static void main(String[] args) { + System.out.println("hello"); + } +}