This commit is contained in:
Lykin 2026-11-08 16:36:13 +08:00 committed by GitHub
commit 2786465fb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 174 additions and 0 deletions

View File

@ -0,0 +1,63 @@
import {execFileSync} from 'node:child_process'
import {mkdtemp, rm, writeFile} from 'node:fs/promises'
import {tmpdir} from 'node:os'
import {join} from 'node:path'
import {pathToFileURL} from 'node:url'
import {expect, it} from '@jest/globals'
import {createCommandManager} from '../src/git-command-manager.js'
function git(cwd: string, ...args: string[]): string {
return execFileSync('git', args, {cwd, encoding: 'utf8'}).trim()
}
it.each([false, true])(
'recovers full history after a filtered fetch (already unshallowed: %s)',
async alreadyUnshallowed => {
const directory = await mkdtemp(join(tmpdir(), 'checkout-partial-'))
try {
git(directory, 'init', '-q', '-b', 'main', 'remote')
git(directory, 'init', '-q', 'local')
const remote = join(directory, 'remote')
const local = join(directory, 'local')
git(remote, 'config', 'uploadpack.allowFilter', 'true')
git(remote, 'config', 'user.name', 'Checkout Test')
git(remote, 'config', 'user.email', 'checkout@example.invalid')
await writeFile(join(remote, 'historical.txt'), 'historical blob\n')
git(remote, 'add', 'historical.txt')
git(remote, 'commit', '-qm', 'Add historical blob')
git(remote, 'rm', '-q', 'historical.txt')
git(remote, 'commit', '-qm', 'Remove historical blob')
git(local, 'remote', 'add', 'origin', pathToFileURL(remote).href)
const refSpec = ['+refs/heads/main:refs/remotes/origin/main']
const manager = await createCommandManager(local, false, false)
await manager.fetch(refSpec, {filter: 'blob:none', fetchDepth: 1})
if (alreadyUnshallowed) {
await manager.fetch(refSpec, {filter: 'blob:none', fetchDepth: 0})
}
await writeFile(join(local, 'keep.txt'), 'worktree file\n')
await manager.fetch(refSpec, {fetchDepth: 0})
expect(git(local, 'rev-parse', '--is-shallow-repository')).toBe('false')
expect(
git(
local,
'rev-list',
'--objects',
'--all',
'--quiet',
'--missing=print'
)
).toBe('')
expect(
await manager.configExists('remote.origin.partialclonefilter')
).toBe(false)
expect(await manager.configExists('remote.origin.promisor')).toBe(false)
expect(git(local, 'status', '--short')).toBe('?? keep.txt')
} finally {
await rm(directory, {recursive: true, force: true})
}
},
20000
)

49
dist/index.js vendored
View File

@ -35597,6 +35597,8 @@ class GitVersion {
// sparse-checkout not [well-]supported before 2.28 (see https://github.com/actions/checkout/issues/1386)
const MinimumGitVersion = new GitVersion('2.18');
const MinimumGitSparseCheckoutVersion = new GitVersion('2.28');
const MinimumGitRefetchVersion = new GitVersion('2.36');
const MinimumGitFetchWithoutWritingHeadVersion = new GitVersion('2.29');
async function createCommandManager(workingDirectory, lfs, doSparseCheckout) {
return await GitCommandManager.createCommandManager(workingDirectory, lfs, doSparseCheckout);
}
@ -35749,9 +35751,19 @@ class GitCommandManager {
if (options.showProgress) {
args.push('--progress');
}
const inheritedFilter = !options.filter &&
(await this.configExists('remote.origin.partialclonefilter'));
const restoreFullHistory = inheritedFilter && !(options.fetchDepth && options.fetchDepth > 0);
if (options.filter) {
args.push(`--filter=${options.filter}`);
}
else if (inheritedFilter) {
args.push('--no-filter');
if (restoreFullHistory &&
this.gitVersion.checkMinimum(MinimumGitRefetchVersion)) {
args.push('--refetch');
}
}
if (options.fetchDepth && options.fetchDepth > 0) {
args.push(`--depth=${options.fetchDepth}`);
}
@ -35766,6 +35778,43 @@ class GitCommandManager {
await execute(async () => {
await that.execGit(args);
});
if (restoreFullHistory) {
const canSkipFetchHead = this.gitVersion.checkMinimum(MinimumGitFetchWithoutWritingHeadVersion);
let fetchedMissingObjects = false;
let previousMissing = new Set();
while (true) {
const output = await this.execGit(['rev-list', '--objects', '--all', '--quiet', '--missing=print'], false, true);
const missing = [
...output.stdout.matchAll(/^\?([0-9a-f]{40}|[0-9a-f]{64})\r?$/gm)
].map(match => match[1]);
if (!missing.length) {
break;
}
if (missing.some(oid => previousMissing.has(oid))) {
throw new Error('Unable to fetch all objects from the previous partial checkout.');
}
previousMissing = new Set(missing);
for (let i = 0; i < missing.length; i += 100) {
await this.execGit([
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--no-recurse-submodules',
'--no-filter',
canSkipFetchHead ? '--no-write-fetch-head' : '--append',
'origin',
...missing.slice(i, i + 100)
]);
}
fetchedMissingObjects = true;
}
if (fetchedMissingObjects && !canSkipFetchHead) {
await this.execGit(args.filter(arg => arg !== '--unshallow'));
}
await this.tryConfigUnset('remote.origin.partialclonefilter');
await this.tryConfigUnset('remote.origin.promisor');
}
}
async getDefaultBranch(repositoryUrl) {
let output;

View File

@ -14,6 +14,8 @@ import {GitVersion} from './git-version.js'
// sparse-checkout not [well-]supported before 2.28 (see https://github.com/actions/checkout/issues/1386)
export const MinimumGitVersion = new GitVersion('2.18')
export const MinimumGitSparseCheckoutVersion = new GitVersion('2.28')
const MinimumGitRefetchVersion = new GitVersion('2.36')
const MinimumGitFetchWithoutWritingHeadVersion = new GitVersion('2.29')
export interface IGitCommandManager {
branchDelete(remote: boolean, branch: string): Promise<void>
@ -292,8 +294,22 @@ class GitCommandManager {
args.push('--progress')
}
const inheritedFilter =
!options.filter &&
(await this.configExists('remote.origin.partialclonefilter'))
const restoreFullHistory =
inheritedFilter && !(options.fetchDepth && options.fetchDepth > 0)
if (options.filter) {
args.push(`--filter=${options.filter}`)
} else if (inheritedFilter) {
args.push('--no-filter')
if (
restoreFullHistory &&
this.gitVersion.checkMinimum(MinimumGitRefetchVersion)
) {
args.push('--refetch')
}
}
if (options.fetchDepth && options.fetchDepth > 0) {
@ -315,6 +331,52 @@ class GitCommandManager {
await retryHelper.execute(async () => {
await that.execGit(args)
})
if (restoreFullHistory) {
const canSkipFetchHead = this.gitVersion.checkMinimum(
MinimumGitFetchWithoutWritingHeadVersion
)
let fetchedMissingObjects = false
let previousMissing = new Set<string>()
while (true) {
const output = await this.execGit(
['rev-list', '--objects', '--all', '--quiet', '--missing=print'],
false,
true
)
const missing = [
...output.stdout.matchAll(/^\?([0-9a-f]{40}|[0-9a-f]{64})\r?$/gm)
].map(match => match[1])
if (!missing.length) {
break
}
if (missing.some(oid => previousMissing.has(oid))) {
throw new Error(
'Unable to fetch all objects from the previous partial checkout.'
)
}
previousMissing = new Set(missing)
for (let i = 0; i < missing.length; i += 100) {
await this.execGit([
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--no-recurse-submodules',
'--no-filter',
canSkipFetchHead ? '--no-write-fetch-head' : '--append',
'origin',
...missing.slice(i, i + 100)
])
}
fetchedMissingObjects = true
}
if (fetchedMissingObjects && !canSkipFetchHead) {
await this.execGit(args.filter(arg => arg !== '--unshallow'))
}
await this.tryConfigUnset('remote.origin.partialclonefilter')
await this.tryConfigUnset('remote.origin.promisor')
}
}
async getDefaultBranch(repositoryUrl: string): Promise<string> {