;;; guix-forge --- Guix software forge meta-service
;;; Copyright © 2023–2025 Arun Isaac <arunisaac@systemreboot.net>
;;;
;;; This file is part of guix-forge.
;;;
;;; guix-forge is free software: you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published
;;; by the Free Software Foundation, either version 3 of the License,
;;; or (at your option) any later version.
;;;
;;; guix-forge is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;;; General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with guix-forge. If not, see
;;; <https://www.gnu.org/licenses/>.
(define-module (forge cgit)
#:use-module (forge environment)
#:use-module (forge fcgiwrap)
#:use-module ((forge git) #:select (git-without-safe-directory-check))
#:use-module (forge nginx)
#:use-module (forge socket)
#:use-module ((gnu packages emacs) #:select (emacs-minimal))
#:use-module ((gnu packages mail) #:select (mailcap))
#:use-module ((gnu packages version-control)
#:select (cgit-pink git-minimal))
#:use-module (gnu services)
#:use-module ((gnu services web) #:select (nginx-server-configuration
nginx-location-configuration))
#:use-module (gnu system file-systems)
#:use-module (guix gexp)
#:use-module (guix records)
#:use-module (ice-9 match)
#:use-module (srfi srfi-26)
#:export (%cgit-readme
<cgit-configuration>
cgit-configuration
cgit-configuration?
cgit-configuration-cgit
cgit-configuration-git
cgit-configuration-server-name
cgit-configuration-repository-directory
cgit-configuration-socket
cgit-configuration-readme
cgit-configuration-snapshots
cgit-configuration-about-filter
cgit-configuration-commit-filter
cgit-configuration-email-filter
cgit-configuration-source-filter
cgit-configuration-mimetype-file
cgit-configuration-repository-sort
cgit-configuration-plain-email?
cgit-configuration-extra-options
cgit-service-type))
(define %cgit-readme
(append (list "README.md" "README.markdown" "README.mdown" "README.mkd"
"README.org" "README.rst")
;; man page READMEs
(map (lambda (n)
(string-append "README." (number->string n)))
(iota 9 1))
(list "README.htm" "README.html"
"README.txt"
"README")))
(define-record-type* <cgit-configuration>
cgit-configuration make-cgit-configuration
cgit-configuration?
this-cgit-configuration
(cgit cgit-configuration-cgit
(default cgit-pink))
(git cgit-configuration-git
(default git-without-safe-directory-check))
(server-name cgit-configuration-server-name)
(repository-directory cgit-configuration-repository-directory
(default "/srv/git"))
(socket cgit-configuration-socket
(default (forge-unix-socket
(path "/var/run/fcgiwrap/cgit/socket"))))
;; TODO: Support org mode and gemtext READMEs too.
(readme cgit-configuration-readme
(default %cgit-readme))
(snapshots cgit-configuration-snapshots
(default (list "tar.gz"))
(sanitize sanitize-cgit-snapshots))
(about-filter cgit-configuration-about-filter
(default (program-file "about-filter"
(about-filter-gexp this-cgit-configuration)))
(thunked))
(commit-filter cgit-configuration-commit-filter
(default #f))
(email-filter cgit-configuration-email-filter
(default #f))
(source-filter cgit-configuration-source-filter
(default (file-append (cgit-configuration-cgit
this-cgit-configuration)
"/lib/cgit/filters/syntax-highlighting.py"))
(thunked))
(mimetype-file cgit-configuration-mimetype-file
(default (file-append mailcap "/etc/mime.types")))
(repository-sort cgit-configuration-repository-sort
(default 'age)
(sanitize sanitize-cgit-repository-sort))
(plain-email? cgit-configuration-noplainemail?
(default #false))
(extra-options cgit-configuration-extra-options
(default '())))
(define (sanitize-cgit-snapshots snapshots)
(let ((valid-snapshots (list "tar" "tar.gz" "tar.bz2" "tar.lz"
"tar.xz" "tar.zst" "zip")))
(for-each (lambda (snapshot)
(unless (member snapshot valid-snapshots)
(leave (G_ "Snapshot ~a is not one of ~s.~%"
snapshot
valid-snapshots))))
snapshots)
snapshots))
(define (sanitize-cgit-repository-sort repository-sort)
(case repository-sort
((name age) repository-sort)
(else
(leave (G_ "Invalid cgit repository-sort '~s. It should either be 'age or 'name.~%")
repository-sort))))
(define (about-filter-gexp config)
"Return G-expression for a cgit about-filter. Use cgit package
configured in @var{config}."
(match-record config <cgit-configuration>
(cgit)
(with-imported-modules '((guix build utils))
#~(begin
(use-modules (guix build utils)
(ice-9 match)
(srfi srfi-1))
(match (command-line)
((_ about-file)
(cond
((or (string-suffix-ci? ".markdown" about-file)
(string-suffix-ci? ".mdown" about-file)
(string-suffix-ci? ".md" about-file)
(string-suffix-ci? ".mkd" about-file))
(invoke #$(file-append cgit "/lib/cgit/filters/html-converters/md2html")
about-file))
((string-suffix-ci? ".org" about-file)
(invoke #$(file-append emacs-minimal "/bin/emacs")
"--script"
#$(local-file "org2html.el")))
((string-suffix-ci? ".rst" about-file)
(invoke #$(file-append cgit "/lib/cgit/filters/html-converters/rst2html")
about-file))
((any (lambda (n)
(string-suffix-ci? (string-append "." (number->string n))
about-file))
(iota 9 1))
(invoke #$(file-append cgit "/lib/cgit/filters/html-converters/rst2html")
about-file))
((or (string-suffix-ci? ".htm" about-file)
(string-suffix-ci? ".html" about-file))
(put-bytevector (current-output-port)
(call-with-input-file about-file
get-bytevector-all)))
(else
(invoke #$(file-append cgit "/lib/cgit/filters/html-converters/txt2html")
about-file)))))))))
(define (cgitrc-gexp config)
"Return G-expression to serialize @var{config} into cgitrc."
(match-record config <cgit-configuration>
(repository-directory
readme snapshots
about-filter commit-filter email-filter source-filter
mimetype-file repository-sort plain-email? extra-options)
#~(begin
(use-modules (ice-9 match))
(call-with-output-file #$output
(lambda (port)
(for-each (match-lambda
((key #t)
(format port "~a=1~%" key))
((key #f)
(format port "~a=0~%" key))
((key value)
(format port "~a=~a~%" key value)))
'#$(append (map (lambda (file)
(list "readme" (string-append ":" file)))
readme)
(if about-filter
`(("about-filter" ,about-filter))
(list))
(if commit-filter
`(("commit-filter" ,commit-filter))
(list))
(if email-filter
`(("email-filter" ,email-filter))
(list))
(if source-filter
`(("source-filter" ,source-filter))
(list))
(if mimetype-file
`(("mimetype-file" ,mimetype-file))
(list))
;; The order of settings in cgitrc is
;; significant.
`(("snapshots" ,(string-join snapshots))
("repository-sort" ,(symbol->string repository-sort))
("noplainemail" ,(not plain-email?))
("enable-index-owner" #f)
("virtual-root" "/")
,@(map (match-lambda
((key . value)
(list key value)))
extra-options)
("scan-path" ,repository-directory)))))))))
(define (cgit-fcgiwrap-instance config)
(match-record config <cgit-configuration>
(cgit git repository-directory)
(let ((cgitrc (computed-file "cgitrc" (cgitrc-gexp config))))
(fcgiwrap-instance
(name "cgit")
(user "nginx")
(group "nginx")
(environment-variables (list (environment-variable
(name "CGIT_CONFIG")
(value cgitrc))))
(mappings (list (file-system-mapping
(source repository-directory)
(target source))
(file-system-mapping
(source cgit)
(target source))
(file-system-mapping
(source cgitrc)
(target source))
(file-system-mapping
(source (file-append git "/libexec/git-core/git-http-backend"))
(target source))))))))
(define cgit-nginx-server-block
(match-record-lambda <cgit-configuration>
(cgit git server-name socket repository-directory)
(nginx-server-configuration
(server-name (list server-name))
;; cgit static files
(root (file-append cgit "/share/cgit"))
(try-files (list "$uri" "@cgit"))
(locations
(list
;; git-http-backend for the smart HTTP protocol
(nginx-location-configuration
(uri "~ ^/.*/(HEAD|info/refs|git-receive-pack|git-upload-pack).*$")
(body (list "fastcgi_param SCRIPT_FILENAME "
(file-append git "/libexec/git-core/git-http-backend;")
(string-append "fastcgi_param GIT_PROJECT_ROOT "
repository-directory
";")
"fastcgi_param GIT_HTTP_EXPORT_ALL yes;"
"fastcgi_param PATH_INFO $uri;"
"fastcgi_param QUERY_STRING $query_string;"
"fastcgi_param REQUEST_METHOD $request_method;"
"fastcgi_param CONTENT_TYPE $content_type;"
(string-append "fastcgi_pass "
(nginx-socket->string socket)
";"))))
;; cgit web interface
(nginx-location-configuration
(uri "@cgit")
(body (list
#~(string-append "fastcgi_param SCRIPT_FILENAME "
#$(file-append cgit "/lib/cgit/cgit.cgi")
";")
"fastcgi_param PATH_INFO $uri;"
"fastcgi_param QUERY_STRING $query_string;"
"fastcgi_param HTTP_HOST $server_name;"
(string-append "fastcgi_pass "
(nginx-socket->string socket)
";")))))))))
(define cgit-service-type
(service-type
(name 'cgit)
(description "Run cgit.")
(extensions (list (service-extension fcgiwrap-service-type
(compose list cgit-fcgiwrap-instance))
(service-extension forge-nginx-service-type
(compose list cgit-nginx-server-block))))))