From 5ee7145ac7b2010d5ec98ea9ca010bffd76adb9e Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Wed, 12 Aug 2026 14:13:40 +0200 Subject: [PATCH] First setup with git cli utility --- .gitignore | 1 + info.rkt | 4 +- main.rkt | 27 ++++++- private/config.rkt | 55 ++++++++++++++ private/git-commands.rkt | 45 ++++++++++++ private/git-provider.rkt | 155 +++++++++++++++++++++++++++++++++++++++ private/utils.rkt | 4 + 7 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 private/config.rkt create mode 100644 private/git-commands.rkt create mode 100644 private/git-provider.rkt create mode 100644 private/utils.rkt diff --git a/.gitignore b/.gitignore index 4e4160d..1f4386b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /doc /scribblings/compiled /tests/compiled +/private/compiled /docs *~ diff --git a/info.rkt b/info.rkt index d814c88..c4d29fe 100644 --- a/info.rkt +++ b/info.rkt @@ -2,13 +2,13 @@ (define collection "git-cli") (define pkg-desc "Command-line-like Git operations for Racket, interface to the git cli command") -(define version "0.3.1") +(define version "0.3.5") (define pkg-authors '("Hans Dijkema")) (define license 'MIT) (define deps '("base" - ("simple-ini" #:version "0.3.3") + ("simple-ini") "racket-index" "scribble-lib" "racket-makefile" diff --git a/main.rkt b/main.rkt index 2385e5f..d68a9b4 100644 --- a/main.rkt +++ b/main.rkt @@ -1 +1,26 @@ -#lang racket/base \ No newline at end of file +#lang racket/base + +(require "private/git-provider.rkt" + "private/git-commands.rkt" + "private/config.rkt" + ) + +(provide git) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Provided commands +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (git command . args) + (cond + ([eq? command 'status] (cmd-git-status args)) + ([eq? command 'add] (cmd-git-add args)) + (else (error "Not supported git command '~a" command)) + ) + ) + +(define (git-status . args) + (cmd-git-status args)) + +(define (git-add . args) + (cmd-git-add args)) diff --git a/private/config.rkt b/private/config.rkt new file mode 100644 index 0000000..d7999d3 --- /dev/null +++ b/private/config.rkt @@ -0,0 +1,55 @@ +#lang racket/base + +(require simple-ini/class + simple-log + ) + +(provide cfg-get + cfg-set! + dbg-git + info-git + err-git + warn-git + fatal-git + sync-log-git + ) + +(sl-def-log git) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Internal state / functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define ini #f) + +(define (check-ini) + (when (eq? ini #f) + (set! ini (new ini% [file 'git-cli])))) + +(define mutex (make-semaphore 1)) + +(define-syntax critical + (syntax-rules () + ((_ b1 ...) + (dynamic-wind + (λ () (semaphore-wait mutex)) + (λ () b1 ...) + (λ () (semaphore-post mutex))) + ) + ) + ) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Provided functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (cfg-set! section key value) + (critical + (check-ini) + (send ini set! section key value))) + +(define (cfg-get section key default-value) + (critical + (check-ini) + (send ini get section key default-value))) + diff --git a/private/git-commands.rkt b/private/git-commands.rkt new file mode 100644 index 0000000..7937219 --- /dev/null +++ b/private/git-commands.rkt @@ -0,0 +1,45 @@ +#lang racket/base + +(require "git-provider.rkt" + racket/string + + ) + +(provide cmd-git-status + cmd-git-add + ) + +(define (cmd-git-status args) + (let ((output (run-git '(status -s)))) + (let-values (((result out) (git-out 'status output))) + (if result + (map (λ (line) + (let* ((state (string->symbol (string-trim (substring line 0 2)))) + (file (string-trim (substring line 3)))) + (cond + ([eq? state '??] (list 'new file)) + ([eq? state 'M] (list 'modified file)) + ([eq? state 'A] (list 'added file)) + ([eq? state 'D] (list 'deleted file)) + ([eq? state 'AM] (list 'modified-added file)) + ([eq? state 'AD] (list 'deleted-added file)) + (else + (git-error 'status "Unexpected state" state)) + ) + )) + out) + (git-error 'status "Error" out) + ) + ) + ) + ) + +(define (cmd-git-add args) + (let ((output (run-git (cons 'add args)))) + (let-values (((result out) (git-out 'add output))) + (if result + (begin + (git-displ out) + result) + (git-error 'add "Error" out))))) + \ No newline at end of file diff --git a/private/git-provider.rkt b/private/git-provider.rkt new file mode 100644 index 0000000..a3b9766 --- /dev/null +++ b/private/git-provider.rkt @@ -0,0 +1,155 @@ +#lang racket/base + +(require racket/path + racket/string + racket/contract + racket/system + "config.rkt" + ) + +(provide git-exe + set-git-exe! + run-git + git-out + git-error + git-displ + ) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Supporting functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define cached-git-exe #f) + +(define (ask-for-git-executable) + (displayln "Git was not found on PATH.") + (displayln "Enter the full path to git/git.exe, or press Enter to abort:") + (let loop () + (display "> ") + (flush-output) + (let ((answer (read-line))) + (when (or (eof-object? answer) + (string=? (string-trim answer) "")) + (error 'git "Git executable not found; configuration aborted")) + (let ((candidate (find-executable-path answer))) + (if (eq? candidate #f) + (begin + (displayln + (format "'~a' is not found, please try again or press Enter to abort." answer)) + (loop)) + (begin + (set-git-exe! candidate) + #t))))) + ) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Provided functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define/contract (git-exe) + (-> (or/c path? #f)) + (if (eq? cached-git-exe #f) + (let ((the-git-exe (let ((exe (cfg-get 'git 'exe #f))) + (if (eq? exe #f) + (let ((path (find-executable-path "git"))) + (if (eq? path #f) + (if (ask-for-git-executable) + (git-exe) + #f) + path)) + exe + )))) + (set! cached-git-exe the-git-exe) + the-git-exe) + cached-git-exe)) + +(define/contract (set-git-exe! exe-path) + (-> path? void?) + (void + (begin + (cfg-set! 'git 'exe exe-path) + (set! cached-git-exe exe-path)))) + + +(define/contract (run-git args) + (-> (listof (or/c path-string? symbol?)) + (listof (list/c (one-of/c 'stdout 'stderr) string?))) + (let-values (((process stdout stdin stderr) + (apply subprocess + #f + #f + #f + (git-exe) + (map (lambda (arg) + (if (symbol? arg) + (symbol->string arg) + arg)) + args)))) + (close-output-port stdin) + (let ((output-channel (make-channel))) + (define (read-output source port) + (thread + (lambda () + (let loop () + (let ((line (read-line port))) + (channel-put output-channel (list source line)) + (if (eof-object? line) + (close-input-port port) + (loop))))))) + + (read-output 'stdout stdout) + (read-output 'stderr stderr) + + (let loop ((open-ports 2) + (result '())) + (if (= open-ports 0) + (begin + (subprocess-wait process) + (reverse result)) + (let* ((output (channel-get output-channel)) + (line (cadr output))) + (if (eof-object? line) + (loop (- open-ports 1) result) + (loop open-ports + (cons output result))))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Provided utility functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (is-output? e) + (or (eq? (car e) 'stdout) + (and (eq? (car e) 'stderr) + (string-prefix? (string-downcase (cadr e)) "warning:")))) + +(define (is-error? e) + (not (is-output? e))) + +(define (git-out cmd output) + (let* ((out (map (λ (e) (cadr e)) (filter (λ (e) (is-output? e)) output))) + (err (map (λ (e) (cadr e)) (filter (λ (e) (is-error? e)) output))) + (r (null? err)) + ) + (values r (if (eq? r #t) out err)))) + +(define-syntax git-error + (syntax-rules () + ((_ cmd msg* out) + (let ((msg (format "git ~a: ~a: ~a" cmd msg* (string-join + (map (λ (e) (format "~a" e)) + (if (list? out) + out + (list out))) + "\n")))) + (err-git msg) + (error 'git msg)) + ) + ) + ) + +(define (git-displ out) + (let ((str (if (string? out) out (string-join out "\n")))) + (info-git str) + (when (cfg-get 'git 'display-output #t) + (displayln str)))) \ No newline at end of file diff --git a/private/utils.rkt b/private/utils.rkt new file mode 100644 index 0000000..d3b5556 --- /dev/null +++ b/private/utils.rkt @@ -0,0 +1,4 @@ +#lang racket/base + +(provide config) +