branch: externals/vc-jj commit dc5a3c79a5d65b7e65d2a2d2b17937c4c36be7f8 Author: Rudi Schlatte <r...@constantly.at> Commit: Rudi Schlatte <r...@constantly.at>
Be robust in the presence of large files If a file is too large to be added to the repository, all jj commands print a warning to stderr. Ignore stderr when calling jj so that we can parse the output. Fixes #52 --- NEWS.org | 10 ++++- vc-jj-tests.el | 13 +++++- vc-jj.el | 130 +++++++++++++++++++++++++++++++++++---------------------- 3 files changed, 99 insertions(+), 54 deletions(-) diff --git a/NEWS.org b/NEWS.org index 9142fc1113..1be5f7f13c 100644 --- a/NEWS.org +++ b/NEWS.org @@ -6,9 +6,15 @@ - Display more information in =vc-dir= headers: - Display change id, commit id, description of the current changeset - in one line + in one line. - Display information about the current changeset's parent(s) in the - same format + same format. + +*** Fixed + +- Fix breakage when the project directory contains a file too large to + be automatically added. (jj prints a warning in that case, even + when run with =--quiet=, so we need to discard output to stderr.) ** [[https://codeberg.org/emacs-jj-vc/vc-jj.el/compare/v0.1...v0.2][0.2]] - 2025-04-18 diff --git a/vc-jj-tests.el b/vc-jj-tests.el index 1cabdca062..9675fa429e 100644 --- a/vc-jj-tests.el +++ b/vc-jj-tests.el @@ -34,9 +34,9 @@ (defun vc-jj-test-environment (seq) "Create a list suitable for prepending to `process-environment'. The purpose is to make tests reproducible by fixing timestamps, -change ids, author information etc. SEQ is an integer that +change ids, author information etc. SEQ is an integer that modifies the JJ_RANDOMNESS_SEED, JJ_TIMESTAMP and JJ_OP_TIMESTAMP -environment variables. Increasing values for SEQ will result in +environment variables. Increasing values for SEQ will result in increasing timestamps. Note that it not necessary to use this function, except when @@ -210,5 +210,14 @@ is needed." ("with'apostrophe.txt" added) ("with\"quotation.txt" added)))))) +(ert-deftest vc-jj-looong-file () + ;; https://codeberg.org/emacs-jj-vc/vc-jj.el/issues/52 + (vc-jj-test-with-repo repo + (shell-command "jj config set --repo snapshot.max-new-file-size 12") + (write-region "1234567890" nil "numbers.txt") + (write-region "abcdefghijklmnopqrstuvwxyz" nil "alphabet.txt") + (should (eq (vc-jj-state "numbers.txt") 'added)) + (should (eq (vc-jj-state "alphabet.txt") 'ignored)))) + (provide 'vc-jj-tests) ;;; vc-jj-tests.el ends here diff --git a/vc-jj.el b/vc-jj.el index d41ca6a9aa..9b97be8746 100644 --- a/vc-jj.el +++ b/vc-jj.el @@ -101,31 +101,58 @@ If ROOT is given, the result is relative to the repository root, otherwise it is relative to the current directory." (if root (concat "root-file:\"" - (string-replace "\"" "\\\"" (file-relative-name filename root)) - "\"") + (string-replace "\"" "\\\"" (file-relative-name filename root)) + "\"") (concat "cwd-file:\"" (string-replace "\"" "\\\"" (file-relative-name filename)) "\""))) -(defun vc-jj-command (buffer okstatus file-or-list &rest flags) +(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'." + (with-temp-buffer + (let ((status (apply #'call-process vc-jj-program nil + ;; (current-buffer) + (list (current-buffer) nil) + nil args))) + (unless (eq status 0) + (error "'jj' exited with status %s" status)) + (goto-char (point-min)) + (let (lines) + (while (not (eobp)) + (setq lines (cons (buffer-substring-no-properties + (line-beginning-position) + (line-end-position)) + lines)) + (forward-line 1)) + (nreverse lines))))) + +(defun vc-jj--command-parseable (&rest args) + "Run jj with ARGS, returning its output as string. + +Note: any filenames in ARGS should be converted via +`vc-jj--filename-to-fileset'. + +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." + (with-temp-buffer + (let ((status (apply #'call-process vc-jj-program nil + (list (current-buffer) nil) + nil args))) + (unless (eq status 0) + (error "'jj' exited with status %s" status)) + (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. -The output goes to BUFFER, the current buffer if BUFFER is t, or a -buffer named \"*vc*\" if BUFFER is nil. If the destination buffer is -not already current, set it up properly and erase it. - -The command is considered successful if its exit status does not exceed -OKSTATUS (if OKSTATUS is nil, that means to ignore error status, if it -is 'async', that means not to wait for termination of the subprocess; if -it is t it means to ignore all execution errors). On unsuccessful -execution, raise an error. - -FILE-OR-LIST is the name of a working file; it may be a list of files or -be nil (to execute commands that don't expect a file name or set of -files). If an optional list of FLAGS is present, that is inserted into -the command line before the filename(s). - -Return the return value of the command in the synchronous case, and the -process object in the asynchronous case." +Note: this function should not be used when we need to parse jj's +output, since jj might print warnings to stderr and `vc-do-command' +cannot separate output to stdout and stderr." (let* ((filesets (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 @@ -163,12 +190,11 @@ process object in the asynchronous case." ;; "M ") or added (output starts with "A "), but no output could ;; be conflicted, ignored or unchanged (let* ((default-directory (vc-jj-root file)) + (file (vc-jj--filename-to-fileset file)) (conflicted-ignored - (with-output-to-string - (vc-jj-command standard-output 0 file "file" "list" "-T" "conflict" "--"))) + (vc-jj--command-parseable "file" "list" "-T" "conflict" "--" file)) (modified-added - (with-output-to-string (vc-jj-command standard-output 0 file - "diff" "--summary" "--")))) + (vc-jj--command-parseable "diff" "--summary" "--" file))) (cond ((string-empty-p conflicted-ignored) 'ignored) ((string= conflicted-ignored "true") 'conflict) @@ -200,11 +226,11 @@ Return the result result of applying UPDATE-FUNCTION to that list." (let* ((dir (expand-file-name dir)) (default-directory dir) (project-root (vc-jj-root dir)) - (registered-files (process-lines vc-jj-program "file" "list" "--" dir)) + (registered-files (vc-jj--process-lines "file" "list" "--" dir)) (ignored-files (seq-difference (cl-delete-if #'file-directory-p (directory-files dir nil nil t)) registered-files)) - (changed (process-lines vc-jj-program "diff" "--summary" "--" dir)) + (changed (vc-jj--process-lines "diff" "--summary" "--" dir)) (added-files (mapcan (lambda (entry) (and (string-prefix-p "A " entry) (list (substring entry 2)))) @@ -218,9 +244,8 @@ Return the result result of applying UPDATE-FUNCTION to that list." ;; expand-file-name / file-relative-name (conflicted-files (mapcar (lambda (entry) (file-relative-name (expand-file-name entry project-root) dir)) - (process-lines vc-jj-program - "file" "list" - "-T" "if(conflict, path ++ \"\\n\")" "--" dir))) + (vc-jj--process-lines "file" "list" + "-T" "if(conflict, path ++ \"\\n\")" "--" dir))) (unchanged-files (cl-remove-if (lambda (entry) (or (member entry conflicted-files) (member entry modified-files) (member entry added-files) @@ -251,8 +276,8 @@ anyway.)" description bookmarks conflict divergent hidden immutable &rest parent-info) (let ((default-directory (file-name-as-directory dir))) - (process-lines vc-jj-program "log" "--no-graph" "-r" "@" "-T" - "concat( + (vc-jj--process-lines "log" "--no-graph" "-r" "@" "-T" + "concat( self.change_id().short(8), \"\\n\", self.change_id().shortest(), \"\\n\", self.commit_id().short(8), \"\\n\", @@ -321,16 +346,19 @@ parents.map(|c| concat( (defun vc-jj-working-revision (file) "Return the current change id of the repository containing FILE." (when-let* ((default-directory (vc-jj-root file))) - (car (process-lines vc-jj-program "log" "--no-graph" - "-r" "@" - "-T" "change_id ++ \"\\n\"")))) + ;; '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" + "-r" "@" + "-T" "change_id ++ \"\\n\""))))) (defun vc-jj-mode-line-string (file) "Return a mode line string and tooltip for FILE." (pcase-let* ((long-rev (vc-jj-working-revision file)) (`(,short-rev ,description) - (process-lines vc-jj-program "log" "--no-graph" "-r" long-rev - "-T" "self.change_id().shortest() ++ \"\\n\" ++ description.first_line() ++ \"\\n\"")) + (vc-jj--process-lines "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)) (face (get-text-property 0 'face def-ml))) @@ -355,28 +383,31 @@ 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 nil 0 files "file" "track" "--")) + (vc-jj--command-dispatched nil 0 files "file" "track" "--")) (defun vc-jj-delete-file (file) "Delete FILE and make sure jj registers the change." (when (file-exists-p file) (delete-file file) - (vc-jj-command nil 0 nil "status"))) + (vc-jj--command-dispatched nil 0 nil "status"))) (defun vc-jj-rename-file (old new) "Rename file OLD to NEW and make sure jj registers the change." (rename-file old new) - (vc-jj-command nil 0 nil "status")) + (vc-jj--command-dispatched nil 0 nil "status")) (defun vc-jj-checkin (files comment &optional _rev) "Run \"jj commit\" with supplied FILES and COMMENT." (setq comment (replace-regexp-in-string "\\`Summary: " "" comment)) (let ((args (append (vc-switches 'jj 'checkin) (list "commit" "-m" comment)))) - (apply #'vc-jj-command nil 0 files args))) + (apply #'vc-jj--command-dispatched nil 0 files args))) (defun vc-jj-find-revision (file rev buffer) "Read revision REV of FILE into BUFFER and return the buffer." - (vc-jj-command buffer 0 file "file" "show" "-r" rev "--") + (let ((revision (vc-jj--command-parseable "file" "show" "-r" rev "--" (vc-jj--filename-to-fileset file)))) + (with-current-buffer buffer + (erase-buffer buffer) + (insert revision))) buffer) (defun vc-jj-checkout (file &optional rev) @@ -385,11 +416,11 @@ If REV is not specified, revert the file as with `vc-jj-revert'." ;; TODO: check that this does the right thing: if REV is not ;; specified, should we revert or leave FILE unchanged? (let ((args (append (and rev (list "--from" rev))))) - (apply #'vc-jj-command nil 0 file "restore" args))) + (apply #'vc-jj--command-dispatched nil 0 file "restore" args))) (defun vc-jj-revert (file &optional _contents-done) "Restore FILE to the state from its parent(s), via \"jj restore\"." - (vc-jj-command nil 0 file "restore")) + (vc-jj--command-dispatched nil 0 file "restore")) (defun vc-jj-print-log (files buffer &optional _shortlog start-revision limit) "Print commit log associated with FILES into specified BUFFER." @@ -459,7 +490,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 nil 0 file "file" (if remove "track" "untrack") "--"))) + (vc-jj--command-dispatched nil 0 file "file" (if remove "track" "untrack") "--"))) (defun vc-jj-diff (files &optional rev1 rev2 buffer _async) "Display diffs for FILES between revisions REV1 and REV2." @@ -477,7 +508,7 @@ For jj, modify `.gitignore' and call `jj untrack' or `jj track'." (erase-buffer)) (apply #'call-process vc-jj-program nil buffer nil "diff" "--from" rev1 "--to" rev2 args) (if (seq-some (lambda (line) (string-prefix-p "M " line)) - (apply #'process-lines vc-jj-program "diff" "--summary" "--" files)) + (apply #'vc-jj--process-lines "diff" "--summary" "--" files)) 1 0))) @@ -485,9 +516,9 @@ For jj, modify `.gitignore' and call `jj untrack' or `jj track'." "Fill BUF with per-line commit history of FILE at REV." (let ((rev (or rev "@")) (file (file-relative-name file))) - (apply #'vc-jj-command buf 'async nil "file" "annotate" - (append (vc-switches 'jj 'annotate) - (list "-r" rev file))))) + (apply #'vc-jj--command-dispatched buf 'async file "file" "annotate" + (append (vc-switches 'jj 'annotate) + (list "-r" rev))))) (defconst vc-jj--annotation-line-prefix-re (rx bol @@ -526,8 +557,7 @@ four groups: change id, author, datetime, line number.") (defun vc-jj-revision-completion-table (files) (let ((revisions - (apply #'process-lines - vc-jj-program "log" "--no-graph" + (apply #'vc-jj--process-lines "log" "--no-graph" "-T" "self.change_id() ++ \"\\n\"" "--" files))) (lambda (string pred action) (if (eq action 'metadata)