Version Control

October 16, 2012

This isn’t a long program, but it is longer than most of the programs that we write, so we’ll present only part of it here, and refer you to the stored code at http://programmingpraxis.codepad.org/VSyK8tNp for the rest. We begin at the end, with the function that prints the directory; it reads each line in the history file and prints those that begin with a triple-asterisk, along with a count:

(define (dir hist-file-name)
  (with-input-from-file hist-file-name
    (lambda ()
      (let loop ((num 0) (line (read-line)))
        (when (not (eof-object? line))
          (cond ((and (< 3 (string-length line))
                      (string=? (substring line 0 3) "@@@"))
                  (display num) (display " ")
                  (display (substring line 4 (string-length line))) (newline)
                  (loop (+ num 1) (read-line)))
          (else (loop num (read-line)))))))))

Get is a little more involved. First it copies the current version of the file into a temporary file called base-file. Then, for as many versions back as the user requests, it collects the commands from the corresponding deltas in a second temporary file called cmd-file. When it is finished, it adds write and quit commands to the editor, calls system to execute the edit operation, and gracefully cleans up its temporary files.

(define (get file-name hist-file-name version)
  (cond ((not (file-exists? hist-file-name))
          (error 'get "missing history file"))
  (else (when (file-exists? file-name) (delete-file file-name))
        (with-input-from-file hist-file-name (lambda ()
          (let ((base-file (mktemp "get")))
            (with-output-to-file base-file (lambda ()
              (read-line) ; throw away header
              (let loop ((line (read-line)))
                (unless (or (eof-object? line)
                            (and (< 3 (string-length line))
                                 (string=? (substring line 0 3) "@@@")))
                  (display line) (newline) (loop (read-line))))))
            (let ((cmd-file (mktemp "get")))
              (with-output-to-file cmd-file (lambda ()
                (let loop ((version version))
                  (when (positive? version)
                    (when (eof-object? (peek-char))
                      (delete-file base-file) (delete-file cmd-file)
                      (error 'get "version doesn't exist"))
                    (let loop ((line (read-line)))
                      (unless (or (eof-object? line)
                                  (and (< 3 (string-length line))
                                       (string=? (substring line 0 3) "@@@")))
                        (display line) (newline) (loop (read-line))))
                    (loop (- version 1))))
                (display "w ") (display file-name) (newline)
                (display "q") (newline)))
              (system (string-append "ed -s " base-file " <" cmd-file))
              (delete-file base-file) (delete-file cmd-file))))))))

The last command is put. Again, the command is conceptually simple, but the code is rather less clear. First get is called to get the previous version of the file, and its header is saved. Then the new version of the file is written to a temporary history file, the saved header and the difference from the new version to the previous version are written to the temporary history file, and the rest of the history file is then appended. Finally the temporary history file replaces the old version of the history file and the previous version of the file is deleted:

(define (put file-name hist-file-name whoami date comment)
  (cond ((not (file-exists? hist-file-name))
          (with-output-to-file hist-file-name (lambda ()
            (display "@@@ ") (display whoami) (display " ")
            (display date) (display " ") (display comment) (newline)
            (with-input-from-file file-name (lambda ()
              (let loop ((line (read-line)))
                (unless (eof-object? line)
                  (display line) (newline) (loop (read-line)))))))))
  (else (let ((old-file (mktemp "put"))
              (header (with-input-from-file hist-file-name
                        (lambda () (read-line)))))
          (get old-file hist-file-name 0)
          (let ((new-hist-file (mktemp "put")))
            (with-output-to-file new-hist-file (lambda ()
              (display "@@@ ") (display whoami) (display " ")
              (display date) (display " ") (display comment) (newline)
              (with-input-from-file file-name (lambda ()
                (let loop ((line (read-line)))
                  (unless (eof-object? line)
                    (display line) (newline) (loop (read-line))))))
              (display header) (newline)
              (with-input-from-shell
                (string-append "diff -e " file-name " " old-file)
                (lambda ()
                  (let loop ((line (read-line)))
                    (unless (eof-object? line)
                      (display line) (newline) (loop (read-line))))))
              (let ((count (with-input-from-shell
                             (string-append "wc -l <" old-file)
                             (lambda () (string->number (read-line))))))
                (with-input-from-file hist-file-name (lambda ()
                  (let loop ((count (+ count 1)))
                    (when (positive? count) (read-line) (loop (- count 1))))
                  (let loop ((line (read-line)))
                    (unless (eof-object? line)
                      (display line) (newline) (loop (read-line)))))))))
            (system (string-append "mv " new-hist-file " " hist-file-name))
            (delete-file old-file))))))

We use read-line, string-split and string-join from the Standard Prelude. Mktemp returns a temporary file name. And a macro called with-input-from-shell executes a command in the file system, making its output available to the Scheme program:

(define-syntax with-input-from-shell
  (syntax-rules ()
    ((with-input-from-shell cmd proc)
      (let ((temp-file (mktemp "shell")) (result #f))
        (system (string-append cmd " > " temp-file))
        (with-input-from-file temp-file
          (lambda () (set! result (proc))))
        (delete-file temp-file)
        result))))

We enclose these functions in a main program that processes command-line arguments and calls functions as appropriate; the details are specific to Chez Scheme, so we leave the code for codepad. The program is stored with three links; put, get and dir are all the same program. Lines beginning with a triple-asterisk will cause trouble, as will lines containing only a single period, but both are low-probability events, and working around them is more work than we care to do.

Our program is basically identical to one in Section 5.9 of The Unix Programming Environment by Brian W. Kernighan and Rob Pike, and you would do well to compare the two. Our program, in Scheme, is far clunkier than theirs, which is written as a collection of Bourne shell scripts; Kernighan and Pike use sed and awk to write simple, natural code.

Pages: 1 2

One Response to “Version Control”

Leave a comment