mirror of
https://github.com/imjasonh/infinite-git
synced 2026-07-18 14:55:01 +00:00
Add cmd/infinite-go: serve Go package with PullTime via git
Refactor infinite-git to support pluggable content generation: - Extract ContentProvider interface (InitialFiles, GenerateFiles, CommitMessage) - Make repo.New accept initial files map instead of hardcoding content - Move infinite-git to cmd/infinite-git with gitContent provider - Add cmd/infinite-go that serves a Go module where each pull generates a new version with PullTime set to the pull timestamp - Include ?go-get=1 discovery middleware for Go module resolution https://claude.ai/code/session_01HpvELk7HnbJoXEUjqHP7SU
This commit is contained in:
parent
16db43a3af
commit
96dd2d9604
8 changed files with 325 additions and 106 deletions
|
|
@ -2,12 +2,14 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/chainguard-dev/clog/gcp/init"
|
_ "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/repo"
|
||||||
"github.com/imjasonh/infinite-git/internal/server"
|
"github.com/imjasonh/infinite-git/internal/server"
|
||||||
"github.com/sethvargo/go-envconfig"
|
"github.com/sethvargo/go-envconfig"
|
||||||
|
|
@ -18,21 +20,39 @@ var env = envconfig.MustProcess(context.Background(), &struct {
|
||||||
RepoPath string `env:"REPO_PATH,default=./infinite-repo"`
|
RepoPath string `env:"REPO_PATH,default=./infinite-repo"`
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
func main() {
|
// gitContent provides the default infinite-git file content.
|
||||||
// clog/gcp/init automatically sets up the logger
|
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)
|
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 {
|
if err != nil {
|
||||||
slog.Error("failed to initialize repository", "error", err)
|
slog.Error("failed to initialize repository", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create server
|
srv := server.New(gitRepo, content)
|
||||||
srv := server.New(gitRepo)
|
|
||||||
|
|
||||||
// Set up HTTP server
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: ":" + env.Port,
|
Addr: ":" + env.Port,
|
||||||
Handler: srv.Handler(),
|
Handler: srv.Handler(),
|
||||||
|
|
@ -17,22 +17,22 @@ import (
|
||||||
"github.com/imjasonh/infinite-git/internal/server"
|
"github.com/imjasonh/infinite-git/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCloneAndPull(t *testing.T) {
|
func newTestServer(t *testing.T) *httptest.Server {
|
||||||
// Create temporary directories
|
t.Helper()
|
||||||
serverRepoDir := t.TempDir()
|
content := &gitContent{}
|
||||||
clientRepoDir := t.TempDir()
|
serverRepo, err := repo.New(t.TempDir(), content.InitialFiles())
|
||||||
|
|
||||||
// Initialize server repository
|
|
||||||
serverRepo, err := repo.New(serverRepoDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create server repo: %v", err)
|
t.Fatalf("failed to create server repo: %v", err)
|
||||||
}
|
}
|
||||||
|
srv := server.New(serverRepo, content)
|
||||||
srv := server.New(serverRepo)
|
|
||||||
|
|
||||||
// Start test server
|
|
||||||
ts := httptest.NewServer(srv.Handler())
|
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
|
// Clone the repository
|
||||||
gitRepo, err := git.PlainClone(clientRepoDir, false, &git.CloneOptions{
|
gitRepo, err := git.PlainClone(clientRepoDir, false, &git.CloneOptions{
|
||||||
|
|
@ -115,20 +115,7 @@ func TestCloneAndPull(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConcurrentPulls(t *testing.T) {
|
func TestConcurrentPulls(t *testing.T) {
|
||||||
// Create temporary directories
|
ts := newTestServer(t)
|
||||||
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()
|
|
||||||
|
|
||||||
// Clone initial repository
|
// Clone initial repository
|
||||||
baseClientDir := t.TempDir()
|
baseClientDir := t.TempDir()
|
||||||
|
|
@ -199,22 +186,9 @@ func TestConcurrentPulls(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPushRejection(t *testing.T) {
|
func TestPushRejection(t *testing.T) {
|
||||||
// Create temporary directories
|
ts := newTestServer(t)
|
||||||
serverRepoDir := t.TempDir()
|
|
||||||
clientRepoDir := t.TempDir()
|
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
|
// Clone the repository
|
||||||
gitRepo, err := git.PlainClone(clientRepoDir, false, &git.CloneOptions{
|
gitRepo, err := git.PlainClone(clientRepoDir, false, &git.CloneOptions{
|
||||||
URL: ts.URL,
|
URL: ts.URL,
|
||||||
122
cmd/infinite-go/main.go
Normal file
122
cmd/infinite-go/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
104
cmd/infinite-go/main_test.go
Normal file
104
cmd/infinite-go/main_test.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/imjasonh/infinite-git/internal/repo"
|
||||||
|
"github.com/imjasonh/infinite-git/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGoCloneAndPull(t *testing.T) {
|
||||||
|
modulePath := "example.com/infinite-go"
|
||||||
|
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()))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
// Clone the repository
|
||||||
|
clientDir := t.TempDir()
|
||||||
|
gitRepo, err := git.PlainClone(clientDir, false, &git.CloneOptions{
|
||||||
|
URL: ts.URL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to clone: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify go.mod exists with correct module path
|
||||||
|
goMod, err := os.ReadFile(filepath.Join(clientDir, "go.mod"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read go.mod: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(goMod), fmt.Sprintf("module %s", modulePath)) {
|
||||||
|
t.Errorf("go.mod does not contain expected module path, got: %s", goMod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify pulltime.go exists with PullTime var
|
||||||
|
goFile, err := os.ReadFile(filepath.Join(clientDir, "pulltime.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read pulltime.go: %v", err)
|
||||||
|
}
|
||||||
|
goFileStr := string(goFile)
|
||||||
|
if !strings.Contains(goFileStr, "package infinitego") {
|
||||||
|
t.Errorf("pulltime.go does not contain expected package name, got: %s", goFile)
|
||||||
|
}
|
||||||
|
if !strings.Contains(goFileStr, "var PullTime = time.Date(") {
|
||||||
|
t.Errorf("pulltime.go does not contain PullTime var, got: %s", goFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull again and verify PullTime changed
|
||||||
|
firstGoFile := goFileStr
|
||||||
|
w, err := gitRepo.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get worktree: %v", err)
|
||||||
|
}
|
||||||
|
err = w.Pull(&git.PullOptions{RemoteName: "origin"})
|
||||||
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
||||||
|
t.Fatalf("pull failed: %v", err)
|
||||||
|
}
|
||||||
|
goFile, err = os.ReadFile(filepath.Join(clientDir, "pulltime.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read pulltime.go after pull: %v", err)
|
||||||
|
}
|
||||||
|
if string(goFile) == firstGoFile {
|
||||||
|
t.Error("pulltime.go did not change after pull")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoGetDiscovery(t *testing.T) {
|
||||||
|
modulePath := "example.com/infinite-go"
|
||||||
|
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()))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
// Test ?go-get=1 returns go-import meta tag
|
||||||
|
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 does not contain expected meta tag.\nexpected to contain: %s\ngot: %s", expected, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,14 +11,16 @@ import (
|
||||||
|
|
||||||
// Generator creates new commits on demand.
|
// Generator creates new commits on demand.
|
||||||
type Generator struct {
|
type Generator struct {
|
||||||
repo *repo.Repository
|
repo *repo.Repository
|
||||||
counter int64
|
counter int64
|
||||||
|
provider ContentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new commit generator.
|
// New creates a new commit generator.
|
||||||
func New(r *repo.Repository) *Generator {
|
func New(r *repo.Repository, provider ContentProvider) *Generator {
|
||||||
return &Generator{
|
return &Generator{
|
||||||
repo: r,
|
repo: r,
|
||||||
|
provider: provider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,29 +73,29 @@ func (g *Generator) GenerateCommit() (string, error) {
|
||||||
// Parse existing tree entries
|
// Parse existing tree entries
|
||||||
existingEntries := parseTree(parentTreeData)
|
existingEntries := parseTree(parentTreeData)
|
||||||
|
|
||||||
// Create updated hello.txt content with nanosecond precision
|
// Generate files from content provider
|
||||||
filename := "hello.txt"
|
now := time.Now()
|
||||||
content := fmt.Sprintf("Pull #%d\nTimestamp: %s\n", count, time.Now().Format("2006-01-02 15:04:05.999999999"))
|
generatedFiles := g.provider.GenerateFiles(count, now)
|
||||||
|
|
||||||
// Create blob for updated file
|
// Create new tree with existing entries, replacing any generated files
|
||||||
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
|
|
||||||
tree := object.NewTree()
|
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 {
|
for _, entry := range existingEntries {
|
||||||
if entry.Name != filename {
|
if _, replaced := generatedFiles[entry.Name]; !replaced {
|
||||||
tree.AddEntry(entry.Mode, entry.Name, entry.Hash)
|
tree.AddEntry(entry.Mode, entry.Name, entry.Hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add updated hello.txt
|
// Add generated files
|
||||||
tree.AddEntry("100644", filename, blobHash)
|
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)
|
treeHash, err := g.repo.WriteObject(tree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -101,7 +103,7 @@ func (g *Generator) GenerateCommit() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create commit
|
// 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(
|
commit := object.NewCommit(
|
||||||
treeHash,
|
treeHash,
|
||||||
parentHash,
|
parentHash,
|
||||||
|
|
|
||||||
14
internal/generator/content.go
Normal file
14
internal/generator/content.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,8 @@ type Repository struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates or opens a Git repository at the given path.
|
// 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{
|
repo := &Repository{
|
||||||
path: path,
|
path: path,
|
||||||
gitDir: filepath.Join(path, ".git"),
|
gitDir: filepath.Join(path, ".git"),
|
||||||
|
|
@ -39,7 +40,7 @@ func New(path string) (*Repository, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create initial commit
|
// 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)
|
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.
|
// createInitialCommit creates the first commit in the repository.
|
||||||
func (r *Repository) createInitialCommit() error {
|
func (r *Repository) createInitialCommit(files map[string][]byte) 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
|
|
||||||
tree := object.NewTree()
|
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)
|
treeHash, err := object.Write(r.gitDir, tree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writing tree: %w", err)
|
return fmt.Errorf("writing tree: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create commit
|
|
||||||
commit := object.NewCommit(
|
commit := object.NewCommit(
|
||||||
treeHash,
|
treeHash,
|
||||||
"", // No parent for initial commit
|
"", // No parent for initial commit
|
||||||
|
|
@ -127,23 +122,11 @@ func (r *Repository) createInitialCommit() error {
|
||||||
return fmt.Errorf("writing commit: %w", err)
|
return fmt.Errorf("writing commit: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update refs/heads/main
|
|
||||||
refPath := filepath.Join(r.gitDir, "refs", "heads", "main")
|
refPath := filepath.Join(r.gitDir, "refs", "heads", "main")
|
||||||
if err := os.WriteFile(refPath, []byte(commitHash+"\n"), 0644); err != nil {
|
if err := os.WriteFile(refPath, []byte(commitHash+"\n"), 0644); err != nil {
|
||||||
return fmt.Errorf("updating ref: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ type Server struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Git HTTP server.
|
// New creates a new Git HTTP server.
|
||||||
func New(r *repo.Repository) *Server {
|
func New(r *repo.Repository, provider generator.ContentProvider) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
repo: r,
|
repo: r,
|
||||||
generator: generator.New(r),
|
generator: generator.New(r, provider),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue