From 4fe00d16c09b7826cdc7481f833aea431d735b16 Mon Sep 17 00:00:00 2001 From: Peng Ding Date: Sun, 13 Sep 2026 04:00:51 -0500 Subject: [PATCH] feat: add branch switch/create/delete and stash operations --- src/tinyleaf/git_ops.py | 90 ++++++++++++++++++++++++++++++++++++++++ src/tinyleaf/handlers.py | 33 +++++++++++++++ src/tinyleaf/server.py | 35 ++++++++++++++++ 3 files changed, 158 insertions(+) diff --git a/src/tinyleaf/git_ops.py b/src/tinyleaf/git_ops.py index 66e7ea7..1d43916 100644 --- a/src/tinyleaf/git_ops.py +++ b/src/tinyleaf/git_ops.py @@ -75,6 +75,96 @@ def fetch(project_dir): return {"success": True, "message": (out + err).strip()} +def switch_branch(project_dir, branch): + """Switch to an existing branch. + + Returns: + Dict with success status and message. + """ + if not has_git(project_dir): + return {"success": False, "message": "Not a git repository"} + + rc, out, err = _run_git(project_dir, "switch", branch) + if rc != 0: + return {"success": False, "message": (err or out).strip()} + return {"success": True, "message": (out + err).strip()} + + +def create_branch(project_dir, name, start_point=None): + """Create and switch to a new branch. + + Returns: + Dict with success status and message. + """ + if not has_git(project_dir): + return {"success": False, "message": "Not a git repository"} + + # Validate branch name + rc, _, err = _run_git(project_dir, "check-ref-format", "--branch", name) + if rc != 0: + return {"success": False, "message": f"Invalid branch name: {name}"} + + args = ["switch", "-c", name] + if start_point: + args.append(start_point) + + rc, out, err = _run_git(project_dir, *args) + if rc != 0: + return {"success": False, "message": (err or out).strip()} + return {"success": True, "message": (out + err).strip()} + + +def delete_branch(project_dir, name, force=False): + """Delete a branch. + + Returns: + Dict with success status and message. + """ + if not has_git(project_dir): + return {"success": False, "message": "Not a git repository"} + + # Refuse to delete the current branch + rc, out, _ = _run_git(project_dir, "branch", "--show-current") + if rc == 0 and out.strip() == name: + 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) + if rc != 0: + return {"success": False, "message": (err or out).strip()} + return {"success": True, "message": (out + err).strip()} + + +def stash(project_dir): + """Stash working directory changes. + + Returns: + Dict with success status and message. + """ + if not has_git(project_dir): + return {"success": False, "message": "Not a git repository"} + + rc, out, err = _run_git(project_dir, "stash") + if rc != 0: + return {"success": False, "message": (err or out).strip()} + return {"success": True, "message": (out + err).strip()} + + +def stash_pop(project_dir): + """Pop the latest stash. + + Returns: + Dict with success status and message. + """ + if not has_git(project_dir): + return {"success": False, "message": "Not a git repository"} + + rc, out, err = _run_git(project_dir, "stash", "pop") + if rc != 0: + return {"success": False, "message": (err or out).strip()} + return {"success": True, "message": (out + err).strip()} + + def status(project_dir): """Get git status as structured data. diff --git a/src/tinyleaf/handlers.py b/src/tinyleaf/handlers.py index 3aa9fd5..8a8430c 100644 --- a/src/tinyleaf/handlers.py +++ b/src/tinyleaf/handlers.py @@ -1128,6 +1128,39 @@ def handle_git_branches(query_params, config, name): return git_ops.list_branches(project_dir) +def handle_git_switch_branch(body, config, name): + project_dir = _get_project_dir(config, name) + branch = body.get("branch", "").strip() + if not branch: + abort(400, "Branch name required") + return git_ops.switch_branch(project_dir, branch) + + +def handle_git_create_branch(body, config, name): + project_dir = _get_project_dir(config, name) + branch_name = body.get("name", "").strip() + if not branch_name: + abort(400, "Branch name required") + start_point = body.get("start_point", "").strip() or None + return git_ops.create_branch(project_dir, branch_name, start_point=start_point) + + +def handle_git_delete_branch(body, config, name, branch_name): + project_dir = _get_project_dir(config, name) + force = body.get("force", False) + return git_ops.delete_branch(project_dir, branch_name, force=force) + + +def handle_git_stash(config, name): + project_dir = _get_project_dir(config, name) + return git_ops.stash(project_dir) + + +def handle_git_stash_pop(config, name): + project_dir = _get_project_dir(config, name) + return git_ops.stash_pop(project_dir) + + def handle_git_status(config, name): project_dir = _get_project_dir(config, name) return git_ops.status(project_dir) diff --git a/src/tinyleaf/server.py b/src/tinyleaf/server.py index 97f252b..704792c 100644 --- a/src/tinyleaf/server.py +++ b/src/tinyleaf/server.py @@ -401,6 +401,41 @@ def api_git_branches(request, name): return handle_git_branches(request.query_params, _config, name) +@app.post("/api/projects//git/branches/switch") +def api_git_switch_branch(request, name): + from tinyleaf.handlers import handle_git_switch_branch + + return handle_git_switch_branch(request.json(), _config, name) + + +@app.post("/api/projects//git/branches") +def api_git_create_branch(request, name): + from tinyleaf.handlers import handle_git_create_branch + + return handle_git_create_branch(request.json(), _config, name) + + +@app.delete("/api/projects//git/branches/") +def api_git_delete_branch(request, name, branch_name): + from tinyleaf.handlers import handle_git_delete_branch + + return handle_git_delete_branch(_json_body(request), _config, name, branch_name) + + +@app.post("/api/projects//git/stash") +def api_git_stash(request, name): + from tinyleaf.handlers import handle_git_stash + + return handle_git_stash(_config, name) + + +@app.post("/api/projects//git/stash/pop") +def api_git_stash_pop(request, name): + from tinyleaf.handlers import handle_git_stash_pop + + return handle_git_stash_pop(_config, name) + + @app.get("/api/projects//git/status") def api_git_status(request, name): from tinyleaf.handlers import handle_git_status