vc-cvs.el 37.8 KB
Newer Older
1 2
;;; vc-cvs.el --- non-resident support for CVS version-control

3
;; Copyright (C) 1995, 1998, 1999, 2000, 2001, 2002, 2003,
Glenn Morris's avatar
Glenn Morris committed
4
;;   2004, 2005, 2006, 2007 Free Software Foundation, Inc.
5 6 7 8

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

9
;; $Id$
10 11 12 13 14

;; 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
15
;; the Free Software Foundation; either version 3, or (at your option)
16 17 18 19 20 21 22 23 24
;; 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
Lute Kamstra's avatar
Lute Kamstra committed
25 26
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
27 28 29 30 31

;;; Commentary:

;;; Code:

Stefan Monnier's avatar
Stefan Monnier committed
32
(eval-when-compile (require 'cl) (require 'vc))
Sam Steingold's avatar
Sam Steingold committed
33

34 35 36 37
;; Clear up the cache to force vc-call to check again and discover
;; new functions when we reload this file.
(put 'CVS 'vc-functions nil)

Sam Steingold's avatar
Sam Steingold committed
38
;;;
André Spiegel's avatar
André Spiegel committed
39 40 41
;;; Customization options
;;;

42 43 44 45 46 47 48
(defcustom vc-cvs-global-switches nil
  "*Global switches to pass to any CVS command."
  :type '(choice (const :tag "None" nil)
		 (string :tag "Argument String")
		 (repeat :tag "Argument List"
			 :value ("")
			 string))
49
  :version "22.1"
50 51
  :group 'vc)

52 53 54 55 56 57 58 59 60
(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))
Dave Love's avatar
Dave Love committed
61
  :version "21.1"
62 63
  :group 'vc)

64 65 66 67 68 69 70 71 72 73
(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)

74 75
(defcustom vc-cvs-header (or (cdr (assoc 'CVS vc-header-alist)) '("\$Id\$"))
  "*Header keywords to be inserted by `vc-insert-headers'."
Dave Love's avatar
Dave Love committed
76
  :version "21.1"
Dave Love's avatar
Dave Love committed
77
  :type '(repeat string)
78 79 80 81 82 83 84
  :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
Dave Love's avatar
Dave Love committed
85
  :version "21.1"
86 87
  :group 'vc)

88
(defcustom vc-cvs-stay-local t
Dave Love's avatar
Dave Love committed
89
  "*Non-nil means use local operations when possible for remote repositories.
Stefan Monnier's avatar
Stefan Monnier committed
90 91
This avoids slow queries over the network and instead uses heuristics
and past information to determine the current status of a file.
92

93 94
The value can also be a regular expression or list of regular
expressions to match against the host name of a repository; then VC
95
only stays local for hosts that match it.  Alternatively, the value
96 97
can be a list of regular expressions where the first element is the
symbol `except'; then VC always stays local except for hosts matched
98
by these regular expressions."
99
  :type '(choice (const :tag "Always stay local" t)
100
                (const :tag "Don't stay local" nil)
101
                 (list :format "\nExamine hostname and %v" :tag "Examine hostname ..."
102 103 104
                       (set :format "%v" :inline t (const :format "%t" :tag "don't" except))
                       (regexp :format " stay local,\n%t: %v" :tag "if it matches")
                       (repeat :format "%v%i\n" :inline t (regexp :tag "or"))))
Dave Love's avatar
Dave Love committed
105
  :version "21.1"
106 107
  :group 'vc)

108 109 110 111 112
(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)
113
  :version "22.1"
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
  :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)
129
    (cond ((eq type 'date) (format-time-string
130 131 132 133 134
                              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,
135
any text before the first occurrence of `-' for sticky symbolic tags.
136 137 138 139 140 141
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\")
142
           ((eq type 'symbolic-name)
143 144 145
            (condition-case nil
                (progn
                  (string-match \"\\\\([^-]*\\\\)\\\\(.*\\\\)\" tag)
146
                  (concat (substring (match-string 1 tag) 0 1) \":\"
147 148 149 150 151
                          (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)
152
  :version "22.1"
153
  :group 'vc)
Sam Steingold's avatar
Sam Steingold committed
154

André Spiegel's avatar
André Spiegel committed
155 156 157 158
;;;
;;; Internal variables
;;;

Sam Steingold's avatar
Sam Steingold committed
159

André Spiegel's avatar
André Spiegel committed
160
;;;
Sam Steingold's avatar
Sam Steingold committed
161
;;; State-querying functions
André Spiegel's avatar
André Spiegel committed
162 163
;;;

164 165 166
;;;###autoload (defun vc-cvs-registered (f)
;;;###autoload   (when (file-readable-p (expand-file-name
;;;###autoload 			  "CVS/Entries" (file-name-directory f)))
167
;;;###autoload       (load "vc-cvs")
168 169 170 171 172 173 174 175 176 177
;;;###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
178
          (vc-cvs-get-entries dirname)
179
          (goto-char (point-min))
Dave Love's avatar
Dave Love committed
180 181
	  (cond
	   ((re-search-forward
182 183
	     ;; CVS-removed files are not taken under VC control.
	     (concat "^/" (regexp-quote basename) "/[^/-]") nil t)
184 185 186 187 188 189 190 191
	    (beginning-of-line)
	    (vc-cvs-parse-entry file)
	    t)
	   (t nil)))
      nil)))

(defun vc-cvs-state (file)
  "CVS-specific version of `vc-state'."
192
  (if (vc-stay-local-p file)
193 194 195
      (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.
196
	(if (memq state '(up-to-date edited nil))
197 198 199 200
	    (vc-cvs-state-heuristic file)
	  state))
    (with-temp-buffer
      (cd (file-name-directory file))
201
      (vc-cvs-command t 0 file "status")
202 203 204 205 206 207 208 209 210 211 212 213
      (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)))

André Spiegel's avatar
André Spiegel committed
214 215
(defun vc-cvs-dir-state (dir)
  "Find the CVS state of all files in DIR."
216 217
  ;; if DIR is not under CVS control, don't do anything.
  (when (file-readable-p (expand-file-name "CVS/Entries" dir))
218
    (if (vc-stay-local-p dir)
219 220 221 222 223 224 225 226 227 228 229 230
	(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-cvs-command t 0 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)))))))
André Spiegel's avatar
André Spiegel committed
231

Eric S. Raymond's avatar
Eric S. Raymond committed
232 233
(defun vc-cvs-working-revision (file)
  "CVS-specific version of `vc-working-revision'."
André Spiegel's avatar
André Spiegel committed
234 235 236 237
  ;; 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)
Eric S. Raymond's avatar
Eric S. Raymond committed
238
  (vc-file-getprop file 'vc-working-revision))
André Spiegel's avatar
André Spiegel committed
239 240 241

(defun vc-cvs-checkout-model (file)
  "CVS-specific version of `vc-checkout-model'."
242
  (if (getenv "CVSREAD")
André Spiegel's avatar
André Spiegel committed
243
      'announce
244 245 246 247 248 249 250 251 252 253 254
    (let ((attrib (file-attributes file)))
      (if (and attrib ;; don't check further if FILE doesn't exist
               ;; 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 attrib)))
          'announce
        'implicit))))
André Spiegel's avatar
André Spiegel committed
255

256 257
(defun vc-cvs-mode-line-string (file)
  "Return string for placement into the modeline for FILE.
258 259 260
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."
261 262 263
  (let* ((sticky-tag (vc-file-getprop file 'vc-cvs-sticky-tag))
	 help-echo
	 (string 
Eric S. Raymond's avatar
Eric S. Raymond committed
264
	  (if (string= (vc-working-revision file) "0")
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
	      ;; A file that is added but not yet committed.
	      (progn
		(setq help-echo "Added file (needs commit) under CVS")
		"CVS @@")
	    (let ((def-ml (vc-default-mode-line-string 'CVS file)))
	      (setq help-echo 
		    (get-text-property 0 'help-echo def-ml))
	      def-ml))))
    (propertize 
     (if (zerop (length sticky-tag))
	 string
       (setq help-echo (format "%s on the '%s' branch" 
			       help-echo sticky-tag))
       (concat string "[" sticky-tag "]"))
     'help-echo help-echo)))
280

André Spiegel's avatar
André Spiegel committed
281 282
(defun vc-cvs-dired-state-info (file)
  "CVS-specific version of `vc-dired-state-info'."
283 284
  (let ((cvs-state (vc-state file)))
    (cond ((eq cvs-state 'edited)
Eric S. Raymond's avatar
Eric S. Raymond committed
285
	   (if (equal (vc-working-revision file) "0")
286 287 288
	       "(added)" "(modified)"))
	  ((eq cvs-state 'needs-patch) "(patch)")
	  ((eq cvs-state 'needs-merge) "(merge)"))))
289

Sam Steingold's avatar
Sam Steingold committed
290

André Spiegel's avatar
André Spiegel committed
291 292 293
;;;
;;; State-changing functions
;;;
294

295 296 297
(defun vc-cvs-register (files &optional rev comment)
  "Register FILES into the CVS version-control system.
COMMENT can be used to provide an initial description of FILES.
298

André Spiegel's avatar
André Spiegel committed
299 300
`vc-register-switches' and `vc-cvs-register-switches' are passed to
the CVS command (in that order)."
301
  (when (and (not (vc-cvs-responsible-p file))
302 303 304 305 306 307 308 309
	       (vc-cvs-could-register file))
      ;; Register the directory if needed.
      (vc-cvs-register (directory-file-name (file-name-directory file))))
    (apply 'vc-cvs-command nil 0 files
	   "add"
	   (and comment (string-match "[^\t\n ]" comment)
		(concat "-m" comment))
	   (vc-switches 'CVS 'register)))
310

André Spiegel's avatar
André Spiegel committed
311 312 313 314 315 316
(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)))))
317

318
(defun vc-cvs-could-register (file)
André Spiegel's avatar
André Spiegel committed
319
  "Return non-nil if FILE could be registered in CVS.
320 321 322 323 324 325 326 327 328 329
This is only possible if CVS is managing FILE's directory or one of
its parents."
  (let ((dir file))
    (while (and (stringp dir)
                (not (equal dir (setq dir (file-name-directory dir))))
                dir)
      (setq dir (if (file-directory-p
                     (expand-file-name "CVS/Entries" dir))
                    t (directory-file-name dir))))
    (eq dir t)))
330

331
(defun vc-cvs-checkin (files rev comment)
332
  "CVS-specific version of `vc-backend-checkin'."
Eric S. Raymond's avatar
Eric S. Raymond committed
333
  (unless (or (not rev) (vc-cvs-valid-revision-number-p rev))
334 335 336 337
    (if (not (vc-cvs-valid-symbolic-tag-name-p rev))
	(error "%s is not a valid symbolic tag name" rev)
      ;; If the input revison is a valid symbolic tag name, we create it
      ;; as a branch, commit and switch to it.
338 339
      (apply 'vc-cvs-command nil 0 files "tag" "-b" (list rev))
      (apply 'vc-cvs-command nil 0 files "update" "-r" (list rev))
340 341 342
      (mapc (lambda (file) (vc-file-setprop file 'vc-cvs-sticky-tag rev))
	    files)))
  (let ((status (apply 'vc-cvs-command nil 1 files
343 344 345
		       "ci" (if rev (concat "-r" rev))
		       (concat "-m" comment)
		       (vc-switches 'CVS 'checkin))))
346 347
    (set-buffer "*vc*")
    (goto-char (point-min))
348 349 350 351
    (when (not (zerop status))
      ;; Check checkin problem.
      (cond
       ((re-search-forward "Up-to-date check failed" nil t)
352 353
	(mapc (lambda (file) (vc-file-setprop file 'vc-state 'needs-merge))
	      files)
354 355 356 357 358 359 360 361
        (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"))))
Eric S. Raymond's avatar
Eric S. Raymond committed
362
    ;; Single-file commit?  Then update the revision by parsing the buffer.
363 364 365 366
    ;; Otherwise we can't necessarily tell what goes with what; clear
    ;; its properties so they have to be refetched.
    (if (= (length files) 1)
	(vc-file-setprop
Eric S. Raymond's avatar
Eric S. Raymond committed
367
	 (car files) 'vc-working-revision
368 369 370
	 (vc-parse-buffer "^\\(new\\|initial\\) revision: \\([0-9.]+\\)" 2))
      (mapc (lambda (file) (vc-file-clearprops file)) files))
    ;; Anyway, forget the checkout model of the file, because we might have
371 372 373
    ;; guessed wrong when we found the file.  After commit, we can
    ;; tell it from the permissions of the file (see
    ;; vc-cvs-checkout-model).
374 375
    (mapc (lambda (file) (vc-file-setprop file 'vc-checkout-model nil))
	  files)
376 377 378 379

    ;; 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)))
380
	(vc-cvs-command nil 0 files "update" "-A"))))
381

Eric S. Raymond's avatar
Eric S. Raymond committed
382
(defun vc-cvs-find-revision (file rev buffer)
383 384 385 386 387 388 389
  (apply 'vc-cvs-command
	 buffer 0 file
	 "-Q"				; suppress diagnostic output
	 "update"
	 (and rev (not (string= rev ""))
	      (concat "-r" rev))
	 "-p"
390
	 (vc-switches 'CVS 'checkout)))
391

392 393
(defun vc-cvs-checkout (file &optional editable rev)
  "Checkout a revision of FILE into the working area.
394
EDITABLE non-nil means that the file should be writable.
395 396 397 398 399 400 401 402 403 404 405 406
REV is the revision to check out."
  (message "Checking out %s..." file)
  ;; Change buffers to get local value of vc-checkout-switches.
  (with-current-buffer (or (get-file-buffer file) (current-buffer))
    (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-cvs-command nil 0 file "edit")
               (set-file-modes file (logior (file-modes file) 128))
               (if (equal file buffer-file-name) (toggle-read-only -1))))
Eric S. Raymond's avatar
Eric S. Raymond committed
407 408
      ;; Check out a particular revision (or recreate the file).
      (vc-file-setprop file 'vc-working-revision nil)
409 410 411 412 413 414 415 416 417 418 419 420 421 422
      (apply 'vc-cvs-command nil 0 file
             (and editable "-w")
             "update"
             (when rev
               (unless (eq rev t)
                 ;; default for verbose checkout: clear the
                 ;; sticky tag so that the actual update will
                 ;; get the head of the trunk
                 (if (string= rev "")
                     "-A"
                   (concat "-r" rev))))
             (vc-switches 'CVS 'checkout)))
    (vc-mode-line file))
  (message "Checking out %s...done" file))
423

424
(defun vc-cvs-delete-file (file)
425 426
  (vc-cvs-command nil 0 file "remove" "-f")
  (vc-cvs-command nil 0 file "commit" "-mRemoved."))
427

428
(defun vc-cvs-revert (file &optional contents-done)
Eric S. Raymond's avatar
Eric S. Raymond committed
429
  "Revert FILE to the working revision on which it was based."
430
  (vc-default-revert 'CVS file contents-done)
431 432
  (unless (eq (vc-checkout-model file) 'implicit)
    (if vc-cvs-use-edit
433
        (vc-cvs-command nil 0 file "unedit")
434 435
      ;; Make the file read-only by switching off all w-bits
      (set-file-modes file (logand (file-modes file) 3950)))))
André Spiegel's avatar
André Spiegel committed
436

Eric S. Raymond's avatar
Eric S. Raymond committed
437
(defun vc-cvs-merge (file first-revision &optional second-revision)
André Spiegel's avatar
André Spiegel committed
438
  "Merge changes into current working copy of FILE.
Eric S. Raymond's avatar
Eric S. Raymond committed
439
The changes are between FIRST-REVISION and SECOND-REVISION."
440
  (vc-cvs-command nil 0 file
André Spiegel's avatar
André Spiegel committed
441
                 "update" "-kk"
Eric S. Raymond's avatar
Eric S. Raymond committed
442 443
                 (concat "-j" first-revision)
                 (concat "-j" second-revision))
André Spiegel's avatar
André Spiegel committed
444
  (vc-file-setprop file 'vc-state 'edited)
445
  (with-current-buffer (get-buffer "*vc*")
André Spiegel's avatar
André Spiegel committed
446 447 448 449 450 451 452 453
    (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)
Eric S. Raymond's avatar
Eric S. Raymond committed
454
  ;; (vc-file-setprop file 'vc-working-revision nil)
455 456 457 458 459
  (vc-file-setprop file 'vc-checkout-time 0)
  (vc-cvs-command nil 0 file "update")
  ;; Analyze the merge result reported by CVS, and set
  ;; file properties accordingly.
  (with-current-buffer (get-buffer "*vc*")
André Spiegel's avatar
André Spiegel committed
460
    (goto-char (point-min))
Eric S. Raymond's avatar
Eric S. Raymond committed
461
    ;; get new working revision
462 463
    (if (re-search-forward
	 "^Merging differences between [0-9.]* and \\([0-9.]*\\) into" nil t)
Eric S. Raymond's avatar
Eric S. Raymond committed
464 465
	(vc-file-setprop file 'vc-working-revision (match-string 1))
      (vc-file-setprop file 'vc-working-revision nil))
André Spiegel's avatar
André Spiegel committed
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
    ;; 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))))

Sam Steingold's avatar
Sam Steingold committed
497

André Spiegel's avatar
André Spiegel committed
498 499 500 501
;;;
;;; History functions
;;;

502
(defun vc-cvs-print-log (files &optional buffer)
André Spiegel's avatar
André Spiegel committed
503
  "Get change log associated with FILE."
504
  (vc-cvs-command
505
   buffer
506
   (if (vc-stay-local-p files) 'async 0)
507 508 509 510 511 512
   files "log"))

(defun vc-cvs-wash-log ()
  "Remove all non-comment information from log output."
  (vc-call-backend 'RCS 'wash-log)
  nil)
André Spiegel's avatar
André Spiegel committed
513

514
(defun vc-cvs-diff (files &optional oldvers newvers buffer)
Eric S. Raymond's avatar
Eric S. Raymond committed
515
  "Get a difference report using CVS between two revisions of FILE."
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
  (let* ((async (and (not vc-disable-async-diff)
		     (vc-stay-local-p files)))
	 (invoke-cvs-diff-list nil)
	 status)
    ;; Look through the file list and see if any files have backups
    ;; that can be used to do a plain "diff" instead of "cvs diff".
    (dolist (file files)
      (let ((ov oldvers)
	    (nv newvers))
	(when (or (not ov) (string-equal ov ""))
	  (setq ov (vc-working-revision file)))
	(when (string-equal nv "")
	  (setq nv nil))
	(let ((file-oldvers (vc-version-backup-file file ov))
	      (file-newvers (if (not nv)
				file
			      (vc-version-backup-file file nv)))
	      (coding-system-for-read (vc-coding-system-for-diff file)))
	  (if (and file-oldvers file-newvers)
	      (progn
		(apply 'vc-do-command (or buffer "*vc-diff*") 1 "diff" nil
		       (append (if (listp diff-switches)
				   diff-switches
				 (list diff-switches))
			       (if (listp vc-diff-switches)
				   vc-diff-switches
				 (list vc-diff-switches))
			       (list (file-relative-name file-oldvers)
				     (file-relative-name file-newvers))))
		(setq status 0))
	    (push file invoke-cvs-diff-list)))))
    (when invoke-cvs-diff-list
      (setq status (apply 'vc-cvs-command (or buffer "*vc-diff*")
549
			  (if async 'async 1)
550
			  invoke-cvs-diff-list "diff"
551 552 553
			  (and oldvers (concat "-r" oldvers))
			  (and newvers (concat "-r" newvers))
			  (vc-switches 'CVS 'diff))))
554 555
    (if async 1 status))) ; async diff, pessimistic assumption

André Spiegel's avatar
André Spiegel committed
556

557 558 559 560
(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)
561
    (if (vc-stay-local-p dir)
562 563 564 565 566 567 568 569 570 571
        ;; 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)
572
                 (vc-diff-internal ',f ',rev1 ',rev2))))))
573 574 575
      ;; cvs diff: use a single call for the entire tree
      (let ((coding-system-for-read
             (or coding-system-for-read 'undecided)))
576
        (apply 'vc-cvs-command "*vc-diff*" 1 nil "diff"
577 578
               (and rev1 (concat "-r" rev1))
               (and rev2 (concat "-r" rev2))
579
               (vc-switches 'CVS 'diff))))))
580

581 582 583 584 585 586 587 588 589 590 591
(defconst vc-cvs-annotate-first-line-re "^[0-9]")

(defun vc-cvs-annotate-process-filter (process string)
  (setq string (concat (process-get process 'output) string))
  (if (not (string-match vc-cvs-annotate-first-line-re string))
      ;; Still waiting for the first real line.
      (process-put process 'output string)
    (let ((vc-filter (process-get process 'vc-filter)))
      (set-process-filter process vc-filter)
      (funcall vc-filter process (substring string (match-beginning 0))))))

Eric S. Raymond's avatar
Eric S. Raymond committed
592
(defun vc-cvs-annotate-command (file buffer &optional revision)
André Spiegel's avatar
André Spiegel committed
593
  "Execute \"cvs annotate\" on FILE, inserting the contents in BUFFER.
Eric S. Raymond's avatar
Eric S. Raymond committed
594
Optional arg REVISION is a revision to annotate from."
595
  (vc-cvs-command buffer
596
                  (if (vc-stay-local-p file)
597 598
		      'async 0)
                  file "annotate"
Eric S. Raymond's avatar
Eric S. Raymond committed
599
                  (if revision (concat "-r" revision)))
600 601 602 603 604 605 606 607 608 609 610
  ;; Strip the leading few lines.
  (let ((proc (get-buffer-process buffer)))
    (if proc
        ;; If running asynchronously, use a process filter.
        (progn
          (process-put proc 'vc-filter (process-filter proc))
          (set-process-filter proc 'vc-cvs-annotate-process-filter))
      (with-current-buffer buffer
        (goto-char (point-min))
        (re-search-forward vc-cvs-annotate-first-line-re)
        (delete-region (point-min) (1- (point)))))))
611

612 613 614 615 616 617 618 619
(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)
Pavel Janík's avatar
Pavel Janík committed
620
systime, or nil if there is none."
621 622
  (let* ((bol (point))
         (cache (get-text-property bol 'vc-cvs-annotate-time))
623 624
         (inhibit-read-only t)
         (inhibit-modification-hooks t))
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
    (cond
     (cache)
     ((looking-at
       "^\\S-+\\s-+\\S-+\\s-+\\([0-9]+\\)-\\(\\sw+\\)-\\([0-9]+\\)): ")
      (let ((day (string-to-number (match-string 1)))
            (month (cdr (assq (intern (match-string 2))
                              '((Jan .  1) (Feb .  2) (Mar .  3)
                                (Apr .  4) (May .  5) (Jun .  6)
                                (Jul .  7) (Aug .  8) (Sep .  9)
                                (Oct . 10) (Nov . 11) (Dec . 12)))))
            (year (let ((tmp (string-to-number (match-string 3))))
                    ;; Years 0..68 are 2000..2068.
                    ;; Years 69..99 are 1969..1999.
                    (+ (cond ((> 69 tmp) 2000)
                             ((> 100 tmp) 1900)
                             (t 0))
                       tmp))))
        (put-text-property
         bol (1+ bol) 'vc-cvs-annotate-time
         (setq cache (cons
                      ;; Position at end makes for nicer overlay result.
                      (match-end 0)
                      (vc-annotate-convert-time
                       (encode-time 0 0 0 day month year))))))))
    (when cache
      (goto-char (car cache))           ; fontify from here to eol
      (cdr cache))))                    ; days (float)
Sam Steingold's avatar
Sam Steingold committed
652

653 654 655 656 657 658 659 660
(defun vc-cvs-annotate-extract-revision-at-line ()
  (save-excursion
    (beginning-of-line)
    (if (re-search-forward "^\\([0-9]+\\.[0-9]+\\(\\.[0-9]+\\)*\\) +("
			   (line-end-position) t)
	(match-string-no-properties 1)
      nil)))

André Spiegel's avatar
André Spiegel committed
661 662 663 664 665
;;;
;;; Snapshot system
;;;

(defun vc-cvs-create-snapshot (dir name branchp)
Eric S. Raymond's avatar
Eric S. Raymond committed
666
  "Assign to DIR's current revision a given NAME.
André Spiegel's avatar
André Spiegel committed
667 668
If BRANCHP is non-nil, the name is created as a branch (and the current
workspace is immediately moved to that new branch)."
669 670
  (vc-cvs-command nil 0 dir "tag" "-c" (if branchp "-b") name)
  (when branchp (vc-cvs-command nil 0 dir "update" "-r" name)))
André Spiegel's avatar
André Spiegel committed
671 672 673 674 675 676

(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*")
677 678
    (let ((default-directory dir)
	  (sticky-tag))
André Spiegel's avatar
André Spiegel committed
679 680
      (erase-buffer)
      (if (or (not name) (string= name ""))
681 682
	  (vc-cvs-command t 0 nil "update")
	(vc-cvs-command t 0 nil "update" "-r" name)
683
	(setq sticky-tag name))
André Spiegel's avatar
André Spiegel committed
684 685 686 687 688 689 690 691 692 693 694 695
      (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)
Eric S. Raymond's avatar
Eric S. Raymond committed
696
		    (vc-file-setprop file 'vc-working-revision nil)
André Spiegel's avatar
André Spiegel committed
697 698 699 700 701
		    (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)
Eric S. Raymond's avatar
Eric S. Raymond committed
702
		    (vc-file-setprop file 'vc-working-revision nil)
André Spiegel's avatar
André Spiegel committed
703
		    (vc-file-setprop file 'vc-checkout-time 0)))
704
		  (vc-file-setprop file 'vc-cvs-sticky-tag sticky-tag)
André Spiegel's avatar
André Spiegel committed
705 706 707
		  (vc-resynch-buffer file t t))))
	  (forward-line 1))))))

Sam Steingold's avatar
Sam Steingold committed
708

André Spiegel's avatar
André Spiegel committed
709 710 711 712
;;;
;;; Miscellaneous
;;;

713
(defalias 'vc-cvs-make-version-backups-p 'vc-stay-local-p
714
  "Return non-nil if version backups should be made for FILE.")
André Spiegel's avatar
André Spiegel committed
715 716 717 718 719 720 721 722

(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)))

Sam Steingold's avatar
Sam Steingold committed
723

André Spiegel's avatar
André Spiegel committed
724 725 726 727
;;;
;;; Internal functions
;;;

728
(defun vc-cvs-command (buffer okstatus files &rest flags)
729 730 731
  "A wrapper around `vc-do-command' for use in vc-cvs.el.
The difference to vc-do-command is that this function always invokes `cvs',
and that it passes `vc-cvs-global-switches' to it before FLAGS."
732
  (apply 'vc-do-command buffer okstatus "cvs" files
733
         (if (stringp vc-cvs-global-switches)
734 735 736 737
             (cons vc-cvs-global-switches flags)
           (append vc-cvs-global-switches
                   flags))))

738 739 740 741 742 743 744 745 746 747 748 749 750 751 752
(defalias 'vc-cvs-stay-local-p 'vc-stay-local-p)  ;Back-compatibility.

(defun vc-cvs-repository-hostname (dirname)
  "Hostname of the CVS server associated to workarea DIRNAME."
  (let ((rootname (expand-file-name "CVS/Root" dirname)))
    (when (file-readable-p rootname)
      (with-temp-buffer
	(let ((coding-system-for-read
	       (or file-name-coding-system
		   default-file-name-coding-system)))
	  (vc-insert-file rootname))
	(goto-char (point-min))
	(nth 2 (vc-cvs-parse-root
		(buffer-substring (point)
				  (line-end-position))))))))
753 754

(defun vc-cvs-parse-root (root)
755 756 757 758 759 760 761 762 763 764 765
  "Split CVS ROOT specification string into a list of fields.
A CVS root specification of the form
  [:METHOD:][[USER@]HOSTNAME:]/path/to/repository
is converted to a normalized record with the following structure:
  \(METHOD USER HOSTNAME CVS-ROOT).
The default METHOD for a CVS root of the form
  /path/to/repository
is `local'.
The default METHOD for a CVS root of the form
  [USER@]HOSTNAME:/path/to/repository
is `ext'.
766
For an empty string, nil is returned (invalid CVS root)."
767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811
  ;; Split CVS root into colon separated fields (0-4).
  ;; The `x:' makes sure, that leading colons are not lost;
  ;; `HOST:/PATH' is then different from `:METHOD:/PATH'.
  (let* ((root-list (cdr (split-string (concat "x:" root) ":")))
         (len (length root-list))
         ;; All syntactic varieties will get a proper METHOD.
         (root-list
          (cond
           ((= len 0)
            ;; Invalid CVS root
            nil)
           ((= len 1)
            ;; Simple PATH => method `local'
            (cons "local"
                  (cons nil root-list)))
           ((= len 2)
            ;; [USER@]HOST:PATH => method `ext'
            (and (not (equal (car root-list) ""))
                 (cons "ext" root-list)))
           ((= len 3)
            ;; :METHOD:PATH
            (cons (cadr root-list)
                  (cons nil (cddr root-list))))
           (t
            ;; :METHOD:[USER@]HOST:PATH
            (cdr root-list)))))
    (if root-list
        (let ((method (car root-list))
              (uhost (or (cadr root-list) ""))
              (root (nth 2 root-list))
              user host)
          ;; Split USER@HOST
          (if (string-match "\\(.*\\)@\\(.*\\)" uhost)
              (setq user (match-string 1 uhost)
                    host (match-string 2 uhost))
            (setq host uhost))
          ;; Remove empty HOST
          (and (equal host "")
               (setq host))
          ;; Fix windows style CVS root `:local:C:\\project\\cvs\\some\\dir'
          (and host
               (equal method "local")
               (setq root (concat host ":" root) host))
          ;; Normalize CVS root record
          (list method user host root)))))
André Spiegel's avatar
André Spiegel committed
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829

(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
830
                    "\\(RCS Version\\|RCS Revision\\|Repository revision\\):\
André Spiegel's avatar
André Spiegel committed
831 832
\[\t ]+\\([0-9.]+\\)"
                    nil t))
Eric S. Raymond's avatar
Eric S. Raymond committed
833
              (vc-file-setprop file 'vc-latest-revision (match-string 2)))
834
          (vc-file-setprop
835 836 837 838 839 840 841 842 843 844
           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))))))))
André Spiegel's avatar
André Spiegel committed
845 846 847 848

(defun vc-cvs-dir-state-heuristic (dir)
  "Find the CVS state of all files in DIR, using only local information."
  (with-temp-buffer
849
    (vc-cvs-get-entries dir)
André Spiegel's avatar
André Spiegel committed
850 851
    (goto-char (point-min))
    (while (not (eobp))
852 853
      ;; CVS-removed files are not taken under VC control.
      (when (looking-at "/\\([^/]*\\)/[^/-]")
André Spiegel's avatar
André Spiegel committed
854 855 856 857 858
	(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))))

859 860 861 862 863 864 865 866
(defun vc-cvs-get-entries (dir)
  "Insert the CVS/Entries file from below DIR into the current buffer.
This function ensures that the correct coding system is used for that,
which may not be the one that is used for the files' contents.
CVS/Entries should only be accessed through this function."
  (let ((coding-system-for-read (or file-name-coding-system
                                    default-file-name-coding-system)))
    (vc-insert-file (expand-file-name "CVS/Entries" dir))))
867

868 869 870 871 872 873 874
(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))))
875

Eric S. Raymond's avatar
Eric S. Raymond committed
876 877
(defun vc-cvs-valid-revision-number-p (tag)
  "Return non-nil if TAG is a valid revision number."
878 879
  (and (string-match "^[0-9]" tag)
       (not (string-match "[^0-9.]" tag))))
880 881

(defun vc-cvs-parse-sticky-tag (match-type match-tag)
882
  "Parse and return the sticky tag as a string.
883 884 885 886 887 888 889 890 891 892 893
`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
894
	  (cond
Juanma Barranquero's avatar
Juanma Barranquero committed
895
	   ;; Sticky Date tag.  Convert to a proper date value (`encode-time')
896
	   ((eq type 'date)
897 898
	    (string-match
	     "\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)"
899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
	     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)
920
		 (cond ((eq type 'date) (format-time-string
921 922 923 924 925
					 vc-cvs-sticky-date-format-string
					 tag))
		       ((eq type 'symbolic-name) tag)
		       ((eq type 'revision-number) tag)
		       (t nil)))
926
		((functionp vc-cvs-sticky-tag-display)
927 928 929 930 931
		 (funcall vc-cvs-sticky-tag-display tag type))
		(t nil)))

      (set-match-data data))))

André Spiegel's avatar
André Spiegel committed
932 933 934 935 936 937 938 939 940
(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)
Eric S. Raymond's avatar
Eric S. Raymond committed
941
    (vc-file-setprop file 'vc-working-revision "0")
André Spiegel's avatar
André Spiegel committed
942 943 944 945 946 947
    (if set-state (vc-file-setprop file 'vc-state 'edited)))
   ;; normal entry
   ((looking-at
     (concat "/[^/]+"
	     ;; revision
	     "/\\([^/]*\\)"
948 949
	     ;; timestamp and optional conflict field
	     "/\\([^/]*\\)/"
950 951 952 953 954
	     ;; options
	     "\\([^/]*\\)/"
	     ;; sticky tag
	     "\\(.\\|\\)" ;Sticky tag type (date or tag name, could be empty)
	     "\\(.*\\)"))		;Sticky tag
Eric S. Raymond's avatar
Eric S. Raymond committed
955
    (vc-file-setprop file 'vc-working-revision (match-string 1))
956
    (vc-file-setprop file 'vc-cvs-sticky-tag
957
		     (vc-cvs-parse-sticky-tag (match-string 4)
958
                                              (match-string 5)))
959 960
    ;; Compare checkout time and modification time.
    ;; This is intentionally different from the algorithm that CVS uses
961
    ;; (which is based on textual comparison), because there can be problems
962
    ;; generating a time string that looks exactly like the one from CVS.
963 964 965 966 967 968 969 970 971 972 973 974
    (let ((mtime (nth 5 (file-attributes file))))
      (require 'parse-time)
      (let ((parsed-time
	     (parse-time-string (concat (match-string 2) " +0000"))))
	(cond ((and (not (string-match "\\+" (match-string 2)))
		    (car parsed-time)
		    (equal mtime (apply 'encode-time parsed-time)))
	       (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)))))))))
975

Stefan Monnier's avatar
Stefan Monnier committed
976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994
;; Completion of revision names.
;; Just so I don't feel like I'm duplicating code from pcl-cvs, I'll use
;; `cvs log' so I can list all the revision numbers rather than only
;; tag names.

(defun vc-cvs-revision-table (file)
  (let ((default-directory (file-name-directory file))
        (res nil))
    (with-temp-buffer
      (vc-cvs-command t nil file "log")
      (goto-char (point-min))
      (when (re-search-forward "^symbolic names:\n" nil t)
        (while (looking-at "^	\\(.*\\): \\(.*\\)")
          (push (cons (match-string 1) (match-string 2)) res)
          (forward-line 1)))
      (while (re-search-forward "^revision \\([0-9.]+\\)" nil t)
        (push (match-string 1) res))
      res)))

995 996
(defun vc-cvs-revision-completion-table (files)
  (lexical-let ((files files)
Stefan Monnier's avatar
Stefan Monnier committed
997 998
                table)
    (setq table (lazy-completion-table
999
                 table (lambda () (vc-cvs-revision-table (car files)))))
Stefan Monnier's avatar
Stefan Monnier committed
1000 1001 1002
    table))
                                           

1003 1004
(provide 'vc-cvs)

Stefan Monnier's avatar
Stefan Monnier committed
1005
;; arch-tag: 60e1402a-aa53-4607-927a-cf74f144b432
1006
;;; vc-cvs.el ends here