From 55e07b1f91fc22e1bec50176e60d6bc9c58dc4c1 Mon Sep 17 00:00:00 2001 From: Peng Ding Date: Sun, 13 Sep 2026 16:11:41 -0500 Subject: [PATCH] fix: fetch ordering, path traversal guard, unsaved check, CSS var - Fix fetch ordering in handle_git_branches(): fetch before listing so ?fetch=true returns up-to-date branch data - Add path traversal guard in handle_git_worktree_add(): reject worktree names containing os.sep, leading dots, or ".." - Broaden unsaved-buffer check in switchWorktree() from current file only to all modified files (S.modified.size > 0) - Add Escape key handler to switchWorktree unsaved dialog - Replace non-existent CSS variable --fg-muted with --text-dim - Add -- separator in delete_branch() to prevent branch names from being interpreted as flags --- src/tinyleaf/git_ops.py | 2 +- src/tinyleaf/handlers.py | 10 +++++++--- src/tinyleaf/static/js/app.js | 20 ++++++++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/tinyleaf/git_ops.py b/src/tinyleaf/git_ops.py index afd7175..ab2d8cc 100644 --- a/src/tinyleaf/git_ops.py +++ b/src/tinyleaf/git_ops.py @@ -141,7 +141,7 @@ def delete_branch(project_dir, name, force=False): return {"success": False, "message": f"Cannot delete the current branch: {name}"} flag = "-D" if force else "-d" - rc, out, err = _run_git(project_dir, "branch", flag, name) + rc, out, err = _run_git(project_dir, "branch", flag, "--", name) if rc != 0: return {"success": False, "message": (err or out).strip()} return {"success": True, "message": (out + err).strip()} diff --git a/src/tinyleaf/handlers.py b/src/tinyleaf/handlers.py index 33b13a8..0f1407b 100644 --- a/src/tinyleaf/handlers.py +++ b/src/tinyleaf/handlers.py @@ -1122,10 +1122,12 @@ def handle_git_branches(query_params, config, name): """List branches, optionally fetching first.""" project_dir = _get_project_dir(config, name) - result = git_ops.list_branches(project_dir) + fetch_info = None if query_params.get("fetch", [""])[0] == "true": - fetch_result = git_ops.fetch(project_dir) - result["fetch"] = fetch_result + fetch_info = git_ops.fetch(project_dir) + result = git_ops.list_branches(project_dir) + if fetch_info is not None: + result["fetch"] = fetch_info return result @@ -1248,6 +1250,8 @@ def handle_git_worktree_add(body, config, name): abort(400, "Branch name required") # Default path: sibling directory named -- wt_name = body.get("name", "").strip() or branch.replace("/", "-") + if os.sep in wt_name or wt_name.startswith(".") or ".." in wt_name: + abort(400, "Invalid worktree name") parent_dir = os.path.dirname(os.path.abspath(project_dir)) project_basename = os.path.basename(os.path.abspath(project_dir)) wt_path = os.path.join(parent_dir, f"{project_basename}--{wt_name}") diff --git a/src/tinyleaf/static/js/app.js b/src/tinyleaf/static/js/app.js index ec00646..ae15421 100644 --- a/src/tinyleaf/static/js/app.js +++ b/src/tinyleaf/static/js/app.js @@ -3804,7 +3804,7 @@ async function showBranchDropdown() { const nameSpan = document.createElement("span"); nameSpan.className = "branch-name"; nameSpan.textContent = b; - nameSpan.style.color = "var(--fg-muted)"; + nameSpan.style.color = "var(--text-dim)"; item.appendChild(nameSpan); // Extract local name from remote ref (e.g., "origin/feature" → "feature") const localName = b.includes("/") ? b.substring(b.indexOf("/") + 1) : b; @@ -4054,13 +4054,17 @@ async function refreshWorktrees() { async function switchWorktree(path) { // Check for unsaved editor changes before switching - if (S.currentFile && S.modified.has(S.currentFile)) { + if (S.modified && S.modified.size > 0) { const action = await new Promise((resolve) => { const overlay = document.createElement("div"); overlay.className = "git-dialog-overlay"; const dialog = document.createElement("div"); dialog.className = "git-dialog"; + const closeDialog = () => { overlay.remove(); document.removeEventListener("keydown", escHandler); resolve("cancel"); }; + const escHandler = (e) => { if (e.key === "Escape") closeDialog(); }; + document.addEventListener("keydown", escHandler); + const title = document.createElement("h3"); title.textContent = t("unsaved_changes"); dialog.appendChild(title); @@ -4071,17 +4075,17 @@ async function switchWorktree(path) { const cancelBtn = document.createElement("button"); cancelBtn.className = "sm"; cancelBtn.textContent = t("cancel"); - cancelBtn.onclick = () => { overlay.remove(); resolve("cancel"); }; + cancelBtn.onclick = () => closeDialog(); const discardBtn = document.createElement("button"); discardBtn.className = "sm"; discardBtn.textContent = t("discard_and_continue"); - discardBtn.onclick = () => { overlay.remove(); resolve("discard"); }; + discardBtn.onclick = () => { overlay.remove(); document.removeEventListener("keydown", escHandler); resolve("discard"); }; const saveBtn = document.createElement("button"); saveBtn.className = "sm primary"; saveBtn.textContent = t("save_and_continue"); - saveBtn.onclick = () => { overlay.remove(); resolve("save"); }; + saveBtn.onclick = () => { overlay.remove(); document.removeEventListener("keydown", escHandler); resolve("save"); }; btns.appendChild(cancelBtn); btns.appendChild(discardBtn); @@ -4090,11 +4094,11 @@ async function switchWorktree(path) { overlay.appendChild(dialog); document.body.appendChild(overlay); - overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve("cancel"); } }; + overlay.onclick = (e) => { if (e.target === overlay) closeDialog(); }; }); if (action === "cancel") return; if (action === "save") await saveCurrentFile(); - if (action === "discard") S.modified.delete(S.currentFile); + if (action === "discard") S.modified.clear(); } setStatus(t("worktree_switching")); @@ -4140,7 +4144,7 @@ async function addWorktreeDialog() { const selectDiv = document.createElement("div"); selectDiv.style.cssText = "margin-bottom:12px"; const selectLabel = document.createElement("label"); - selectLabel.style.cssText = "font-size:12px;color:var(--fg-muted);display:block;margin-bottom:4px"; + selectLabel.style.cssText = "font-size:12px;color:var(--text-dim);display:block;margin-bottom:4px"; selectLabel.textContent = t("select_branch"); selectDiv.appendChild(selectLabel);