mirror of
https://github.com/dsh-market/dsh-market.git
synced 2026-09-28 05:03:07 +08:00
fix(updates): read the peeled commit of an annotated tag (#723)
A tag-pinned git install advertises twice: the tag OBJECT on `refs/tags/<t>`
and the COMMIT it points at on `refs/tags/<t>^{}`. `resolveGitRemoteHead` ran
the same regex over `heads` then `tags` and stopped at the first hit, so a tag
resolved to the tag object — which a pnpm lockfile never records. `current` was
the commit, `latest` was the tag object, the two could not be equal, and the row
claimed an update forever; applying it reinstalled the same commit and reported
"version did not change". #597 fixed exactly this on the GitHub path
(`resolveHeadCommit`); the smart-HTTP path self-hosted git uses kept the bug.
The tag namespace now checks `^{}` first and falls back to the direct ref, so
lightweight tags — which advertise no peeled line — resolve as before.
Two cases added, both in the gitea harness the #525 work built: the annotated
tag resolves to the commit and reports no update, and the lightweight tag still
resolves. Removing the peeled branch turns the first red and leaves the second
green, which is the pair the fix has to satisfy.
This commit is contained in:
@@ -85,6 +85,17 @@ export async function resolveGitRemoteHead(spec: string, ref?: string): Promise<
|
||||
}
|
||||
const quoted = ref.replace(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`)
|
||||
for (const namespace of ['heads', 'tags']) {
|
||||
// An annotated tag is advertised TWICE: the tag OBJECT on
|
||||
// `refs/tags/<t>`, and the COMMIT it points at on `refs/tags/<t>^{}`.
|
||||
// A lockfile records the commit, so resolving the tag object can never
|
||||
// equal it — the row reports an update forever, and applying it
|
||||
// reinstalls the same commit. The peeled ref has to win, and only the
|
||||
// tag namespace has one (#597 resolved this on the GitHub path; this is
|
||||
// the same fact on the smart-HTTP path).
|
||||
if (namespace === 'tags') {
|
||||
const peeled = new RegExp(String.raw`([0-9a-f]{40}) refs/tags/${quoted}\^\{\}`, 'u').exec(body)
|
||||
if (peeled !== null) return peeled[1]!
|
||||
}
|
||||
const found = new RegExp(String.raw`([0-9a-f]{40}) refs/${namespace}/${quoted}(?![^\s])`, 'u').exec(body)
|
||||
if (found !== null) return found[1]!
|
||||
}
|
||||
|
||||
@@ -307,6 +307,53 @@ describe('checkUpdates — private git hosts (#525)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('reads the peeled commit of an annotated tag, not the tag object (#723)', async () => {
|
||||
// A tag-pinned git install advertises the tag OBJECT on `refs/tags/<t>` and
|
||||
// the COMMIT on `refs/tags/<t>^{}`. pnpm's lockfile records the commit, so
|
||||
// resolving the tag object can never equal it: the row claims an update
|
||||
// forever, and applying it reinstalls the same commit and reports "version
|
||||
// did not change". #597 fixed this on the GitHub path; the smart-HTTP path
|
||||
// used by self-hosted git still matched the tag object first.
|
||||
const tagObject = '769ec5e093fb58d694fa8db09a5d555469646b49'
|
||||
const commit = 'a3341ec2b28e3fb3b5fc3569012fc39acacc9fb2'
|
||||
const gitea = 'git+https://gitea.example.com/me/themer.git#v1.0.0'
|
||||
vi.stubGlobal('fetch', vi.fn(async (url: string) => {
|
||||
if (String(url).includes('registry.npmjs.org')) {
|
||||
return { ok: true, status: 200, json: async () => ({}), text: async () => '' }
|
||||
}
|
||||
return {
|
||||
ok: true, status: 200,
|
||||
headers: { get: () => 'application/x-git-upload-pack-advertisement' },
|
||||
json: async () => ({}),
|
||||
text: async () => `001e# service=git-upload-pack\n0000`
|
||||
+ `003f${tagObject} refs/tags/v1.0.0\n`
|
||||
+ `003f${commit} refs/tags/v1.0.0^{}\n`,
|
||||
}
|
||||
}))
|
||||
const result = await checkUpdates('web', true, profileWith(gitea, commit))
|
||||
expect(result.themer).toMatchObject({ kind: 'github', current: commit, latest: commit, updateAvailable: false })
|
||||
})
|
||||
|
||||
it('still resolves a lightweight tag, which advertises no peeled ref', async () => {
|
||||
// The other half of the same contract: preferring `^{}` must not lose the
|
||||
// tag that has none.
|
||||
const commit = 'a3341ec2b28e3fb3b5fc3569012fc39acacc9fb2'
|
||||
const gitea = 'git+https://gitea.example.com/me/themer.git#v1.0.0'
|
||||
vi.stubGlobal('fetch', vi.fn(async (url: string) => {
|
||||
if (String(url).includes('registry.npmjs.org')) {
|
||||
return { ok: true, status: 200, json: async () => ({}), text: async () => '' }
|
||||
}
|
||||
return {
|
||||
ok: true, status: 200,
|
||||
headers: { get: () => 'application/x-git-upload-pack-advertisement' },
|
||||
json: async () => ({}),
|
||||
text: async () => `001e# service=git-upload-pack\n0000` + `003f${commit} refs/tags/v1.0.0\n`,
|
||||
}
|
||||
}))
|
||||
const result = await checkUpdates('web', true, profileWith(gitea, 'ffffffffffffffffffffffffffffffffffffffff'))
|
||||
expect(result.themer).toMatchObject({ current: 'ffffffffffffffffffffffffffffffffffffffff', latest: commit, updateAvailable: true })
|
||||
})
|
||||
|
||||
it('treats a bare https Gitea remote (no .git suffix) as git, not npm (#525)', async () => {
|
||||
const gitea = 'https://gitea.example.com/me/themer'
|
||||
let npmHits = 0
|
||||
|
||||
Reference in New Issue
Block a user