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
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches:
- main
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.22.x", "stable"]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}

- name: Run vet
run: go vet ./...

- name: Run tests
run: go test ./...
132 changes: 132 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Developer Guide

This guide covers how to use `git-testkit`, contribute safely, run checks locally, and maintain releases.

## Audience

- **Users** writing tests that need real git repositories.
- **Contributors** making code/docs changes.
- **Maintainers/admins** preparing releases and keeping CI healthy.

## Local setup

Requirements:

- Go 1.22+
- `git` on `PATH`

Clone and verify:

```bash
# HTTPS (no SSH keys required)
git clone https://github.com/git-fire/git-testkit.git

# or SSH
# git clone git@github.com:git-fire/git-testkit.git

cd git-testkit
go test ./...
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Project structure

- `fixtures.go`: base repo creation and command helpers.
- `scenarios.go`: fluent scenario builder and predefined multi-repo scenarios.
- `snapshots.go`: snapshot/restore utilities for expensive test setup reuse.
- `fixtures_test.go`: external package tests (`testutil_test`) that validate public API usage.
- `scenarios_test.go`: package-internal tests for scenario/snapshot behavior.

## Design principles

- Prefer real git commands over mocks for behavior confidence.
- Keep helper APIs composable and minimal.
- Fail fast in setup helpers (`t.Fatalf`) so fixture errors are obvious.
- Keep tests isolated using `t.TempDir()`.

## Testing and quality checks

Run before opening a PR:

```bash
gofmt -w *.go
go vet ./...
go test ./...
```

Optional:

```bash
go test -short ./...
```

CI mirrors the required checks (`go vet` and `go test`) on pull requests.

## Usage guidance for library consumers

- Prefer scenario builders for integration-style tests with remotes/worktrees.
- Prefer base fixtures for small unit/integration tests that only need one repo.
- Keep assertions close to setup; helper failures already include command output.
- Use snapshots for expensive setups that are reused across subtests.

Common usage split:

- `CreateTestRepo`: fast setup for single-repo state.
- `NewScenario`: multi-repo topologies and fluent setup.
- `SnapshotRepo`/`RestoreSnapshot`: performance optimization for repeated expensive setups.

## Adding new helpers

When adding new exported helpers:

- Add or update tests in `fixtures_test.go` and/or `scenarios_test.go`.
- Add usage notes/examples to `README.md` when the helper is user-facing.
- Keep function names explicit and test-focused (avoid generic utility names).

## Snapshot safety expectations

- Snapshot restore must never write outside the restore root.
- Avoid introducing behavior that can restore unsupported file types silently.
- Keep snapshot behavior deterministic for repeatable tests.

## Pull request guidance

- Keep PRs focused and small when possible.
- Include a clear "why" in the commit/PR description.
- Include a short test plan with commands you ran locally.
- Prefer additive, backward-compatible changes for existing exported helpers.

Suggested PR checklist:

- [ ] public API changes documented in `README.md`
- [ ] tests added/updated for behavior changes
- [ ] `gofmt`, `go vet`, and `go test` all pass locally
- [ ] no unrelated refactors mixed into functional changes

## Maintainer/admin workflow

### Branch and PR flow

1. Create a feature branch from `main`.
2. Commit focused changes with clear commit messages.
3. Open a PR with summary and test plan.
4. Merge only after CI is green.

### Release flow

1. Verify `main` has passed CI.
2. Confirm `README.md` and this guide reflect current API/behavior.
3. Pull release notes from merged PRs (user-visible changes first).
4. Create and push a version tag.
5. Publish a GitHub release with:
- notable changes
- compatibility notes (for example, minimum Go version)
- migration notes when behavior changed

## Release notes checklist

Before cutting a release:

- Ensure CI is green on `main`.
- Summarize user-visible changes (new helpers, behavior changes, compatibility notes).
- Call out any minimum Go version changes.
- Verify examples in `README.md` still compile conceptually with current API.
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,57 @@

`git-testkit` provides helpers for writing Go tests that exercise real Git repositories.

## Why use this

- Exercise real git behavior instead of mocking command output.
- Build common repo states quickly (dirty trees, detached HEAD, diverged remotes, worktrees).
- Reuse expensive setups across tests with in-memory snapshots.

## Install

```bash
go get github.com/git-fire/git-testkit
```

## Requirements

- `git` must be installed and available on `PATH`
- Go 1.22+

## What it includes

- Repository fixtures (`CreateTestRepo`, `CreateBareRemote`, `RunGitCmd`)
- Scenario builders for common multi-repo states (`NewScenario`, conflict/worktree helpers)
- Snapshot helpers for capturing and restoring repository state in tests

## API overview

- `CreateTestRepo(t, RepoOptions)` creates a real repository with optional files/remotes/branches.
- `CreateBareRemote(t, name)` creates a bare repository for remote testing.
- `NewScenario(t)` returns a fluent builder for multi-repo test topologies.
- `SnapshotRepo(t, path)` and `RestoreSnapshot(t, snap)` speed up repeated fixture setup.
- `IsDirty`, `GetCurrentSHA`, `GetBranches`, `GetRemotes` provide common assertions/helpers.

## Quickstart for using in tests

1. Create fixture repos with `CreateTestRepo` or `NewScenario`.
2. Apply setup operations with fluent helpers (`AddFile`, `Commit`, `WithRemote`, `Push`).
3. Run your code under test against the real repository paths.
4. Assert with helper methods (`IsDirty`, `GetCurrentSHA`, `GetBranches`).

Minimal test flow:

```go
func TestMyGitBehavior(t *testing.T) {
repoPath := testutil.CreateTestRepo(t, testutil.RepoOptions{Name: "subject"})
testutil.RunGitCmd(t, repoPath, "checkout", "-b", "feature")
// call your package functions here
if testutil.IsDirty(t, repoPath) {
t.Fatal("repo should be clean")
}
}
```

## Example

```go
Expand All @@ -34,3 +73,50 @@ func TestWithRepo(t *testing.T) {
}
}
```

## Cleanup behavior

- All helper-created repositories use `t.TempDir()`.
- Repos/worktrees are automatically removed by Go's test framework at test completion.
- As with any temp directories, force-killed test processes may leave files behind.

## Common patterns

### Build a conflict scenario

```go
func TestConflictFlow(t *testing.T) {
_, local, _ := testutil.CreateConflictScenario(t)

// Exercise your logic against a real diverged local clone.
testutil.RunGitCmd(t, local.Path(), "status")
}
```

### Snapshot expensive setup

```go
func TestUsingSnapshot(t *testing.T) {
_, repo := testutil.CreateLargeRepoScenario(t, 20, 10)

snap := testutil.SnapshotRepo(t, repo.Path())
clonePath := testutil.RestoreSnapshot(t, snap)

// Use clonePath in assertions without rebuilding the fixture each time.
testutil.RunGitCmd(t, clonePath, "status")
}
```

## Notes

- Snapshots are intended for deterministic test fixtures and only restore regular files/directories.
- Helpers fail tests immediately (`t.Fatalf`) when git commands fail, so errors surface close to setup code.
- When tests build large repo graphs repeatedly, prefer snapshot/restore to reduce total runtime.

## Developer docs

- See `DEVELOPER_GUIDE.md` for:
- testing and quality gates
- usage guidance for library consumers
- administration/maintenance and release workflow
- contribution process and PR expectations
79 changes: 22 additions & 57 deletions fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

Expand Down Expand Up @@ -193,69 +194,39 @@ func GetRemotes(t *testing.T, repoPath string) map[string]string {
// "origin /path/to/remote (push)"
remotes := make(map[string]string)

lines := string(output)
lines := strings.TrimSpace(string(output))
if lines == "" {
return remotes
}

// Simple parsing - just extract remote names
// Full parsing not needed for tests
for _, line := range splitLines(lines) {
for _, line := range strings.Split(lines, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Just check if "origin" appears in the line
// Good enough for test validation
if len(line) > 0 {
// Extract first word (remote name)
parts := splitWhitespace(line)
if len(parts) >= 2 {
name := parts[0]
url := parts[1]
remotes[name] = url
name, remainder, ok := strings.Cut(line, "\t")
if !ok {
// Fallback for unusual formatting that does not use tabs.
idx := strings.IndexAny(line, " \t")
if idx == -1 {
continue
}
name = strings.TrimSpace(line[:idx])
remainder = strings.TrimSpace(line[idx+1:])
} else {
name = strings.TrimSpace(name)
remainder = strings.TrimSpace(remainder)
}
}

return remotes
}
remainder = strings.TrimSuffix(remainder, " (fetch)")
remainder = strings.TrimSuffix(remainder, " (push)")

// Helper: split by newlines
func splitLines(s string) []string {
var lines []string
current := ""
for _, ch := range s {
if ch == '\n' {
lines = append(lines, current)
current = ""
} else {
current += string(ch)
if name != "" && remainder != "" {
remotes[name] = remainder
}
}
if current != "" {
lines = append(lines, current)
}
return lines
}

// Helper: split by whitespace/tabs
func splitWhitespace(s string) []string {
var parts []string
current := ""
for _, ch := range s {
if ch == ' ' || ch == '\t' {
if current != "" {
parts = append(parts, current)
current = ""
}
} else {
current += string(ch)
}
}
if current != "" {
parts = append(parts, current)
}
return parts
return remotes
}

// RunGitCmd runs a git command and fails the test if it errors
Expand All @@ -277,13 +248,7 @@ func GetCurrentSHA(t *testing.T, repoPath string) string {
t.Fatalf("Failed to get current SHA: %v", err)
}

sha := string(output)
// Trim newline
if len(sha) > 0 && sha[len(sha)-1] == '\n' {
sha = sha[:len(sha)-1]
}

return sha
return strings.TrimSpace(string(output))
}

// GetBranches returns all branches in the repo
Expand All @@ -298,7 +263,7 @@ func GetBranches(t *testing.T, repoPath string) []string {
t.Fatalf("Failed to get branches: %v", err)
}

branches := splitLines(string(output))
branches := strings.Split(strings.TrimSpace(string(output)), "\n")

// Filter out empty lines
var result []string
Expand Down
Loading
Loading