update runtime, fix lock bug

AuthorKonata <konata@posteo.jp>
Date
Commit9ff943dd93d66c1221cf3d88a0ebae36e55b652c
Parent5612339
5 files changed, 75 insertions(+), 6 deletions(-)
▾MContainerfile
@@ -1,4 +1,4 @@
FROM oven/bun:1 AS builder
FROM oven/bun:1.4 AS builder
WORKDIR /app
COPY package.json bun.lock* ./
COPY scripts ./scripts
@@ -6,7 +6,7 @@ COPY src/styles ./src/styles
RUN mkdir -p public/assets
RUN bun install --frozen-lockfile
FROM oven/bun:1 AS base
FROM oven/bun:1.4 AS base
WORKDIR /app
RUN apt-get --update install -y --no-install-recommends \
▾Msrc/constants.ts
@@ -101,6 +101,8 @@ export const MAX_BRANCH_CACHE = 200;
export const MAX_TAG_CACHE = 200;
export const REF_CACHE_TTL_MS = 30_000;
export const STALE_LOCK_MS = 60_000;
// Paths — derived from config.DATA_DIR via getters so they reflect overrides.
export const paths = {
get DB_PATH() {
▾Msrc/services/git.ts
@@ -354,6 +354,25 @@ export const git = {
});
},
async ensureBare(name: string): Promise<void> {
const cfg = path.join(repoPath(name), "config");
return withRepoLock(name, async () => {
const current = await $`git config --file ${cfg} --get core.bare`
.quiet()
.nothrow();
if (current.exitCode === 0 && current.text().trim() === "true") {
return;
}
const res = await $`git config --file ${cfg} core.bare true`
.quiet()
.nothrow();
if (res.exitCode !== 0) {
logGitError("ensureBare", name, res.stderr.toString().trim());
}
});
},
async log(
name: string,
ref = "HEAD",
▾Msrc/services/repoSync.ts
@@ -5,11 +5,12 @@ import {
readFileSync,
renameSync,
rmSync,
statSync,
unlinkSync,
} from "node:fs";
import path from "node:path";
import { $ } from "bun";
import config from "../config.ts";
import { paths, VALID_REPO_NAME_RE } from "../constants.ts";
import { paths, STALE_LOCK_MS, VALID_REPO_NAME_RE } from "../constants.ts";
import type { RepositoryRow } from "../db/index.ts";
import { db } from "../db/index.ts";
import { git, repoPath } from "../services/git.ts";
@@ -73,6 +74,28 @@ async function convertNonBareRepos(): Promise<void> {
await Promise.all(conversions);
}
function clearStaleConfigLocks(): void {
let entries: Dirent[];
try {
entries = readdirSync(paths.REPOS_DIR, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (!entry.isDirectory() || !entry.name.endsWith(".git")) continue;
const lockPath = path.join(paths.REPOS_DIR, entry.name, "config.lock");
try {
if (Date.now() - statSync(lockPath).mtimeMs < STALE_LOCK_MS)
continue;
unlinkSync(lockPath);
console.log(`Removed stale config lock: ${entry.name}`);
} catch {
// No lock file (the normal case), or it vanished meanwhile.
}
}
}
export function listDiskRepoNames(): string[] {
try {
return readdirSync(paths.REPOS_DIR, { withFileTypes: true })
@@ -99,7 +122,11 @@ export async function ensureRepoRecord(name: string): Promise<RepositoryRow> {
// freshly created): make sure git treats it as bare before we record and
// serve it. Doing this only on discovery — not on every read — keeps repo
// page views free of a per-request subprocess spawn and config write.
await $`git config --file ${path.join(repoPath(name), "config")} core.bare true`;
//
// ensureBare never throws. It must not be able to block the insert below:
// without a DB record every later request lands here again, so one failed
// config write would turn into a permanent per-request failure loop.
await git.ensureBare(name);
const branch = await git.defaultBranch(name);
const now = new Date().toISOString();
return await db
@@ -183,6 +210,7 @@ async function ensureSigningSetup(): Promise<void> {
export async function syncStartup(): Promise<void> {
await ensureSigningSetup();
clearStaleConfigLocks();
await convertNonBareRepos();
const diskNames = new Set(listDiskRepoNames());
const dbRepos = await db
▾Mtests/e2e.repos.test.ts
@@ -1,5 +1,5 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { rmSync } from 'node:fs';
import { rmSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { chromium } from 'playwright';
import type { Browser, BrowserContext } from 'playwright';
@@ -368,4 +368,24 @@ describe('repos', () => {
await db.deleteFrom('repositories').where('name', '=', name).execute();
rmSync(dir, { recursive: true, force: true });
});
test('repo is registered even with a stale config.lock', async () => {
const name = 'stale-lock-repo';
const dir = `${process.cwd()}/${DATA_DIR}/repos/${name}.git`;
rmSync(dir, { recursive: true, force: true });
spawnSync('git', ['init', '--bare', dir], { stdio: 'ignore' });
// Drop core.bare so ensureBare has to attempt a write, then block it.
spawnSync('git', ['config', '--file', `${dir}/config`, '--unset', 'core.bare'], {
stdio: 'ignore',
});
writeFileSync(`${dir}/config.lock`, '');
const { ensureRepoRecord } = await import('../src/services/repoSync.ts');
const repo = await ensureRepoRecord(name);
expect(repo.name).toBe(name);
await db.deleteFrom('repositories').where('name', '=', name).execute();
rmSync(dir, { recursive: true, force: true });
});
});