;; ;; util-git.scm ;; ;; Support for simple git integration. ;; ;; ISC License ;; ;; Copyright 2023 Brmlab, z.s. ;; Dominik Pantůček ;; ;; Permission to use, copy, modify, and/or distribute this software ;; for any purpose with or without fee is hereby granted, provided ;; that the above copyright notice and this permission notice appear ;; in all copies. ;; ;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL ;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED ;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE ;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR ;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS ;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, ;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN ;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ;; (declare (unit util-git)) (module util-git ( git git-status git-blame ) (import scheme (chicken base) (chicken string) util-io util-dict-list util-parser util-time) ;; Valid git operating modes (define git-modes '((#:exit exit) (#:output output) (#:exit+output exit+output))) ;; Used for actual invocation of git binary, returns two values: exit ;; code and output lines (define (invoke-git repo args) (apply get-process-exit+output-lines "git" "-C" repo args)) ;; Curried git repo command wrapper (define (git repo . defargs) (let ((defmode (if (null? defargs) 'output ;; Raises an error if not valid (cadr (assq (car defargs) git-modes))))) (case defmode ((exit) (lambda args (let-values (((exit-code _) (invoke-git repo args))) exit-code))) ((output) (lambda args (let-values (((_ output) (invoke-git repo args))) output))) ((exit+output) (lambda args (invoke-git repo args)))))) ;; Known status types (define git-status-types '((" M" modified) ("??" untracked))) ;; Returns a dictionary of unknown, modified, deleted and added files (define (git-status repo) (let* ((lines ((git repo) 'status '--porcelain)) (clean? (null? lines))) (let loop ((lines lines) (res (make-ldict `((clean . ,clean?))))) (if (null? lines) res (let* ((line (car lines)) (st (substring line 0 2)) (fname (substring line 3)) (maybe-status (assoc st git-status-types)) (status (if maybe-status (cadr maybe-status) 'unknown))) (loop (cdr lines) (ldict-set res status (cons fname (ldict-ref res status '()))))))))) ;; Returns detailed file annotation with each line being represented ;; by dictionary with keys from git output (define (git-blame repo fname) (let loop ((lines ((git repo) 'blame '--line-porcelain fname)) (blame (make-ldict)) (blames '())) (if (null? lines) (reverse (if (ldict-empty? blame) blames (cons blame blames))) (let ((line (car lines))) (cond ((ldict-empty? blame) ;; First row - commit (let ((ll (string-split line))) (loop (cdr lines) (ldict-set blame 'commit (car ll)) blames))) ((memq (string-ref line 0) '(#\space #\tab)) ;; Actual line with data (let ((rline (substring line 1))) (loop (cdr lines) (make-ldict) (cons (ldict-set blame 'line rline) blames)))) (else ;; Any header (let ((kv (parser-parse-line line))) (if (pair? kv) (let* ((k (car kv)) (v (cdr kv)) (v1 (case k ((committer-time) (seconds->iso-date-string (string->number v))) (else v)))) (loop (cdr lines) (ldict-set blame k v1) blames)) (loop (cdr lines) blame blames))))))))) )