mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-24 23:31:20 +00:00
## Summary When Git is available but the opened folder is not a repository (temporary workspaces, plain folders), the Files panel's Changes and Commits views now say so and offer **Initialize Git**. Directory-only mode, where the backend cannot run Git, is unchanged. ## Design - **Route:** `POST /api/workspaces/:id/git-init` accepts only the exact opened folder. - It refuses directory-only mode, folders already inside a repository, unowned folders and folders being deleted. - It checks that OpenCode resolves the same folder as the project directory, then calls native `vcs.init` once, without retry. - A 501 maps to a localized "not supported" message. - **Refresh after init:** - The server re-reads the worktree inventory and resets the git-status cache. - The UI refreshes file caches, worktrees, project metadata and the session list. - Initialization changes the folder's native project ID, so stale project metadata would otherwise leave later session pages empty. - **No confirmation dialog:** creating `.git` is non-destructive and easily undone. - **Also fixed:** a spurious "detached HEAD" suffix on non-repository folders in the worktree selector. ## Validation - Server unit and route tests: authorization, refusals, 501, refresh. - New browser regression `git-init.test.ts` (2/2): empty state, button, refreshed Changes and branch, 501 message with no retry. The existing Files panel suite passes 18/18. - Isolated native check `scripts/test-git-init-native.mjs` against a private OpenCode 2.0.27 daemon. - Server and UI typechecks pass. Oversized files touched: `workspaces/manager.ts` (~1307 lines) and `lib/api-client.ts` (~809 lines). An independent gatekeeper review will be published before the admin merge.
49 lines
3.2 KiB
JavaScript
49 lines
3.2 KiB
JavaScript
// Native Git initialization fixture. Invoke with an absolute CLI path:
|
|
// node scripts/test-git-init-native.mjs <absolute opencode executable>
|
|
// runIsolated starts a private daemon with its own HOME/XDG/config/database and
|
|
// uses synthetic folders only; it never touches the shared daemon.
|
|
import assert from "node:assert/strict"
|
|
import { mkdir, writeFile } from "node:fs/promises"
|
|
import path from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
import { tsImport } from "tsx/esm/api"
|
|
import { runIsolated } from "./test-opencode-location-native.mjs"
|
|
|
|
export async function testNativeGitInit({ client, root }) {
|
|
const { initializeNativeGit, GitInitError } = await tsImport("../packages/server/src/workspaces/git-init.ts", import.meta.url)
|
|
const { listNativeWorktrees } = await tsImport("../packages/server/src/workspaces/native-worktrees.ts", import.meta.url)
|
|
const { getWorktreeGitStatus } = await tsImport("../packages/server/src/workspaces/git-status.ts", import.meta.url)
|
|
const { getGitHistory } = await tsImport("../packages/server/src/workspaces/git-history.ts", import.meta.url)
|
|
const parent = path.join(root, "markerless-parent")
|
|
const folder = path.join(parent, "opened")
|
|
await mkdir(folder, { recursive: true })
|
|
await writeFile(path.join(folder, "notes.txt"), "synthetic fixture\n")
|
|
const context = { client, location: { directory: folder }, workspacePath: folder, toHost: async directory => directory }
|
|
|
|
// A markerless child resolves to its own project directory, never its parent.
|
|
const before = await client.location.get({ location: { directory: folder } })
|
|
assert.equal(path.resolve(before.project.directory), path.resolve(folder))
|
|
const session = await client.session.create({ location: { directory: folder } })
|
|
assert.equal((await listNativeWorktrees(context)).isGitRepo, false)
|
|
|
|
await initializeNativeGit(context)
|
|
const after = await client.location.get({ location: { directory: folder } })
|
|
assert.equal(path.resolve(after.project.directory), path.resolve(folder))
|
|
// The native project identity changes with the repository; sessions follow it.
|
|
const moved = await client.session.get({ sessionID: session.id })
|
|
assert.equal(moved.projectID, after.project.id)
|
|
assert.ok((await client.session.list({ directory: folder })).data.some(entry => entry.id === session.id))
|
|
|
|
const catalogue = await listNativeWorktrees(context)
|
|
assert.equal(catalogue.isGitRepo, true)
|
|
assert.deepEqual(catalogue.worktrees.map(entry => entry.kind), ["root"])
|
|
const status = await getWorktreeGitStatus({ workspaceFolder: folder })
|
|
assert.ok(status.some(entry => entry.path === "notes.txt"), "Changes lists the untracked file of the unborn repository")
|
|
assert.deepEqual((await getGitHistory(folder)).commits, [])
|
|
|
|
await assert.rejects(initializeNativeGit(context), error => error instanceof GitInitError && error.code === "git_init_repository_exists")
|
|
await client.session.remove({ sessionID: session.id })
|
|
console.log("PASS: native vcs.init on the exact markerless folder, project/session continuity, unborn inventory/status/history and repeat refusal")
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await runIsolated(process.argv[2], testNativeGitInit)
|