#lang scribble/manual @(require (for-label racket/base git)) @title{git} @author{Hans Dijkema} @defmodule[git] 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 short form is intended for build scripts and interactive use: @racketblock[ (require git) (git 'status) (git 'diff) (git 'diff '--cached) (git 'add "main.rkt" "info.rkt") (git 'restore '--staged "scratch.rkt") (git 'reset 'HEAD "--" "main.rkt") (git 'grep '-i '-n "todo") (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]{ 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{Version} @defproc[(git-version) string?]{Returns the package version from @tt{info.rkt}. The command form is @racket[(git 'version)]. The version is not duplicated in @tt{main.rkt}; @tt{info.rkt} is the single source of truth.} @racketblock[ (git 'version) ; => "0.2.12" ] @section{Repository} @defproc[(git-repository? [path path-string? (current-directory)]) boolean?]{Returns whether @racket[path] is inside a Git repository.} @defproc[(git-root [path path-string? (current-directory)]) path?]{Returns the repository worktree root.} @defproc[(git-init [path path-string? (current-directory)] [#:bare? bare? any/c #f]) path?]{Initializes a repository.} @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. The command form @racket[(git 'add '-A)] stages all current status entries, including new, modified, and removed paths.} @racketblock[ (git 'add '-A) (git 'commit "Update all changed files") ] @defproc[(git-restore [argument any/c] ...) void?]{Restores paths using Git-like command arguments. With only paths, the worktree is restored from the index, as in @tt{git restore path}. With @racket['--staged], matching index entries are restored from HEAD while the worktree is left untouched. @racket['--worktree] can be combined with @racket['--staged], and @racket['--source] selects another revision.} @racketblock[ (git 'restore "main.rkt") (git 'restore '--staged "scrbl/racket-makefile.bak") (git 'restore '--source "HEAD~1" "main.rkt") ] @defproc[(git-reset [argument any/c] ...) void?]{Resets HEAD, the index, or selected paths using Git-like command arguments. With @racket['--soft], @racket['--mixed], or @racket['--hard], the corresponding whole-repository reset is performed. Path resets use the familiar @tt{--} separator.} @racketblock[ (git 'reset 'HEAD "--" "main.rkt") (git 'reset '--mixed 'HEAD) (git 'reset '--hard 'HEAD) ] @section{Grep} @defstruct*[git-grep-entry ([path string?] [line-number exact-positive-integer?] [line string?])]{Describes one line selected by @racket[git-grep]. Results are always structured this way, regardless of display-oriented flags such as @racket['-n], @racket['-l], or @racket['-c].} @defproc[(git-grep [argument any/c] ...) (listof git-grep-entry?)]{Searches tracked files in the current worktree, or in an optional revision supplied after the pattern. String patterns are regular expressions. @racket['-i] makes matching case-insensitive and @racket['-v] inverts the match. The flags @racket['-n], @racket['-l], and @racket['-c] do not change the structured result; they control how @racket[dgit] displays it. Binary files are skipped.} @racketblock[ (git 'grep "TODO") (git 'grep '-i "todo") (git 'grep '-v "generated") (git 'grep "old-name" 'HEAD~1) (dgit 'grep '-n "TODO") ; path:line-number:text (dgit 'grep '-l "TODO") ; matching file names only (dgit 'grep '-c "TODO") ; number of matching lines per file ] @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.} @defproc[(git-prompt [message string? #f]) string?]{Returns @racket[message] when supplied. Without an argument, displays @tt{Give (commit) message: }, reads one line from the current input port, and returns it. This is convenient in interactive make targets before staging and committing changes.} @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. The command form is @racket[(git 'branch-current)].} @defproc*[([(git-branch) (listof string?)] [(git-branch [name string?]) string?])]{Lists local branches, or creates @racket[name] at HEAD.} @defproc[(git-branch-create [name string?] [start-point string? "HEAD"]) string?]{Creates a local branch named @racket[name] at @racket[start-point] and returns @racket[name]. The default start point is the current HEAD, including detached HEAD. The command forms are @racket[(git 'branch-create name)] and @racket[(git 'branch-create name start-point)].} @defproc[(git-branch-delete [name string?]) void?]{Deletes a local branch. The command form is @racket[(git 'branch '-d name)].} @defproc[(git-switch [name string?]) (or/c string? #f)]{Switches to an existing local branch and attaches HEAD to that branch. The command form is @racket[(git 'switch name)]. An unknown local branch raises an exception.} @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-merge [name string?] [message (or/c string? #f) #f]) (or/c string? #f)]{Merges @racket[name] into the currently attached local branch. An up-to-date merge returns @racket[#f]. A fast-forward returns the new HEAD OID. A clean non-fast-forward merge creates a two-parent merge commit and returns its OID. If @racket[message] is @racket[#f], the merge commit message is @tt{Merge branch 'name'}. Conflicting merges raise an exception before changing HEAD or the worktree. The command forms are @racket[(git 'merge name)] and @racket[(git 'merge name message)].} @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.} @subsection{Recovering a commit made with detached HEAD} A commit made while HEAD is detached is not attached to a local branch. The following sequence gives the commit a temporary branch name, switches back to @tt{main}, merges the rescued commit, verifies the result, pushes it, and removes the temporary branch: @racketblock[ (git 'branch-current) ; => #f (git 'branch-create "rescue-readme") (git 'switch "main") (git 'merge "rescue-readme") (git 'status) (dgit 'log 5) (git 'push) (git 'branch '-d "rescue-readme") ] The @racket[(dgit 'log 5)] form is the compact Racket equivalent of using a short command-line log for verification: it displays the abbreviated commit OID and summary for the five newest commits. @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. A start and completion message and transfer progress are written to the current output port unless @racket[quiet] is true. Progress callbacks only record transfer state; the libgit2 operation runs in a parallel Racket thread while the calling Racket thread performs output outside FFI callback context. Push progress distinguishes packing/compression from sending when libgit2 reports those phases.} @defproc[(git-push-tag [tag string?] [remote string? "origin"] [#:quiet quiet any/c #f]) void?]{Pushes one tag. A start and completion message and transfer progress are written to the current output port unless @racket[quiet] is true. Progress callbacks only record transfer state; the libgit2 operation runs in a parallel Racket thread while the calling Racket thread performs output outside FFI callback context. Push progress distinguishes packing/compression from sending when libgit2 reports those phases.} 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[ (git 'version) (git 'init) (git 'clone "https://example/repo.git") (git 'status) (git 'add "file.rkt") (git 'add '-A) (git 'restore '--staged "file.rkt") (git 'reset 'HEAD "--" "file.rkt") (git 'config "user.name" "Name") (git 'commit "message") (git 'branch-current) (git 'branch) (git 'branch "feature") (git 'branch-create "rescue" "HEAD") (git 'switch "feature") (git 'merge "rescue") (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 #:quiet #t) (git 'push-tag "v0.1") ] @section{HTTPS credentials} Git credentials are stored in @tt{racket-git.ini} in the normal Racket preferences directory. Tokens are encrypted with AES-GCM. The encryption key is derived from the store password with PBKDF2-HMAC-SHA256. @defproc[(git-credentials-init! [password string?] [#:unlock-for seconds real? 86400]) void?]{ Creates the credential store and leaves it unlocked for @racket[seconds].} @defproc[(git-credentials-unlock! [password string?] [#:for seconds real? 86400]) void?]{ Unlocks the credential store. The temporary unlock state is stored separately in @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.} @defproc[(git-credentials-unlocked?) boolean?]{Returns whether a non-expired unlock key is currently available.} @defproc[(git-credentials-set! [remote string?] [username string?] [token string?]) void?]{ Stores an HTTPS username and token. Credentials are keyed by host.} @defproc[(git-credentials-ref [remote string?]) (or/c #f pair?)]{ Returns the username/token pair for @racket[remote], or @racket[#f] when none is stored. The store must be unlocked when a credential exists.} @defproc[(git-credentials-remove! [remote string?]) void?]{Removes credentials for the host represented by @racket[remote].}