Last active
February 6, 2025 15:14
-
-
Save death/468f2bb4c0b6c383697ca2cc8f15e064 to your computer and use it in GitHub Desktop.
Update Lisp repos
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;; A GUI for fetching all third party git Lisp repos. | |
| ;; | |
| ;; Kinda silly and limited, but what the hell. | |
| (defpackage #:snippets/clim-repos | |
| (:use #:clim-lisp #:clim) | |
| (:shadow | |
| #:merge) | |
| (:import-from | |
| #:uiop) | |
| (:import-from | |
| #:alexandria) | |
| (:import-from | |
| #:legit) | |
| (:import-from | |
| #:bordeaux-threads) | |
| (:import-from | |
| #:lparallel) | |
| (:export | |
| #:run)) | |
| (in-package #:snippets/clim-repos) | |
| ;;;; Repo | |
| (defclass repo () | |
| ((directory :initarg :directory :reader repo-directory) | |
| (status :initform :idle :accessor repo-status))) | |
| (defmethod repo-name ((repo repo)) | |
| (alexandria:last-elt (pathname-directory (repo-directory repo)))) | |
| (defmethod print-object ((repo repo) stream) | |
| (print-unreadable-object (repo stream :type t) | |
| (format stream "~S" (repo-name repo))) | |
| repo) | |
| (defmacro repo-command (repo interest form) | |
| (multiple-value-bind (speakup shutup) | |
| (ecase interest | |
| (:errors (values 'legit:*git-errors* 'legit:*git-output*)) | |
| (:output (values 'legit:*git-output* 'legit:*git-errors*))) | |
| `(let ((,speakup (make-string-output-stream)) | |
| (,shutup (make-broadcast-stream))) | |
| (legit:with-chdir ((repo-directory ,repo)) | |
| ,form | |
| (get-output-stream-string ,speakup))))) | |
| (defmethod fetch ((repo repo)) | |
| (not | |
| (alexandria:emptyp | |
| (repo-command repo :errors (legit:git-fetch))))) | |
| (defmethod merge ((repo repo)) | |
| ;; FIXME: This is actually pulling, not merging. | |
| (repo-command repo :errors (legit:git-pull))) | |
| (defmethod update-status ((repo repo)) | |
| (let* ((display-text (repo-command repo :output (legit:git-status "."))) | |
| (new-status (cond ((search "fast-forwarded" display-text) | |
| :changes-available) | |
| (t | |
| :idle)))) | |
| (setf (repo-status repo) new-status))) | |
| ;;;; Repo Collection | |
| (defclass repo-collection () | |
| ((list :initarg :list :accessor repo-collection-list))) | |
| (defun list-files (base-directory) | |
| (directory | |
| (merge-pathnames | |
| (make-pathname :name :wild :type :wild) | |
| base-directory))) | |
| (defun list-directories (base-directory) | |
| (remove-if-not #'uiop:directory-pathname-p | |
| (list-files base-directory))) | |
| (defun git-repo-directory-p (directory) | |
| (probe-file | |
| (merge-pathnames | |
| (make-pathname :directory '(:relative ".git")) | |
| directory))) | |
| (defun list-repo-directories (base-directory) | |
| (remove-if-not #'git-repo-directory-p | |
| (list-directories base-directory))) | |
| (defun create-repo (directory) | |
| (make-instance 'repo :directory directory)) | |
| (defun create-repo-collection (base-directory) | |
| (let ((list (mapcar #'create-repo (list-repo-directories base-directory)))) | |
| (make-instance 'repo-collection :list list))) | |
| ;;;; Worker | |
| (defclass worker () | |
| ()) | |
| (defgeneric start (worker)) | |
| (defgeneric alivep (worker)) | |
| (defgeneric join (worker)) | |
| (defmethod start ((worker null))) | |
| (defmethod alivep ((worker null)) | |
| nil) | |
| (defmethod join ((worker null))) | |
| ;;;; Fetcher | |
| (defclass fetcher (worker) | |
| ((frame :initarg :frame :reader fetcher-frame) | |
| (repos :initarg :repos :reader fetcher-repos) | |
| (thread :initform nil :accessor fetcher-thread))) | |
| (defun fetcher-thread-function (fetcher) | |
| (let ((frame (fetcher-frame fetcher)) | |
| (repos (fetcher-repos fetcher))) | |
| (lp:pmapc (lambda (repo) | |
| (fetch-and-update frame repo)) | |
| :parts 4 | |
| repos))) | |
| (defmethod start ((fetcher fetcher)) | |
| (unless (alivep fetcher) | |
| (setf (fetcher-thread fetcher) | |
| (bt:make-thread (lambda () | |
| (fetcher-thread-function fetcher)) | |
| :name "Fetch All Worker")))) | |
| (defmethod alivep ((fetcher fetcher)) | |
| (let ((thread (fetcher-thread fetcher))) | |
| (and (bt:threadp thread) | |
| (bt:thread-alive-p thread)))) | |
| (defmethod join ((fetcher fetcher)) | |
| (when (alivep fetcher) | |
| (bt:join-thread (fetcher-thread fetcher)))) | |
| ;;;; UI | |
| (defclass main-view (view) | |
| ()) | |
| (defvar +main-view+ | |
| (make-instance 'main-view)) | |
| (defclass main-pane (application-pane) | |
| () | |
| (:default-initargs | |
| :display-function 'display-main | |
| :incremental-redisplay t | |
| :text-style (make-text-style :fix :roman :normal))) | |
| (define-application-frame repos () | |
| ((repo-collection :initarg :repo-collection | |
| :reader repo-collection) | |
| (fetcher :initform nil :accessor fetcher) | |
| (runningp :initform t :accessor runningp)) | |
| (:default-initargs | |
| :repo-collection (create-repo-collection "/home/death/quicklisp/third-party/")) | |
| (:panes | |
| (main (make-pane 'main-pane)) | |
| (interactor :interactor)) | |
| (:layouts | |
| (default | |
| (vertically () | |
| (9/10 (scrolling () main)) | |
| (+fill+ interactor))))) | |
| (defun list-repos (frame) | |
| (repo-collection-list (repo-collection frame))) | |
| (defun display-main (frame pane) | |
| ;; FIXME: MAX-WIDTH is hardcoded because mcclim doesn't redisplay | |
| ;; after window-configuration-event, and this sucks on a tiling | |
| ;; window manager. | |
| (formatting-item-list (pane :max-width 1920) | |
| (dolist (repo (list-repos frame)) | |
| (formatting-cell (pane) | |
| (updating-output (pane :unique-id repo | |
| :cache-value (repo-status repo)) | |
| (present repo 'repo :stream pane :view +main-view+)))))) | |
| (define-presentation-method present (repo (type repo) stream (view main-view) &key) | |
| (let* ((name (repo-name repo)) | |
| (status (repo-status repo))) | |
| (multiple-value-bind (foreground-ink background-ink) | |
| (ecase status | |
| (:pending (values +foreground-ink+ +light-yellow+)) | |
| (:changes-available (values +foreground-ink+ +light-blue+)) | |
| (:idle (values +foreground-ink+ +background-ink+))) | |
| (surrounding-output-with-border (stream :ink background-ink | |
| :background background-ink | |
| :padding 3) | |
| (with-drawing-options (stream :ink foreground-ink) | |
| (write-string name stream)))))) | |
| (define-presentation-method present (repo (type repo) stream (view textual-view) &key) | |
| (let ((name (repo-name repo))) | |
| (write-string name stream))) | |
| (defmethod run-frame-top-level :before ((frame repos) &key) | |
| (setf (runningp frame) t)) | |
| (defmethod run-frame-top-level :after ((frame repos) &key) | |
| (setf (runningp frame) nil) | |
| (join (fetcher frame))) | |
| (defun run () | |
| (run-frame-top-level | |
| (make-application-frame 'repos))) | |
| (define-repos-command (com-fetch :name t) | |
| ((repo 'repo)) | |
| (fetch repo) | |
| (update-status repo)) | |
| (define-presentation-to-command-translator fetch | |
| (repo com-fetch repos :gesture nil) | |
| (repo) | |
| (list repo)) | |
| (define-repos-command (com-merge :name t) | |
| ((repo 'repo)) | |
| (merge repo) | |
| (update-status repo)) | |
| (define-presentation-to-command-translator merge | |
| (repo com-merge repos :gesture nil) | |
| (repo) | |
| (list repo)) | |
| (define-repos-command (com-fetch-all :name t :menu t) | |
| () | |
| (let ((frame *application-frame*)) | |
| (unless (alivep (fetcher frame)) | |
| (let ((repos (list-repos frame))) | |
| (dolist (repo repos) | |
| (setf (repo-status repo) :pending)) | |
| (setf (fetcher frame) (make-instance 'fetcher :frame frame :repos repos)) | |
| (start (fetcher frame)))))) | |
| (defclass repo-update (climi::standard-event) | |
| ((repo :initarg :repo :reader event-repo))) | |
| (defun queue-repo-update-event (frame repo) | |
| (queue-event (frame-top-level-sheet frame) | |
| (make-instance 'repo-update | |
| :repo repo | |
| :sheet (get-frame-pane frame 'main)))) | |
| (defmethod handle-event ((pane main-pane) (event repo-update)) | |
| ;; FIXME: Redisplaying the whole pane is kinda slow; we could | |
| ;; recreate the presentation. | |
| (redisplay-frame-pane (pane-frame pane) pane)) | |
| (defmacro with-errors-as-warnings (&body body) | |
| `(handler-case (progn ,@body) | |
| (error (e) | |
| (warn "Error: ~A" e)))) | |
| (defun fetch-and-update (frame repo) | |
| (flet ((check-running () | |
| (unless (runningp frame) | |
| (return-from fetch-and-update)))) | |
| (check-running) | |
| (with-errors-as-warnings | |
| (fetch repo) | |
| (check-running) | |
| (update-status repo)) | |
| (check-running) | |
| (queue-repo-update-event frame repo))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment