From abb70f29ff55ad9b6f070da70d29dc7a113b5415 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 11 Aug 2026 13:56:50 +0200 Subject: [PATCH] More working commands --- README.md | 21 ++++++++-- info.rkt | 2 +- main.rkt | 87 +++++++++++++++++++++++++++++++++++++++--- scribblings/git.scrbl | 31 ++++++++++++++- tests/basic.rkt | 11 ++++-- tests/branch-merge.rkt | 66 ++++++++++++++++++++++++++++++++ 6 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 tests/branch-merge.rkt diff --git a/README.md b/README.md index 682d06f..9c29098 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ A small command-line-like Git module for Racket, implemented directly on top of (git 'add "main.rkt" "info.rkt") (git 'commit "Implement raco support") (git 'tag "v0.2") -(git 'checkout "main") +(git 'branch-current) +(git 'switch "main") ;; Display-oriented variant: (dgit 'status) @@ -20,7 +21,7 @@ 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. -The same operations are available as normal procedures such as `git-status`, `git-add`, `git-commit`, `git-tag`, and `git-checkout`. +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`. ## HTTPS credentials @@ -90,7 +91,7 @@ operations use them automatically: ## Supported Git operations Version 0.2 supports repository discovery, init, clone, status, add, config, -commit, branch, checkout, lightweight tags, log, remotes, fetch, +commit, branch, branch-current, switch, checkout, lightweight tags, log, remotes, fetch, fast-forward-only pull, push, tag push, network transfer progress, and HTTPS username/token credentials. SSH credentials, merge/rebase pull, annotated tags, and submodules are not yet @@ -101,3 +102,17 @@ Install from the package directory with: ```sh raco pkg install . ``` + + +## Recover a detached HEAD commit + +```racket +(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") +``` diff --git a/info.rkt b/info.rkt index 67c054d..34e9b90 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.8") +(define version "0.2.10") (define pkg-authors '("Hans Dijkema")) (define license 'MIT) diff --git a/main.rkt b/main.rkt index dac965b..dd02e28 100644 --- a/main.rkt +++ b/main.rkt @@ -27,11 +27,14 @@ git-commit git-head git-current-branch + git-switch git-branches + git-branch-create git-branch git-branch-delete git-checkout git-checkout-new + git-merge git-tags git-tag git-tag-delete @@ -66,6 +69,7 @@ (define GIT-CREDTYPE-USERNAME #x0020) (define GIT-PASSTHROUGH -30) (define GIT-CHECKOUT-OPTIONS-VERSION 1) +(define GIT-MERGE-OPTIONS-VERSION 1) (define (git-credential-callback out url username-from-url allowed-types _payload) ;; Never let a Racket exception escape through a C callback. In particular, @@ -550,18 +554,23 @@ #"") (sort branches string #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].} @@ -126,8 +149,12 @@ The following command-like forms are supported directly: (git 'add "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") diff --git a/tests/basic.rkt b/tests/basic.rkt index 513410e..6e33c1c 100644 --- a/tests/basic.rkt +++ b/tests/basic.rkt @@ -14,6 +14,7 @@ (git 'init) (check-true (git-repository?)) (check-equal? (git-current-branch) "master") + (check-equal? (git 'branch-current) "master") (check-true (git-clean?)) (git 'config "user.name" "Racket Git Test") @@ -83,7 +84,8 @@ (git 'commit "work change") (check-equal? (file->string (build-path "sub" "hello.txt")) "work\n") - (git 'checkout "master") + (check-equal? (git 'branch-current) "work") + (git 'switch "master") (check-equal? (file->string (build-path "sub" "hello.txt")) "changed\n") (check-equal? (git-current-branch) "master") (check-not-false (member "work" (git 'branch))) @@ -92,7 +94,7 @@ (call-with-output-file (build-path "sub" "hello.txt") #:exists 'truncate/replace (lambda (out) (displayln "dirty" out))) - (check-exn exn:fail? (lambda () (git 'checkout "work"))) + (check-exn exn:fail? (lambda () (git 'switch "work"))) (check-equal? (git-current-branch) "master") (check-equal? (file->string (build-path "sub" "hello.txt")) "dirty\n") (call-with-output-file (build-path "sub" "hello.txt") @@ -107,7 +109,10 @@ (check-equal? (git 'tag) '("v0.1")) (git 'checkout "v0.1") (check-false (git-current-branch)) - (git 'checkout "master") + (check-false (git 'branch-current)) + (check-exn exn:fail? (lambda () (git 'switch "missing"))) + (git 'switch "master") + (check-equal? (git 'branch-current) "master") (git 'tag '-d "v0.1") (check-equal? (git 'tag) '()) diff --git a/tests/branch-merge.rkt b/tests/branch-merge.rkt new file mode 100644 index 0000000..c7fd24a --- /dev/null +++ b/tests/branch-merge.rkt @@ -0,0 +1,66 @@ +#lang racket/base + +(require rackunit + racket/file + git) + +(define tmp (make-temporary-file "racket-git-branch-merge-test~a" 'directory)) + +(define (write-file path text) + (call-with-output-file path + #:exists 'truncate/replace + (lambda (out) (display text out)))) + +(dynamic-wind + void + (lambda () + (parameterize ([current-directory tmp]) + (git 'init) + (git 'config "user.name" "Racket Git Test") + (git 'config "user.email" "racket-git-test@example.invalid") + + (write-file "README.md" "base\n") + (git 'add "README.md") + (define base (git 'commit "base")) + + ;; Reproduce the recovery case: create a commit while HEAD is detached, + ;; give that commit a branch name, switch back, and merge it. + (git 'checkout base) + (check-false (git 'branch-current)) + (write-file "README.md" "base\nrescued\n") + (git 'add "README.md") + (define rescued (git 'commit "detached README change")) + (check-false (git 'branch-current)) + + (check-equal? (git 'branch-create "rescue-readme") "rescue-readme") + (check-not-false (member "rescue-readme" (git 'branch))) + (git 'switch "master") + (check-equal? (git 'branch-current) "master") + (check-equal? (git 'merge "rescue-readme") rescued) + (check-equal? (git-head) rescued) + (check-equal? (file->string "README.md") "base\nrescued\n") + (check-false (git 'merge "rescue-readme")) + (git 'branch '-d "rescue-readme") + (check-false (member "rescue-readme" (git 'branch))) + + ;; Also exercise a real two-parent, conflict-free merge. + (git 'branch-create "feature") + (git 'switch "feature") + (write-file "feature.txt" "feature\n") + (git 'add "feature.txt") + (git 'commit "feature change") + + (git 'switch "master") + (write-file "master.txt" "master\n") + (git 'add "master.txt") + (git 'commit "master change") + (define merge-id (git 'merge "feature")) + (check-equal? (string-length merge-id) 40) + (check-equal? (file->string "feature.txt") "feature\n") + (check-equal? (file->string "master.txt") "master\n") + (check-true (git-clean?)) + (check-equal? (git 'branch-current) "master") + (check-true (>= (length (git 'log 10)) 5)) + (git 'branch '-d "feature"))) + (lambda () + (delete-directory/files tmp)))