Skip to content

Commit bcec731

Browse files
Shallow fetch: negotiate from mirror tips with alternate ref discovery off (#50)
* Shallow fetch: negotiate from mirror tips with alternate ref discovery off With the mirror attached as an alternate, a deepening fetch treats every mirror ref as a known tip: negotiation offers them all as haves and the post-fetch connectivity check walks the whole mirror object graph, so the shallow path costs time proportional to mirror history instead of to the requested checkout. Set core.alternateRefsCommand to a no-op so alternates only contribute objects, and pass the mirror's copy of the requested branch/tag (or the PR base branch, or mirror HEAD) as --negotiation-tip so the server still sends only the delta from the mirror. Requires git >= 2.19; older git keeps the previous behaviour. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: cover shallow fetch when the mirror is ahead of the wanted commit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Gate core.alternateRefsCommand on git 2.20 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 046e278 commit bcec731

8 files changed

Lines changed: 735 additions & 51 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ When a mirror already exists, the action mounts its Sticky Disk and uses [Git's
4848

4949
The workspace then performs the normal upstream-compatible fetch. Objects already present in the mirror are reused locally, while objects that are newer than the mirror are fetched from GitHub. This means a stale mirror does not block checkout or prevent the requested commit from being fetched.
5050

51+
For shallow checkouts (`fetch-depth` greater than 0), the fetch tells GitHub which commits the mirror already has for the requested branch or tag, so only the delta since the mirror was last refreshed is transferred. Git is also stopped from treating every mirror ref as a starting point for its post-fetch checks, which would otherwise cost time proportional to the whole mirror history rather than to the requested checkout.
52+
5153
The mirror always contains full history, but the workspace still respects inputs such as `fetch-depth`, `fetch-tags`, sparse checkout, LFS, and submodules. For example, `fetch-depth: 1` still produces a shallow workspace checkout.
5254

5355
### 3. Post-job refresh

__test__/blacksmith-cache.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,49 @@ describe('blacksmith-cache tests', () => {
448448
})
449449
})
450450

451+
describe('shallowNegotiationTipRefs', () => {
452+
it('offers the same branch then HEAD', () => {
453+
expect(
454+
blacksmithCache.shallowNegotiationTipRefs('refs/heads/feature', '')
455+
).toEqual(['refs/heads/feature', 'HEAD'])
456+
})
457+
458+
it('offers the same tag then HEAD', () => {
459+
expect(
460+
blacksmithCache.shallowNegotiationTipRefs('refs/tags/v1', 'main')
461+
).toEqual(['refs/tags/v1', 'HEAD'])
462+
})
463+
464+
it('offers the base branch for pull request refs', () => {
465+
expect(
466+
blacksmithCache.shallowNegotiationTipRefs('refs/pull/42/merge', 'main')
467+
).toEqual(['refs/heads/main', 'HEAD'])
468+
expect(
469+
blacksmithCache.shallowNegotiationTipRefs(
470+
'refs/pull/42/head',
471+
'refs/heads/release'
472+
)
473+
).toEqual(['refs/heads/release', 'HEAD'])
474+
})
475+
476+
it('falls back to HEAD for pull request refs without a base and for bare SHAs', () => {
477+
expect(
478+
blacksmithCache.shallowNegotiationTipRefs('refs/pull/42/merge', '')
479+
).toEqual(['HEAD'])
480+
expect(blacksmithCache.shallowNegotiationTipRefs('', '')).toEqual([
481+
'HEAD'
482+
])
483+
})
484+
485+
it('tries both branch and tag for an unqualified ref', () => {
486+
expect(blacksmithCache.shallowNegotiationTipRefs('v1', '')).toEqual([
487+
'refs/heads/v1',
488+
'refs/tags/v1',
489+
'HEAD'
490+
])
491+
})
492+
})
493+
451494
describe('parseMissingRemoteRefs', () => {
452495
it('extracts a single missing ref', () => {
453496
const stderr =

__test__/git-command-manager.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,46 @@ describe('Test fetchDepth and fetchTags options', () => {
375375
expect.any(Object)
376376
)
377377
})
378+
379+
it('should disable alternate ref discovery and pass negotiation tips for a shallow mirror fetch', async () => {
380+
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
381+
382+
const workingDirectory = 'test'
383+
const lfs = false
384+
const doSparseCheckout = false
385+
git = await commandManager.createCommandManager(
386+
workingDirectory,
387+
lfs,
388+
doSparseCheckout
389+
)
390+
const refSpec = ['refspec1']
391+
const options = {
392+
fetchDepth: 1,
393+
fetchTags: false,
394+
ignoreAlternateRefs: true,
395+
negotiationTips: ['aaaa', 'bbbb']
396+
}
397+
398+
await git.fetch(refSpec, options)
399+
400+
expect(mockExec).toHaveBeenCalledWith(
401+
expect.any(String),
402+
[
403+
'-c',
404+
'protocol.version=2',
405+
'-c',
406+
'core.alternateRefsCommand=true',
407+
'fetch',
408+
'--no-tags',
409+
'--prune',
410+
'--no-recurse-submodules',
411+
'--negotiation-tip=aaaa',
412+
'--negotiation-tip=bbbb',
413+
'--depth=1',
414+
'origin',
415+
'refspec1'
416+
],
417+
expect.any(Object)
418+
)
419+
})
378420
})

0 commit comments

Comments
 (0)