mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
fix(cases): clone into the chosen folder itself, not a subfolder of it
The custom folder was treated as a parent, so the repository landed in <folder>/<case name>. The chosen folder is now the clone destination: an empty existing folder is cloned into directly (CloneOptions.intoEmptyDir, no temporary sibling, never removed on failure), a new folder is created, and a folder with files is refused.
This commit is contained in:
1 parent
0bd15ddc84
commit
68bd7e4d15
8 files changed
+93
-43
No files matched your search
@@ -3658,15 +3658,15 @@
|
||||
</div>
|
||||
<div class="form-row" id="cloneCustomPathToggleRow">
|
||||
<label class="checkbox-row"><input type="checkbox" id="cloneCustomPathToggle" onchange="app.toggleCloneCustomPath()"> 📁 Clone into a custom folder</label>
|
||||
<span class="form-hint">By default the clone goes under ~/codeman-cases. Choose another folder and it is cloned there instead; it is listed like any other case, and deleting the case only unlinks it (your files stay).</span>
|
||||
<span class="form-hint">By default the clone goes under ~/codeman-cases. Choose a folder and the repository is cloned into that folder itself (it must be empty); it is listed like any other case, and deleting the case only unlinks it (your files stay).</span>
|
||||
</div>
|
||||
<div class="form-row" id="cloneCustomPathRow" style="display:none">
|
||||
<label>Parent Folder</label>
|
||||
<label>Folder</label>
|
||||
<div class="path-input-group">
|
||||
<input type="text" id="cloneCasePath" placeholder="~/projects" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" oninput="app.updateCloneCasePathPreview()">
|
||||
<button type="button" class="btn path-input-browse" onclick="app.openCloneCasePathPicker()">Browse…</button>
|
||||
</div>
|
||||
<span class="form-hint" id="cloneCasePathPreview">Pick the folder the clone should be created inside.</span>
|
||||
<span class="form-hint" id="cloneCasePathPreview">Pick the empty folder to clone into.</span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Branch or Tag (optional)</label>
|
||||
|
||||
@@ -4142,26 +4142,24 @@ Object.assign(CodemanApp.prototype, {
|
||||
const hint = document.getElementById('cloneCaseNameHint');
|
||||
if (hint) {
|
||||
hint.textContent = custom.checked
|
||||
? 'Filled in from the URL. Cloned into a folder of this name inside the parent folder below.'
|
||||
? 'Filled in from the URL. Names the case only: the repository is cloned into the folder chosen below.'
|
||||
: 'Filled in from the URL. Cloned into ~/codeman-cases/<name>, so deleting the case deletes this working tree.';
|
||||
}
|
||||
this.updateCloneCasePathPreview();
|
||||
},
|
||||
|
||||
/** The folder the clone would be created in: the parent field plus the case name. */
|
||||
/** The folder the repository is cloned into: exactly what the field holds (no case name is added). */
|
||||
_cloneTargetPath() {
|
||||
const rawParent = (document.getElementById('cloneCasePath')?.value || '').trim();
|
||||
const name = (document.getElementById('cloneCaseName')?.value || '').trim();
|
||||
if (!rawParent || !name) return '';
|
||||
const raw = (document.getElementById('cloneCasePath')?.value || '').trim();
|
||||
// Trailing slashes off, but `/` stays the root rather than becoming an empty path.
|
||||
return `${rawParent.replace(/\/+$/, '')}/${name}`;
|
||||
return raw.length > 1 ? raw.replace(/\/+$/, '') || '/' : raw;
|
||||
},
|
||||
|
||||
updateCloneCasePathPreview() {
|
||||
const hint = document.getElementById('cloneCasePathPreview');
|
||||
if (!hint) return;
|
||||
const target = this._cloneTargetPath();
|
||||
hint.textContent = target ? `Will clone into: ${target}` : 'Pick the folder the clone should be created inside.';
|
||||
hint.textContent = target ? `Will clone into: ${target}` : 'Pick the empty folder to clone into.';
|
||||
},
|
||||
|
||||
openCloneCasePathPicker() {
|
||||
@@ -4214,14 +4212,12 @@ Object.assign(CodemanApp.prototype, {
|
||||
onCloneNameEdited() {
|
||||
// Once the user types a name, autofill stops fighting them.
|
||||
this._cloneNameEdited = !!document.getElementById('cloneCaseName')?.value.trim();
|
||||
this.updateCloneCasePathPreview();
|
||||
},
|
||||
|
||||
onCloneUrlInput() {
|
||||
const url = document.getElementById('cloneRepoUrl')?.value.trim() || '';
|
||||
const nameInput = document.getElementById('cloneCaseName');
|
||||
if (nameInput && !this._cloneNameEdited) nameInput.value = this._repoNameFromUrl(url);
|
||||
this.updateCloneCasePathPreview();
|
||||
clearTimeout(this._clonePreflightTimer);
|
||||
this._clonePreflightAbort?.abort();
|
||||
this._clonePreflightAbort = null;
|
||||
@@ -4277,7 +4273,6 @@ 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) {
|
||||
|
||||
@@ -714,6 +714,7 @@ export function registerCaseRoutes(
|
||||
|
||||
const casesDir = resolveCasesDir(user);
|
||||
let casePath: string | null;
|
||||
let intoEmptyDir = false;
|
||||
if (customPath !== undefined) {
|
||||
const prepared = await prepareNewCasePath(customPath, {
|
||||
home: homedir(),
|
||||
@@ -738,18 +739,12 @@ export function registerCaseRoutes(
|
||||
: prepared.code === 'EXISTS'
|
||||
? ApiErrorCode.ALREADY_EXISTS
|
||||
: ApiErrorCode.INVALID_INPUT,
|
||||
prepared.reason
|
||||
);
|
||||
}
|
||||
// git clones into a fresh folder it creates itself; an empty existing one is refused rather than
|
||||
// removed and re-made (the removal could race another writer).
|
||||
if (prepared.existedEmpty) {
|
||||
reply.code(409);
|
||||
return createErrorResponse(
|
||||
ApiErrorCode.ALREADY_EXISTS,
|
||||
`${prepared.path} already exists. Pick a parent folder where the case folder does not exist yet.`
|
||||
prepared.code === 'EXISTS'
|
||||
? `${customPath} already has files in it. Pick an empty folder, or a folder that does not exist yet.`
|
||||
: prepared.reason
|
||||
);
|
||||
}
|
||||
intoEmptyDir = prepared.existedEmpty;
|
||||
casePath = prepared.path;
|
||||
// The same folder listed as a local case in the cases dir would make deleting it remove files.
|
||||
if (existsSync(join(casesDir, name))) {
|
||||
@@ -776,7 +771,8 @@ export function registerCaseRoutes(
|
||||
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, `That folder is already the case "${linkedAs}"`);
|
||||
}
|
||||
if (
|
||||
existsSync(casePath) ||
|
||||
// A custom folder was judged by prepareNewCasePath (an existing empty one is fine).
|
||||
(customPath === undefined && existsSync(casePath)) ||
|
||||
linkedCases[name] ||
|
||||
dockerCases.some((item) => item.name === name) ||
|
||||
remoteCases.some((item) => item.name === name)
|
||||
@@ -797,6 +793,7 @@ export function registerCaseRoutes(
|
||||
const clone = await cloneRepository({
|
||||
repository: parsed.repository,
|
||||
destination: casePath,
|
||||
...(intoEmptyDir ? { intoEmptyDir: true } : {}),
|
||||
withoutCredentialHelpers: cloneWithoutCredentialHelpers(req),
|
||||
...(ref ? { ref } : {}),
|
||||
...(shallow ? { shallow: true } : {}),
|
||||
|
||||
Reference in new issue
Block a user