1
0
Fork 0
mirror of https://github.com/imjasonh/infinite-git synced 2026-07-07 00:32:51 +00:00

Merge pull request #2 from imjasonh/claude/add-infinite-go-package-dYfNY

Add cmd/infinite-go: serve Go package with PullTime via git
This commit is contained in:
Jason Hall 2026-04-03 13:18:28 +01:00 committed by GitHub
commit 4ff9f1cae0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 438 additions and 106 deletions

View file

@ -2,12 +2,14 @@ package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"time"
_ "github.com/chainguard-dev/clog/gcp/init"
"github.com/imjasonh/infinite-git/internal/generator"
"github.com/imjasonh/infinite-git/internal/repo"
"github.com/imjasonh/infinite-git/internal/server"
"github.com/sethvargo/go-envconfig"
@ -18,21 +20,39 @@ var env = envconfig.MustProcess(context.Background(), &struct {
RepoPath string `env:"REPO_PATH,default=./infinite-repo"`
}{})
func main() {
// clog/gcp/init automatically sets up the logger
// gitContent provides the default infinite-git file content.
type gitContent struct{}
// Initialize repository
func (g *gitContent) InitialFiles() map[string][]byte {
return map[string][]byte{
"README.md": []byte("# Infinite Git Repository\n\nThis repository generates a new commit every time you pull.\n"),
"hello.txt": []byte("Pull #0\nTimestamp: Initial commit\n"),
}
}
func (g *gitContent) GenerateFiles(count int64, now time.Time) map[string][]byte {
return map[string][]byte{
"hello.txt": []byte(fmt.Sprintf("Pull #%d\nTimestamp: %s\n", count, now.Format("2006-01-02 15:04:05.999999999"))),
}
}
func (g *gitContent) CommitMessage(count int64, now time.Time) string {
return fmt.Sprintf("Pull #%d at %s", count, now.Format("2006-01-02 15:04:05"))
}
var _ generator.ContentProvider = (*gitContent)(nil)
func main() {
slog.Info("initializing repository", "env", env)
gitRepo, err := repo.New(env.RepoPath)
content := &gitContent{}
gitRepo, err := repo.New(env.RepoPath, content.InitialFiles())
if err != nil {
slog.Error("failed to initialize repository", "error", err)
os.Exit(1)
}
// Create server
srv := server.New(gitRepo)
srv := server.New(gitRepo, content)
// Set up HTTP server
httpServer := &http.Server{
Addr: ":" + env.Port,
Handler: srv.Handler(),

View file

@ -17,22 +17,22 @@ import (
"github.com/imjasonh/infinite-git/internal/server"
)
func TestCloneAndPull(t *testing.T) {
// Create temporary directories
serverRepoDir := t.TempDir()
clientRepoDir := t.TempDir()
// Initialize server repository
serverRepo, err := repo.New(serverRepoDir)
func newTestServer(t *testing.T) *httptest.Server {
t.Helper()
content := &gitContent{}
serverRepo, err := repo.New(t.TempDir(), content.InitialFiles())
if err != nil {
t.Fatalf("failed to create server repo: %v", err)
}
srv := server.New(serverRepo)
// Start test server
srv := server.New(serverRepo, content)
ts := httptest.NewServer(srv.Handler())
defer ts.Close()
t.Cleanup(ts.Close)
return ts
}
func TestCloneAndPull(t *testing.T) {
ts := newTestServer(t)
clientRepoDir := t.TempDir()
// Clone the repository
gitRepo, err := git.PlainClone(clientRepoDir, false, &git.CloneOptions{
@ -115,20 +115,7 @@ func TestCloneAndPull(t *testing.T) {
}
func TestConcurrentPulls(t *testing.T) {
// Create temporary directories
serverRepoDir := t.TempDir()
// Initialize server repository
serverRepo, err := repo.New(serverRepoDir)
if err != nil {
t.Fatalf("failed to create server repo: %v", err)
}
srv := server.New(serverRepo)
// Start test server
ts := httptest.NewServer(srv.Handler())
defer ts.Close()
ts := newTestServer(t)
// Clone initial repository
baseClientDir := t.TempDir()
@ -199,22 +186,9 @@ func TestConcurrentPulls(t *testing.T) {
}
func TestPushRejection(t *testing.T) {
// Create temporary directories
serverRepoDir := t.TempDir()
ts := newTestServer(t)
clientRepoDir := t.TempDir()
// Initialize server repository
serverRepo, err := repo.New(serverRepoDir)
if err != nil {
t.Fatalf("failed to create server repo: %v", err)
}
srv := server.New(serverRepo)
// Start test server
ts := httptest.NewServer(srv.Handler())
defer ts.Close()
// Clone the repository
gitRepo, err := git.PlainClone(clientRepoDir, false, &git.CloneOptions{
URL: ts.URL,

122
cmd/infinite-go/main.go Normal file
View file

@ -0,0 +1,122 @@
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"path"
"strings"
"time"
_ "github.com/chainguard-dev/clog/gcp/init"
"github.com/imjasonh/infinite-git/internal/generator"
"github.com/imjasonh/infinite-git/internal/repo"
"github.com/imjasonh/infinite-git/internal/server"
"github.com/sethvargo/go-envconfig"
)
var env = envconfig.MustProcess(context.Background(), &struct {
Port string `env:"PORT,default=8080"`
RepoPath string `env:"REPO_PATH,default=./infinite-go-repo"`
ModulePath string `env:"MODULE_PATH,default=example.com/infinite-go"`
}{})
// goContent generates a Go package with PullTime set to the pull timestamp.
type goContent struct {
modulePath string
pkgName string
}
func newGoContent(modulePath string) *goContent {
// Derive package name from last path element, removing hyphens.
pkg := path.Base(modulePath)
pkg = strings.ReplaceAll(pkg, "-", "")
pkg = strings.ReplaceAll(pkg, ".", "")
return &goContent{
modulePath: modulePath,
pkgName: pkg,
}
}
func (g *goContent) InitialFiles() map[string][]byte {
return g.GenerateFiles(0, time.Now())
}
func (g *goContent) GenerateFiles(count int64, now time.Time) map[string][]byte {
now = now.UTC()
goMod := fmt.Sprintf("module %s\n\ngo 1.24\n", g.modulePath)
goFile := fmt.Sprintf(`package %s
import "time"
// PullTime is the time this module version was generated.
var PullTime = time.Date(%d, time.%s, %d, %d, %d, %d, %d, time.UTC)
`,
g.pkgName,
now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(), now.Second(), now.Nanosecond(),
)
return map[string][]byte{
"go.mod": []byte(goMod),
"pulltime.go": []byte(goFile),
}
}
func (g *goContent) CommitMessage(count int64, now time.Time) string {
return fmt.Sprintf("Pull #%d at %s", count, now.Format("2006-01-02 15:04:05"))
}
var _ generator.ContentProvider = (*goContent)(nil)
// goGetMiddleware intercepts ?go-get=1 requests for Go module discovery.
func goGetMiddleware(modulePath string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("go-get") == "1" {
scheme := "https"
if r.TLS == nil {
scheme = "http"
}
repoURL := fmt.Sprintf("%s://%s", scheme, r.Host)
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `<!DOCTYPE html>
<html><head>
<meta name="go-import" content="%s git %s">
</head></html>
`, modulePath, repoURL)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
slog.Info("initializing repository", "env", env)
content := newGoContent(env.ModulePath)
gitRepo, err := repo.New(env.RepoPath, content.InitialFiles())
if err != nil {
slog.Error("failed to initialize repository", "error", err)
os.Exit(1)
}
srv := server.New(gitRepo, content)
handler := goGetMiddleware(env.ModulePath, srv.Handler())
httpServer := &http.Server{
Addr: ":" + env.Port,
Handler: handler,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
slog.Info("starting HTTP server", "port", env.Port, "module", env.ModulePath)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("HTTP server error", "error", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,217 @@
package main
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/imjasonh/infinite-git/internal/repo"
"github.com/imjasonh/infinite-git/internal/server"
)
func newGoTestServer(t *testing.T, modulePath string) *httptest.Server {
t.Helper()
content := newGoContent(modulePath)
serverRepo, err := repo.New(t.TempDir(), content.InitialFiles())
if err != nil {
t.Fatalf("failed to create server repo: %v", err)
}
srv := server.New(serverRepo, content)
ts := httptest.NewServer(goGetMiddleware(modulePath, srv.Handler()))
t.Cleanup(ts.Close)
return ts
}
// newGoTestServerOnPort starts a test server bound to 127.0.0.1:port.
// It skips the test if the port cannot be bound (e.g. no permission for :80).
func newGoTestServerOnPort(t *testing.T, modulePath string, port int) *httptest.Server {
t.Helper()
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
t.Skipf("cannot bind to :%d: %v", port, err)
}
content := newGoContent(modulePath)
serverRepo, err := repo.New(t.TempDir(), content.InitialFiles())
if err != nil {
t.Fatalf("failed to create server repo: %v", err)
}
srv := server.New(serverRepo, content)
ts := httptest.NewUnstartedServer(goGetMiddleware(modulePath, srv.Handler()))
ts.Listener = ln
ts.Start()
t.Cleanup(ts.Close)
return ts
}
// TestGoGetE2E runs `go get 127.0.0.1@latest` against the server on :80.
// This is the full end-to-end test of the Go module workflow.
// Skipped when :80 cannot be bound (requires CAP_NET_BIND_SERVICE or root).
func TestGoGetE2E(t *testing.T) {
goBin, err := exec.LookPath("go")
if err != nil {
t.Skip("go binary not found in PATH")
}
modulePath := "127.0.0.1"
newGoTestServerOnPort(t, modulePath, 80)
var pullTimes []string
for i := 0; i < 3; i++ {
modCache := t.TempDir()
workDir := t.TempDir()
if err := os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module test\n\ngo 1.24\n"), 0644); err != nil {
t.Fatalf("iteration %d: failed to write go.mod: %v", i, err)
}
if err := os.WriteFile(filepath.Join(workDir, "tools.go"), []byte(fmt.Sprintf("package tools\n\nimport _ \"%s\"\n", modulePath)), 0644); err != nil {
t.Fatalf("iteration %d: failed to write tools.go: %v", i, err)
}
cmd := exec.Command(goBin, "get", modulePath+"@latest")
cmd.Dir = workDir
cmd.Env = append(os.Environ(),
"GOMODCACHE="+modCache,
"GOPROXY=direct",
"GONOSUMCHECK=*",
"GONOSUMDB=*",
"GOINSECURE="+modulePath,
)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("go get #%d failed: %v\noutput: %s", i+1, err, out)
}
t.Logf("go get #%d succeeded", i+1)
// Find pulltime.go in the module cache.
var pulltimeFile string
filepath.Walk(modCache, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.Name() == "pulltime.go" {
pulltimeFile = path
return filepath.SkipAll
}
return nil
})
if pulltimeFile == "" {
t.Fatalf("go get #%d: pulltime.go not found in module cache", i+1)
}
data, err := os.ReadFile(pulltimeFile)
if err != nil {
t.Fatalf("go get #%d: failed to read pulltime.go: %v", i+1, err)
}
for _, line := range strings.Split(string(data), "\n") {
if strings.Contains(line, "var PullTime") {
pullTimes = append(pullTimes, strings.TrimSpace(line))
t.Logf("go get #%d: %s", i+1, strings.TrimSpace(line))
break
}
}
}
if len(pullTimes) != 3 {
t.Fatalf("expected 3 PullTime values, got %d", len(pullTimes))
}
seen := make(map[string]bool)
for i, pt := range pullTimes {
if seen[pt] {
t.Errorf("go get #%d returned duplicate PullTime: %s", i+1, pt)
}
seen[pt] = true
}
}
// TestGoGetPull clones and then pulls repeatedly using the git CLI,
// verifying each pull produces a buildable Go package with a unique PullTime.
// This always runs (no privileged port needed) as a fallback for environments
// where TestGoGetE2E is skipped.
func TestGoGetPull(t *testing.T) {
goBin, err := exec.LookPath("go")
if err != nil {
t.Skip("go binary not found in PATH")
}
gitBin, err := exec.LookPath("git")
if err != nil {
t.Skip("git binary not found in PATH")
}
modulePath := "example.com/infinite-go"
ts := newGoTestServer(t, modulePath)
cloneDir := t.TempDir()
// Initial clone.
cmd := exec.Command(gitBin, "clone", ts.URL, cloneDir)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git clone failed: %v\noutput: %s", err, out)
}
readPullTime := func() string {
data, err := os.ReadFile(filepath.Join(cloneDir, "pulltime.go"))
if err != nil {
t.Fatalf("failed to read pulltime.go: %v", err)
}
for _, line := range strings.Split(string(data), "\n") {
if strings.Contains(line, "var PullTime") {
return strings.TrimSpace(line)
}
}
t.Fatal("pulltime.go does not contain PullTime declaration")
return ""
}
prev := readPullTime()
t.Logf("clone: %s", prev)
for i := 0; i < 3; i++ {
cmd := exec.Command(gitBin, "-C", cloneDir, "pull")
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git pull #%d failed: %v\noutput: %s", i+1, err, out)
}
// Verify it still builds.
cmd = exec.Command(goBin, "build", "./...")
cmd.Dir = cloneDir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("pull #%d: go build failed: %v\noutput: %s", i+1, err, out)
}
cur := readPullTime()
t.Logf("pull #%d: %s", i+1, cur)
if cur == prev {
t.Errorf("pull #%d did not change PullTime", i+1)
}
prev = cur
}
}
func TestGoGetDiscovery(t *testing.T) {
modulePath := "example.com/infinite-go"
ts := newGoTestServer(t, modulePath)
resp, err := http.Get(ts.URL + "/?go-get=1")
if err != nil {
t.Fatalf("failed to fetch go-get: %v", err)
}
defer resp.Body.Close()
buf := make([]byte, 4096)
n, _ := resp.Body.Read(buf)
body := string(buf[:n])
expected := fmt.Sprintf(`<meta name="go-import" content="%s git`, modulePath)
if !strings.Contains(body, expected) {
t.Errorf("go-get response missing expected meta tag.\nwant substring: %s\ngot: %s", expected, body)
}
}

View file

@ -11,14 +11,16 @@ import (
// Generator creates new commits on demand.
type Generator struct {
repo *repo.Repository
counter int64
repo *repo.Repository
counter int64
provider ContentProvider
}
// New creates a new commit generator.
func New(r *repo.Repository) *Generator {
func New(r *repo.Repository, provider ContentProvider) *Generator {
return &Generator{
repo: r,
repo: r,
provider: provider,
}
}
@ -71,29 +73,29 @@ func (g *Generator) GenerateCommit() (string, error) {
// Parse existing tree entries
existingEntries := parseTree(parentTreeData)
// Create updated hello.txt content with nanosecond precision
filename := "hello.txt"
content := fmt.Sprintf("Pull #%d\nTimestamp: %s\n", count, time.Now().Format("2006-01-02 15:04:05.999999999"))
// Generate files from content provider
now := time.Now()
generatedFiles := g.provider.GenerateFiles(count, now)
// Create blob for updated file
blob := object.NewBlob([]byte(content))
blobHash, err := g.repo.WriteObject(blob)
if err != nil {
return "", fmt.Errorf("writing blob: %w", err)
}
// Create new tree with all existing entries, replacing hello.txt
// Create new tree with existing entries, replacing any generated files
tree := object.NewTree()
// Add existing entries, but skip hello.txt as we'll add the updated one
// Add existing entries, skipping any that will be replaced
for _, entry := range existingEntries {
if entry.Name != filename {
if _, replaced := generatedFiles[entry.Name]; !replaced {
tree.AddEntry(entry.Mode, entry.Name, entry.Hash)
}
}
// Add updated hello.txt
tree.AddEntry("100644", filename, blobHash)
// Add generated files
for name, content := range generatedFiles {
blob := object.NewBlob(content)
blobHash, err := g.repo.WriteObject(blob)
if err != nil {
return "", fmt.Errorf("writing blob for %s: %w", name, err)
}
tree.AddEntry("100644", name, blobHash)
}
treeHash, err := g.repo.WriteObject(tree)
if err != nil {
@ -101,7 +103,7 @@ func (g *Generator) GenerateCommit() (string, error) {
}
// Create commit
commitMsg := fmt.Sprintf("Pull #%d at %s", count, time.Now().Format("2006-01-02 15:04:05"))
commitMsg := g.provider.CommitMessage(count, now)
commit := object.NewCommit(
treeHash,
parentHash,

View file

@ -0,0 +1,14 @@
package generator
import "time"
// ContentProvider defines how to generate files for each pull.
type ContentProvider interface {
// InitialFiles returns the files for the initial commit.
InitialFiles() map[string][]byte
// GenerateFiles returns files to create/update on each pull.
// Existing files not in this map are preserved.
GenerateFiles(count int64, now time.Time) map[string][]byte
// CommitMessage returns the commit message for a pull.
CommitMessage(count int64, now time.Time) string
}

View file

@ -20,7 +20,8 @@ type Repository struct {
}
// New creates or opens a Git repository at the given path.
func New(path string) (*Repository, error) {
// initialFiles specifies the files to include in the initial commit.
func New(path string, initialFiles map[string][]byte) (*Repository, error) {
repo := &Repository{
path: path,
gitDir: filepath.Join(path, ".git"),
@ -39,7 +40,7 @@ func New(path string) (*Repository, error) {
}
// Create initial commit
if err := repo.createInitialCommit(); err != nil {
if err := repo.createInitialCommit(initialFiles); err != nil {
return nil, fmt.Errorf("creating initial commit: %w", err)
}
}
@ -86,35 +87,29 @@ func (r *Repository) init() error {
}
// createInitialCommit creates the first commit in the repository.
func (r *Repository) createInitialCommit() error {
// Create README content
readmeContent := []byte("# Infinite Git Repository\n\nThis repository generates a new commit every time you pull.\n")
// Create blob for README
readmeBlob := object.NewBlob(readmeContent)
readmeBlobHash, err := object.Write(r.gitDir, readmeBlob)
if err != nil {
return fmt.Errorf("writing README blob: %w", err)
}
// Create initial hello.txt content
helloContent := []byte("Pull #0\nTimestamp: Initial commit\n")
helloBlob := object.NewBlob(helloContent)
helloBlobHash, err := object.Write(r.gitDir, helloBlob)
if err != nil {
return fmt.Errorf("writing hello.txt blob: %w", err)
}
// Create tree with README and hello.txt
func (r *Repository) createInitialCommit(files map[string][]byte) error {
tree := object.NewTree()
tree.AddEntry("100644", "README.md", readmeBlobHash)
tree.AddEntry("100644", "hello.txt", helloBlobHash)
for name, content := range files {
blob := object.NewBlob(content)
blobHash, err := object.Write(r.gitDir, blob)
if err != nil {
return fmt.Errorf("writing blob for %s: %w", name, err)
}
tree.AddEntry("100644", name, blobHash)
// Also write to working directory
filePath := filepath.Join(r.path, name)
if err := os.WriteFile(filePath, content, 0644); err != nil {
return fmt.Errorf("writing %s to working directory: %w", name, err)
}
}
treeHash, err := object.Write(r.gitDir, tree)
if err != nil {
return fmt.Errorf("writing tree: %w", err)
}
// Create commit
commit := object.NewCommit(
treeHash,
"", // No parent for initial commit
@ -127,23 +122,11 @@ func (r *Repository) createInitialCommit() error {
return fmt.Errorf("writing commit: %w", err)
}
// Update refs/heads/main
refPath := filepath.Join(r.gitDir, "refs", "heads", "main")
if err := os.WriteFile(refPath, []byte(commitHash+"\n"), 0644); err != nil {
return fmt.Errorf("updating ref: %w", err)
}
// Also write README and hello.txt to working directory
readmePath := filepath.Join(r.path, "README.md")
if err := os.WriteFile(readmePath, readmeContent, 0644); err != nil {
return fmt.Errorf("writing README to working directory: %w", err)
}
helloPath := filepath.Join(r.path, "hello.txt")
if err := os.WriteFile(helloPath, helloContent, 0644); err != nil {
return fmt.Errorf("writing hello.txt to working directory: %w", err)
}
return nil
}

View file

@ -17,10 +17,10 @@ type Server struct {
}
// New creates a new Git HTTP server.
func New(r *repo.Repository) *Server {
func New(r *repo.Repository, provider generator.ContentProvider) *Server {
return &Server{
repo: r,
generator: generator.New(r),
generator: generator.New(r, provider),
}
}