branch: externals/vc-jj
commit 5ab6538d55c99f995cd4f7487063cc1c7c1e5008
Author: Kristoffer Balintona <[email protected]>
Commit: Kristoffer Balintona <[email protected]>

    refactor: Internal process functions accept FILE-OR-LIST argument
    
    `vc-jj--command-parseable`, `vc-jj--process-lines`, and
    `vc-jj--command-dispatched` are the three internal functions we
    currently use for invoking jj commands.  Unify calling conventions
    such that each accepts a FILE-OR-LIST argument.  This centralizes
    their logic and ensures that a fileset is passed to jj as opposed to
    trusting the caller to convert files to fileset format.  (If the
    caller needs to pass the raw filename instead of a fileset, then they
    can do so using ARGS.)
    
    Also clean up these functions' docstrings and make them more
    informative.
---
 project-jj.el |   4 +-
 vc-jj.el      | 196 +++++++++++++++++++++++++++++++++-------------------------
 2 files changed, 115 insertions(+), 85 deletions(-)

diff --git a/project-jj.el b/project-jj.el
index f5d9619383..d68f24f758 100644
--- a/project-jj.el
+++ b/project-jj.el
@@ -39,8 +39,8 @@
       (progn
         (require 'vc-jj)
         (let* ((default-directory (expand-file-name (project-root project)))
-               (args (cons "--" (mapcar #'file-relative-name dirs)))
-               (files (apply #'vc-jj--process-lines "file" "list" args)))
+               (files (vc-jj--process-lines (mapcar #'file-relative-name dirs)
+                                            "file" "list")))
           (mapcar #'expand-file-name files)))
     (cl-call-next-method)))
 
diff --git a/vc-jj.el b/vc-jj.el
index 42b1672051..7226634cfd 100644
--- a/vc-jj.el
+++ b/vc-jj.el
@@ -247,15 +247,30 @@ When FILENAME is not inside a JJ repository, throw an 
error."
       (ansi-color-filter-region (point-min) (point-max))))
   (vc-set-async-update buffer))
 
-(defun vc-jj--process-lines (&rest args)
-  "Run jj with ARGS, returning its output to stdout as a list of strings.
-In contrast to `process-lines', discard output to stderr since jj prints
-warnings to stderr even when run with '--quiet'."
+(defun vc-jj--process-lines (file-or-list &rest args)
+  "Run jj with FILE-OR-LIST and ARGS, returning stdout as a list of strings.
+Return the process\\='s stdout as a list of strings, one string for
+every line in stdout, with ANSI escape sequences removed from each
+string.  An error is signaled if jj exits with a non-zero status.
+
+All stderr is discarded (since jj prints warnings to stderr even when
+run with '--quiet').  As a result, the returned strings are safe for
+parsing.
+
+Unlike `vc-jj--command-dispatched', this function is synchronous and
+does not report on the status of the process.
+
+FILE-OR-LIST may be a string, a list of strings, or nil.  When non-nil,
+the file names are appended to ARGS as a jj fileset and passed to jj.
+If the caller would like to pass a file or list of files as paths not
+converted to a jj fileset, they must be passed in ARGS.
+
+See also `vc-jj--command-parseable' and `vc-jj--command-dispatched.'"
   (with-temp-buffer
-    (let ((status (apply #'process-file vc-jj-program nil
-                         ;; (current-buffer)
-                         (list (current-buffer) nil)
-                         nil args)))
+    (let* ((fileset (mapcar #'vc-jj--filename-to-fileset (ensure-list 
file-or-list)))
+           (status (apply #'process-file vc-jj-program nil
+                          (list (current-buffer) nil) nil
+                          (append args (when fileset (cons "--" fileset))))))
       (unless (eq status 0)
        (error "'jj' exited with status %s" status))
       (ansi-color-filter-region (point-min) (point-max))
@@ -269,41 +284,62 @@ warnings to stderr even when run with '--quiet'."
           (forward-line 1))
         (nreverse lines)))))
 
-(defun vc-jj--command-parseable (&rest args)
-  "Run jj with ARGS, returning its output as string.
+(defun vc-jj--command-parseable (file-or-list &rest args)
+  "Call jj with FILE-OR-LIST and ARGS, returning stdout as a string.
+Return is the process\\='s stdout with ANSI escape sequences removed.
+An error is signaled if jj exits with a non-zero status.
 
-Note: any filenames in ARGS should be converted via
-`vc-jj--filename-to-fileset'.
+All stderr is discarded (since jj prints warnings to stderr even when
+run with '--quiet').  As a result, the returned string is safe for
+parsing.
 
-In contrast to `vc-jj--command-dispatched', discard output to stderr so
-the output can be safely parsed.  Does not support many of the features
-of `vc-jj--command-dispatched', such as async execution and checking of
-process status."
+Unlike `vc-jj--command-dispatched', this function is synchronous and
+does not report on the status of the process.
+
+FILE-OR-LIST may be a string, a list of strings, or nil.  When non-nil,
+the file names are appended to ARGS as a jj fileset and passed to jj.
+If the caller would like to pass a file or list of files as paths not
+converted to a jj fileset, they must be passed in ARGS.
+
+See also `vc-jj--process-lines'."
   (with-temp-buffer
-    (let ((status (apply #'process-file vc-jj-program nil
-                         (list (current-buffer) nil)
-                         nil args)))
+    (let* ((fileset (mapcar #'vc-jj--filename-to-fileset (ensure-list 
file-or-list)))
+           (status (apply #'process-file vc-jj-program nil
+                          (list (current-buffer) nil) nil
+                          (append args (when fileset (cons "--" fileset))))))
       (unless (eq status 0)
        (error "'jj' exited with status %s" status))
       (ansi-color-filter-region (point-min) (point-max))
       (buffer-substring-no-properties (point-min) (point-max)))))
 
-(defun vc-jj--command-dispatched (buffer okstatus file-or-list &rest flags)
-  "Execute `vc-jj-program', notifying the user and checking for errors.
-See `vc-do-command' for documentation of the BUFFER, OKSTATUS,
-FILE-OR-LIST and FLAGS arguments.
-
-Note: if we need to parse jj's output `vc-jj--command-parseable' should
-be used instead of this function, since jj might print warnings to
-stderr and1 `vc-do-command' cannot separate output to stdout and stderr."
-  (let* ((filesets (mapcar #'vc-jj--filename-to-fileset (ensure-list 
file-or-list)))
+(defun vc-jj--command-dispatched (buffer okstatus file-or-list &rest args)
+  "Run `vc-jj-program' with ARGS.
+The meaning of BUFFER, OKSTATUS, and ARGS is the same as in
+`vc-do-command'; see its documentation for details.  All stderr output
+from the process is discarded so that the returned string is safe for
+parsing.
+
+If it is necessary to analyze or interpret jj\\='s output
+programmatically, use `vc-jj--command-parseable' or
+`vc-jj--process-lines' instead.  Those functions are separate from this
+one because jj may write warnings to stderr, and `vc-do-command' (which
+the this function uses and the above ones do not) does not distinguish
+stdout from stderr, making the output unsafe to process automatically.
+
+FILE-OR-LIST may be a string, a list of strings, or nil.  When non-nil,
+the file names are appended to ARGS as a jj fileset and passed to jj.
+If the caller would like to pass a file or list of files as paths not
+converted to a jj fileset, they must be passed in ARGS.
+
+See also `vc-jj--command-parseable' and `vc-jj--process-lines'."
+  (let* ((fileset (mapcar #'vc-jj--filename-to-fileset (ensure-list 
file-or-list)))
          (global-switches (ensure-list vc-jj-global-switches)))
     (apply #'vc-do-command (or buffer "*vc*") okstatus vc-jj-program
-           ;; Note that we pass NIL for FILE-OR-LIST to avoid
+           ;; Note that we pass nil for FILE-OR-LIST to avoid
            ;; vc-do-command mangling of filenames; we pass the fileset
            ;; expressions in ARGS instead.
            nil
-           (append global-switches flags filesets))))
+           (append global-switches args (when fileset (cons "--" fileset))))))
 
 ;;; BACKEND PROPERTIES
 
@@ -332,8 +368,7 @@ stderr and1 `vc-do-command' cannot separate output to 
stdout and stderr."
   "Check whether FILE is registered with jj.
 Return non-nil when FILE is file tracked by JJ and nil when not."
   (when-let ((default-directory (vc-jj-root file)))
-    (vc-jj--process-lines "file" "list" "--"
-                          (vc-jj--filename-to-fileset file))))
+    (vc-jj--process-lines file "file" "list")))
 
 ;;;; state
 
@@ -367,15 +402,13 @@ there is no such state in jj since jj automatically 
registers new files."
   ;;   of these are already covered by the previous command, so we
   ;;   deduce "up-to-date".
   (let* ((default-directory (vc-jj-root file))
-         (file (vc-jj--filename-to-fileset file))
-         (changed (vc-jj--command-parseable "diff" "--summary" "--" file)))
+         (changed (vc-jj--command-parseable file "diff" "--summary")))
     (cond
      ((string-prefix-p "A " changed) 'added)
      ((string-prefix-p "D " changed) 'removed)
      ((string-prefix-p "M " changed) 'edited)
      (t (let ((conflicted-ignored
-               (vc-jj--command-parseable "file" "list" "-T" "conflict" "--"
-                                         file)))
+               (vc-jj--command-parseable file "file" "list" "-T" "conflict")))
           (cond
            ((string= conflicted-ignored "true") 'conflict)
            ((string-empty-p conflicted-ignored) 'ignored)
@@ -414,12 +447,11 @@ For a description of the states relevant to jj, see 
`vc-jj-state'."
     (let* ((dir (expand-file-name dir))
            (default-directory dir)
            (project-root (vc-jj-root dir))
-           (fileset (vc-jj--filename-to-fileset dir))
-           (registered-files (vc-jj--process-lines "file" "list" "--" fileset))
+           (registered-files (vc-jj--process-lines dir "file" "list"))
            (ignored-files (seq-difference (cl-delete-if #'file-directory-p
                                                         (directory-files dir 
nil nil t))
                                           registered-files))
-           (changed (vc-jj--process-lines "diff" "--summary" "--" fileset))
+           (changed (vc-jj--process-lines dir "diff" "--summary"))
            (added-files (mapcan (lambda (entry)
                                   (and (string-prefix-p "A " entry)
                                        (list (substring entry 2))))
@@ -437,9 +469,8 @@ For a description of the states relevant to jj, see 
`vc-jj-state'."
            ;; expand-file-name / file-relative-name
            (conflicted-files (mapcar (lambda (entry)
                                        (file-relative-name (expand-file-name 
entry project-root) dir))
-                                     (vc-jj--process-lines "file" "list"
-                                                           "-T" "if(conflict, 
path ++ '\n')"
-                                                           "--" fileset)))
+                                     (vc-jj--process-lines dir "file" "list"
+                                                           "-T" "if(conflict, 
path ++ '\n')")))
            (up-to-date-files (cl-remove-if (lambda (entry) (or (member entry 
conflicted-files)
                                                               (member entry 
edited-files)
                                                               (member entry 
added-files)
@@ -473,7 +504,7 @@ anyway.)"
         description bookmarks conflict divergent hidden immutable
         &rest parent-info)
       (let ((default-directory (file-name-as-directory dir)))
-        (vc-jj--process-lines "log" "--no-graph" "-r" "@" "-T"
+        (vc-jj--process-lines nil "log" "--no-graph" "-r" "@" "-T"
                               "concat(
 self.change_id().short(8), '\n',
 self.change_id().shortest(), '\n',
@@ -552,7 +583,7 @@ parents.map(|c| concat(
     ;; 'jj log' might print a warning at the start of its output,
     ;; e.g., "Warning: Refused to snapshot some files."  The output we
     ;; want will be printed afterwards.
-    (car (last (vc-jj--process-lines "log" "--no-graph"
+    (car (last (vc-jj--process-lines nil "log" "--no-graph"
                                      "-r" "@"
                                      "-T" "change_id ++ '\n'")))))
 
@@ -568,7 +599,7 @@ parents.map(|c| concat(
   "Return a mode line string and tooltip for FILE."
   (pcase-let* ((long-rev (vc-jj-working-revision file))
                (`(,short-rev ,description)
-                (vc-jj--process-lines "log" "--no-graph" "-r" long-rev
+                (vc-jj--process-lines nil "log" "--no-graph" "-r" long-rev
                                       "-T" "self.change_id().shortest() ++ 
'\n' ++ description.first_line() ++ '\n'"))
                (def-ml (vc-default-mode-line-string 'JJ file))
                (help-echo (get-text-property 0 'help-echo def-ml))
@@ -600,7 +631,7 @@ parents.map(|c| concat(
   ;; run "jj file track" for the case where some of FILES are excluded
   ;; via the "snapshot.auto-track" setting or via git's mechanisms
   ;; such as the .gitignore file.
-  (vc-jj--command-dispatched nil 0 files "file" "track" "--"))
+  (vc-jj--command-dispatched nil 0 files "file" "track"))
 
 ;;;; responsible-p
 
@@ -625,7 +656,7 @@ parents.map(|c| concat(
 
 (defun vc-jj-find-revision (file rev buffer)
   "Read revision REV of FILE into BUFFER and return the buffer."
-  (let ((revision (vc-jj--command-parseable "file" "show" "-r" rev "--" 
(vc-jj--filename-to-fileset file))))
+  (let ((revision (vc-jj--command-parseable file "file" "show" "-r" rev)))
     (with-current-buffer buffer
       (erase-buffer)
       (insert revision)))
@@ -705,7 +736,7 @@ push\")."
 
 (defun vc-jj-get-change-comment (_files rev)
   "Get the change comment of revision REV."
-  (vc-jj--command-parseable "log" "--no-graph" "-n" "1"
+  (vc-jj--command-parseable nil "log" "--no-graph" "-n" "1"
                             "-r" rev "-T" "description"))
 
 ;;;; modify-change-comment
@@ -794,7 +825,7 @@ reverting.  If that revision no longer exists, do not move 
the point."
         ;; something has gone wrong (because that would likely be the
         ;; case with the default behavior, without this function)
         (message "Buffer reverted and point restored to revision %s"
-                 (propertize (vc-jj--command-parseable "show" "--no-patch"
+                 (propertize (vc-jj--command-parseable nil "show" "--no-patch"
                                                        "-r" rev
                                                        "-T" 
"change_id.shortest()")
                              'face 'log-view-message))))))
@@ -802,17 +833,15 @@ reverting.  If that revision no longer exists, do not 
move the point."
 (defun vc-jj--expanded-log-entry (revision)
   "Return a string of the commit details of REVISION.
 Called by `log-view-toggle-entry-display' in a JJ Log View buffer."
-  (with-temp-buffer
-    (vc-jj--command-dispatched
-     t 0 nil "log"
-     ;; REVISION may be divergent (i.e., several revisions with the
-     ;; same change ID).  In those cases, we opt to avoid jj erroring
-     ;; via "-r change_id(REVISION)" and show only all the divergent
-     ;; commits.  This is preferable to confusing or misinforming the
-     ;; user by showing only some of the divergent commits.
-     "-r" (format "change_id(%s)" revision)
-     "--no-graph" "-T" "builtin_log_detailed")
-    (buffer-string)))
+  (vc-jj--command-parseable
+   nil "log" "--no-graph"
+   ;; REVISION may be divergent (i.e., several revisions with the same
+   ;; change ID).  In those cases, we opt to avoid jj erroring via "-r
+   ;; change_id(REVISION)" and show only all the divergent commits.
+   ;; This is preferable to confusing or misinforming the user by
+   ;; showing only some of the divergent commits.
+   "-r" (format "change_id(%s)" revision)
+   "-T" "builtin_log_detailed"))
 
 (defvar auto-revert-mode)
 
@@ -854,7 +883,7 @@ Call \"jj abandon\" on the revision at point."
   (let ((rev (log-view-current-tag)))
     (when (y-or-n-p (format "Abandon revision %s?"
                             (propertize
-                             (vc-jj--command-parseable "show" "--no-patch"
+                             (vc-jj--command-parseable nil "show" "--no-patch"
                                                        "-r" rev
                                                        "-T" 
"change_id.shortest()")
                              'face 'log-view-message)))
@@ -879,7 +908,7 @@ user first."
   (interactive nil vc-jj-log-view-mode)
   (when (derived-mode-p 'vc-jj-log-view-mode)
     (let* ((target-rev (log-view-current-tag))
-           (bookmarks (vc-jj--process-lines "bookmark" "list" "-T" 
"self.name() ++ '\n'"))
+           (bookmarks (vc-jj--process-lines nil "bookmark" "list" "-T" 
"self.name() ++ '\n'"))
            (bookmark (completing-read "Move or create bookmark: " bookmarks))
            (new-bookmark-p (not (member bookmark bookmarks)))
            ;; If the bookmark already exists and target-rev is not a
@@ -894,7 +923,7 @@ user first."
               ;; descendants, and TARGET-REV.  That intersection is
               ;; non-empty only when TARGET-REV is BOOKMARK or a
               ;; descendant or BOOKMARK
-              (not (vc-jj--process-lines "log" "-r" (format "%s & %s::" 
target-rev bookmark))))))
+              (not (vc-jj--process-lines nil "log" "-r" (format "%s & %s::" 
target-rev bookmark))))))
       (when backwards-move-p
         (unless (yes-or-no-p
                  (format-prompt "Moving bookmark %s to revision %s would move 
it either backwards or sideways. Is this okay?"
@@ -914,14 +943,14 @@ rename."
   (when (derived-mode-p 'vc-jj-log-view-mode)
     (let* ((target-rev (log-view-current-tag))
            (bookmarks-at-rev
-            (or (vc-jj--process-lines "bookmark" "list" "-r" target-rev
+            (or (vc-jj--process-lines nil "bookmark" "list" "-r" target-rev
                                       "-T" "if(!self.remote(), self.name() ++ 
'\n')")
                 ;; FIXME(KrisB 2025-12-09): Is there a more
                 ;; idiomatic/cleaner way to exit with a message than a
                 ;; `user-error' in the middle of a let binding?
                 (user-error "No bookmarks at %s"
                             (propertize
-                             (vc-jj--command-parseable "show" "--no-patch"
+                             (vc-jj--command-parseable nil "show" "--no-patch"
                                                        "-r" target-rev
                                                        "-T" 
"change_id.shortest()")
                              'face 'log-view-message))))
@@ -947,7 +976,7 @@ delete."
            (revision-bookmarks
             (string-split
              (vc-jj--command-parseable
-              "show" "-r" rev "--no-patch"
+              nil "show" "-r" rev "--no-patch"
               "-T" "self.local_bookmarks().map(|b| b.name()) ++ '\n'")
              " " t "\n"))
            (bookmarks
@@ -1096,7 +1125,8 @@ line of its description."
   (let ((description
          (if (listp elem)
              (cl-second elem)
-           (vc-jj--command-parseable "log" "-r" elem "--no-graph" "-T" 
"self.description().first_line()"))))
+           (vc-jj--command-parseable nil "log" "--no-graph"
+                                     "-r" elem "-T" 
"self.description().first_line()"))))
     (format " %s" (propertize description 'face 'completions-annotations))))
 
 (defun vc-jj-revision-completion-table (files)
@@ -1105,8 +1135,8 @@ line of its description."
           (mapcar
            ;; Boldly assuming that jj's change ids won't suddenly change length
            (lambda (line) (list (substring line 0 31) (substring line 32)))
-           (apply #'vc-jj--process-lines "log" "--no-graph"
-                  "-T" "self.change_id() ++ self.description().first_line() ++ 
'\n'" "--" files))))
+           (apply #'vc-jj--process-lines files "log" "--no-graph"
+                  "-T" "self.change_id() ++ self.description().first_line() ++ 
'\n'"))))
     (lambda (string pred action)
       (if (eq action 'metadata)
           `(metadata . ((display-sort-function . ,#'identity)
@@ -1117,14 +1147,16 @@ line of its description."
 
 (defun vc-jj-annotate-command (file buf &optional rev)
   "Fill BUF with per-line change history of FILE at REV."
-  (let ((rev (or rev "@"))
-        (file (file-relative-name file)))
+  (let ((rev (or rev "@")))
     ;; Contrary to most other jj commands, 'jj file annotate' takes a
     ;; path instead of a fileset expression, so we append FILE to the
     ;; unprocessed argument list here.
     (apply #'vc-jj--command-dispatched buf 'async nil "file" "annotate"
            (append (vc-switches 'jj 'annotate)
-                   (list "-r" rev file)))))
+                   (list "-r" rev
+                         ;; "jj file annotate" expects a file path,
+                         ;; not a fileset expression
+                         (file-relative-name file))))))
 
 ;;;; annotate-time
 
@@ -1236,7 +1268,7 @@ For jj, modify `.gitignore' and call `jj untrack' or `jj 
track'."
   (let ((default-directory
          (if directory (file-name-as-directory directory)
            default-directory)))
-    (vc-jj--command-dispatched nil 0 file "file" (if remove "track" "untrack") 
"--")))
+    (vc-jj--command-dispatched nil 0 file "file" (if remove "track" 
"untrack"))))
 
 ;;;; ignore-completion-table
 
@@ -1256,15 +1288,14 @@ For jj, modify `.gitignore' and call `jj untrack' or 
`jj track'."
 Return the revision number that precedes REV for FILE, or nil if no such
 revision exists."
   (if file
-      (vc-jj--command-parseable "log" "--no-graph" "--limit" "1"
+      (vc-jj--command-parseable file "log" "--no-graph" "--limit" "1"
                                 "-r" (format "ancestors(%s) & ~%s" rev rev)
-                                "-T" "change_id"
-                                "--" (vc-jj--filename-to-fileset file))
+                                "-T" "change_id")
     ;; The jj manual states that "for merges, [first_parent] only
     ;; returns the first parent instead of returning all parents";
     ;; given the choice, we do want to return the first parent of a
     ;; merge change.
-    (vc-jj--command-parseable "log" "--no-graph"
+    (vc-jj--command-parseable nil "log" "--no-graph"
                               "-r" (concat "first_parent(" rev ")")
                               "-T" "change_id")))
 
@@ -1277,17 +1308,16 @@ revision exists."
 Return the revision that follows REV for FILE, or nil if no such
 revision exists."
   (if file
-      (vc-jj--command-parseable "log" "--no-graph" "--limit" "1"
+      (vc-jj--command-parseable file "log" "--no-graph" "--limit" "1"
                                 "-r" (concat "descendants(" rev ")")
-                                 "-T" "change_id"
-                                 "--" (vc-jj--filename-to-fileset file))
+                                "-T" "change_id")
     ;; Note: experimentally, jj (as of 0.35.0) prints children in LIFO
     ;; order (newest child first), but we should not rely on that
     ;; behavior and since none of the children of a change are
     ;; special, we return an arbitrary one.
-    (car (vc-jj--process-lines "log" "--no-graph"
-                                 "-r" (concat "children(" rev ")")
-                                 "-T" "change_id ++ '\n'"))))
+    (car (vc-jj--process-lines nil "log" "--no-graph"
+                               "-r" (concat "children(" rev ")")
+                               "-T" "change_id ++ '\n'"))))
 
 ;;;; log-edit-mode
 

Reply via email to