;;; CVS (Concurrent Versioning System) interface ;;; http://www.cyclic.com ;;; http://www.sourcegear.com/CVS ;;; ;;; Copyright (C) 1996 by Bruno Haible ;;; Copyright (C) 1998-2000 by Sam Steingold ;;; This is Free Software, covered by the GNU GPL (v2) ;;; See http://www.gnu.org/copyleft/gpl.html ;;; ;;; $Id: cvs.lisp,v 2.19 2002/03/27 23:24:52 sds Exp $ ;;; $Source: /cvsroot/clocc/clocc/src/cllib/cvs.lisp,v $ (eval-when (compile load eval) (require :cllib-base (translate-logical-pathname "clocc:src;cllib;base")) ;; `string-beg-with', `substitute-subseq' (require :cllib-string (translate-logical-pathname "cllib:string")) ;; `skip-to-line', `read-list-from-stream', `file-size-t' (require :cllib-fileio (translate-logical-pathname "cllib:fileio")) ;; `string->dttm', `dttm->string' (require :cllib-date (translate-logical-pathname "cllib:date")) ;; `hash-table->alist' (require :cllib-miscprint (translate-logical-pathname "cllib:miscprint")) ;; `top-bottom-ui' (require :cllib-sorted (translate-logical-pathname "cllib:sorted")) ;; `default-directory', `pathname-ensure-name', `probe-directory' (require :port-sys (translate-logical-pathname "port:sys")) ;; `with-open-pipe' (require :port-shell (translate-logical-pathname "port:shell"))) (in-package :cllib) (export '(cvs-diff2patch cvs-stat-log cvs-change-root)) ;;; ;;; CVS diff ---> patch ;;; ;; Bruno Haible originally wrote this in 1996 as a filter. ;; Sam Steingold fixed some bugs in 1998 and ;; converted to the generic function in 2000. (eval-when (compile load eval) (fmakunbound 'cvs-diff2patch)) ;;;###autoload (defgeneric cvs-diff2patch (in out) (:documentation "Convert a CVS diff to a patchable diff.") (:method ((in stream) (out stream)) (do* ((line (read-line in nil nil) (read-line in nil nil)) (len (length line) (length line)) path base) ((null line)) (declare (simple-string line) (type index-t len)) (cond ((or (string-beg-with "? " line len) ; skip (string-beg-with "==" line len) (string-beg-with "RCS" line len) (string-beg-with "diff" line len) (string-beg-with "retrieving" line len))) ((string-beg-with "Index: " line len) (setq path (subseq line 7) base (subseq path (1+ (or (position #\/ path :from-end t) -1))))) ((and (>= len (+ 4 (length base))) (string= line "*** " :end1 4) (or (string= line base :start1 4 :end1 (+ 4 (length base))) (string= line "/tmp/" :start1 4 :end1 9))) (format out "*** ~a~a~%" path (subseq line (position #\Tab line)))) ((and (>= len (+ 4 (length base))) (string= line "--- " :end1 4) (or (string= line base :start1 4 :end1 (+ 4 (length base))) (string= line "/tmp/" :start1 4 :end1 9))) (format out "--- ~a~a~%" path (subseq line (position #\Tab line)))) ((write-line line out))))) (:method ((in string) (out t)) (cvs-diff2patch (pathname in) out)) (:method ((in t) (out string)) (cvs-diff2patch in (pathname out))) (:method ((in pathname) (out t)) (with-open-file (istream in :direction :input) (format t "~&~s: reading: ~s [~:d bytes]~%" in) (cvs-diff2patch istream out))) (:method ((in t) (out pathname)) (with-open-file (ostream out :direction :output) (format t "~&~s: writing: ~s~%" out) (cvs-diff2patch in ostream)))) ;;; ;;; CVS log stat ;;; (defstruct (revision #+cmu (:print-function print-struct-object)) (rev "" :type string) (time 0 :type integer) (author "" :type string) (lines+ 0 :type index-t) (lines- 0 :type index-t) (log nil :type list)) ; of strings (defstruct (cvs-file (:conc-name cvsf-) #+cmu (:print-function print-struct-object)) (rcs "" :type string) (work "" :type string) (head "" :type string) (size 0 :type file-size-t) (tot-rev 0 :type index-t) (revs nil :type list)) ; of revisions (defparameter *cvs-log-sep-1* (make-string 28 :initial-element #\-)) (defparameter *cvs-log-sep-2* (make-string 77 :initial-element #\=)) (defun cvs-read-change (in ra) "Read a CHANGE from a stream. Suitable for `read-list-from-stream'." (declare (stream in) (symbol ra)) (unless (eq ra :revision) (error "~s: read-ahead is `~s' (`~s' expected)" 'cvs-read-change ra :revision)) (flet ((extract (line label) (let ((pos (search label line :test #'char=))) (when pos (subseq line (+ (length label) pos) (position #\; line :start pos :test #'char=)))))) (let* ((rev (read-line in)) (id (read-line in)) fin (lines (extract id "lines: ")) (lines+- (and lines (string-tokens lines)))) (values (make-revision :log (loop :for line = (read-line in) :do (setq fin (string= line *cvs-log-sep-2*)) :until (or fin (string= line *cvs-log-sep-1*)) :collect line) :rev rev :time (string->dttm (extract id "date: ")) :author (extract id "author: ") :lines+ (or (car lines+-) 0) :lines- (abs (or (cadr lines+-) 0))) (if fin +eof+ (read in)))))) (defun cvs-read-file (in ra) "Read a CVS-FILE from a stream. Suitable for `read-list-from-stream'." (declare (stream in) (symbol ra)) (loop :while (eq ra :cvs) :do (read-line in) (setq ra (read in))) (unless (eq ra :rcs) (error "~s: read-ahead is `~s' (`~s' or `~s' expected)" 'cvs-read-file ra :rcs :cvs)) (flet ((from-colon (line) (subseq line (+ 2 (position #\: line :test #'char=))))) (let* ((rcs (from-colon (read-line in))) (work (from-colon (read-line in))) (head (from-colon (read-line in))) (tot-rev-l (skip-to-line in "total revisions:" nil)) (p0 (position #\; tot-rev-l :test #'char=)) (p1 (position #\: tot-rev-l :test #'char= :from-end t)) (tot-rev (parse-integer tot-rev-l :end p0)) (sel-rev (parse-integer tot-rev-l :start (1+ p1))) (revs (progn (skip-to-line in *cvs-log-sep-1* nil) (read-list-from-stream in #'cvs-read-change))) (path (merge-pathnames (pathname-ensure-name work) (directory-namestring in)))) (unless (= tot-rev sel-rev) (warn "total revision (~d) != selected revisions (~d)" tot-rev sel-rev)) (unless (= tot-rev (length revs)) (warn "total revision (~d) != number of revisions (~d)" tot-rev (length revs))) (values (make-cvs-file :revs revs :work work :head head :tot-rev tot-rev :rcs rcs :size (if (probe-file path) (file-size path) 0)) (read in nil +eof+))))) (defun cvs-read-log (path) "Read CVS log, return a list of CVS-FILE structures." (with-timing (:done t) (with-open-file (in path :direction :input) (format t "~&~s: ~a [~:d bytes]..." 'cvs-read-log path (file-length in)) (force-output) (loop :while (char= #\? (peek-char nil in)) :do (read-line in)) (read-list-from-stream in #'cvs-read-file)))) (defsubst rev-lines (rr) (declare (type revision rr)) (+ (revision-lines+ rr) (revision-lines- rr))) (defsubst cvsf-lines (ff) (declare (type cvs-file ff)) (reduce #'+ (cvsf-revs ff) :key #'rev-lines)) (defsubst cvsf-list-size (fl) (declare (type list fl)) (reduce #'+ fl :key #'cvsf-size)) ;;; ;;; stat by the author ;;; (eval-when (compile load eval) ; CMUCL (defstruct (author #+cmu (:print-function print-struct-object)) (name "" :type string) (owns nil :type list) ; list of files owned (mods nil :type list) ; list of files modified (revs nil :type list)) ; list of revisions ) (defun author-lines (au) (reduce #'+ (author-revs au) :key #'rev-lines)) (defmethod print-object ((au author) (out stream)) (if *print-readably* (call-next-method) (format out "[~a: owns: ~:d mods: ~:d revs: ~:d lines: ~:d]" (author-name au) (length (author-owns au)) (length (author-mods au)) (length (author-revs au)) (author-lines au)))) (defun cvs-stat-files (fl) "Generate and print some statistics about a list of CVS-FILE structs." (let ((aht (make-hash-table :test 'equal)) aul) (format t "~:d file~:p (~:d byte~:p), ~:d revision~:p, ~:d line~:p changed~%" (length fl) (cvsf-list-size fl) (reduce #'+ fl :key #'cvsf-tot-rev) (reduce #'+ fl :key #'cvsf-lines)) (dolist (ff fl) (do ((rr (cvsf-revs ff) (cdr rr)) au re na) ((null rr)) (setq re (car rr) na (revision-author re) au (or (gethash na aht) (setf (gethash na aht) (make-author :name na)))) (push re (author-revs au)) (pushnew ff (author-mods au) :key #'cvsf-rcs) (when (null (cdr rr)) (push ff (author-owns au))))) (setq aul (sort (mapcar #'cdr (cdr (hash-table->alist aht))) #'< :key (compose length author-revs))) (format t "name~10t ~21@a ~10@a ~10@a ~15@a~%" "owns: files bytes" "modified" "revisions" "lines changed") (dolist (au aul) (format t "~a~10t ~5:d ~15:d ~10:d ~10:d ~15:d~%" (author-name au) (length (author-owns au)) (cvsf-list-size (author-owns au)) (length (author-mods au)) (length (author-revs au)) (author-lines au))) (format t "total~10t ~5:d ~15:d ~10:d ~10:d ~15:d~%" (reduce #'+ aul :key (compose length author-owns)) (reduce #'+ aul :key (compose cvsf-list-size author-owns)) (reduce #'+ aul :key (compose length author-mods)) (reduce #'+ aul :key (compose length author-revs)) (reduce #'+ aul :key #'author-lines)) (values aul aht))) ;;;###autoload (defun cvs-stat-log (path) "Generate and print some statistics of the CVS repository." (when (char= #\/ (char path (1- (length path)))) (with-timing () (format t "~&~s: ~a is a directory, running `cvs log`..." 'cvs-stat-log path) (force-output) (let ((old-path (default-directory))) (setf (default-directory) path) (setq path (merge-pathnames "cvs.log" path)) (unwind-protect (with-open-pipe (pipe (pipe-input "cvs" "-q" "log")) (with-open-file (log path :direction :output :if-exists :supersede) (loop :for line = (read-line pipe nil nil) :while line :do (write-line line log)) (format t "done [~:d byte~:p]" (file-length log)))) (setf (default-directory) old-path))))) (format t "~a: " path) (let ((logl (cvs-read-log path))) (cvs-stat-files logl) (format t "~2& -- by the number of revisions --~%") (top-bottom-ui logl 10 nil nil :key #'cvsf-tot-rev :label #'cvsf-work) (format t "~2& -- the most recently modified --~%") (top-bottom-ui logl 10 nil nil :key (compose revision-time car cvsf-revs) :label #'cvsf-work :klabel #'dttm->string))) ;;; ;;; change the repository of the current sandbox ;;; ;;;###autoload (defun cvs-change-root (root substitutions &key (dry-run nil) (log t)) "Change Root and Repository files in the CVS directories under ROOT. When `DRY-RUN' is non-NIL, no actual changes are done." (flet ((change-one-line (file substitutions) (let ((line (with-open-file (in file) (read-line in)))) (mesg :log log "~s:~%" file) (dolist (subst substitutions) (mesg :log log " - ~s:~%" line) (setq line (substitute-subseq line (car subst) (cdr subst)))) (mesg :log log " * ~s~%" line) (unless dry-run (with-open-file (out file :direction :output #+clisp :external-format #+clisp :unix :if-exists :supersede) (write-line line out)))))) (let ((root-cvs (mk-path root :directory '(:relative "CVS"))) (root-subdirs (mk-path root :directory '(:relative "*")))) (when (probe-directory root-cvs) (change-one-line (merge-pathnames "Repository" root-cvs) substitutions) (change-one-line (merge-pathnames "Root" root-cvs) substitutions)) ;; CMUCL's `directory' is buggy - won't work! (dolist (dir (directory root-subdirs)) (unless (string-equal "CVS" (car (last (pathname-directory dir)))) (cvs-change-root dir substitutions :dry-run dry-run :log log)))))) (provide :cllib-cvs) ;;; file cvs.lisp ends here