44 Commits

Author SHA1 Message Date
hans c90c407dce Before adding authentication layer 2026-08-13 16:46:06 +02:00
hans 575d41ae9f version 2026-08-13 15:04:11 +02:00
hans 405c17c9f6 stdin processing for run-git 2026-08-13 14:48:52 +02:00
hans 2d0aae1f87 Added git-tag and structured listing with -l/--list 2026-08-13 14:32:57 +02:00
hans 2e7cf0290a version 2026-08-13 13:56:16 +02:00
hans 14be30dfac Documentation. 2026-08-13 13:55:52 +02:00
hans 70883de138 output diff functions 2026-08-13 12:44:22 +02:00
hans 302dc06e89 Added configuration for diff2html 2026-08-13 12:43:44 +02:00
hans d1fe41b5cc Refactored show functionality 2026-08-13 12:07:29 +02:00
hans af9b57a8b9 version 2026-08-13 12:02:03 +02:00
hans d7b0520530 Added git show functionality 2026-08-13 12:01:21 +02:00
hans f4f2c76ec6 Documentation added and extended git log / git diif 2026-08-13 11:12:08 +02:00
hans 9741b1cf51 Revert "Diverse commando's toegevoegd. Ik weet nog niet of ik ze allemaal ga houden"
This reverts commit 003f3713f0.
2026-08-13 09:17:46 +02:00
hans 003f3713f0 Diverse commando's toegevoegd. Ik weet nog niet of ik ze allemaal ga houden 2026-08-13 02:14:49 +02:00
hans 2cb7e9310e Small changes. git main function is now a real function, not syntax 2026-08-13 02:00:08 +02:00
hans a025481ae4 Cleaned up the code. added some documentation. 2026-08-13 00:33:22 +02:00
hans c556b4f452 Version functionality added. 2026-08-13 00:28:09 +02:00
hans 32bf9570d3 Enter in prompt for commit message. 2026-08-12 22:44:42 +02:00
hans f4b1c7631f make commit ask for a commit message. 2026-08-12 22:44:04 +02:00
hans 5e7ee0cf22 arity 2026-08-12 22:39:40 +02:00
hans 78731ee677 arity problems solved 2026-08-12 22:32:44 +02:00
hans aebd420c3c Added git-grep and git-diff 2026-08-12 22:28:27 +02:00
hans e4e37d2214 exit code checking added 2026-08-12 16:50:57 +02:00
hans b832ab8184 Added add-porcelain 2026-08-12 16:04:04 +02:00
hans 0e804bb180 porcelain added where needed 2026-08-12 15:57:40 +02:00
hans 5319c0ce37 removed ' from command 2026-08-12 15:56:13 +02:00
hans e861f1f706 error handling 2026-08-12 15:54:32 +02:00
hans 90a2b1758d git command structure now in place 2026-08-12 15:50:53 +02:00
hans 5773450329 command structure 2026-08-12 14:59:50 +02:00
hans 7be7007285 added command structure for git calling 2026-08-12 14:58:47 +02:00
hans 5ee7145ac7 First setup with git cli utility 2026-08-12 14:13:40 +02:00
hans 6f558868a1 Restarting 2026-08-12 10:23:53 +02:00
hans 069fdbf921 make collection xref for git help 2026-08-11 22:53:28 +02:00
hans 7c6a338326 new version with git help 2026-08-11 22:35:01 +02:00
hans fea7fb7a59 a complete git commandline 2026-08-11 21:41:52 +02:00
hans c45f666f23 added several commands 2026-08-11 20:50:08 +02:00
hans 1c7d71ab07 missing commands git 'version/'switch and branch merge commands added 2026-08-11 15:30:24 +02:00
hans 856c9bd58e depdendency to package-zipper added 2026-08-11 14:20:33 +02:00
hans 4b8582f9e6 racket-makefile dependency added 2026-08-11 14:03:35 +02:00
hans e8fde58867 added git 'add '-A command 2026-08-11 13:59:52 +02:00
hans abb70f29ff More working commands 2026-08-11 13:56:50 +02:00
hans bf8c5b7e89 cleanup temporary information 2026-08-11 13:05:44 +02:00
hans 8027ad2c9c better handling for reporting on callbacks from c library 2026-08-11 11:08:17 +02:00
hans baf0ef5868 test 0.2.7a 2026-08-11 10:38:07 +02:00
25 changed files with 1604 additions and 4329 deletions
+5 -1
View File
@@ -1,5 +1,9 @@
/compiled /compiled
/doc /doc
/*.bak
/scribblings/compiled /scribblings/compiled
/tests/compiled /tests/compiled
/private/compiled
/docs
*~
*.bak
+32 -2
View File
@@ -2,6 +2,7 @@
(require racket-makefile (require racket-makefile
package-zipper package-zipper
net/sendurl
) )
@@ -11,11 +12,40 @@
(target clean (target clean
(for-each (λ (f) (displayln f) (rm-f f)) (list-files "." #px"([.]bak|~)$" #:recursive #t)) (for-each (λ (f) (displayln f) (rm-f f)) (list-files "." #px"([.]bak|~)$" #:recursive #t))
(for-each (λ (d) (displayln d) (rm-rf d)) (list-dirs "." #px"(compiled|doc)$" #:recursive #t)) (for-each (λ (d) (displayln d) (rm-rf d)) (list-dirs "." #px"(compiled|doc|docs)$" #:recursive #t))
(for-each (λ (f) (displayln f) (rm-f f)) (list-files "scribblings" #px"[.](css|js|html)$")) (when (directory-exists? "scribblings")
(for-each (λ (f) (displayln f) (rm-f f)) (list-files "scribblings" #px"[.](css|js|html)$")))
) )
(target package (target package
(deps clean) (deps clean)
(zip-package)) (zip-package))
(target zip
(deps package))
(define doc-target "docs/git.html")
(define doc-src "scribblings/git.scrbl")
(target doc-target
(deps doc-src)
(unless (directory-exists? "docs")
(make-directory "docs"))
(raco '(scribble --html +m --dest "docs" $<)))
(target doc
(deps doc-target)
(displayln "Documentation built")
)
(target showdoc
(deps doc)
(send-url/file doc-target))
(target refresh
(displayln "Refreshing makefile")
(refresh-makefile)
(displayln "done.")
)
(target setup
(raco '(setup git)))
+47 -82
View File
@@ -1,100 +1,65 @@
# git # git-cli
A small command-line-like Git module for Racket, implemented directly on top of the `libgit2` package. A small command-line-like Git interface for Racket. The package invokes the
installed `git` executable and exposes commands both through the generic `git`
procedure and through direct procedures.
```racket ```racket
(require git) (require git-cli)
(git 'status) (git 'status)
(git 'log '-l '-5)
(git 'fetch '--prune)
(git 'switch "main")
(git 'tag "v0.3.16")
(git 'diff) (git 'diff)
(git 'diff '--cached) (git 'show 'HEAD)
(git 'add "main.rkt" "info.rkt")
(git 'commit "Implement raco support")
(git 'tag "v0.2")
(git 'checkout "main")
;; Display-oriented variant: (git-status)
(dgit 'status) (git-fetch '--prune)
(git-switch "main")
(git-tag "v0.3.16")
``` ```
`git` is an ordinary procedure; command names are symbols. `dgit` performs the `git` is an ordinary procedure. The first argument is the Git command symbol
same operation, displays a compact human-readable result, and returns that result. and the remaining arguments are passed to that command.
The same operations are available as normal procedures such as `git-status`, `git-add`, `git-commit`, `git-tag`, and `git-checkout`. Several commands provide Racket-oriented output in addition to the normal Git
behavior:
## HTTPS credentials - `git-status` uses Git's porcelain status and returns structured status items.
- `git-log -l` / `git-log --list` returns `(commit subject)` items.
- `git-tag -l` / `git-tag --list` returns tag names; with `-n` it returns `(tag subject)` items and `-n<number>` supports multiple content lines.
- `git-diff` renders HTML by default; `--output=-` selects stdout and
`--output=string` returns a string.
- `git-show` renders a commit and its diff as HTML by default. `-l` /
`--list` provides structured variants for `--stat`, `--name-only`, and
`--name-status`.
Version 0.2 adds persistent HTTPS credentials. They are stored in the Git is searched on `PATH`. Git itself remains responsible for remotes,
`racket-git.ini` file in Racket's preferences directory. Tokens are encrypted credentials, SSH keys, pull strategy, and other repository configuration.
with AES-GCM using a key derived from the store password with
PBKDF2-HMAC-SHA256.
Create the credential store once: ## Commands
The package currently registers commands including `status`, `add`, `commit`,
`push`, `pull`, `fetch`, `branch`, `switch`, `clone`, `tag`, `log`,
`rev-list`, `diff`, `show`, `grep`, `help`, `version`, and `new-version`.
Most are also exported as direct procedures such as `git-status`, `git-add`,
`git-fetch`, `git-switch`, `git-tag`, `git-log`, `git-diff`, and `git-show`.
See the Scribble documentation for command-specific behavior and return values.
## Low-level Git execution
`run-git` can be used when direct access to Git's stdin/stdout protocol is
needed. Optional text can be supplied to Git with `#:input`.
```racket ```racket
(git 'credentials 'init "store password") (run-git '(credential fill)
#:input "protocol=https\nhost=git.dijkewijk.nl\n\n")
``` ```
Store a token for a Git host: The result remains two values: Git's exit code and the ordered
`(source line)` output items.
```racket
(git 'credentials 'set
"https://git.dijkewijk.nl"
"hans"
token)
```
The host is used as the credential key, so the same entry is used for all HTTPS
repositories on that host.
The store is unlocked for one day by default:
```racket
(git 'credentials 'unlock "store password")
```
or for an explicit number of seconds:
```racket
(git 'credentials 'unlock "store password" (* 8 60 60))
```
The temporary unlock state is stored in `racket-git-unlock.ini` in Racket's
preferences directory, so it survives restarting DrRacket and starting a new
Racket process. Both files are opened through `simple-ini` with `#:private? #t`; on Unix this
restricts them to mode 0600 before sensitive contents are written. The cached
derived key grants access to the credentials until its expiry,
so `racket-git-unlock.ini` must be treated as sensitive during that period.
Lock immediately with:
```racket
(git 'credentials 'lock)
```
After credentials have been stored and the store is unlocked, normal remote
operations use them automatically:
```racket
(git 'fetch)
(git 'pull)
(git 'push)
;; Suppress network progress when desired:
(git 'push #:quiet #t)
```
## Supported Git operations
Version 0.2 supports repository discovery, init, clone, status, add, config,
commit, branch, checkout, lightweight tags, log, remotes, fetch,
fast-forward-only pull, push, tag push, network transfer progress, and HTTPS username/token credentials.
SSH credentials, merge/rebase pull, annotated tags, and submodules are not yet
part of this module.
Install from the package directory with:
```sh
raco pkg install .
```
-230
View File
@@ -1,230 +0,0 @@
#lang racket/base
(require crypto
crypto/all
net/base64
racket/file
racket/string
simple-ini)
(provide git-credentials-store
git-credentials-unlock-store
current-git-credentials-store
current-git-credentials-unlock-store
git-credentials-init!
git-credentials-unlock!
git-credentials-lock!
git-credentials-unlocked?
git-credentials-unlock-expires
git-credentials-set!
git-credentials-ref
git-credentials-configured?
git-credentials-remove!)
(define git-credentials-store 'racket-git)
(define git-credentials-unlock-store 'racket-git-unlock)
;; Parameters make the storage location overridable for tests or embedded use,
;; while the public default remains the normal Racket preference stores.
(define current-git-credentials-store (make-parameter git-credentials-store))
(define current-git-credentials-unlock-store (make-parameter git-credentials-unlock-store))
(define settings-section 'settings)
(define unlock-section 'unlock)
(define kdf-iterations 200000)
(define cipher '(aes gcm))
(define check-text #"racket-git credential store")
(define-syntax-rule (with-git-crypto body ...)
(parameterize ([crypto-factories all-factories])
body ...))
(define (b64-encode bytes)
(bytes->string/utf-8 (base64-encode bytes #"")))
(define (b64-decode string)
(base64-decode (string->bytes/utf-8 string)))
(define (store-read name)
(file->ini name))
(define (store-write name ini)
(make-directory* (find-system-path 'pref-dir))
(ini->file ini name #:private? #t)
(void))
(define (store-set! name section key value)
(define ini (store-read name))
(ini-set! ini section key value)
(store-write name ini))
(define (store-get name section key [default #f])
(ini-get (store-read name) section key default))
(define (derive-key password salt)
(with-git-crypto
(pbkdf2-hmac 'sha256
(string->bytes/utf-8 password)
salt
#:iterations kdf-iterations
#:key-size 32)))
(define (encrypt-value key plaintext aad)
(with-git-crypto
(define iv (generate-cipher-iv cipher))
(define encrypted
(encrypt cipher key iv (string->bytes/utf-8 plaintext)
#:aad (string->bytes/utf-8 aad)))
(string-append (b64-encode iv) ":" (b64-encode encrypted))))
(define (decrypt-value key encoded aad)
(with-git-crypto
(define parts (string-split encoded ":"))
(unless (= (length parts) 2)
(error 'git-credentials "invalid encrypted credential data"))
(bytes->string/utf-8
(decrypt cipher key
(b64-decode (car parts))
(b64-decode (cadr parts))
#:aad (string->bytes/utf-8 aad)))))
(define (credential-key remote)
(define url-match
(regexp-match #px"^[A-Za-z][A-Za-z0-9+.-]*://(?:[^/@]+@)?([^/:]+)" remote))
(define ssh-match
(regexp-match #px"^[^@]+@([^:]+):" remote))
(string-downcase
(cond
[url-match (cadr url-match)]
[ssh-match (cadr ssh-match)]
[else remote])))
(define (string->hex string)
(apply string-append
(for/list ([b (in-bytes (string->bytes/utf-8 string))])
(let ([h (number->string b 16)])
(if (= (string-length h) 1) (string-append "0" h) h)))))
(define (credential-section remote)
;; simple-ini deliberately accepts a conservative section-name syntax.
;; Hex keeps arbitrary host names reversible and section-safe.
(string->symbol (string-append "credential."
(string->hex (credential-key remote)))))
(define (git-credentials-init! password #:unlock-for [seconds 86400])
(unless (and (string? password) (positive? (string-length password)))
(raise-argument-error 'git-credentials-init! "non-empty string?" password))
(define existing-salt (store-get (current-git-credentials-store) settings-section 'salt #f))
(when existing-salt
(error 'git-credentials-init! "credential store is already initialized"))
(define salt (with-git-crypto (crypto-random-bytes 16)))
(define key (derive-key password salt))
(define ini (store-read (current-git-credentials-store)))
(ini-set! ini settings-section 'version 1)
(ini-set! ini settings-section 'kdf "pbkdf2-hmac-sha256")
(ini-set! ini settings-section 'iterations kdf-iterations)
(ini-set! ini settings-section 'salt (b64-encode salt))
(ini-set! ini settings-section 'check
(encrypt-value key (bytes->string/utf-8 check-text) "check"))
(store-write (current-git-credentials-store) ini)
(cache-unlock-key! key seconds)
(void))
(define (cache-unlock-key! key seconds)
(unless (and (real? seconds) (> seconds 0))
(raise-argument-error 'git-credentials-unlock! "positive real?" seconds))
(define ini (store-read (current-git-credentials-unlock-store)))
(ini-set! ini unlock-section 'key (b64-encode key))
(ini-set! ini unlock-section 'expires (+ (current-seconds) seconds))
(store-write (current-git-credentials-unlock-store) ini)
(void))
(define (git-credentials-unlock! password #:for [seconds 86400])
(define salt-text (store-get (current-git-credentials-store) settings-section 'salt #f))
(define check (store-get (current-git-credentials-store) settings-section 'check #f))
(unless (and salt-text check)
(error 'git-credentials-unlock! "credential store is not initialized"))
(define key (derive-key password (b64-decode salt-text)))
(with-handlers ([exn:fail?
(lambda (_)
(error 'git-credentials-unlock! "invalid password"))])
(unless (string=? (decrypt-value key check "check")
(bytes->string/utf-8 check-text))
(error 'git-credentials-unlock! "invalid password")))
(cache-unlock-key! key seconds)
(void))
(define (git-credentials-lock!)
(define ini (store-read (current-git-credentials-unlock-store)))
(ini-set! ini unlock-section 'key "")
(ini-set! ini unlock-section 'expires 0)
(store-write (current-git-credentials-unlock-store) ini)
(void))
(define (git-credentials-unlock-expires)
(define expires (store-get (current-git-credentials-unlock-store) unlock-section 'expires 0))
(if (number? expires) expires 0))
(define (git-credentials-unlocked?)
(define key (store-get (current-git-credentials-unlock-store) unlock-section 'key ""))
(define expires (git-credentials-unlock-expires))
(cond
[(and (string? key)
(not (string=? key ""))
(> expires (current-seconds)))
#t]
[else
(when (and (number? expires) (positive? expires))
(git-credentials-lock!))
#f]))
(define (current-key who)
(unless (git-credentials-unlocked?)
(error who "credential store 'racket-git is locked"))
(b64-decode
(store-get (current-git-credentials-unlock-store) unlock-section 'key "")))
(define (git-credentials-set! remote username token)
(unless (string? remote)
(raise-argument-error 'git-credentials-set! "string?" remote))
(unless (string? username)
(raise-argument-error 'git-credentials-set! "string?" username))
(unless (string? token)
(raise-argument-error 'git-credentials-set! "string?" token))
(define key (current-key 'git-credentials-set!))
(define section (credential-section remote))
(define ini (store-read (current-git-credentials-store)))
(ini-set! ini section 'username username)
(ini-set! ini section 'token
(encrypt-value key token (string-append (credential-key remote) ":" username)))
(store-write (current-git-credentials-store) ini)
(void))
(define (git-credentials-configured? remote)
(define section (credential-section remote))
(define username (store-get (current-git-credentials-store) section 'username #f))
(define encrypted (store-get (current-git-credentials-store) section 'token #f))
(and (string? username) (not (string=? username ""))
(string? encrypted) (not (string=? encrypted ""))))
(define (git-credentials-ref remote)
(define section (credential-section remote))
(define username (store-get (current-git-credentials-store) section 'username #f))
(define encrypted (store-get (current-git-credentials-store) section 'token #f))
(cond
[(and (string? username) (not (string=? username ""))
(string? encrypted) (not (string=? encrypted "")))
(define key (current-key 'git-credentials-ref))
(cons username
(decrypt-value key encrypted (string-append (credential-key remote) ":" username)))]
[else #f]))
(define (git-credentials-remove! remote)
;; simple-ini has no section-delete primitive. Clearing both values keeps
;; the file format simple and makes git-credentials-ref return #f.
(define section (credential-section remote))
(define ini (store-read (current-git-credentials-store)))
(ini-set! ini section 'username "")
(ini-set! ini section 'token "")
(store-write (current-git-credentials-store) ini)
(void))
+9 -8
View File
@@ -1,21 +1,22 @@
#lang info #lang info
(define collection "git") (define collection "git-cli")
(define pkg-desc "Command-line-like Git operations for Racket, implemented with libgit2") (define pkg-desc "Command-line-like Git operations for Racket, interface to the git cli command")
(define version "0.2.6") (define version "0.4")
(define pkg-authors '("Hans Dijkema")) (define pkg-authors '("Hans Dijkema"))
(define license 'MIT) (define license 'MIT)
(define deps (define deps
'("base" '("base"
"libgit2" "simple-ini"
("simple-ini" #:version "0.3.3") "simple-log"
"crypto-lib" "racket-index"
"net-lib")) "scribble-lib"
"racket-makefile"
"package-zipper"))
(define build-deps (define build-deps
'("rackunit-lib" '("rackunit-lib"
"scribble-lib"
"racket-doc")) "racket-doc"))
(define scribblings (define scribblings
+573 -767
View File
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
#lang racket/base
(require simple-ini/class
simple-log
)
(provide cfg-get
cfg-set!
dbg-git
info-git
err-git
warn-git
fatal-git
sync-log-git
)
(sl-def-log git)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Internal state / functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define ini #f)
(define (check-ini)
(when (eq? ini #f)
(set! ini (new ini% [file 'git-cli]))))
(define mutex (make-semaphore 1))
(define-syntax critical
(syntax-rules ()
((_ b1 ...)
(dynamic-wind
(λ () (semaphore-wait mutex))
(λ () b1 ...)
(λ () (semaphore-post mutex)))
)
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Provided functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Store one git-cli configuration value.
; pre : section and key identify a simple-ini setting.
; post : value has been persisted.
; result : The result returned by simple-ini.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (cfg-set! section key value)
(critical
(check-ini)
(send ini set! section key value)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Read one git-cli configuration value.
; pre : section and key identify a simple-ini setting.
; post : Configuration has not been changed.
; result : The stored value or default-value.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (cfg-get section key default-value)
(critical
(check-ini)
(send ini get section key default-value)))
+161
View File
@@ -0,0 +1,161 @@
#lang racket/base
(require net/sendurl
racket/list
json
xml
racket/string
"config.rkt"
"utils.rkt"
)
(provide diff->html
show->html
config-diff2html
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Helper functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (make-js . args)
(string-join args "\n"))
(define (highlight-css)
(cfg-get 'diff 'highlight-css
"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css")
)
(define (diff2html-min-css)
(cfg-get 'diff 'diff2html-min-css
"https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css")
)
(define (diff2html-ui-min-js)
(cfg-get 'diff 'diff2html-ui-min-js
"https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js")
)
(define (diff-script diff)
(format
(make-js
"window.do_diff = function() {"
"const diff = ~a;"
"const ui = new Diff2HtmlUI("
" document.getElementById('diff'),"
" diff,"
" {"
" drawFileList: true,"
" matching: 'lines',"
" outputFormat: 'side-by-side',"
" });"
"ui.draw();"
"ui.highlightCode();"
"};"
)
(jsexpr->string diff))
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exported functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (config-diff2html)
(define (checker f)
(λ (inp)
(let ((url (string-trim inp)))
(if (string=? url "")
(f)
(if (valid-http-or-file-url? url)
url
(begin
(displayln "! Not a valid url, please input a valid url")
#f)))))
)
(define (inp name f)
(input-prompt (string-join
(list
(format "Give the url for the ~a" name)
"Enter keeps the current value:"
(string-append " - " (f))
">")
"\n")
#:loop-until (checker f)))
(let ((h-css (inp "Highlighting CSS" highlight-css))
(d-css (inp "Diff2Html CSS" diff2html-min-css))
(d-js (inp "Diff2Html UI Javascript" diff2html-ui-min-js))
)
(cfg-set! 'diff 'highlight-css h-css)
(cfg-set! 'diff 'diff2html-min-css d-css)
(cfg-set! 'diff 'diff2html-ui-min-js d-js))
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Render a Git diff in a temporary HTML file.
; pre : diff is a unified Git diff string.
; post : The generated HTML file has been opened in the default browser.
; result : void.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (diff->html diff)
(let ((html `(html
(head
(meta ((charset "utf-8")))
(link ((rel "stylesheet") (href ,(highlight-css))))
(link ((rel "stylesheet") (href ,(diff2html-min-css))))
(script ((src ,(diff2html-ui-min-js))) "")
(script ,(diff-script diff))
)
(body
(div ((id "diff")))
(script ((type "text/javascript"))
"window.do_diff();")
))))
(let ((tmp-file (build-path (find-system-path 'temp-dir) "racket-git-diff.html")))
(call-with-output-file tmp-file #:exists 'truncate
(λ (out)
(display (xexpr->string html) out)))
(send-url/file tmp-file))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Render git show output as commit information followed by a diff.
; pre : show is the textual output produced by git show.
; post : The generated HTML file has been opened in the default browser.
; result : void.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (show->html show)
(let* ((lines (string-split show "\n" #:trim? #f))
(diff-pos (let loop ((rest lines)
(n 0))
(cond
((null? rest) #f)
((string-prefix? (car rest) "diff --git ") n)
(else (loop (cdr rest) (+ n 1))))))
(header (string-join (if diff-pos (take lines diff-pos) lines) "\n"))
(diff (string-join (if diff-pos (drop lines diff-pos) '()) "\n"))
(html `(html
(head
(meta ((charset "utf-8")))
(link ((rel "stylesheet") (href ,(highlight-css))))
(link ((rel "stylesheet") (href ,(diff2html-min-css))))
(style "body { font-family: sans-serif; margin: 1.5em; } pre.commit { white-space: pre-wrap; }")
,@(if diff-pos
`((script ((src ,(diff2html-ui-min-js))) "")
(script ,(diff-script diff)))
'())
)
(body
(pre ((class "commit")) ,header)
,@(if diff-pos
`((div ((id "diff")))
(script ((type "text/javascript"))
"window.do_diff();"))
'())
))))
(let ((tmp-file (build-path (find-system-path 'temp-dir) "racket-git-show.html")))
(call-with-output-file tmp-file #:exists 'truncate
(λ (out)
(display (xexpr->string html) out)))
(send-url/file tmp-file))))
+100
View File
@@ -0,0 +1,100 @@
#lang racket/base
(require "git-provider.rkt"
racket/string
racket/list
)
(provide def-git-cmd-proxy
check-git-args
has-git-arg?
std-process-git-result
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Determine whether a Git option occurs in an argument list.
; pre : args is a list and opt is a symbol, string or regular expression.
; post : args has only been inspected.
; result : The match result, or #f when the option is absent.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (has-git-arg? args opt)
(let ((cmp (cond ((symbol? opt) (λ (x) (eq? x opt)))
((string? opt) (λ (x) (string=? (format "~a" x) opt)))
((regexp? opt) (λ (x) (regexp-match opt (format "~a" x))))
(else (error "opt must be a string, symbol or regular expression")))))
(letrec ((f (λ (args)
(if (null? args)
#f
(let ((m (cmp (car args))))
(if m
m
(f (cdr args))))))))
(if (list? args)
(f args)
(error 'has-git-arg? "args must be a list of arguments")))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Check that mandatory Git options and their arguments are present.
; pre : flags contains (option argument-count error-message) items.
; post : Missing options have raised an exception.
; result : args when every mandatory option is present.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (check-git-args cmd args flags)
(for-each
(λ (opt)
(let ((flag (car opt))
(num-args (cadr opt))
(err-msg (caddr opt)))
(letrec ((find (λ (l)
(if (null? l)
#f
(if (eq? (car l) flag)
(if (>= (length (cdr l)) num-args)
#t
#f)
(find (cdr l)))))))
(let ((found (find args)))
(unless found
(error 'git (format "git ~a: ~a" cmd err-msg))))
)
))
flags)
args)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Process the standard result of a Git command.
; pre : exit-code and out belong to the completed Git command.
; post : Successful output has been displayed or a Git exception has been raised.
; result : #t when exit-code is zero.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (std-process-git-result cmd exit-code result output out info)
(if (= exit-code 0)
(begin
(git-displ (map cadr output))
#t)
(git-error cmd (format "Exitcode <> 0: ~a" exit-code) out)
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Define the internal proxy for a Git command.
; pre : pre-code and process-result accept the command proxy arguments.
; post : The proxy invokes Git without standard input and processes its result.
; result : A procedure named f accepting a list of Git arguments.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-syntax def-git-cmd-proxy
(syntax-rules ()
((_ f cmd pre-code process-result)
(define (f args*)
(let* ((args (flatten args*))
(info (make-hash))
(nargs (pre-code args info)))
(let-values (((exit-code output) (run-git (cons cmd nargs))))
(let-values (((result out) (git-out cmd output)))
(process-result cmd exit-code result output out info))))))
)
)
+208
View File
@@ -0,0 +1,208 @@
#lang racket/base
(require racket/path
racket/string
racket/contract
racket/system
"config.rkt"
)
(provide git-exe
set-git-exe!
run-git
git-out
git-error
git-displ
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Supporting functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define cached-git-exe #f)
(define (ask-for-git-executable)
(displayln "Git was not found on PATH.")
(displayln "Enter the full path to git/git.exe, or press Enter to abort:")
(let loop ()
(display "> ")
(flush-output)
(let ((answer (read-line)))
(when (or (eof-object? answer)
(string=? (string-trim answer) ""))
(error 'git "Git executable not found; configuration aborted"))
(let ((candidate (find-executable-path answer)))
(if (eq? candidate #f)
(begin
(displayln
(format "'~a' is not found, please try again or press Enter to abort." answer))
(loop))
(begin
(set-git-exe! candidate)
#t)))))
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Provided functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Find the configured Git executable.
; pre : Git is on PATH or a valid executable can be selected interactively.
; post : The executable path has been cached.
; result : The path to git or git.exe.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define/contract (git-exe)
(-> (or/c path? #f))
(if (eq? cached-git-exe #f)
(let ((the-git-exe (let ((exe (cfg-get 'git 'exe #f)))
(if (eq? exe #f)
(let ((path (find-executable-path "git")))
(if (eq? path #f)
(if (ask-for-git-executable)
(git-exe)
#f)
path))
exe
))))
(set! cached-git-exe the-git-exe)
the-git-exe)
cached-git-exe))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Configure the Git executable.
; pre : exe-path names an executable path.
; post : The path has been stored and cached.
; result : void.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define/contract (set-git-exe! exe-path)
(-> path? void?)
(void
(begin
(cfg-set! 'git 'exe exe-path)
(set! cached-git-exe exe-path))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Run Git without allowing interactive terminal prompts.
; pre : args contains the Git command and its arguments; input is #f or a string
; that must be written to Git's standard input.
; post : Optional input has been written and standard output and error have been
; read completely.
; result : The exit code and ordered (source line) output items.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (run-git args #:input (input #f))
(putenv "GIT_TERMINAL_PROMPT" "0")
(let-values (((process stdout stdin stderr)
(apply subprocess
#f
#f
#f
(git-exe)
(map (λ (arg) (format "~a" arg)) args)
)))
(when input
(display input stdin)
(flush-output stdin))
(close-output-port stdin)
(let ((output-channel (make-channel)))
(define (read-output source port)
(thread
(lambda ()
(let loop ()
(let ((line (read-line port)))
(channel-put output-channel (list source line))
(if (eof-object? line)
(close-input-port port)
(loop)))))))
(read-output 'stdout stdout)
(read-output 'stderr stderr)
(let loop ((open-ports 2)
(result '()))
(if (= open-ports 0)
(begin
(subprocess-wait process)
(values (subprocess-status process)
(reverse result)))
(let* ((output (channel-get output-channel))
(line (cadr output)))
(if (eof-object? line)
(loop (- open-ports 1) result)
(loop open-ports
(cons output result)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Provided utility functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (is-output? e)
(or (eq? (car e) 'stdout)
(and (eq? (car e) 'stderr)
(string-prefix? (string-downcase (cadr e)) "warning:"))))
(define (is-error? e)
(not (is-output? e)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Separate normal Git output from error output.
; pre : output contains (source line) items returned by run-git.
; post : output has only been inspected.
; result : Whether no error occurred and either normal or error lines.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (git-out cmd output)
(let* ((out (map (λ (e) (cadr e)) (filter (λ (e) (is-output? e)) output)))
(err (map (λ (e) (cadr e)) (filter (λ (e) (is-error? e)) output)))
(r (null? err))
)
(values r (if (eq? r #t) out err))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Log and raise a Git exception.
; pre : cmd, msg* and outp describe a failed Git command.
; post : The message has been logged and an exception has been raised.
; result : No normal return value.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-syntax git-error
(syntax-rules ()
((_ cmd msg* outp)
(let* ((out (map (λ (e) (if (list? e)
(if (null? e)
""
(if (or (eq? (car e) 'stdout)
(eq? (car e) 'stderr))
(format "~a" (cadr e))
(format "~a" e)))
(format "~a" e)))
(if (list? outp) outp (list outp))))
(enter (if (eq? (system-type 'os) 'windows) "\n" "\n"))
(msg (format "git ~a: ~a: ~a" cmd msg* (string-join out enter)))
)
(err-git msg)
(error 'git msg))
)
)
)
(define re-a #px"~+")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Log Git output and optionally display it.
; pre : out is a string or a list of displayable lines.
; post : Non-empty output has been logged.
; result : void.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (git-displ out)
(let ((str (if (string? out) out (string-join out "\n"))))
(unless (string=? (string-trim str) "")
(let ((s (regexp-replace* re-a str "~~")))
(info-git s))
(when (cfg-get 'git 'display-output #t)
(displayln str)))))
+101
View File
@@ -0,0 +1,101 @@
#lang racket/base
(require setup/getinfo
racket/string
)
(provide info-version
set-info-version!
git-next-version
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Read a package version from info.rkt.
; pre : dir contains a readable info.rkt.
; post : info.rkt has only been inspected.
; result : A list containing major, minor and patch.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (info-version dir)
(let* ((l (get-info/full dir))
(re #px"([0-9]+)[.]([0-9]+)([.]([0-9]+))?")
(v (with-handlers ([exn:fail?
(λ (e) "0.1")])
(l 'version)))
(m (regexp-match re v))
)
(map string->number
(list (cadr m) (caddr m) (if (eq? (cadddr (cdr m)) #f)
"0"
(cadddr (cdr m)))))
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Store a package version in info.rkt.
; pre : dir contains info.rkt and version parts are numbers.
; post : The version definition has been replaced.
; result : #t after writing the file.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (set-info-version! dir maj min patch)
(define (write-version fh)
(let ((str (if (= patch 0)
(format "(define version \"~a.~a\")" maj min)
(format "(define version \"~a.~a.~a\")" maj min patch))))
(unless (eq? fh #f)
(displayln str fh))
str))
(let ((info-file (build-path dir "info.rkt")))
(unless (file-exists? info-file)
(error (format "No info.rkt exists at ~a" info-file)))
(let ((fh (open-input-file info-file)))
(letrec ((reader (λ ()
(let ((line (read-line fh)))
(if (eof-object? line)
'()
(cons line (reader)))))))
(let* ((text (string-join (reader) "\n")))
(close-input-port fh)
(let* ((re #px"[(]define\\s+version\\s+[\"][^\"]+[\"]\\s*[)]")
(ntext (regexp-replace re text (write-version #f)))
(fout (open-output-file info-file #:exists 'replace))
)
(display ntext fout)
(close-output-port fout)
#t)))))
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Increment a package version.
; pre : kind is maj, major, min, minor or patch.
; post : The version definition in info.rkt has been updated.
; result : #t after writing the new version.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (git-next-version kind . dir*)
(let ((dir (if (null? dir*)
"."
(car dir*))))
(if (memq kind '(maj major min minor patch))
(let ((v (info-version dir)))
(cond
((or (eq? kind 'maj)
(eq? kind 'major))
(apply set-info-version! (cons dir
(list (+ (car v) 1) 0 0))))
((or (eq? kind 'min)
(eq? kind 'minor))
(apply set-info-version! (cons dir
(list (car v) (+ (cadr v) 1) 0))))
(else
(apply set-info-version! (cons dir
(list (car v) (cadr v)
(+ (caddr v) 1)))))
)
)
(error "kind must be 'maj, 'major, 'min, 'minor or 'patch")
)
)
)
+32
View File
@@ -0,0 +1,32 @@
#lang racket/base
(require net/url)
(provide input-prompt
valid-http-or-file-url?
)
(define (input-prompt p #:loop-until [until (λ (x) x)])
(let loop ()
(display p)
(flush-output)
(let ((inp (read-line)))
(let ((i (until inp)))
(if i
i
(loop)))
)
)
)
(define (valid-http-or-file-url? value)
(if (not (string? value))
#f
(with-handlers ([exn:fail? (λ (exn) #f)])
(let ((url (string->url value)))
(and
(member (url-scheme url) '("http" "https" "file"))
(string? (url-host url))
(not (string=? (url-host url) "")))))))
File diff suppressed because one or more lines are too long
+242 -150
View File
@@ -1,179 +1,271 @@
#lang scribble/manual #lang scribble/manual
@(require (for-label racket/base git)) @(require (for-label racket/base
racket/contract
"../main.rkt"))
@title{git} @title[#:tag "top"]{git-cli}
@author{Hans Dijkema} @author{Hans Dijkema}
@defmodule[git] @defmodule[git-cli]
The @racketmodname[git] module provides a small command-line-like Git interface implemented on top of the @tt{libgit2} package. It does not invoke the @tt{git} executable. The @racketmodname[git-cli] module provides a command-line-like Git interface
implemented by invoking the @tt{git} executable. Commands do not allow Git to
read credentials or other answers from the terminal.
The short form is intended for build scripts and interactive use: @section{Command interface}
@racketblock[ @defform[(git command argument ...)]{
(require git) Runs a registered Git @racket[command]. The arguments are passed to the command.
Registered command symbols are @racket['status], @racket['add],
@racket['commit], @racket['push], @racket['pull], @racket['fetch],
@racket['branch], @racket['switch], @racket['clone], @racket['tag],
@racket['log], @racket['rev-list], @racket['diff],
@racket['show], @racket['grep], @racket['help], @racket['version], and
@racket['new-version].
(git 'status) Most registered commands invoke the Git command with the same name. Some
(git 'diff) commands process the result into a Racket value, such as @racket['status],
(git 'diff '--cached) @racket['grep], @racket['log] with @tt{--list}, @racket['version], and
(git 'add "main.rkt" "info.rkt") @racket['new-version].
(git 'commit "Implement raco support")
(git 'tag "v0.1")
(git 'checkout "main")
]
@defproc[(git [command symbol?] [#:quiet quiet any/c #f] [argument any/c] ...) any/c]{
Dispatches @racket[command] to the corresponding Git procedure. For example, @racket[(git 'status)] calls @racket[git-status], and @racket[(git 'commit "message")] calls @racket[git-commit]. Command names are ordinary symbols, so @racket[git] can safely be used inside other macros and DSLs. For network commands, @racket[#:quiet] suppresses progress output.
} }
@defproc[(dgit [command symbol?] [#:quiet quiet any/c #f] [argument any/c] ...) any/c]{ @section{Provided commands}
Calls @racket[git], displays its result in a compact human-readable form, and returns the original result. Status entries are displayed with labels such as @tt{Modified}, @tt{New}, @tt{Deleted}, and @tt{Renamed}. Ignored files remain omitted, just as with @racket[git-status].
@defproc[(git-status [argument any/c] ...) list?]{
Runs @tt{git status --porcelain} with the supplied arguments.
Each result item has the form
@racket[(index-status worktree-status file)]. The index status describes the
change staged for the next commit. The worktree status describes the change in
the working tree relative to the index.
Both statuses are one of @racket['unchanged], @racket['modified],
@racket['type-changed], @racket['added], @racket['deleted], @racket['renamed],
@racket['copied], @racket['unmerged], @racket['untracked], or
@racket['ignored]. For an untracked file, Git reports @tt{??}, so both statuses
are @racket['untracked].
@racketblock[
'((modified unchanged "staged.rkt")
(unchanged modified "working-tree.rkt")
(modified modified "both.rkt")
(renamed unchanged "old.rkt -> new.rkt")
(untracked untracked "new.rkt"))
]}
@defproc[(git-add [argument any/c] ...) boolean?]{
Adds file contents to the index. Returns @racket[#t] when Git exits with status
zero; otherwise an exception is raised.
} }
@racketblock[ @defproc[(git-commit [argument any/c] ...) boolean?]{
(dgit 'status) Creates a commit. When @tt{-m} is omitted, a commit message is requested before
] Git is started. A repository with nothing to commit returns @racket[#t]. Other
non-zero exit statuses, including a rejected commit hook, raise an exception.
}
@section{Repository} @defproc[(git-push [argument any/c] ...) boolean?]{
Pushes changes using @tt{--porcelain}. Returns @racket[#t] when Git exits with
status zero; otherwise an exception is raised.
}
@defproc[(git-repository? [path path-string? (current-directory)]) boolean?]{Returns whether @racket[path] is inside a Git repository.} @defproc[(git-pull [argument any/c] ...) boolean?]{
Fetches and integrates changes. Normal progress written by Git to standard
error is accepted when Git exits successfully.
}
@defproc[(git-root [path path-string? (current-directory)]) path?]{Returns the repository worktree root.} @defproc[(git-fetch [argument any/c] ...) boolean?]{
Downloads refs and objects from a remote repository without integrating them
into the current branch. Arguments are passed directly to @tt{git fetch}.
@defproc[(git-init [path path-string? (current-directory)] [#:bare? bare? any/c #f]) path?]{Initializes a repository.} For example:
@defproc*[([(git-clone [url string?] [#:quiet quiet any/c #f]) path?]
[(git-clone [url string?] [path path-string?] [#:quiet quiet any/c #f]) path?])]{Clones @racket[url]. If @racket[path] is omitted, a directory name is derived from the URL. Progress is written to the current output port unless @racket[quiet] is true.}
@section{Status and index}
@defstruct*[git-status-entry ([path string?] [code string?] [flags list?])]{Describes one status entry. The @racket[code] field uses the familiar two-character Git status notation.}
@defproc[(git-status) (listof git-status-entry?)]{Returns worktree and index status.}
@defproc[(git-status-lines [entries (listof git-status-entry?) (git-status)]) (listof string?)]{Formats status entries as short Git-like lines.}
@defproc[(git-clean?) boolean?]{Returns @racket[#t] when @racket[git-status] is empty.}
@defproc*[([(git-diff) string?]
[(git-diff [option (or/c '--cached)]) string?])]{Returns a unified patch as a string. With no arguments it compares the index with the worktree, like @tt{git diff}. With @racket['--cached] it compares HEAD with the index, like @tt{git diff --cached}.}
@defproc[(git-add [path path-string?] ...) void?]{Stages the given paths. With no paths, stages the whole repository, including tracked removals.}
@section{Configuration and commits}
@defproc*[([(git-config [key string?]) string?]
[(git-config [key string?] [value string?]) string?])]{Reads or writes a repository configuration value. The two-argument form returns @racket[value].}
@defproc[(git-head) (or/c string? #f)]{Returns the full OID of HEAD, or @racket[#f] for a repository without commits.}
@defproc[(git-commit [message string?]) string?]{Creates a commit from the index and returns its full OID. The author and committer are read from the repository configuration.}
@section{Branches, checkout, and tags}
@defproc[(git-current-branch) (or/c string? #f)]{Returns the current local branch name, or @racket[#f] for detached HEAD.}
@defproc*[([(git-branch) (listof string?)]
[(git-branch [name string?]) string?])]{Lists local branches, or creates @racket[name] at HEAD.}
@defproc[(git-branch-delete [name string?]) void?]{Deletes a local branch.}
@defproc[(git-checkout [name string?]) (or/c string? #f)]{Checks out a local branch, tag, or commit. A tag or commit produces detached HEAD.}
@defproc[(git-checkout-new [name string?]) string?]{Creates and checks out a new branch.}
@defproc*[([(git-tag) (listof string?)]
[(git-tag [name string?]) string?])]{Lists tags, or creates a lightweight tag at HEAD and returns its OID.}
@defproc[(git-tag-delete [name string?]) void?]{Deletes a tag.}
@section{Log}
@defstruct*[git-log-entry ([id string?] [summary string?] [time integer?])]{Describes one commit returned by @racket[git-log].}
@defproc[(git-log [max-count exact-nonnegative-integer? 20]) (listof git-log-entry?)]{Returns commits from HEAD in topological/time order.}
@defproc[(git-log-lines [entries (listof git-log-entry?) (git-log)]) (listof string?)]{Formats log entries as short OID plus summary.}
@section{Remotes}
@defproc[(git-remotes) (listof string?)]{Lists remotes.}
@defproc[(git-remote-add [name string?] [url string?]) string?]{Adds a remote.}
@defproc[(git-remote-url [name string? "origin"]) string?]{Returns the remote URL.}
@defproc[(git-fetch [remote string? "origin"] [#:quiet quiet any/c #f]) void?]{Fetches the configured refspecs from a remote. Progress is written to the current output port unless @racket[quiet] is true.}
@defproc[(git-pull [remote string? "origin"] [#:quiet quiet any/c #f]) (or/c string? #f)]{Fetches and performs a fast-forward-only update of the current branch. Returns the new OID, or @racket[#f] when already up to date. A non-fast-forward update raises an exception.}
@defproc[(git-push [remote string? "origin"] [branch (or/c string? #f) #f] [#:quiet quiet any/c #f]) void?]{Pushes a branch to a branch with the same name. With no positional arguments, the current branch is pushed to @tt{origin}; with only @racket[remote], the current branch is pushed there. Progress is written to the current output port unless @racket[quiet] is true.}
@defproc[(git-push-tag [tag string?] [remote string? "origin"] [#:quiet quiet any/c #f]) void?]{Pushes one tag. Progress is written to the current output port unless @racket[quiet] is true.}
Remote HTTPS operations automatically use credentials from the @tt{racket-git} credential store when an entry exists for the remote host.
@section{Command form}
The following command-like forms are supported directly:
@racketblock[ @racketblock[
(git 'init) (git-fetch)
(git 'clone "https://example/repo.git") (git-fetch '--prune)
(git 'status) (git 'fetch '--prune)
(git 'add "file.rkt") ]
(git 'config "user.name" "Name") }
(git 'commit "message")
(git 'branch) @defproc[(git-branch [argument any/c] ...) boolean?]{
(git 'branch "feature") Runs @tt{git branch} with the supplied arguments. This can be used to list,
(git 'branch '-d "feature") create, rename, or delete branches according to the options supported by the
(git 'checkout "main") installed Git executable.
(git 'checkout '-b "feature") }
(git 'tag)
(git 'tag "v0.1") @defproc[(git-switch [argument any/c] ...) boolean?]{
(git 'tag '-d "v0.1") Runs @tt{git switch} with the supplied arguments.
(git 'log 10)
(git 'remote) @racketblock[
(git 'remote 'add "origin" "https://example/repo.git") (git-switch "main")
(git 'remote 'get-url "origin") (git-switch '-c "feature")
(git 'fetch) (git 'switch "main")
(git 'pull) ]
(git 'push) }
(git 'push #:quiet #t)
(git 'push-tag "v0.1") @defproc[(git-clone [argument any/c] ...) boolean?]{
Runs @tt{git clone} with the supplied arguments.
}
@defproc[(git-tag [argument any/c] ...) (or/c boolean? list?)]{
Runs @tt{git tag} with the supplied arguments. It can list, create, delete, or
verify tags according to the options supported by Git.
When @tt{-l} or @tt{--list} is supplied, the matching tag names are returned
as a Racket list. Git's sorting options are passed through unchanged, so the
returned list keeps Git's order.
@racketblock[
(git-tag '-l)
(git-tag '--list "--sort=version:refname")
(git-tag '--list "--sort=-creatordate")
] ]
@section{HTTPS credentials} When @tt{-n} or @tt{-n1} is combined with @tt{-l} or @tt{--list}, each result
item contains the tag name and the subject reported by Git.
Git credentials are stored in @tt{racket-git.ini} in the normal Racket @racketblock[
preferences directory. Tokens are encrypted with AES-GCM. The encryption key is (git-tag '-l '-n)
derived from the store password with PBKDF2-HMAC-SHA256.
@defproc[(git-credentials-init! [password string?] '(("v0.3.16" "Release 0.3.16")
[#:unlock-for seconds real? 86400]) void?]{ ("v0.3.17" "Release 0.3.17"))
Creates the credential store and leaves it unlocked for @racket[seconds].} ]
@defproc[(git-credentials-unlock! [password string?] With @tt{-n<number>} and a number greater than one, git-cli asks Git for that
[#:for seconds real? 86400]) void?]{ many content lines using @tt{%(contents:lines=<number>)}. The returned message
Unlocks the credential store. The temporary unlock state is stored separately in is kept as one string, including embedded newlines.
@tt{racket-git-unlock.ini}, allowing the unlock to survive restarting DrRacket
or starting another Racket process. Both credential INI files use
@racket[#:private? #t] storage from @racketmodname[simple-ini], which restricts
them to mode 0600 on Unix.}
@defproc[(git-credentials-lock!) void?]{Locks the credential store immediately.} For structured tag output git-cli asks Git for an explicit format using
@tt{%(refname:strip=2)} and either @tt{%(contents:subject)} or
@tt{%(contents:lines=<number>)}. Generated field and record delimiters are used
to split the result safely.
@defproc[(git-credentials-unlocked?) boolean?]{Returns whether a non-expired Other forms keep the normal command behavior and return @racket[#t] when Git
unlock key is currently available.} exits successfully. Git errors are handled by the standard git-cli result
processor.
}
@defproc[(git-rev-list [argument any/c] ...) boolean?]{
Runs @tt{git rev-list} with the supplied arguments and displays Git's normal
output. It returns @racket[#t] when Git exits successfully.
}
@defproc[(git-credentials-set! [remote string?] [username string?] [token string?]) void?]{ @defproc[(git-diff [argument any/c] ...) (or/c boolean? string?)]{
Stores an HTTPS username and token. Credentials are keyed by host.} Shows differences between Git objects or the working tree and index.
@defproc[(git-credentials-ref [remote string?]) (or/c #f pair?)]{ By default a successful diff is rendered as HTML in the default browser. The
Returns the username/token pair for @racket[remote], or @racket[#f] when none is git-cli-specific option @tt{--output=-} keeps Git's textual output on standard
stored. The store must be unlocked when a credential exists.} output. @tt{--output=string} returns the textual diff as a string.
@defproc[(git-credentials-remove! [remote string?]) void?]{Removes credentials @racketblock[
for the host represented by @racket[remote].} (git-diff)
(git-diff '--cached)
(git-diff '--output=-)
(git-diff '--output=string)
]
}
@defproc[(git-log [argument any/c] ...) (or/c boolean? list?)]{
Displays Git log output and returns @racket[#t] when Git exits successfully.
The git-cli-specific option @tt{--list}, or its short form @tt{-l}, changes the
result to a Racket list. Internally this option is replaced by Git's
@tt{--oneline} option. Each returned item contains the abbreviated commit id and
the commit subject.
@racketblock[
(git-log '--list '-5)
'(("003f371" "Diverse commando's toegevoegd. Ik weet nog niet of ik ze allemaal ga houden")
("2cb7e93" "Small changes. git main function is now a real function, not syntax"))
]
Other Git log options are still passed to Git. Consequently, options that add
extra output lines can also influence how useful @tt{--list} is as a structured
result.
}
@defproc[(git-show [argument any/c] ...) (or/c boolean? string? list?)]{
Shows a Git object.
For a commit that includes a patch, the default git-cli output is HTML. The
commit information is shown above the diff and the diff is rendered using the
same Diff2Html presentation as @racket[git-diff].
The git-cli-specific output options are @tt{--output=html},
@tt{--output=-}, and @tt{--output=string}. @tt{--output=html} explicitly
selects the HTML presentation, @tt{--output=-} keeps Git's normal textual
output, and @tt{--output=string} returns that textual output as a string.
Options such as @tt{--stat}, @tt{--name-only}, @tt{--name-status}, and
@tt{--no-patch} default to textual output because they do not normally contain
a patch.
The git-cli-specific option @tt{--list}, or its short form @tt{-l}, returns a
Racket value. Without another show-format option it implies @tt{--stat}.
@racketblock[
(git-show '-l "9741b1c")
]
The result of @tt{--stat --list} contains @racket['file] and
@racket['total] items:
@racketblock[
'((file "README.md" 67 "+++---")
(file "main.rkt" 532 "++++-------------------------------------------")
(total 9 124 823))
]
With @tt{--name-only --list}, the result is a list of file names. With
@tt{--name-status --list}, every result item is the tab-separated Git
name-status record converted to a list of strings.
@tt{--list}/@tt{-l} cannot be combined with @tt{--output=...}. Only one of
@tt{--stat}, @tt{--name-only}, and @tt{--name-status} can be used with
@tt{--list}.
}
@defproc[(git-grep [argument any/c] ...) list?]{
Searches tracked files. Each result contains the file, optional line number,
optional match count, and matched text. Exit status one means that no matches
were found and returns an empty list.
}
@defproc[(git-help [argument any/c] ...) boolean?]{
Runs @tt{git help} with the supplied arguments and returns @racket[#t] when Git
exits successfully.
}
@section{Package version}
@defproc[(git-version) list?]{
Reads the package version from @filepath{info.rkt} and returns it as a list
containing major, minor, and patch.
}
@defproc[(git-new-version [kind symbol?]) list?]{
Updates the version in @filepath{info.rkt}. The kind is @racket['major],
@racket['minor], or @racket['patch], with @racket['maj] and @racket['min] as
abbreviations. The result is the new version as a list of three integers.
}
@section{Low-level Git execution}
@defproc[(run-git [args list?]
[#:input input (or/c #f string?) #f])
(values exact-integer? list?)]{
Runs Git without interactive terminal prompts. When @racket[input] is a string,
it is written to Git's standard input before that input port is closed.
The procedure returns two values: Git's exit code and the ordered output items,
where each item identifies either @racket['stdout] or @racket['stderr].
@racketblock[
(run-git '(credential fill)
#:input "protocol=https\nhost=git.dijkewijk.nl\n\n")
]
}
File diff suppressed because one or more lines are too long
-326
View File
@@ -1,326 +0,0 @@
/* See the beginning of "manual.css". */
/* Monospace: */
.RktIn, .RktRdr, .RktPn, .RktMeta,
.RktMod, .RktKw, .RktVar, .RktSym,
.RktRes, .RktOut, .RktCmt, .RktVal,
.RktBlk, .RktErr {
font-family: 'Fira-Mono', monospace;
white-space: inherit;
font-size: 1rem;
line-height: 1.5;
}
/* this selctor grabs the first linked Racket symbol
in a definition box (i.e., the symbol being defined) */
a.RktValDef, a.RktStxDef, a.RktSymDef,
span.RktValDef, span.RktStxDef, span.RktSymDef
{
font-size: 1.1rem;
color: black;
font-weight: 500;
}
.inheritedlbl {
font-family: 'Fira', sans-serif;
}
.RBackgroundLabelInner {
font-family: inherit;
}
/* ---------------------------------------- */
/* Inherited methods, left margin */
.inherited {
width: 95%;
margin-top: 0.5em;
text-align: left;
background-color: inherit;
}
.inherited td {
font-size: 82%;
padding-left: 0.5rem;
line-height: 1.3;
text-indent: 0;
padding-right: 0;
}
.inheritedlbl {
font-style: normal;
}
/* ---------------------------------------- */
/* Racket text styles */
.RktIn {
color: #cc6633;
background-color: #eee;
white-space: pre;
}
.RktInBG {
background-color: #eee;
}
.refcolumn .RktInBG {
background-color: white;
}
.RktRdr {
}
.RktPn {
color: #843c24;
}
.RktMeta {
color: black;
}
.RktMod {
color: inherit;
}
.RktOpt {
color: black;
font-style: italic;
}
.RktKw {
color: black;
}
.RktErr {
color: red;
font-style: italic;
font-weight: 400;
}
.RktVar {
position: relative;
left: -1px; font-style: italic;
color: #444;
}
.SVInsetFlow .RktVar {
font-weight: 400;
color: #444;
}
.RktSym {
color: inherit;
}
.RktValLink, .RktStxLink, .RktModLink {
text-decoration: none;
color: #07A;
font-size: 1rem;
}
/* for syntax links within headings */
h1 a.RktStxLink, h2 a.RktStxLink, h3 a.RktStxLink, h4 a.RktStxLink, h5 a.RktStxLink,
h1 a.RktValLink, h2 a.RktValLink, h3 a.RktValLink, h4 a.RktValLink, h5 a.RktValLink,
h1 .RktSym, h2 .RktSym, h3 .RktSym, h4 .RktSym, h5 .RktSym,
h1 .RktMod, h2 .RktMod, h3 .RktMod, h4 .RktMod, h5 .RktMod,
h1 .RktVal, h2 .RktVal, h3 .RktVal, h4 .RktVal, h5 .RktVal,
h1 .RktPn, h2 .RktPn, h3 .RktPn, h4 .RktPn, h5 .RktPn {
color: #333;
font-size: 1.50rem;
font-weight: 400;
}
.toptoclink .RktStxLink, .toclink .RktStxLink,
.toptoclink .RktValLink, .toclink .RktValLink,
.toptoclink .RktModLink, .toclink .RktModLink {
color: inherit;
}
.tocset .RktValLink, .tocset .RktStxLink, .tocset .RktModLink, .tocset .RktSym {
color: black;
font-weight: 400;
font-size: 0.9rem;
}
.tocset td a.tocviewselflink .RktValLink,
.tocset td a.tocviewselflink .RktStxLink,
.tocset td a.tocviewselflink .RktMod,
.tocset td a.tocviewselflink .RktSym {
font-weight: lighter;
color: white;
}
.RktRes {
color: #0000af;
}
.RktOut {
color: #960096;
}
.RktCmt {
color: #c2741f;
}
.RktVal {
color: #228b22;
}
/* ---------------------------------------- */
/* Some inline styles */
.together { /* for definitions grouped together in one box */
width: 100%;
border-top: 2px solid white;
}
tbody > tr:first-child > td > .together {
border-top: 0px; /* erase border on first instance of together */
}
.RktBlk {
white-space: pre;
text-align: left;
}
.highlighted {
font-size: 1rem;
background-color: #fee;
}
.defmodule {
font-family: 'Fira-Mono', monospace;
padding: 0.25rem 0.75rem 0.25rem 0.5rem;
margin-bottom: 1rem;
width: 100%;
background-color: #ebf0f4;
}
.defmodule a {
color: #444;
}
.defmodule td span.hspace:first-child {
position: absolute;
width: 0;
display: inline-block;
}
.defmodule .RpackageSpec .Smaller,
.defmodule .RpackageSpec .stt {
font-size: 1rem;
}
/* make parens ordinary color in defmodule */
.defmodule .RktPn {
color: inherit;
}
.specgrammar {
float: none;
padding-left: 1em;
}
.RBibliography td {
vertical-align: text-top;
padding-top: 1em;
}
.leftindent {
margin-left: 2rem;
margin-right: 0em;
}
.insetpara {
margin-left: 1em;
margin-right: 1em;
}
.SCodeFlow .Rfilebox {
margin-left: -1em; /* see 17.2 of guide, module languages */
}
.Rfiletitle {
text-align: right;
background-color: #eee;
}
.SCodeFlow .Rfiletitle {
border-top: 1px dotted gray;
border-right: 1px dotted gray;
}
.Rfilename {
border-top: 0;
border-right: 0;
padding-left: 0.5em;
padding-right: 0.5em;
background-color: inherit;
}
.Rfilecontent {
margin: 0.5em;
}
.RpackageSpec {
padding-right: 0;
}
/* ---------------------------------------- */
/* For background labels */
.RBackgroundLabel {
float: right;
width: 0px;
height: 0px;
}
.RBackgroundLabelInner {
position: relative;
width: 25em;
left: -25.5em;
top: 0.20rem; /* sensitive to monospaced font choice */
text-align: right;
z-index: 0;
font-weight: 300;
font-family: 'Fira-Mono', monospace;
font-size: 0.9rem;
color: gray;
}
.RpackageSpec .Smaller {
font-weight: 300;
font-family: 'Fira-Mono', monospace;
font-size: 0.9rem;
}
.RForeground {
position: relative;
left: 0px;
top: 0px;
z-index: 1;
}
/* ---------------------------------------- */
/* For section source modules & tags */
.RPartExplain {
background: #eee;
font-size: 0.9rem;
margin-top: 0.2rem;
padding: 0.2rem;
text-align: left;
}
-268
View File
@@ -1,268 +0,0 @@
/* For the Racket manual style */
AddOnLoad(function() {
/* Look for header elements that have x-source-module and x-part tag.
For those elements, add a hidden element that explains how to
link to the section, and set the element's onclick() to display
the explanation. */
var tag_names = ["h1", "h2", "h3", "h4", "h5"];
for (var j = 0; j < tag_names.length; j++) {
elems = document.getElementsByTagName(tag_names[j]);
for (var i = 0; i < elems.length; i++) {
var elem = elems.item(i);
AddPartTitleOnClick(elem);
}
}
})
// for copies that start in Racket code, strip out extra newlines
AddOnLoad(function() {
const codeBlocks = document.getElementsByClassName("SCodeFlow");
for (var i = 0; i < codeBlocks.length; i++) {
var codeBlock = codeBlocks[i];
console.log("add");
codeBlock.addEventListener('copy', function(e) {
var selection = window.getSelection();
var text = selection.toString();
var codeText = text.replace(/\n\n/g, '\n');
if (text != codeText) {
e.preventDefault();
e.clipboardData.setData('text/plain', codeText);
}
})
}
})
// cache of source urls
var cache = {};
function ParseSource(source, mod_path, single_collection) {
var source_url = new URL(source);
if (source_url.protocol == "github:") {
// browser URL parser only works with http(s) URLs
source_url = new URL("https" + source.substring(6));
var host = source_url.host;
var url_path = source_url.pathname.substring(1).split("/");
if (!(url_path.length >= 2)) return null;
var user = url_path.shift();
var repo = url_path.shift();
var branch = url_path.shift();
var source_path = url_path.join("/");
}
else if (("https:" == source_url.protocol) || ("git:" == source_url.protocol)) {
// browser URL parser only works with http(s) URLs
if ("git:" == source_url.protocol)
source_url = new URL("https" + source.substring(3));
var host = source_url.host;
var source_path = source_url.searchParams.get("path");
var branch = (source_url.hash || "#master").substring(1);
var url_path = source_url.pathname.substring(1).split("/");
if (url_path.length < 2) throw [source_url.pathname, url_path];
var user = url_path.shift();
var repo = url_path.shift();
var mtch = repo.match(/(.*)\.git$/);
if (mtch) repo = mtch[1];
}
else return null;
var mod_path_re = /^\(lib "(.+)"\)$/;
var mod_path_elems = mod_path && mod_path.match(mod_path_re)[1].split("/");
if (!user || !repo || !mod_path_elems)
return null;
if (single_collection)
mod_path_elems.shift();
var file_path = mod_path_elems.join("/");
if (source_path) {
file_path = source_path + "/" + file_path;
}
return { user: user,
repo: repo,
file_path: file_path,
branch: branch,
host: host };
}
function AddSourceElement(pkg_url, info) {
info.appendChild(document.createTextNode("Document source "));
var url_line = document.createElement("div");
var a = document.createElement("a");
a.href = pkg_url;
a.style.whiteSpace = "nowrap";
a.appendChild(document.createTextNode(pkg_url));
addSpan(url_line, "\xA0", "RktRdr");
url_line.appendChild(a);
info.appendChild(url_line);
}
var prefixes = { "github.com": "tree",
"gitlab.com": "-/blob" };
function AddSourceUrl(source, mod_path, collection, info) {
// multi is encoded as an array, empty as false
single_collection = (typeof collection === "string");
var parsed = source && mod_path && ParseSource(source, mod_path, single_collection);
if (!parsed) return;
prefix = prefixes.hasOwnProperty(parsed.host) && prefixes[parsed.host];
if (!prefix) return;
var correct_url = "https://" + [parsed.host, parsed.user, parsed.repo, prefix, parsed.branch, parsed.file_path].join("/");
if (info) AddSourceElement(correct_url, info);
}
function addSpan(dest, str, cn) {
var s = document.createElement("span");
s.className = cn;
s.style.whiteSpace = "nowrap";
s.appendChild(document.createTextNode(str));
dest.appendChild(s);
}
// test cases
if (false) {
console.log(ParseSource("git://gitlab.com/benn/foo?path=xxx",
'(lib "asn1/scribblings/asn1.scrbl")',
false))
console.log(ParseSource("github://github.com/carl-eastlund/mischief/master",
'(lib "asn1/scribblings/asn1.scrbl")',
false))
console.log(ParseSource("github://github.com/carl-eastlund/mischief/stable/dir",
'(lib "asn1/scribblings/asn1.scrbl")',
false))
console.log(ParseSource("git://github.com/racket/racket/?path=pkgs/racket-doc",
'(lib "asn1/scribblings/asn1.scrbl")',
false));
console.log(ParseSource("git://github.com/rmculpepper/asn1.git?path=asn1-doc",
'(lib "asn1/scribblings/asn1.scrbl")',
true));
console.log(ParseSource("git://github.com/rmculpepper/asn1",
'(lib "asn1/scribblings/asn1.scrbl")',
true));
console.log(ParseSource("git://github.com/rmculpepper/asn1",
'(lib "asn1/scribblings/asn1.scrbl")',
false));
}
function AddPartTitleOnClick(elem) {
var mod_path = elem.getAttribute("x-source-module");
var tag = elem.getAttribute("x-part-tag");
var source_pkg = elem.getAttribute("x-source-pkg");
// create here to share
var info = document.createElement("div");
// tag is not needed, but this way we can add the element in only one place
// avoid failing on browser that don't have `fetch`
if (mod_path && source_pkg && tag && window.fetch) {
var cached = cache[mod_path]
if (cached) {
AddSourceElement(cached[0], mod_path, cached[1], info);
}
else {
fetch("https://pkgs.racket-lang.org/pkg/" + source_pkg + ".json")
.then(function (response) { return response.json(); })
.then(function (data) {
var vers = data["versions"] || {};
var def = vers["default"] || {};
var source = def["source"] || undefined;
var collection = data["collection"];
if (source) {
cache[mod_path] = [source, collection];
AddSourceUrl(source, mod_path, collection, info);
}
});
}
}
if (mod_path && tag) {
// Might not be present:
var prefixes = elem.getAttribute("x-part-prefixes");
info.className = "RPartExplain";
/* The "top" tag refers to a whole document: */
var is_top = (tag == "\"top\"");
info.appendChild(document.createTextNode("Link to this "
+ (is_top ? "document" : "section")
+ " with "));
/* Break `secref` into two lines if the module path and tag
are long enough: */
var is_long = (is_top ? false : ((mod_path.length
+ tag.length
+ (prefixes ? (16 + prefixes.length) : 0))
> 60));
var line1 = document.createElement("div");
var line1x = ((is_long && prefixes) ? document.createElement("div") : line1);
var line2 = (is_long ? document.createElement("div") : line1);
/* Construct a `secref` call with suitable syntax coloring: */
addSpan(line1, "\xA0@", "RktRdr");
addSpan(line1, (is_top ? "other-doc" : "secref"), "RktSym");
addSpan(line1, "[", "RktPn");
if (!is_top)
addSpan(line1, tag, "RktVal");
if (is_long) {
/* indent additional lines: */
if (prefixes)
addSpan(line1x, "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0", "RktPn");
addSpan(line2, "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0", "RktPn");
}
if (prefixes) {
addSpan(line1x, " #:tag-prefixes ", "RktPn");
addSpan(line1x, "'", "RktVal");
addSpan(line1x, prefixes, "RktVal");
}
if (!is_top)
addSpan(line2, " #:doc ", "RktPn");
addSpan(line2, "'", "RktVal");
addSpan(line2, mod_path, "RktVal");
addSpan(line2, "]", "RktPn");
info.appendChild(line1);
if (is_long)
info.appendChild(line1x);
if (is_long)
info.appendChild(line2);
info.style.display = "none";
/* Add the new element afterthe header: */
var n = elem.nextSibling;
if (n)
elem.parentNode.insertBefore(info, n);
else
elem.parentNode.appendChild(info);
/* Clicking the information button shows the explanation element: */
const heading = elem.querySelector('.heading-source');
if (heading) {
heading.onclick = function () {
if (info.style.display === "none")
info.style.display = "block";
else
info.style.display = "none";
}
}
}
}
-815
View File
@@ -1,815 +0,0 @@
/* See the beginning of "scribble.css".
This file is used by the `scribble/manual` language, along with
"manual-racket.css". */
@import url("manual-fonts.css");
* {
margin: 0;
padding: 0;
}
@media all {html {font-size: 15px;}}
@media all and (max-width:940px){html {font-size: 14px;}}
@media all and (max-width:850px){html {font-size: 13px;}}
@media all and (max-width:830px){html {font-size: 12px;}}
@media all and (max-width:740px){html {font-size: 11px;}}
/* CSS seems backward: List all the classes for which we want a
particular font, so that the font can be changed in one place. (It
would be nicer to reference a font definition from all the places
that we want it.)
As you read the rest of the file, remember to double-check here to
see if any font is set. */
/* Monospace: */
.maincolumn, .refpara, .refelem, .tocset, .stt, .hspace, .refparaleft, .refelemleft {
font-family: 'Fira-Mono', monospace;
white-space: inherit;
font-size: 1rem;
}
/* Enable heading-source */
.button-group > .heading-source {
visibility: inherit;
cursor: pointer;
user-select: none;
color: gray;
}
/* embolden the "Racket Guide" and "Racket Reference" links on the TOC */
/* there isn't an obvious tag in the markup that designates the top TOC page, which is called "start.scrbl" */
/* nor a tag that designates these two links as special */
/* so we'll use this slightly tortured sibling selector that hooks onto the h1 tag */
h1[x-source-module='(lib "scribblings/main/start.scrbl")'] ~ table a[href="guide/index.html"],
h1[x-source-module='(lib "scribblings/main/start.scrbl")'] ~ table a[href="reference/index.html"] {
font-weight: bold;
}
h1 .stt {
font-size: 2.3rem;
/* prevent automatic bolding from h1 */
font-weight: 400;
}
.toptoclink .stt {
font-size: inherit;
}
.toclink .stt {
font-size: 90%;
}
.RpackageSpec .stt {
font-weight: 300;
font-family: 'Fira-Mono', monospace;
font-size: 0.9rem;
}
h2 .stt, h3 .stt, h4 .stt, h5 .stt {
color: #333;
font-size: 1.65rem;
font-weight: 400;
}
/* Serif: */
.main, .refcontent, .tocview, .tocsub, .sroman, i {
font-family: 'Charter-Racket', serif;
font-size: 1.18rem;
/* Don't use font-feature-settings with Charter,
it fouls up loading for reasons mysterious */
/* font-feature-settings: 'tnum' 1, 'liga' 0; */
}
/* Sans-serif: */
.version, .versionNoNav, .ssansserif, .navfamily, .famlink {
font-family: 'Fira', sans-serif;
}
/* used mostly for DrRacket menu commands */
.ssansserif {
font-family: 'Fira', sans-serif;
font-size: 0.9em;
}
.tocset .ssansserif {
font-size: 100%;
}
/* ---------------------------------------- */
p, .SIntrapara {
display: block;
margin: 0 0 1em 0;
line-height: 1.4;
}
.compact {
padding: 0 0 1em 0;
}
li {
list-style-position: outside;
margin-left: 1.2em;
}
h1, h2, h3, h4, h5, h6, h7, h8 {
font-family: 'Fira', sans-serif;
font-weight: 300;
font-size: 1.6rem;
color: #333;
margin-top: inherit;
margin-bottom: 1rem;
line-height: 1.25;
}
h2, h3, h4, h5, h6, h7, h8 {
border-top: 1px solid black;
}
h1 { /* per-page main title */
font-family: 'Cooper-Hewitt';
margin-top: 4rem;
font-size: 2.3rem;
font-weight: bold;
line-height: 1.2;
width: 90%;
/* a little nudge to make text visually lower than 4rem rule in left margin */
position: relative;
top: 6px;
}
h2, h3, h4, h5, h6, h7, h8 {
margin-top: 2em;
padding-top: 0.1em;
margin-bottom: 0.75em;
}
/* ---------------------------------------- */
/* Main */
body {
color: black;
background-color: white;
}
.maincolumn {
width: auto;
margin-top: 4rem;
margin-left: 17rem;
margin-right: 2rem;
margin-bottom: 10rem; /* to avoid fixed bottom nav bar */
max-width: 700px;
min-width: 370px; /* below this size, code samples don't fit */
}
a {
text-decoration: inherit;
}
a, .toclink, .toptoclink, .tocviewlink, .tocviewselflink, .tocviewtoggle, .plainlink,
.techinside, .techoutside:hover, .techinside:hover {
color: #07A;
}
a:hover {
text-decoration: underline;
}
/* ---------------------------------------- */
/* Navigation */
.navsettop, .navsetbottom {
left: 0;
width: 15rem;
height: 6rem;
font-family: 'Fira', sans-serif;
font-size: 0.9rem;
border-bottom: 0px solid hsl(216, 15%, 70%);
background-color: inherit;
padding: 0;
}
.navsettop {
position: fixed;
z-index: 2;
background: #a7b0be;
top: 0;
left: 0;
margin-bottom: 0;
border-bottom: 0;
}
.navsettop a, .navsetbottom a {
color: black;
}
.navsettop a:hover, .navsetbottom a:hover {
background: hsl(216, 78%, 95%);
text-decoration: none;
}
.navleft, .navright {
position: static;
float: none;
margin: 0;
white-space: normal;
}
.navleft a {
display: inline-block;
}
.navright a {
display: inline-block;
text-align: center;
}
.navleft a, .navright a, .navright span {
display: inline-block;
padding: 0.5rem;
min-width: 1rem;
}
.navright {
white-space: nowrap;
}
.navsetbottom {
display: none;
}
.nonavigation {
color: #889;
}
.searchform {
display: block;
margin: 0;
padding: 0;
border-bottom: 1px solid #eee;
height: 4rem;
}
.nosearchform {
margin: 0;
padding: 0;
height: 4rem;
}
.searchbox {
font-size: 0.9rem;
width: 12rem;
margin: 1rem;
padding: 0.25rem 0.4rem ;
vertical-align: middle;
background-color: white;
font-family: 'Fira-Mono', monospace;
}
#search_box {
font-family: 'Fira-Mono', monospace;
font-size: 1rem;
padding: 0.25rem 0.3rem ;
}
/* Default to local view. Global will specialize */
.plt_global_only { display: none; }
.plt_local_only { display: block; }
/* ---------------------------------------- */
/* Version */
.versionbox {
position: absolute;
float: none;
top: 0.25rem;
left: 17rem;
z-index: 11000;
height: 2em;
font-size: 70%;
font-weight: lighter;
width: inherit;
margin: 0;
}
.version, .versionNoNav {
font-size: inherit;
}
.version:before, .versionNoNav:before {
content: "v";
}
/* ---------------------------------------- */
/* Language Family */
.navfamily {
position: sticky;
top: 0;
left: 100%;
width: 13em;
margin-top: -4.75em;
margin-right: -15em;
font-size: 70%;
font-weight: lighter;
height: auto;
padding-top: 0.5em;
}
/* ---------------------------------------- */
/* Margin notes */
/* cancel scribble.css styles: */
.refpara, .refelem {
position: static;
float: none;
height: auto;
width: auto;
margin: 0;
}
.refcolumn {
position: static;
display: block;
width: auto;
font-size: inherit;
margin: 2rem;
margin-left: 2rem;
padding: 0.5em;
padding-left: 0.75em;
padding-right: 1em;
background: hsl(60, 29%, 94%);
border: 1px solid #ccb;
border-left: 0.4rem solid #ccb;
}
/* slightly different handling for margin-note* on narrow screens */
@media all and (max-width:1340px) {
span.refcolumn {
float: right;
width: 50%;
margin-left: 1rem;
margin-bottom: 0.8rem;
margin-top: 1.2rem;
}
.navfamily {
position: static;
margin: -4.75em 0em 0em 0em;
}
}
.refcontent, .refcontent p {
line-height: 1.5;
margin: 0;
}
.refcontent p + p {
margin-top: 1em;
}
.refcontent a {
font-weight: 400;
}
.refpara, .refparaleft {
top: -1em;
}
@media all and (max-width:600px) {
.refcolumn {
margin-left: 0;
margin-right: 0;
}
}
@media all and (min-width:1340px) {
.refcolumn {
margin: 0 -22.5rem 1rem 0;
float: right;
clear: right;
width: 18rem;
}
}
.refcontent {
font-family: 'Fira', sans-serif;
font-size: 1rem;
line-height: 1.6;
margin: 0 0 0 0;
}
.refparaleft, .refelemleft {
position: relative;
float: left;
right: 2em;
height: 0em;
width: 13em;
margin: 0em 0em 0em 0em;
display: contents;
}
.refcolumnleft {
background-color: hsl(60, 29%, 94%);
display: block;
position: relative;
width: 13em;
font-size: 85%;
border: 0.5em solid hsl(60, 29%, 94%);
margin: 0 0 0 0;
}
/* ---------------------------------------- */
/* Table of contents, left margin */
.tocset {
position: fixed;
z-index: 2;
overflow-y: scroll;
float: none;
left: 0;
top: 0rem;
bottom: 0;
width: 14rem;
padding: 0rem 0.5rem 0.5rem 0.5rem;
background-color: hsl(216, 15%, 70%);
border-top: 6rem solid hsl(216, 15%, 70%);
}
.tocset td {
vertical-align: text-top;
padding-bottom: 0.4rem;
padding-left: 0.2rem;
line-height: 1.1;
font-family: 'Fira', sans-serif;
}
.tocset td a {
color: black;
font-weight: 400;
}
.tocview {
text-align: left;
background-color: inherit;
margin-top: 1em;
}
.tocview td, .tocsub td {
line-height: 1.3;
}
.tocview table, .tocsub table {
width: 90%;
}
.tocset td a.tocviewselflink {
font-weight: lighter;
font-size: 110%; /* monospaced styles below don't need to enlarge */
color: white;
}
.tocviewselflink {
text-decoration: none;
}
.tocsub {
text-align: left;
margin-top: 0.5em;
background-color: inherit;
}
.tocviewlist, .tocsublist {
margin-left: 0.2em;
margin-right: 0.2em;
padding-top: 0.2em;
padding-bottom: 0.2em;
}
.tocviewlist table {
font-size: 82%;
}
.tocviewlisttopspace {
margin-bottom: 1em;
}
.tocviewsublist, .tocviewsublistonly, .tocviewsublisttop, .tocviewsublistbottom {
margin-left: 0.4em;
border-left: 1px solid #99a;
padding-left: 0.8em;
}
.tocviewsublist {
margin-bottom: 1em;
}
.tocviewsublist table,
.tocviewsublistonly table,
.tocviewsublisttop table,
.tocviewsublistbottom table,
table.tocsublist {
font-size: 1rem;
}
.tocviewsublist td,
.tocviewsublistbottom td,
.tocviewsublisttop td,
.tocsub td,
.tocviewsublistonly td {
font-size: 90%;
}
/* shrink the monospaced text (`stt`) within nav */
.tocviewsublist td .stt,
.tocviewsublistbottom td .stt,
.tocviewsublisttop td .stt,
.tocsub td .stt,
.tocviewsublistonly td .stt {
font-size: 95%;
}
.tocviewtoggle {
font-size: 75%; /* looks better, and avoids bounce when toggling sub-sections due to font alignments */
}
.tocsublist td {
padding-left: 0.5rem;
padding-top: 0.25rem;
text-indent: 0;
}
.tocsublinknumber {
font-size: 100%;
}
.tocsublink {
font-size: 82%;
text-decoration: none;
}
.tocsubseclink {
font-size: 100%;
text-decoration: none;
}
.tocsubnonseclink {
font-size: 82%;
text-decoration: none;
margin-left: 1rem;
padding-left: 0;
display: inline-block;
}
/* the label "on this page" */
.tocsubtitle {
display: block;
font-size: 62%;
font-family: 'Fira', sans-serif;
font-weight: bolder;
font-style: normal;
letter-spacing: 2px;
text-transform: uppercase;
margin: 0.5em;
}
.toptoclink {
font-weight: bold;
font-size: 110%;
margin-bottom: 0.5rem;
margin-top: 1.5rem;
display: inline-block;
}
.toclink {
font-size: inherit;
}
/* ---------------------------------------- */
/* Some inline styles */
.indexlink {
text-decoration: none;
}
pre {
margin-left: 2em;
}
blockquote {
margin-left: 2em;
margin-right: 2em;
margin-bottom: 1em;
}
.SCodeFlow {
border-left: 1px dotted black;
padding-left: 1em;
padding-right: 1em;
margin-top: 1em;
margin-bottom: 1em;
margin-left: 0em;
margin-right: 2em;
white-space: nowrap;
line-height: 1.5;
}
.SCodeFlow img {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
/* put a little air between lines of code sample */
/* Fira Mono appears taller than Source Code Pro */
.SCodeFlow td {
padding-bottom: 1px;
}
.boxed {
margin: 0;
margin-top: 2em;
padding: 0.25em;
padding-top: 0.3em;
padding-bottom: 0.4em;
background: #f3f3f3;
box-sizing:border-box;
border-top: 1px solid #99b;
background: hsl(216, 78%, 95%);
background: -moz-linear-gradient(to bottom left, hsl(0, 0%, 99%) 0%, hsl(216, 62%, 95%) 100%);
background: -webkit-linear-gradient(to bottom left, hsl(0, 0%, 99%) 0%, hsl(216, 62%, 95%) 100%);
background: -o-linear-gradient(to bottom left, hsl(0, 0%, 99%) 0%, hsl(216, 62%, 95%) 100%);
background: -ms-linear-gradient(to bottom left, hsl(0, 0%, 99%) 0%, hsl(216, 62%, 95%) 100%);
background: linear-gradient(to bottom left, hsl(0, 0%, 99%) 0%, hsl(216, 62%, 95%) 100%);
}
blockquote > blockquote.SVInsetFlow {
/* resolves issue in e.g. /reference/notation.html */
margin-top: 0em;
}
.leftindent .SVInsetFlow { /* see e.g. section 4.5 of Racket Guide */
margin-top: 1em;
margin-bottom: 1em;
}
.SVInsetFlow a, .SCodeFlow a {
color: #07A;
}
.SubFlow {
display: block;
margin: 0em;
}
.boxed {
width: 100%;
background-color: inherit;
}
.techoutside { text-decoration: none; }
.SAuthorListBox {
position: static;
float: none;
font-family: 'Fira', sans-serif;
font-weight: 300;
font-size: 110%;
margin-top: 1rem;
margin-bottom: 2rem;
width: 30rem;
height: auto;
}
.author > a { /* email links within author block */
font-weight: inherit;
color: inherit;
}
.SAuthorList {
font-size: 82%;
}
.SAuthorList:before {
content: "by ";
}
.author {
display: inline;
white-space: nowrap;
}
/* phone + tablet styles */
@media all and (max-width:720px){
@media all and (max-width:720px){
@media all {html {font-size: 15px;}}
@media all and (max-width:700px){html {font-size: 14px;}}
@media all and (max-width:630px){html {font-size: 13px;}}
@media all and (max-width:610px){html {font-size: 12px;}}
@media all and (max-width:550px){html {font-size: 11px;}}
@media all and (max-width:520px){html {font-size: 10px;}}
.navsettop, .navsetbottom {
display: flex;
position: absolute;
width: 100%;
height: 4rem;
border: 0;
background-color: hsl(216, 15%, 70%);
align-items: center;
}
.tocsetoverlay .navsettop {
position: fixed;
}
.navleft {
flex: 1;
}
.searchform {
display: inline;
border: 0;
}
.searchbox {
margin-top: 0;
margin-bottom: 0;
}
.navleft .tocsettoggle {
display: initial;
}
.navright {
margin-right: 1.3rem;
border: 0px solid red;
}
.navsetbottom {
display: block;
margin-top: 8rem;
}
.tocset {
display: none;
border-top-width: 4rem;
}
.tocsetoverlay .tocset {
display: block;
}
.versionbox {
top: 4.5rem;
left: 1rem; /* same distance as main-column */
z-index: 1;
height: 2em;
font-size: 70%;
font-weight: lighter;
}
.navfamily {
position: static;
margin: -3.25em 0em 0em 0em;
}
.maincolumn {
margin-left: 1em;
margin-top: 7rem;
margin-bottom: 0rem;
}
}
}
/* print styles : hide the navigation elements */
@media print {
.tocset,
.navsettop,
.navsetbottom { display: none; }
.maincolumn {
width: auto;
margin-right: 13em;
margin-left: 0;
}
}
-251
View File
@@ -1,251 +0,0 @@
/* See the beginning of "scribble.css". */
/* Monospace: */
.RktIn, .RktRdr, .RktPn, .RktMeta,
.RktMod, .RktKw, .RktVar, .RktSym,
.RktRes, .RktOut, .RktCmt, .RktVal,
.RktBlk {
font-family: monospace;
white-space: inherit;
}
/* Serif: */
.inheritedlbl {
font-family: serif;
}
/* Sans-serif: */
.RBackgroundLabelInner {
font-family: sans-serif;
}
/* ---------------------------------------- */
/* Inherited methods, left margin */
.inherited {
width: 100%;
margin-top: 0.5em;
text-align: left;
background-color: #ECF5F5;
}
.inherited td {
font-size: 82%;
padding-left: 1em;
text-indent: -0.8em;
padding-right: 0.2em;
}
.inheritedlbl {
font-style: italic;
}
/* ---------------------------------------- */
/* Racket text styles */
.RktIn {
color: #cc6633;
background-color: #eeeeee;
white-space: pre;
}
.RktInBG {
background-color: #eeeeee;
}
.RktRdr {
}
.RktPn {
color: #843c24;
}
.RktMeta {
color: black;
}
.RktMod {
color: black;
}
.RktOpt {
color: black;
font-style: italic;
}
.RktKw {
color: black;
}
.RktErr {
color: red;
font-style: italic;
}
.RktVar {
color: #262680;
font-style: italic;
}
.RktSym {
color: #262680;
}
.RktSymDef { /* used with RktSym at def site */
}
.RktValLink {
text-decoration: none;
color: blue;
}
.RktValDef { /* used with RktValLink at def site */
}
.RktModLink {
text-decoration: none;
color: blue;
}
.RktStxLink {
text-decoration: none;
color: black;
}
.RktStxDef { /* used with RktStxLink at def site */
}
.RktRes {
color: #0000af;
}
.RktOut {
color: #960096;
}
.RktCmt {
color: #c2741f;
}
.RktVal {
color: #228b22;
}
/* ---------------------------------------- */
/* Some inline styles */
.together {
width: 100%;
}
.prototype, .argcontract, .RBoxed {
white-space: nowrap;
}
.prototype td {
vertical-align: text-top;
}
.RktBlk {
white-space: inherit;
text-align: left;
}
.RktBlk tr {
white-space: inherit;
}
.RktBlk td {
vertical-align: baseline;
white-space: inherit;
}
.argcontract td {
vertical-align: text-top;
}
.highlighted {
background-color: #ddddff;
}
.defmodule {
width: 100%;
background-color: #F5F5DC;
}
.specgrammar {
float: right;
}
.RBibliography td {
vertical-align: text-top;
}
.leftindent {
margin-left: 1em;
margin-right: 0em;
}
.insetpara {
margin-left: 1em;
margin-right: 1em;
}
.Rfilebox {
}
.Rfiletitle {
text-align: right;
margin: 0em 0em 0em 0em;
}
.Rfilename {
border-top: 1px solid #6C8585;
border-right: 1px solid #6C8585;
padding-left: 0.5em;
padding-right: 0.5em;
background-color: #ECF5F5;
}
.Rfilecontent {
margin: 0em 0em 0em 0em;
}
.RpackageSpec {
padding-right: 0.5em;
}
/* ---------------------------------------- */
/* For background labels */
.RBackgroundLabel {
float: right;
width: 0px;
height: 0px;
}
.RBackgroundLabelInner {
position: relative;
width: 25em;
left: -25.5em;
top: 0px;
text-align: right;
color: white;
z-index: 0;
font-weight: bold;
}
.RForeground {
position: relative;
left: 0px;
top: 0px;
z-index: 1;
}
/* ---------------------------------------- */
/* History */
.SHistory {
font-size: 82%;
}
-263
View File
@@ -1,263 +0,0 @@
// Common functionality for PLT documentation pages
// Page Parameters ------------------------------------------------------------
var plt_root_as_query = false;
function GetURL() {
return new URL(location);
}
function GetPageArgs() {
return GetURL().searchParams;
}
function GetPageQueryString() {
return GetPageArgs().toString();
}
function GetPageArg(key, def) {
return GetPageArgs().get(key) || def;
}
function MergePageArgsIntoLink(a) {
if ((GetPageArgs().size === 0 || !a.dataset.pltdoc) && !plt_root_as_query) return;
a.href = MergePageArgsIntoUrl(a.href);
}
function MergePageArgsIntoUrl(href) {
const url = new URL(href, window.location.href);
MergePageArgsIntoUrlObject(url);
return url.href;
}
function MergePageArgsIntoUrlObject(url) {
for (const [key, val] of GetPageArgs()) {
if (key[0] == "q") continue; // use "q" to mean "don't propagate automatcially"
if (url.searchParams.has(key)) continue;
url.searchParams.append(key, val)
}
if (plt_root_as_query && !url.searchParams.has("PLT_Root")) {
url.searchParams.append("PLT_Root", plt_root_as_query);
}
}
// Cookies --------------------------------------------------------------------
// Actually, try localStorage (a la HTML 5), first.
function GetCookie(key, def) {
try {
var v = localStorage[key];
if (!v) v = def;
return v;
} catch (e) {
var i, cookiestrs;
try {
if (document.cookie.length <= 0) return def;
cookiestrs = document.cookie.split(/; */);
} catch (e) { return def; }
for (i = 0; i < cookiestrs.length; i++) {
var cur = cookiestrs[i];
var eql = cur.indexOf('=');
if (eql >= 0 && cur.substring(0,eql) == key)
return unescape(cur.substring(eql+1));
}
return def;
}
}
function SetCookie(key, val) {
try {
localStorage[key] = val;
} catch(e) {
var d = new Date();
d.setTime(d.getTime()+(365*24*60*60*1000));
try {
document.cookie =
key + "=" + escape(val) + "; expires="+ d.toGMTString() + "; path=/";
} catch (e) {}
}
}
// note that this always stores a directory name, ending with a "/"
function SetPLTRoot(ver, relative) {
var root = location.protocol + "//" + location.host
+ NormalizePath(location.pathname.replace(/[^\/]*$/, relative));
if (location.protocol == "file:") {
// local storage or cookies are not going to work in modern browsers,
// so add a query parameter to all URLs
plt_root_as_query=root
} else {
SetCookie("PLT_Root."+ver, root);
}
}
// adding index.html works because of the above
function GotoPLTRoot(ver, root_relative, here_to_root_relative) {
// the relative path is optional, default goes to the toplevel start page
if (!root_relative) root_relative = "index.html";
if (here_to_root_relative == undefined) here_to_root_relative = "../"
var famroot = false;
if (root_relative == "index.html") {
famroot = (GetPageArg("fam", false) ? GetPageArg("famroot", false) : false)
if (famroot) {
root_relative = famroot + "/index.html";
}
}
var u = GetRootPath(ver);
if (u == null) {
if (famroot) {
location = MergePageArgsIntoUrl(here_to_root_relative + famroot + "/index.html");
return false;
}
// no cookie and no famroot => follow href, instead
return true;
}
location = MergePageArgsIntoUrl(u + root_relative);
return false;
}
function GetRootPath(ver) {
var u = GetCookie("PLT_Root."+ver, null);
if (u != null)
return u;
// via query argument? (especially for `file://` URLs)
u = GetPageArg("PLT_Root", null)
if (u != null)
return u;
// use root specified by local-redirect wrapper, if present
if (typeof user_doc_root != "undefined")
return user_doc_root;
return null;
}
// Utilities ------------------------------------------------------------------
var normalize_rxs = [/\/\/+/g, /\/\.(\/|$)/, /\/[^\/]*\/\.\.(\/|$)/];
function NormalizePath(path) {
var tmp, i;
for (i = 0; i < normalize_rxs.length; i++)
while ((tmp = path.replace(normalize_rxs[i], "/")) != path) path = tmp;
return path;
}
// `noscript' is problematic in some browsers (always renders as a
// block), use this hack instead (does not always work!)
// document.write("<style>mynoscript { display:none; }</style>");
// Interactions ---------------------------------------------------------------
function DoSearchKey(event, field, ver, top_path) {
var val = field.value;
if (event && event.key === 'Enter') {
var u = GetRootPath(ver);
if (u == null) u = top_path; // default: go to the top path
u += "search/index.html?q=" + encodeURIComponent(val);
u = MergePageArgsIntoUrl(u);
location = u;
return false;
}
return true;
}
function TocviewToggle(glyph, id) {
var s = document.getElementById(id).style;
var expand = s.display == "none";
s.display = expand ? "block" : "none";
glyph.innerHTML = expand ? "&#9660;" : "&#9658;";
}
function TocsetToggle() {
document.body.classList.toggle("tocsetoverlay");
}
// Page Init ------------------------------------------------------------------
// Note: could make a function that inspects and uses window.onload to chain to
// a previous one, but this file needs to be required first anyway, since it
// contains utilities for all other files.
var on_load_funcs = [];
function AddOnLoad(fun) { on_load_funcs.push(fun); }
window.onload = function() {
for (var i=0; i<on_load_funcs.length; i++) on_load_funcs[i]();
};
AddOnLoad(function(){
var links = document.getElementsByTagName("a");
for (var i=0; i<links.length; i++) MergePageArgsIntoLink(links[i]);
var label = GetPageArg("ctxtname",false);
if (!label) return;
var indicator = document.getElementById("contextindicator");
if (!indicator) return;
indicator.innerHTML = label;
indicator.style.display = "block";
});
// Pressing "S" or "s" focuses on the "...search manuals..." text field
AddOnLoad(function(){
window.addEventListener("keyup", function(e) {
if ((e.key === 's' || e.key === 'S') && e.target === document.body) {
var searchBox = document.getElementById('searchbox');
if (searchBox) {
searchBox.focus();
}
}
}, false);
});
AddOnLoad(function(){
var es = document.getElementsByClassName("family-navigation");
if (es.length > 0) {
var fams = es[0].dataset.familynav.split(/,/);
var fam = GetPageArg("famroot", false) && GetPageArg("fam", false);
if (!fam) fam = "Racket";
if (fams.indexOf(fam) == -1) {
for (var i=0; i < es.length; i++) {
es[i].style.display = "inline-block";
}
}
}
});
AddOnLoad(function(){
var es = document.getElementsByClassName("navfamily");
for (var i=0; i < es.length; i++) {
var e = es[i];
if (e.dataset.fam != undefined) {
var fams = e.dataset.fam.split(/,/);
var fam = GetPageArg("fam", false);
if (!fam) fam = "Racket";
var link = document.createElement('a');
var root = GetRootPath(e.dataset.version)
var family_url;
if (root == null) {
family_url = new URL(e.dataset.famPath + "family/index.html", window.location.href);
} else {
family_url = new URL(root + "family/index.html", window.location.href);
}
family_url.searchParams.append("qfrom", window.location.href)
MergePageArgsIntoUrlObject(family_url);
if (fams.indexOf(fam) == -1) {
var nav_as = document.createElement('div');
link.textContent = "navigating as " + fam;
link.href = family_url
nav_as.appendChild(link)
e.appendChild(nav_as)
} else {
var link = document.createElement('a');
var span = e.children[0]
link.textContent = span.textContent;
link.href = family_url
span.textContent = ''; // Clear span
e.removeChild(span);
link.appendChild(span);
e.appendChild(link);
}
}
}
});
-587
View File
@@ -1,587 +0,0 @@
/* This file is used by default by all Scribble documents.
See also "manual.css", which is added by default by the
`scribble/manual` language. */
/* CSS seems backward: List all the classes for which we want a
particular font, so that the font can be changed in one place. (It
would be nicer to reference a font definition from all the places
that we want it.)
As you read the rest of the file, remember to double-check here to
see if any font is set. */
/* Monospace: */
.maincolumn, .refpara, .refelem, .tocset, .stt, .hspace, .refparaleft, .refelemleft, .reffootnote {
font-family: monospace;
}
/* Serif: */
.main, .refcontent, .tocview, .tocsub, .sroman, i {
font-family: serif;
}
/* Sans-serif: */
.version, .versionNoNav, .ssansserif, .navfamily {
font-family: sans-serif;
}
.ssansserif {
font-size: 80%;
font-weight: bold;
}
/* Emphasis: alternate italics and normal as we nest */
.emph {
font-style: italic;
}
.emph .emph {
font-style: normal;
}
.emph .emph .emph {
font-style: italic;
}
.emph .emph .emph .emph {
font-style: normal;
}
.emph .emph .emph .emph .emph {
font-style: italic;
}
.emph .emph .emph .emph .emph .emph {
font-style: normal;
}
/* ---------------------------------------- */
p, .SIntrapara {
display: block;
margin: 1em 0;
}
h1 { /* per-page main title */
font-size: 1.5em;
margin-top: 0;
}
h2, h3, h4, h5, h6, h7, h8 {
margin-top: 1.75em;
margin-bottom: 0.5em;
}
h2 {
font-size: 1.17em;
}
h3 {
font-size: 1.00em;
}
h4 {
font-size: 0.83em;
}
.SSubSubSubSection {
font-weight: bold;
font-size: 0.83em; /* should match h5; from HTML 4 reference */
}
/* Needed for browsers like Opera, and eventually for HTML 4 conformance.
This means that multiple paragraphs in a table element do not have a space
between them. */
table p {
margin-top: 0;
margin-bottom: 0;
}
/* ---------------------------------------- */
/* Main */
body {
color: black;
background-color: #ffffff;
}
table td {
padding-left: 0;
padding-right: 0;
}
.maincolumn {
width: 43em;
margin-right: -40em;
margin-left: 15em;
}
.main {
text-align: left;
}
/* ---------------------------------------- */
/* Navigation */
.navsettop, .navsetbottom {
background-color: #f0f0e0;
padding: 0.25em 0 0.25em 0;
}
.navsettop {
margin-bottom: 1.5em;
border-bottom: 2px solid #e0e0c0;
}
.navsetbottom {
margin-top: 2em;
border-top: 2px solid #e0e0c0;
}
.navleft {
margin-left: 1ex;
position: relative;
float: left;
white-space: nowrap;
}
.navright {
margin-right: 1ex;
position: relative;
float: right;
white-space: nowrap;
}
.nonavigation {
color: #e0e0e0;
}
.navleft .tocsettoggle {
display: none;
}
.searchform {
display: inline;
margin: 0;
padding: 0;
}
.nosearchform {
display: none;
}
.searchbox {
width: 16em;
margin: 0px;
padding: 0px;
background-color: #eee;
border: 1px solid #ddd;
vertical-align: middle;
}
.searchbox::placeholder {
text-align: center;
}
#contextindicator {
position: fixed;
background-color: #c6f;
color: #000;
font-family: monospace;
font-weight: bold;
padding: 2px 10px;
display: none;
right: 0;
bottom: 0;
}
/* ---------------------------------------- */
/* Version */
.versionbox {
position: relative;
float: right;
left: 2em;
height: 0em;
width: 13em;
margin: 0em -13em 0em 0em;
}
.version {
font-size: small;
}
.versionNoNav {
font-size: xx-small; /* avoid overlap with author */
}
.version:before, .versionNoNav:before {
content: "Version ";
}
/* ---------------------------------------- */
/* Language Family */
.navfamily {
float: right;
white-space: nowrap;
left: 2em;
height: 0em;
width: 10.5em;
margin: 1.5em -13em 0em 0em;
font-size: small;
}
.docfamily:after {
content: " language family";
}
/* ---------------------------------------- */
/* Margin notes */
.refpara, .refelem, .reffootnote {
position: relative;
float: right;
left: 2em;
height: 0em;
width: 13em;
margin: 0em -13em 0em 0em;
}
.refpara, .refparaleft, .reffootnote {
top: -1em;
}
.refcolumn {
background-color: #F5F5DC;
display: block;
position: relative;
width: 13em;
font-size: 85%;
border: 0.5em solid #F5F5DC;
margin: 0 0 0 0;
white-space: normal; /* in case margin note is inside code sample */
}
.refcontent {
margin: 0 0 0 0;
}
.refcontent p {
margin-top: 0;
margin-bottom: 0;
}
.refparaleft, .refelemleft {
position: relative;
float: left;
right: 2em;
height: 0em;
width: 13em;
margin: 0em 0em 0em -13em;
}
.refcolumnleft {
background-color: #F5F5DC;
display: block;
position: relative;
width: 13em;
font-size: 85%;
border: 0.5em solid #F5F5DC;
margin: 0 0 0 0;
}
/* narrow display => inline right-hand margin notes */
@media screen and (max-width: 55em) {
.refpara, .refelem, .reffootnote {
all: unset !important;
display: block !important;
margin: 1em 0 !important;
padding: 0.75em !important;
}
}
/* ---------------------------------------- */
/* Table of contents, inline */
.toclink {
text-decoration: none;
color: blue;
font-size: 85%;
}
.toptoclink {
text-decoration: none;
color: blue;
font-weight: bold;
}
/* ---------------------------------------- */
/* Table of contents, left margin */
.tocset {
position: relative;
float: left;
width: 12.5em;
margin-right: 2em;
}
.tocset td {
vertical-align: text-top;
}
.tocview {
text-align: left;
background-color: #f0f0e0;
}
.tocsub {
text-align: left;
margin-top: 0.5em;
background-color: #f0f0e0;
}
.tocviewlist, .tocsublist {
margin-left: 0.2em;
margin-right: 0.2em;
padding-top: 0.2em;
padding-bottom: 0.2em;
}
.tocviewlist table {
font-size: 82%;
}
.tocviewlisttopspace {
margin-bottom: 1em;
}
.tocviewsublist, .tocviewsublistonly, .tocviewsublisttop, .tocviewsublistbottom {
margin-left: 0.4em;
border-left: 1px solid #bbf;
padding-left: 0.8em;
}
.tocviewsublist {
margin-bottom: 1em;
}
.tocviewsublist table,
.tocviewsublistonly table,
.tocviewsublisttop table,
.tocviewsublistbottom table {
font-size: 75%;
}
.tocviewtitle * {
font-weight: bold;
}
.tocviewlink {
text-decoration: none;
color: blue;
}
.tocviewselflink {
text-decoration: underline;
color: blue;
}
.tocviewtoggle {
text-decoration: none;
color: blue;
font-size: 75%; /* looks better, and avoids bounce when toggling sub-sections due to font alignments */
}
.tocsublist td {
padding-left: 1em;
text-indent: -1em;
}
.tocsublinknumber {
font-size: 82%;
}
.tocsublink {
font-size: 82%;
text-decoration: none;
}
.tocsubseclink {
font-size: 82%;
text-decoration: none;
}
.tocsubnonseclink {
font-size: 82%;
text-decoration: none;
padding-left: 0.5em;
}
.tocsubtitle {
font-size: 82%;
font-style: italic;
margin: 0.2em;
}
/* ---------------------------------------- */
/* Some inline styles */
.indexlink {
text-decoration: none;
}
.nobreak {
white-space: nowrap;
}
pre { margin-left: 2em; }
blockquote { margin-left: 2em; }
ol { list-style-type: decimal; }
ol ol { list-style-type: lower-alpha; }
ol ol ol { list-style-type: lower-roman; }
ol ol ol ol { list-style-type: upper-alpha; }
.SCodeFlow {
display: block;
margin-left: 1em;
margin-bottom: 0em;
margin-right: 1em;
margin-top: 0em;
white-space: nowrap;
}
.SVInsetFlow {
display: block;
margin-left: 0em;
margin-bottom: 0em;
margin-right: 0em;
margin-top: 0em;
}
.SubFlow {
display: block;
margin: 0em;
}
.boxed {
width: 100%;
background-color: #E8E8FF;
}
.hspace {
}
.slant {
font-style: oblique;
}
.badlink {
text-decoration: underline;
color: red;
}
.plainlink {
text-decoration: none;
color: blue;
}
.techoutside { text-decoration: underline; color: #b0b0b0; }
.techoutside:hover { text-decoration: underline; color: blue; }
/* .techinside:hover doesn't work with FF, .techinside:hover>
.techinside doesn't work with IE, so use both (and IE doesn't
work with inherit in the second one, so use blue directly) */
.techinside { color: black; }
.techinside:hover { color: blue; }
.techoutside:hover>.techinside { color: inherit; }
.SCentered {
text-align: center;
}
.imageleft {
float: left;
margin-right: 0.3em;
}
.Smaller {
font-size: 82%;
}
.Larger {
font-size: 122%;
}
/* A hack, inserted to break some Scheme ids: */
.mywbr {
display: inline-block;
height: 0;
width: 0;
font-size: 1px;
}
.compact li p {
margin: 0em;
padding: 0em;
}
.noborder img {
border: 0;
}
.SVerbatim {
white-space: nowrap;
}
.SAuthorListBox {
position: relative;
float: right;
left: 2em;
top: -2.25em;
height: 0em;
width: 13em;
margin: 0em -13em 0em 0em;
}
.SAuthorList {
font-size: 82%;
}
.SAuthorList:before {
content: "by ";
}
.author {
display: inline;
white-space: nowrap;
}
/* print styles : hide the navigation elements */
@media print {
.tocset,
.navsettop,
.navsetbottom { display: none; }
.maincolumn {
width: auto;
margin-right: 13em;
margin-left: 0;
}
}
/** Begin headings */
/* Hide the button group by default, but show them on hovering the heading title */
.button-group {
padding-left: 0.3em;
visibility: hidden;
position: absolute;
}
.heading:hover > .button-group {
visibility: visible;
}
.button-group > a {
margin: 0 0.25em;
}
.button-group > a, .button-group > a:hover {
text-decoration: none;
}
.heading-anchor {
font-size: 60%;
/* A trick to color an emoji from https://stackoverflow.com/questions/32413731/color-for-unicode-emoji */
color: transparent;
text-shadow: 0 0 0 gray;
vertical-align: 5%;
}
.heading-source {
visibility: hidden;
}
-116
View File
@@ -1,116 +0,0 @@
#lang racket/base
(require rackunit
racket/file
git)
(define tmp (make-temporary-file "racket-git-test~a" 'directory))
(dynamic-wind
void
(lambda ()
(make-directory (build-path tmp "sub"))
(parameterize ([current-directory tmp])
(git 'init)
(check-true (git-repository?))
(check-equal? (git-current-branch) "master")
(check-true (git-clean?))
(git 'config "user.name" "Racket Git Test")
(git 'config "user.email" "racket-git-test@example.invalid")
(check-equal? (git-config "user.name") "Racket Git Test")
(call-with-output-file ".gitignore"
#:exists 'truncate/replace
(lambda (out) (displayln "ignored.txt" out)))
(call-with-output-file "ignored.txt"
#:exists 'truncate/replace
(lambda (out) (displayln "ignored" out)))
(call-with-output-file (build-path "sub" "hello.txt")
#:exists 'truncate/replace
(lambda (out) (displayln "hello" out)))
(check-equal? (git-status-lines)
'("?? .gitignore" "?? sub/hello.txt"))
(define status-output (open-output-string))
(define displayed-status
(parameterize ([current-output-port status-output])
(dgit 'status)))
(check-equal? displayed-status (git 'status))
(check-equal? (get-output-string status-output)
"New - .gitignore\nNew - sub/hello.txt\n")
(check-false (regexp-match? #rx"ignored[.]txt"
(get-output-string status-output)))
(parameterize ([current-directory (build-path tmp "sub")])
(git 'add "hello.txt"))
(check-equal? (git-status-lines)
'("?? .gitignore" "A sub/hello.txt"))
(git 'add ".gitignore")
(define first (git-commit "initial commit"))
(check-equal? (string-length first) 40)
(check-true (git-clean?))
(check-equal? (git 'diff) "")
(check-equal? (git 'diff '--cached) "")
(call-with-output-file (build-path "sub" "hello.txt")
#:exists 'truncate/replace
(lambda (out) (displayln "changed" out)))
(define worktree-diff (git 'diff))
(check-true (regexp-match? #rx"-hello" worktree-diff))
(check-true (regexp-match? #rx"[+]changed" worktree-diff))
(check-equal? (git 'diff '--cached) "")
(define diff-output (open-output-string))
(parameterize ([current-output-port diff-output])
(dgit 'diff))
(check-equal? (get-output-string diff-output) worktree-diff)
(git 'add "sub/hello.txt")
(check-equal? (git 'diff) "")
(define cached-diff (git 'diff '--cached))
(check-true (regexp-match? #rx"-hello" cached-diff))
(check-true (regexp-match? #rx"[+]changed" cached-diff))
(git 'commit "prepare branches")
(git 'checkout '-b "work")
(call-with-output-file (build-path "sub" "hello.txt")
#:exists 'truncate/replace
(lambda (out) (displayln "work" out)))
(git 'add)
(git 'commit "work change")
(check-equal? (file->string (build-path "sub" "hello.txt")) "work\n")
(git 'checkout "master")
(check-equal? (file->string (build-path "sub" "hello.txt")) "changed\n")
(check-equal? (git-current-branch) "master")
(check-not-false (member "work" (git 'branch)))
;; Safe checkout must not overwrite an uncommitted tracked change.
(call-with-output-file (build-path "sub" "hello.txt")
#:exists 'truncate/replace
(lambda (out) (displayln "dirty" out)))
(check-exn exn:fail? (lambda () (git 'checkout "work")))
(check-equal? (git-current-branch) "master")
(check-equal? (file->string (build-path "sub" "hello.txt")) "dirty\n")
(call-with-output-file (build-path "sub" "hello.txt")
#:exists 'truncate/replace
(lambda (out) (displayln "changed" out)))
(git 'branch '-d "work")
(check-false (member "work" (git 'branch)))
(define tag-id (git 'tag "v0.1"))
(check-equal? (string-length tag-id) 40)
(check-equal? (git 'tag) '("v0.1"))
(git 'checkout "v0.1")
(check-false (git-current-branch))
(git 'checkout "master")
(git 'tag '-d "v0.1")
(check-equal? (git 'tag) '())
(check-equal? (length (git 'log 10)) 2)))
(lambda ()
(delete-directory/files tmp)))
-30
View File
@@ -1,30 +0,0 @@
#lang racket/base
(require rackunit
"../credentials.rkt")
;; Never touch the real racket-git preferences from the test suite.
(parameterize ([current-git-credentials-store 'racket-git-test]
[current-git-credentials-unlock-store 'racket-git-test-unlock])
(with-handlers ([exn:fail? (lambda (_) (void))])
(git-credentials-lock!))
(with-handlers ([exn:fail? (lambda (_) (void))])
(git-credentials-init! "test-password" #:unlock-for 60))
(unless (git-credentials-unlocked?)
(git-credentials-unlock! "test-password" #:for 60))
(check-true (git-credentials-unlocked?))
(git-credentials-set! "https://credentials-test.invalid"
"tester" "secret-token")
(check-equal?
(git-credentials-ref "https://credentials-test.invalid")
'("tester" . "secret-token"))
(check-equal?
(git-credentials-ref "https://credentials-test.invalid/a/b.git")
'("tester" . "secret-token"))
(git-credentials-remove! "https://credentials-test.invalid")
(check-false (git-credentials-ref "https://credentials-test.invalid"))
(git-credentials-lock!)
(check-false (git-credentials-unlocked?)))
-127
View File
@@ -1,127 +0,0 @@
#lang racket/base
(require rackunit
racket/file
git)
(define tmp (make-temporary-file "racket-git-remote-test~a" 'directory))
(define origin (build-path tmp "origin.git"))
(define a (build-path tmp "a"))
(define b (build-path tmp "b"))
(define c (build-path tmp "c"))
(define (configure!)
(git 'config "user.name" "Racket Git Test")
(git 'config "user.email" "racket-git-test@example.invalid"))
(dynamic-wind
void
(lambda ()
(git-init origin #:bare? #t)
(make-directory a)
(parameterize ([current-directory a])
(git 'init)
(configure!)
(call-with-output-file "value.txt"
#:exists 'truncate/replace
(lambda (out) (displayln "one" out)))
(git 'add "value.txt")
(git 'commit "one")
(git 'remote 'add "origin" (path->string origin))
(check-equal? (git 'remote) '("origin"))
(check-equal? (git 'remote 'get-url "origin") (path->string origin))
(define push-out (open-output-string))
(parameterize ([current-output-port push-out])
(git 'push))
;; A successful push must remain safe when the returned credentials,
;; callbacks and options become eligible for collection.
(collect-garbage)
(collect-garbage)
(collect-garbage)
(check-true (regexp-match? #rx"\\[git\\] push origin/master"
(get-output-string push-out)))
(define quiet-out (open-output-string))
(parameterize ([current-output-port quiet-out])
(git 'push #:quiet #t))
(check-equal? (get-output-string quiet-out) ""))
(git-clone (path->string origin) b)
(define clone-quiet-out (open-output-string))
(parameterize ([current-output-port clone-quiet-out])
(git-clone (path->string origin) c #:quiet #t))
(check-equal? (get-output-string clone-quiet-out) "")
(parameterize ([current-directory b])
(configure!)
(check-equal? (git-current-branch) "master")
(call-with-output-file "value.txt"
#:exists 'truncate/replace
(lambda (out) (displayln "two" out)))
(git 'add "value.txt")
(git 'commit "two")
(git 'push)
(git 'tag "v2")
(git 'push-tag "v2")
(define push-tag-quiet-out (open-output-string))
(parameterize ([current-output-port push-tag-quiet-out])
(git 'push-tag "v2" #:quiet #t))
(check-equal? (get-output-string push-tag-quiet-out) ""))
(parameterize ([current-directory a])
(check-equal? (file->string "value.txt") "one\n")
(check-equal? (string-length (git 'pull)) 40)
(check-equal? (file->string "value.txt") "two\n")
(check-true (git-clean?))
(git 'fetch)
(check-equal? (git 'tag) '("v2"))
(define fetch-quiet-out (open-output-string))
(parameterize ([current-output-port fetch-quiet-out])
(git 'fetch #:quiet #t))
(check-equal? (get-output-string fetch-quiet-out) "")
(define pull-quiet-out (open-output-string))
(check-false
(parameterize ([current-output-port pull-quiet-out])
(git 'pull #:quiet #t)))
(check-equal? (get-output-string pull-quiet-out) "")
(call-with-output-file "local.txt"
#:exists 'truncate/replace
(lambda (out) (displayln "local" out)))
(git 'add "local.txt")
(git 'commit "local change"))
(parameterize ([current-directory b])
(call-with-output-file "remote.txt"
#:exists 'truncate/replace
(lambda (out) (displayln "remote" out)))
(git 'add "remote.txt")
(git 'commit "remote change")
(git 'push))
(parameterize ([current-directory a])
(define before (git-head))
(check-exn #rx"non-fast-forward"
(lambda () (git 'pull)))
(check-equal? (git-head) before)
(check-true (file-exists? "local.txt"))
(check-false (file-exists? "remote.txt"))))
(lambda ()
(delete-directory/files tmp)))
;; HTTPS push without configured credentials must fail before entering libgit2.
(let ([tmp2 (make-temporary-file "racket-git-https-test~a" 'directory)])
(dynamic-wind
void
(lambda ()
(parameterize ([current-directory tmp2])
(git 'init)
(configure!)
(call-with-output-file "x.txt" #:exists 'truncate/replace
(lambda (out) (displayln "x" out)))
(git 'add "x.txt")
(git 'commit "x")
(git 'remote 'add "origin" "https://example.invalid/private/repo.git")
(check-exn #rx"no HTTPS credentials are stored"
(lambda () (git 'push)))))
(lambda () (delete-directory/files tmp2))))
-18
View File
@@ -1,18 +0,0 @@
#lang racket/base
(require racket/file rackunit git)
(define tmp (make-temporary-file "racket-git-stress~a" 'directory))
(dynamic-wind
void
(lambda ()
(parameterize ([current-directory tmp])
(git 'init)
(git 'config "user.name" "Stress Test")
(git 'config "user.email" "stress@example.invalid")
(for ([i (in-range 50)])
(call-with-output-file "counter.txt" #:exists 'truncate/replace
(lambda (out) (fprintf out "~a\n" i)))
(git 'add "counter.txt")
(define oid (git 'commit (format "commit ~a" i)))
(check-equal? (string-length oid) 40))
(check-equal? (length (git 'log 100)) 50)))
(lambda () (delete-directory/files tmp)))