aboutsummaryrefslogtreecommitdiff
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2019-2020, 2022, 2024 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix 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.
;;;
;;; GNU Guix 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 GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (test-git)
  #:use-module (git)
  #:use-module (guix git)
  #:use-module (guix tests git)
  #:use-module ((guix utils) #:select (call-with-temporary-directory))
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-64)
  #:use-module (srfi srfi-71)
  #:use-module (ice-9 popen)
  #:use-module (ice-9 textual-ports))

;; Test the (guix git) tools.

(test-begin "git")

(test-assert "commit-difference, linear history"
  (with-temporary-git-repository directory
      '((add "a.txt" "A")
        (commit "first commit")
        (add "b.txt" "B")
        (commit "second commit")
        (add "c.txt" "C")
        (commit "third commit")
        (add "d.txt" "D")
        (commit "fourth commit"))
    (with-repository directory repository
      (let ((commit1 (find-commit repository "first"))
            (commit2 (find-commit repository "second"))
            (commit3 (find-commit repository "third"))
            (commit4 (find-commit repository "fourth")))
        (and (lset= eq? (commit-difference commit4 commit1)
                    (list commit2 commit3 commit4))
             (lset= eq? (commit-difference commit4 commit2)
                    (list commit3 commit4))
             (equal? (commit-difference commit3 commit2)
                     (list commit3))

             ;; COMMIT4 is not an ancestor of COMMIT1 so we should get the
             ;; empty list.
             (null? (commit-difference commit1 commit4)))))))

(test-assert "commit-difference, fork"
  (with-temporary-git-repository directory
      '((add "a.txt" "A")
        (commit "first commit")
        (branch "devel")
        (checkout "devel")
        (add "devel/1.txt" "1")
        (commit "first devel commit")
        (add "devel/2.txt" "2")
        (commit "second devel commit")
        (checkout "master")
        (add "b.txt" "B")
        (commit "second commit")
        (add "c.txt" "C")
        (commit "third commit")
        (merge "devel" "merge")
        (add "d.txt" "D")
        (commit "fourth commit"))
    (with-repository directory repository
      (let ((master1 (find-commit repository "first commit"))
            (master2 (find-commit repository "second commit"))
            (master3 (find-commit repository "third commit"))
            (master4 (find-commit repository "fourth commit"))
            (devel1  (find-commit repository "first devel"))
            (devel2  (find-commit repository "second devel"))
            (merge   (find-commit repository "merge")))
        (and (equal? (commit-difference master4 merge)
                     (list master4))
             (lset= eq? (commit-difference master3 master1)
                    (list master3 master2))
             (lset= eq? (commit-difference devel2 master1)
                    (list devel2 devel1))

             ;; The merge occurred between MASTER2 and MASTER4 so here we
             ;; expect to see all the commits from the "devel" branch in
             ;; addition to those on "master".
             (lset= eq? (commit-difference master4 master2)
                    (list master4 merge master3 devel1 devel2)))))))

(test-assert "commit-difference, excluded commits"
  (with-temporary-git-repository directory
      '((add "a.txt" "A")
        (commit "first commit")
        (add "b.txt" "B")
        (commit "second commit")
        (add "c.txt" "C")
        (commit "third commit")
        (add "d.txt" "D")
        (commit "fourth commit")
        (add "e.txt" "E")
        (commit "fifth commit"))
    (with-repository directory repository
      (let ((commit1 (find-commit repository "first"))
            (commit2 (find-commit repository "second"))
            (commit3 (find-commit repository "third"))
            (commit4 (find-commit repository "fourth"))
            (commit5 (find-commit repository "fifth")))
        (and (lset= eq? (commit-difference commit4 commit1 (list commit2))
                    (list commit3 commit4))
             (lset= eq? (commit-difference commit4 commit1 (list commit3))
                    (list commit4))
             (null? (commit-difference commit4 commit1 (list commit5))))))))

(test-equal "commit-relation"
  '(self                                          ;master3 master3
    ancestor                                      ;master1 master3
    descendant                                    ;master3 master1
    unrelated                                     ;master2 branch1
    unrelated                                     ;branch1 master2
    ancestor                                      ;branch1 merge
    descendant                                    ;merge branch1
    ancestor                                      ;master1 merge
    descendant)                                   ;merge master1
  (with-temporary-git-repository directory
      '((add "a.txt" "A")
        (commit "first commit")
        (branch "hack")
        (checkout "hack")
        (add "1.txt" "1")
        (commit "branch commit")
        (checkout "master")
        (add "b.txt" "B")
        (commit "second commit")
        (add "c.txt" "C")
        (commit "third commit")
        (merge "hack" "merge"))
    (with-repository directory repository
      (let ((master1 (find-commit repository "first"))
            (master2 (find-commit repository "second"))
            (master3 (find-commit repository "third"))
            (branch1 (find-commit repository "branch"))
            (merge   (find-commit repository "merge")))
        (list (commit-relation master3 master3)
              (commit-relation master1 master3)
              (commit-relation master3 master1)
              (commit-relation master2 branch1)
              (commit-relation branch1 master2)
              (commit-relation branch1 merge)
              (commit-relation merge branch1)
              (commit-relation master1 merge)
              (commit-relation merge master1))))))

(test-equal "commit-descendant?"
  '((master3 master3 => #t)
    (master1 master3 => #f)
    (master3 master1 => #t)
    (master2 branch1 => #f)
    (master2 branch1 master1 => #t)
    (branch1 master2 => #f)
    (branch1 merge   => #f)
    (merge branch1   => #t)
    (master1 merge   => #f)
    (merge master1   => #t))
  (with-temporary-git-repository directory
      '((add "a.txt" "A")
        (commit "first commit")
        (branch "hack")
        (checkout "hack")
        (add "1.txt" "1")
        (commit "branch commit")
        (checkout "master")
        (add "b.txt" "B")
        (commit "second commit")
        (add "c.txt" "C")
        (commit "third commit")
        (merge "hack" "merge"))
    (with-repository directory repository
      (let ((master1 (find-commit repository "first"))
            (master2 (find-commit repository "second"))
            (master3 (find-commit repository "third"))
            (branch1 (find-commit repository "branch"))
            (merge   (find-commit repository "merge")))
        (letrec-syntax ((verify
                         (syntax-rules ()
                           ((_) '())
                           ((_ (new old ...) rest ...)
                            (cons `(new old ... =>
                                        ,(commit-descendant? new
                                                             (list old ...)))
                                  (verify rest ...))))))
          (verify (master3 master3)
                  (master1 master3)
                  (master3 master1)
                  (master2 branch1)
                  (master2 branch1 master1)
                  (branch1 master2)
                  (branch1 merge)
                  (merge branch1)
                  (master1 merge)
                  (merge master1)))))))

(test-equal "remote-refs"
  '("refs/heads/develop" "refs/heads/master"
    "refs/tags/v1.0" "refs/tags/v1.1")
  (with-temporary-git-repository directory
      '((add "a.txt" "A")
        (commit "First commit")
        (tag "v1.0" "release-1.0")
        (branch "develop")
        (checkout "develop")
        (add "b.txt" "B")
        (commit "Second commit")
        (tag "v1.1" "release-1.1"))
    (remote-refs directory)))

(test-equal "remote-refs: only tags"
 '("refs/tags/v1.0" "refs/tags/v1.1")
  (with-temporary-git-repository directory
      '((add "a.txt" "A")
        (commit "First commit")
        (tag "v1.0" "Release 1.0")
        (add "b.txt" "B")
        (commit "Second commit")
        (tag "v1.1" "Release 1.1"))
    (remote-refs directory #:tags? #t)))

(test-assert "update-cached-checkout, tag"
  (call-with-temporary-directory
   (lambda (cache)
     (with-temporary-git-repository directory
         '((add "a.txt" "A")
           (commit "First commit")
           (tag "v1.0" "release-1.0")
           (branch "develop")
           (checkout "develop")
           (add "b.txt" "B")
           (commit "Second commit")
           (tag "v1.1" "release-1.1"))
       (let ((directory commit relation
                        (update-cached-checkout directory
                                                #:ref '(tag . "v1.1")
                                                #:cache-directory cache))
             (head   (let* ((pipe (open-pipe* OPEN_READ (git-command)
                                              "-C" directory
                                              "rev-parse" "HEAD"))
                            (str  (get-string-all pipe)))
                       (close-pipe pipe)
                       (string-trim-right str))))
         ;; COMMIT should be the ID of the commit object, not that of the tag.
         (string=? commit head))))))

(test-assert "update-cached-checkout, untracked files removed"
  (call-with-temporary-directory
   (lambda (cache)
     (with-temporary-git-repository directory
         '((add "a.txt" "A")
           (add ".gitignore" ".~\n")
           (commit "First commit"))
       (let ((directory commit relation
                        (update-cached-checkout directory
                                                #:ref '()
                                                #:cache-directory cache)))
         (close-port
          (open-output-file (in-vicinity cache "stale-untracked-file")))
         (let ((directory2 commit2 relation2
                           (update-cached-checkout directory
                                                   #:ref '()
                                                   #:cache-directory cache)))
           (not (file-exists?
                 (in-vicinity cache "stale-untracked-file")))))))))

(test-end "git")
(@code{sunday}, @code{monday}, and so on) or using one of the symbols @code{weekdays}, @code{weekends} or @code{all}.") (alt-speed-time-begin (non-negative-integer 540) "The time of day at which to enable the alternate speed limits, expressed as a number of minutes since midnight.") (alt-speed-time-end (non-negative-integer 1020) "The time of day at which to disable the alternate speed limits, expressed as a number of minutes since midnight.") ;; Peer networking. (bind-address-ipv4 (string "0.0.0.0") "The IP address at which to listen for peer connections, or ``0.0.0.0'' to listen at all available IP addresses.") (bind-address-ipv6 (string "::") "The IPv6 address at which to listen for peer connections, or ``::'' to listen at all available IPv6 addresses.") (peer-port-random-on-start? (boolean #f) "If @code{#t}, when the daemon starts it will select a port at random on which to listen for peer connections, from the range specified (inclusively) by @code{peer-port-random-low} and @code{peer-port-random-high}. Otherwise, it listens on the port specified by @code{peer-port}.") (peer-port-random-low (port-number 49152) "The lowest selectable port number when @code{peer-port-random-on-start?} is @code{#t}.") (peer-port-random-high (port-number 65535) "The highest selectable port number when @code{peer-port-random-on-start} is @code{#t}.") (peer-port (port-number 51413) "The port on which to listen for peer connections when @code{peer-port-random-on-start?} is @code{#f}.") (port-forwarding-enabled? (boolean #t) "If @code{#t}, the daemon will attempt to configure port-forwarding on an upstream gateway automatically using @acronym{UPnP} and @acronym{NAT-PMP}.") (encryption (encryption-mode 'prefer-encrypted-connections) "The encryption preference for peer connections, one of @code{prefer-unencrypted-connections}, @code{prefer-encrypted-connections} or @code{require-encrypted-connections}.") (peer-congestion-algorithm maybe-string "The TCP congestion-control algorithm to use for peer connections, specified using a string recognized by the operating system in calls to @code{setsockopt} (or leave it unset, in which case the operating-system default is used). Note that on GNU/Linux systems, the kernel must be configured to allow processes to use a congestion-control algorithm not in the default set; otherwise, it will deny these requests with ``Operation not permitted''. To see which algorithms are available on your system and which are currently permitted for use, look at the contents of the files @file{tcp_available_congestion_control} and @file{tcp_allowed_congestion_control} in the @file{/proc/sys/net/ipv4} directory. As an example, to have Transmission Daemon use @uref{http://www-ece.rice.edu/networks/TCP-LP/, the TCP Low Priority congestion-control algorithm}, you'll need to modify your kernel configuration to build in support for the algorithm, then update your operating-system configuration to allow its use by adding a @code{sysctl-service-type} service (or updating the existing one's configuration) with lines like the following: @lisp (service sysctl-service-type (sysctl-configuration (settings (\"net.ipv4.tcp_allowed_congestion_control\" . \"reno cubic lp\")))) @end lisp The Transmission Daemon configuration can then be updated with @lisp (peer-congestion-algorithm \"lp\") @end lisp and the system reconfigured to have the changes take effect.") (peer-socket-tos (tcp-type-of-service 'default) "The type of service to request in outgoing @acronym{TCP} packets, one of @code{default}, @code{low-cost}, @code{throughput}, @code{low-delay} and @code{reliability}.") (peer-limit-global (non-negative-integer 200) "The global limit on the number of connected peers.") (peer-limit-per-torrent (non-negative-integer 50) "The per-torrent limit on the number of connected peers.") (upload-slots-per-torrent (non-negative-integer 14) "The maximum number of peers to which the daemon will upload data simultaneously for each torrent.") (peer-id-ttl-hours (non-negative-integer 6) "The maximum lifespan, in hours, of the peer ID associated with each public torrent before it is regenerated.") ;; Peer blocklists. (blocklist-enabled? (boolean #f) "When @code{#t}, the daemon will ignore peers mentioned in the blocklist it has most recently downloaded from @code{blocklist-url}.") (blocklist-url maybe-string "The URL of a peer blocklist (in @acronym{P2P}-plaintext or eMule @file{.dat} format) to be periodically downloaded and applied when @code{blocklist-enabled?} is @code{#t}.") ;; Queueing. (download-queue-enabled? (boolean #t) "If @code{#t}, the daemon will be limited to downloading at most @code{download-queue-size} non-stalled torrents simultaneously.") (download-queue-size (non-negative-integer 5) "The size of the daemon's download queue, which limits the number of non-stalled torrents it will download at any one time when @code{download-queue-enabled?} is @code{#t}.") (seed-queue-enabled? (boolean #f) "If @code{#t}, the daemon will be limited to seeding at most @code{seed-queue-size} non-stalled torrents simultaneously.") (seed-queue-size (non-negative-integer 10) "The size of the daemon's seed queue, which limits the number of non-stalled torrents it will seed at any one time when @code{seed-queue-enabled?} is @code{#t}.") (queue-stalled-enabled? (boolean #t) "When @code{#t}, the daemon will consider torrents for which it has not shared data in the past @code{queue-stalled-minutes} minutes to be stalled and not count them against its @code{download-queue-size} and @code{seed-queue-size} limits.") (queue-stalled-minutes (non-negative-integer 30) "The maximum period, in minutes, a torrent may be idle before it is considered to be stalled, when @code{queue-stalled-enabled?} is @code{#t}.") ;; Seeding limits. (ratio-limit-enabled? (boolean #f) "When @code{#t}, a torrent being seeded will automatically be paused once it reaches the ratio specified by @code{ratio-limit}.") (ratio-limit (non-negative-rational 2.0) "The ratio at which a torrent being seeded will be paused, when @code{ratio-limit-enabled?} is @code{#t}.") (idle-seeding-limit-enabled? (boolean #f) "When @code{#t}, a torrent being seeded will automatically be paused once it has been idle for @code{idle-seeding-limit} minutes.") (idle-seeding-limit (non-negative-integer 30) "The maximum period, in minutes, a torrent being seeded may be idle before it is paused, when @code{idle-seeding-limit-enabled?} is @code{#t}.") ;; BitTorrent extensions. (dht-enabled? (boolean #t) "Enable @uref{http://bittorrent.org/beps/bep_0005.html, the distributed hash table (@acronym{DHT}) protocol}, which supports the use of trackerless torrents.") (lpd-enabled? (boolean #f) "Enable @url{https://en.wikipedia.org/wiki/Local_Peer_Discovery, local peer discovery} (@acronym{LPD}), which allows the discovery of peers on the local network and may reduce the amount of data sent over the public Internet.") (pex-enabled? (boolean #t) "Enable @url{https://en.wikipedia.org/wiki/Peer_exchange, peer exchange} (@acronym{PEX}), which reduces the daemon's reliance on external trackers and may improve its performance.") (utp-enabled? (boolean #t) "Enable @url{http://bittorrent.org/beps/bep_0029.html, the micro transport protocol} (@acronym{uTP}), which aims to reduce the impact of BitTorrent traffic on other users of the local network while maintaining full utilization of the available bandwidth.") ;; Remote procedure call (RPC) interface. (rpc-enabled? (boolean #t) "If @code{#t}, enable the remote procedure call (@acronym{RPC}) interface, which allows remote control of the daemon via its Web interface, the @command{transmission-remote} command-line client, and similar tools.") (rpc-bind-address (string "0.0.0.0") "The IP address at which to listen for @acronym{RPC} connections, or ``0.0.0.0'' to listen at all available IP addresses.") (rpc-port (port-number 9091) "The port on which to listen for @acronym{RPC} connections.") (rpc-url (string "/transmission/") "The path prefix to use in the @acronym{RPC}-endpoint @acronym{URL}.") (rpc-authentication-required? (boolean #f) "When @code{#t}, clients must authenticate (see @code{rpc-username} and @code{rpc-password}) when using the @acronym{RPC} interface. Note this has the side effect of disabling host-name whitelisting (see @code{rpc-host-whitelist-enabled?}.") (rpc-username maybe-string "The username required by clients to access the @acronym{RPC} interface when @code{rpc-authentication-required?} is @code{#t}.") (rpc-password maybe-transmission-password-hash "The password required by clients to access the @acronym{RPC} interface when @code{rpc-authentication-required?} is @code{#t}. This must be specified using a password hash in the format recognized by Transmission clients, either copied from an existing @file{settings.json} file or generated using the @code{transmission-password-hash} procedure.") (rpc-whitelist-enabled? (boolean #t) "When @code{#t}, @acronym{RPC} requests will be accepted only when they originate from an address specified in @code{rpc-whitelist}.") (rpc-whitelist (string-list '("127.0.0.1" "::1")) "The list of IP and IPv6 addresses from which @acronym{RPC} requests will be accepted when @code{rpc-whitelist-enabled?} is @code{#t}. Wildcards may be specified using @samp{*}.") (rpc-host-whitelist-enabled? (boolean #t) "When @code{#t}, @acronym{RPC} requests will be accepted only when they are addressed to a host named in @code{rpc-host-whitelist}. Note that requests to ``localhost'' or ``localhost.'', or to a numeric address, are always accepted regardless of these settings. Note also this functionality is disabled when @code{rpc-authentication-required?} is @code{#t}.") (rpc-host-whitelist (string-list '()) "The list of host names recognized by the @acronym{RPC} server when @code{rpc-host-whitelist-enabled?} is @code{#t}.") ;; Miscellaneous. (message-level (message-level 'info) "The minimum severity level of messages to be logged (to @file{/var/log/transmission.log}) by the daemon, one of @code{none} (no logging), @code{error}, @code{info} and @code{debug}.") (start-added-torrents? (boolean #t) "When @code{#t}, torrents are started as soon as they are added; otherwise, they are added in ``paused'' state.") (script-torrent-done-enabled? (boolean #f) "When @code{#t}, the script specified by @code{script-torrent-done-filename} will be invoked each time a torrent completes.") (script-torrent-done-filename maybe-file-object "A file name or file-like object specifying a script to run each time a torrent completes, when @code{script-torrent-done-enabled?} is @code{#t}.") (scrape-paused-torrents-enabled? (boolean #t) "When @code{#t}, the daemon will scrape trackers for a torrent even when the torrent is paused.") (cache-size-mb (non-negative-integer 4) "The amount of memory, in megabytes, to allocate for the daemon's in-memory cache. A larger value may increase performance by reducing the frequency of disk I/O.") (prefetch-enabled? (boolean #t) "When @code{#t}, the daemon will try to improve I/O performance by hinting to the operating system which data is likely to be read next from disk to satisfy requests from peers.")) (define (transmission-daemon-shepherd-service config) "Return a <shepherd-service> for Transmission Daemon with CONFIG." (let ((transmission (transmission-daemon-configuration-transmission config)) (stop-wait-period (transmission-daemon-configuration-stop-wait-period config))) (list (shepherd-service (provision '(transmission-daemon transmission bittorrent)) (requirement '(networking)) (documentation "Share files using the BitTorrent protocol.") (start #~(make-forkexec-constructor '(#$(file-append transmission "/bin/transmission-daemon") "--config-dir" #$%transmission-daemon-configuration-directory "--foreground") #:user #$%transmission-daemon-user #:group #$%transmission-daemon-group #:directory #$%transmission-daemon-configuration-directory #:log-file #$%transmission-daemon-log-file #:environment-variables '("CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt"))) (stop #~(lambda (pid) (kill pid SIGTERM) ;; Transmission Daemon normally needs some time to shut down, ;; as it will complete some housekeeping and send a final ;; update to trackers before it exits. ;; ;; Wait a reasonable period for it to stop before continuing. ;; If we don't do this, restarting the service can fail as the ;; new daemon process finds the old one still running and ;; attached to the port used for peer connections. (let wait-before-killing ((period #$stop-wait-period)) (if (zero? (car (waitpid pid WNOHANG))) (if (positive? period) (begin (sleep 1) (wait-before-killing (- period 1))) (begin (format #t #$(G_ "Wait period expired; killing \ transmission-daemon (pid ~a).~%") pid) (display #$(G_ "(If you see this message \ regularly, you may need to increase the value of 'stop-wait-period' in the service configuration.)\n")) (kill pid SIGKILL))))) #f)) (actions (list (shepherd-action (name 'reload) (documentation "Reload the settings file from disk.") (procedure #~(lambda (pid) (if pid (begin (kill pid SIGHUP) (display #$(G_ "Service transmission-daemon has \ been asked to reload its settings file."))) (display #$(G_ "Service transmission-daemon is not \ running.")))))))))))) (define %transmission-daemon-accounts (list (user-group (name %transmission-daemon-group) (system? #t)) (user-account (name %transmission-daemon-user) (group %transmission-daemon-group) (comment "Transmission Daemon service account") (home-directory %transmission-daemon-configuration-directory) (shell (file-append shadow "/sbin/nologin")) (system? #t)))) (define %transmission-daemon-log-rotations (list (log-rotation (files (list %transmission-daemon-log-file))))) (define (transmission-daemon-computed-settings-file config) "Return a @code{computed-file} object that, when unquoted in a G-expression, produces a Transmission settings file (@file{settings.json}) matching CONFIG." (let ((settings ;; "Serialize" the configuration settings as a list of G-expressions ;; containing a name-value pair, which will ultimately be sorted and ;; serialized to the settings file as a JSON object. (map (lambda (field) ((configuration-field-serializer field) (configuration-field-name field) ((configuration-field-getter field) config))) (filter (lambda (field) ;; Omit configuration fields that are used only internally by ;; this service definition. (not (memq (configuration-field-name field) '(transmission stop-wait-period)))) transmission-daemon-configuration-fields)))) (computed-file "settings.json" (with-extensions (list guile-gcrypt guile-json-4) (with-imported-modules (source-module-closure '((json builder))) #~(begin (use-modules (json builder)) (with-output-to-file #$output (lambda () (scm->json (sort-list '(#$@settings) (lambda (x y) (string<=? (car x) (car y)))) #:pretty #t))))))))) (define (transmission-daemon-activation config) "Return the Transmission Daemon activation GEXP for CONFIG." (let ((config-dir %transmission-daemon-configuration-directory) (incomplete-dir-enabled (transmission-daemon-configuration-incomplete-dir-enabled? config)) (incomplete-dir (transmission-daemon-configuration-incomplete-dir config)) (watch-dir-enabled (transmission-daemon-configuration-watch-dir-enabled? config)) (watch-dir (transmission-daemon-configuration-watch-dir config))) (with-imported-modules (source-module-closure '((guix build utils))) #~(begin (use-modules (guix build utils)) (let ((owner (getpwnam #$%transmission-daemon-user))) (define (mkdir-p/perms directory perms) (mkdir-p directory) (chown directory (passwd:uid owner) (passwd:gid owner)) (chmod directory perms)) ;; Create the directories Transmission Daemon is configured to use ;; and assign them suitable permissions. (for-each (lambda (directory-specification) (apply mkdir-p/perms directory-specification)) '(#$@(append `((,config-dir #o750)) (if incomplete-dir-enabled `((,incomplete-dir #o750)) '()) (if watch-dir-enabled `((,watch-dir #o770)) '()))))) ;; Generate and activate the daemon's settings file, settings.json. (activate-special-files '((#$(string-append config-dir "/settings.json") #$(transmission-daemon-computed-settings-file config)))))))) (define transmission-daemon-service-type (service-type (name 'transmission) (extensions (list (service-extension shepherd-root-service-type transmission-daemon-shepherd-service) (service-extension account-service-type (const %transmission-daemon-accounts)) (service-extension rottlog-service-type (const %transmission-daemon-log-rotations)) (service-extension activation-service-type transmission-daemon-activation))) (default-value (transmission-daemon-configuration)) (description "Share files using the BitTorrent protocol."))) (define (generate-transmission-daemon-documentation) (generate-documentation `((transmission-daemon-configuration ,transmission-daemon-configuration-fields)) 'transmission-daemon-configuration))