From df1fddb218d65d464325d19b5f7fb202e54aed11 Mon Sep 17 00:00:00 2001 From: Nicholas Lona Date: Thu, 29 Jan 2026 14:42:41 -0500 Subject: [PATCH 1/2] fixed checkout bug --- fallback-dependencies.js | 10 +- test/test.js | 14 +++ test/util/gitCheckoutTag.js | 179 ++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 test/util/gitCheckoutTag.js diff --git a/fallback-dependencies.js b/fallback-dependencies.js index cfb4da0..bd5982c 100644 --- a/fallback-dependencies.js +++ b/fallback-dependencies.js @@ -138,6 +138,11 @@ function executeFallbackList (listTypes) { break } } + const fetch = spawnSync('git', ['fetch', '--tags'], { // make sure current clone has all tags + shell: false, + cwd: path.resolve(fallbackDependenciesDir + '/' + dependency, '') + }) + if (fetch.status !== 0) throw fetch.stderr.toString() const output = spawnSync('git', ['tag'], { // get list of tags shell: false, cwd: path.resolve(fallbackDependenciesDir + '/' + dependency, '') @@ -153,11 +158,6 @@ function executeFallbackList (listTypes) { if (!rerunNpmCi) break // stop checking fallbacks } else { // version supplied is a valid tag, but differs from current tag if (enableCheckout) { - const fetch = spawnSync('git', ['fetch', '--tags'], { - shell: false, - cwd: path.resolve(fallbackDependenciesDir + '/' + dependency, '') - }) - if (fetch.status !== 0) throw fetch.stderr.toString() const checkout = spawnSync('git', ['checkout', version], { shell: false, cwd: path.resolve(fallbackDependenciesDir + '/' + dependency, '') diff --git a/test/test.js b/test/test.js index 5d310c0..4b71f84 100644 --- a/test/test.js +++ b/test/test.js @@ -16,6 +16,7 @@ const failedToClone = path.join(__dirname, './util/failedToClone') const failedToCloneVersion = path.join(__dirname, './util/failedToCloneVersion') const gitCheckoutTagError = path.join(__dirname, './util/gitCheckoutTagError.js') const gitCheckoutCommitError = path.join(__dirname, './util/gitCheckoutCommitError.js') +const gitCheckoutTag = path.join(__dirname, './util/gitCheckoutTag.js') const gitCloneCheckoutError = path.join(__dirname, './util/gitCloneCheckoutError.js') const gitError = path.join(__dirname, './util/gitError.js') const gitFetchHeadError = path.join(__dirname, './util/gitFetchHeadError.js') @@ -153,6 +154,19 @@ describe('universal fallback-dependencies tests', () => { delete process.env.FALLBACK_DEPENDENCIES_ENABLE_CHECKOUT }) + it('should checkout a specific git tag even when current clone is behind remote repo', () => { + process.env.FALLBACK_DEPENDENCIES_ENABLE_CHECKOUT = true + const listTypes = ['fallbackDependencies', 'fallbackDevDependencies'] + while (listTypes.length) { + fs.rmSync(path.join(__dirname, './clones'), { recursive: true, force: true }) + fs.rmSync(path.join(__dirname, './repos'), { recursive: true, force: true }) + require(gitCheckoutTag)(listTypes.pop()) + assert(fs.existsSync(path.join(__dirname, './clones/repo1/lib')), './clones/repo1/lib does not exist') + assert(fs.existsSync(path.join(__dirname, './clones/repo1/lib/fallback-deps-test-repo-2')), './clones/repo1/lib/fallback-deps-test-repo-2 does not exist') + } + delete process.env.FALLBACK_DEPENDENCIES_ENABLE_CHECKOUT + }) + it('should checkout a non-tagged commit', () => { const listTypes = ['fallbackDependencies', 'fallbackDevDependencies'] while (listTypes.length) { diff --git a/test/util/gitCheckoutTag.js b/test/util/gitCheckoutTag.js new file mode 100644 index 0000000..3dd75a5 --- /dev/null +++ b/test/util/gitCheckoutTag.js @@ -0,0 +1,179 @@ +module.exports = (listType) => { + const fs = require('fs') + const path = require('path') + const { spawnSync } = require('child_process') + const testSrc = path.resolve(__dirname, '../../test') + const repoList = ['repo1', 'repo2', 'repo3'] + + try { + fs.rmSync(path.join(__dirname, './clones'), { recursive: true, force: true }) + fs.rmSync(path.join(__dirname, './repos'), { recursive: true, force: true }) + if (!fs.existsSync(`${testSrc}/repos`)) fs.mkdirSync(`${testSrc}/repos`) + if (!fs.existsSync(`${testSrc}/clones`)) fs.mkdirSync(`${testSrc}/clones`) + + const repo1Package = { + dependencies: { + 'fallback-dependencies': '../../../' + }, + scripts: { + postinstall: 'node node_modules/fallback-dependencies/fallback-dependencies.js' + } + } + repo1Package[listType] = { + dir: 'lib', + reposFile: 'reposFile.json' + } + const repo1PackageLock = { + name: 'repo1', + lockfileVersion: 3, + requires: true, + packages: { + '': { + hasInstallScript: true, + dependencies: { + 'fallback-dependencies': '../../..' + } + }, + '../../..': { + version: '0.1.0', + license: 'CC-BY-1.0', + dependencies: {} + }, + 'node_modules/fallback-dependencies': { + resolved: '../../..', + link: true + } + } + } + let repo1FileData = { + 'fallback-deps-test-repo-2': [ + '../../../repos/repo2' + ] + } + const repo2Package = { + dependencies: { + 'fallback-dependencies': '../../../../../' + }, + scripts: { + postinstall: 'node node_modules/fallback-dependencies/fallback-dependencies.js' + } + } + repo2Package[listType] = { + dir: 'lib', + repos: { + 'fallback-deps-test-repo-3:': '../../../../../repos/repo3' + } + } + const repo2PackageLock = { + name: 'repo2', + lockfileVersion: 3, + requires: true, + packages: { + '': { + hasInstallScript: true, + dependencies: { + 'fallback-dependencies': '../../../../..' + } + }, + '../../../../..': { + version: '0.1.0', + license: 'CC-BY-1.0', + dependencies: {} + }, + 'node_modules/fallback-dependencies': { + resolved: '../../../../..', + link: true + } + } + } + const repo3Package = {} + const repo3PackageLock = { + name: 'repo3', + lockfileVersion: 3, + requires: true, + packages: {} + } + + // initialize repos + const packageList = [[repo1Package, repo1PackageLock], [repo2Package, repo2PackageLock], [repo3Package, repo3PackageLock]] + for (const id in repoList) { + if (!fs.existsSync(`${testSrc}/repos/${repoList[id]}/`)) fs.mkdirSync(`${testSrc}/repos/${repoList[id]}/`) + spawnSync('git', ['--bare', 'init'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/repos/${repoList[id]}`, '') // where we're cloning the repo to + }) + spawnSync('git', ['clone', `${testSrc}/repos/${repoList[id]}`], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/clones`, '') // where we're cloning the repo to + }) + if (repoList[id] === 'repo1') fs.writeFileSync(`${testSrc}/clones/repo1/reposFile.json`, JSON.stringify(repo1FileData)) + fs.writeFileSync(`${testSrc}/clones/${repoList[id]}/package.json`, JSON.stringify(packageList[id][0])) + fs.writeFileSync(`${testSrc}/clones/${repoList[id]}/package-lock.json`, JSON.stringify(packageList[id][1])) + spawnSync('git', ['add', '.'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/clones/${repoList[id]}`, '') // where we're cloning the repo to + }) + spawnSync('git', ['commit', '-m', '"commit"'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/clones/${repoList[id]}`, '') // where we're cloning the repo to + }) + spawnSync('git', ['push'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/clones/${repoList[id]}`, '') // where we're cloning the repo to + }) + } + + // add 1.0.0 tag and attempt to clone repo while specifying it + spawnSync('git', ['tag', '1.0.0'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/clones/repo2`, '') // where we're cloning the repo to + }) + spawnSync('git', ['push', '--tags'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/clones/repo2`, '') // where we're cloning the repo to + }) + repo1FileData = { + 'fallback-deps-test-repo-2': [ + '../../../repos/repo2 -b 1.0.0' + ] + } + fs.writeFileSync(`${testSrc}/clones/repo1/reposFile.json`, JSON.stringify(repo1FileData)) + spawnSync('npm', ['ci'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/clones/repo1`, '') // where we're cloning the repo to + }) + + // add 1.0.1 tag and attempt to clone repo while specifying it + spawnSync('git', ['tag', '1.0.1'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/repos/repo2`, '') // where we're cloning the repo to + }) + spawnSync('git', ['push', '--tags'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/repos/repo2`, '') // where we're cloning the repo to + }) + + // attempt to clone repo + repo1FileData = { + 'fallback-deps-test-repo-2': [ + '../../../repos/repo2 -b 1.0.1' + ] + } + fs.writeFileSync(`${testSrc}/clones/repo1/reposFile.json`, JSON.stringify(repo1FileData)) + spawnSync('npm', ['ci'], { + shell: false, + stdio: 'pipe', // hide output from git + cwd: path.normalize(`${testSrc}/clones/repo1`, '') // where we're cloning the repo to + }) + } catch {} +} From c624759a82456a5743d62f43d676cba30bc7a8f3 Mon Sep 17 00:00:00 2001 From: Nicholas Lona Date: Thu, 29 Jan 2026 18:07:16 -0500 Subject: [PATCH 2/2] 100 percent test coverage --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- test/test.js | 2 +- test/util/gitFetchRemoteError.js | 15 +++++------- test/util/gitTagError.js | 10 ++++---- test/util/interceptGitFetchRemoteSpawnSync.js | 24 +++++++++++++++++++ test/util/interceptGitTagSpawnSync.js | 23 ++++++++++++++++++ 8 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 test/util/interceptGitFetchRemoteSpawnSync.js create mode 100644 test/util/interceptGitTagSpawnSync.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b6723d6..2ed657b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.4 + +- Fixed bug where attempting to checkout a new tag present only on the remote incorrectly reported the clone was already on that version. + ## 1.1.3 - Added option to enable checking out rather than recloning when changing versions via `FALLBACK_DEPENDENCIES_ENABLE_CHECKOUT` environment variable or `enableCheckout` in `fallbackDependencies` package.json config. diff --git a/package-lock.json b/package-lock.json index 5b48efd..6a995c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fallback-dependencies", - "version": "1.1.3", + "version": "1.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fallback-dependencies", - "version": "1.1.3", + "version": "1.1.4", "license": "CC-BY-4.0", "dependencies": { "roosevelt-logger": "1.0.1" diff --git a/package.json b/package.json index 3fe99ca..9f8df3e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "https://github.com/rooseveltframework/fallback-dependencies/graphs/contributors" } ], - "version": "1.1.3", + "version": "1.1.4", "files": [ "fallback-dependencies.js", "*.md" diff --git a/test/test.js b/test/test.js index 4b71f84..fdcd41f 100644 --- a/test/test.js +++ b/test/test.js @@ -357,7 +357,7 @@ describe('universal fallback-dependencies tests', () => { fs.rmSync(path.join(__dirname, './clones'), { recursive: true, force: true }) fs.rmSync(path.join(__dirname, './repos'), { recursive: true, force: true }) const output = require(gitTagError)('fallbackDependencies') - assert(output.includes('fatal: unterminated line in'), 'git tag command didn\'t throw an error') + assert(output.includes('fatal: simulated git tag error'), 'git tag command didn\'t throw an error') }) it('should throw an error if the "git fetch --tags" command fails', () => { diff --git a/test/util/gitFetchRemoteError.js b/test/util/gitFetchRemoteError.js index 758ff98..53fff09 100644 --- a/test/util/gitFetchRemoteError.js +++ b/test/util/gitFetchRemoteError.js @@ -3,6 +3,7 @@ module.exports = (listType) => { const path = require('path') const { spawnSync } = require('child_process') const testSrc = path.resolve(__dirname, '../../test') + const interceptGitFetchRemoteSpawnSyncPath = path.resolve(__dirname, 'interceptGitFetchRemoteSpawnSync.js') const repoList = ['repo1', 'repo2'] try { @@ -139,14 +140,6 @@ module.exports = (listType) => { cwd: path.normalize(`${testSrc}/clones/repo1`, '') // where we're cloning the repo to }) - // edit git config to trigger error - const config = fs.readFileSync(path.normalize(`${testSrc}/clones/repo1/lib/fallback-deps-test-repo-2/.git/config`)).toString() - const updatedConfig = config.split('\n').map(line => { - if (line.includes('fetch =')) return '\tfetch = not-valid' - return line - }).join('\n') - fs.writeFileSync(path.normalize(`${testSrc}/clones/repo1/lib/fallback-deps-test-repo-2/.git/config`), updatedConfig) - // get commit id and attempt to clone repo while specifying it const commit = spawnSync('git', ['log', '--oneline'], { shell: false, @@ -162,7 +155,11 @@ module.exports = (listType) => { const output = spawnSync('npm', ['ci'], { shell: false, stdio: 'pipe', // hide output from git - cwd: path.normalize(`${testSrc}/clones/repo1`, '') // where we're cloning the repo to + cwd: path.normalize(`${testSrc}/clones/repo1`, ''), // where we're cloning the repo to + env: { + ...process.env, + NODE_OPTIONS: `-r ${interceptGitFetchRemoteSpawnSyncPath} ${process.env.NODE_OPTIONS || ''}` + } }) return output.stderr.toString() diff --git a/test/util/gitTagError.js b/test/util/gitTagError.js index bc200c0..e8e223d 100644 --- a/test/util/gitTagError.js +++ b/test/util/gitTagError.js @@ -3,6 +3,7 @@ module.exports = (listType) => { const path = require('path') const { spawnSync } = require('child_process') const testSrc = path.resolve(__dirname, '../../test') + const interceptGitTagSpawnSyncPath = path.resolve(__dirname, 'interceptGitTagSpawnSync.js') const repoList = ['repo1', 'repo2'] try { @@ -139,13 +140,14 @@ module.exports = (listType) => { cwd: path.normalize(`${testSrc}/clones/repo1`, '') // where we're cloning the repo to }) - // edit git packed-refs to trigger error - fs.writeFileSync(path.normalize(`${testSrc}/clones/repo1/lib/fallback-deps-test-repo-2/.git/packed-refs`), 'invalid content') - const output = spawnSync('npm', ['ci'], { shell: false, stdio: 'pipe', // hide output from git - cwd: path.normalize(`${testSrc}/clones/repo1`, '') // where we're cloning the repo to + cwd: path.normalize(`${testSrc}/clones/repo1`, ''), // where we're cloning the repo to + env: { + ...process.env, + NODE_OPTIONS: `-r ${interceptGitTagSpawnSyncPath} ${process.env.NODE_OPTIONS || ''}` + } }) return output.stderr.toString() diff --git a/test/util/interceptGitFetchRemoteSpawnSync.js b/test/util/interceptGitFetchRemoteSpawnSync.js new file mode 100644 index 0000000..1411f5c --- /dev/null +++ b/test/util/interceptGitFetchRemoteSpawnSync.js @@ -0,0 +1,24 @@ +const fs = require('fs') +const path = require('path') +const testSrc = path.resolve(__dirname, '../../test') + +// replace original spawnSync +const childProcess = require('child_process') +const originalSpawnSync = childProcess.spawnSync +childProcess.spawnSync = function (command, args, options) { + const argv = Array.isArray(args) ? args : [] + const isGit = command === 'git' + const isFetchRemote = argv.length === 2 && argv[0] === 'fetch' && argv[1] !== '--tags' + + if (isGit && isFetchRemote) { + // edit git config to trigger error + const config = fs.readFileSync(path.normalize(`${testSrc}/clones/repo1/lib/fallback-deps-test-repo-2/.git/config`)).toString() + const updatedConfig = config.split('\n').map(line => { + if (line.includes('fetch =')) return '\tfetch = not-valid' + return line + }).join('\n') + fs.writeFileSync(path.normalize(`${testSrc}/clones/repo1/lib/fallback-deps-test-repo-2/.git/config`), updatedConfig) + } + + return originalSpawnSync.apply(this, arguments) +} diff --git a/test/util/interceptGitTagSpawnSync.js b/test/util/interceptGitTagSpawnSync.js new file mode 100644 index 0000000..c52d9f0 --- /dev/null +++ b/test/util/interceptGitTagSpawnSync.js @@ -0,0 +1,23 @@ +// replace original spawnSync +const childProcess = require('child_process') +const originalSpawnSync = childProcess.spawnSync +childProcess.spawnSync = function (command, args, options) { + const argv = Array.isArray(args) ? args : [] + const isGit = command === 'git' + const isTag = argv.length === 1 && argv[0] === 'tag' + + if (isGit && isTag) { + const msg = 'fatal: simulated git tag error\n' + return { + pid: -1, + output: [null, Buffer.from(''), Buffer.from(msg)], + stdout: Buffer.from(''), + stderr: Buffer.from(msg), + status: 128, + signal: null, + error: null + } + } + + return originalSpawnSync.apply(this, arguments) +}