feat/multi-repo
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}