Commit 86c4659999eb
Changed files (2)
dots
config
emacs
site-lisp
dots/config/emacs/site-lisp/pi-from-github.el
@@ -0,0 +1,151 @@
+;;; pi-from-github.el --- Start pi sessions from GitHub PRs/issues -*- lexical-binding: t -*-
+;;; Commentary:
+;; Start a pi-coding-agent session from a GitHub PR or issue.
+;; Uses `gh` CLI for listing and `lazyworktree` for worktree creation.
+;;; Code:
+
+(require 'json)
+(require 'project)
+
+;; Declare pi-coding-agent functions
+(declare-function pi-coding-agent--setup-session "pi-coding-agent")
+(declare-function pi-coding-agent--show-session-buffers "pi-coding-agent")
+
+(defun pi-from-github--repo-root ()
+ "Return the git repository root for the current project."
+ (let ((default-directory (if (and (fboundp 'project-current) (project-current))
+ (project-root (project-current))
+ default-directory)))
+ (string-trim (shell-command-to-string "git rev-parse --show-toplevel"))))
+
+(defun pi-from-github--in-github-repo-p ()
+ "Return non-nil if current project is a GitHub repository."
+ (let ((default-directory (pi-from-github--repo-root)))
+ (string-match-p "github\\.com"
+ (shell-command-to-string "git remote -v"))))
+
+(defun pi-from-github--fetch-prs ()
+ "Fetch open PRs from current GitHub repo."
+ (let* ((default-directory (pi-from-github--repo-root))
+ (output (shell-command-to-string
+ "gh pr list --limit=500 --json number,title,author,headRefName,isDraft")))
+ (json-read-from-string output)))
+
+(defun pi-from-github--fetch-issues ()
+ "Fetch open issues from current GitHub repo."
+ (let* ((default-directory (pi-from-github--repo-root))
+ (output (shell-command-to-string
+ "gh issue list --limit=500 --json number,title,author,labels")))
+ (json-read-from-string output)))
+
+(defun pi-from-github--format-pr (pr)
+ "Format PR for completion display."
+ (let-alist pr
+ (cons (format "#%-5d %s%s (by @%s) [%s]"
+ .number .title
+ (if (eq .isDraft t) " 🚧" "")
+ .author.login .headRefName)
+ .number)))
+
+(defun pi-from-github--format-issue (issue)
+ "Format ISSUE for completion display."
+ (let-alist issue
+ (let ((label-str (mapconcat (lambda (l) (alist-get 'name l))
+ (append .labels nil) ",")))
+ (cons (format "#%-5d %s (by @%s)%s"
+ .number .title .author.login
+ (if (string-empty-p label-str) "" (format " [%s]" label-str)))
+ .number))))
+
+(defun pi-from-github--create-worktree-for-pr (number)
+ "Create a worktree for PR NUMBER using lazyworktree.
+Uses `gh pr checkout' to fetch the PR (handles forks transparently),
+then creates a worktree from the resulting local branch. Returns the
+worktree path."
+ (let* ((default-directory (pi-from-github--repo-root))
+ ;; First try lazyworktree directly
+ (cmd (format "lazyworktree create --from-pr %d --json --silent 2>&1" number))
+ (output (shell-command-to-string cmd))
+ (json-data (condition-case nil
+ (json-read-from-string output)
+ (error nil))))
+ (if (and json-data (alist-get 'path json-data))
+ (alist-get 'path json-data)
+ ;; Fallback: gh pr checkout handles forks transparently
+ (message "Fetching PR #%d via gh..." number)
+ (let* ((branch (format "pr-%d" number))
+ (checkout-cmd (format "gh pr checkout %d --branch %s 2>&1"
+ number (shell-quote-argument branch)))
+ (checkout-out (shell-command-to-string checkout-cmd)))
+ ;; gh pr checkout switches the current worktree — switch back
+ (shell-command-to-string "git checkout - 2>/dev/null")
+ ;; Now create worktree from the local branch
+ (let* ((cmd2 (format "lazyworktree create --from-branch %s --json --silent 2>&1"
+ (shell-quote-argument branch)))
+ (out2 (shell-command-to-string cmd2))
+ (json2 (condition-case nil
+ (json-read-from-string out2)
+ (error nil))))
+ (if (and json2 (alist-get 'path json2))
+ (alist-get 'path json2)
+ (error "Failed to create worktree for PR #%d: %s" number out2)))))))
+
+(defun pi-from-github--create-worktree-for-issue (number)
+ "Create a worktree for issue NUMBER using lazyworktree.
+Returns the worktree path."
+ (let* ((default-directory (pi-from-github--repo-root))
+ (cmd (format "lazyworktree create --from-issue %d --json --silent 2>&1" number))
+ (output (shell-command-to-string cmd))
+ (json-data (condition-case nil
+ (json-read-from-string output)
+ (error nil))))
+ (if (and json-data (alist-get 'path json-data))
+ (alist-get 'path json-data)
+ (error "Failed to create worktree for issue #%d: %s" number output))))
+
+;;;###autoload
+(defun pi-from-github-pr ()
+ "Start a pi-coding-agent session from a GitHub PR.
+Lists open PRs using `gh`, creates a worktree with `lazyworktree`,
+and starts a pi session in that worktree."
+ (interactive)
+ (unless (pi-from-github--in-github-repo-p)
+ (user-error "Not in a GitHub repository"))
+ (let* ((prs (pi-from-github--fetch-prs))
+ (candidates (mapcar #'pi-from-github--format-pr (append prs nil)))
+ (selected (completing-read "PR: " candidates nil t))
+ (number (cdr (assoc selected candidates))))
+ (unless number (user-error "No PR selected"))
+ (message "Creating worktree for PR #%d..." number)
+ (let ((wt-path (pi-from-github--create-worktree-for-pr number)))
+ (message "Starting pi session in %s" wt-path)
+ (let* ((chat-buf (pi-coding-agent--setup-session wt-path
+ (format "pr-%d" number)))
+ (input-buf (buffer-local-value 'pi-coding-agent--input-buffer
+ chat-buf)))
+ (pi-coding-agent--show-session-buffers chat-buf input-buf)))))
+
+;;;###autoload
+(defun pi-from-github-issue ()
+ "Start a pi-coding-agent session from a GitHub issue.
+Lists open issues using `gh`, creates a worktree with `lazyworktree`,
+and starts a pi session in that worktree."
+ (interactive)
+ (unless (pi-from-github--in-github-repo-p)
+ (user-error "Not in a GitHub repository"))
+ (let* ((issues (pi-from-github--fetch-issues))
+ (candidates (mapcar #'pi-from-github--format-issue (append issues nil)))
+ (selected (completing-read "Issue: " candidates nil t))
+ (number (cdr (assoc selected candidates))))
+ (unless number (user-error "No issue selected"))
+ (message "Creating worktree for issue #%d..." number)
+ (let ((wt-path (pi-from-github--create-worktree-for-issue number)))
+ (message "Starting pi session in %s" wt-path)
+ (let* ((chat-buf (pi-coding-agent--setup-session wt-path
+ (format "issue-%d" number)))
+ (input-buf (buffer-local-value 'pi-coding-agent--input-buffer
+ chat-buf)))
+ (pi-coding-agent--show-session-buffers chat-buf input-buf)))))
+
+(provide 'pi-from-github)
+;;; pi-from-github.el ends here
dots/config/emacs/init.el
@@ -3293,6 +3293,12 @@ With \\[universal-argument] \\[universal-argument]: always prompt with completio
(use-package phscroll
:defer t)
+(use-package pi-from-github
+ :after pi-coding-agent
+ :commands (pi-from-github-pr pi-from-github-issue)
+ :bind (("C-c a r" . pi-from-github-pr)
+ ("C-c a i" . pi-from-github-issue)))
+
(use-package pi-coding-agent
:commands (pi-coding-agent pi-coding-agent-toggle)
:bind (("C-c a p" . vde/pi-coding-agent-dwim)