branch: externals/vc-jj
commit 276257b4c3a49cc85f8aa8452ccd761eb2593d5c
Author: Kristoffer Balintona <[email protected]>
Commit: Kristoffer Balintona <[email protected]>
refactor: Delegate VC state-deducing logic to helper functions
`vc-jj--deduce-state` now does the heavy-lifting for deducing the
state for a given file. It takes two other arguments that provide all
the information sufficient to deduce every state. Those arguments can
be constructed appropriately with our new helpers:
`vc-jj--parse-diff-types-file-alist` and
`vc-jj--parse-conflict-aware-file-alist`.
`vc-jj-state` and similar functions are expected to rely on the new
`vc-jj--deduce-state` from now on.
---
vc-jj.el | 193 ++++++++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 148 insertions(+), 45 deletions(-)
diff --git a/vc-jj.el b/vc-jj.el
index 0fb4c0845a..bf5ed78502 100644
--- a/vc-jj.el
+++ b/vc-jj.el
@@ -392,52 +392,155 @@ Return non-nil when FILE is file tracked by JJ and nil
when not."
;;;; state
+(defconst vc-jj--conflict-aware-file-list-template
+ "if(self.conflict(), 'C', 'T') ++ ' ' ++ self.path() ++ '\n'"
+ "Template for outputting a file list with conflict information.
+This template is intended as the value of the \"-T\" argument passed to
+\"jj file list\". It is meant to be used in conjunction with
+`vc-jj--parse-conflict-aware-file-table'.")
+
+(defun vc-jj--parse-conflict-aware-file-table (lines)
+ "Return a hash table of tracked files with information on conflicted state.
+The returned table maps FILE to CONFLICTEDP, where FILE is a tracked
+file and CONFLICTEDP is t if that file is in a conflicted state. This
+table helps deduce whether a given file is tracked and whether it is
+conflicted.
+
+LINES is a list of strings, where each string is a line of the output of
+\"jj file list\" with a template of
+`vc-jj--conflict-aware-file-list-template', which shows a list of all
+tracked files, with each path prepended with \"C\" or \"T\" depending on
+its conflict state."
+ (let ((table (make-hash-table :test #'equal)))
+ (mapc (lambda (line)
+ (puthash (substring line 2) (eq ?C (aref line 0)) table))
+ lines)
+ table))
+
+(defun vc-jj--parse-diff-types-file-table (lines)
+ "Return a hash table of changed files with their diff types.
+The returned table maps FILE to TYPES, where FILE is a changed file and
+TYPES is a two-character string indicating the before and after types of
+that file. See `vc-jj--deduce-state-from-diff-types' for which VC
+states can be deduced from this two-character string.
+
+LINES is a list of strings, where each string is a line of the output of
+\"jj diff --types\", which shows a list of all changed files, with each
+path prepended with a two-character type string indicating the before
+and after types of the file."
+ (let ((table (make-hash-table :test #'equal)))
+ (mapc (lambda (line)
+ (puthash (substring line 3) (substring line 0 2) table))
+ lines)
+ table))
+
+(defun vc-jj--deduce-state-from-diff-types (diff-types)
+ "Return the file state deduced from DIFF-TYPES.
+DIFF-TYPES is either nil or a two-character string at the beginning of
+each line in of \"jj diff --types\". See \"jj diff --help\" for a list
+of all possible characters and their meanings.
+
+When DIFF-TYPES is nil, return nil. When it is a two-character string,
+return a symbol representing the VC state deduced from DIFF-TYPES. The
+possible state symbols returned are:
+- \\='conflict
+- \\='added
+- \\='removed
+- \\='edited
+See `vc-jj-state' for a list of all VC states relevant to Jujutsu.
+
+Please note that the conflict state is only partially deduced by this
+function: \"jj diff\" reports a conflict only when the conflict
+originates in the working copy, not if the conflict originates in an
+earlier revision. To definitively determine the conflict state of a
+file, something like \"jj file list -T \\='self.conflict()\\='\" should
+be used (see `vc-jj--parse-conflict-aware-file-table'.)"
+ (when diff-types
+ (let ((before (aref diff-types 0))
+ (after (aref diff-types 1)))
+ (cond ((eq ?C after) 'conflict)
+ ((eq ?- before) 'added)
+ ((eq ?- after) 'removed)
+ ((and (memq before '(?F ?L ?G)) (memq after '(?F ?L ?G)))
+ 'edited)
+ (t (error "vc-jj: Unexpected diff types string: %s"
diff-types))))))
+
+(defun vc-jj--deduce-state (file file-diff-types-table file-conflict-table)
+ "Deduce the VC state of FILE from already parsed jj output.
+Return a symbol corresponding to the VC state of FILE. See
+`vc-jj-state' for the possible VC state symbols.
+
+FILE is a path relative to the repository root. FILE-DIFF-TYPES-TABLE
+is a hash table produced by `vc-jj--parse-diff-types-file-table'.
+FILE-CONFLICT-TABLE is a hash table produced by
+`vc-jj--parse-conflict-aware-file-table'. Together,
+FILE-DIFF-TYPES-TABLE and FILE-CONFLICT-TABLE suffice to deduce the VC
+state of FILE.
+
+Note that since FILE-DIFF-TYPES-TABLE should be produced by
+`vc-jj--parse-diff-types-file-table', FILE-DIFF-TYPES-TABLE will only
+contain file entries for files with changes in the working copy. On the
+other hand, since FILE-CONFLICT-TABLE should be produced by
+`vc-jj--parse-conflict-aware-file-table', FILE-CONFLICT-TABLE will only
+contain file entries for tracked files.
+
+To avoid unnecessary computation, calls to
+`vc-jj--parse-diff-types-file-table' and
+`vc-jj--parse-conflict-aware-file-table' can be passed jj output
+specific to FILE. As an example for
+`vc-jj--parse-diff-types-file-table',
+
+ (vc-jj--process-lines FILE \"diff\" \"--types\")"
+ ;; We use hash tables rather than e.g. alists for the sake of
+ ;; performance: table lookups are O(1) whereas list lookups are O(n)
+ (let* ((diff-types (gethash file file-diff-types-table))
+ (diff-types-state (vc-jj--deduce-state-from-diff-types diff-types)))
+ (or
+ ;; DIFF-TYPES-STATE definitely deduces the edited, removed, and
+ ;; added states, as well as the conflict state when the conflict
+ ;; originates in the working copy
+ diff-types-state
+ ;; When DIFF-TYPES-STATE is nil (i.e., FILE has no changes in the
+ ;; working copy), use `file-conflict-table' to distinguish
+ ;; between the ignored, up-to-date, and conflict states. (The
+ ;; conflict state can only be caught by DIFF-TYPES-STATE when the
+ ;; conflict originates in the working copy; `file-conflict-table'
+ ;; catches when a conflict originates in an earlier revision.)
+ (let ((conflictp (gethash file file-conflict-table 'not-tracked)))
+ (cond ((eq conflictp 'not-tracked) 'ignored)
+ (conflictp 'conflict)
+ (t 'up-to-date))))))
+
(defun vc-jj-state (file)
- "JJ implementation of `vc-state' for FILE.
-There are several file states recognized by vc (see the docstring of
-`vc-state' for the full list). Several of these are relevant to
-jujutsu. They are:
-- Added (new file)
-- Removed (deleted file)
-- Edited (modified file)
-- Up-to-date (unmodified file)
-- Conflict (merge conflict)
-- Ignored (ignored by repository)
-
-Other VCS backends would also recognize the \"unregistered\" state, but
-there is no such state in jj since jj automatically registers new files."
- ;; We can deduce all of the vc states listed above with two
- ;; commands:
- ;;
- ;; - "jj diff --summary FILE" gets us the "added" state (when output
- ;; starts with "A "), the "removed" state (when output starts with
- ;; "D ") and "edited" state (when output starts with "M "). No
- ;; output is also possible (this could mean the "conflict",
- ;; "ignored" or "unchanged" state), so we deduce these with the
- ;; next command:
- ;;
- ;; - "jj file list -T 'conflict' FILE" prints "true" when the file
- ;; is in "conflict" state, prints an empty string when the file is
- ;; in "ignored" state, and "false" for any other state -- but most
- ;; of these are already covered by the previous command, so we
- ;; deduce "up-to-date".
- (let* ((default-directory (vc-jj-root 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 "file" "list" "-T" "conflict")))
- (cond
- ((string= conflicted-ignored "true") 'conflict)
- ((string-empty-p conflicted-ignored) 'ignored)
- ;; If the file hasn't matched anything yet, this leaves
- ;; only one possible state: up-to-date
- ((string= conflicted-ignored "false") 'up-to-date)
- (t
- (warn "VC state of %s is not recognized, assuming up-to-date" file)
- 'up-to-date)))))))
+ "Return VC state symbol of FILE.
+There are several file states recognized by VC (see the docstring of
+`vc-state' for the full list). Only several of these are relevant to
+Jujutsu. They are:
+- \\='added (new file)
+- \\='removed (deleted file)
+- \\='edited (modified file)
+- \\='conflict (merge conflict)
+- \\='up-to-date (unmodified file)
+- \\='ignored (ignored by repository)
+Other VC backends would also recognize the \"unregistered\" state, but
+there is no such state in Jujutsu since Jujutsu automatically registers
+new files."
+ ;; TODO 2026-03-22: Ideally, we would only call one jj command, or
+ ;; only call a second one when necessary (instead of calling two
+ ;; commands always). If, in the future, jj provides templates that
+ ;; make this possible, then this function, its helpers, and
+ ;; `vc-jj-dir-status-files' should be refactored
+ (let* ((root (vc-jj-root file))
+ (file (file-relative-name file root))
+ (default-directory root)
+ (file-diff-types-table
+ (vc-jj--parse-diff-types-file-table
+ (vc-jj--process-lines file "diff" "--types")))
+ (file-conflict-table
+ (vc-jj--parse-conflict-aware-file-table
+ (vc-jj--process-lines file "file" "list"
+ "-T"
vc-jj--conflict-aware-file-list-template))))
+ (vc-jj--deduce-state file file-diff-types-table file-conflict-table)))
;;;; dir-status-file