Skip to content

fix: address review feedback from PRs #67, #68, #71 - #72

Merged
Oaklight merged 1 commit into
masterfrom
fix/review-feedback-roundup
Sep 13, 2026
Merged

Oaklight merged 1 commit into
masterfrom
fix/review-feedback-roundup

Conversation

@Oaklight

Copy link
Copy Markdown
Owner

Summary

Comprehensive follow-up addressing all outstanding review feedback from PRs #67, #68, and #71 (all already merged into master).

Backend: src/tinyleaf/git_ops.py

Backend: src/tinyleaf/handlers.py

Frontend: src/tinyleaf/static/js/app.js

Test plan

  • Verify list_branches() does not include detached HEAD entries
  • Verify worktree add/remove work with paths containing special characters
  • Verify worktree_list() returns all worktrees including the last entry
  • Verify fetch result is surfaced in branch list API response
  • Verify worktree switch rejects non-git directories and multi-project mode
  • Verify branch names with <script> or HTML tags are safely rendered in the add-worktree dialog
  • Verify main worktree item is clickable in the versions tab
  • Verify openAsWorktree auto-switches to the new worktree
  • Verify removeWorktree shows custom dialog instead of native confirm()
  • Verify unsaved editor changes prompt a save/discard dialog before worktree switch
  • Verify i18n keys render correctly in both English and Chinese

@Oaklight
Oaklight merged commit 3aadf3f into master Sep 13, 2026
2 checks passed
@Oaklight
Oaklight deleted the fix/review-feedback-roundup branch September 13, 2026 18:20

@clementine-oaklight clementine-oaklight Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid fix PR — XSS、path validation、dialog 统一都做对了。一个实际 bug,一个 medium:

🔴 Bug: handle_git_branches() fetch 顺序反了

result = git_ops.list_branches(project_dir)      # ← 先 list
if query_params.get("fetch", [""])[0] == "true":
    fetch_result = git_ops.fetch(project_dir)     # ← 后 fetch
    result["fetch"] = fetch_result
return result

老代码是 fetch → list,新代码变成 list → fetch。?fetch=true 的意图是拿最新的 remote branches,但现在 list 在 fetch 之前执行,返回的 remote branch 列表是 stale 的。fetch result 挂上了(好),但 branch list 本身没更新。

修法:调回来,先 fetch 再 list:

fetch_info = None
if query_params.get("fetch", [""])[0] == "true":
    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

🟡 Medium: unsaved-buffer check 只看当前文件

if (S.currentFile && S.modified.has(S.currentFile)) {

S.modified 可以包含多个文件。如果用户改了文件 A 和 B,当前看的是未修改的文件 C,check 通过,worktree switch 后所有未保存的改动静默丢失。建议改成 S.modified.size > 0,dialog 文案也更准确。


其余全部正确:

  • XSS fix ✅ — addWorktreeDialog 从 innerHTML 完全改成 createElement + textContent,和 codebase 其余部分一致。
  • removeprefix("* ") ✅ — 正确修了 #67 的 char-set stripping bug,加 startswith("(") 过滤 detached HEAD 也对。
  • worktree_list flush ✅ — porcelain output 不以空行结尾时不再丢最后一个 entry。
  • -- separator ✅ — worktree_add 和 worktree_remove 都加了,--force 位置也修正了。
  • worktree_switch validation ✅ — multi-mode 400 reject + has_git() check,docstring 标明 ephemeral。
  • confirm() → custom dialog ✅ — removeWorktree 用 git-dialog-overlay,UX 统一。
  • Main worktree clickable ✅ — onclick 移到 if (!wt.is_main) 外面。
  • openAsWorktree auto-switch ✅ — 创建后调 switchWorktree(result.path)。
  • await refreshWorktrees() ✅ — 两处都加了 await。
  • i18n keys ✅ — en/zh 双语都补了,命名合理。

fetch 顺序修一下就能合。

@elena-oaklight elena-oaklight Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix: address review feedback from PRs #67, #68, #71

Thorough fix-up PR — most of the changes are solid and well-implemented. Two issues need addressing before merge.

Issue Checklist

# Issue Status Notes
1 🔴 XSS in addWorktreeDialog ✅ Fixed innerHTML → DOM API with textContent. Clean, correct.
2 🔴 Silent fetch failure hiding stale data ⚠️ Regressed Fetch result is now surfaced, but branch list is generated before the fetch (see below).
3 🟡 Wrong i18n keys ("Create Branch" → "Create Worktree") ✅ Fixed New create_worktree / select_branch keys in both en and zh.
4 🟡 worktree_switch doesn't validate git repo ✅ Fixed has_git() check added.
5 🟡 worktree_switch silently no-ops in multi-project mode ✅ Fixed Early abort(400) in non-single mode.
6 🟡 Missing -- separator in git commands ✅ Fixed Added in both worktree_add and worktree_remove.
7 🟡 Path traversal in worktree_add ❌ Not fixed See below.
8 🟡 Detached HEAD leaking into branch list ✅ Fixed Lines starting with ( now skipped.
9 🟡 lstrip("* ") stripping by character set ✅ Fixed removeprefix("* ") — correct.
10 🟡 worktree_list dropping last entry ✅ Fixed Flush after loop.

Additional fixes not in the original list — all good:

  • Main worktree now clickable ✅
  • openAsWorktree auto-switches ✅
  • confirm() → custom dialog ✅
  • Unsaved-buffer check before switch ✅
  • Missing await on refreshWorktrees() ✅

🔴 1. Fetch ordering regression (handlers.py)

The refactored handle_git_branches now calls list_branches() before fetch():

result = git_ops.list_branches(project_dir)       # ← branches listed with stale data
if query_params.get("fetch", [""])[0] == "true":
    fetch_result = git_ops.fetch(project_dir)       # ← fetch happens after
    result["fetch"] = fetch_result
return result

The original code had the correct order (fetch first, then list), but discarded the fetch result. This refactoring inverts the order — so when fetch=true, the response always contains stale branch data. The fetch result field is a good addition, but the ordering needs to be restored:

fetch_result = None
if query_params.get("fetch", [""])[0] == "true":
    fetch_result = git_ops.fetch(project_dir)

result = git_ops.list_branches(project_dir)
if fetch_result is not None:
    result["fetch"] = fetch_result
return result

🔴 2. Path traversal in worktree_add still unfixed (handlers.py)

handle_git_worktree_add uses the name body parameter directly in path construction:

wt_name = body.get("name", "").strip() or branch.replace("/", "-")
parent_dir = os.path.dirname(os.path.abspath(project_dir))
wt_path = os.path.join(parent_dir, f"{project_basename}--{wt_name}")

A request with {"branch": "main", "name": "../../etc/evil"} would create a worktree at <grandparent>/project--../../etc/evil, escaping the expected sibling directory. Suggested fix — reject path separators and traversal patterns:

wt_name = body.get("name", "").strip() or branch.replace("/", "-")
if "/" in wt_name or "\\" in wt_name or wt_name in (".", "..") or ".." in wt_name:
    abort(400, "Invalid worktree name")

Or, more robustly, validate that the resolved path stays under parent_dir:

resolved = os.path.realpath(wt_path)
if not resolved.startswith(os.path.realpath(parent_dir) + os.sep):
    abort(400, "Worktree path escapes project directory")

Minor note (not blocking)

The slashed branch name issue with the DELETE /api/projects/<name>/git/branches/<branch_name> route (from PR #69 review) is not in scope for this PR but should be tracked separately — <branch_name> in the Bottle route won't match names like feature/x.


Summary

Most fixes are clean and correct — the XSS fix, -- separators, removeprefix, detached HEAD filtering, worktree_switch validation, custom dialogs, and unsaved-buffer check are all well done. Requesting changes for the fetch ordering regression and the missing path traversal sanitization.

@milo-oaklight milo-oaklight Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #72 — fix: address review feedback from PRs #67, #68, #71

CI: ✅ All checks pass (lint, GitGuardian)

Nice comprehensive follow-up. Most of the original findings are resolved. A few items are still open, and the fix for one introduced a regression. Going through each systematically:


✅ Resolved — PR #71 Findings

1. XSS in addWorktreeDialog — FIXED

The entire dialog is now built via DOM API (createElement + .textContent). No innerHTML with user-controlled data remains. This is the correct fix. 👍

2. openAsWorktree auto-switch — FIXED

openAsWorktree() now calls await switchWorktree(result.path) after creation. One cosmetic note: switchGitSubTab("versions") is called after switchWorktree(), which internally calls refreshFiles() + refreshGit(). If those reload the git panel, the sub-tab switch might have no visible effect or reference stale DOM. Not a bug, but worth testing — if it works, fine.

3. Wrong button label — FIXED

t("create_branch") → t("create_worktree") with proper i18n keys in both en and zh. ✅

4. confirm() → custom dialog — FIXED

removeWorktree() now uses the git-dialog-overlay pattern consistently with the rest of the UI.

5. Unsaved-buffer check — FIXED

switchWorktree() checks S.modified before switching and shows a save/discard/cancel dialog. Clean implementation using the same dialog pattern.

Minor concern: if the user picks "Save & Continue" but saveCurrentFile() fails (network error, etc.), the switch proceeds anyway. Consider checking the save result before continuing:

if (action === "save") {
  try { await saveCurrentFile(); }
  catch { setStatus("Save failed — switch cancelled", "error"); return; }
}

6. Missing await on refreshWorktrees() — FIXED

Both addWorktreeDialog and removeWorktree now await refreshWorktrees().

7. New i18n keys — FIXED

create_worktree, select_branch, unsaved_changes, save_and_continue, discard_and_continue added in both language blocks. ✅


✅ Resolved — PR #68 Findings

8. -- separator in worktree_add / worktree_remove — FIXED

Both now use -- to separate options from path arguments, preventing flag injection from paths starting with -. worktree_remove also correctly places --force before --.

9. worktree_switch validation — FIXED

Now rejects multi-project mode (abort(400)) and validates has_git(path). This is a clear improvement.

10. worktree_list last-entry flush — FIXED

The parser now flushes current after the loop exits, handling porcelain output that doesn't end with a blank line. ✅


✅ Resolved — PR #67 Findings

11. Detached HEAD leak in list_branches() — FIXED

Two-part fix: lstrip("* ") → removeprefix("* ") (fixes the char-set strip bug — lstrip would have mangled branch names starting with * or space), and if line.startswith("("): continue skips detached HEAD entries. Correct. ✅


⚠️ Still Open / New Issues

12. 🔴 Fetch-then-list ordering regression (handlers.py:1125-1129)

This is a new bug introduced by this PR. The old code:

if query_params.get("fetch", ...):
    git_ops.fetch(project_dir)
return git_ops.list_branches(project_dir)

The new code:

result = git_ops.list_branches(project_dir)   # ← branches listed FIRST
if query_params.get("fetch", ...):
    fetch_result = git_ops.fetch(project_dir)  # ← fetch happens AFTER
    result["fetch"] = fetch_result
return result

When fetch=true, the branch list is now populated before the fetch runs, so newly fetched remote branches won't appear in the response. The fetch result is surfaced (good!), but the ordering needs to be:

fetch_result = None
if query_params.get("fetch", [""])[0] == "true":
    fetch_result = git_ops.fetch(project_dir)
result = git_ops.list_branches(project_dir)
if fetch_result is not None:
    result["fetch"] = fetch_result
return result

13. 🟡 .current still marks main, not active worktree (app.js:3975)

Still the same issue flagged in PR #71. The code is:

item.className = "git-worktree-item" + (wt.is_main ? " current" : "");

The accent border always highlights the main worktree, not the one currently active via switchWorktree(). After switching to a non-main worktree, the visual indicator is misleading. The API would need to return which worktree is actually active (matching config["project_path"]), and the frontend should key on that.

14. 🟡 Path traversal in handle_git_worktree_add still possible (handlers.py:1249-1252)

The -- separator prevents flag injection but not path traversal. The name parameter flows into os.path.join(parent_dir, f"{project_basename}--{wt_name}") unsanitized. A request with name=../../etc/foo would create a worktree outside the expected parent directory. Consider:

wt_name = body.get("name", "").strip() or branch.replace("/", "-")
if os.sep in wt_name or wt_name.startswith("."):
    abort(400, "Invalid worktree name")

Or validate that the resolved wt_path stays within the expected parent directory.

15. 🟡 worktree_switch doesn't verify same repo (handlers.py:1278)

has_git(path) confirms the path is a git repo, but not necessarily a worktree of the same repository. You could switch to a completely unrelated repo. Consider cross-checking the common git directory:

def is_same_repo(project_dir, path):
    """Check if two paths belong to the same git repository."""
    def common_dir(d):
        rc, out, _ = _run_git(d, "rev-parse", "--git-common-dir")
        return os.path.realpath(out.strip()) if rc == 0 else None
    return common_dir(project_dir) == common_dir(path)

Summary

Finding Status
XSS in addWorktreeDialog ✅ Fixed
openAsWorktree auto-switch ✅ Fixed
Wrong button label (create_branch) ✅ Fixed
confirm() → custom dialog ✅ Fixed
Unsaved-buffer check ✅ Fixed (minor edge case)
Missing await on refresh ✅ Fixed
i18n keys ✅ Fixed
-- separator (flag injection) ✅ Fixed
worktree_switch basic validation ✅ Fixed
worktree_list last-entry flush ✅ Fixed
Detached HEAD in branch list ✅ Fixed
Fetch result surfaced ✅ Fixed (but ordering regression ↓)
Fetch-before-list ordering 🔴 Regression
.current marks main, not active 🟡 Still open
Path traversal in worktree_add 🟡 Still open
worktree_switch same-repo check 🟡 Still open

The big one is #12 — the fetch ordering regression is a functional bug that should be fixed before merge. The rest are pre-existing issues from the previous review that weren't addressed in this round.

Solid work overall. The XSS fix is clean, the dialog migrations are consistent, and the backend validations are meaningful improvements.

— Milo 👻

@Oaklight

Copy link
Copy Markdown
Owner Author

Addressing remaining feedback

Thanks all three reviewers. Fixing the outstanding items in a follow-up:

  • 🔴 Fetch ordering regression: Will restore fetch-before-list order while keeping fetch result in response ✅
  • 🟡 Path traversal in worktree_add: Will validate worktree name and reject path separators/traversal ✅
  • 🟡 Unsaved-buffer check too narrow: Will check S.modified.size > 0 instead of just current file ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant