diff options
| author | tslil <tslil@posteo.de> | 2026-09-02 18:59:20 +0100 |
|---|---|---|
| committer | tslil <tslil@posteo.de> | 2026-09-02 19:03:29 +0100 |
| commit | 601df114c430bbaf7d49b201295b29d6fed2b9b8 (patch) | |
| tree | 4b728e62efec48c33a8047704775be78a0137d84 /emacs/scripts | |
| parent | f8b6b5ec5157b42da21e02a03874f8879c9c7305 (diff) | |
[emacs] monospace some fonts, jj-review.el
Diffstat (limited to 'emacs/scripts')
| -rw-r--r-- | emacs/scripts/jj-review.el | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/emacs/scripts/jj-review.el b/emacs/scripts/jj-review.el new file mode 100644 index 0000000..fc9567e --- /dev/null +++ b/emacs/scripts/jj-review.el @@ -0,0 +1,245 @@ +;; -*- lexical-binding: t; -*- + +;;; jj-review.el --- PR-style review of Jujutsu changes -*- lexical-binding: t; -*- + +;; Author: tslil clingman +;; Package-Requires: ((emacs "29.1")) + +(require 'diff-mode) +(require 'outline) +(require 'ansi-color) + +(defgroup jj-review nil + "PR-style review of jj changes." + :group 'vc) + +(defcustom jj-review-executable "jj" + "Name or path of the jj executable." + :type 'string) + +(defcustom jj-review-open-collapsed t + "If non-nil, open review buffers with all file sections folded." + :type 'boolean) + +(defvar-local jj-review--root nil + "Repository root of this review buffer.") + +(defvar-local jj-review--revset nil + "Revset this buffer is reviewing.") + +(defvar jj-review--revset-history nil) + +;;;; jj plumbing + +(defun jj-review--find-root () + "Return the jj repository root containing `default-directory'." + (or (locate-dominating-file default-directory ".jj") + (user-error "Not inside a jj repository"))) + +(defun jj-review--run (root &rest args) + "Run jj with ARGS in ROOT and return stdout as a string. +stderr is kept separate (jj prints hints and warnings there) and +included in the `user-error' signalled on nonzero exit." + (let ((err-file (make-temp-file "jj-review-stderr"))) + (unwind-protect + (with-temp-buffer + (let* ((default-directory root) + (exit (apply #'call-process jj-review-executable nil + (list t err-file) nil + "--color=never" args))) + (unless (eql exit 0) + (user-error "jj %s: %s" + (string-join args " ") + (string-trim + (with-temp-buffer + (insert-file-contents err-file) + (buffer-string))))) + (buffer-string))) + (delete-file err-file)))) + +;;;; Locating the file at point + +(defun jj-review--paths () + "Return (BEFORE-PATH . AFTER-PATH) for the file section at point. +Paths are repository-relative, taken from the nearest preceding +\"diff --git a/... b/...\" header." + (save-excursion + (end-of-line) + (if (re-search-backward "^diff --git a/\\(.*\\) b/\\(.*\\)$" nil t) + (cons (match-string-no-properties 1) + (match-string-no-properties 2)) + (user-error "No file at or before point")))) + +;;;; jj's own diff formatter (difftastic etc.), ANSI-rendered + +(defun jj-review--formatted-diff-buffer (root revset &optional path) + "Return a buffer with `jj diff -r REVSET' for ROOT, colors rendered. +Restrict to PATH if non-nil. Uses whatever diff formatter jj is +configured with; DFT_COLOR/DFT_WIDTH are set so difftastic emits +color at the current window's width despite writing to a pipe." + (let ((buf (get-buffer-create + (format "*jj diff: %s*" (or path revset)))) + (process-environment + (append (list "DFT_COLOR=always" + (format "DFT_WIDTH=%d" (window-width))) + process-environment))) + (with-current-buffer buf + (let ((inhibit-read-only t) + (default-directory root)) + (erase-buffer) + (let ((exit (apply #'call-process jj-review-executable nil t nil + "--color=always" "diff" "-r" revset + (and path (list "--" path))))) + (unless (eql exit 0) + (user-error "jj diff: %s" (string-trim (buffer-string))))) + (ansi-color-apply-on-region (point-min) (point-max))) + (setq default-directory root) + (goto-char (point-min)) + (special-mode)) + buf)) + +;;;; Commands + +(defun jj-review-diff-file () + "Show `jj diff' for the file at point in jj's configured formatter." + (interactive) + (pop-to-buffer + (jj-review--formatted-diff-buffer jj-review--root jj-review--revset + (cdr (jj-review--paths))))) + +(defun jj-review-diff-revision () + "Show `jj diff' for the whole revision in jj's configured formatter." + (interactive) + (pop-to-buffer + (jj-review--formatted-diff-buffer jj-review--root jj-review--revset))) + +(defun jj-review-collapse-all () + "Fold every file section, leaving the file list." + (interactive) + (outline-hide-sublevels 1)) + +(defun jj-review-expand-all () + "Unfold everything." + (interactive) + (outline-show-all)) + +;;;; Staying up to date (auto-revert wiring) + +(defvar-local jj-review--commit-ids nil + "Commit ids the revset denoted when this buffer was last filled.") + +(defun jj-review--query-commit-ids (root revset) + "Return the commit ids REVSET currently denotes in ROOT, as one string. +Running jj snapshots the working copy, so on-disk edits are +reflected in the result." + (string-trim + (jj-review--run root "log" "--no-graph" "-r" revset "-T" "commit_id"))) + +(defun jj-review--stale-p (&optional _noconfirm) + "Non-nil if the revset's commit ids changed since the last fill. +Errors (e.g. the revset no longer resolving) count as not stale, +so a broken state doesn't spam; refresh manually with \\[revert-buffer]." + (and jj-review--root jj-review--revset + (condition-case nil + (not (equal (jj-review--query-commit-ids jj-review--root + jj-review--revset) + jj-review--commit-ids)) + (error nil)))) + +(defun jj-review--expanded-files () + "Return after-paths of file sections whose bodies are visible." + (let (res) + (save-excursion + (goto-char (point-min)) + (while (re-search-forward "^diff --git a/.* b/\\(.*\\)$" nil t) + (unless (invisible-p (min (line-end-position) (point-max))) + (push (match-string-no-properties 1) res)))) + (nreverse res))) + +(defun jj-review--expand-files (paths) + "Reveal the file sections whose after-path is in PATHS." + (save-excursion + (goto-char (point-min)) + (while (re-search-forward "^diff --git a/.* b/\\(.*\\)$" nil t) + (when (member (match-string-no-properties 1) paths) + (beginning-of-line) + (outline-show-subtree) + (end-of-line))))) + +(defun jj-review--revert (&optional _ignore-auto _noconfirm) + "Refill the review buffer, preserving point and per-file fold state. +Installed as `revert-buffer-function', so both \\[revert-buffer] +and `auto-revert-mode' use it." + (let ((expanded (jj-review--expanded-files)) + (pos (point))) + (jj-review--fill (current-buffer) jj-review--root jj-review--revset) + (jj-review--expand-files expanded) + (goto-char (min pos (point-max))))) + +;;;; Mode and buffer construction + +(defvar-keymap jj-review-mode-map + :doc "Keymap for `jj-review-mode'." + "n" #'diff-hunk-next + "p" #'diff-hunk-prev + "N" #'diff-file-next + "P" #'diff-file-prev + "RET" #'diff-goto-source + "TAB" #'outline-toggle-children + "<backtab>" #'outline-cycle-buffer + "d" #'jj-review-diff-file + "D" #'jj-review-diff-revision + "c" #'jj-review-collapse-all + "x" #'jj-review-expand-all + "g" #'revert-buffer + "q" #'quit-window) + +(define-derived-mode jj-review-mode diff-mode "jj-review" + "PR-style review of a jj revision. +An ordinary read-only `diff-mode' buffer with per-file folding +and access to the before/after version of each file. + +\\{jj-review-mode-map}" + (setq buffer-read-only t) + (setq-local revert-buffer-function #'jj-review--revert + buffer-stale-function #'jj-review--stale-p) + (outline-minor-mode 1)) + +(defun jj-review--fill (buf root revset) + "Fill BUF with the review of REVSET in ROOT." + (let ((ids (jj-review--query-commit-ids root revset)) + (stat (jj-review--run root "diff" "--stat" "-r" revset)) + (diff (jj-review--run root "diff" "--git" "-r" revset))) + (when (string-empty-p (string-trim diff)) + (user-error "Revset %s introduces no changes" revset)) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (erase-buffer) + (insert "Reviewing: " revset "\n\n" stat "\n" diff)) + (setq default-directory root) + (unless (derived-mode-p 'jj-review-mode) + (jj-review-mode)) + (setq jj-review--root root + jj-review--revset revset + jj-review--commit-ids ids) + (when jj-review-open-collapsed + (jj-review-collapse-all)) + (goto-char (point-min))) + buf)) + +(defun jj-review-buffer (revset &optional dir) + "Return a review buffer for REVSET in the repo containing DIR." + (let* ((default-directory (or dir default-directory)) + (root (jj-review--find-root)) + (buf (get-buffer-create (format "*jj-review: %s*" revset)))) + (jj-review--fill buf root revset))) + +;;;###autoload +(defun jj-review (revset) + "Review the changes introduced by REVSET (default @), PR-style." + (interactive + (list (read-string "Revset (default @): " nil 'jj-review--revset-history "@"))) + (pop-to-buffer (jj-review-buffer revset))) + +(provide 'jj-review) +;;; jj-review.el ends here |
