commit 53b6727af430ed942a9e624b0425fe4d624122db Author: Catherine Date: Fri Sep 5 01:26:37 2025 +0000 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3af0ccb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/data diff --git a/src/fetch.go b/src/fetch.go new file mode 100644 index 0000000..6f852ce --- /dev/null +++ b/src/fetch.go @@ -0,0 +1,121 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/go-git/go-billy/v6/osfs" + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/storage/memory" +) + +func splitHash(hash plumbing.Hash) string { + head := hash.String() + return filepath.Join(head[:2], head[2:]) +} + +func fetch( + dataDir string, + webroot string, + url string, + branch plumbing.ReferenceName, +) (*plumbing.Hash, error) { + storer := memory.NewStorage() + + repo, err := git.Clone(storer, nil, &git.CloneOptions{ + URL: url, + ReferenceName: branch, + SingleBranch: true, + Depth: 1, + Tags: git.NoTags, + }) + if err != nil { + return nil, fmt.Errorf("git clone: %s", err) + } + + ref, err := repo.Head() + if err != nil { + return nil, fmt.Errorf("git head: %s", err) + } + head := ref.Hash() + + destDir := filepath.Join(dataDir, "tree", splitHash(head)) + if _, err := os.Stat(destDir); errors.Is(err, os.ErrNotExist) { + // check out to a temporary directory to avoid TOCTTOU race on destDir + tempDir, err := os.MkdirTemp(dataDir, ".tree") + if err != nil { + return nil, fmt.Errorf("mkdir temp: %s", err) + } + defer os.RemoveAll(tempDir) + + repo, err = git.Open(storer, osfs.New(tempDir)) + if err != nil { + return nil, fmt.Errorf("git open: %s", err) + } + + worktree, err := repo.Worktree() + if err != nil { + return nil, fmt.Errorf("git worktree: %s", err) + } + + if err := worktree.Checkout(&git.CheckoutOptions{ + Hash: head, + }); err != nil { + return nil, fmt.Errorf("git checkout: %s", err) + } + + if err := os.MkdirAll(filepath.Dir(destDir), 0o755); err != nil { + return nil, fmt.Errorf("mkdir parent dest: %s", err) + } + + // commit atomically; assume another fetch has won the race if directory exists + if err := os.Rename(tempDir, destDir); err != nil && !errors.Is(err, os.ErrExist) { + return nil, fmt.Errorf("rename dest: %s", err) + } + } + + webLink := filepath.Join(dataDir, "www", webroot) + destDirRel, _ := filepath.Rel(filepath.Dir(webLink), destDir) + + tempLink := filepath.Join(dataDir, + fmt.Sprintf(".link.%s.%s", strings.ReplaceAll(webroot, "/", ".."), head.String())) + if err := os.Symlink(destDirRel, tempLink); err != nil { + return nil, fmt.Errorf("symlink temp: %s", err) + } + defer os.Remove(tempLink) + + if err := os.MkdirAll(filepath.Dir(webLink), 0o755); err != nil { + return nil, fmt.Errorf("mkdir parent web: %s", err) + } + + // commit atomically; assume another fetch has won the race if symlink exists + // FIXME: might not have the same target + if err := os.Rename(tempLink, webLink); err != nil && !errors.Is(err, os.ErrExist) { + return nil, fmt.Errorf("rename web: %s", err) + } + + return &head, nil +} + +func Fetch( + dataDir string, + webroot string, + url string, + branch plumbing.ReferenceName, +) error { + log.Println("fetch:", webroot, url, branch) + + head, err := fetch(dataDir, webroot, url, branch) + if err != nil { + log.Println("fetch err:", fmt.Errorf("%s: %s", webroot, err)) + return err + } + + log.Println("fetch ok:", webroot, head) + return nil +} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..cdbc6bf --- /dev/null +++ b/src/go.mod @@ -0,0 +1,26 @@ +module whitequark.org/git-pages + +go 1.23.0 + +toolchain go1.24.4 + +require github.com/go-git/go-git/v6 v6.0.0-20250831162718-34f273445e00 + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg/v2 v2.0.2 // indirect + github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/pjbgf/sha1cd v0.4.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..a1762c0 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,47 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= +github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= +github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 h1:4KqVJTL5eanN8Sgg3BV6f2/QzfZEFbCd+rTak1fGRRA= +github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30/go.mod h1:snwvGrbywVFy2d6KJdQ132zapq4aLyzLMgpo79XdEfM= +github.com/go-git/go-git/v6 v6.0.0-20250831162718-34f273445e00 h1:eW0gxk9rk3jv7mf4r+sKNLXNgex2LMReedRCRJewQhw= +github.com/go-git/go-git/v6 v6.0.0-20250831162718-34f273445e00/go.mod h1:O7tkz+vcaOSOSRqAGC+MG6evNI8NsTmyH98ey4BTYwk= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= +github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..752b85a --- /dev/null +++ b/src/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "os" +) + +func main() { + dataDir := os.Args[1] + + Fetch(dataDir, "codeberg.page/index", "https://codeberg.org/Codeberg/pages-server/", "pages") +}