Commit a108f3a
pkg/git/git.go
@@ -74,14 +74,16 @@ func Tags(repoDir string) ([]Tag, error) {
if err != nil {
return nil, fmt.Errorf("failed to parse tag creation date: %w", err)
}
- if commitHash == "" {
- commitHash = objectName // tag is lightweight
- }
- tags = append(tags, Tag{
+ t := Tag{
Name: name,
Date: time.Unix(int64(timestampInt), 0),
+ ObjectHash: objectName,
CommitHash: commitHash,
- })
+ }
+ if t.CommitHash == "" {
+ t.CommitHash = objectName // tag is lightweight
+ }
+ tags = append(tags, t)
}
return tags, nil
pkg/git/types.go
@@ -69,5 +69,6 @@ type RefName struct {
type Tag struct {
Name string
Date time.Time
- CommitHash string
+ ObjectHash string // The hash the tag ref directly points to (tag object for annotated, commit for lightweight)
+ CommitHash string // The peeled commit hash
}
dumb.go
@@ -0,0 +1,173 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/antonmedv/gitmal/pkg/git"
+)
+
+func generateDumbProtocol(params Params, branches []git.Ref, defaultBranch string, tags []git.Tag, commitsFor map[git.Ref][]git.Commit, allCommits map[string]git.Commit) error {
+ out := params.OutputDir
+
+ dirs := []string{
+ filepath.Join(out, "info"),
+ filepath.Join(out, "objects", "info"),
+ filepath.Join(out, "objects", "pack"),
+ filepath.Join(out, "refs", "heads"),
+ filepath.Join(out, "refs", "tags"),
+ }
+ for _, d := range dirs {
+ if err := os.MkdirAll(d, 0o755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", d, err)
+ }
+ }
+
+ // HEAD
+ head := fmt.Sprintf("ref: refs/heads/%s\n", defaultBranch)
+ if err := os.WriteFile(filepath.Join(out, "HEAD"), []byte(head), 0o644); err != nil {
+ return fmt.Errorf("write HEAD: %w", err)
+ }
+
+ // info/refs
+ var refsBuf strings.Builder
+ for _, branch := range branches {
+ commits, ok := commitsFor[branch]
+ if !ok {
+ continue
+ }
+ if len(commits) > 0 {
+ h := commits[0].Hash
+ refsBuf.WriteString(fmt.Sprintf("%s\trefs/heads/%s\n", h, branch.String()))
+ if branch.String() == defaultBranch {
+ refsBuf.WriteString(fmt.Sprintf("%s\tHEAD\n", h))
+ }
+ }
+ }
+ for _, t := range tags {
+ refsBuf.WriteString(fmt.Sprintf("%s\trefs/tags/%s\n", t.ObjectHash, t.Name))
+ if t.ObjectHash != t.CommitHash {
+ refsBuf.WriteString(fmt.Sprintf("%s\trefs/tags/%s^{}\n", t.CommitHash, t.Name))
+ }
+ }
+ if err := os.WriteFile(filepath.Join(out, "info", "refs"), []byte(refsBuf.String()), 0o644); err != nil {
+ return fmt.Errorf("write info/refs: %w", err)
+ }
+
+ // refs/heads/<branch>
+ for _, branch := range branches {
+ commits, ok := commitsFor[branch]
+ if !ok || len(commits) == 0 {
+ continue
+ }
+ p := filepath.Join(out, "refs", "heads", branch.String())
+ if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", filepath.Dir(p), err)
+ }
+ if err := os.WriteFile(p, []byte(commits[0].Hash+"\n"), 0o644); err != nil {
+ return fmt.Errorf("write %s: %w", p, err)
+ }
+ }
+
+ // refs/tags/<tag>
+ for _, t := range tags {
+ p := filepath.Join(out, "refs", "tags", t.Name)
+ if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", filepath.Dir(p), err)
+ }
+ if err := os.WriteFile(p, []byte(t.ObjectHash+"\n"), 0o644); err != nil {
+ return fmt.Errorf("write %s: %w", p, err)
+ }
+ }
+
+ // Copy objects
+ srcObjects, err := findObjectsDir(params.RepoDir)
+ if err != nil {
+ return fmt.Errorf("find objects dir: %w", err)
+ }
+ dstObjects := filepath.Join(out, "objects")
+ if err := hardlinkDir(srcObjects, dstObjects); err != nil {
+ return fmt.Errorf("copy objects: %w", err)
+ }
+
+ // objects/info/packs
+ packsDir := filepath.Join(out, "objects", "pack")
+ entries, err := os.ReadDir(packsDir)
+ if os.IsNotExist(err) {
+ // no pack files — nothing to advertise
+ } else if err != nil {
+ return fmt.Errorf("read packs dir: %w", err)
+ } else {
+ var packsBuf strings.Builder
+ for _, e := range entries {
+ if !e.IsDir() && strings.HasSuffix(e.Name(), ".pack") {
+ packsBuf.WriteString("P " + e.Name() + "\n")
+ }
+ }
+ if packsBuf.Len() > 0 {
+ p := filepath.Join(out, "objects", "info", "packs")
+ if err := os.WriteFile(p, []byte(packsBuf.String()), 0o644); err != nil {
+ return fmt.Errorf("write objects/info/packs: %w", err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func findObjectsDir(repoDir string) (string, error) {
+ cmd := exec.Command("git", "rev-parse", "--git-dir")
+ cmd.Dir = repoDir
+ out, err := cmd.Output()
+ if err != nil {
+ return "", fmt.Errorf("rev-parse --git-dir: %w", err)
+ }
+ gitDir := strings.TrimSpace(string(out))
+ if !filepath.IsAbs(gitDir) {
+ gitDir = filepath.Join(repoDir, gitDir)
+ }
+ objectsDir := filepath.Join(gitDir, "objects")
+ if fi, err := os.Stat(objectsDir); err != nil || !fi.IsDir() {
+ return "", fmt.Errorf("objects dir not found at %s", objectsDir)
+ }
+ return objectsDir, nil
+}
+
+func hardlinkDir(src, dst string) error {
+ return filepath.Walk(src, func(path string, fi os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ rel, err := filepath.Rel(src, path)
+ if err != nil {
+ return err
+ }
+ target := filepath.Join(dst, rel)
+ if fi.IsDir() {
+ return os.MkdirAll(target, fi.Mode())
+ }
+ if err := os.Link(path, target); err != nil {
+ return copyFile(path, target, fi.Mode())
+ }
+ return nil
+ })
+}
+
+func copyFile(src, dst string, mode os.FileMode) error {
+ in, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer in.Close()
+ out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ _, err = io.Copy(out, in)
+ return err
+}
main.go
@@ -23,6 +23,7 @@ var (
flagPreviewThemes bool
flagMinify bool
flagGzip bool
+ flagGit bool
)
type Params struct {
@@ -68,6 +69,7 @@ func main() {
flag.BoolVar(&flagPreviewThemes, "preview-themes", false, "Preview available themes")
flag.BoolVar(&flagMinify, "minify", false, "Minify all generated HTML files")
flag.BoolVar(&flagGzip, "gzip", false, "Compress all generated HTML files")
+ flag.BoolVar(&flagGit, "git", false, "Generate static files for Git dumb HTTP protocol")
flag.Usage = usage
flag.Parse()
@@ -244,6 +246,14 @@ func main() {
panic(err)
}
+ // Git dumb protocol files generation
+ if flagGit {
+ echo("> generating dumb protocol files...")
+ if err := generateDumbProtocol(params, branches, flagDefaultBranch, tags, commitsFor, commits); err != nil {
+ panic(err)
+ }
+ }
+
// Index page generation
if !noFiles {
if len(defaultBranchFiles) == 0 {