From 7c6a338326917ad36d08f604b1583c41770a7c92 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 11 Aug 2026 22:35:01 +0200 Subject: [PATCH] new version with git help --- .gitignore | 5 +++- Makefile.rkt | 33 ++++++++++++++++++++-- README.md | 24 +++++++++++++++- info.rkt | 5 ++-- main.rkt | 66 +++++++++++++++++++++++++++++++++++++++++++ scribblings/git.scrbl | 56 ++++++++++++++++++++++++++++-------- tests/basic.rkt | 5 ++++ 7 files changed, 175 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index e053672..4e4160d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ /compiled /doc -/*.bak /scribblings/compiled /tests/compiled + +/docs +*~ +*.bak \ No newline at end of file diff --git a/Makefile.rkt b/Makefile.rkt index de04d4e..1f1babb 100644 --- a/Makefile.rkt +++ b/Makefile.rkt @@ -2,6 +2,7 @@ (require racket-makefile package-zipper + net/sendurl ) @@ -11,11 +12,37 @@ (target clean (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 (λ (f) (displayln f) (rm-f f)) (list-files "scribblings" #px"[.](css|js|html)$")) + (for-each (λ (d) (displayln d) (rm-rf d)) (list-dirs "." #px"(compiled|doc|docs)$" #:recursive #t)) + (when (directory-exists? "scribblings") + (for-each (λ (f) (displayln f) (rm-f f)) (list-files "scribblings" #px"[.](css|js|html)$"))) ) (target package (deps clean) (zip-package)) - \ No newline at end of file + +(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))) \ No newline at end of file diff --git a/README.md b/README.md index d988980..bc86b6f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ A small command-line-like Git module for Racket, implemented directly on top of ```racket (require git) +(git 'help) +(git 'help 'grep) (git 'status) (git 'diff) (git 'diff '--cached) @@ -24,12 +26,32 @@ A small command-line-like Git module for Racket, implemented directly on top of `git` is an ordinary procedure; command names are symbols. `dgit` performs the same operation, displays a compact human-readable result, and returns that result. +`(git 'help)` opens the locally installed Scribble documentation. A command can +be supplied to jump directly to its section, for example `(git 'help 'restore)` +or `(git 'help 'grep)`. + For `grep`, the natural `(git 'grep '-i "pattern")` spelling is supported even though Racket's reader represents `-i` as the complex number `0-1i`; in grep option position that value is interpreted as Git's `-i` flag. The same operations are available as normal procedures such as `git-status`, `git-add`, `git-commit`, `git-tag`, `git-current-branch`, `git-switch`, and `git-checkout`. + +## Help + +Open the locally installed Scribble documentation through Racket's +documentation cross-reference index: + +```racket +(git 'help) +(git 'help 'grep) +(git 'help 'restore) +``` + +`raco setup git` builds and indexes the package documentation. The help command +uses the indexed path and anchor instead of assuming a particular documentation +directory. + ## HTTPS credentials Version 0.2 adds persistent HTTPS credentials. They are stored in the @@ -97,7 +119,7 @@ operations use them automatically: ## Supported Git operations -Version 0.2 supports repository discovery, init, clone, status, diff, add, restore, reset, grep, config, +Version 0.2 supports help, repository discovery, init, clone, status, diff, add, restore, reset, grep, config, commit, branch, branch-current, switch, checkout, merge, lightweight tags, log, remotes, fetch, fast-forward-only pull, push, tag push, network transfer progress, and HTTPS username/token credentials. diff --git a/info.rkt b/info.rkt index 9499620..6a4ab45 100644 --- a/info.rkt +++ b/info.rkt @@ -2,7 +2,7 @@ (define collection "git") (define pkg-desc "Command-line-like Git operations for Racket, implemented with libgit2") -(define version "0.2.13") +(define version "0.2.15") (define pkg-authors '("Hans Dijkema")) (define license 'MIT) @@ -12,12 +12,13 @@ ("simple-ini" #:version "0.3.3") "crypto-lib" "net-lib" + "racket-index" + "scribble-lib" "racket-makefile" "package-zipper")) (define build-deps '("rackunit-lib" - "scribble-lib" "racket-doc")) (define scribblings diff --git a/main.rkt b/main.rkt index 784a694..7908f92 100644 --- a/main.rkt +++ b/main.rkt @@ -9,12 +9,16 @@ racket/match racket/path racket/string + net/sendurl + setup/xref + scribble/xref "credentials.rkt" libgit2) (provide git dgit git-version + git-help git-repository? git-root git-init @@ -80,6 +84,63 @@ (error 'git-version "cannot read info.rkt")) (info 'version (lambda () (error 'git-version "info.rkt has no version")))) +(define git-help-bindings + (hash 'version 'git-version + 'init 'git-init + 'clone 'git-clone + 'status 'git-status + 'diff 'git-diff + 'add 'git-add + 'restore 'git-restore + 'reset 'git-reset + 'grep 'git-grep + 'config 'git-config + 'commit 'git-commit + 'branch-current 'git-current-branch + 'branch 'git-branch + 'branch-create 'git-branch-create + 'switch 'git-switch + 'checkout 'git-checkout + 'merge 'git-merge + 'tag 'git-tag + 'log 'git-log + 'remote 'git-remotes + 'fetch 'git-fetch + 'pull 'git-pull + 'push 'git-push + 'push-tag 'git-push-tag + 'credentials 'git-credentials-init! + 'prompt 'git-prompt + 'git-prompt 'git-prompt + 'help 'git-help)) + +(define (git-help [topic #f]) + (unless (or (not topic) (symbol? topic)) + (raise-argument-error 'git-help "(or/c #f symbol?)" topic)) + (define binding + (if topic + (hash-ref git-help-bindings topic + (lambda () + (error 'git-help "unknown help topic: ~a" topic))) + 'git)) + ;; Use Racket's installed-documentation cross-reference database instead of + ;; guessing where raco setup placed this package's generated HTML. + (define xref (load-collections-xref)) + (define tag + (xref-binding->definition-tag xref (list 'git binding) #f)) + (unless tag + (error 'git-help + "documentation for ~a is not indexed; run raco setup git" + binding)) + (define-values (path anchor) + (xref-tag->path+anchor xref tag)) + (unless path + (error 'git-help + "documentation for ~a is indexed but has no local path" + binding)) + (send-url/file path #:fragment anchor) + (void)) + (define zero-oid-string (make-string GIT_OID_HEXSZ #\0)) (define branch-prefix "refs/heads/") (define tag-prefix "refs/tags/") @@ -1170,6 +1231,11 @@ (unless (null? args) (error 'git "version takes no arguments")) (git-version)] + [(help) + (match args + ['() (git-help)] + [(list topic) (git-help topic)] + [_ (error 'git "help takes zero or one topic")])] [(init) (apply git-init args)] [(clone) (keyword-apply git-clone '(#:quiet) (list quiet) args)] [(status) (apply git-status args)] diff --git a/scribblings/git.scrbl b/scribblings/git.scrbl index 4f396dc..539497c 100644 --- a/scribblings/git.scrbl +++ b/scribblings/git.scrbl @@ -2,7 +2,7 @@ @(require (for-label racket/base git)) -@title{git} +@title[#:tag "top"]{git} @author{Hans Dijkema} @defmodule[git] @@ -38,15 +38,27 @@ Calls @racket[git], displays its result in a compact human-readable form, and re (dgit 'status) ] -@section{Version} +@section[#:tag "help"]{Help} + +@defproc[(git-help [topic (or/c symbol? #f) #f]) void?]{Opens the installed Scribble documentation for this package in the default web browser. With a command topic, opens the section for that command. The command forms are @racket[(git 'help)] and @racket[(git 'help 'grep)].} + +@racketblock[ +(git 'help) +(git 'help 'grep) +(git 'help 'restore) +] + +Help uses Racket's installed-documentation cross-reference database. It resolves the documented binding for the requested command through @racketmodname[setup/xref] and @racketmodname[scribble/xref], then opens the path and anchor recorded by @exec{raco setup}. No documentation directory is guessed or constructed by @racketmodname[git]. If the binding is not indexed, @racket[git-help] reports that @exec{raco setup git} should be run. + +@section[#:tag "version"]{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.13" +(git 'version) ; => "0.2.15" ] -@section{Repository} +@section[#:tag "repository"]{Repository} @defproc[(git-repository? [path path-string? (current-directory)]) boolean?]{Returns whether @racket[path] is inside a Git repository.} @@ -57,7 +69,7 @@ Calls @racket[git], displays its result in a compact human-readable form, and re @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} +@section[#:tag "status"]{Status} @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.} @@ -67,10 +79,14 @@ Calls @racket[git], displays its result in a compact human-readable form, and re @defproc[(git-clean?) boolean?]{Returns @racket[#t] when @racket[git-status] is empty.} +@section[#:tag "diff"]{Diff} + @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}.} +@section[#:tag "add"]{Add} + @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[ @@ -78,6 +94,8 @@ Calls @racket[git], displays its result in a compact human-readable form, and re (git 'commit "Update all changed files") ] +@section[#:tag "restore"]{Restore} + @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[ @@ -86,6 +104,8 @@ Calls @racket[git], displays its result in a compact human-readable form, and re (git 'restore '--source "HEAD~1" "main.rkt") ] +@section[#:tag "reset"]{Reset} + @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[ @@ -94,7 +114,7 @@ Calls @racket[git], displays its result in a compact human-readable form, and re (git 'reset '--hard 'HEAD) ] -@section{Grep} +@section[#:tag "grep"]{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].} @@ -111,19 +131,23 @@ Calls @racket[git], displays its result in a compact human-readable form, and re (dgit 'grep '-c "TODO") ; number of matching lines per file ] -@section{Configuration and commits} +@section[#:tag "config"]{Configuration} @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.} +@section[#:tag "commit"]{Commit} + @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.} +@subsection[#:tag "git-prompt"]{Commit prompt} + @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} +@section[#:tag "branches"]{Branches} @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)].} @@ -134,14 +158,20 @@ Calls @racket[git], displays its result in a compact human-readable form, and re @defproc[(git-branch-delete [name string?]) void?]{Deletes a local branch. The command form is @racket[(git 'branch '-d name)].} +@section[#:tag "switch-checkout"]{Switch and checkout} + @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.} +@section[#:tag "merge"]{Merge} + @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)].} +@section[#:tag "tags"]{Tags} + @defproc*[([(git-tag) (listof string?)] [(git-tag [name string?]) string?])]{Lists tags, or creates a lightweight tag at HEAD and returns its OID.} @@ -164,7 +194,7 @@ A commit made while HEAD is detached is not attached to a local branch. The foll 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} +@section[#:tag "log"]{Log} @defstruct*[git-log-entry ([id string?] [summary string?] [time integer?])]{Describes one commit returned by @racket[git-log].} @@ -172,7 +202,7 @@ The @racket[(dgit 'log 5)] form is the compact Racket equivalent of using a shor @defproc[(git-log-lines [entries (listof git-log-entry?) (git-log)]) (listof string?)]{Formats log entries as short OID plus summary.} -@section{Remotes} +@section[#:tag "remotes"]{Remotes} @defproc[(git-remotes) (listof string?)]{Lists remotes.} @@ -190,11 +220,13 @@ The @racket[(dgit 'log 5)] form is the compact Racket equivalent of using a shor Remote HTTPS operations automatically use credentials from the @tt{racket-git} credential store when an entry exists for the remote host. -@section{Command form} +@section[#:tag "command-form"]{Command form} The following command-like forms are supported directly: @racketblock[ +(git 'help) +(git 'help 'grep) (git 'version) (git 'init) (git 'clone "https://example/repo.git") @@ -228,7 +260,7 @@ The following command-like forms are supported directly: (git 'push-tag "v0.1") ] -@section{HTTPS credentials} +@section[#:tag "credentials"]{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 diff --git a/tests/basic.rkt b/tests/basic.rkt index c38680e..b18b0d7 100644 --- a/tests/basic.rkt +++ b/tests/basic.rkt @@ -182,3 +182,8 @@ (check-equal? (length (git 'log 10)) 2))) (lambda () (delete-directory/files tmp))) + + +;; Help validates its topic before trying to open the installed documentation. +(check-exn exn:fail? (lambda () (git 'help 'not-a-git-command))) +(check-exn exn:fail:contract? (lambda () (git-help "grep")))