vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
refactor
c5902451905acdaa478f8476844ccd4a510d9ca9 · Keenan Nemetz · 17d ago · parent ec2aba4b
internal/releases/releases.go → metadata/releases.go +28−18
| @@ -1,4 +1,4 @@ | |||
| 1 | // Package releases reads the artifacts attached to a release. | ||
| 1 | // Release artifacts are the files attached to a release. | ||
| 2 | 2 | // | |
| 3 | 3 | // The artifact list is a directory on the metadata branch: releases/<tag>/<name> | |
| 4 | 4 | // holds a standard git-lfs pointer blob, and the pointer is the whole record. | |
| @@ -15,20 +15,17 @@ | |||
| 15 | 15 | // A JSON manifest listing sha256 strings would have been none of those things: it | |
| 16 | 16 | // parses as no pointer, so the sweep would treat every published artifact as | |
| 17 | 17 | // garbage once the grace period passed. | |
| 18 | package releases | ||
| 18 | package metadata | ||
| 19 | 19 | ||
| 20 | 20 | import ( | |
| 21 | 21 | "fmt" | |
| 22 | 22 | "sort" | |
| 23 | 23 | "strings" | |
| 24 | 24 | ||
| 25 | "github.com/rodent-software/vole/internal/gitx" | ||
| 26 | "github.com/rodent-software/vole/internal/lfs" | ||
| 25 | "github.com/rodent-software/vole/core/git" | ||
| 26 | "github.com/rodent-software/vole/core/lfs" | ||
| 27 | 27 | ) | |
| 28 | 28 | ||
| 29 | // Dir is the metadata-branch directory releases live under. | ||
| 30 | const Dir = "releases" | ||
| 31 | |||
| 32 | 29 | // Artifact is one file attached to a release. | |
| 33 | 30 | type Artifact struct { | |
| 34 | 31 | Name string | |
| @@ -37,13 +34,13 @@ type Artifact struct { | |||
| 37 | 34 | } | |
| 38 | 35 | ||
| 39 | 36 | // Path is where an artifact's pointer lives on the metadata branch. | |
| 40 | func Path(tag, name string) string { return Dir + "/" + tag + "/" + name } | ||
| 37 | func ReleasePath(tag, name string) string { return ReleasesDir + "/" + tag + "/" + name } | ||
| 41 | 38 | ||
| 42 | 39 | // ValidName rejects artifact names that would not be one path segment under the | |
| 43 | 40 | // release's directory. A name is a filename, not a path: allowing "/" would let | |
| 44 | 41 | // an attach write outside releases/<tag>/, and the download route could not name | |
| 45 | 42 | // the result. | |
| 46 | func ValidName(name string) error { | ||
| 43 | func ValidReleaseName(name string) error { | ||
| 47 | 44 | switch { | |
| 48 | 45 | case name == "": | |
| 49 | 46 | return fmt.Errorf("an artifact needs a name") | |
| @@ -62,8 +59,8 @@ func ValidName(name string) error { | |||
| 62 | 59 | // List reads the artifacts of one tag, sorted by name. A tag with nothing | |
| 63 | 60 | // attached is an empty list rather than an error: the directory only exists once | |
| 64 | 61 | // something is attached to it. | |
| 65 | func List(g *gitx.Repo, tag string) ([]Artifact, error) { | ||
| 66 | entries, err := g.LsTree("refs/heads/vole:" + Dir + "/" + tag) | ||
| 62 | func ListReleaseArtifacts(g *git.Repo, tag string) ([]Artifact, error) { | ||
| 63 | entries, err := g.LsTree("refs/heads/vole:" + ReleasesDir + "/" + tag) | ||
| 67 | 64 | if err != nil { | |
| 68 | 65 | return nil, nil // no releases/<tag>/ directory: no artifacts | |
| 69 | 66 | } | |
| @@ -72,7 +69,7 @@ func List(g *gitx.Repo, tag string) ([]Artifact, error) { | |||
| 72 | 69 | if e.Type != "blob" { | |
| 73 | 70 | continue | |
| 74 | 71 | } | |
| 75 | data, err := g.BlobAt("refs/heads/vole", Path(tag, e.Name)) | ||
| 72 | data, err := g.BlobAt("refs/heads/vole", ReleasePath(tag, e.Name)) | ||
| 76 | 73 | if err != nil { | |
| 77 | 74 | return nil, fmt.Errorf("read artifact pointer %s: %w", e.Name, err) | |
| 78 | 75 | } | |
| @@ -94,7 +91,7 @@ func List(g *gitx.Repo, tag string) ([]Artifact, error) { | |||
| 94 | 91 | // that is not releases/<tag>/<name> pointing at an object nobody uploaded is | |
| 95 | 92 | // either a mistake or a download that will 404, and both are cheaper to refuse | |
| 96 | 93 | // here than to discover later. | |
| 97 | func Problems(g *gitx.Repo, commit string) []string { | ||
| 94 | func ReleaseProblems(g *git.Repo, commit string) []string { | ||
| 98 | 95 | if commit == "" { | |
| 99 | 96 | return nil | |
| 100 | 97 | } | |
| @@ -104,7 +101,7 @@ func Problems(g *gitx.Repo, commit string) []string { | |||
| 104 | 101 | } | |
| 105 | 102 | var problems []string | |
| 106 | 103 | for _, e := range blobs { | |
| 107 | if !strings.HasPrefix(e.Name, Dir+"/") { | ||
| 104 | if !strings.HasPrefix(e.Name, ReleasesDir+"/") { | ||
| 108 | 105 | continue | |
| 109 | 106 | } | |
| 110 | 107 | parts := strings.Split(e.Name, "/") | |
| @@ -112,13 +109,13 @@ func Problems(g *gitx.Repo, commit string) []string { | |||
| 112 | 109 | case len(parts) != 3: | |
| 113 | 110 | problems = append(problems, fmt.Sprintf( | |
| 114 | 111 | "%s: releases are %s/<tag>/<name>; a release directory holds files, not more directories", | |
| 115 | e.Name, Dir)) | ||
| 112 | e.Name, ReleasesDir)) | ||
| 116 | 113 | continue | |
| 117 | 114 | case parts[1] == "": | |
| 118 | 115 | problems = append(problems, fmt.Sprintf("%s: missing tag name", e.Name)) | |
| 119 | 116 | continue | |
| 120 | 117 | } | |
| 121 | if err := ValidName(parts[2]); err != nil { | ||
| 118 | if err := ValidReleaseName(parts[2]); err != nil { | ||
| 122 | 119 | problems = append(problems, e.Name+": "+err.Error()) | |
| 123 | 120 | continue | |
| 124 | 121 | } | |
| @@ -136,7 +133,20 @@ func Problems(g *gitx.Repo, commit string) []string { | |||
| 136 | 133 | ||
| 137 | 134 | // TagWithArtifacts reports whether a tag has an artifacts directory. The release | |
| 138 | 135 | // page uses it to say "no artifacts" without a second tree read. | |
| 139 | func TagWithArtifacts(g *gitx.Repo, tag string) bool { | ||
| 140 | _, err := g.LsTree("refs/heads/vole:" + Dir + "/" + tag) | ||
| 136 | func TagWithArtifacts(g *git.Repo, tag string) bool { | ||
| 137 | _, err := g.LsTree("refs/heads/vole:" + ReleasesDir + "/" + tag) | ||
| 141 | 138 | return err == nil | |
| 142 | 139 | } | |
| 140 | |||
| 141 | // WritePointers records artifacts on the metadata branch, one literal LFS | ||
| 142 | // pointer per name. It starts from the branch's current tree, so applying it | ||
| 143 | // again after a lost push race is idempotent. | ||
| 144 | func WriteReleasePointers(g *git.Repo, ref, tag string, arts []Artifact, message, author string) error { | ||
| 145 | _, err := git.CommitFiles(g, ref, func(files map[string][]byte) error { | ||
| 146 | for _, a := range arts { | ||
| 147 | files[ReleasePath(tag, a.Name)] = lfs.Pointer{OID: a.OID, Size: a.Size}.Encode() | ||
| 148 | } | ||
| 149 | return nil | ||
| 150 | }, message, author) | ||
| 151 | return err | ||
| 152 | } | ||