;;; vc-cvs.el --- non-resident support for CVS version-control

;; Copyright (C) 1995,98,99,2000,2001  Free Software Foundation, Inc.

;; Author:      FSF (see vc.el for full credits)
;; Maintainer:  Andre Spiegel <spiegel@gnu.org>

;; $Id: vc-cvs.el,v 1.33 2002/02/21 20:16:47 spiegel Exp $

;; This file is part of GNU Emacs.

;; GNU Emacs is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;;; Code:

(eval-when-compile
  (require 'vc))

;;;
;;; Customization options
;;;

(defcustom vc-cvs-register-switches nil
  "*Extra switches for registering a file into CVS.
A string or list of strings passed to the checkin program by
\\[vc-register]."
  :type '(choice (const :tag "None" nil)
		 (string :tag "Argument String")
		 (repeat :tag "Argument List"
			 :value ("")
			 string))
  :version "21.1"
  :group 'vc)

(defcustom vc-cvs-diff-switches nil
  "*A string or list of strings specifying extra switches for cvs diff under VC."
    :type '(choice (const :tag "None" nil)
		 (string :tag "Argument String")
		 (repeat :tag "Argument List"
			 :value ("")
			 string))
  :version "21.1"
  :group 'vc)

(defcustom vc-cvs-header (or (cdr (assoc 'CVS vc-header-alist)) '("\$Id\$"))
  "*Header keywords to be inserted by `vc-insert-headers'."
  :version "21.1"
  :type '(repeat string)
  :group 'vc)

(defcustom vc-cvs-use-edit t
  "*Non-nil means to use `cvs edit' to \"check out\" a file.
This is only meaningful if you don't use the implicit checkout model
\(i.e. if you have $CVSREAD set)."
  :type 'boolean
  :version "21.1"
  :group 'vc)

(defcustom vc-cvs-stay-local t
  "*Non-nil means use local operations when possible for remote repositories.
This avoids slow queries over the network and instead uses heuristics
and past information to determine the current status of a file.
The value can also be a regular expression to match against the host name
of a repository; then VC only stays local for hosts that match it."
  :type '(choice (const :tag "Always stay local" t)
		 (string :tag "Host regexp")
		 (const :tag "Don't stay local" nil))
  :version "21.1"
  :group 'vc)

(defcustom vc-cvs-sticky-date-format-string "%c"
  "*Format string for mode-line display of sticky date.
Format is according to `format-time-string'.  Only used if
`vc-cvs-sticky-tag-display' is t."
  :type '(string)
  :version "21.3"
  :group 'vc)

(defcustom vc-cvs-sticky-tag-display t
  "*Specify the mode-line display of sticky tags.
Value t means default display, nil means no display at all.  If the
value is a function or macro, it is called with the sticky tag and
its' type as parameters, in that order.  TYPE can have three different
values: `symbolic-name' (TAG is a string), `revision-number' (TAG is a
string) and `date' (TAG is a date as returned by `encode-time').  The
return value of the function or macro will be displayed as a string.

Here's an example that will display the formatted date for sticky
dates and the word \"Sticky\" for sticky tag names and revisions.

  (lambda (tag type)
    (cond ((eq type 'date) (format-time-string 
                              vc-cvs-sticky-date-format-string tag))
          ((eq type 'revision-number) \"Sticky\")
          ((eq type 'symbolic-name) \"Sticky\")))

Here's an example that will abbreviate to the first character only,
any text before the first occurence of `-' for sticky symbolic tags.
If the sticky tag is a revision number, the word \"Sticky\" is
displayed.  Date and time is displayed for sticky dates.

   (lambda (tag type)
     (cond ((eq type 'date) (format-time-string \"%Y%m%d %H:%M\" tag))
           ((eq type 'revision-number) \"Sticky\")
           ((eq type 'symbolic-name) 
            (condition-case nil
                (progn
                  (string-match \"\\\\([^-]*\\\\)\\\\(.*\\\\)\" tag)
                  (concat (substring (match-string 1 tag) 0 1) \":\" 
                          (substring (match-string 2 tag) 1 nil)))
              (error tag)))))       ; Fall-back to given tag name.

See also variable `vc-cvs-sticky-date-format-string'."
  :type '(choice boolean function)
  :version "21.3"
  :group 'vc)

;;;
;;; Internal variables
;;;

(defvar vc-cvs-local-month-numbers
  '(("Jan" . 1) ("Feb" .  2) ("Mar" .  3) ("Apr" .  4)
    ("May" . 5) ("Jun" .  6) ("Jul" .  7) ("Aug" .  8)
    ("Sep" . 9) ("Oct" . 10) ("Nov" . 11) ("Dec" . 12))
  "Local association list of month numbers.")


;;;
;;; State-querying functions
;;;

;;;###autoload (defun vc-cvs-registered (f)
;;;###autoload   (when (file-readable-p (expand-file-name
;;;###autoload 			  "CVS/Entries" (file-name-directory f)))
;;;###autoload       (require 'vc-cvs)
;;;###autoload       (vc-cvs-registered f)))

(defun vc-cvs-registered (file)
  "Check if FILE is CVS registered."
  (let ((dirname (or (file-name-directory file) ""))
	(basename (file-name-nondirectory file))
        ;; make sure that the file name is searched case-sensitively
        (case-fold-search nil))
    (if (file-readable-p (expand-file-name "CVS/Entries" dirname))
	(with-temp-buffer
          (vc-insert-file (expand-file-name "CVS/Entries" dirname))
          (goto-char (point-min))
	  (cond
	   ((re-search-forward
	     (concat "^/" (regexp-quote basename) "/") nil t)
	    (beginning-of-line)
	    (vc-cvs-parse-entry file)
	    t)
	   (t nil)))
      nil)))

(defun vc-cvs-state (file)
  "CVS-specific version of `vc-state'."
  (if (vc-cvs-stay-local-p file)
      (let ((state (vc-file-getprop file 'vc-state)))
        ;; If we should stay local, use the heuristic but only if
        ;; we don't have a more precise state already available.
	(if (memq state '(up-to-date edited))
	    (vc-cvs-state-heuristic file)
	  state))
    (with-temp-buffer
      (cd (file-name-directory file))
      (vc-do-command t 0 "cvs" file "status")
      (vc-cvs-parse-status t))))

(defun vc-cvs-state-heuristic (file)
  "CVS-specific state heuristic."
  ;; If the file has not changed since checkout, consider it `up-to-date'.
  ;; Otherwise consider it `edited'.
  (let ((checkout-time (vc-file-getprop file 'vc-checkout-time))
        (lastmod (nth 5 (file-attributes file))))
    (if (equal checkout-time lastmod)
        'up-to-date
      'edited)))

(defun vc-cvs-dir-state (dir)
  "Find the CVS state of all files in DIR."
  (if (vc-cvs-stay-local-p dir)
      (vc-cvs-dir-state-heuristic dir)
    (let ((default-directory dir))
      ;; Don't specify DIR in this command, the default-directory is
      ;; enough.  Otherwise it might fail with remote repositories.
      (with-temp-buffer
	(vc-do-command t 0 "cvs" nil "status" "-l")
	(goto-char (point-min))
	(while (re-search-forward "^=+\n\\([^=\n].*\n\\|\n\\)+" nil t)
	  (narrow-to-region (match-beginning 0) (match-end 0))
	  (vc-cvs-parse-status)
	  (goto-char (point-max))
	  (widen))))))

(defun vc-cvs-workfile-version (file)
  "CVS-specific version of `vc-workfile-version'."
  ;; There is no need to consult RCS headers under CVS, because we
  ;; get the workfile version for free when we recognize that a file
  ;; is registered in CVS.
  (vc-cvs-registered file)
  (vc-file-getprop file 'vc-workfile-version))

(defun vc-cvs-checkout-model (file)
  "CVS-specific version of `vc-checkout-model'."
  (if (or (getenv "CVSREAD")
          ;; If the file is not writable (despite CVSREAD being
          ;; undefined), this is probably because the file is being
          ;; "watched" by other developers.
          ;; (If vc-mistrust-permissions was t, we actually shouldn't
          ;; trust this, but there is no other way to learn this from CVS
          ;; at the moment (version 1.9).)
          (string-match "r-..-..-." (nth 8 (file-attributes file))))
      'announce
    'implicit))

(defun vc-cvs-mode-line-string (file)
  "Return string for placement into the modeline for FILE.
Compared to the default implementation, this function does two things:
Handle the special case of a CVS file that is added but not yet
committed and support display of sticky tags."
  (let* ((state   (vc-state file))
	 (rev     (vc-workfile-version file))
	 (sticky-tag (vc-file-getprop file 'vc-cvs-sticky-tag))
 	 (sticky-tag-printable (and sticky-tag
				    (not (string= sticky-tag ""))
 				    (concat "[" sticky-tag "]"))))
    (cond ((string= rev "0")
	   ;; A file that is added but not yet committed.
	   "CVS @@")
	  ((or (eq state 'up-to-date)
	       (eq state 'needs-patch))
	   (concat "CVS-" rev sticky-tag-printable))
          ((stringp state)
	   (concat "CVS:" state ":" rev sticky-tag-printable))
          (t
           ;; Not just for the 'edited state, but also a fallback
           ;; for all other states.  Think about different symbols
           ;; for 'needs-patch and 'needs-merge.
           (concat "CVS:" rev sticky-tag-printable)))))

(defun vc-cvs-dired-state-info (file)
  "CVS-specific version of `vc-dired-state-info'."
  (let* ((cvs-state (vc-state file))
	 (state (cond ((eq cvs-state 'edited)	"modified")
		      ((eq cvs-state 'needs-patch)	"patch")
		      ((eq cvs-state 'needs-merge)	"merge")
		      ;; FIXME: those two states cannot occur right now
		      ((eq cvs-state 'unlocked-changes)	"conflict")
		      ((eq cvs-state 'locally-added)	"added")
		      )))
    (if state (concat "(" state ")"))))


;;;
;;; State-changing functions
;;;

(defun vc-cvs-register (file &optional rev comment)
  "Register FILE into the CVS version-control system.
COMMENT can be used to provide an initial description of FILE.

`vc-register-switches' and `vc-cvs-register-switches' are passed to
the CVS command (in that order)."
    (let ((switches (list
		     (if (stringp vc-register-switches)
			 (list vc-register-switches)
		       vc-register-switches)
		     (if (stringp vc-cvs-register-switches)
			 (list vc-cvs-register-switches)
		       vc-cvs-register-switches))))

      (apply 'vc-do-command nil 0 "cvs" file
	     "add"
	     (and comment (string-match "[^\t\n ]" comment)
		  (concat "-m" comment))
	     switches)))

(defun vc-cvs-responsible-p (file)
  "Return non-nil if CVS thinks it is responsible for FILE."
  (file-directory-p (expand-file-name "CVS"
				      (if (file-directory-p file)
					  file
					(file-name-directory file)))))

(defun vc-cvs-could-register (file)
  "Return non-nil if FILE could be registered in CVS.
This is only possible if CVS is responsible for FILE's directory."
  (vc-cvs-responsible-p file))

(defun vc-cvs-checkin (file rev comment)
  "CVS-specific version of `vc-backend-checkin'."
  (let ((switches (if (stringp vc-checkin-switches)
		      (list vc-checkin-switches)
		    vc-checkin-switches))
	status)
    (if (not rev)
        (setq status (apply 'vc-do-command nil 1 "cvs" file
                            "ci" (if rev (concat "-r" rev))
                            (concat "-m" comment)
                            switches))
      (if (not (vc-cvs-valid-symbolic-tag-name-p rev))
          (error "%s is not a valid symbolic tag name")
        ;; If the input revison is a valid symbolic tag name, we create it
        ;; as a branch, commit and switch to it.       
        (apply 'vc-do-command nil 0 "cvs" file "tag" "-b" (list rev))
        (apply 'vc-do-command nil 0 "cvs" file "update" "-r" (list rev))
        (setq status (apply 'vc-do-command nil 1 "cvs" file
                            "ci" 
                            (concat "-m" comment)
                            switches))
        (vc-file-setprop file 'vc-cvs-sticky-tag rev)))
    (set-buffer "*vc*")
    (goto-char (point-min))
    (when (not (zerop status))
      ;; Check checkin problem.
      (cond
       ((re-search-forward "Up-to-date check failed" nil t)
        (vc-file-setprop file 'vc-state 'needs-merge)
        (error (substitute-command-keys
                (concat "Up-to-date check failed: "
                        "type \\[vc-next-action] to merge in changes"))))
       (t
        (pop-to-buffer (current-buffer))
        (goto-char (point-min))
        (shrink-window-if-larger-than-buffer)
        (error "Check-in failed"))))
    ;; Update file properties
    (vc-file-setprop
     file 'vc-workfile-version
     (vc-parse-buffer "^\\(new\\|initial\\) revision: \\([0-9.]+\\)" 2))
    ;; Forget the checkout model of the file, because we might have
    ;; guessed wrong when we found the file.  After commit, we can
    ;; tell it from the permissions of the file (see
    ;; vc-cvs-checkout-model).
    (vc-file-setprop file 'vc-checkout-model nil)

    ;; if this was an explicit check-in (does not include creation of
    ;; a branch), remove the sticky tag.
    (if (and rev (not (vc-cvs-valid-symbolic-tag-name-p rev)))
	(vc-do-command nil 0 "cvs" file "update" "-A"))))

(defun vc-cvs-checkout (file &optional editable rev workfile)
  "Retrieve a revision of FILE into a WORKFILE.
EDITABLE non-nil means that the file should be writable.
REV is the revision to check out into WORKFILE."
  (let ((filename (or workfile file))
	(file-buffer (get-file-buffer file))
	switches)
    (message "Checking out %s..." filename)
    (save-excursion
      ;; Change buffers to get local value of vc-checkout-switches.
      (if file-buffer (set-buffer file-buffer))
      (setq switches (if (stringp vc-checkout-switches)
			 (list vc-checkout-switches)
		       vc-checkout-switches))
      ;; Save this buffer's default-directory
      ;; and use save-excursion to make sure it is restored
      ;; in the same buffer it was saved in.
      (let ((default-directory default-directory))
	(save-excursion
	  ;; Adjust the default-directory so that the check-out creates
	  ;; the file in the right place.
	  (setq default-directory (file-name-directory filename))
	  (if workfile
	      (let ((failed t)
                    (backup-name (if (string= file workfile)
                                     (car (find-backup-file-name filename)))))
                (when backup-name
                  (copy-file filename backup-name
                             'ok-if-already-exists 'keep-date)
                  (unless (file-writable-p filename)
                    (set-file-modes filename
                                    (logior (file-modes filename) 128))))
		(unwind-protect
		    (progn
                      (let ((coding-system-for-read 'no-conversion)
                            (coding-system-for-write 'no-conversion))
                        (with-temp-file filename
                          (apply 'vc-do-command
                                 (current-buffer) 0 "cvs" file
                                 "-Q"	; suppress diagnostic output
	 "update"
                                 (and rev (not (string= rev ""))
                                      (concat "-r" rev))
                                 "-p"
                                 switches)))
		      (setq failed nil))
		  (if failed
                      (if backup-name
                          (rename-file backup-name filename
                                       'ok-if-already-exists)
                        (if (file-exists-p filename)
                            (delete-file filename)))
                    (and backup-name
                         (not vc-make-backup-files)
                         (delete-file backup-name)))))
	    (if (and (file-exists-p file) (not rev))
		;; If no revision was specified, just make the file writable
		;; if necessary (using `cvs-edit' if requested).
      (and editable (not (eq (vc-cvs-checkout-model file) 'implicit))
		     (if vc-cvs-use-edit
			 (vc-do-command nil 0 "cvs" file "edit")
		       (set-file-modes file (logior (file-modes file) 128))
		       (if file-buffer (toggle-read-only -1))))
	      ;; Check out a particular version (or recreate the file).
	      (vc-file-setprop file 'vc-workfile-version nil)
	      (apply 'vc-do-command nil 0 "cvs" file
	   (and editable
		(or (not (file-exists-p file))
		    (not (eq (vc-cvs-checkout-model file)
			     'implicit)))
		"-w")
	   "update"
	   ;; default for verbose checkout: clear the sticky tag so
	   ;; that the actual update will get the head of the trunk
		     (if (or (not rev) (string= rev ""))
			 "-A"
		       (concat "-r" rev))
		     switches))))
	(vc-mode-line file)
	(message "Checking out %s...done" filename)))))

(defun vc-cvs-revert (file &optional contents-done)
  "Revert FILE to the version it was based on."
  (unless contents-done
    ;; Check out via standard output (caused by the final argument
    ;; FILE below), so that no sticky tag is set.
    (vc-cvs-checkout file nil (vc-workfile-version file) file))
  (unless (eq (vc-checkout-model file) 'implicit)
    (if vc-cvs-use-edit
        (vc-do-command nil 0 "cvs" file "unedit")
      ;; Make the file read-only by switching off all w-bits
      (set-file-modes file (logand (file-modes file) 3950)))))

(defun vc-cvs-merge (file first-version &optional second-version)
  "Merge changes into current working copy of FILE.
The changes are between FIRST-VERSION and SECOND-VERSION."
  (vc-do-command nil 0 "cvs" file
                 "update" "-kk"
                 (concat "-j" first-version)
                 (concat "-j" second-version))
  (vc-file-setprop file 'vc-state 'edited)
  (save-excursion
    (set-buffer (get-buffer "*vc*"))
    (goto-char (point-min))
    (if (re-search-forward "conflicts during merge" nil t)
        1				; signal error
      0)))				; signal success

(defun vc-cvs-merge-news (file)
  "Merge in any new changes made to FILE."
  (message "Merging changes into %s..." file)
  (save-excursion
    ;; (vc-file-setprop file 'vc-workfile-version nil)
    (vc-file-setprop file 'vc-checkout-time 0)
    (vc-do-command nil 0 "cvs" file "update")
    ;; Analyze the merge result reported by CVS, and set
    ;; file properties accordingly.
    (set-buffer (get-buffer "*vc*"))
    (goto-char (point-min))
    ;; get new workfile version
    (if (re-search-forward (concat "^Merging differences between "
				   "[01234567890.]* and "
				   "\\([01234567890.]*\\) into")
			   nil t)
	(vc-file-setprop file 'vc-workfile-version (match-string 1))
      (vc-file-setprop file 'vc-workfile-version nil))
    ;; get file status
    (prog1
        (if (eq (buffer-size) 0)
            0 ;; there were no news; indicate success
          (if (re-search-forward
               (concat "^\\([CMUP] \\)?"
                       (regexp-quote (file-name-nondirectory file))
                       "\\( already contains the differences between \\)?")
               nil t)
              (cond
               ;; Merge successful, we are in sync with repository now
               ((or (match-string 2)
                    (string= (match-string 1) "U ")
                    (string= (match-string 1) "P "))
                (vc-file-setprop file 'vc-state 'up-to-date)
                (vc-file-setprop file 'vc-checkout-time
                                 (nth 5 (file-attributes file)))
                0);; indicate success to the caller
               ;; Merge successful, but our own changes are still in the file
               ((string= (match-string 1) "M ")
                (vc-file-setprop file 'vc-state 'edited)
                0);; indicate success to the caller
               ;; Conflicts detected!
               (t
                (vc-file-setprop file 'vc-state 'edited)
                1);; signal the error to the caller
               )
            (pop-to-buffer "*vc*")
            (error "Couldn't analyze cvs update result")))
      (message "Merging changes into %s...done" file))))


;;;
;;; History functions
;;;

(defun vc-cvs-print-log (file)
  "Get change log associated with FILE."
  (vc-do-command
   nil
   (if (and (vc-cvs-stay-local-p file) (fboundp 'start-process)) 'async 0)
   "cvs" file "log"))

(defun vc-cvs-show-log-entry (version)
  (when (re-search-forward
	 ;; also match some context, for safety
	 (concat "----\nrevision " version
		 "\\(\tlocked by:.*\n\\|\n\\)date: ") nil t)
    ;; set the display window so that
    ;; the whole log entry is displayed
    (let (start end lines)
      (beginning-of-line) (forward-line -1) (setq start (point))
      (if (not (re-search-forward "^----*\nrevision" nil t))
	  (setq end (point-max))
	(beginning-of-line) (forward-line -1) (setq end (point)))
      (setq lines (count-lines start end))
      (cond
       ;; if the global information and this log entry fit
       ;; into the window, display from the beginning
       ((< (count-lines (point-min) end) (window-height))
	(goto-char (point-min))
	(recenter 0)
	(goto-char start))
       ;; if the whole entry fits into the window,
       ;; display it centered
       ((< (1+ lines) (window-height))
	(goto-char start)
	(recenter (1- (- (/ (window-height) 2) (/ lines 2)))))
       ;; otherwise (the entry is too large for the window),
       ;; display from the start
       (t
	(goto-char start)
	(recenter 0))))))

(defun vc-cvs-diff (file &optional oldvers newvers)
  "Get a difference report using CVS between two versions of FILE."
  (let (options status (diff-switches-list (vc-diff-switches-list 'CVS)))
    (if (string= (vc-workfile-version file) "0")
	;; This file is added but not yet committed; there is no master file.
	(if (or oldvers newvers)
	    (error "No revisions of %s exist" file)
	  ;; we regard this as "changed".
	  ;; diff it against /dev/null.
          (apply 'vc-do-command "*vc-diff*"
                 1 "diff" file
                 (append diff-switches-list '("/dev/null"))))
      (setq status
            (apply 'vc-do-command "*vc-diff*"
                   (if (and (vc-cvs-stay-local-p file)
			    (fboundp 'start-process))
		       'async
		     1)
                   "cvs" file "diff"
                   (and oldvers (concat "-r" oldvers))
                   (and newvers (concat "-r" newvers))
                   diff-switches-list))
      (if (vc-cvs-stay-local-p file)
          1 ;; async diff, pessimistic assumption
        status))))

(defun vc-cvs-diff-tree (dir &optional rev1 rev2)
  "Diff all files at and below DIR."
  (with-current-buffer "*vc-diff*"
    (setq default-directory dir)
    (if (vc-cvs-stay-local-p dir)
        ;; local diff: do it filewise, and only for files that are modified
        (vc-file-tree-walk
         dir
         (lambda (f)
           (vc-exec-after
            `(let ((coding-system-for-read (vc-coding-system-for-diff ',f)))
               ;; possible optimization: fetch the state of all files
               ;; in the tree via vc-cvs-dir-state-heuristic
               (unless (vc-up-to-date-p ',f)
                 (message "Looking at %s" ',f)
                 (vc-diff-internal ',f ',rel1 ',rel2))))))
      ;; cvs diff: use a single call for the entire tree
      (let ((coding-system-for-read
             (or coding-system-for-read 'undecided)))
        (apply 'vc-do-command "*vc-diff*" 1 "cvs" nil "diff"
               (and rel1 (concat "-r" rel1))
               (and rel2 (concat "-r" rel2))
               (vc-diff-switches-list 'CVS))))))

(defun vc-cvs-annotate-command (file buffer &optional version)
  "Execute \"cvs annotate\" on FILE, inserting the contents in BUFFER.
Optional arg VERSION is a version to annotate from."
  (vc-do-command buffer 0 "cvs" file "annotate" (if version
                                                    (concat "-r" version))))

(defun vc-cvs-annotate-current-time ()
  "Return the current time, based at midnight of the current day, and
encoded as fractional days."
  (vc-annotate-convert-time
   (apply 'encode-time 0 0 0 (nthcdr 3 (decode-time (current-time))))))

(defun vc-cvs-annotate-time ()
  "Return the time of the next annotation (as fraction of days)
systime, or nil if there is none."
  (let ((time-stamp 
	 "^\\S-+\\s-+\\S-+\\s-+\\([0-9]+\\)-\\(\\sw+\\)-\\([0-9]+\\)): "))
    (if (looking-at time-stamp)
      (progn
	(let* ((day (string-to-number (match-string 1)))
		 (month (cdr (assoc (match-string 2) 
				    vc-cvs-local-month-numbers)))
	       (year-tmp (string-to-number (match-string 3)))
	       ;; Years 0..68 are 2000..2068.
	       ;; Years 69..99 are 1969..1999.
	       (year (+ (cond ((> 69 year-tmp) 2000)
			      ((> 100 year-tmp) 1900)
			      (t 0))
			year-tmp)))
	  (goto-char (match-end 0)) ; Position at end makes for nicer overlay result
	    (vc-annotate-convert-time (encode-time 0 0 0 day month year))))
    ;; If we did not look directly at an annotation, there might be
    ;; some further down.  This is the case if we are positioned at
    ;; the very top of the buffer, for instance.
      (if (re-search-forward time-stamp nil t)
	(progn
	  (beginning-of-line nil)
	    (vc-cvs-annotate-time))))))

;;;
;;; Snapshot system
;;;

(defun vc-cvs-create-snapshot (dir name branchp)
  "Assign to DIR's current version a given NAME.
If BRANCHP is non-nil, the name is created as a branch (and the current
workspace is immediately moved to that new branch)."
  (vc-do-command nil 0 "cvs" dir "tag" "-c" (if branchp "-b") name)
  (when branchp (vc-do-command nil 0 "cvs" dir "update" "-r" name)))

(defun vc-cvs-retrieve-snapshot (dir name update)
  "Retrieve a snapshot at and below DIR.
NAME is the name of the snapshot; if it is empty, do a `cvs update'.
If UPDATE is non-nil, then update (resynch) any affected buffers."
  (with-current-buffer (get-buffer-create "*vc*")
    (let ((default-directory dir)
	  (sticky-tag))
      (erase-buffer)
      (if (or (not name) (string= name ""))
	  (vc-do-command t 0 "cvs" nil "update")
	(vc-do-command t 0 "cvs" nil "update" "-r" name)
	(setq sticky-tag name))
      (when update
	(goto-char (point-min))
	(while (not (eobp))
	  (if (looking-at "\\([CMUP]\\) \\(.*\\)")
	      (let* ((file (expand-file-name (match-string 2) dir))
		     (state (match-string 1))
		     (buffer (find-buffer-visiting file)))
		(when buffer
		  (cond
		   ((or (string= state "U")
			(string= state "P"))
		    (vc-file-setprop file 'vc-state 'up-to-date)
		    (vc-file-setprop file 'vc-workfile-version nil)
		    (vc-file-setprop file 'vc-checkout-time
				     (nth 5 (file-attributes file))))
		   ((or (string= state "M")
			(string= state "C"))
		    (vc-file-setprop file 'vc-state 'edited)
		    (vc-file-setprop file 'vc-workfile-version nil)
		    (vc-file-setprop file 'vc-checkout-time 0)))
		  (vc-file-setprop file 'vc-cvs-sticky-tag sticky-tag)
		  (vc-resynch-buffer file t t))))
	  (forward-line 1))))))


;;;
;;; Miscellaneous
;;;

(defun vc-cvs-make-version-backups-p (file)
  "Return non-nil if version backups should be made for FILE."
  (vc-cvs-stay-local-p file))

(defun vc-cvs-check-headers ()
  "Check if the current file has any headers in it."
  (save-excursion
    (goto-char (point-min))
    (re-search-forward "\\$[A-Za-z\300-\326\330-\366\370-\377]+\
\\(: [\t -#%-\176\240-\377]*\\)?\\$" nil t)))


;;;
;;; Internal functions
;;;

(defun vc-cvs-stay-local-p (file)
  "Return non-nil if VC should stay local when handling FILE."
  (if vc-cvs-stay-local
      (let* ((dirname (if (file-directory-p file)
			  (directory-file-name file)
			(file-name-directory file)))
	     (prop
	      (or (vc-file-getprop dirname 'vc-cvs-stay-local-p)
		  (let ((rootname (expand-file-name "CVS/Root" dirname)))
		    (vc-file-setprop
		     dirname 'vc-cvs-stay-local-p
		     (when (file-readable-p rootname)
		       (with-temp-buffer
			 (vc-insert-file rootname)
			 (goto-char (point-min))
			 (if (looking-at "\\([^:]*\\):")
			     (if (not (stringp vc-cvs-stay-local))
				 'yes
			       (let ((hostname (match-string 1)))
				 (if (string-match vc-cvs-stay-local hostname)
				     'yes
				   'no)))
			   'no))))))))
	(if (eq prop 'yes) t nil))))

(defun vc-cvs-parse-status (&optional full)
  "Parse output of \"cvs status\" command in the current buffer.
Set file properties accordingly.  Unless FULL is t, parse only
essential information."
  (let (file status)
    (goto-char (point-min))
    (if (re-search-forward "^File: " nil t)
        (cond
         ((looking-at "no file") nil)
         ((re-search-forward "\\=\\([^ \t]+\\)" nil t)
	  (setq file (expand-file-name (match-string 1)))
          (vc-file-setprop file 'vc-backend 'CVS)
          (if (not (re-search-forward "\\=[ \t]+Status: \\(.*\\)" nil t))
              (setq status "Unknown")
            (setq status (match-string 1)))
          (if (and full
                   (re-search-forward
                    "\\(RCS Version\\|RCS Revision\\|Repository revision\\):\
\[\t ]+\\([0-9.]+\\)"
                    nil t))
              (vc-file-setprop file 'vc-latest-version (match-string 2)))
          (vc-file-setprop 
           file 'vc-state
           (cond
            ((string-match "Up-to-date" status)
             (vc-file-setprop file 'vc-checkout-time
                              (nth 5 (file-attributes file)))
             'up-to-date)
            ((string-match "Locally Modified" status)             'edited)
            ((string-match "Needs Merge" status)                  'needs-merge)
            ((string-match "Needs \\(Checkout\\|Patch\\)" status) 'needs-patch)
            (t 'edited))))))))

(defun vc-cvs-dir-state-heuristic (dir)
  "Find the CVS state of all files in DIR, using only local information."
  (with-temp-buffer
    (vc-insert-file (expand-file-name "CVS/Entries" dir))
    (goto-char (point-min))
    (while (not (eobp))
      (when (looking-at "/\\([^/]*\\)/")
	(let ((file (expand-file-name (match-string 1) dir)))
	  (unless (vc-file-getprop file 'vc-state)
	    (vc-cvs-parse-entry file t))))
      (forward-line 1))))


(defun vc-cvs-valid-symbolic-tag-name-p (tag)
  "Return non-nil if TAG is a valid symbolic tag name."
  ;; According to the CVS manual, a valid symbolic tag must start with
  ;; an uppercase or lowercase letter and can contain uppercase and
  ;; lowercase letters, digits, `-', and `_'.
  (and (string-match "^[a-zA-Z]" tag)
       (not (string-match "[^a-z0-9A-Z-_]" tag))))
      

(defun vc-cvs-parse-sticky-tag (match-type match-tag)
  "Parse and return the sticky tag as a string.  
`match-data' is protected."
  (let ((data (match-data))
	(tag)
	(type (cond ((string= match-type "D") 'date)
		    ((string= match-type "T")
		     (if (vc-cvs-valid-symbolic-tag-name-p match-tag)
			 'symbolic-name
		       'revision-number))
		    (t nil))))
    (unwind-protect
	(progn
	  (cond 
	   ;; Sticky Date tag.  Convert to to a proper date value (`encode-time')
	   ((eq type 'date)
	    (string-match 
	     "\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)" 
	     match-tag)
	    (let* ((year-tmp (string-to-number (match-string 1 match-tag)))
		   (month    (string-to-number (match-string 2 match-tag)))
		   (day      (string-to-number (match-string 3 match-tag)))
		   (hour     (string-to-number (match-string 4 match-tag)))
		   (min      (string-to-number (match-string 5 match-tag)))
		   (sec      (string-to-number (match-string 6 match-tag)))
		   ;; Years 0..68 are 2000..2068.
		   ;; Years 69..99 are 1969..1999.
		   (year (+ (cond ((> 69 year-tmp) 2000)
				  ((> 100 year-tmp) 1900)
				  (t 0))
			    year-tmp)))
	      (setq tag (encode-time sec min hour day month year))))
	   ;; Sticky Tag name or revision number
	   ((eq type 'symbolic-name) (setq tag match-tag))
	   ((eq type 'revision-number) (setq tag match-tag))
	   ;; Default is no sticky tag at all
	   (t nil))
	  (cond ((eq vc-cvs-sticky-tag-display nil) nil)
		((eq vc-cvs-sticky-tag-display t)
		 (cond ((eq type 'date) (format-time-string 
					 vc-cvs-sticky-date-format-string
					 tag))
		       ((eq type 'symbolic-name) tag)
		       ((eq type 'revision-number) tag)
		       (t nil)))
		((functionp vc-cvs-sticky-tag-display) 
		 (funcall vc-cvs-sticky-tag-display tag type))
		(t nil)))

      (set-match-data data))))

(defun vc-cvs-parse-entry (file &optional set-state)
  "Parse a line from CVS/Entries.
Compare modification time to that of the FILE, set file properties
accordingly.  However, `vc-state' is set only if optional arg SET-STATE
is non-nil."
  (cond
   ;; entry for a "locally added" file (not yet committed)
   ((looking-at "/[^/]+/0/")
    (vc-file-setprop file 'vc-checkout-time 0)
    (vc-file-setprop file 'vc-workfile-version "0")
    (if set-state (vc-file-setprop file 'vc-state 'edited)))
   ;; normal entry
   ((looking-at
     (concat "/[^/]+"
	     ;; revision
	     "/\\([^/]*\\)"
	     ;; timestamp
	     "/\\([^/]*\\)"
	     ;; optional conflict field
	     "\\(+[^/]*\\)?/"
	     ;; options
	     "\\([^/]*\\)/"
	     ;; sticky tag
	     "\\(.\\|\\)" ;Sticky tag type (date or tag name, could be empty)
	     "\\(.*\\)"))		;Sticky tag
    (vc-file-setprop file 'vc-workfile-version (match-string 1))
    (vc-file-setprop file 'vc-cvs-sticky-tag 
		     (vc-cvs-parse-sticky-tag (match-string 5) (match-string 6)))
    ;; compare checkout time and modification time
    (let ((mtime (nth 5 (file-attributes file)))
	  (system-time-locale "C"))
      (cond ((equal (format-time-string "%c" mtime 'utc) (match-string 2))
	     (vc-file-setprop file 'vc-checkout-time mtime)
	     (if set-state (vc-file-setprop file 'vc-state 'up-to-date)))
	    (t
	     (vc-file-setprop file 'vc-checkout-time 0)
	     (if set-state (vc-file-setprop file 'vc-state 'edited))))))))
           
(provide 'vc-cvs)

;;; vc-cvs.el ends here
