/ jujutsu-core.el
jujutsu-core.el
1 ;;; jujutsu-core.el --- Description -*- lexical-binding: t; -*- 2 ;; 3 ;; Copyright (C) 2024 Benjamin Andresen 4 ;; 5 ;; Author: Benjamin Andresen <b@lambda.icu> 6 ;; Maintainer: Benjamin Andresen <b@lambda.icu> 7 ;; Created: August 19, 2024 8 ;; Modified: August 19, 2024 9 ;; Version: 0.0.1 10 ;; Keywords: abbrev bib c calendar comm convenience data docs emulations extensions faces files frames games hardware help hypermedia i18n internal languages lisp local maint mail matching mouse multimedia news outlines processes terminals tex tools unix vc wp 11 ;; Homepage: https://github.com/bennyandresen/jujutsu-core 12 ;; Package-Requires: ((emacs "24.3")) 13 ;; 14 ;; This file is not part of GNU Emacs. 15 ;; 16 ;;; Commentary: 17 ;; 18 ;; Description 19 ;; 20 ;;; Code: 21 (require 's) 22 (require 'ht) 23 (require 'dash) 24 (require 'dash-x) 25 26 (require 'jujutsu-vars) 27 28 (defun jujutsu-core--template-list (s) 29 "Format string S as a semicolon-joined list in jujutsu template syntax. 30 Wraps S with .join(\";\") for use in jujutsu templates where multiple 31 values need to be concatenated into a single string." 32 (s-concat s ".join(\\\";\\\")")) 33 34 (defun jujutsu-core--find-project-root () 35 "Find the root directory of the Jujutsu project." 36 (locate-dominating-file default-directory ".jj")) 37 38 (defun jujutsu-core--get-project-name () 39 "Get the directory name of the project root." 40 (-> (jujutsu-core--find-project-root) 41 file-name-directory 42 directory-file-name 43 file-name-nondirectory)) 44 45 (-comment 46 (jujutsu-core--get-project-name) 47 1) 48 49 (defun jujutsu-core--run-command (command &optional use-personal-config) 50 "Run a jj COMMAND from the project root and return its output as a string. 51 If USE-PERSONAL-CONFIG is t, run jj without suppressing the user config." 52 (let* ((default-directory (or (jujutsu-core--find-project-root) 53 (error "Not in a Jujutsu project"))) 54 (jj-cmds (list "jj" "--no-pager" "--color" "never" command)) 55 (cmd-list (if use-personal-config 56 jj-cmds 57 (-concat '("env" "JJ_CONFIG=/dev/null") jj-cmds))) 58 (cmd-string (s-join " " cmd-list))) 59 (shell-command-to-string cmd-string))) 60 61 (defun jujutsu-core--show-w/template (template &optional rev) 62 "Run `jj show' command with a custom TEMPLATE and optional REV. 63 64 TEMPLATE is a string containing the custom template for the `jj show' command. 65 REV is an optional revision specifier. If not provided, it defaults to '@' 66 67 This function constructs and executes a `jj show' command with the given 68 template and revision, returning the command's output as a string." 69 (let* ((rev (or rev "@")) 70 (formatted (format "show --summary --template \"%s\" %s" 71 template 72 rev))) 73 (jujutsu-core--run-command formatted))) 74 75 (defun jujutsu-core--log-w/template (template &optional revset) 76 "Run `jj log' command with a custom TEMPLATE and optional REVSET. 77 78 TEMPLATE is a string containing the custom template for the `jj log' command. 79 80 This function constructs and executes a `jj log' command with the given 81 template, disabling graph output and adding newlines between entries. It returns 82 the command's output as a string, with each log entry separated by newlines." 83 (let* ((revset (or revset jujutsu-log-revset-fallback)) 84 (formatted (format "log --revisions \"%s\" --no-graph --template \"%s ++ \\\"\\n\\n\\\"\"" 85 revset 86 template))) 87 (jujutsu-core--run-command formatted))) 88 89 (defun jujutsu-core--file-list () 90 "Get the list of files from `jj file list' command." 91 (--> "file list" 92 jujutsu-core--run-command 93 (s-split "\n" it t))) 94 95 (defun jujutsu-core--map-to-escaped-string (map) 96 "Convert MAP (hash-table) to an escaped string for use as a jj template." 97 (let ((k->s (lambda (k) (if (keywordp k) 98 (intern (s-replace "$" "\\$" (substring (symbol-name k) 1))) 99 k)))) 100 (->> map 101 (ht-map (lambda (key value) 102 (format "\\\"%s \\\" ++ %s ++ \\\"\\\\n\\\"" 103 (funcall k->s key) value))) 104 (s-join " ++ ")))) 105 106 (-tests 107 (let ((m (ht (:foo nil) 108 (:foo$bool "hello world") 109 (:bar 1)))) 110 (jujutsu-core--map-to-escaped-string m)) 111 := 112 "\\\"bar \\\" ++ 1 ++ \\\"\\\\n\\\" ++ \\\"foo \\\" ++ nil ++ \\\"\\\\n\\\"") 113 114 (defun jujutsu-core--parse-file-change (line) 115 "Parse a file change LINE into a hash-table." 116 (-let* [(regex (rx bos (group (any "AMD")) " " (group (+ not-newline)) eos) line) 117 ((res m1 m2) (s-match regex line))] 118 (when res 119 (ht (m1 m2))))) 120 121 (defun jujutsu-core--parse-key-value (line) 122 "Parse a KEY-VALUE LINE into a hash-table. 123 If the key contains `:list', the value is split based on `;'." 124 (let ((s->kw (lambda (s) (intern (s-prepend ":" s))))) 125 (unless (s-matches? (rx bos (any "AMD") " ") line) 126 (-when-let* [(regex (rx bos 127 (group (+ (not (any " ")))) ; key 128 " " 129 (optional (group (+ not-newline))) ; optional value 130 eos)) 131 ((res m1 m2) (s-match regex line))] 132 (let* ((key (funcall s->kw m1)) 133 (value (cond 134 ((s-ends-with? "$list" (symbol-name key)) 135 (s-split ";" m2 t)) 136 ((s-ends-with? "$bool" (symbol-name key)) 137 (s-equals? m2 "true")) 138 (t m2)))) 139 (ht (key value))))))) 140 141 (-comment 142 ;; Regular key-value pair 143 (jujutsu-core--parse-key-value "commit-id abc123") 144 ;; => #s(hash-table ... (:commit-id "abc123")) 145 146 ;; List key-value pair 147 (jujutsu-core--parse-key-value "bookmarks$list main;feature-1;hotfix") 148 ;; => #s(hash-table ... (:bookmarks$list ("main" "feature-1" "hotfix"))) 149 150 ;; Key with "$list" but no semicolons 151 (jujutsu-core--parse-key-value "files$list file1.txt") 152 ;; => #s(hash-table ... (:files$list ("file1.txt"))) 153 154 ;; Regular key with semicolons (not treated as a list) 155 (jujutsu-core--parse-key-value "description Some; text; here") 156 ;; => #s(hash-table ... (:description "Some; text; here")) 157 158 ;; Boolean key-value pair 159 (jujutsu-core--parse-key-value "empty$bool true") 160 ;; => #s(hash-table ... (:empty$bool t)) 161 (jujutsu-core--parse-key-value "empty$bool false") 162 ;; => #s(hash-table ... (:empty$bool nil)) 163 164 nil) 165 166 (defun jujutsu-core--parse-and-group-file-changes (file-changes) 167 "Parse and group FILE-CHANGES by their change type into a hash-table." 168 (let ((grouped-changes (ht (:files-added nil) 169 (:files-modified nil) 170 (:files-deleted nil)))) 171 (when-let ((parsed-changes (-map #'jujutsu-core--parse-file-change file-changes))) 172 (dolist (change parsed-changes) 173 (-let* [((&hash "A" a-file "M" m-file "D" d-file) change) 174 ((&hash :files-added a-coll :files-modified m-coll :files-deleted d-coll) grouped-changes)] 175 (when a-file (ht-set! grouped-changes :files-added (-concat a-coll (list a-file)))) 176 (when m-file (ht-set! grouped-changes :files-modified (-concat m-coll (list m-file)))) 177 (when d-file (ht-set! grouped-changes :files-deleted (-concat d-coll (list d-file))))))) 178 grouped-changes)) 179 180 (defun jujutsu-core--parse-string-to-map (input-string) 181 "Parse INPUT-STRING into a hash-table and an organized list of file change." 182 (let* ((lines (s-split "\n" input-string t)) 183 (file-change-lines (-filter #'jujutsu-core--parse-file-change lines)) 184 (grouped-file-changes (jujutsu-core--parse-and-group-file-changes file-change-lines)) 185 (key-values (-keep #'jujutsu-core--parse-key-value lines)) 186 (result-map (apply #'ht-merge key-values))) 187 (ht-merge result-map grouped-file-changes))) 188 189 (defun jujutsu-core--split-string-on-empty-lines (input-string) 190 "Split INPUT-STRING into multiple strings based on empty lines." 191 (let ((rx-split (rx bol (zero-or-more space) eol 192 (one-or-more (any space ?\n)) 193 bol (zero-or-more space) eol))) 194 (s-split rx-split input-string t))) 195 196 (defun jujutsu-core--get-status-data (rev) 197 "Get status data for the given REV." 198 (let ((template (ht (:change-id-short "change_id.short(8)") 199 (:change-id-shortest "change_id.shortest()") 200 (:commit-id-short "commit_id.short(8)") 201 (:commit-id-shortest "commit_id.shortest()") 202 (:empty$bool "empty") 203 (:bookmarks$list (jujutsu-core--template-list "bookmarks")) 204 (:git-head$bool "git_head") 205 (:description "description.first_line()")))) 206 (-> (jujutsu-core--map-to-escaped-string template) 207 (jujutsu-core--show-w/template rev) 208 jujutsu-core--parse-string-to-map))) 209 210 (-comment 211 212 (-time+ 213 (let ((template (ht (:change-id-short "change_id.short(8)") 214 (:change-id-shortest "change_id.shortest()") 215 (:commit-id-short "commit_id.short(8)") 216 (:commit-id-shortest "commit_id.shortest()") 217 (:empty$bool "empty") 218 (:bookmarks$list (jujutsu-core--template-list "bookmarks")) 219 (:git-head$bool "git_head") 220 (:description "description.first_line()")))) 221 (-> (jujutsu-core--map-to-escaped-string template)))) 222 nil) 223 224 225 (provide 'jujutsu-core) 226 ;;; jujutsu-core.el ends here