guix_mirror_bot pushed a commit to branch master
in repository guix.
commit 3433fb987bbc826a585a0d4d0a80e1f5369769a3
Author: Nguyễn Gia Phong <[email protected]>
AuthorDate: Mon Nov 17 15:46:53 2025 +0900
services: Add fossil-service-type.
* gnu/services/version-control.scm
(fossil-service-type, fossil-configuration): New public variables.
* gnu/tests/version-control.scm (%test-fossil): Add system tests.
* doc/guix.texi (Version Control Services): Add Fossil documentation.
Change-Id: I84e09fe8c11e161ed7c4bdba42b0ae38ef4c2096
Signed-off-by: Ludovic Courtès <[email protected]>
---
doc/guix.texi | 150 +++++++++++++++++++++++
gnu/services/version-control.scm | 257 ++++++++++++++++++++++++++++++++++++++-
gnu/tests/version-control.scm | 75 +++++++++++-
3 files changed, 480 insertions(+), 2 deletions(-)
diff --git a/doc/guix.texi b/doc/guix.texi
index 9924f4771f..8841a483a8 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -149,6 +149,7 @@ Copyright @copyright{} 2025 Rodion Goritskov@*
Copyright @copyright{} 2025 dan@*
Copyright @copyright{} 2025 Noé Lopez@*
Copyright @copyright{} 2026 David Elsing@*
+Copyright @copyright{} 2026 Nguyễn Gia Phong@*
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -43773,6 +43774,155 @@ like to serve.
@end table
@end deftp
+@anchor{fossil-service-type}
+@subsubheading Fossil Service
+
+@cindex Fossil service
+@cindex Fossil, forge
+@uref{https://fossil-scm.org, Fossil} is a distributed
+software configuration management system. In addition to version control
+like Git, Fossil also supports bug tracking, wiki, forum, chat, etc.,
+all accessible via its built-in web interface.
+
+Fossil is highly reliable thanks to its robust file format based on SQLite
+with atomic transactions. Its server is CPU, memory and bandwidth efficient
+enough run comfortably on a cheap VPS or single board computer,
+and be accessed over suboptimal connections.
+
+The following example will configure Fossil to listen on a unix socket
+behind a reverse proxy and serve repositories from a custom location.
+
+@lisp
+(service fossil-service-type
+ (fossil-configuration
+ (repository "/srv/museum")
+ (repo-list? #t)
+ (base-url "https://museum.example")
+ (socket-file "/var/run/fossil.sock")
+ (compress? #f)))
+@end lisp
+
+@deftp {Data Type} fossil-configuration
+Available @code{fossil-configuration} fields are:
+
+@table @asis
+@item @code{package} (default: @code{fossil}) (type: package)
+The Fossil package to use.
+
+@item @code{user} (default: @code{"fossil"}) (type: string)
+The user running the Fossil server.
+
+@item @code{group} (default: @code{"fossil"}) (type: string)
+The user group running the Fossil server.
+
+@item @code{log-file} (default: @code{"/var/log/fossil.log"}) (type: string)
+The path to the server's log.
+
+@item @code{repository} (default: @code{"/var/lib/fossil"}) (type: string)
+The name of the Fossil repository to be served, or a directory
+containing one or more repositories with names ending in @code{.fossil}.
+In the latter case, a prefix of the URL pathname is used to search the
+directory for an appropriate repository. Files not matching the pattern
+@code{*.fossil*} will be served as static content. Invoke
+@command{fossil server --help} for more information.
+
+@item @code{acme?} (default: @code{#f}) (type: boolean)
+Deliver files from the @code{.well-known} subdirectory.
+
+@item @code{base-url} (type: maybe-string)
+The URL used as the base (useful for reverse proxies)
+
+@item @code{chroot} (type: maybe-string)
+The directory to use for chroot instead of @code{repository}.
+
+@item @code{ckout-alias} (type: maybe-string)
+The @var{name} for @code{/doc/@var{name}/...} to be treated as
+@code{/doc/ckout/...}.
+
+@item @code{compress?} (default: @code{#t}) (type: boolean)
+Compress HTTP response.
+
+@item @code{create?} (default: @code{#f}) (type: boolean)
+Create a new @code{repository} if it does not already exist.
+
+@item @code{error-log-file} (type: maybe-string)
+The path for HTTP error log.
+
+@item @code{ext-root} (type: maybe-string)
+The document root for the /ext extension mechanism.
+
+@item @code{files} (type: maybe-list-of-strings)
+The glob patterns for static files.
+
+@item @code{from} (type: maybe-string)
+The path to be used as the diff baseline for the /ckout page.
+
+@item @code{jail?} (default: @code{#t}) (type: boolean)
+Whether to enter the chroot jail after dropping root privileges.
+
+@item @code{js-mode} (type: maybe-fossil-js-mode)
+How JavaScript is delivered with pages, either @code{'inline} at the end
+of the HTML file, as @code{'separate} HTTP requests, or one single HTTP
+request for all JavaScript @code{'bundled} together. Depending on the
+needs of any given page, @code{'inline} and @code{'bundled} modes might
+result in a single amalgamated script or several, but both approaches
+result in fewer HTTP requests than the @code{'separate} mode.
+
+@item @code{https?} (default: @code{#f}) (type: boolean)
+Indicate that the requests are coming through a reverse proxy that has
+already translated HTTPS into HTTP.
+
+@item @code{ip} (type: maybe-string)
+The IP for the server to listen on.
+
+@item @code{local-authentication?} (default: @code{#f}) (type: boolean)
+Enable automatic login for requests from localhost.
+
+@item @code{localhost?} (default: @code{#f}) (type: boolean)
+Listen on @code{127.0.0.1} only.
+
+@item @code{main-menu} (type: maybe-string)
+The file whose contents is to override the repository's @code{mainmenu}
+setting.
+
+@item @code{max-latency} (type: maybe-number)
+The maximum latency in seconds for a single HTTP request.
+
+@item @code{port} (default: @code{8080}) (type: port-number)
+The port number for the server to listen on.
+
+@item @code{list-repositories?} (default: @code{#f}) (type: boolean)
+If @code{repository} is dir, URL @code{/} lists repos.
+
+@item @code{redirect-to-https?} (default: @code{#t}) (type: boolean)
+If set to @code{#f}, do not force redirects to HTTPS regardless of the
+repository setting @code{redirect-to-https}.
+
+@item @code{scgi?} (default: @code{#f}) (type: boolean)
+Accept SCGI rather than HTTP.
+
+@item @code{skin} (type: maybe-string)
+The skin label to use, overriding repository settings.
+
+@item @code{socket-file} (type: maybe-string)
+The unix-domain socket to use instead of TCP/IP.
+
+@item @code{socket-mode} (default: @code{0o640}) (type: mode-number)
+The file permissions to set for the unix socket.
+
+@item @code{th-trace?} (default: @code{#f}) (type: boolean)
+Trace TH1 execution (for debugging purposes).
+
+@item @code{tls-certificate} (type: maybe-string)
+The certicate file (@file{fullchain.pem}) with which to enable TLS
+(HTTPS) encryption.
+
+@item @code{tls-private-key} (type: maybe-string)
+The file storing the TLS private key.
+
+@end table
+
+@end deftp
@node Game Services
@subsection Game Services
diff --git a/gnu/services/version-control.scm b/gnu/services/version-control.scm
index a7f40812a6..dba38faa46 100644
--- a/gnu/services/version-control.scm
+++ b/gnu/services/version-control.scm
@@ -7,6 +7,7 @@
;;; Copyright © 2021 Julien Lepiller <[email protected]>
;;; Copyright © 2025 Tomas Volf <[email protected]>
;;; Copyright © 2025 Evgeny Pisemsky <[email protected]>
+;;; Copyright © 2026 Nguyễn Gia Phong <[email protected]>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -26,12 +27,14 @@
(define-module (gnu services version-control)
#:use-module (gnu services)
#:use-module (gnu services base)
+ #:use-module (gnu services configuration)
#:use-module (gnu services shepherd)
#:use-module (gnu services web)
#:use-module (gnu system shadow)
#:use-module (gnu packages version-control)
#:use-module (gnu packages admin)
#:use-module (guix deprecation)
+ #:use-module (guix packages)
#:use-module (guix records)
#:use-module (guix gexp)
#:use-module (guix store)
@@ -93,7 +96,44 @@
gitile-configuration-footer
gitile-configuration-nginx
- gitile-service-type))
+ gitile-service-type
+
+ fossil-configuration
+ fossil-configuration-fields
+ fossil-configuration?
+ fossil-configuration-package
+ fossil-configuration-user
+ fossil-configuration-group
+ fossil-configuration-log-file
+ fossil-configuration-repository
+ fossil-configuration-acme?
+ fossil-configuration-base-url
+ fossil-configuration-chroot
+ fossil-configuration-ckout-alias
+ fossil-configuration-compress?
+ fossil-configuration-create?
+ fossil-configuration-error-log-file
+ fossil-configuration-ext-root
+ fossil-configuration-files
+ fossil-configuration-from
+ fossil-configuration-jail?
+ fossil-configuration-js-mode
+ fossil-configuration-https?
+ fossil-configuration-ip
+ fossil-configuration-local-authentication?
+ fossil-configuration-main-menu
+ fossil-configuration-max-latency
+ fossil-configuration-port
+ fossil-configuration-list-repositories?
+ fossil-configuration-redirect-to-https?
+ fossil-configuration-skin
+ fossil-configuration-socket-file
+ fossil-configuration-socket-mode
+ fossil-configuration-th-trace?
+ fossil-configuration-tls-certificate
+ fossil-configuration-tls-private-key
+
+ fossil-service-type))
;;; Commentary:
;;;
@@ -603,3 +643,218 @@ on the web.")
gitile-shepherd-service)
(service-extension nginx-service-type
gitile-nginx-server-block)))))
+
+
+;;;
+;;; Fossil HTTP server.
+;;;
+
+(define (port-number? n)
+ (and (integer? n)
+ (> n 0)
+ (< n (expt 2 16))))
+
+(define (mode-number? n)
+ (and (integer? n)
+ (>= n 0)
+ (<= n #o777)))
+
+(define (fossil-js-mode? x)
+ (and (memq x '(inline separate bundled))
+ #t))
+
+(define-maybe/no-serialization number)
+(define-maybe/no-serialization string)
+(define-maybe/no-serialization list-of-strings)
+(define-maybe/no-serialization fossil-js-mode)
+
+(define-configuration/no-serialization fossil-configuration
+ (package (package fossil)
+ "The Fossil package to use.")
+ (user (string "fossil")
+ "The user running the Fossil server.")
+ (group (string "fossil")
+ "The user group running the Fossil server.")
+ (log-file (string "/var/log/fossil.log")
+ "The path to the server's log.")
+ (repository (string "/var/lib/fossil")
+ "The name of the Fossil repository to be served, or a directory
+containing one or more repositories with names ending in @code{.fossil}.
+
+In the latter case, a prefix of the URL pathname is used
+to search the directory for an appropriate repository.
+Files not matching the pattern @code{*.fossil*}
+will be served as static content. Invoke @command{fossil server --help}
+for more information.")
+ (acme? (boolean #f)
+ "Deliver files from the @code{.well-known} subdirectory.")
+ (base-url maybe-string
+ "The URL used as the base (useful for reverse proxies)")
+ (chroot maybe-string
+ "The directory to use for chroot instead of @code{repository}.")
+ (ckout-alias maybe-string
+ "The @var{name} for @code{/doc/@var{name}/...}
+to be treated as @code{/doc/ckout/...}.")
+ (compress? (boolean #t) "Compress HTTP response.")
+ (create? (boolean #f)
+ "Create a new @code{repository} if it does not already exist.")
+ (error-log-file maybe-string "The path for HTTP error log.")
+ (ext-root maybe-string "The document root for the /ext extension mechanism.")
+ (files maybe-list-of-strings "The glob patterns for static files.")
+ (from maybe-string
+ "The path to be used as the diff baseline for the /ckout page.")
+ (jail? (boolean #t)
+ "Whether to enter the chroot jail after dropping root privileges.")
+ (js-mode maybe-fossil-js-mode
+ "How JavaScript is delivered with pages, either @code{'inline}
+at the end of the HTML file, as @code{'separate} HTTP requests,
+or one single HTTP request for all JavaScript @code{'bundled} together.
+
+Depending on the needs of any given page, @code{'inline}
+and @code{'bundled} modes might result in a single amalgamated script
+or several, but both approaches result in fewer HTTP requests
+than the @code{'separate} mode.")
+ (https? (boolean #f)
+ "Indicate that the requests are coming through a reverse proxy
+that has already translated HTTPS into HTTP.")
+ (ip maybe-string "The IP for the server to listen on.")
+ (local-authentication? (boolean #f)
+ "Enable automatic login for requests from localhost.")
+ (localhost? (boolean #f) "Listen on @code{127.0.0.1} only.")
+ (main-menu maybe-string ;TODO: structure
+ "The file whose contents is to override
+the repository's @code{mainmenu} setting.")
+ (max-latency maybe-number
+ "The maximum latency in seconds for a single HTTP request.")
+ (port (port-number 8080) "The port number for the server to listen on.")
+ (list-repositories? (boolean #f)
+ "If @code{repository} is dir, URL @code{/} lists repos.")
+ (redirect-to-https? (boolean #t)
+ "If set to @code{#f}, do not force redirects to HTTPS
+regardless of the repository setting @code{redirect-to-https}.")
+ (scgi? (boolean #f) "Accept SCGI rather than HTTP.")
+ (skin maybe-string "The skin label to use, overriding repository settings.")
+ (socket-file maybe-string
+ "The unix-domain socket to use instead of TCP/IP.")
+ (socket-mode (mode-number #o640)
+ "The file permissions to set for the unix socket.")
+ (th-trace? (boolean #f)
+ "Trace TH1 execution (for debugging purposes).")
+ (tls-certificate maybe-string
+ "The certicate file (@file{fullchain.pem})
+with which to enable TLS (HTTPS) encryption.")
+ (tls-private-key maybe-string "The file storing the TLS private key."))
+
+(define (fossil-accounts config)
+ (match-record config <fossil-configuration> (user group)
+ (list (user-group (name group)
+ (system? #t))
+ (user-account (name user)
+ (group group)
+ (system? #t)
+ (comment "Fossil server user")
+ (home-directory "/var/empty")
+ (shell (file-append shadow "/sbin/nologin"))))))
+
+(define (fossil-activation config)
+ (match-record config <fossil-configuration> (user create? repository)
+ (with-imported-modules '((guix build utils))
+ #~(begin
+ (use-modules (guix build utils))
+ (let* ((pw (getpwnam #$user))
+ (uid (passwd:uid pw))
+ (gid (passwd:gid pw)))
+ (unless #$create? (chown #$repository uid gid)))))))
+
+(define (fossil-shepherd-service config)
+ (match-record config <fossil-configuration>
+ (package user group log-file repository acme? base-url
+ chroot ckout-alias compress? create? error-log-file ext-root
+ files from https? ip jail? js-mode list-repositories?
+ local-authentication? localhost? main-menu max-latency port
+ redirect-to-https? scgi? skin socket-file socket-mode
+ th-trace? tls-certificate tls-private-key)
+ (shepherd-service
+ (provision '(fossil))
+ (requirement '(user-processes networking))
+ (start #~(make-forkexec-constructor
+ (list #$(file-append package "/bin/fossil")
+ "server"
+ #$@(if acme? '("--acme") '())
+ #$@(if (maybe-value-set? base-url)
+ (list "--baseurl" base-url)
+ '())
+ #$@(if (maybe-value-set? chroot)
+ (list "--chroot" chroot)
+ '())
+ #$@(if (maybe-value-set? ckout-alias)
+ (list "--ckout-alias" ckout-alias)
+ '())
+ #$@(if compress? '() '("--nocompress"))
+ #$@(if create? '("--create") '())
+ #$@(if (maybe-value-set? error-log-file)
+ (list "--errorlog" error-log-file)
+ '())
+ #$@(if (maybe-value-set? ext-root)
+ (list "--extroot" ext-root)
+ '())
+ #$@(if (maybe-value-set? files)
+ (list "--files" (string-join files ","))
+ '())
+ #$@(if (maybe-value-set? from) (list "--from" from) '())
+ #$@(if https? '("--https") '())
+ #$@(if jail? '() '("--nojail"))
+ #$@(if (maybe-value-set? js-mode)
+ (list "--jsmode" (symbol->string js-mode))
+ '())
+ #$@(if local-authentication? '("--localauth") '())
+ #$@(if localhost? '("--localhost") '())
+ #$@(if (maybe-value-set? main-menu)
+ (list "--mainmenu" main-menu)
+ '())
+ #$@(if (maybe-value-set? max-latency)
+ (list "--max-latency"
+ (number->string max-latency))
+ '())
+ #$@(if redirect-to-https? '() '("--nossl"))
+ #$@(if scgi? '("--scgi") '())
+ #$@(if list-repositories? '("--repolist") '())
+ #$@(if (maybe-value-set? skin) (list "--skin" skin) '())
+ #$@(if (maybe-value-set? socket-file)
+ (list "--socket-name" socket-file
+ "--socket-mode" socket-mode
+ "--socket-owner"
+ (simple-format #f "~a:~a" user group))
+ (list "--port"
+ (if (maybe-value-set? ip)
+ (simple-format #f "~a:~a" ip port)
+ (number->string port))))
+ #$@(if th-trace? '("--th-trace") '())
+ #$@(if (maybe-value-set? tls-certificate)
+ (list "--cert" tls-certificate)
+ '())
+ #$@(if (maybe-value-set? tls-private-key)
+ (list "--pkey" tls-private-key)
+ '())
+ "--user" #$user
+ #$repository)
+ #:user #$user
+ #:group #$group
+ #:log-file #$log-file))
+ (stop #~(make-kill-destructor))
+ (documentation
+ "Run the HTTP server
+for the Fossil software configuration management system."))))
+
+(define fossil-service-type
+ (service-type
+ (name 'fossil)
+ (extensions
+ (list (service-extension account-service-type fossil-accounts)
+ (service-extension activation-service-type fossil-activation)
+ (service-extension shepherd-root-service-type
+ (compose list fossil-shepherd-service))))
+ (description
+ "Run the HTTP server for the Fossil software configuration management
+system. In addition to distributed version control, Fossil also supports
+bug tracking, wiki, forum, email alerts, chat, and technotes.")))
diff --git a/gnu/tests/version-control.scm b/gnu/tests/version-control.scm
index 8426555a18..9df3aa9dbd 100644
--- a/gnu/tests/version-control.scm
+++ b/gnu/tests/version-control.scm
@@ -3,6 +3,7 @@
;;; Copyright © 2017-2018, 2020-2022 Ludovic Courtès <[email protected]>
;;; Copyright © 2017, 2018 Clément Lassieur <[email protected]>
;;; Copyright © 2018 Christopher Baines <[email protected]>
+;;; Copyright © 2026 Nguyễn Gia Phong <[email protected]>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -39,7 +40,8 @@
#:export (%test-cgit
%test-git-http
%test-gitolite
- %test-gitile))
+ %test-gitile
+ %test-fossil))
(define README-contents
"Hello! This is what goes inside the 'README' file.")
@@ -519,3 +521,74 @@ HTTP-PORT."
(name "gitile")
(description "Connect to a running Gitile server.")
(value (run-gitile-test))))
+
+
+;;;
+;;; Fossil server.
+;;;
+
+(define %test-fossil
+ (system-test
+ (name "fossil")
+ (description "Connect to a running Fossil server.")
+ (value
+ (gexp->derivation
+ (string-append name "-test")
+ (let* ((port 8080)
+ (base-url (simple-format #f "http://localhost:~a" port))
+ (index-url (string-append base-url "/index"))
+ (os (marionette-operating-system
+ (simple-operating-system
+ (service dhcpcd-service-type)
+ (service fossil-service-type
+ (fossil-configuration
+ (repository "/tmp/test.fossil")
+ (base-url base-url)
+ (create? #t)
+ (port port))))))
+ (vm (virtual-machine (operating-system os)
+ (port-forwardings (list (cons port port))))))
+ (with-imported-modules '((gnu build marionette)
+ (guix build utils))
+ #~(begin
+ (use-modules (gnu build marionette)
+ (guix build utils)
+ (srfi srfi-64)
+ (srfi srfi-71)
+ (web client)
+ (web response))
+ (define marionette (make-marionette (list #$vm)))
+ (test-runner-current (system-test-runner #$output))
+ (test-begin #$name)
+
+ (test-assert "server running"
+ (wait-for-tcp-port #$port marionette))
+
+ (test-assert "server log file"
+ (wait-for-file "/var/log/fossil.log" marionette))
+
+ (test-assert "cloning"
+ (begin
+ (setenv "HOME" #$output) ; fossil writes to $HOME
+ (invoke/quiet #$(file-append fossil "/bin/fossil") "clone"
+ "--admin-user" "alice"
+ "--httptrace"
+ "--verbose"
+ #$base-url
+ (string-append #$output "/test.fossil"))))
+
+ (test-assert "index redirect"
+ (let ((response text
+ (http-get #$base-url #:decode-body? #t)))
+ (and (= 302 (response-code response))
+ (string-contains text #$index-url))))
+
+ (test-equal "index page"
+ 200 (response-code (http-get #$index-url)))
+
+ (test-equal "tarball download"
+ 200 (response-code
+ (http-get (string-append #$base-url
+ "/tarball/test.tar.gz"))))
+
+ (test-end))))))))