master
  1package main
  2
  3import (
  4	"fmt"
  5	"io"
  6	"os"
  7	"os/exec"
  8	"path/filepath"
  9	"strings"
 10
 11	"github.com/antonmedv/gitmal/pkg/git"
 12)
 13
 14func generateDumbProtocol(params Params, branches []git.Ref, defaultBranch string, tags []git.Tag, commitsFor map[git.Ref][]git.Commit, allCommits map[string]git.Commit) error {
 15	out := params.OutputDir
 16
 17	dirs := []string{
 18		filepath.Join(out, "info"),
 19		filepath.Join(out, "objects", "info"),
 20		filepath.Join(out, "objects", "pack"),
 21		filepath.Join(out, "refs", "heads"),
 22		filepath.Join(out, "refs", "tags"),
 23	}
 24	for _, d := range dirs {
 25		if err := os.MkdirAll(d, 0o755); err != nil {
 26			return fmt.Errorf("mkdir %s: %w", d, err)
 27		}
 28	}
 29
 30	// HEAD
 31	head := fmt.Sprintf("ref: refs/heads/%s\n", defaultBranch)
 32	if err := os.WriteFile(filepath.Join(out, "HEAD"), []byte(head), 0o644); err != nil {
 33		return fmt.Errorf("write HEAD: %w", err)
 34	}
 35
 36	// info/refs
 37	var refsBuf strings.Builder
 38	for _, branch := range branches {
 39		commits, ok := commitsFor[branch]
 40		if !ok {
 41			continue
 42		}
 43		if len(commits) > 0 {
 44			h := commits[0].Hash
 45			refsBuf.WriteString(fmt.Sprintf("%s\trefs/heads/%s\n", h, branch.String()))
 46			if branch.String() == defaultBranch {
 47				refsBuf.WriteString(fmt.Sprintf("%s\tHEAD\n", h))
 48			}
 49		}
 50	}
 51	for _, t := range tags {
 52		refsBuf.WriteString(fmt.Sprintf("%s\trefs/tags/%s\n", t.ObjectHash, t.Name))
 53		if t.ObjectHash != t.CommitHash {
 54			refsBuf.WriteString(fmt.Sprintf("%s\trefs/tags/%s^{}\n", t.CommitHash, t.Name))
 55		}
 56	}
 57	if err := os.WriteFile(filepath.Join(out, "info", "refs"), []byte(refsBuf.String()), 0o644); err != nil {
 58		return fmt.Errorf("write info/refs: %w", err)
 59	}
 60
 61	// refs/heads/<branch>
 62	for _, branch := range branches {
 63		commits, ok := commitsFor[branch]
 64		if !ok || len(commits) == 0 {
 65			continue
 66		}
 67		p := filepath.Join(out, "refs", "heads", branch.String())
 68		if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
 69			return fmt.Errorf("mkdir %s: %w", filepath.Dir(p), err)
 70		}
 71		if err := os.WriteFile(p, []byte(commits[0].Hash+"\n"), 0o644); err != nil {
 72			return fmt.Errorf("write %s: %w", p, err)
 73		}
 74	}
 75
 76	// refs/tags/<tag>
 77	for _, t := range tags {
 78		p := filepath.Join(out, "refs", "tags", t.Name)
 79		if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
 80			return fmt.Errorf("mkdir %s: %w", filepath.Dir(p), err)
 81		}
 82		if err := os.WriteFile(p, []byte(t.ObjectHash+"\n"), 0o644); err != nil {
 83			return fmt.Errorf("write %s: %w", p, err)
 84		}
 85	}
 86
 87	// Copy objects
 88	srcObjects, err := findObjectsDir(params.RepoDir)
 89	if err != nil {
 90		return fmt.Errorf("find objects dir: %w", err)
 91	}
 92	dstObjects := filepath.Join(out, "objects")
 93	if err := hardlinkDir(srcObjects, dstObjects); err != nil {
 94		return fmt.Errorf("copy objects: %w", err)
 95	}
 96
 97	// objects/info/packs
 98	packsDir := filepath.Join(out, "objects", "pack")
 99	entries, err := os.ReadDir(packsDir)
100	if os.IsNotExist(err) {
101		// no pack files — nothing to advertise
102	} else if err != nil {
103		return fmt.Errorf("read packs dir: %w", err)
104	} else {
105		var packsBuf strings.Builder
106		for _, e := range entries {
107			if !e.IsDir() && strings.HasSuffix(e.Name(), ".pack") {
108				packsBuf.WriteString("P " + e.Name() + "\n")
109			}
110		}
111		if packsBuf.Len() > 0 {
112			p := filepath.Join(out, "objects", "info", "packs")
113			if err := os.WriteFile(p, []byte(packsBuf.String()), 0o644); err != nil {
114				return fmt.Errorf("write objects/info/packs: %w", err)
115			}
116		}
117	}
118
119	return nil
120}
121
122func findObjectsDir(repoDir string) (string, error) {
123	cmd := exec.Command("git", "rev-parse", "--git-dir")
124	cmd.Dir = repoDir
125	out, err := cmd.Output()
126	if err != nil {
127		return "", fmt.Errorf("rev-parse --git-dir: %w", err)
128	}
129	gitDir := strings.TrimSpace(string(out))
130	if !filepath.IsAbs(gitDir) {
131		gitDir = filepath.Join(repoDir, gitDir)
132	}
133	objectsDir := filepath.Join(gitDir, "objects")
134	if fi, err := os.Stat(objectsDir); err != nil || !fi.IsDir() {
135		return "", fmt.Errorf("objects dir not found at %s", objectsDir)
136	}
137	return objectsDir, nil
138}
139
140func hardlinkDir(src, dst string) error {
141	return filepath.Walk(src, func(path string, fi os.FileInfo, err error) error {
142		if err != nil {
143			return err
144		}
145		rel, err := filepath.Rel(src, path)
146		if err != nil {
147			return err
148		}
149		target := filepath.Join(dst, rel)
150		if fi.IsDir() {
151			return os.MkdirAll(target, fi.Mode())
152		}
153		if err := os.Link(path, target); err != nil {
154			return copyFile(path, target, fi.Mode())
155		}
156		return nil
157	})
158}
159
160func copyFile(src, dst string, mode os.FileMode) error {
161	in, err := os.Open(src)
162	if err != nil {
163		return err
164	}
165	defer in.Close()
166	out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
167	if err != nil {
168		return err
169	}
170	defer out.Close()
171	_, err = io.Copy(out, in)
172	return err
173}