nftable-migration
1;;; project-func.el --- -*- lexical-binding: t -*-
2;;; Commentary:
3;;; Code:
4(require 'project)
5(require 'vterm)
6(require 'json)
7(require 'vc)
8
9(defun in-git-repo-p ()
10 "Check if current directory is in a git repository."
11 (eq (vc-backend (or buffer-file-name default-directory))
12 'Git))
13
14(defun is-github-repo-p ()
15 "Check if current git repository has a GitHub remote."
16 (when (in-git-repo-p)
17 (string-match-p "github\\.com"
18 (shell-command-to-string "git remote -v"))))
19
20(defun fetch-github-prs ()
21 "Fetch GitHub PRs synchronously."
22 (let* ((output (shell-command-to-string "gh pr list --limit=5000 --json number,title,author,url,baseRefName,labels"))
23 (prs (json-read-from-string output)))
24 prs))
25
26(defun format-pr-candidates (prs)
27 "Format PR data into candidates for completion."
28 (mapcar (lambda (pr)
29 (let-alist pr
30 (cons (format "#%d %s (by @%s) on %s" .number .title .author.login .baseRefName)
31 .number)))
32 prs))
33
34;;;###autoload
35(defun checkout-github-pr ()
36 "Interactive function to select and checkout a GitHub PR."
37 (interactive)
38 (cond
39 ((not (in-git-repo-p))
40 (message "Not in a Git repository"))
41 ((not (is-github-repo-p))
42 (message "Not a GitHub repository"))
43 (t
44 (let* ((prs (fetch-github-prs))
45 (candidates (format-pr-candidates prs))
46 (selected (if candidates
47 (cdr (assoc (completing-read "Checkout PR: " candidates)
48 candidates))
49 nil)))
50 (if selected
51 (shell-command (format "gh pr checkout %d" selected))
52 (message "No pull requests found"))))))
53
54;;;###autoload
55(defun vde-project--project-current ()
56 "Return directory from `project-current' based on Emacs version."
57 (if (>= emacs-major-version 29)
58 (project-root (project-current))
59 (cdr (project-current))))
60
61;;;###autoload
62(defun vde-project--project-root-or-default-directory ()
63 "Return path to the project root *or* the default-directory."
64 (cond
65 ((and (featurep 'project) (project-current))
66 (project-root (project-current)))
67 (t default-directory)))
68
69;;;##autoload
70(defun vde/project-run-in-vterm (command &optional directory)
71 "Run the given `COMMAND' in a new vterm buffer in `project-root' or the
72given `DIRECTORY'.
73
74This is similar to `compile' but with vterm.
75One reason for this is to be able to run commands that needs a TTY."
76 (interactive "sCommand: ")
77 (let* ((cwd (or directory (vde-project--project-root-or-default-directory)))
78 (default-directory cwd)
79 (buffer-name (format "*vterm %s: %s*" cwd command))
80 (buffer (get-buffer buffer-name))
81 (vterm-kill-buffer-on-exit nil)
82 (vterm-shell (concat "bash -c '" command ";exit'")))
83 (when buffer
84 (kill-buffer buffer))
85 (let ((buffer (generate-new-buffer buffer-name)))
86 (pop-to-buffer buffer)
87 (with-current-buffer buffer
88 (vterm-mode)))))
89
90;;;###autoload
91(defun vde/open-readme ()
92 "Open a README file in the current project.
93It will search for README.org, README.md or README in that order"
94 (interactive)
95 (let* ((default-directory (vde-project--project-current)))
96 (cond ((file-exists-p (expand-file-name "README.org" default-directory))
97 (find-file "README.org"))
98 ((file-exists-p (expand-file-name "README.md" default-directory))
99 (find-file "README.md"))
100 ((file-exists-p (expand-file-name "README" default-directory))
101 (find-file "README")))))
102
103;;;###autoload
104(defun vde/project-try-local (dir)
105 "Determine if DIR is a non-VC project."
106 (if-let ((root (if (listp vde/project-local-identifier)
107 (seq-some (lambda (n)
108 (locate-dominating-file dir n))
109 vde/project-local-identifier)
110 (locate-dominating-file dir vde/project-local-identifier))))
111 (cons 'local root)))
112
113;;;###autoload
114(defun vde/project-vterm (&optional command)
115 "Run `vterm' on project.
116If a buffer already exists for running a vterm shell in the project's root,
117switch to it. Otherwise, create a new vterm shell."
118 (interactive)
119 (let* ((default-directory (vde-project--project-current))
120 (default-project-vterm-name (project-prefixed-buffer-name "vterm"))
121 (vterm-buffer (get-buffer default-project-vterm-name)))
122 (if (and vterm-buffer (not current-prefix-arg))
123 (pop-to-buffer-same-window vterm-buffer)
124 (let* ((cd-cmd (concat " cd " (shell-quote-argument default-directory))))
125 (vterm default-project-vterm-name)
126 (with-current-buffer vterm-buffer
127 (vterm-send-string cd-cmd)
128 (vterm-send-return))))
129 (when command
130 (vterm-send-string command)
131 (vterm-send-return))))
132
133;;;###autoload
134(defun vde/project-eat ()
135 "Run Eat term in the current project's root directory.
136If a buffer already exists for running Eshell in the project's root,
137switch to it. Otherwise, create a new Eshell buffer.
138With \\[universal-argument] prefix arg, create a new Eshell buffer even
139if one already exists."
140 (interactive)
141 (defvar eat-buffer-name)
142 (let* ((default-directory (project-root (project-current t)))
143 (eat-buffer-name (project-prefixed-buffer-name "eat"))
144 (eat-buffer (get-buffer eat-buffer-name)))
145 (if (and eat-buffer (not current-prefix-arg))
146 (pop-to-buffer eat-buffer (bound-and-true-p display-comint-buffer-action))
147 (eat shell-file-name))))
148
149(provide 'project-func)
150;;; project-func.el ends here