-
Notifications
You must be signed in to change notification settings - Fork 0
Add persistent cache for git mirrors and npm artifacts #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
SoonIter
wants to merge
1
commit into
main
Choose a base branch
from
codex/git-clone-perf-cache
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| import { execFile } from 'node:child_process' | ||
| import { access, mkdir, rm } from 'node:fs/promises' | ||
| import path from 'node:path' | ||
| import { promisify } from 'node:util' | ||
| import { ErrorCode, GitError } from '../errors' | ||
| import { createCacheTempDir, getCacheKeyPath, getCachePaths, withCacheLock } from './index' | ||
|
|
||
| const execFileAsync = promisify(execFile) | ||
|
|
||
| function getGitEnv() { | ||
| return { ...process.env, GIT_TERMINAL_PROMPT: '0' } | ||
| } | ||
|
|
||
| function isFullCommitHash(ref: string): boolean { | ||
| return /^[0-9a-f]{40}$/i.test(ref) | ||
| } | ||
|
|
||
| function getRepoLockKey(repoUrl: string): string { | ||
| return `git-repo:${repoUrl}` | ||
| } | ||
|
|
||
| async function getMirrorPath(repoUrl: string): Promise<string> { | ||
| const cache = await getCachePaths() | ||
| return path.join(getCacheKeyPath(cache.reposDir, repoUrl), 'mirror.git') | ||
| } | ||
|
|
||
| async function runGit(args: string[], options?: { cwd?: string; timeout?: number }) { | ||
| return execFileAsync('git', args, { | ||
| cwd: options?.cwd, | ||
| env: getGitEnv(), | ||
| timeout: options?.timeout ?? 60_000, | ||
| }) | ||
| } | ||
|
|
||
| async function ensureMirrorExists(repoUrl: string, mirrorPath: string): Promise<void> { | ||
| try { | ||
| await access(path.join(mirrorPath, 'HEAD')) | ||
| return | ||
| } catch {} | ||
|
|
||
| await mkdir(path.dirname(mirrorPath), { recursive: true }) | ||
|
|
||
| try { | ||
| await runGit(['clone', '--mirror', repoUrl, mirrorPath]) | ||
| } catch (error) { | ||
| await rm(path.dirname(mirrorPath), { recursive: true, force: true }).catch(() => {}) | ||
| throw new GitError({ | ||
| code: ErrorCode.GIT_CLONE_FAILED, | ||
| operation: 'clone', | ||
| repoUrl, | ||
| message: `Failed to clone repository ${repoUrl}`, | ||
| cause: error as Error, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| async function updateMirror(repoUrl: string, mirrorPath: string): Promise<void> { | ||
| try { | ||
| await runGit(['--git-dir', mirrorPath, 'remote', 'update', '--prune', 'origin']) | ||
| } catch (error) { | ||
| throw new GitError({ | ||
| code: ErrorCode.GIT_FETCH_FAILED, | ||
| operation: 'fetch', | ||
| repoUrl, | ||
| message: `Failed to fetch repository ${repoUrl}`, | ||
| cause: error as Error, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| async function tryResolveCommit(mirrorPath: string, target: string): Promise<string | null> { | ||
| try { | ||
| const { stdout } = await runGit([ | ||
| '--git-dir', | ||
| mirrorPath, | ||
| 'rev-parse', | ||
| '--verify', | ||
| `${target}^{commit}`, | ||
| ]) | ||
| return stdout.trim().split('\n')[0]?.trim() || null | ||
| } catch { | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| async function fetchCommit(repoUrl: string, mirrorPath: string, commit: string): Promise<void> { | ||
| try { | ||
| await runGit(['--git-dir', mirrorPath, 'fetch', 'origin', commit]) | ||
| } catch (error) { | ||
| throw new GitError({ | ||
| code: ErrorCode.GIT_FETCH_FAILED, | ||
| operation: 'fetch', | ||
| repoUrl, | ||
| ref: commit, | ||
| message: `Failed to fetch commit ${commit} from ${repoUrl}`, | ||
| cause: error as Error, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| export async function resolveGitCommitFromMirror( | ||
| repoUrl: string, | ||
| ref: string | null, | ||
| ): Promise<string> { | ||
| const target = ref ?? 'HEAD' | ||
|
|
||
| return withCacheLock(getRepoLockKey(repoUrl), async () => { | ||
| const mirrorPath = await getMirrorPath(repoUrl) | ||
| await ensureMirrorExists(repoUrl, mirrorPath) | ||
|
|
||
| if (isFullCommitHash(target)) { | ||
| const existingCommit = await tryResolveCommit(mirrorPath, target) | ||
| if (existingCommit) { | ||
| return existingCommit | ||
| } | ||
|
|
||
| await fetchCommit(repoUrl, mirrorPath, target) | ||
| } else { | ||
| await updateMirror(repoUrl, mirrorPath) | ||
| } | ||
|
|
||
| const resolvedCommit = await tryResolveCommit(mirrorPath, target) | ||
| if (resolvedCommit) { | ||
| return resolvedCommit | ||
| } | ||
|
|
||
| throw new GitError({ | ||
| code: ErrorCode.GIT_REF_NOT_FOUND, | ||
| operation: 'resolve-ref', | ||
| repoUrl, | ||
| ref: target, | ||
| message: `Unable to resolve git ref "${target}" for ${repoUrl}`, | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| async function addWorktree( | ||
| mirrorPath: string, | ||
| worktreePath: string, | ||
| target: string, | ||
| ): Promise<void> { | ||
| await runGit(['--git-dir', mirrorPath, 'worktree', 'add', '--detach', worktreePath, target]) | ||
| } | ||
|
|
||
| async function removeWorktree(mirrorPath: string, worktreePath: string): Promise<void> { | ||
| try { | ||
| await runGit(['--git-dir', mirrorPath, 'worktree', 'remove', '--force', worktreePath]) | ||
| } catch {} | ||
| await rm(worktreePath, { recursive: true, force: true }).catch(() => {}) | ||
| } | ||
|
|
||
| export async function createGitWorktree( | ||
| repoUrl: string, | ||
| ref: string | null, | ||
| ): Promise<{ worktreePath: string; resolvedCommit: string; cleanup: () => Promise<void> }> { | ||
| const worktreePath = await createCacheTempDir('skills-pm-git-worktree-') | ||
| const resolvedCommit = await resolveGitCommitFromMirror(repoUrl, ref) | ||
| const mirrorPath = await getMirrorPath(repoUrl) | ||
|
|
||
| await withCacheLock(getRepoLockKey(repoUrl), async () => { | ||
| await addWorktree(mirrorPath, worktreePath, resolvedCommit) | ||
| }).catch(async (error) => { | ||
| await rm(worktreePath, { recursive: true, force: true }).catch(() => {}) | ||
| throw new GitError({ | ||
| code: ErrorCode.GIT_CHECKOUT_FAILED, | ||
| operation: 'checkout', | ||
| repoUrl, | ||
| ref: resolvedCommit, | ||
| message: `Failed to checkout commit ${resolvedCommit}`, | ||
| cause: error as Error, | ||
| }) | ||
| }) | ||
|
|
||
| return { | ||
| worktreePath, | ||
| resolvedCommit, | ||
| cleanup: async () => { | ||
| await withCacheLock(getRepoLockKey(repoUrl), async () => { | ||
| await removeWorktree(mirrorPath, worktreePath) | ||
| }).catch(async () => { | ||
| await rm(worktreePath, { recursive: true, force: true }).catch(() => {}) | ||
| }) | ||
| }, | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createGitWorktreecreatesworktreePathbefore resolving the commit; ifresolveGitCommitFromMirror()throws (e.g., network/auth/ref error), the newly created temp directory will be leaked under the cache tmp dir. Resolve the commit first (or wrap resolve in a try/catch that removesworktreePathon failure) before proceeding to add the worktree.