Files

761 lines
24 KiB
Racket

#lang scribble/manual
@(require (for-label racket/base
racket/contract
"../main.rkt"))
@title[#:tag "top"]{git-cli}
@author{Hans Dijkema}
@defmodule[git-cli]
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.
@section{Command interface}
@defform[(git command argument ...)]{
Runs a Git @racket[command]. When the command has a registered git-cli wrapper,
that wrapper is used. Registered wrappers can provide structured Racket results,
argument handling, or other command-specific behavior.
Registered command symbols include @racket['init], @racket['status],
@racket['add], @racket['commit], @racket['push], @racket['pull],
@racket['fetch], @racket['config], @racket['branch], @racket['remote],
@racket['stash], @racket['restore], @racket['reset], @racket['revert],
@racket['rebase], @racket['merge], @racket['cherry-pick],
@racket['mergetool], @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].
When no wrapper is registered, the command and arguments are passed directly to
the installed Git executable through @racket[run-git]. The result is handled by
the same standard result processing used by ordinary pass-through wrappers:
normal Git output is displayed and a successful command returns @racket[#t];
a non-zero exit status raises a git-cli error.
This makes dedicated wrappers optional for Git commands where git-cli does not
add useful behavior.
@racketblock[
(git 'blame "main.rkt")
(git* clean -n)
(git* worktree list)
(git* archive --format=zip HEAD)
]
Some registered commands process the result into a Racket value, such as
@racket['status], @racket['grep], @racket['log] with @tt{--list},
@racket['version], and @racket['new-version].
}
@defform[(git* command argument ...)]{
Provides compact command-style syntax for @racket[git]. The command name is
used as a symbol. Other literal arguments are converted to strings.
@racketblock[
(git* remote -v)
(git* switch main)
]
An argument written as @racket[(eval expression)] is evaluated instead of being
converted from its literal syntax.
@racketblock[
(define branch "develop")
(git* switch (eval branch))
]
@bold{Important:} bare arguments to @racket[git*] are command-line text, not
Racket values. An identifier is quoted syntactically and converted to a string,
even when that identifier is also bound to a Racket variable or procedure.
@racketblock[
(define branch "develop")
(git* switch branch)
; passes "branch"
(git* switch (eval branch))
; passes "develop"
]
This distinction matters most for git-cli commands whose arguments are not
ordinary Git command-line strings. Such wrappers should accept the textual
arguments produced by @racket[git*]. For example, @racket[git-new-version] now
accepts both symbols and text:
@racketblock[
(git 'new-version 'min)
(git* new-version min)
]
Because @racket[git] falls back to direct Git execution for commands without a
registered wrapper, @racket[git*] can also be used with those commands.
@racket[gt] is retained as a compatibility alias for @racket[git*].
}
@section{Provided commands}
@defproc[(git-init [argument any/c] ...) boolean?]{
Runs @tt{git init} with the supplied arguments and returns @racket[#t] when Git
exits successfully.
@racketblock[
(git-init)
(git* init)
(git* init --bare)
]
}
@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.
}
@defproc[(git-commit [argument any/c] ...) boolean?]{
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.
}
@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-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-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}.
For example:
@racketblock[
(git-fetch)
(git-fetch '--prune)
(git 'fetch '--prune)
]
}
@defproc[(git-config [argument any/c] ...) any/c]{
Provides a Racket-oriented interface to @tt{git config}. The same interface is
available through @racket[git] with command @racket['config].
@racketblock[
(git 'config '--all)
(git 'config 'get '--all)
(git 'config '--global 'get '--all)
(git 'config 'get '--global '--all)
]
returns all visible configuration entries as key/value items:
@racketblock[
'(("user.name" "Hans Dijkema")
("user.email" "hans@example.invalid")
("credential.helper" "manager"))
]
@racketblock[
(git 'config 'get "credential.helper")
]
returns one value as a string, or @racket[#f] when the key is absent.
@racketblock[
(git 'config 'get '--all "credential.helper")
]
returns all values for one key as a list. An absent key produces the empty
list.
Configuration values can be written with @racket['set!]:
@racketblock[
(git 'config 'set! "user.email" "hans@example.invalid")
(git 'config '--global 'set! "user.email" "hans@example.invalid")
(git 'config 'set! '--global "user.email" "hans@example.invalid")
]
The optional scope can be @tt{--global}, @tt{--local}, or @tt{--system}. It may
appear directly after @racket['config] or directly after @racket['get] /
@racket['set!]. A successful write returns @racket[#t].
}
@subsection{git-cli editor configuration}
The @racket[git-config] procedure also recognizes the git-cli-specific
@tt{editor} operation. This does not write Git's @tt{core.editor}; it controls
the editor command used by git-cli through @tt{GIT_EDITOR} and
@tt{GIT_SEQUENCE_EDITOR}.
With no additional argument an interactive selection is displayed.
@racketblock[
(git* config editor)
]
The available editors can also be returned without prompting.
@racketblock[
(git* config editor --list)
(git* config editor --downloads)
]
Each item contains the short editor name, description, command and a boolean
indicating whether that command is currently selected.
A detected editor can be selected by its short name, or automatic detection can
be restored.
@racketblock[
(git* config editor vscode)
(git* config editor notepad++)
(git* config editor auto)
]
An arbitrary editor command can be supplied using the ordinary procedure form.
@racketblock[
(git 'config 'editor "C:\\Program Files\\MyEditor\\editor.exe --wait")
]
Changing the editor updates both @tt{GIT_EDITOR} and
@tt{GIT_SEQUENCE_EDITOR} immediately for subsequent Git commands.
@defproc[(find-editors) list?]{
Returns all well-known GUI editors found on the current platform as
@racket[(name description command)] items.
}
@defproc[(set-editor-auto!) string?]{
Clears the explicit git-cli editor selection, activates the first automatically
detected editor and returns its command. An exception is raised when no
well-known GUI editor can be found.
}
@defproc[(editor-downloads) list?]{
Returns official download pointers for optional GUI editors as
@racket[(name description url)] items. No network request is performed.
}
On Windows, Notepad++ is detected both on @tt{PATH} and in the normal Program
Files locations. git-cli invokes it with @tt{-multiInst -nosession}.
@subsection{git-cli merge tool configuration}
The @racket[git-config] procedure also recognizes the git-cli-specific
@tt{mergetool} operation.
@racketblock[
(git* config mergetool)
(git* config mergetool --list)
(git* config mergetool --downloads)
(git* config mergetool winmerge)
(git* config mergetool auto)
]
With no additional argument an interactive selection is displayed.
@tt{--list} returns @racket[(name description path current?)] items and
@tt{--downloads} returns official download pointers. A detected merge tool can
be selected by its short Git tool name. @tt{auto} clears the explicit git-cli
selection and restores automatic detection.
@defproc[(find-mergetools) list?]{
Returns all well-known graphical merge tools found on the current platform as
@racket[(name description path)] items.
}
@defproc[(mergetool-downloads) list?]{
Returns official download pointers for optional merge tools as
@racket[(name description url)] items. No network request is performed.
}
@defproc[(set-mergetool-auto!) string?]{
Clears the explicit git-cli merge tool selection and returns the first
automatically detected Git merge tool name. An exception is raised when no
well-known merge tool can be found.
}
@defproc[(git-branch [argument any/c] ...) (or/c boolean? list?)]{
Runs @tt{git branch} with the supplied arguments. This can be used to list,
create, rename, or delete branches according to the options supported by the
installed Git executable.
With Git's @tt{-l} or @tt{--list} option, git-cli returns structured branch
information. Each item starts with one of @racket['current], @racket['local],
or @racket['remote], followed by the branch name.
@racketblock[
(git-branch '-l)
'((current "main")
(local "develop"))
]
Git's normal branch selection and sorting options are passed through. For
example, remote branches can be requested with @tt{-r}, all branches with
@tt{-a}, and Git's @tt{--sort=<key>} option controls the returned order.
@racketblock[
(git 'branch '-l '-a "--sort=refname")
'((current "main")
(local "develop")
(remote "origin/main"))
]
Without @tt{-l} or @tt{--list}, normal Git output is displayed and the
procedure returns @racket[#t] when Git exits successfully.
}
@defproc[(git-remote [argument any/c] ...) any/c]{
Runs @tt{git remote} with the supplied arguments and keeps the command's own
subcommand structure.
With no arguments, the remote names are returned as a Racket list.
@racketblock[
(git-remote)
'("origin" "upstream")
]
With top-level @tt{-v} or @tt{--verbose}, each line reported by Git is returned
as a separate structured item containing the remote name, URL, and the
@racket['fetch] or @racket['push] role.
@racketblock[
(git-remote '-v)
'(("origin" "https://example.invalid/project.git" fetch)
("origin" "https://example.invalid/project.git" push))
]
The two Git lines are deliberately not merged. This keeps the result close to
the output and semantics of @tt{git remote -v}.
For @tt{get-url}, one URL is returned as a string. With @tt{--all}, a list of
URLs is returned.
@racketblock[
(git 'remote 'get-url "origin")
(git 'remote 'get-url '--all "origin")
(git 'remote 'get-url '--push '--all "origin")
]
Other forms, including @tt{add}, @tt{rename}, @tt{remove}, @tt{set-head},
@tt{show}, @tt{prune}, @tt{update}, @tt{set-branches}, and @tt{set-url}, are
passed to Git unchanged and use the normal git-cli command result processing.
}
@defproc[(git-stash [argument any/c] ...) (or/c boolean? list?)]{
Runs @tt{git stash} with the supplied arguments. Calling it without a
subcommand keeps Git's normal behavior, which is equivalent to
@tt{git stash push}.
@tt{git stash list} is returned as structured Racket data. Each item contains
the stash reference and Git's stash description.
@racketblock[
(git-stash 'list)
'(("stash@{0}" "WIP on main: 1234567 Example")
("stash@{1}" "On main: older work"))
]
The structured form is only used when the caller has not supplied a
@tt{--format} or @tt{--pretty} option. Explicit Git formatting is left
unchanged.
Other stash subcommands, including @tt{push}, @tt{show}, @tt{pop},
@tt{apply}, @tt{drop}, @tt{clear}, @tt{branch}, @tt{create}, @tt{store},
@tt{export}, and @tt{import}, are passed to Git unchanged.
}
@defproc[(git-restore [argument any/c] ...) boolean?]{
Runs @tt{git restore} with the supplied arguments. Git's path, source,
@tt{--staged}, @tt{--worktree}, and patch semantics are preserved.
@racketblock[
(git-restore "main.rkt")
(git-restore '--staged "main.rkt")
(git* restore --source=HEAD~1 main.rkt)
]
}
@defproc[(git-reset [argument any/c] ...) boolean?]{
Runs @tt{git reset} with the supplied arguments. Modes such as @tt{--soft},
@tt{--mixed}, @tt{--hard}, @tt{--merge}, and @tt{--keep}, as well as path
forms, are passed through unchanged.
@racketblock[
(git-reset '--hard 'HEAD)
(git* reset --soft HEAD~1)
]
}
@defproc[(git-revert [argument any/c] ...) boolean?]{
Runs @tt{git revert} with the supplied arguments. Sequencer controls such as
@tt{--continue}, @tt{--skip}, @tt{--quit}, and @tt{--abort} are passed through
unchanged.
@racketblock[
(git-revert 'HEAD)
(git* revert --abort)
]
}
@defproc[(git-rebase [argument any/c] ...) boolean?]{
Runs @tt{git rebase} with the supplied arguments, including normal, interactive,
and continuation/abort forms.
@racketblock[
(git-rebase "main")
(git* rebase --continue)
(git* rebase --abort)
]
}
@defproc[(git-merge [argument any/c] ...) boolean?]{
Runs @tt{git merge} with the supplied arguments and preserves Git's merge
options and control forms.
@racketblock[
(git-merge "feature")
(git* merge --abort)
]
}
@defproc[(git-cherry-pick [argument any/c] ...) boolean?]{
Runs @tt{git cherry-pick} with the supplied arguments. Sequencer controls such
as @tt{--continue}, @tt{--skip}, @tt{--quit}, and @tt{--abort} are passed
through unchanged.
@racketblock[
(git-cherry-pick "abc1234")
(git* cherry-pick --continue)
]
}
@defproc[(git-mergetool [argument any/c] ...) boolean?]{
Runs @tt{git mergetool}. When the caller does not specify @tt{-t},
@tt{--tool=<tool>}, or @tt{--tool-help}, git-cli first tries
@racket[find-mergetool] and supplies the selected tool through Git's normal
@tt{--tool=<tool>} option. If no known graphical tool is found, Git is allowed
to choose its own default.
@racketblock[
(git-mergetool)
(git* mergetool --tool=meld)
]
}
@defproc[(find-editor) (or/c string? #f)]{
Returns the configured or detected GUI editor command. When git-cli is
loaded, the detected editor is assigned once to @tt{GIT_EDITOR} and
@tt{GIT_SEQUENCE_EDITOR}. The editor is not started by this procedure.
}
@defproc[(set-editor! [command string?]) any/c]{
Stores a git-cli-specific editor command.
}
@defproc[(find-mergetool) (or/c string? #f)]{
Returns the configured or detected Git merge tool name.
}
@defproc[(find-mergetool-path) (or/c path? #f)]{
Returns the executable path of an automatically detected merge tool. The finder
checks @tt{PATH} first and then well-known platform installation locations.
When the merge tool was configured explicitly by name, this procedure returns
@racket[#f].
}
@defproc[(set-mergetool! [tool string?]) any/c]{
Stores the Git merge tool name preferred by git-cli.
}
@defproc[(git-switch [argument any/c] ...) boolean?]{
Runs @tt{git switch} with the supplied arguments.
@racketblock[
(git-switch "main")
(git-switch '-c "feature")
(git 'switch "main")
]
}
@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")
]
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.
@racketblock[
(git-tag '-l '-n)
'(("v0.3.16" "Release 0.3.16")
("v0.3.17" "Release 0.3.17"))
]
With @tt{-n<number>} and a number greater than one, git-cli asks Git for that
many content lines using @tt{%(contents:lines=<number>)}. The returned message
is kept as one string, including embedded newlines.
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.
Other forms keep the normal command behavior and return @racket[#t] when Git
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-diff [argument any/c] ...) (or/c boolean? string?)]{
Shows differences between Git objects or the working tree and index.
By default a successful diff is rendered as HTML in the default browser. The
git-cli-specific option @tt{--output=-} keeps Git's textual output on standard
output. @tt{--output=string} returns the textual diff as a string.
@racketblock[
(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.
}
@racketblock[
(git-new-version 'min)
(git 'new-version 'min)
(git* new-version min)
]
The version kind may be supplied as a symbol or string. This makes the command
compatible with @racket[git*], whose bare arguments are converted to text.
@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")
]
}
@section{Authentication retry}
Git commands recognize common authentication failures immediately after the
Git process finishes and before command-specific result processing takes
place. Such a failure is represented by @racket[exn:fail:git-auth?].
@defparam[current-git-authentication-handler handler procedure?]{
Controls the callback used when an authentication failure is detected. The
callback receives the Git command symbol, the processed Git argument list and
the @racket[exn:fail:git-auth] exception.
The callback returns a true value when it has handled authentication and the
original Git command should be tried again. A command is retried at most once.
The default callback is @racket[default-git-authentication-handler].
@racketblock[
(current-git-authentication-handler
(lambda (cmd args e)
;; Perform credential handling here.
#t))
]
}
@defproc[(exn:fail:git-auth? [v any/c]) boolean?]{
Recognizes the authentication exception used internally by git-cli.
}
@defproc[(exn:fail:git-auth-command [e exn:fail:git-auth?]) symbol?]{
Returns the Git command of the failed invocation.
}
@defproc[(exn:fail:git-auth-args [e exn:fail:git-auth?]) list?]{
Returns the processed Git arguments of the failed invocation.
}
@defproc[(exn:fail:git-auth-exit-code [e exn:fail:git-auth?]) exact-integer?]{
Returns Git's exit code.
}
@defproc[(exn:fail:git-auth-output [e exn:fail:git-auth?]) list?]{
Returns the ordered @racket['stdout]/@racket['stderr] output items from the
failed Git process.
}
@section{Authentication}
@defproc[(default-git-authentication-handler
[cmd symbol?]
[args list?]
[e exn:fail:git-auth?])
boolean?]{
Handles one authentication failure. A credential that already failed is first
rejected. An existing Git credential helper is then asked for a replacement
credential. If that does not succeed, git-cli requests a username and
password/token using @racket[input-prompt]. Its @racket[#:loop-until] callbacks
both validate the input and return the value that is used. If no helper is
configured, Git's non-persistent @tt{cache} helper is configured locally before
the credential is approved. The original command is retried once; a credential
that fails on the retry is rejected before the Git error is raised.
}
@defparam[current-git-authentication-handler handler procedure?]{
Contains the authentication callback used after a recognized authentication
failure. Its default value is @racket[default-git-authentication-handler].
}