#lang racket/base (require ffi/unsafe racket/list racket/match racket/path racket/string "credentials.rkt" (except-in libgit2 git_remote_push) (only-in libgit2/private/base define-libgit2 _git_error_code/check)) (provide git dgit git-repository? git-root git-init git-clone (struct-out git-status-entry) git-status git-status-lines git-clean? git-add git-config git-config-get git-config-set git-commit git-head git-current-branch git-branches git-branch git-branch-delete git-checkout git-checkout-new git-tags git-tag git-tag-delete (struct-out git-log-entry) git-log git-log-lines git-remotes git-remote-add git-remote-url git-fetch git-pull git-push git-push-tag git-credentials-init! git-credentials-unlock! git-credentials-lock! git-credentials-unlocked? git-credentials-unlock-expires git-credentials-set! git-credentials-ref git-credentials-remove!) (struct git-status-entry (path code flags) #:transparent) (struct git-log-entry (id summary time) #:transparent) ;; The current Racket libgit2 package has an incorrect Scheme -> C ;; conversion for git_strarray pointers. Keep this tiny corrected binding ;; local to this module for push refspecs. (define-cstruct _git_strarray/raw ([strings _pointer] [count _size])) (define-libgit2 git_remote_push/raw (_fun _git_remote _git_strarray/raw-pointer _git_push_opts-pointer -> (_git_error_code/check)) #:c-id git_remote_push) (define zero-oid-string (make-string GIT_OID_HEXSZ #\0)) (define branch-prefix "refs/heads/") (define tag-prefix "refs/tags/") (define GIT-CREDTYPE-USERPASS-PLAINTEXT #x0001) (define GIT-CREDTYPE-USERNAME #x0020) (define GIT-PASSTHROUGH -30) (define (git-credential-callback out url username-from-url allowed-types _payload) (define saved (git-credentials-ref url)) (cond [(not saved) GIT-PASSTHROUGH] [else (define username (if (and username-from-url (not (string=? username-from-url ""))) username-from-url (car saved))) (define token (cdr saved)) (cond [(not (zero? (bitwise-and allowed-types GIT-CREDTYPE-USERPASS-PLAINTEXT))) (ptr-set! out _git_credential (git_credential_userpass_plaintext_new username token)) 0] [(not (zero? (bitwise-and allowed-types GIT-CREDTYPE-USERNAME))) (ptr-set! out _git_credential (git_credential_username_new username)) 0] [else GIT-PASSTHROUGH])])) (define (set-credential-callback! callbacks) (set-git_remote_callbacks-credentials! callbacks git-credential-callback) callbacks) (define (make-fetch-options) (define options (cast (malloc _git_fetch_opts 'atomic) _pointer _git_fetch_opts-pointer)) (git_fetch_options_init options GIT_FETCH_OPTS_VERSION) (set-credential-callback! (git_fetch_opts-callbacks options)) options) (define (make-push-options) (define options (cast (malloc _git_push_opts 'atomic) _pointer _git_push_opts-pointer)) (git_push_options_init options GIT_PUSH_OPTS_VERSION) (set-credential-callback! (git_push_opts-callbacks options)) options) (define (make-clone-options) (define options (cast (malloc _git_clone_opts 'atomic) _pointer _git_clone_opts-pointer)) (git_clone_options_init options GIT_CLONE_OPTS_VERSION) (set-credential-callback! (git_fetch_opts-callbacks (git_clone_opts-fetch_opts options))) options) (define (blank-oid) (git_oid_fromstr zero-oid-string)) (define (repository-path [start (current-directory)]) (or (git_repository_discover start) (error 'git "not inside a Git repository: ~a" start))) (define (open-repository [start (current-directory)]) (git_repository_open (repository-path start))) (define (git-repository? [path (current-directory)]) (and (git_repository_discover path) #t)) (define (git-root [path (current-directory)]) (define repo (open-repository path)) (define root (if (git_repository_is_bare repo) (git_repository_path repo) (git_repository_workdir repo))) (simplify-path (string->path root))) (define (git-init [path (current-directory)] #:bare? [bare? #f]) (git_repository_init path #:bare? bare?) (git-root path)) (define (default-clone-directory url) (define cleaned (regexp-replace #rx"/+$" url "")) (define parts (filter (lambda (s) (not (string=? s ""))) (regexp-split #rx"[/\\\\:]" cleaned))) (unless (pair? parts) (error 'git-clone "cannot derive a directory name from ~a" url)) (regexp-replace #rx"[.]git$" (last parts) "")) (define git-clone (case-lambda [(url) (git-clone url (default-clone-directory url))] [(url path) (git_clone url (path->string (if (path? path) path (string->path path))) (make-clone-options)) (git-root path)])) (define (normalize-status-flags flags) (cond [(list? flags) flags] [(symbol? flags) (if (eq? flags 'GIT_STATUS_CURRENT) null (list flags))] [else null])) (define (has-status? flags flag) (and (memq flag flags) #t)) (define (index-status-char flags) (cond [(has-status? flags 'GIT_STATUS_INDEX_NEW) #\A] [(has-status? flags 'GIT_STATUS_INDEX_MODIFIED) #\M] [(has-status? flags 'GIT_STATUS_INDEX_DELETED) #\D] [(has-status? flags 'GIT_STATUS_INDEX_RENAMED) #\R] [(has-status? flags 'GIT_STATUS_INDEX_TYPECHANGE) #\T] [else #\space])) (define (worktree-status-char flags) (cond [(has-status? flags 'GIT_STATUS_WT_MODIFIED) #\M] [(has-status? flags 'GIT_STATUS_WT_DELETED) #\D] [(has-status? flags 'GIT_STATUS_WT_RENAMED) #\R] [(has-status? flags 'GIT_STATUS_WT_TYPECHANGE) #\T] [(has-status? flags 'GIT_STATUS_WT_UNREADABLE) #\?] [else #\space])) (define (status-code flags) (cond [(has-status? flags 'GIT_STATUS_CONFLICTED) "UU"] [(has-status? flags 'GIT_STATUS_IGNORED) "!!"] [(has-status? flags 'GIT_STATUS_WT_NEW) "??"] [else (string (index-status-char flags) (worktree-status-char flags))])) (define (git-status) (define repo (open-repository)) (define result null) (git_status_foreach repo (lambda (path raw-flags _payload) (define flags (normalize-status-flags raw-flags)) ;; Match normal `git status`: ignored files are not shown unless ;; explicitly requested. git_status_foreach uses libgit2's defaults, ;; which may include them. (unless (has-status? flags 'GIT_STATUS_IGNORED) (set! result (cons (git-status-entry path (status-code flags) flags) result))) 0) #"") (sort result (lambda (a b) (stringstring path) "/")) (define (relative-pathspec workdir path) (define p0 (if (path? path) path (string->path path))) ;; A relative path supplied by the caller is relative to the caller's ;; current directory, not automatically to the repository root. (define p (path->complete-path p0 (current-directory))) (define rel (find-relative-path workdir p)) (define s (git-path-string rel)) (when (or (string=? s "..") (string-prefix? s "../")) (error 'git-add "path is outside the repository: ~a" path)) s) (define (index-accept _path _matched-pathspec _payload) 0) (define (git-add . paths) (define repo (open-repository)) (define workdir (string->path (git_repository_workdir repo))) (define index (git_repository_index repo)) (define pathspecs (make-git_strarray (for/list ([path (in-list paths)]) (relative-pathspec workdir path)))) ;; update-all stages changes and removals of already tracked files; ;; add-all adds new files and updates existing files while respecting ignores. (git_index_update_all index pathspecs index-accept #"") (git_index_add_all index pathspecs 'GIT_INDEX_ADD_DEFAULT index-accept #"") (git_index_write index) (void)) (define (git-config-get key) (define repo (open-repository)) (define config (git_repository_config repo)) ;; git_config_get_string is incorrectly declared as an allocating wrapper ;; in the current Racket libgit2 package. The entry API has the correct ;; ownership model and works on normal repository config objects. (define entry (git_config_get_entry config key)) (git_config_entry-value entry)) (define (git-config-set key value) (define repo (open-repository)) (define config (git_repository_config repo)) (git_config_set_string config key value) value) (define git-config (case-lambda [(key) (git-config-get key)] [(key value) (git-config-set key value)])) (define (head-commit repo) (cond [(git_repository_is_empty repo) #f] [(git_repository_head_unborn repo) #f] [else (define head (git_repository_head repo)) (git_commit_lookup repo (git_reference_target head))])) (define (git-head) (define repo (open-repository)) (define commit (head-commit repo)) (and commit (git_oid_fmt (git_commit_id commit)))) (define (git-commit message) (define repo (open-repository)) (define index (git_repository_index repo)) (when (git_index_has_conflicts index) (error 'git-commit "the index contains unresolved conflicts")) (define tree-id (blank-oid)) (git_index_write_tree tree-id index) (define tree (git_tree_lookup repo tree-id)) (define parent (head-commit repo)) (cond [(and (not parent) (zero? (git_index_entrycount index))) (error 'git-commit "nothing staged to commit")] [(and parent (git_oid_equal tree-id (git_commit_tree_id parent))) (error 'git-commit "nothing staged to commit")]) (define signature (git_signature_default repo)) (define commit-id (blank-oid)) (define result (if parent (git_commit_create_v commit-id repo "HEAD" signature signature #f message tree 1 parent) (git_commit_create_v commit-id repo "HEAD" signature signature #f message tree 0))) (unless (zero? result) (error 'git-commit "libgit2 git_commit_create_v failed with code ~a" result)) (git_oid_fmt commit-id)) (define (git-current-branch) (define repo (open-repository)) (cond [(git_repository_head_detached repo) #f] [(git_repository_head_unborn repo) (define head (git_reference_lookup repo "HEAD")) (define target (git_reference_symbolic_target head)) (and target (string-prefix? target branch-prefix) (substring target (string-length branch-prefix)))] [else (git_reference_shorthand (git_repository_head repo))])) (define (git-branches) (define repo (open-repository)) (define branches null) (git_reference_foreach_name repo (lambda (name _payload) (when (string-prefix? name branch-prefix) (set! branches (cons (substring name (string-length branch-prefix)) branches))) 0) #"") (sort branches string