Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
overrides:
minimatch@<10.2.1: '>=10.2.1'
24 changes: 23 additions & 1 deletion src/git/fetch-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,12 +561,34 @@ const handleMissingCache = async (
return { usedCache: false, worktreeUsed: false };
};

const cloneOrUpdateInFlight = new Map<string, Promise<CloneResult>>();

// Clone or update a repository using persistent cache
const cloneOrUpdateRepo = async (
const cloneOrUpdateRepo = (
params: FetchParams,
outDir: string,
): Promise<CloneResult> => {
const cachePath = getPersistentCachePath(params.repo);
const inflight = cloneOrUpdateInFlight.get(cachePath);
if (inflight !== undefined) {
return inflight.then(() => cloneOrUpdateRepo(params, outDir));
}
const promise = (async () => {
try {
return await cloneOrUpdateRepoImpl(params, outDir, cachePath);
} finally {
cloneOrUpdateInFlight.delete(cachePath);
}
})();
cloneOrUpdateInFlight.set(cachePath, promise);
return promise;
};

const cloneOrUpdateRepoImpl = async (
params: FetchParams,
outDir: string,
cachePath: string,
): Promise<CloneResult> => {
const cacheExists = await exists(cachePath);
const cacheValid = cacheExists && (await isValidGitRepo(cachePath));
const isCommitRef = /^[0-9a-f]{7,40}$/i.test(params.ref);
Expand Down
80 changes: 80 additions & 0 deletions tests/fetch-source-file-protocol.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,83 @@ test("sync fetches missing commit from local cache", async () => {
await rm(tmpRoot, { recursive: true, force: true });
}
});

test("concurrent syncs for the same repo clone only once", async () => {
const tmpRoot = path.join(
tmpdir(),
`docs-cache-concurrent-${Date.now().toString(36)}`,
);
const binDir = path.join(tmpRoot, "bin");
const logPath = path.join(tmpRoot, "git.log");
const cacheDir = path.join(tmpRoot, ".docs");
const configPath = path.join(tmpRoot, "docs.config.json");
const gitCacheRoot = path.join(tmpRoot, "git-cache");
const repo = "https://example.com/concurrent-repo.git";

await mkdir(binDir, { recursive: true });
await writeGitShim(binDir, logPath);
await writeFile(logPath, "", "utf8");

const config = {
$schema:
"https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",
sources: [
{ id: "a", repo, include: ["docs"] },
{ id: "b", repo, include: ["docs"] },
{ id: "c", repo, include: ["docs"] },
],
};
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");

const previousPath = process.env.PATH ?? process.env.Path;
const previousPathExt = process.env.PATHEXT;
const previousGitDir = process.env.DOCS_CACHE_GIT_DIR;
const nextPath =
process.platform === "win32"
? binDir
: `${binDir}${path.delimiter}${previousPath ?? ""}`;
process.env.PATH = nextPath;
process.env.Path = nextPath;
if (process.platform === "win32") {
process.env.PATHEXT = previousPathExt ?? ".COM;.EXE;.BAT;.CMD";
}
process.env.DOCS_CACHE_GIT_DIR = gitCacheRoot;
process.env.GIT_TERMINAL_PROMPT = "0";

try {
await runSync(
{
configPath,
cacheDirOverride: cacheDir,
json: false,
lockOnly: false,
offline: false,
failOnMiss: false,
concurrency: 3,
},
{
resolveRemoteCommit: async () => ({
repo,
ref: "HEAD",
resolvedCommit: "abc123",
}),
},
);

const logRaw = await readFile(logPath, "utf8");
const entries = logRaw
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
const clones = entries.filter(
(args) => args.includes("clone") && args.includes(repo),
);
assert.equal(clones.length, 1, "expected repo to be cloned exactly once");
} finally {
process.env.PATH = previousPath;
process.env.Path = previousPath;
process.env.PATHEXT = previousPathExt;
process.env.DOCS_CACHE_GIT_DIR = previousGitDir;
await rm(tmpRoot, { recursive: true, force: true });
}
});
Loading