Skip to content

Commit 96e571a

Browse files
klassikerpacotedev
andauthored
fix: ensure that resolved git ref matches expected sha (#439)
In npm/rfcs#525 about ignoring `integrity` values in lockfiles it was stated: > the sha is already what gets stored in the resolved field today This is only true for resolutions from non-commits to commits. A dependency like `git://...#4b559c4c663a23f988f6be5094c9a45faf6231bc` will be stored using the same "reference" in `resolved` even when it cloned a branch or a tag that resolved to a different sha. The update is only done if it hasn't been resolved yet, which is already the case if a full "commit" was specified: https://github.com/npm/pacote/blob/4b559c4c663a23f988f6be5094c9a45faf6231bc/lib/git.js#L263-L265 This also applies to `npm ci` after reading `package-lock.json` as it will use the same resolution. This will compare the newly returned commit-hash with a previously set `resolvedSha` and prevent that from happening. Co-authored-by: pacotedev <i+pacotedev@izs.me>
1 parent 91847c4 commit 96e571a

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

lib/git.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ const repoUrl = (h, opts) =>
2525
// add git+ to the url, but only one time.
2626
const addGitPlus = url => url && `git+${url}`.replace(/^(git\+)+/, 'git+')
2727

28+
const checkoutError = (expected, found) => {
29+
const err = new Error(`Commit mismatch: expected SHA ${expected} and cloned HEAD ${found}`)
30+
err.code = 'EGITCHECKOUT'
31+
err.sha = expected
32+
err.head = found
33+
return err
34+
}
35+
2836
class GitFetcher extends Fetcher {
2937
constructor (spec, opts) {
3038
super(spec, opts)
@@ -259,6 +267,10 @@ class GitFetcher extends Fetcher {
259267
h ? this.#cloneHosted(ref, tmp)
260268
: this.#cloneRepo(this.spec.fetchSpec, ref, tmp)
261269
)
270+
// if we already have a resolved sha ensure it doesn't change
271+
if (this.resolvedSha && this.resolvedSha !== sha) {
272+
throw checkoutError(this.resolvedSha, sha)
273+
}
262274
this.resolvedSha = sha
263275
if (!this.resolved) {
264276
await this.#addGitSha(sha)

test/git.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ HostedGit.addHost('localhostssh', {
7272
})
7373

7474
const remote = `git://localhost:${gitPort}/repo`
75+
const remoteBroken = `git://localhost:${gitPort}/broken`
7576
const remoteHosted = `git://127.0.0.1:${gitPort}/repo`
7677
const submodsRemote = `git://localhost:${gitPort}/submodule-repo`
7778
const workspacesRemote = `git://localhost:${gitPort}/workspaces-repo`
@@ -86,6 +87,7 @@ const me = t.testdir({
8687
cache: {},
8788
})
8889
const repo = resolve(me, 'repo')
90+
const broken = resolve(me, 'broken')
8991
const cache = resolve(me, 'cache')
9092
const cycleA = resolve(me, 'cycle-a')
9193
const cycleB = resolve(me, 'cycle-b')
@@ -455,6 +457,37 @@ t.test('ignores integrity for git deps', { skip: isWindows && 'posix only' }, as
455457
t.end()
456458
})
457459

460+
t.test('detects changes in the resolved sha', {}, async (t) => {
461+
const git = (...cmd) => spawnGit(cmd, { cwd: broken })
462+
const write = (f, c) => fs.writeFileSync(f, c)
463+
464+
await mkdir(broken, { recursive: true })
465+
.then(() => git('config', 'user.name', 'pacotedev'))
466+
.then(() => git('config', 'user.email', 'i+pacotedev@izs.me'))
467+
.then(() => git('config', 'tag.gpgSign', 'false'))
468+
.then(() => git('config', 'commit.gpgSign', 'false'))
469+
.then(() => git('config', 'tag.forceSignAnnotated', 'false'))
470+
.then(() => git('init'))
471+
.then(() => write(`${broken}/package.json`, JSON.stringify({
472+
name: 'repo',
473+
version: '0.0.0',
474+
})))
475+
.then(() => git('add', 'package.json'))
476+
.then(() => git('commit', '-m', 'package json file'))
477+
.then(() => git('checkout', '-b', REPO_HEAD))
478+
479+
const { stdout: actual } = await git('rev-parse', 'HEAD')
480+
481+
const fetcher = new GitFetcher(remoteBroken + '#' + REPO_HEAD, opts)
482+
await t.rejects(() => fetcher.manifest(), {
483+
message: `Commit mismatch: expected SHA ${REPO_HEAD} and cloned HEAD ${actual}`,
484+
code: 'EGITCHECKOUT',
485+
sha: REPO_HEAD,
486+
head: actual,
487+
})
488+
t.end()
489+
})
490+
458491
t.test('weird hosted that doesnt provide any fetch targets', { skip: isWindows && 'posix only' },
459492
t => {
460493
const hosted = {

0 commit comments

Comments
 (0)