build.go

v1.0.1
Doc Versions Source
1
package build
2
3
import (
4
	"bytes"
5
	"encoding/json"
6
	"fmt"
7
	"io"
8
	"io/fs"
9
	"os/exec"
10
	"strings"
11
	"time"
12
13
	"golang.org/x/mod/module"
14
	"golang.org/x/mod/zip"
15
16
	"example.com/curator/internal/git"
17
)
18
19
// RevInfo is the JSON structure returned by .info and @latest endpoints.
20
type RevInfo struct {
21
	Version string    `json:"Version"`
22
	Time    time.Time `json:"Time"`
23
	Origin  *Origin   `json:"Origin,omitempty"`
24
}
25
26
// Origin describes the provenance of a module version.
27
type Origin struct {
28
	VCS  string `json:"VCS,omitempty"`
29
	URL  string `json:"URL,omitempty"`
30
	Hash string `json:"Hash,omitempty"`
31
	Ref  string `json:"Ref,omitempty"`
32
}
33
34
// Info builds a JSON .info response for the given version.
35
func Info(repoPath string, rv *git.ResolvedVersion, modulePath string) ([]byte, string, error) {
36
	hash, t, err := git.CommitInfo(repoPath, rv.GitRev)
37
	if err != nil {
38
		return nil, "", err
39
	}
40
41
	info := RevInfo{
42
		Version: rv.Version,
43
		Time:    t.UTC(),
44
		Origin: &Origin{
45
			VCS:  "git",
46
			URL:  modulePath,
47
			Hash: hash,
48
			Ref:  rv.GitRev,
49
		},
50
	}
51
52
	data, err := json.Marshal(info)
53
	return data, "application/json", err
54
}
55
56
// Mod builds a go.mod response. Falls back to a synthetic go.mod if none exists.
57
func Mod(repoPath string, rv *git.ResolvedVersion, modulePath string) ([]byte, string, error) {
58
	data, err := git.ReadFile(repoPath, rv.GitRev, "go.mod")
59
	if err != nil {
60
		data = []byte("module " + modulePath + "\n")
61
	}
62
63
	return data, "text/plain; charset=utf-8", nil
64
}
65
66
// Zip builds a module zip archive. Works with both bare and non-bare git repos
67
// by reading files directly via git commands.
68
func Zip(repoPath string, rv *git.ResolvedVersion, modulePath string) ([]byte, string, error) {
69
	var buf bytes.Buffer
70
	mv := module.Version{Path: modulePath, Version: rv.Version}
71
72
	files, err := listGitFiles(repoPath, rv.GitRev)
73
	if err != nil {
74
		return nil, "", fmt.Errorf("list files: %w", err)
75
	}
76
77
	var zipFiles []zip.File
78
	for _, path := range files {
79
		zipFiles = append(zipFiles, &gitFile{
80
			repoPath: repoPath,
81
			rev:      rv.GitRev,
82
			path:     path,
83
		})
84
	}
85
86
	if err := zip.Create(&buf, mv, zipFiles); err != nil {
87
		return nil, "", err
88
	}
89
90
	return buf.Bytes(), "application/zip", nil
91
}
92
93
// listGitFiles returns all file paths at a revision using git ls-tree.
94
func listGitFiles(repoPath, rev string) ([]string, error) {
95
	cmd := exec.Command("git", "-C", repoPath, "ls-tree", "-r", "--name-only", rev)
96
	out, err := cmd.Output()
97
	if err != nil {
98
		return nil, fmt.Errorf("git ls-tree: %w", err)
99
	}
100
101
	var files []string
102
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
103
		if line != "" {
104
			files = append(files, line)
105
		}
106
	}
107
	return files, nil
108
}
109
110
// gitFile implements zip.File by reading from a bare git repo.
111
type gitFile struct {
112
	repoPath string
113
	rev      string
114
	path     string
115
}
116
117
func (f *gitFile) Path() string { return f.path }
118
119
func (f *gitFile) Lstat() (fs.FileInfo, error) {
120
	data, err := f.readContent()
121
	if err != nil {
122
		return nil, err
123
	}
124
	return &gitFileInfo{name: f.path, size: int64(len(data))}, nil
125
}
126
127
func (f *gitFile) Open() (io.ReadCloser, error) {
128
	data, err := f.readContent()
129
	if err != nil {
130
		return nil, err
131
	}
132
	return io.NopCloser(bytes.NewReader(data)), nil
133
}
134
135
func (f *gitFile) readContent() ([]byte, error) {
136
	return git.ReadFile(f.repoPath, f.rev, f.path)
137
}
138
139
// gitFileInfo implements fs.FileInfo for git files.
140
type gitFileInfo struct {
141
	name string
142
	size int64
143
}
144
145
func (fi *gitFileInfo) Name() string      { return fi.name }
146
func (fi *gitFileInfo) Size() int64       { return fi.size }
147
func (fi *gitFileInfo) Mode() fs.FileMode { return 0o644 }
148
func (fi *gitFileInfo) ModTime() time.Time { return time.Time{} }
149
func (fi *gitFileInfo) IsDir() bool       { return false }
150
func (fi *gitFileInfo) Sys() any          { return nil }
151

Source Files