mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(clone): refresh the preview after the preflight, tidy wording and the invariants paragraph
This commit is contained in:
1 parent
d2c75784d3
commit
a4bd35b704
3 files changed
+18
-3
No files matched your search
@@ -474,7 +474,7 @@ Tests: `test/file-preview-markdown.test.ts` (jsdom-in-vm, pins every rule above)
|
||||
|
||||
### Clone a repository as a case
|
||||
|
||||
**Clone Repo tab** (issue #236, proposed by @DodgyBadger): `POST /api/cases/clone` clones a public repository into the caller's case space and registers it as a normal local case; `POST /api/cases/clone-preflight` answers "can this be cloned anonymously, and what refs does it have?" while the user is still typing. Core in `src/git-clone.ts`, split into a PURE half (URL parse, argv/env, `ls-remote` parse, stderr classification) and a thin IO half (`probeGitRemote`, `cloneRepository`). An optional `path` (the **Clone into a custom folder** checkbox) is the folder the repository is cloned INTO, exactly as given (the case name adds no subfolder), and it registers a LINKED case, the same way `POST /api/cases` with a `path` does: it is judged by `prepareNewCasePath()` (`web/case-path.ts`: blocked trees, symlinks, parent must exist), is admin-only in multi-user mode (it writes an absolute path into the shared linked-cases registry), and must be empty or not exist yet. An existing empty folder is cloned into directly (`CloneOptions.intoEmptyDir`: no temporary sibling and no rename, so a mount point works; a failed attempt empties it again but never removes it); a folder with anything in it is refused. Deleting such a case unlinks it and never removes the files. If the registry write fails after git finished, the folder is kept and the error says where it is.
|
||||
**Clone Repo tab** (issue #236, proposed by @DodgyBadger): `POST /api/cases/clone` clones a public repository into the caller's case space and registers it as a normal local case; `POST /api/cases/clone-preflight` answers "can this be cloned anonymously, and what refs does it have?" while the user is still typing. Core in `src/git-clone.ts`, split into a PURE half (URL parse, argv/env, `ls-remote` parse, stderr classification) and a thin IO half (`probeGitRemote`, `cloneRepository`). An optional `path` is the folder the repository is cloned INTO, exactly as given (the case name adds no subfolder), and it registers a LINKED case, the same way `POST /api/cases` with a `path` does: it is judged by `prepareNewCasePath()` (`web/case-path.ts`: blocked trees, symlinks, parent must exist), is admin-only in multi-user mode (it writes an absolute path into the shared linked-cases registry), and must be empty or not exist yet. An existing empty folder is cloned into directly (`CloneOptions.intoEmptyDir`: no temporary sibling and no rename, so a mount point works; a failed attempt empties it again but never removes it); a folder with anything in it is refused. Deleting such a case unlinks it and never removes the files. If the registry write fails after git finished, the folder is kept and the error says where it is.
|
||||
|
||||
- ⚠️ **The URL is a code-execution surface, which is why it is parsed rather than forwarded.** `ext::sh -c <cmd>` makes git run an arbitrary command as its transport, and ANY `<name>::<payload>` dispatches to a `git-remote-<name>` helper, so every `::` form is refused outright. A repository starting with `-` is read by git as a flag; that is rejected AND every spawn puts `--` before the operands, because either defence alone is one edit away from being a hole. Spawns are argv arrays, never a shell (unlike `remote-hosts.ts`, which does build a shell line and must `shellescape`). The Zod schema deliberately only length-bounds `repository` — a weaker regex duplicate of `parseGitRepositoryUrl` would be the copy that drifts.
|
||||
- ⚠️ **Non-interactive or it hangs the request.** The clone is synchronous by design (no job store, no polling, no cancellation surface), so an invisible credential prompt would pin an open HTTP request until the timeout. `gitNonInteractiveEnv()` closes all four prompt paths at once: `GIT_TERMINAL_PROMPT=0`, empty `GIT_ASKPASS`/`SSH_ASKPASS` + `SSH_ASKPASS_REQUIRE=never` + empty `DISPLAY`, `GCM_INTERACTIVE=never`, and `ssh -oBatchMode=yes`. `HOME`/`PATH` are inherited on purpose — a user whose own agent or credential helper already works keeps working (so a private repo may well clone; Codeman just never collects or stores credentials, and refuses a `user:password@` URL). ⚠️ In multi-user mode a NON-ADMIN's clone and preflight run with `git -c credential.helper=` (`cloneWithoutCredentialHelpers`), because the helpers belong to the one server account every user shares; admins and single-user mode keep them. The Docker server image relies on exactly this: when built with the opt-in `CODEMAN_INSTALL_GH`/`CODEMAN_INSTALL_AZ` args it configures `gh` (github.com) and an `az`-backed helper (`docker/git-credential-azure-cli`, Azure DevOps) in its SYSTEM gitconfig, both silent until the user signs the CLI in, so a private clone there still fails fast rather than prompting.
|
||||
|
||||
@@ -4291,7 +4291,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
openCloneCasePathPicker() {
|
||||
const input = document.getElementById('cloneCasePath');
|
||||
PathPicker.open({
|
||||
title: 'Choose the folder to clone into',
|
||||
title: 'Choose the folder to clone inside',
|
||||
initialPath: input.value.trim(),
|
||||
directoriesOnly: true,
|
||||
onSelect: (path) => {
|
||||
@@ -4401,6 +4401,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// the case name is validated against), but never over a name the user typed.
|
||||
const nameInput = document.getElementById('cloneCaseName');
|
||||
if (nameInput && !this._cloneNameEdited && parse.suggestedName) nameInput.value = parse.suggestedName;
|
||||
this.updateCloneCasePathPreview();
|
||||
|
||||
const where = parse.owner ? `${parse.provider} ${parse.owner}/${parse.repo}` : `${parse.provider} ${parse.repo}`;
|
||||
if (data.gitAvailable === false) {
|
||||
@@ -4455,7 +4456,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
const customFolder = !!document.getElementById('cloneCustomPathToggle')?.checked;
|
||||
if (customFolder && !(document.getElementById('cloneCasePath')?.value || '').trim()) {
|
||||
this.showToast('Choose the folder to clone into', 'error');
|
||||
this.showToast('Choose the folder to clone inside', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,4 +72,18 @@ describe('Clone Repo custom folder', () => {
|
||||
const { body } = await requestBody(fetchMock);
|
||||
expect(body.path).toBeUndefined();
|
||||
});
|
||||
|
||||
it('refreshes the preview when the preflight replaces the case name', () => {
|
||||
const { app, doc, set } = bootClone();
|
||||
set('cloneRepoUrl', 'https://github.com/opticon454/bifrost?tab=readme-ov-file');
|
||||
set('cloneCaseName', 'bifrost-tab-readme-ov-file');
|
||||
set('cloneCasePath', '/mnt/projects');
|
||||
app.updateCloneCasePathPreview();
|
||||
expect(doc.getElementById('cloneCasePathPreview')?.textContent).toBe(
|
||||
'Will clone into: /mnt/projects/bifrost-tab-readme-ov-file'
|
||||
);
|
||||
app._applyClonePreflight({ gitAvailable: true, parse: { cloneable: true, suggestedName: 'bifrost' } }, '');
|
||||
expect((doc.getElementById('cloneCaseName') as HTMLInputElement).value).toBe('bifrost');
|
||||
expect(doc.getElementById('cloneCasePathPreview')?.textContent).toBe('Will clone into: /mnt/projects/bifrost');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user