[svn.haxx.se] · SVN Dev · SVN Users · SVN Org · TSVN Dev · TSVN Users · Subclipse Dev · Subclipse Users · this month's index

Emacs VC Subversion back end: release early, release often

From: Jim Blandy <jimb_at_red-bean.com>
Date: 2002-07-15 06:51:25 CEST

This implements all the "required" VC backend functions, and can do
updates, commits, logs and diffs. It's almost certainly riddled with
bugs, and I'd love to hear about each of them.

One big problem here is that VC only lets you commit a file at a time.
For CVS, that's not such a big deal, as long as it's easy to re-use
old commit messages (which it is). For Subversion, it's a very big
deal: you can only make transactions that affect a single file at a
time, which sort of misses the whole point. It basically means VC is
useless for anything but diff, log, and a meaningful mode line. Of
course, you can still do commits at the command line; that's what I do
with CVS.

I think VC's maintainer would be happy to see this restriction lifted.
But I want to first produce code that Subversion users can use with an
already released Emacs distribution. So for now I'm working within
the limitations of the released VC; once we've got something
functional, then we can start writing VC patches.

;;;; vc-svn.el --- a VC backend for Subversion
;;;; Jim Blandy <jimb@red-bean.com> --- July 2002

;;; Writing this back end has shown up some problems in VC: bugs,
;;; shortcomings in the back end interface, and so on. But I want to
;;; first produce code that Subversion users can use with an already
;;; released Emacs distribution.
;;;
;;; So for now we're working within the limitations of the released
;;; VC; once we've got something functional, then we can start writing
;;; VC patches.

;;; To do here:
;;; Provide more of the optional VC backend functions:
;;; - dir-state
;;; - mode-line-string, to show newly added files, modified props?
;;; see vc-cvs-mode-line-string
;;;
;;; VC passes the vc-svn-register function a COMMENT argument, which
;;; is like the file description in CVS and RCS. Could we store the
;;; COMMENT as a Subversion property? Would that show up in fancy DAV
;;; web folder displays, or would it just languish in obscurity, the
;;; way CVS and RCS descriptions do?

;;; To do in VC:
;;;
;;; Make sure vc's documentation for `workfile-unchanged-p' default
;;; function mentions that it must not run asynchronously, and the
;;; symptoms if it does.
;;;
;;; Fix logic for finding log entries.
;;;
;;; Allow historical diff to choose an appropriate default previous
;;; revision number. I think this entails moving vc-previous-revision
;;; et al into the back end.
;;;
;;; Should vc-BACKEND-checkout really have to set the workfile version
;;; itself?

(add-to-list 'vc-handled-backends 'SVN)

(defcustom vc-svn-program-name "svn"
  "*Name of Subversion client program, for use by Emacs's VC package."
  :type 'string
  :group 'vc
  :version "21.2.90.2")

(defcustom vc-svn-diff-switches nil
  "*A string or list of strings specifying extra switches for `svn diff' under VC."
  :type '(repeat string)
  :group 'vc
  :version "21.2.90.2")

(defun vc-svn-registered (file)
  "Return true if FILE is registered under Subversion."
  ;; First, a quick false positive test: is there a `.svn/entries' file?
  (and (file-exists-p (expand-file-name ".svn/entries"
                                        (file-name-directory file)))
       (not (null (vc-svn-run-status file)))))

(put 'vc-svn-with-output-buffer 'lisp-indent-function 0)
(defmacro vc-svn-with-output-buffer (&rest body)
  "Save excursion, switch to buffer ` *Subversion Output*', and erase it."
  `(save-excursion
     ;; Let's not delete this buffer when we're done --- leave
     ;; it around for debugging.
     (set-buffer (get-buffer-create " *Subversion Output*"))
     (erase-buffer)
     ,@body))

(defun vc-svn-pop-up-error (&rest args)
  "Pop up the Subversion output buffer, and raise an error with ARGS."
  (pop-to-buffer " *Subversion Output*")
  (goto-char (point-min))
  (shrink-window-if-larger-than-buffer)
  (apply 'error args))

(defun vc-svn-run-status (file &optional update)
  "Run `svn status -v' on FILE, and return the result.
If optional arg UPDATE is true, pass the `-u' flag to check against
the repository, across the network.
See `vc-svn-parse-status' for a description of the result."
  (vc-svn-with-output-buffer

    ;; We should call vc-do-command here, but Subversion exits with an
    ;; error status if FILE isn't under its control, and we want to
    ;; return that as nil, not display it to the user. We can tell
    ;; vc-do-command to
    
    (let ((status (apply 'call-process vc-svn-program-name nil t nil
                         (append '("status" "-v")
                                 (if update '("-u") '())
                                 (list file)))))
      (goto-char (point-min))
      (if (not (equal 0 status)) ; not zerop; status can be a string
          ;; If you ask for the status of a file that isn't even in a
          ;; Subversion-controlled directory, then Subversion exits with
          ;; this error.
          (if (or (looking-at "\\(.*\n\\)*.*is not a working copy directory")
                  (looking-at "\\(.*\n\\)*.*is not a versioned resource"))
              nil
            ;; Other errors we should actually report to the user.
            (vc-svn-pop-up-error
             "Error running Subversion to check status of `%s'"
             (file-name-nondirectory file)))

        ;; Otherwise, we've got valid status output in the buffer, so
        ;; just parse that.
        (vc-svn-parse-status)))))

(defun vc-svn-parse-status ()
  "Parse the output from `svn status -v' at point.
We return nil for a file not under Subversion's control,
or (STATE LOCAL CHANGED) for files that are, where:
STATE is the file's VC state (see the documentation for `vc-state'),
LOCAL is the base revision in the working copy, and
CHANGED is the last revision in which it was changed.
Both LOCAL and CHANGED are strings, not numbers.
If we passed `svn status' the `-u' flag, then CHANGED could be a later
revision than LOCAL.
If the file is newly added, LOCAL is \"0\" and CHANGED is nil."
  (let ((state (vc-svn-parse-state-only)))
    (cond
     ((not state) nil)
     ;; A newly added file has no revision.
     ((looking-at "\\S-+\\s-+\\(\\*\\s-+\\)?\\0\\s-+\\?")
      (list state 0 nil))
     ((looking-at "\\S-+\\s-+\\(\\*\\s-+\\)?\\([0-9]+\\)\\s-+\\([0-9]+\\)")
      (list state
            (match-string 2)
            (match-string 3)))
     (t (error "Couldn't parse output from `svn status -v'")))))

(defun vc-svn-parse-state-only ()
  "Parse the output from `svn status -v' at point, and return a state.
The documentation for the function `vc-state' describes the possible values."
  (cond
   ;; Be careful --- some of the later clauses here could yield false
   ;; positives, if the clauses preceding them didn't screen those
   ;; out. Making a pattern more selective could break something.

   ;; nil The given file is not under version control.
   ((looking-at "\\?") nil)

   ;; 'needs-patch The file has not been edited by the
   ;; user, but there is a more recent version
   ;; on the current branch stored in the
   ;; master file.
   ((looking-at "_[_ ]..\\s-+\\*") 'needs-patch)

   ;; 'up-to-date The working file is unmodified with
   ;; respect to the latest version on the
   ;; current branch, and not locked.
   ((looking-at "_[_ ]") 'up-to-date)

   ;; 'needs-merge The file has been edited by the user,
   ;; and there is also a more recent version
   ;; on the current branch stored in the
   ;; master file. This state can only occur
   ;; if locking is not used for the file.
   ((looking-at "\\S-+\\s-+\\*") 'needs-merge)

   ;; 'edited The working file has been edited by the
   ;; user. If locking is used for the file,
   ;; this state means that the current
   ;; version is locked by the calling user.
   (t 'edited)))

(defun vc-svn-state (file)
  "Return the current version control state of FILE.
For a list of possible return values, see `vc-state'.
This function should do a full and reliable state computation; it is
usually called immediately after `C-x v v'. `vc-svn-state-heuristic'
provides a faster heuristic when visiting a file."
  (car (vc-svn-run-status file 'update)))

(defun vc-svn-state-heuristic (file)
  "Estimate the version control state of FILE at visiting time.
For a list of possible values, see the doc string of `vc-state'.
This is supposed to be considerably faster than `vc-svn-state'. It
just runs `svn status -v', without the `-u' flag, so it's a strictly
local operation."
  (car (vc-svn-run-status file)))

(defun vc-svn-workfile-version (file)
  "Return the current workfile version of FILE."
  (cadr (vc-svn-run-status file)))

(defun vc-svn-checkout-model (file)
  'implicit)

(defun vc-svn-register (file &optional rev comment)
  "Register FILE with Subversion.
REV is an initial revision; Subversion ignores it.
COMMENT is an initial description of the file; currently this is ignored."
  (vc-svn-with-output-buffer
    (let ((status (call-process vc-svn-program-name nil t nil "add" file)))
      (or (equal 0 status) ; not zerop; status can be a string
          (vc-svn-pop-up-error "Error running Subversion to add `%s'"
                               (file-name-nondirectory file))))))

(defun vc-svn-checkin (file rev comment)
  (apply 'vc-do-command nil 0 vc-svn-program-name file
         "commit" (if comment (list "-m" comment) '())))

(defun vc-svn-checkout (file &optional editable rev destfile)
  "Check out revision REV of FILE into the working area.
The EDITABLE argument must be non-nil, since Subversion doesn't
support locking.
If REV is non-nil, that is the revision to check out (default is
current workfile version). If REV is the empty string, that means to
check out the head of the trunk. For Subversion, that's equivalent to
passing nil.
If optional arg DESTFILE is given, it is an alternate filename to
write the contents to; we raise an error."
  (unless editable
    (error "VC asked Subversion to check out a read-only copy of file"))
  (when destfile
    (error "VC asked Subversion to check out a file under another name"))
  (when (equal rev "")
    (setq rev nil))
  (apply 'vc-do-command nil 0 vc-svn-program-name file
         "update" (if rev (list "-r" rev) '()))
  (vc-file-setprop file 'vc-workfile-version nil))

(defun vc-svn-revert (file &optional contents-done)
  "Revert FILE back to the current workfile version.
If optional arg CONTENTS-DONE is non-nil, then the contents of FILE
have already been reverted from a version backup, and this function
only needs to update the status of FILE within the backend. This
function ignores the CONTENTS-DONE argument."
  (vc-do-command nil 0 vc-svn-program-name file "revert"))

(defun vc-svn-print-log (file)
  "Insert the revision log of FILE into the *vc* buffer."
  (vc-do-command nil 'async vc-svn-program-name file "log"))

;;; This never gets called, due to a bug vc-print-log that makes it
;;; try to use log-view-mode no matter what the back end. Boo.
(defun vc-svn-show-log-entry (version)
  "Search the log entry for VERSION in the current buffer.
Make sure it is displayed in the buffer's window."
  (when (re-search-forward (concat "^-+\n\\(rev\\) "
                                   (regexp-quote version)
                                   ":[^|]|[^|]| [0-9]+ lines?"))
    (goto-char (match-beginning 1))
    (recenter 1)))

(defun vc-svn-diff (file &optional rev1 rev2)
  "Insert the diff for FILE into the *vc-diff* buffer.
If REV1 and REV2 are non-nil, report differences from REV1 to REV2.
If REV1 is nil, use the current workfile version (as found in the
repository) as the older version; if REV2 is nil, use the current
workfile contents as the newer version.
This function returns a status of either 0 (no differences found), or
1 (either non-empty diff or the diff is run asynchronously)."
  (let* ((diff-switches-list (vc-diff-switches-list svn))
         (status (vc-svn-run-status file))
         (local (elt status 1))
         (changed (elt status 2))
         
         ;; If rev1 is the default (the base revision) set it to nil.
         ;; This is nice because it lets us recognize when the diff
         ;; will run locally, and thus when we shouldn't bother to run
         ;; it asynchronously. But it's also necessary, since a diff
         ;; for vc-default-workfile-unchanged-p *must* run
         ;; synchronously, or else you'll end up with two diffs in the
         ;; *vc-diff* buffer. `vc-diff-workfile-unchanged-p' passes
         ;; the base revision explicitly, but this kludge lets us
         ;; recognize that we can run the diff synchronously anyway.
         ;; Fragile, no?
         (rev1 (if (and rev1 (not (equal rev1 local))) rev1))

         (rev-switches-list
          (cond
           ;; Given base rev against given rev.
           ((and rev1 rev2) (list "-r" (format "%s:%s" rev1 rev2)))
           ;; Given base rev against working copy.
           (rev1 (list "-r" rev1))
           ;; Working copy base against given rev.
           (rev2 (list "-r" (format "%s:%s" local rev2)))
           ;; Working copy base against working copy.
           (t '())))

         ;; Run diff asynchronously if we're going to have to go
         ;; across the network.
         (async (or rev1 rev2)))

    (let ((status (apply 'vc-do-command "*vc-diff*" (if async 'async 0)
                         vc-svn-program-name file
                         (append '("diff") rev-switches-list))))
      (if (or async (> (buffer-size (get-buffer "*vc-diff*")) 0))
          1 0))))

(provide 'vc-svn)

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@subversion.tigris.org
For additional commands, e-mail: dev-help@subversion.tigris.org
Received on Mon Jul 15 06:51:59 2002

This is an archived mail posted to the Subversion Dev mailing list.

This site is subject to the Apache Privacy Policy and the Apache Public Forum Archive Policy.