branch: externals/vc-jj
commit eb68f2049527574b0ea3b8a5fd01d79e2f179c70
Author: Rudi Schlatte <[email protected]>
Commit: Rudi Schlatte <[email protected]>

    Reorganize file layout to follow the API list in vc.el
    
    - Also rename commands that are bound in `vc-jj-log-view-mode`
      (edit-change, bookmark-set, etc.) to have a `vc-jj--` prefix instead
      of `vc-jj-`, since otherwise they look a lot like "official" vc API
      implementations.
    
    - Add autoload cookie to `(add-to-list 'vc-handled-backends 'JJ)`
    
    Fixes #127
---
 vc-jj.el | 952 ++++++++++++++++++++++++++++++++++++++-------------------------
 1 file changed, 575 insertions(+), 377 deletions(-)

diff --git a/vc-jj.el b/vc-jj.el
index 1916b81bf4..f881e5bcda 100644
--- a/vc-jj.el
+++ b/vc-jj.el
@@ -24,7 +24,39 @@
 
 ;;; Commentary:
 
-;; A backend for vc.el to handle Jujutsu repositories.
+;; A backend for vc.el to handle Jujutsu (jj) repositories.
+;;
+;; Jujutsu is a distributed version control system that uses the same
+;; on-disk storage format as git (although other storage backends are
+;; possible).  Jj repositories are "co-located" by default, which
+;; means they contain both a '.jj' and a '.git' directory in the
+;; repository root.  Therefore, vc-jj can re-use some functionality of
+;; vc-git, like handling of '.gitignore' files.  It is possible to run
+;; git commands in a co-located repository, although git will complain
+;; about being in "detached HEAD" state.
+;;
+;; Note that 'JJ' should come before 'Git' in `vc-handled-backends' so
+;; that 'vc' chooses the jj backend.
+;;
+;; In jj terminology, vc "revisions" are called "changes" and are
+;; identified by a "change ID"; the code in vc-jj tries to adhere to
+;; this terminology.
+;;
+;; In contrast to git, there is no staging area or check-in operation:
+;; as the user modifies a file, the changes are automatically
+;; recorded.  Note that the change ID stays constant when the current
+;; change is being modified, e.g., by editing a file.  Underlying each
+;; change is a (git-type) commit with a commit ID; the commit ID
+;; changes when a change gets modified.  Most jj commands accept both
+;; change IDs and commit IDs.  We display both change ID and commit ID
+;; where appropriate, for example in the extra headers of vc-dir
+;; buffers.
+;;
+
+;; After the "Customization" and "Internal Utilities" sections, the
+;; organization of this file follows the "BACKEND PROPERTIES" section
+;; at the beginning of 'vc.el', implementing the functions listed
+;; there in the same order.
 
 ;;; Code:
 
@@ -38,12 +70,9 @@
 (require 'ansi-color)
 (require 'iso8601)
 (require 'time-date)
+(declare-function vc-annotate-convert-time "vc-annotate" (&optional time))
 
-(add-to-list 'vc-handled-backends 'JJ)
-
-(defun vc-jj-revision-granularity () 'repository)
-(defun vc-jj-checkout-model (_files) 'implicit)
-(defun vc-jj-update-on-retrieve-tag () nil)
+;;; Customization
 
 (defgroup vc-jj nil
   "VC Jujutsu backend."
@@ -54,35 +83,6 @@
   :type 'string
   :risky t)
 
-(defvar vc-jj--log-default-template
-  "
-if(root,
-  format_root_commit(self),
-  label(if(current_working_copy, \"working_copy\"),
-    concat(
-      separate(\" \",
-        change_id.shortest(8).prefix() ++ \"​\" ++ 
change_id.shortest(8).rest(),
-        if(author.name(), author.name(), if(author.email(), 
author.email().local(), email_placeholder)),
-        commit_timestamp(self).format(\"%Y-%m-%d\"),
-        bookmarks,
-        tags,
-        working_copies,
-        if(git_head, label(\"git_head\", \"git_head()\")),
-        format_short_commit_id(commit_id),
-        if(conflict, label(\"conflict\", \"conflict\")),
-        if(config(\"ui.show-cryptographic-signatures\").as_boolean(),
-          format_short_cryptographic_signature(signature)),
-        if(empty, label(\"empty\", \"(empty)\")),
-        if(description,
-          description.first_line(),
-          label(if(empty, \"empty\"), description_placeholder),
-        ),
-      ) ++ \"\n\",
-    ),
-  )
-)
-")
-
 (defcustom vc-jj-global-switches '("--no-pager" "--color" "never")
   "Global switches to pass to any jj command."
   :type '(choice (const :tag "None" nil)
@@ -113,6 +113,15 @@ If nil, use the value of `vc-diff-switches'.  If t, use no 
switches."
                 (string :tag "Argument String")
                 (repeat :tag "Argument List" :value ("") string)))
 
+;;; Internal Utilities
+
+;; Note that 'JJ' should come before 'Git' in `vc-handled-backends',
+;; since by default a jj repository contains both '.jj' and '.git'
+;; directories.
+
+;;;###autoload
+(add-to-list 'vc-handled-backends 'JJ)
+
 (defun vc-jj--filename-to-fileset (filename)
   "Convert FILENAME to a JJ fileset expression.
 The fileset expression returned is relative to the JJ repository root.
@@ -123,6 +132,21 @@ When FILENAME is not inside a JJ repository, throw an 
error."
       (format "root:%S" (file-relative-name filename root))
     (error "File is not inside a JJ repository: %s" filename)))
 
+(defun vc-jj--set-up-process-buffer (buffer root command)
+  (with-current-buffer buffer
+    (vc-run-delayed
+      (vc-compilation-mode 'jj)
+      (setq-local compile-command (string-join command " "))
+      (setq-local compilation-directory root)
+      ;; Either set `compilation-buffer-name-function' locally to nil
+      ;; or use `compilation-arguments' to set `name-function'.
+      ;; See `compilation-buffer-name'.
+      (setq-local compilation-arguments
+                  (list compile-command nil
+                        (lambda (_name-of-mode) buffer)
+                        nil))))
+  (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
@@ -179,15 +203,123 @@ stderr and1 `vc-do-command' cannot separate output to 
stdout and stderr."
            nil
            (append global-switches flags filesets))))
 
-(defun vc-jj-clone (remote directory rev)
-  "Attempt to clone REMOTE repository into DIRECTORY at revision REV.
-On failure, return nil.  Upon success, return DIRECTORY."
-  (let ((successp (ignore-errors
-                    (vc-jj--command-dispatched nil 0 nil "git" "clone" 
"--colocate" remote directory))))
-    (when (and successp rev)
-      (let ((default-directory directory))
-        (vc-jj--command-dispatched nil 0 nil "new" rev "--quiet")))
-    (when successp directory)))
+(defun vc-jj--reload-log-buffers ()
+  (and vc-parent-buffer
+    (with-current-buffer vc-parent-buffer
+      (revert-buffer)))
+  (revert-buffer))
+
+(defun vc-jj-edit-change ()
+  (interactive)
+  (let ((rev (log-view-current-tag)))
+    (vc-jj-retrieve-tag nil rev nil)
+    (vc-jj--reload-log-buffers)))
+
+(defun vc-jj-abandon-change ()
+  (interactive)
+  ;; TODO: should probably ask for confirmation, although this would be
+  ;; different from the cli
+  (let ((rev (log-view-current-tag)))
+    (vc-jj--command-dispatched nil 0 nil "abandon" rev "--quiet")
+    (vc-jj--reload-log-buffers)))
+
+(defun vc-jj-new-change ()
+  (interactive)
+  (let ((rev (log-view-current-tag)))
+    (vc-jj--command-dispatched nil 0 nil "new" rev "--quiet")
+    (vc-jj--reload-log-buffers)))
+
+(defun vc-jj-bookmark-set ()
+  "Set the bookmark of revision at point.
+When called in a `vc-jj-log-view-mode' buffer, prompt for a bookmark to
+set at the revision at point.  If the bookmark already exists and would
+be moved backwards or sideways in the revision history, confirm with the
+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\""))
+           (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
+           ;; descendant of the revision that the bookmark is
+           ;; currently on, this means that the bookmark will be moved
+           ;; sideways or backwards
+           (backwards-move-p
+            (when (not new-bookmark-p)
+              (let* ((bookmark-rev
+                      (car (vc-jj--process-lines "show" bookmark "--no-patch"
+                                                 "-T" 
"self.change_id().shortest() ++ \"\n\"")))
+                     (bookmark-descendants
+                      (vc-jj--process-lines "log" "--no-graph" "-r" (concat 
bookmark-rev "..")
+                                            "-T" "self.change_id().shortest() 
++ \"\n\"")))
+                (not (member target-rev bookmark-descendants))))))
+      (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?"
+                                nil bookmark target-rev bookmark))
+          (user-error "Aborted moving bookmark %s to revision %s" bookmark 
target-rev)))
+      (vc-jj--command-dispatched nil 0 nil "bookmark" "set" bookmark "-r" 
target-rev
+                                 "--allow-backwards" "--quiet")
+      (revert-buffer))))
+
+(defun vc-jj-bookmark-rename ()
+  "Rename a bookmark pointing to the revision at point.
+When called in a `vc-jj-log-view-mode' buffer, rename the bookmark
+pointing to the revision at point.  If there are multiple bookmarks
+pointing to the revision, prompt the user to one of these bookmarks to
+rename."
+  (interactive nil vc-jj-log-view-mode)
+  (when (derived-mode-p 'vc-jj-log-view-mode)
+    (let* ((target-rev (log-view-current-tag))
+           (bookmarks-at-rev
+            (vc-jj--process-lines "bookmark" "list" "-r" target-rev
+                                  "-T" "if(!self.remote(), self.name() ++ 
\"\n\")"))
+           (bookmark-old
+            (if (= 1 (length bookmarks-at-rev))
+                (car bookmarks-at-rev)
+              (completing-read "Which bookmark to rename? " bookmarks-at-rev)))
+           (bookmark-new
+            (read-string (format-prompt "Rename %s to" nil bookmark-old))))
+      (vc-jj--command-dispatched nil 0 nil "bookmark" "rename" bookmark-old 
bookmark-new
+                                 "--quiet")
+      (revert-buffer))))
+
+(defun vc-jj-bookmark-delete ()
+  "Delete bookmark of the revision at point.
+When called in a `vc-jj-log-view-mode' buffer, delete the bookmark of
+the revision at point.  If there are multiple bookmarks attached to the
+revision, prompt the user to choose one or more of these bookmarks to
+delete."
+  (interactive nil vc-jj-log-view-mode)
+  (when (derived-mode-p 'vc-jj-log-view-mode)
+    (let* ((rev (log-view-current-tag))
+           (revision-bookmarks
+            (string-split
+             (vc-jj--command-parseable
+              "show" "-r" rev "--no-patch"
+              "-T" "self.local_bookmarks().map(|b| b.name()) ++ \"\n\"")
+             " " t "\n"))
+           (bookmarks
+            (if (< 1 (length revision-bookmarks))
+                (completing-read-multiple "Delete bookmarks: " 
revision-bookmarks nil t)
+              revision-bookmarks)))
+      (apply #'vc-jj--command-dispatched nil 0 nil "--quiet" "bookmark" 
"delete" bookmarks)
+      (revert-buffer))))
+
+
+
+;;; BACKEND PROPERTIES
+
+;;;; revision-granularity
+(defun vc-jj-revision-granularity () 'repository)
+
+;;;; update-on-retrieve-tag
+(defun vc-jj-update-on-retrieve-tag () nil)
+
+;;; STATE-QUERYING FUNCTIONS
+
+;;;; registered
 
 ;;;###autoload (defun vc-jj-registered (file)
 ;;;###autoload   "Return non-nil if FILE is registered with jj."
@@ -200,7 +332,10 @@ On failure, return nil.  Upon success, return DIRECTORY."
   "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" "list" "--"
+                          (vc-jj--filename-to-fileset file))))
+
+;;;; state
 
 (defun vc-jj-state (file)
   "JJ implementation of `vc-state' for FILE.
@@ -251,6 +386,8 @@ there is no such state in jj since jj automatically 
registers new files."
             (warn "VC state of %s is not recognized, assuming up-to-date" file)
             'up-to-date)))))))
 
+;;;; dir-status-file
+
 (defun vc-jj-dir-status-files (dir _files update-function)
   "Calculate a list of (FILE STATE EXTRA) entries for DIR.
 Return the result of applying UPDATE-FUNCTION to that list.
@@ -315,6 +452,8 @@ For a description of the states relevant to jj, see 
`vc-jj-state'."
                    (mapcar (lambda (entry) (list entry 'up-to-date)) 
up-to-date-files))))
       (funcall update-function result nil))))
 
+;;;; dir-extra-headers
+
 (defun vc-jj-dir-extra-headers (dir)
   "Return extra headers for `vc-dir' when executed inside DIR.
 
@@ -399,6 +538,12 @@ parents.map(|c| concat(
            parent-keys))
          "\n")))))
 
+;;;; dir-printer
+
+;;;; status-fileinfo-extra
+
+;;;; working-revision
+
 (defun vc-jj-working-revision (file)
   "Return the current change id of the repository containing FILE."
   (when-let* ((default-directory (vc-jj-root file)))
@@ -409,6 +554,12 @@ parents.map(|c| concat(
                                      "-r" "@"
                                      "-T" "change_id ++ \"\\n\"")))))
 
+;;;; checkout-model
+
+(defun vc-jj-checkout-model (_files) 'implicit)
+
+;;;; mode-line-string
+
 (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))
@@ -426,12 +577,18 @@ parents.map(|c| concat(
                                    "\nCurrent change: " long-rev
                                    " (" description ")"))))
 
+;;; STATE-CHANGING FUNCTIONS
+
+;;;; create-repo
+
 (defun vc-jj-create-repo ()
   "Create an empty jj repository in the current directory."
   (if current-prefix-arg
       (call-process vc-jj-program nil nil nil "git" "init" "--colocate")
     (call-process vc-jj-program nil nil nil "git" "init")))
 
+;;;; register
+
 (defun vc-jj-register (files &optional _comment)
   "Register FILES into the jj version-control system."
   ;; This is usually a no-op since jj auto-registers all files, so we
@@ -441,16 +598,15 @@ parents.map(|c| concat(
   ;; such as the .gitignore file.
   (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-dispatched nil 0 nil "status")))
+;;;; responsible-p
 
-(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-dispatched nil 0 nil "status"))
+(defalias 'vc-jj-responsible-p #'vc-jj-root)
+
+;;;; receive-file
+
+;;;; unregister
+
+;;;; checkin
 
 (defun vc-jj-checkin (files comment &optional _rev)
   "Run \"jj commit\" with supplied FILES and COMMENT."
@@ -459,6 +615,10 @@ parents.map(|c| concat(
                        (list "commit" "-m" comment))))
     (apply #'vc-jj--command-dispatched nil 0 files args)))
 
+;;;; checkin-patch
+
+;;;; find-revision
+
 (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))))
@@ -467,16 +627,7 @@ parents.map(|c| concat(
       (insert revision)))
   buffer)
 
-(defun vc-jj-create-tag (_dir name branchp)
-  "Attach tag named NAME to the current revision.
-When BRANCHP is non-nil, a bookmark named NAME is created at the current
-revision.
-
-Since jujutsu does not support tagging revisions, a nil value of BRANCHP
-has no effect."
-  (if branchp
-      (vc-jj--command-dispatched nil 0 nil "bookmark" "create" name "--quiet")
-    (user-error "Setting tags is not supported by jujutsu")))
+;;;; checkout
 
 (defun vc-jj-checkout (file &optional rev)
   "Restore the contents of FILE to be the same as in change REV.
@@ -486,54 +637,158 @@ If REV is not specified, revert the file as with 
`vc-jj-revert'."
   (let ((args (append (and rev (list "--from" rev)))))
     (apply #'vc-jj--command-dispatched nil 0 file "restore" args)))
 
+;;;; revert
+
 (defun vc-jj-revert (file &optional _contents-done)
   "Restore FILE to the state from its parent(s), via \"jj 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."
-  ;; FIXME: limit can be a revision string, in which case we should
-  ;; print revisions between start-revision and limit
-  (vc-setup-buffer buffer)
-  (let ((inhibit-read-only t)
-        (args (append
-               (and limit
-                    (list "-n" (number-to-string limit)))
-               (if start-revision
-                 (list "-r" (concat "::" start-revision))
-                 (list "-r" "::"))
-               (list "-T" vc-jj--log-default-template "--")
-               (unless (string-equal (vc-jj-root (car files)) (car files))
-                 files))))
-    (with-current-buffer buffer
-      (apply #'vc-jj--command-dispatched buffer
-        'async nil "log" args))))
+;;;; merge-file
 
-(defun vc-jj-show-log-entry (revision)
-  "Move to the log entry for REVISION."
-  ;; TODO: check that this works for all forms of log output, or at
-  ;; least the predefined ones
-  (goto-char (point-min))
-  (when (search-forward-regexp
-         ;; TODO: reformulate using `rx'
-         (concat "^[^|]\\s-+\\(" (regexp-quote revision) "\\)\\s-+")
-         nil t)
-    (goto-char (match-beginning 1))))
+;;;; merge-branch
 
-;; (defun vc-jj-log-outgoing (buffer remote-location)
-;;   ;; TODO
-;;   )
-;; (defun vc-jj-log-incoming (buffer remote-location)
-;;   ;; TODO
-;;   )
+;;;; merge-news
 
-(defvar vc-jj--logline-re
-  (rx
-    line-start
-    ;; graph
-    (+? nonl)
-    " "
-    ;; change-id
+;;;; pull
+
+(defvar vc-jj-pull-history nil
+  "History variable for `vc-jj-pull'.")
+
+(defun vc-jj-pull (prompt)
+  "Pull changes from an upstream repository, invoked via \\[vc-update].
+Normally, this runs \"jj git fetch\".  If PROMPT is non-nil, prompt for
+the jj command to run."
+  (let* ((command (if prompt
+                      (split-string-shell-command
+                      (read-shell-command
+                        (format "jj git fetch command: ")
+                        (concat vc-jj-program " git fetch")
+                        'vc-jj-pull-history))
+                    `(,vc-jj-program "git" "fetch")))
+         (jj-program (car command))
+         (args (cdr command))
+         (root (vc-jj-root default-directory))
+         (buffer (format "*vc-jj : %s*" (expand-file-name root))))
+    (apply #'vc-do-async-command buffer root jj-program args)
+    (vc-jj--set-up-process-buffer buffer root command)))
+
+;;;; push
+
+(defvar vc-jj-push-history nil
+  "History variable for `vc-jj-push'.")
+
+(defun vc-jj-push (prompt)
+  "Push changes to an upstream repository, invoked via \\[vc-push].
+Normally, this runs \"jj git push\".  If PROMPT is non-nil, prompt for
+the command to run, e.g., the semi-standard \"jj git push -c @-\"."
+  (let* ((command (if prompt
+                      (split-string-shell-command
+                      (read-shell-command
+                        (format "jj git push command: ")
+                        (concat vc-jj-program " git push")
+                        'vc-jj-push-history))
+                    `(,vc-jj-program "git" "push")))
+         (jj-program (car command))
+         (args (cdr command))
+         (root (vc-jj-root default-directory))
+         (buffer (format "*vc-jj : %s*" (expand-file-name root))))
+    (apply #'vc-do-async-command buffer root jj-program args)
+    (vc-jj--set-up-process-buffer buffer root command)))
+
+;;;; steal-lock
+
+;;;; get-change-comment
+
+(defun vc-jj-get-change-comment (_files rev)
+  (vc-jj--command-parseable "log" "--no-graph" "-n" "1"
+                            "-r" rev "-T" "description"))
+
+;;;; modify-change-comment
+
+;; TODO: protect immutable changes
+(defun vc-jj-modify-change-comment (_files rev comment)
+  (let ((comment (car (log-edit-extract-headers () comment))))
+    (vc-jj--command-dispatched nil 0 nil "desc" rev "-m" comment "--quiet")))
+
+;;;; mark-resolved
+
+;;;; find-admin-dir
+
+;;; HISTORY FUNCTIONS
+
+;;;; print-log
+
+(defvar vc-jj--log-default-template
+  "
+if(root,
+  format_root_commit(self),
+  label(if(current_working_copy, \"working_copy\"),
+    concat(
+      separate(\" \",
+        change_id.shortest(8).prefix() ++ \"​\" ++ 
change_id.shortest(8).rest(),
+        if(author.name(), author.name(), if(author.email(), 
author.email().local(), email_placeholder)),
+        commit_timestamp(self).format(\"%Y-%m-%d\"),
+        bookmarks,
+        tags,
+        working_copies,
+        if(git_head, label(\"git_head\", \"git_head()\")),
+        format_short_commit_id(commit_id),
+        if(conflict, label(\"conflict\", \"conflict\")),
+        if(config(\"ui.show-cryptographic-signatures\").as_boolean(),
+          format_short_cryptographic_signature(signature)),
+        if(empty, label(\"empty\", \"(empty)\")),
+        if(description,
+          description.first_line(),
+          label(if(empty, \"empty\"), description_placeholder),
+        ),
+      ) ++ \"\n\",
+    ),
+  )
+)
+")
+
+(defun vc-jj-print-log (files buffer &optional _shortlog start-revision limit)
+  "Print commit log associated with FILES into specified BUFFER."
+  ;; FIXME: limit can be a revision string, in which case we should
+  ;; print revisions between start-revision and limit
+  (vc-setup-buffer buffer)
+  (let ((inhibit-read-only t)
+        (args (append
+               (and limit
+                    (list "-n" (number-to-string limit)))
+               (if start-revision
+                 (list "-r" (concat "::" start-revision))
+                 (list "-r" "::"))
+               (list "-T" vc-jj--log-default-template "--")
+               (unless (string-equal (vc-jj-root (car files)) (car files))
+                 files))))
+    (with-current-buffer buffer
+      (apply #'vc-jj--command-dispatched buffer
+        'async nil "log" args))))
+
+;;;; log-outgoing
+
+;; (defun vc-jj-log-outgoing (buffer remote-location)
+;;   ;; TODO
+;;   )
+
+;;;; log-incoming
+
+;; (defun vc-jj-log-incoming (buffer remote-location)
+;;   ;; TODO
+;;   )
+
+;;;; log-search
+
+;;;; log-view-mode
+
+(defvar vc-jj--logline-re
+  (rx
+    line-start
+    ;; graph
+    (+? nonl)
+    " "
+    ;; change-id
     (group (+ (any "K-Zk-z")))
     space
     (group (+ (any "K-Zk-z")))
@@ -562,7 +817,23 @@ If REV is not specified, revert the file as with 
`vc-jj-revert'."
                 "")))
     ;; regular description
     (* nonl)
-    line-end))
+    line-end)
+  "Regular expression matching one line in the outpout of 'jj log'.")
+
+(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)))
 
 (define-derived-mode vc-jj-log-view-mode log-view-mode "JJ-Log-View"
   (require 'add-log) ;; We need the faces add-log.
@@ -573,205 +844,43 @@ If REV is not specified, revert the file as with 
`vc-jj-revert'."
   ;; Allow expanding short log entries.
   (setq truncate-lines t)
   (setq-local log-view-expanded-log-entry-function
-    'vc-jj-expanded-log-entry)
+    'vc-jj--expanded-log-entry)
   (setq-local log-view-font-lock-keywords
     `((,vc-jj--logline-re
         (1 'log-view-message)
         (2 'change-log-list)
         (3 'change-log-name)
         (4 'change-log-date)
-        (5 'change-log-file)
-        (6 'change-log-list)
-        (7 'change-log-function)
-        (8 'change-log-function))))
-
-  (keymap-set vc-jj-log-view-mode-map "r" #'vc-jj-edit-change)
-  (keymap-set vc-jj-log-view-mode-map "x" #'vc-jj-abandon-change)
-  (keymap-set vc-jj-log-view-mode-map "i" #'vc-jj-new-change)
-  (keymap-set vc-jj-log-view-mode-map "b s" #'vc-jj-bookmark-set)
-  (keymap-set vc-jj-log-view-mode-map "b r" #'vc-jj-bookmark-rename)
-  (keymap-set vc-jj-log-view-mode-map "b D" #'vc-jj-bookmark-delete))
-
-
-(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)))
-
-(defun vc-jj-previous-revision (file rev)
-  "JJ-specific version of `vc-previous-revision'."
-  (if file
-      (vc-jj--command-parseable "log" "--no-graph" "--limit" "1"
-                                "-r" (concat "ancestors(" rev ")")
-                                "-T" "change_id"
-                                "--" (vc-jj--filename-to-fileset file))
-    ;; 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"
-                              "-r" (concat "first_parent(" rev ")")
-                              "-T" "change_id")))
-
-(defun vc-jj-next-revision (file rev)
-  "JJ-specific version of `vc-next-revision'."
-  (if file
-      (vc-jj--command-parseable "log" "--no-graph" "--limit" "1"
-                                "-r" (concat "descendants(" rev ")")
-                                 "-T" "change_id"
-                                 "--" (vc-jj--filename-to-fileset file))
-    ;; 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\""))))
-
-(defun vc-jj-get-change-comment (_files rev)
-  (vc-jj--command-parseable "log" "--no-graph" "-n" "1"
-                            "-r" rev "-T" "description"))
-
-;; TODO: protect immutable changes
-(defun vc-jj-modify-change-comment (_files rev comment)
-  (let ((comment (car (log-edit-extract-headers () comment))))
-    (vc-jj--command-dispatched nil 0 nil "desc" rev "-m" comment "--quiet")))
-
-(defun vc-jj--reload-log-buffers ()
-  (and vc-parent-buffer
-    (with-current-buffer vc-parent-buffer
-      (revert-buffer)))
-  (revert-buffer))
-
-(defun vc-jj-edit-change ()
-  (interactive)
-  (let ((rev (log-view-current-tag)))
-    (vc-jj-retrieve-tag nil rev nil)
-    (vc-jj--reload-log-buffers)))
-
-(defun vc-jj-abandon-change ()
-  (interactive)
-  ;; TODO: should probably ask for confirmation, although this would be
-  ;; different from the cli
-  (let ((rev (log-view-current-tag)))
-    (vc-jj--command-dispatched nil 0 nil "abandon" rev "--quiet")
-    (vc-jj--reload-log-buffers)))
-
-(defun vc-jj-new-change ()
-  (interactive)
-  (let ((rev (log-view-current-tag)))
-    (vc-jj--command-dispatched nil 0 nil "new" rev "--quiet")
-    (vc-jj--reload-log-buffers)))
-
-(defun vc-jj-bookmark-set ()
-  "Set the bookmark of revision at point.
-When called in a `vc-jj-log-view-mode' buffer, prompt for a bookmark to
-set at the revision at point.  If the bookmark already exists and would
-be moved backwards or sideways in the revision history, confirm with the
-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\""))
-           (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
-           ;; descendant of the revision that the bookmark is
-           ;; currently on, this means that the bookmark will be moved
-           ;; sideways or backwards
-           (backwards-move-p
-            (when (not new-bookmark-p)
-              (let* ((bookmark-rev
-                      (car (vc-jj--process-lines "show" bookmark "--no-patch"
-                                                 "-T" 
"self.change_id().shortest() ++ \"\n\"")))
-                     (bookmark-descendants
-                      (vc-jj--process-lines "log" "--no-graph" "-r" (concat 
bookmark-rev "..")
-                                            "-T" "self.change_id().shortest() 
++ \"\n\"")))
-                (not (member target-rev bookmark-descendants))))))
-      (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?"
-                                nil bookmark target-rev bookmark))
-          (user-error "Aborted moving bookmark %s to revision %s" bookmark 
target-rev)))
-      (vc-jj--command-dispatched nil 0 nil "bookmark" "set" bookmark "-r" 
target-rev
-                                 "--allow-backwards" "--quiet")
-      (revert-buffer))))
-
-(defun vc-jj-bookmark-rename ()
-  "Rename a bookmark pointing to the revision at point.
-When called in a `vc-jj-log-view-mode' buffer, rename the bookmark
-pointing to the revision at point.  If there are multiple bookmarks
-pointing to the revision, prompt the user to one of these bookmarks to
-rename."
-  (interactive nil vc-jj-log-view-mode)
-  (when (derived-mode-p 'vc-jj-log-view-mode)
-    (let* ((target-rev (log-view-current-tag))
-           (bookmarks-at-rev
-            (vc-jj--process-lines "bookmark" "list" "-r" target-rev
-                                  "-T" "if(!self.remote(), self.name() ++ 
\"\n\")"))
-           (bookmark-old
-            (if (= 1 (length bookmarks-at-rev))
-                (car bookmarks-at-rev)
-              (completing-read "Which bookmark to rename? " bookmarks-at-rev)))
-           (bookmark-new
-            (read-string (format-prompt "Rename %s to" nil bookmark-old))))
-      (vc-jj--command-dispatched nil 0 nil "bookmark" "rename" bookmark-old 
bookmark-new
-                                 "--quiet")
-      (revert-buffer))))
-
-(defun vc-jj-bookmark-delete ()
-  "Delete bookmark of the revision at point.
-When called in a `vc-jj-log-view-mode' buffer, delete the bookmark of
-the revision at point.  If there are multiple bookmarks attached to the
-revision, prompt the user to choose one or more of these bookmarks to
-delete."
-  (interactive nil vc-jj-log-view-mode)
-  (when (derived-mode-p 'vc-jj-log-view-mode)
-    (let* ((rev (log-view-current-tag))
-           (revision-bookmarks
-            (string-split
-             (vc-jj--command-parseable
-              "show" "-r" rev "--no-patch"
-              "-T" "self.local_bookmarks().map(|b| b.name()) ++ \"\n\"")
-             " " t "\n"))
-           (bookmarks
-            (if (< 1 (length revision-bookmarks))
-                (completing-read-multiple "Delete bookmarks: " 
revision-bookmarks nil t)
-              revision-bookmarks)))
-      (apply #'vc-jj--command-dispatched nil 0 nil "--quiet" "bookmark" 
"delete" bookmarks)
-      (revert-buffer))))
+        (5 'change-log-file)
+        (6 'change-log-list)
+        (7 'change-log-function)
+        (8 'change-log-function))))
 
-(defun vc-jj-root (file)
-  "Return the root of the repository containing FILE.
-Return NIL if FILE is not in a jj repository."
-  (vc-find-root file ".jj"))
+  (keymap-set vc-jj-log-view-mode-map "r" #'vc-jj-edit-change)
+  (keymap-set vc-jj-log-view-mode-map "x" #'vc-jj-abandon-change)
+  (keymap-set vc-jj-log-view-mode-map "i" #'vc-jj-new-change)
+  (keymap-set vc-jj-log-view-mode-map "b s" #'vc-jj-bookmark-set)
+  (keymap-set vc-jj-log-view-mode-map "b r" #'vc-jj-bookmark-rename)
+  (keymap-set vc-jj-log-view-mode-map "b D" #'vc-jj-bookmark-delete))
 
-(defalias 'vc-jj-responsible-p #'vc-jj-root)
+;;;; show-log-entry
 
-(defun vc-jj-ignore (file &optional directory remove)
-  "Ignore FILE under DIRECTORY.
+(defun vc-jj-show-log-entry (revision)
+  "Move to the log entry for REVISION."
+  ;; TODO: check that this works for all forms of log output, or at
+  ;; least the predefined ones
+  (goto-char (point-min))
+  (when (search-forward-regexp
+         ;; TODO: reformulate using `rx'
+         (concat "^[^|]\\s-+\\(" (regexp-quote revision) "\\)\\s-+")
+         nil t)
+    (goto-char (match-beginning 1))))
 
-FILE is a wildcard specification relative to DIRECTORY.
-DIRECTORY defaults to `default-directory'.
+;;;; comment-history
 
-If REMOVE is non-nil, remove FILE from ignored files instead.
+;;;; update-changelog
 
-For jj, modify `.gitignore' and call `jj untrack' or `jj track'."
-  (vc-default-ignore 'Git file directory remove)
-  (let ((default-directory
-         (if directory (file-name-as-directory directory)
-           default-directory)))
-    (vc-jj--command-dispatched nil 0 file "file" (if remove "track" "untrack") 
"--")))
+;;;; diff
 
 (defun vc-jj-diff (files &optional rev1 rev2 buffer _async)
   "Display diffs for FILES between revisions REV1 and REV2."
@@ -794,6 +903,36 @@ For jj, modify `.gitignore' and call `jj untrack' or `jj 
track'."
         1
       0)))
 
+;;;; revision-completion-table
+
+(defun vc-jj--revision-annotation-function (elem)
+  "Calculate propertized change description from ELEM.
+ELEM can be of the form (change-id . description), as produced in
+`vc-jj-revision-completion-table', in which case we return the second
+element, or can be a change id, in which case we query for the first
+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()"))))
+    (format " %s" (propertize description 'face 'completions-annotations))))
+
+(defun vc-jj-revision-completion-table (files)
+  "Return a completion table for existing revisions of FILES."
+  (let* ((revisions
+          (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))))
+    (lambda (string pred action)
+      (if (eq action 'metadata)
+          `(metadata . ((display-sort-function . ,#'identity)
+                        (annotation-function . 
,#'vc-jj--revision-annotation-function)))
+        (complete-with-action action revisions string pred)))))
+
+;;;; annotate-command
+
 (defun vc-jj-annotate-command (file buf &optional rev)
   "Fill BUF with per-line change history of FILE at REV."
   (let ((rev (or rev "@"))
@@ -805,6 +944,8 @@ For jj, modify `.gitignore' and call `jj untrack' or `jj 
track'."
            (append (vc-switches 'jj 'annotate)
                    (list "-r" rev file)))))
 
+;;;; annotate-time
+
 (defconst vc-jj--annotation-line-prefix-re
   (rx bol
       (group (+ (any "a-z")))           ; change id
@@ -823,8 +964,6 @@ For jj, modify `.gitignore' and call `jj untrack' or `jj 
track'."
 The regex matches each line's change information and captures
 four groups: change id, author, datetime, line number.")
 
-(declare-function vc-annotate-convert-time "vc-annotate" (&optional time))
-
 (defun vc-jj-annotate-time ()
   "Return the time for the annotated line."
   (and-let*
@@ -835,6 +974,10 @@ four groups: change id, author, datetime, line number.")
     (vc-annotate-convert-time
      (encode-time (decoded-time-set-defaults decoded)))))
 
+;;;; annotate-current-time
+
+;;;; annotate-extract-revision-at-line
+
 (defun vc-jj-annotate-extract-revision-at-line ()
   "Return the revision (change id) for the annotated line."
   (save-excursion
@@ -842,31 +985,38 @@ four groups: change id, author, datetime, line number.")
     (when (looking-at vc-jj--annotation-line-prefix-re)
       (match-string-no-properties 1))))
 
-(defun vc-jj--revision-annotation-function (elem)
-  "Calculate propertized change description from ELEM.
-ELEM can be of the form (change-id . description), as produced in
-`vc-jj-revision-completion-table', in which case we return the second
-element, or can be a change id, in which case we query for the first
-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()"))))
-    (format " %s" (propertize description 'face 'completions-annotations))))
+;;;; region-history
 
-(defun vc-jj-revision-completion-table (files)
-  "Return a completion table for existing revisions of FILES."
-  (let* ((revisions
-          (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))))
-    (lambda (string pred action)
-      (if (eq action 'metadata)
-          `(metadata . ((display-sort-function . ,#'identity)
-                        (annotation-function . 
,#'vc-jj--revision-annotation-function)))
-        (complete-with-action action revisions string pred)))))
+(defun vc-jj-region-history (file buffer lfrom lto)
+  ;; Support for vc-region-history via the git version which works
+  ;; fine at least when co-located with git.
+  (vc-git-region-history file buffer lfrom lto))
+
+;;;; region-history-mode
+
+(define-derived-mode vc-jj-region-history-mode vc-git-region-history-mode
+  "JJ/Git-Region-History")
+
+;;;; mergebase
+
+;;;; last-change
+
+;;; TAG/BRANCH SYSTEM
+
+;;;; create-tag
+
+(defun vc-jj-create-tag (_dir name branchp)
+  "Attach tag named NAME to the current revision.
+When BRANCHP is non-nil, a bookmark named NAME is created at the current
+revision.
+
+Since jujutsu does not support tagging revisions, a nil value of BRANCHP
+has no effect."
+  (if branchp
+      (vc-jj--command-dispatched nil 0 nil "bookmark" "create" name "--quiet")
+    (user-error "Setting tags is not supported by jujutsu")))
+
+;;;; retrieve-tag
 
 (defun vc-jj-retrieve-tag (_dir rev _update)
   "Call jj edit on REV inside DIR.
@@ -875,62 +1025,77 @@ REV is the change ID of a jj revision.
 _DIR and _UPDATE are as described in the vc.el specification."
   (vc-jj--command-dispatched nil 0 nil "edit" rev "--quiet"))
 
-(defvar vc-jj-pull-history nil
-  "History variable for `vc-jj-pull'.")
+;;; MISCELLANEOUS
 
-(defun vc-jj-pull (prompt)
-  "Pull changes from an upstream repository, invoked via \\[vc-update].
-Normally, this runs \"jj git fetch\".  If PROMPT is non-nil, prompt for
-the jj command to run."
-  (let* ((command (if prompt
-                      (split-string-shell-command
-                      (read-shell-command
-                        (format "jj git fetch command: ")
-                        (concat vc-jj-program " git fetch")
-                        'vc-jj-pull-history))
-                    `(,vc-jj-program "git" "fetch")))
-         (jj-program (car command))
-         (args (cdr command))
-         (root (vc-jj-root default-directory))
-         (buffer (format "*vc-jj : %s*" (expand-file-name root))))
-    (apply #'vc-do-async-command buffer root jj-program args)
-    (vc-jj--set-up-process-buffer buffer root command)))
+;;;; make-version-backups-p
 
-(defvar vc-jj-push-history nil
-  "History variable for `vc-jj-push'.")
+;;;; root
 
-(defun vc-jj-push (prompt)
-  "Push changes to an upstream repository, invoked via \\[vc-push].
-Normally, this runs \"jj git push\".  If PROMPT is non-nil, prompt for
-the command to run, e.g., the semi-standard \"jj git push -c @-\"."
-  (let* ((command (if prompt
-                      (split-string-shell-command
-                      (read-shell-command
-                        (format "jj git push command: ")
-                        (concat vc-jj-program " git push")
-                        'vc-jj-push-history))
-                    `(,vc-jj-program "git" "push")))
-         (jj-program (car command))
-         (args (cdr command))
-         (root (vc-jj-root default-directory))
-         (buffer (format "*vc-jj : %s*" (expand-file-name root))))
-    (apply #'vc-do-async-command buffer root jj-program args)
-    (vc-jj--set-up-process-buffer buffer root command)))
+(defun vc-jj-root (file)
+  "Return the root of the repository containing FILE.
+Return NIL if FILE is not in a jj repository."
+  (vc-find-root file ".jj"))
 
-(defun vc-jj--set-up-process-buffer (buffer root command)
-  (with-current-buffer buffer
-    (vc-run-delayed
-      (vc-compilation-mode 'jj)
-      (setq-local compile-command (string-join command " "))
-      (setq-local compilation-directory root)
-      ;; Either set `compilation-buffer-name-function' locally to nil
-      ;; or use `compilation-arguments' to set `name-function'.
-      ;; See `compilation-buffer-name'.
-      (setq-local compilation-arguments
-                  (list compile-command nil
-                        (lambda (_name-of-mode) buffer)
-                        nil))))
-  (vc-set-async-update buffer))
+;;;; ignore
+
+(defun vc-jj-ignore (file &optional directory remove)
+  "Ignore FILE under DIRECTORY.
+
+FILE is a wildcard specification relative to DIRECTORY.
+DIRECTORY defaults to `default-directory'.
+
+If REMOVE is non-nil, remove FILE from ignored files instead.
+
+For jj, modify `.gitignore' and call `jj untrack' or `jj track'."
+  (vc-default-ignore 'Git file directory remove)
+  (let ((default-directory
+         (if directory (file-name-as-directory directory)
+           default-directory)))
+    (vc-jj--command-dispatched nil 0 file "file" (if remove "track" "untrack") 
"--")))
+
+;;;; ignore-completion-table
+
+;;;; find-ignore-file
+
+;;;; previous-revision
+
+(defun vc-jj-previous-revision (file rev)
+  "JJ-specific version of `vc-previous-revision'."
+  (if file
+      (vc-jj--command-parseable "log" "--no-graph" "--limit" "1"
+                                "-r" (concat "ancestors(" rev ")")
+                                "-T" "change_id"
+                                "--" (vc-jj--filename-to-fileset file))
+    ;; 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"
+                              "-r" (concat "first_parent(" rev ")")
+                              "-T" "change_id")))
+
+;;;; file-name-changes
+
+;;;; next-revision
+
+(defun vc-jj-next-revision (file rev)
+  "JJ-specific version of `vc-next-revision'."
+  (if file
+      (vc-jj--command-parseable "log" "--no-graph" "--limit" "1"
+                                "-r" (concat "descendants(" rev ")")
+                                 "-T" "change_id"
+                                 "--" (vc-jj--filename-to-fileset file))
+    ;; 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\""))))
+
+;;;; log-edit-mode
+
+;; Set up `log-edit-mode' to handle jj description files.
 
 ;;;###autoload
 (add-to-list 'auto-mode-alist '("\\.jjdescription\\'" . log-edit-mode))
@@ -947,13 +1112,46 @@ the command to run, e.g., the semi-standard \"jj git 
push -c @-\"."
                                     (save-buffer)
                                     (kill-buffer)))))
 
-;; Support for vc-region-history via the git version which works fine
-;; at least when co-located with git.
-(defun vc-jj-region-history (file buffer lfrom lto)
-  (vc-git-region-history file buffer lfrom lto))
+;;;; check-headers
 
-(define-derived-mode vc-jj-region-history-mode vc-git-region-history-mode
-  "JJ/Git-Region-History")
+;;;; delete-file
+
+(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-dispatched nil 0 nil "status")))
+
+;;;; rename-file
+
+(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-dispatched nil 0 nil "status"))
+
+;;;; find-file-hook
+
+;;;; extra-menu
+
+;;;; extra-dir-menu
+
+;;;; conflicted-files
+
+;;;; repository-url
+
+;;;; prepare-patch
+
+;;;; clone
+
+(defun vc-jj-clone (remote directory rev)
+  "Attempt to clone REMOTE repository into DIRECTORY at revision REV.
+On failure, return nil.  Upon success, return DIRECTORY."
+  (let ((successp (ignore-errors
+                    (vc-jj--command-dispatched nil 0 nil "git" "clone" 
"--colocate" remote directory))))
+    (when (and successp rev)
+      (let ((default-directory directory))
+        (vc-jj--command-dispatched nil 0 nil "new" rev "--quiet")))
+    (when successp directory)))
 
 (provide 'vc-jj)
 ;;; vc-jj.el ends here


Reply via email to