/ 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