channels: 'latest-channel-instances' doesn't leak internal state.
[jackhill/guix/guix.git] / guix / git.scm
CommitLineData
6b7b3ca9 1;;; GNU Guix --- Functional package management for GNU
c3574749 2;;; Copyright © 2017, 2020 Mathieu Othacehe <m.othacehe@gmail.com>
6a7c4636 3;;; Copyright © 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
6b7b3ca9
MO
4;;;
5;;; This file is part of GNU Guix.
6;;;
7;;; GNU Guix is free software; you can redistribute it and/or modify it
8;;; under the terms of the GNU General Public License as published by
9;;; the Free Software Foundation; either version 3 of the License, or (at
10;;; your option) any later version.
11;;;
12;;; GNU Guix is distributed in the hope that it will be useful, but
13;;; WITHOUT ANY WARRANTY; without even the implied warranty of
14;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;;; GNU General Public License for more details.
16;;;
17;;; You should have received a copy of the GNU General Public License
18;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20(define-module (guix git)
21 #:use-module (git)
22 #:use-module (git object)
a3d77c51 23 #:use-module (guix i18n)
6b7b3ca9 24 #:use-module (guix base32)
ca719424 25 #:use-module (gcrypt hash)
0ad5f809 26 #:use-module ((guix build utils) #:select (mkdir-p))
6b7b3ca9
MO
27 #:use-module (guix store)
28 #:use-module (guix utils)
49ae3f6d
LC
29 #:use-module (guix records)
30 #:use-module (guix gexp)
873f6f13 31 #:use-module (guix sets)
6b7b3ca9
MO
32 #:use-module (rnrs bytevectors)
33 #:use-module (ice-9 match)
34 #:use-module (srfi srfi-1)
91881986 35 #:use-module (srfi srfi-11)
95bd9f65
LC
36 #:use-module (srfi srfi-34)
37 #:use-module (srfi srfi-35)
6b7b3ca9 38 #:export (%repository-cache-directory
bc041b3e
LC
39 honor-system-x509-certificates!
40
873f6f13 41 with-repository
91881986 42 update-cached-checkout
053b10c3 43 url+commit->name
49ae3f6d 44 latest-repository-commit
873f6f13 45 commit-difference
c098c11b 46 commit-relation
49ae3f6d
LC
47
48 git-checkout
49 git-checkout?
50 git-checkout-url
a063bac6
LC
51 git-checkout-branch
52 git-checkout-commit
53 git-checkout-recursive?))
6b7b3ca9
MO
54
55(define %repository-cache-directory
e83b2b0f
LC
56 (make-parameter (string-append (cache-directory #:ensure? #f)
57 "/checkouts")))
6b7b3ca9 58
bc041b3e
LC
59(define (honor-system-x509-certificates!)
60 "Use the system's X.509 certificates for Git checkouts over HTTPS. Honor
61the 'SSL_CERT_FILE' and 'SSL_CERT_DIR' environment variables."
62 ;; On distros such as CentOS 7, /etc/ssl/certs contains only a couple of
63 ;; files (instead of all the certificates) among which "ca-bundle.crt". On
64 ;; other distros /etc/ssl/certs usually contains the whole set of
65 ;; certificates along with "ca-certificates.crt". Try to choose the right
66 ;; one.
67 (let ((file (letrec-syntax ((choose
68 (syntax-rules ()
69 ((_ file rest ...)
70 (let ((f file))
71 (if (and f (file-exists? f))
72 f
73 (choose rest ...))))
74 ((_)
75 #f))))
76 (choose (getenv "SSL_CERT_FILE")
77 "/etc/ssl/certs/ca-certificates.crt"
78 "/etc/ssl/certs/ca-bundle.crt")))
79 (directory (or (getenv "SSL_CERT_DIR") "/etc/ssl/certs")))
80 (and (or file
81 (and=> (stat directory #f)
82 (lambda (st)
83 (> (stat:nlink st) 2))))
84 (begin
85 (set-tls-certificate-locations! directory file)
86 #t))))
87
88(define %certificates-initialized?
89 ;; Whether 'honor-system-x509-certificates!' has already been called.
90 #f)
91
6b7b3ca9 92(define-syntax-rule (with-libgit2 thunk ...)
b02469d2
MO
93 (begin
94 ;; XXX: The right thing to do would be to call (libgit2-shutdown) here,
95 ;; but pointer finalizers used in guile-git may be called after shutdown,
96 ;; resulting in a segfault. Hence, let's skip shutdown call for now.
97 (libgit2-init!)
bc041b3e
LC
98 (unless %certificates-initialized?
99 (honor-system-x509-certificates!)
100 (set! %certificates-initialized? #t))
b02469d2 101 thunk ...))
6b7b3ca9
MO
102
103(define* (url-cache-directory url
104 #:optional (cache-directory
60cbc6a8
LC
105 (%repository-cache-directory))
106 #:key recursive?)
6b7b3ca9
MO
107 "Return the directory associated to URL in %repository-cache-directory."
108 (string-append
109 cache-directory "/"
60cbc6a8
LC
110 (bytevector->base32-string
111 (sha256 (string->utf8 (if recursive?
112 (string-append "R:" url)
113 url))))))
6b7b3ca9 114
c3574749
MO
115;; Authentication appeared in Guile-Git 0.3.0, check if it is available.
116(define auth-supported?
117 (false-if-exception (resolve-interface '(git auth))))
118
6b7b3ca9
MO
119(define (clone* url directory)
120 "Clone git repository at URL into DIRECTORY. Upon failure,
121make sure no empty directory is left behind."
122 (with-throw-handler #t
123 (lambda ()
124 (mkdir-p directory)
195f0d05
LC
125
126 ;; Note: Explicitly pass options to work around the invalid default
127 ;; value in Guile-Git: <https://bugs.gnu.org/29238>.
b1488c76
LC
128 (if (module-defined? (resolve-interface '(git))
129 'clone-init-options)
c3574749
MO
130 (let ((auth-method (and auth-supported?
131 (%make-auth-ssh-agent))))
132 (clone url directory
133 (if auth-supported?
134 (make-clone-options
135 #:fetch-options (make-fetch-options auth-method))
136 (clone-init-options))))
b1488c76 137 (clone url directory)))
6b7b3ca9
MO
138 (lambda _
139 (false-if-exception (rmdir directory)))))
140
6b7b3ca9
MO
141(define (url+commit->name url sha1)
142 "Return the string \"<REPO-NAME>-<SHA1:7>\" where REPO-NAME is the name of
143the git repository, extracted from URL and SHA1:7 the seven first digits
144of SHA1 string."
145 (string-append
146 (string-replace-substring
147 (last (string-split url #\/)) ".git" "")
148 "-" (string-take sha1 7)))
149
6b7b3ca9 150(define (switch-to-ref repository ref)
91881986
LC
151 "Switch to REPOSITORY's branch, commit or tag specified by REF. Return the
152OID (roughly the commit hash) corresponding to REF."
95bd9f65 153 (define obj
c4c2449f
LC
154 (let resolve ((ref ref))
155 (match ref
156 (('branch . branch)
157 (let ((oid (reference-target
158 (branch-lookup repository branch BRANCH-REMOTE))))
159 (object-lookup repository oid)))
160 (('commit . commit)
161 (let ((len (string-length commit)))
162 ;; 'object-lookup-prefix' appeared in Guile-Git in Mar. 2018, so we
163 ;; can't be sure it's available. Furthermore, 'string->oid' used to
164 ;; read out-of-bounds when passed a string shorter than 40 chars,
165 ;; which is why we delay calls to it below.
166 (if (< len 40)
167 (if (module-defined? (resolve-interface '(git object))
168 'object-lookup-prefix)
169 (object-lookup-prefix repository (string->oid commit) len)
170 (raise (condition
171 (&message
172 (message "long Git object ID is required")))))
173 (object-lookup repository (string->oid commit)))))
174 (('tag-or-commit . str)
175 (if (or (> (string-length str) 40)
176 (not (string-every char-set:hex-digit str)))
177 (resolve `(tag . ,str)) ;definitely a tag
178 (catch 'git-error
179 (lambda ()
180 (resolve `(tag . ,str)))
181 (lambda _
182 ;; There's no such tag, so it must be a commit ID.
183 (resolve `(commit . ,str))))))
184 (('tag . tag)
185 (let ((oid (reference-name->oid repository
186 (string-append "refs/tags/" tag))))
efa578ec
LC
187 ;; OID may point to a "tag" object, but it can also point directly
188 ;; to a "commit" object, as surprising as it may seem. Return that
189 ;; object, whatever that is.
190 (object-lookup repository oid))))))
95bd9f65 191
91881986
LC
192 (reset repository obj RESET_HARD)
193 (object-id obj))
194
60cbc6a8
LC
195(define (call-with-repository directory proc)
196 (let ((repository #f))
197 (dynamic-wind
198 (lambda ()
199 (set! repository (repository-open directory)))
200 (lambda ()
201 (proc repository))
202 (lambda ()
203 (repository-close! repository)))))
204
205(define-syntax-rule (with-repository directory repository exp ...)
206 "Open the repository at DIRECTORY and bind REPOSITORY to it within the
207dynamic extent of EXP."
208 (call-with-repository directory
209 (lambda (repository) exp ...)))
210
6a7c4636
LC
211(define (load-git-submodules)
212 "Attempt to load (git submodules), which was missing until Guile-Git 0.2.0.
213Return true on success, false on failure."
214 (match (false-if-exception (resolve-interface '(git submodule)))
215 (#f
216 (set! load-git-submodules (const #f))
217 #f)
218 (iface
058d0251 219 (module-use! (resolve-module '(guix git)) iface)
6a7c4636
LC
220 (set! load-git-submodules (const #t))
221 #t)))
222
60cbc6a8
LC
223(define* (update-submodules repository
224 #:key (log-port (current-error-port)))
225 "Update the submodules of REPOSITORY, a Git repository object."
226 ;; Guile-Git < 0.2.0 did not have (git submodule).
6a7c4636 227 (if (load-git-submodules)
60cbc6a8
LC
228 (for-each (lambda (name)
229 (let ((submodule (submodule-lookup repository name)))
230 (format log-port (G_ "updating submodule '~a'...~%")
231 name)
232 (submodule-update submodule)
233
234 ;; Recurse in SUBMODULE.
235 (let ((directory (string-append
236 (repository-working-directory repository)
237 "/" (submodule-path submodule))))
238 (with-repository directory repository
239 (update-submodules repository
240 #:log-port log-port)))))
241 (repository-submodules repository))
242 (format (current-error-port)
243 (G_ "Support for submodules is missing; \
244please upgrade Guile-Git.~%"))))
245
a78dcb3d
LC
246(define (reference-available? repository ref)
247 "Return true if REF, a reference such as '(commit . \"cabba9e\"), is
248definitely available in REPOSITORY, false otherwise."
249 (match ref
250 (('commit . commit)
251 (catch 'git-error
252 (lambda ()
253 (->bool (commit-lookup repository (string->oid commit))))
254 (lambda (key error . rest)
255 (if (= GIT_ENOTFOUND (git-error-code error))
256 #f
257 (apply throw key error rest)))))
258 (_
259 #f)))
260
91881986
LC
261(define* (update-cached-checkout url
262 #:key
37a6cdbf 263 (ref '(branch . "master"))
60cbc6a8
LC
264 recursive?
265 (log-port (%make-void-port "w"))
91881986 266 (cache-directory
ffc3fcad 267 (url-cache-directory
60cbc6a8
LC
268 url (%repository-cache-directory)
269 #:recursive? recursive?)))
91881986
LC
270 "Update the cached checkout of URL to REF in CACHE-DIRECTORY. Return two
271values: the cache directory name, and the SHA1 commit (a string) corresponding
272to REF.
273
c4c2449f
LC
274REF is pair whose key is [branch | commit | tag | tag-or-commit ] and value
275the associated data: [<branch name> | <sha1> | <tag name> | <string>].
60cbc6a8
LC
276
277When RECURSIVE? is true, check out submodules as well, if any."
37a6cdbf
LC
278 (define canonical-ref
279 ;; We used to require callers to specify "origin/" for each branch, which
280 ;; made little sense since the cache should be transparent to them. So
281 ;; here we append "origin/" if it's missing and otherwise keep it.
282 (match ref
283 (('branch . branch)
284 `(branch . ,(if (string-prefix? "origin/" branch)
285 branch
286 (string-append "origin/" branch))))
287 (_ ref)))
288
91881986 289 (with-libgit2
ffc3fcad 290 (let* ((cache-exists? (openable-repository? cache-directory))
91881986 291 (repository (if cache-exists?
e3e1a7ba 292 (repository-open cache-directory)
ffc3fcad 293 (clone* url cache-directory))))
91881986 294 ;; Only fetch remote if it has not been cloned just before.
a78dcb3d
LC
295 (when (and cache-exists?
296 (not (reference-available? repository ref)))
c3574749
MO
297 (if auth-supported?
298 (let ((auth-method (and auth-supported?
299 (%make-auth-ssh-agent))))
300 (remote-fetch (remote-lookup repository "origin")
301 #:fetch-options (make-fetch-options auth-method)))
302 (remote-fetch (remote-lookup repository "origin"))))
60cbc6a8
LC
303 (when recursive?
304 (update-submodules repository #:log-port log-port))
37a6cdbf 305 (let ((oid (switch-to-ref repository canonical-ref)))
91881986
LC
306
307 ;; Reclaim file descriptors and memory mappings associated with
308 ;; REPOSITORY as soon as possible.
309 (when (module-defined? (resolve-interface '(git repository))
310 'repository-close!)
311 (repository-close! repository))
312
ffc3fcad 313 (values cache-directory (oid->string oid))))))
6b7b3ca9
MO
314
315(define* (latest-repository-commit store url
316 #:key
60cbc6a8 317 recursive?
35cb37ea 318 (log-port (%make-void-port "w"))
6b7b3ca9
MO
319 (cache-directory
320 (%repository-cache-directory))
37a6cdbf 321 (ref '(branch . "master")))
6b7b3ca9
MO
322 "Return two values: the content of the git repository at URL copied into a
323store directory and the sha1 of the top level commit in this directory. The
324reference to be checkout, once the repository is fetched, is specified by REF.
325REF is pair whose key is [branch | commit | tag] and value the associated
326data, respectively [<branch name> | <sha1> | <tag name>].
327
60cbc6a8
LC
328When RECURSIVE? is true, check out submodules as well, if any.
329
6b7b3ca9 330Git repositories are kept in the cache directory specified by
35cb37ea
LC
331%repository-cache-directory parameter.
332
333Log progress and checkout info to LOG-PORT."
91881986
LC
334 (define (dot-git? file stat)
335 (and (string=? (basename file) ".git")
60cbc6a8
LC
336 (or (eq? 'directory (stat:type stat))
337
338 ;; Submodule checkouts end up with a '.git' regular file that
339 ;; contains metadata about where their actual '.git' directory
340 ;; lives.
341 (and recursive?
342 (eq? 'regular (stat:type stat))))))
dfca2418 343
35cb37ea 344 (format log-port "updating checkout of '~a'...~%" url)
ffc3fcad
OP
345 (let*-values
346 (((checkout commit)
347 (update-cached-checkout url
60cbc6a8 348 #:recursive? recursive?
ffc3fcad
OP
349 #:ref ref
350 #:cache-directory
60cbc6a8
LC
351 (url-cache-directory url cache-directory
352 #:recursive?
353 recursive?)
354 #:log-port log-port))
ffc3fcad
OP
355 ((name)
356 (url+commit->name url commit)))
35cb37ea 357 (format log-port "retrieved commit ~a~%" commit)
91881986
LC
358 (values (add-to-store store name #t "sha256" checkout
359 #:select? (negate dot-git?))
360 commit)))
49ae3f6d 361
1d8b10d0
LC
362(define (print-git-error port key args default-printer)
363 (match args
364 (((? git-error? error) . _)
365 (format port (G_ "Git error: ~a~%")
366 (git-error-message error)))))
367
368(set-exception-printer! 'git-error print-git-error)
369
49ae3f6d 370\f
873f6f13
LC
371;;;
372;;; Commit difference.
373;;;
374
785af04a
LC
375(define* (commit-closure commit #:optional (visited (setq)))
376 "Return the closure of COMMIT as a set. Skip commits contained in VISITED,
377a set, and adjoin VISITED to the result."
873f6f13 378 (let loop ((commits (list commit))
785af04a 379 (visited visited))
873f6f13
LC
380 (match commits
381 (()
382 visited)
383 ((head . tail)
384 (if (set-contains? visited head)
385 (loop tail visited)
386 (loop (append (commit-parents head) tail)
387 (set-insert head visited)))))))
388
785af04a 389(define* (commit-difference new old #:optional (excluded '()))
873f6f13 390 "Return the list of commits between NEW and OLD, where OLD is assumed to be
785af04a
LC
391an ancestor of NEW. Exclude all the commits listed in EXCLUDED along with
392their ancestors.
873f6f13
LC
393
394Essentially, this computes the set difference between the closure of NEW and
395that of OLD."
396 (let loop ((commits (list new))
397 (result '())
785af04a 398 (visited (commit-closure old (list->setq excluded))))
873f6f13
LC
399 (match commits
400 (()
401 (reverse result))
402 ((head . tail)
403 (if (set-contains? visited head)
404 (loop tail result visited)
405 (loop (append (commit-parents head) tail)
406 (cons head result)
407 (set-insert head visited)))))))
408
c098c11b
LC
409(define (commit-relation old new)
410 "Return a symbol denoting the relation between OLD and NEW, two commit
411objects: 'ancestor (meaning that OLD is an ancestor of NEW), 'descendant, or
412'unrelated, or 'self (OLD and NEW are the same commit)."
413 (if (eq? old new)
414 'self
415 (let ((newest (commit-closure new)))
416 (if (set-contains? newest old)
417 'ancestor
418 (let* ((seen (list->setq (commit-parents new)))
419 (oldest (commit-closure old seen)))
420 (if (set-contains? oldest new)
421 'descendant
422 'unrelated))))))
423
873f6f13 424\f
49ae3f6d
LC
425;;;
426;;; Checkouts.
427;;;
428
b18f7234 429;; Representation of the "latest" checkout of a branch or a specific commit.
49ae3f6d
LC
430(define-record-type* <git-checkout>
431 git-checkout make-git-checkout
432 git-checkout?
433 (url git-checkout-url)
b18f7234 434 (branch git-checkout-branch (default "master"))
177fecb5 435 (commit git-checkout-commit (default #f)) ;#f | tag | commit
06fff484 436 (recursive? git-checkout-recursive? (default #f)))
49ae3f6d 437
06fff484 438(define* (latest-repository-commit* url #:key ref recursive? log-port)
a3d77c51
LC
439 ;; Monadic variant of 'latest-repository-commit'.
440 (lambda (store)
441 ;; The caller--e.g., (guix scripts build)--may not handle 'git-error' so
442 ;; translate it into '&message' conditions that we know will be properly
443 ;; handled.
444 (catch 'git-error
445 (lambda ()
446 (values (latest-repository-commit store url
06fff484
LC
447 #:ref ref
448 #:recursive? recursive?
449 #:log-port log-port)
a3d77c51
LC
450 store))
451 (lambda (key error . _)
452 (raise (condition
453 (&message
454 (message
455 (match ref
456 (('commit . commit)
457 (format #f (G_ "cannot fetch commit ~a from ~a: ~a")
458 commit url (git-error-message error)))
459 (('branch . branch)
460 (format #f (G_ "cannot fetch branch '~a' from ~a: ~a")
461 branch url (git-error-message error)))
462 (_
463 (format #f (G_ "Git failure while fetching ~a: ~a")
464 url (git-error-message error))))))))))))
49ae3f6d
LC
465
466(define-gexp-compiler (git-checkout-compiler (checkout <git-checkout>)
467 system target)
468 ;; "Compile" CHECKOUT by updating the local checkout and adding it to the
469 ;; store.
470 (match checkout
06fff484 471 (($ <git-checkout> url branch commit recursive?)
49ae3f6d 472 (latest-repository-commit* url
b18f7234 473 #:ref (if commit
177fecb5 474 `(tag-or-commit . ,commit)
b18f7234 475 `(branch . ,branch))
06fff484 476 #:recursive? recursive?
49ae3f6d 477 #:log-port (current-error-port)))))
60cbc6a8
LC
478
479;; Local Variables:
480;; eval: (put 'with-repository 'scheme-indent-function 2)
481;; End: