Simplifying.
This commit is contained in:
@@ -5,13 +5,19 @@ A small command-line-like Git module for Racket, implemented directly on top of
|
||||
```racket
|
||||
(require git)
|
||||
|
||||
(git status)
|
||||
(git add "main.rkt" "info.rkt")
|
||||
(git commit "Implement raco support")
|
||||
(git tag "v0.2")
|
||||
(git checkout "main")
|
||||
(git 'status)
|
||||
(git 'add "main.rkt" "info.rkt")
|
||||
(git 'commit "Implement raco support")
|
||||
(git 'tag "v0.2")
|
||||
(git 'checkout "main")
|
||||
|
||||
;; Display-oriented variant:
|
||||
(dgit 'status)
|
||||
```
|
||||
|
||||
`git` is an ordinary procedure; command names are symbols. `dgit` performs the
|
||||
same operation, displays a compact human-readable result, and returns that result.
|
||||
|
||||
The same operations are available as normal procedures such as `git-status`, `git-add`, `git-commit`, `git-tag`, and `git-checkout`.
|
||||
|
||||
## HTTPS credentials
|
||||
@@ -24,13 +30,13 @@ PBKDF2-HMAC-SHA256.
|
||||
Create the credential store once:
|
||||
|
||||
```racket
|
||||
(git credentials init "store password")
|
||||
(git 'credentials 'init "store password")
|
||||
```
|
||||
|
||||
Store a token for a Git host:
|
||||
|
||||
```racket
|
||||
(git credentials set
|
||||
(git 'credentials 'set
|
||||
"https://git.dijkewijk.nl"
|
||||
"hans"
|
||||
token)
|
||||
@@ -42,13 +48,13 @@ repositories on that host.
|
||||
The store is unlocked for one day by default:
|
||||
|
||||
```racket
|
||||
(git credentials unlock "store password")
|
||||
(git 'credentials 'unlock "store password")
|
||||
```
|
||||
|
||||
or for an explicit number of seconds:
|
||||
|
||||
```racket
|
||||
(git credentials unlock "store password" (* 8 60 60))
|
||||
(git 'credentials 'unlock "store password" (* 8 60 60))
|
||||
```
|
||||
|
||||
The temporary unlock state is stored in `racket-git-unlock.ini` in Racket's
|
||||
@@ -61,16 +67,16 @@ so `racket-git-unlock.ini` must be treated as sensitive during that period.
|
||||
Lock immediately with:
|
||||
|
||||
```racket
|
||||
(git credentials lock)
|
||||
(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)
|
||||
(git 'fetch)
|
||||
(git 'pull)
|
||||
(git 'push)
|
||||
```
|
||||
|
||||
## Supported Git operations
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
(define collection "git")
|
||||
(define pkg-desc "Command-line-like Git operations for Racket, implemented with libgit2")
|
||||
(define version "0.2.1")
|
||||
(define version "0.2.2")
|
||||
(define pkg-authors '("Hans Dijkema"))
|
||||
(define license 'MIT)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
(require ffi/unsafe
|
||||
racket/list
|
||||
racket/match
|
||||
racket/path
|
||||
racket/string
|
||||
"credentials.rkt"
|
||||
@@ -11,6 +12,7 @@
|
||||
_git_error_code/check))
|
||||
|
||||
(provide git
|
||||
dgit
|
||||
git-repository?
|
||||
git-root
|
||||
git-init
|
||||
@@ -555,44 +557,93 @@
|
||||
remote-name
|
||||
(format "refs/tags/~a:refs/tags/~a" tag tag))]))
|
||||
|
||||
(define-syntax git
|
||||
(syntax-rules (init clone status add config commit branch -d checkout -b
|
||||
tag log remote get-url fetch pull push push-tag
|
||||
credentials init unlock lock set remove unlocked?)
|
||||
[(_ init args ...) (git-init args ...)]
|
||||
[(_ clone args ...) (git-clone args ...)]
|
||||
[(_ status) (git-status)]
|
||||
[(_ add paths ...) (git-add paths ...)]
|
||||
[(_ config args ...) (git-config args ...)]
|
||||
[(_ commit message) (git-commit message)]
|
||||
[(_ branch -d name) (git-branch-delete name)]
|
||||
[(_ branch) (git-branch)]
|
||||
[(_ branch name) (git-branch name)]
|
||||
[(_ checkout -b name) (git-checkout-new name)]
|
||||
[(_ checkout name) (git-checkout name)]
|
||||
[(_ tag -d name) (git-tag-delete name)]
|
||||
[(_ tag) (git-tag)]
|
||||
[(_ tag name) (git-tag name)]
|
||||
[(_ log) (git-log)]
|
||||
[(_ log count) (git-log count)]
|
||||
[(_ remote) (git-remotes)]
|
||||
[(_ remote add name url) (git-remote-add name url)]
|
||||
[(_ remote get-url name) (git-remote-url name)]
|
||||
[(_ fetch) (git-fetch)]
|
||||
[(_ fetch remote-name) (git-fetch remote-name)]
|
||||
[(_ pull) (git-pull)]
|
||||
[(_ pull remote-name) (git-pull remote-name)]
|
||||
[(_ push) (git-push)]
|
||||
[(_ push remote-name) (git-push remote-name)]
|
||||
[(_ push remote-name branch) (git-push remote-name branch)]
|
||||
[(_ push-tag tag-name) (git-push-tag tag-name)]
|
||||
[(_ push-tag tag-name remote-name) (git-push-tag tag-name remote-name)]
|
||||
[(_ credentials init password) (git-credentials-init! password)]
|
||||
[(_ credentials unlock password) (git-credentials-unlock! password)]
|
||||
[(_ credentials unlock password seconds)
|
||||
(define (git command . args)
|
||||
(unless (symbol? command)
|
||||
(raise-argument-error 'git "symbol?" command))
|
||||
(case command
|
||||
[(init) (apply git-init args)]
|
||||
[(clone) (apply git-clone args)]
|
||||
[(status) (apply git-status args)]
|
||||
[(add) (apply git-add args)]
|
||||
[(config) (apply git-config args)]
|
||||
[(commit) (apply git-commit args)]
|
||||
[(branch)
|
||||
(match args
|
||||
[(list '-d name) (git-branch-delete name)]
|
||||
[_ (apply git-branch args)])]
|
||||
[(checkout)
|
||||
(match args
|
||||
[(list '-b name) (git-checkout-new name)]
|
||||
[_ (apply git-checkout args)])]
|
||||
[(tag)
|
||||
(match args
|
||||
[(list '-d name) (git-tag-delete name)]
|
||||
[_ (apply git-tag args)])]
|
||||
[(log) (apply git-log args)]
|
||||
[(remote)
|
||||
(match args
|
||||
['() (git-remotes)]
|
||||
[(list 'add name url) (git-remote-add name url)]
|
||||
[(list 'get-url name) (git-remote-url name)]
|
||||
[_ (error 'git "invalid remote arguments: ~e" args)])]
|
||||
[(fetch) (apply git-fetch args)]
|
||||
[(pull) (apply git-pull args)]
|
||||
[(push) (apply git-push args)]
|
||||
[(push-tag) (apply git-push-tag args)]
|
||||
[(credentials)
|
||||
(match args
|
||||
[(list 'init password) (git-credentials-init! password)]
|
||||
[(list 'unlock password) (git-credentials-unlock! password)]
|
||||
[(list 'unlock password seconds)
|
||||
(git-credentials-unlock! password #:for seconds)]
|
||||
[(_ credentials lock) (git-credentials-lock!)]
|
||||
[(_ credentials unlocked?) (git-credentials-unlocked?)]
|
||||
[(_ credentials set remote username token)
|
||||
[(list 'lock) (git-credentials-lock!)]
|
||||
[(list 'unlocked?) (git-credentials-unlocked?)]
|
||||
[(list 'set remote username token)
|
||||
(git-credentials-set! remote username token)]
|
||||
[(_ credentials remove remote) (git-credentials-remove! remote)]))
|
||||
[(list 'remove remote) (git-credentials-remove! remote)]
|
||||
[_ (error 'git "invalid credentials arguments: ~e" args)])]
|
||||
[else (error 'git "unknown command: ~a" command)]))
|
||||
|
||||
(define (status-description entry)
|
||||
(define flags (git-status-entry-flags entry))
|
||||
(cond
|
||||
[(has-status? flags 'GIT_STATUS_CONFLICTED) "Conflicted"]
|
||||
[(has-status? flags 'GIT_STATUS_WT_NEW) "New"]
|
||||
[(or (has-status? flags 'GIT_STATUS_INDEX_RENAMED)
|
||||
(has-status? flags 'GIT_STATUS_WT_RENAMED)) "Renamed"]
|
||||
[(or (has-status? flags 'GIT_STATUS_INDEX_DELETED)
|
||||
(has-status? flags 'GIT_STATUS_WT_DELETED)) "Deleted"]
|
||||
[(has-status? flags 'GIT_STATUS_INDEX_NEW) "Added"]
|
||||
[(or (has-status? flags 'GIT_STATUS_INDEX_TYPECHANGE)
|
||||
(has-status? flags 'GIT_STATUS_WT_TYPECHANGE)) "Type changed"]
|
||||
[(or (has-status? flags 'GIT_STATUS_INDEX_MODIFIED)
|
||||
(has-status? flags 'GIT_STATUS_WT_MODIFIED)) "Modified"]
|
||||
[(has-status? flags 'GIT_STATUS_WT_UNREADABLE) "Unreadable"]
|
||||
[else (git-status-entry-code entry)]))
|
||||
|
||||
(define (pad-right value width)
|
||||
(define text (format "~a" value))
|
||||
(string-append text (make-string (max 0 (- width (string-length text))) #\space)))
|
||||
|
||||
(define (display-git-result command result [out (current-output-port)])
|
||||
(cond
|
||||
[(eq? command 'status)
|
||||
(for ([entry (in-list result)])
|
||||
(fprintf out "~a - ~a\n"
|
||||
(pad-right (status-description entry) 12)
|
||||
(git-status-entry-path entry)))]
|
||||
[(eq? command 'log)
|
||||
(for ([entry (in-list result)])
|
||||
(fprintf out "~a ~a\n"
|
||||
(substring (git-log-entry-id entry) 0 7)
|
||||
(git-log-entry-summary entry)))]
|
||||
[(list? result)
|
||||
(for ([item (in-list result)])
|
||||
(displayln item out))]
|
||||
[(void? result) (void)]
|
||||
[else (displayln result out)]))
|
||||
|
||||
(define (dgit command . args)
|
||||
(define result (apply git command args))
|
||||
(display-git-result command result)
|
||||
result)
|
||||
|
||||
+37
-29
@@ -14,17 +14,25 @@ The short form is intended for build scripts and interactive use:
|
||||
@racketblock[
|
||||
(require git)
|
||||
|
||||
(git status)
|
||||
(git add "main.rkt" "info.rkt")
|
||||
(git commit "Implement raco support")
|
||||
(git tag "v0.1")
|
||||
(git checkout "main")
|
||||
(git 'status)
|
||||
(git 'add "main.rkt" "info.rkt")
|
||||
(git 'commit "Implement raco support")
|
||||
(git 'tag "v0.1")
|
||||
(git 'checkout "main")
|
||||
]
|
||||
|
||||
@defform[(git command argument ...)]{
|
||||
The @racket[git] form is syntax sugar for the corresponding procedures. For example, @racket[(git status)] calls @racket[git-status], and @racket[(git commit "message")] calls @racket[git-commit].
|
||||
@defproc[(git [command symbol?] [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.
|
||||
}
|
||||
|
||||
@defproc[(dgit [command symbol?] [argument any/c] ...) any/c]{
|
||||
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].
|
||||
}
|
||||
|
||||
@racketblock[
|
||||
(dgit 'status)
|
||||
]
|
||||
|
||||
@section{Repository}
|
||||
|
||||
@defproc[(git-repository? [path path-string? (current-directory)]) boolean?]{Returns whether @racket[path] is inside a Git repository.}
|
||||
@@ -108,28 +116,28 @@ Remote HTTPS operations automatically use credentials from the @tt{racket-git} c
|
||||
The following command-like forms are supported directly:
|
||||
|
||||
@racketblock[
|
||||
(git init)
|
||||
(git clone "https://example/repo.git")
|
||||
(git status)
|
||||
(git add "file.rkt")
|
||||
(git config "user.name" "Name")
|
||||
(git commit "message")
|
||||
(git branch)
|
||||
(git branch "feature")
|
||||
(git branch -d "feature")
|
||||
(git checkout "main")
|
||||
(git checkout -b "feature")
|
||||
(git tag)
|
||||
(git tag "v0.1")
|
||||
(git tag -d "v0.1")
|
||||
(git log 10)
|
||||
(git remote)
|
||||
(git remote add "origin" "https://example/repo.git")
|
||||
(git remote get-url "origin")
|
||||
(git fetch)
|
||||
(git pull)
|
||||
(git push)
|
||||
(git push-tag "v0.1")
|
||||
(git 'init)
|
||||
(git 'clone "https://example/repo.git")
|
||||
(git 'status)
|
||||
(git 'add "file.rkt")
|
||||
(git 'config "user.name" "Name")
|
||||
(git 'commit "message")
|
||||
(git 'branch)
|
||||
(git 'branch "feature")
|
||||
(git 'branch '-d "feature")
|
||||
(git 'checkout "main")
|
||||
(git 'checkout '-b "feature")
|
||||
(git 'tag)
|
||||
(git 'tag "v0.1")
|
||||
(git 'tag '-d "v0.1")
|
||||
(git 'log 10)
|
||||
(git 'remote)
|
||||
(git 'remote 'add "origin" "https://example/repo.git")
|
||||
(git 'remote 'get-url "origin")
|
||||
(git 'fetch)
|
||||
(git 'pull)
|
||||
(git 'push)
|
||||
(git 'push-tag "v0.1")
|
||||
]
|
||||
|
||||
@section{HTTPS credentials}
|
||||
|
||||
+29
-19
@@ -11,13 +11,13 @@
|
||||
(lambda ()
|
||||
(make-directory (build-path tmp "sub"))
|
||||
(parameterize ([current-directory tmp])
|
||||
(git init)
|
||||
(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")
|
||||
(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"
|
||||
@@ -33,41 +33,51 @@
|
||||
(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"))
|
||||
(git 'add "hello.txt"))
|
||||
(check-equal? (git-status-lines)
|
||||
'("?? .gitignore" "A sub/hello.txt"))
|
||||
|
||||
(git add ".gitignore")
|
||||
(git 'add ".gitignore")
|
||||
(define first (git-commit "initial commit"))
|
||||
(check-equal? (string-length first) 40)
|
||||
(check-true (git-clean?))
|
||||
|
||||
(git checkout -b "work")
|
||||
(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")
|
||||
(git 'add)
|
||||
(git 'commit "work change")
|
||||
(check-equal? (file->string (build-path "sub" "hello.txt")) "work\n")
|
||||
|
||||
(git checkout "master")
|
||||
(git 'checkout "master")
|
||||
(check-equal? (file->string (build-path "sub" "hello.txt")) "hello\n")
|
||||
(check-equal? (git-current-branch) "master")
|
||||
(check-not-false (member "work" (git branch)))
|
||||
(check-not-false (member "work" (git 'branch)))
|
||||
|
||||
(git branch -d "work")
|
||||
(check-false (member "work" (git branch)))
|
||||
(git 'branch '-d "work")
|
||||
(check-false (member "work" (git 'branch)))
|
||||
|
||||
(define tag-id (git tag "v0.1"))
|
||||
(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-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) '())
|
||||
(git 'checkout "master")
|
||||
(git 'tag '-d "v0.1")
|
||||
(check-equal? (git 'tag) '())
|
||||
|
||||
(check-equal? (length (git log 10)) 1)))
|
||||
(check-equal? (length (git 'log 10)) 1)))
|
||||
(lambda ()
|
||||
(delete-directory/files tmp)))
|
||||
|
||||
+23
-23
@@ -10,8 +10,8 @@
|
||||
(define b (build-path tmp "b"))
|
||||
|
||||
(define (configure!)
|
||||
(git config "user.name" "Racket Git Test")
|
||||
(git config "user.email" "racket-git-test@example.invalid"))
|
||||
(git 'config "user.name" "Racket Git Test")
|
||||
(git 'config "user.email" "racket-git-test@example.invalid"))
|
||||
|
||||
(dynamic-wind
|
||||
void
|
||||
@@ -20,17 +20,17 @@
|
||||
(make-directory a)
|
||||
|
||||
(parameterize ([current-directory a])
|
||||
(git init)
|
||||
(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))
|
||||
(git push))
|
||||
(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))
|
||||
(git 'push))
|
||||
|
||||
(git-clone (path->string origin) b)
|
||||
(parameterize ([current-directory b])
|
||||
@@ -39,38 +39,38 @@
|
||||
(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"))
|
||||
(git 'add "value.txt")
|
||||
(git 'commit "two")
|
||||
(git 'push)
|
||||
(git 'tag "v2")
|
||||
(git 'push-tag "v2"))
|
||||
|
||||
(parameterize ([current-directory a])
|
||||
(check-equal? (file->string "value.txt") "one\n")
|
||||
(check-equal? (string-length (git pull)) 40)
|
||||
(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"))
|
||||
(git 'fetch)
|
||||
(check-equal? (git 'tag) '("v2"))
|
||||
|
||||
(call-with-output-file "local.txt"
|
||||
#:exists 'truncate/replace
|
||||
(lambda (out) (displayln "local" out)))
|
||||
(git add "local.txt")
|
||||
(git commit "local change"))
|
||||
(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))
|
||||
(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)))
|
||||
(lambda () (git 'pull)))
|
||||
(check-equal? (git-head) before)
|
||||
(check-true (file-exists? "local.txt"))
|
||||
(check-false (file-exists? "remote.txt"))))
|
||||
|
||||
Reference in New Issue
Block a user