Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .entire/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"enabled": true,
"telemetry": true,
"strategy_options": {
"checkpoint_remote": {
"provider": "github",
"repo": "go-git/entire"
}
}
}
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:
shell: bash
env:
GO_GIT_REF: ${{ inputs.go_git_ref }}
CONFORMANCE_VERBOSE: "1"
run: make conformance
- name: Upload TAP results
if: failure()
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ $(GOLANGCI):

.PHONY: build
build:
go build -o build/ ./...
go build -o build/bin/ ./...

validate: validate-lint validate-dirty

Expand Down
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

This provides a Go Git CLI binary using `go-git`.

## Purpose

The CLI exposes key features from `go-git` so it can be assessed and tested
against upstream Git on a like-for-like basis. Each subcommand is a thin
wrapper over a `go-git` API, so the behaviour exercised by `gogit <command>`
is the behaviour that `go-git` consumers will see.

## Bridging gaps in `go-git`

When upstream Git tests require a feature that `go-git` does not yet
expose, the gap is filled temporarily in `internal/<package>/`. This
directory is a staging area whose layout mirrors the eventual go-git
home (e.g. `internal/plumbing/format/idxfile` is shaped for upstream's
`plumbing/format/idxfile`). Not everything that lives here belongs in
`go-git` — some bits are test-only shims — but the layering keeps
upstream tests that exercise the bulk of `go-git` unblocked while the
upstreaming conversation is in progress.

## Installation

```bash
Expand All @@ -14,7 +32,41 @@ go install github.com/go-git/cli/cmd/gogit
gogit <command> [flags]
```

## Conformance testing

`make conformance` runs a curated set of upstream Git tests against
`gogit`, using upstream's own `t/` harness. The runner
(`conformance/run.sh`) builds `gogit`, symlinks it as `git` in a
staging directory, and points `GIT_TEST_INSTALLED` there so the
upstream framework picks our binary up unchanged.

The upstream test source comes from either:

- A local checkout pointed at by `$GIT_SRC` (e.g.
`GIT_SRC=$HOME/git/go-git/git make conformance`), or
- A shallow clone of `github.com/git/git`'s default branch, re-fetched
on every run so upstream behaviour drift surfaces immediately.

The set of curated tests is `conformance/tests.txt` — one filename per
line, `#` comments are ignored:

```
t2008-checkout-subdir.sh
t5308-pack-detect-duplicates.sh
t5325-reverse-index.sh
```

Individual tests can be run directly:

```bash
./conformance/run.sh t2008-checkout-subdir.sh # one test, all cases
./conformance/run.sh t2008-checkout-subdir.sh 1,3 # one test, selected cases
```

`$GO_GIT_REF` (commit SHA, tag, or branch) overrides the `go.mod` pin
for a single run so the gate can be evaluated against an in-flight
`go-git` change.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

38 changes: 38 additions & 0 deletions cmd/gogit-test-tool/delta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"errors"
"fmt"
"os"

"github.com/go-git/go-git/v6/plumbing/format/packfile"
)

// runDelta implements `test-tool delta {-d|-p} <base> <delta> <out>`. We only
// need -p (apply); upstream's -d (compute) is not used by the curated tests.
func runDelta(args []string) error {
if len(args) != 4 || args[0] != "-p" {
return errors.New("usage: delta -p <base> <delta> <output>")
}

base, err := os.ReadFile(args[1])
if err != nil {
return fmt.Errorf("read base %s: %w", args[1], err)
}

delta, err := os.ReadFile(args[2])
if err != nil {
return fmt.Errorf("read delta %s: %w", args[2], err)
}

result, err := packfile.PatchDelta(base, delta)
if err != nil {
return fmt.Errorf("apply delta: %w", err)
}

if err := os.WriteFile(args[3], result, 0o644); err != nil {
return fmt.Errorf("write %s: %w", args[3], err)
}

return nil
}
68 changes: 68 additions & 0 deletions cmd/gogit-test-tool/delta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package main

import (
"os"
"path/filepath"
"testing"

"github.com/go-git/go-git/v6/plumbing/format/packfile"
)

func TestDeltaApplyRoundTrip(t *testing.T) {
t.Parallel()
dir := t.TempDir()

src := []byte("hello, world\nthis is the base text\n")
tgt := []byte("hello, world\nthis is the target text with edits\n")

deltaBytes := packfile.DiffDelta(src, tgt)

srcPath := filepath.Join(dir, "src")
deltaPath := filepath.Join(dir, "delta")
outPath := filepath.Join(dir, "out")

if err := os.WriteFile(srcPath, src, 0o644); err != nil {
t.Fatal(err)
}

if err := os.WriteFile(deltaPath, deltaBytes, 0o644); err != nil {
t.Fatal(err)
}

if err := runDelta([]string{"-p", srcPath, deltaPath, outPath}); err != nil {
t.Fatalf("runDelta: %v", err)
}

got, err := os.ReadFile(outPath)
if err != nil {
t.Fatal(err)
}

if string(got) != string(tgt) {
t.Fatalf("round-trip mismatch:\n got: %q\nwant: %q", got, tgt)
}
}

func TestDeltaApplyRejectsCorrupt(t *testing.T) {
t.Parallel()
dir := t.TempDir()

src := []byte("base")
corruptDelta := []byte{0xff, 0xff, 0xff} // not a valid delta header

srcPath := filepath.Join(dir, "src")
deltaPath := filepath.Join(dir, "delta")
outPath := filepath.Join(dir, "out")

if err := os.WriteFile(srcPath, src, 0o644); err != nil {
t.Fatal(err)
}

if err := os.WriteFile(deltaPath, corruptDelta, 0o644); err != nil {
t.Fatal(err)
}

if err := runDelta([]string{"-p", srcPath, deltaPath, outPath}); err == nil {
t.Fatal("expected non-nil error on corrupt delta")
}
}
58 changes: 58 additions & 0 deletions cmd/gogit-test-tool/genrandom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"bufio"
"errors"
"fmt"
"io"
"strconv"
)

// genRandom emits length deterministic bytes derived from seed, byte-for-byte
// equivalent to upstream's t/helper/test-genrandom.c. The seed loop in C is
// `do { next = next*11 + *c; } while (*c++);` — the body runs once per byte
// INCLUDING the terminating NUL. The PRNG is the POSIX.1-2001 rand() LCG:
// next = next*1103515245 + 12345; output byte = (next>>16) & 0xff. All
// arithmetic is uint64 to match `unsigned long` on 64-bit hosts.
func genRandom(w io.Writer, seed string, length uint64) error {
var next uint64

for _, c := range []byte(seed) {
next = next*11 + uint64(c)
}
// Fold the implicit NUL terminator (matching C do/while semantics).
next *= 11

bw := bufio.NewWriter(w)
defer bw.Flush()

for range length {
next = next*1103515245 + 12345

if err := bw.WriteByte(byte((next >> 16) & 0xff)); err != nil {
return err
}
}
Comment on lines +29 to +35

return nil
}

func runGenRandom(args []string) error {
if len(args) < 1 || len(args) > 2 {
return errors.New("usage: genrandom <seed_string> [<size>]")
}

seed := args[0]
length := ^uint64(0) // ULONG_MAX equivalent

if len(args) == 2 {
v, err := strconv.ParseUint(args[1], 10, 64)
if err != nil {
return fmt.Errorf("cannot parse size %q: %w", args[1], err)
}

length = v
}

return genRandom(stdoutWriter(), seed, length)
}
46 changes: 46 additions & 0 deletions cmd/gogit-test-tool/genrandom_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"bytes"
"testing"
)

func TestGenRandomMatchesUpstream(t *testing.T) {
t.Parallel()

var buf bytes.Buffer

if err := genRandom(&buf, "foo", 8); err != nil {
t.Fatalf("genRandom: %v", err)
}

// Golden bytes locked in from genRandom("foo", 8) output and verified
// against a hand-trace of the algorithm for the first two bytes.
want := []byte{0xd3, 0x1c, 0x75, 0x5b, 0xc4, 0x0f, 0x9d, 0xd0}

if !bytes.Equal(buf.Bytes(), want) {
t.Fatalf("genRandom(\"foo\", 8) = %x; want %x", buf.Bytes(), want)
}
}

func TestGenRandomDeterministic(t *testing.T) {
t.Parallel()

var a, b bytes.Buffer

if err := genRandom(&a, "seed", 64); err != nil {
t.Fatalf("first: %v", err)
}

if err := genRandom(&b, "seed", 64); err != nil {
t.Fatalf("second: %v", err)
}

if !bytes.Equal(a.Bytes(), b.Bytes()) {
t.Fatal("two invocations with same seed/length differ")
}

if a.Len() != 64 {
t.Fatalf("length = %d want 64", a.Len())
}
}
51 changes: 51 additions & 0 deletions cmd/gogit-test-tool/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Package main is the gogit-test-tool helper: a drop-in for upstream's
// t/helper/test-tool used during conformance runs. Subcommands implemented
// here mirror the upstream subcommands the curated tests actually exercise.
package main

import (
"errors"
"fmt"
"io"
"os"
)

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run(args []string) error {
if len(args) == 0 {
return errors.New("usage: gogit-test-tool <subcommand> [args...]")
}

sub, rest := args[0], args[1:]

switch sub {
case "genrandom":
return runGenRandom(rest)
case "delta":
return runDelta(rest)
case "sha1":
return runSHA1(rest)
case "sha256":
return runSHA256(rest)
case "date":
return runDate(rest)
case "path-utils":
return runPathUtils(rest)
case "env-helper":
return runEnvHelper(rest)
default:
return fmt.Errorf("unimplemented subcommand: %s", sub)
}
}

// stdoutWriter returns the writer that subcommands emit raw output to.
// Indirected so tests can drop a buffer in if needed (not done in v1).
func stdoutWriter() io.Writer {
return os.Stdout
}
Loading
Loading