Fosi Fossil Service
Small teams. Open source. Built on Fossil.

CaaS / Lamina

Unified Management for Fossil Repositories

Public | Owner: CaaS | Default branch: trunk | Last update: 2026-10-25 00:41

Artifact 3cdf5e9eb085a05dcf3c18fd239f399719b8df216713f77e05103df159dcd21d:


;; Fossil 仓库基础操作。
(defmodule lamina_fossil_repo
  (export all))

;; 新建仓库;admin-user 为 Setup 角色用户名。
(defun init (repo-path admin-user)
  (lamina_fossil:run (list "init" "-A" admin-user repo-path)))

;; 创建时可直接写入项目名称和描述,避免额外 SQL 操作。
(defun init (repo-path admin-user project-name project-description)
  (lamina_fossil:run
   (list "init" "-A" admin-user
         "--project-name" project-name
         "--project-desc" project-description
         repo-path)))

;; 设置仓库用户密码(Setup 角色凭据创建/更新)。
(defun set-user-pw (repo-path username password)
  (lamina_fossil:run (list "user" "password" username password "-R" repo-path)))

;; Fossil CLI 没有跨进程参数绑定接口;这里使用 SQLite 标准字符串转义,
;; SQL 作为单独的 open_port 参数传递,不经过 shell。
(defun set-description (repo-path name description)
  (let ((sql (lists:flatten
              (io_lib:format
               "INSERT OR REPLACE INTO config(name,value) VALUES('project-name','~s'),('project-description','~s');"
               (list (sql-quote name) (sql-quote description))))))
    (lamina_fossil:run (list "sql" "-R" repo-path sql))))

;; 克隆远程仓库(超时放宽到 300s)。
(defun clone (remote-url repo-path admin-user)
  (lamina_fossil:run
   (list "clone" remote-url repo-path "--admin-user" admin-user)
   (lamina_fossil:clone-timeout-ms)))

;; 从已登记的远程源同步本地仓库。
(defun sync (remote-url repo-path)
  (lamina_fossil:run
   (list "sync" remote-url "-R" repo-path)
   (lamina_fossil:clone-timeout-ms)))

(defun info (repo-path)
  (lamina_fossil:run (list "info" "-R" repo-path)))

;; 取仓库最后活动时间(字符串或 'unknown)。
;; Fossil CLI 的 timeline 把日期和时刻拆成两行,不能用 "YYYY-MM-DD HH:MM:SS" 整段匹配。
(defun last-timeline (repo-path)
  (let ((path (to-str repo-path)))
    (case (lamina_fossil:run
           (list "sql" "-R" path
                 "SELECT strftime('%Y-%m-%d %H:%M',mtime,'localtime') FROM event ORDER BY mtime DESC LIMIT 1;"))
      (`#(ok ,out)
       (case (parse-timeline-output out)
         ('unknown (last-timeline-cli path))
         (stamp stamp)))
      (_ (last-timeline-cli path)))))

(defun last-timeline-cli (path)
  (case (lamina_fossil:run (list "timeline" "-n" "1" "-R" path))
    (`#(ok ,out) (parse-timeline-output out))
    (_ 'unknown)))

(defun parse-timeline-output (out)
  (let ((s (to-str out)))
    (case (parse-inline-datetime s)
      ('unknown (parse-split-timeline s))
      (stamp stamp))))

(defun parse-inline-datetime (s)
  (case (re:run s "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}(?::[0-9]{2})?"
                (list (tuple 'capture 'first 'list)))
    (`#(match (,text)) text)
    (_ 'unknown)))

(defun parse-split-timeline (s)
  (case (re:run s "=== ([0-9]{4}-[0-9]{2}-[0-9]{2}) ===\\s+([0-9]{2}:[0-9]{2}(?::[0-9]{2})?)"
                (list (tuple 'capture 'all 'list)))
    (`#(match (,_all ,date ,time)) (lists:flatten (list date " " time)))
    (_ 'unknown)))

;; 应用 skin.conf(skin.conf 为 fossil configuration 导出格式)。
(defun apply-skin (repo-path skin-conf-path)
  (lamina_fossil:run
   (list "configuration" "import" skin-conf-path "-R" repo-path)))

;; 仓库首页改为 Files(/dir),并写入与设计图一致的主菜单。
;; index-page 在 Admin/Configuration 里,不是 fossil setting 列表中的项。
(defun configure-web (repo-path)
  (let ((sql (lists:flatten
              (list
               "INSERT OR REPLACE INTO config(name,value,mtime) VALUES"
               "('index-page','/dir?ci=tip',julianday('now')),"
               "('mainmenu','Files /dir?ci=tip * {} Timeline /timeline * {} "
               "Branches /brlist * {} Tags /taglist * {} Tickets /ticket * {} "
               "Wiki /wiki * {} Forum /forum * {} Admin /setup s {}',julianday('now')),"
               "('robot-restrict','timelineX,diff,annotate,fileage,reports,tree,hexdump,download',julianday('now'));"))))
    (lamina_fossil:run (list "sql" "-R" repo-path sql))))

(defun sql-quote (value)
  (string:replace (to-str value) "'" "''" 'all))

(defun to-str (value)
  (cond
    ((is_binary value) (binary_to_list value))
    ((is_list value) value)
    ('true (lists:flatten (io_lib:format "~p" (list value))))))

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/CaaS/Lamina

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.