Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/hook/releases.go

Line attribution on main · 109 line(s)

948a6d7d
Keenan Nemetz
17d ago
1 package hook
2
3 // Published releases are immutable, and this is the only place that can enforce
4 // it: the tag and the pointers arrive in ordinary pushes, and nothing else sees
5 // both. A download link whose bytes can change is worse than no link — the ETag
6 // moves with the bytes, so a cache tells the truth while the human who saved the
7 // old file has no way to notice.
8
9 import (
10 "fmt"
11 "sort"
12 "strings"
13
d37d301f
Keenan Nemetz
17h ago
14 "rodent.software/vole/core/git"
15 "rodent.software/vole/metadata"
948a6d7d
Keenan Nemetz
17d ago
16 )
17
18 // releaseFindings reports the changes that would detach a published artifact from
19 // the bytes its link serves:
20 //
21 // - moving or deleting a tag that releases/<tag>/ holds artifacts for;
22 // - deleting a pointer, or re-pointing one at different bytes.
23 //
24 // Adding artifacts is allowed: an artifact is published when its pointer lands, so
25 // there is nothing for an addition to invalidate.
c5902451
Keenan Nemetz
16d ago
26 func releaseFindings(g *git.Repo, c change) []string {
948a6d7d
Keenan Nemetz
17d ago
27 if strings.HasPrefix(c.ref, "refs/tags/") {
28 name := strings.TrimPrefix(c.ref, "refs/tags/")
29 if !tagHasArtifacts(g, name) {
30 return nil
31 }
32 verb := "moved"
c5902451
Keenan Nemetz
16d ago
33 if c.new == git.ZeroSHA || c.new == "" {
948a6d7d
Keenan Nemetz
17d ago
34 verb = "deleted"
35 }
36 return []string{fmt.Sprintf(
37 "%s: this tag has release artifacts attached, so it cannot be %s; "+
38 "a release is immutable once published. Tag the new commit with a new name",
39 c.ref, verb)}
40 }
c5902451
Keenan Nemetz
16d ago
41 if c.ref != metadata.Ref {
948a6d7d
Keenan Nemetz
17d ago
42 return nil
43 }
c5902451
Keenan Nemetz
16d ago
44 if c.old == "" || c.old == git.ZeroSHA {
948a6d7d
Keenan Nemetz
17d ago
45 return nil // a new metadata branch publishes nothing old
46 }
47 before := releaseBlobs(g, c.old)
48 if len(before) == 0 {
49 return nil
50 }
51 after := releaseBlobs(g, c.new) // an empty map when the branch is deleted
52 var problems []string
53 for path, sha := range before {
54 now, ok := after[path]
55 switch {
56 case !ok:
57 problems = append(problems, fmt.Sprintf(
58 "%s: a published artifact cannot be removed; publish a new release instead", path))
59 case now != sha:
60 problems = append(problems, fmt.Sprintf(
61 "%s: a published artifact cannot be re-pointed at different bytes; "+
62 "attach the new file under a new name", path))
63 }
64 }
65 sort.Strings(problems)
66 return problems
67 }
68
69 // releaseBlobs maps every path under releases/ in a commit to its blob id. The
70 // blob id is the pointer text, so an unchanged artifact has an unchanged id: the
71 // comparison needs no parsing and cannot be fooled by a reformatted pointer.
c5902451
Keenan Nemetz
16d ago
72 func releaseBlobs(g *git.Repo, commit string) map[string]string {
73 if commit == "" || commit == git.ZeroSHA {
948a6d7d
Keenan Nemetz
17d ago
74 return nil
75 }
76 entries, err := g.LsTreeR(commit)
77 if err != nil {
78 return nil
79 }
80 out := map[string]string{}
81 for _, e := range entries {
c5902451
Keenan Nemetz
16d ago
82 if strings.HasPrefix(e.Name, metadata.ReleasesDir+"/") {
948a6d7d
Keenan Nemetz
17d ago
83 out[e.Name] = e.SHA
84 }
85 }
86 return out
87 }
88
89 // tagHasArtifacts reports whether the metadata branch has a releases/<tag>/
90 // directory. It reads the current tip rather than the pushed tree: a tag push and
91 // a pointer push can arrive in either order, and a tag is only bound to artifacts
92 // that already exist.
c5902451
Keenan Nemetz
16d ago
93 func tagHasArtifacts(g *git.Repo, tag string) bool {
94 tip, err := g.RevParse(metadata.Ref)
948a6d7d
Keenan Nemetz
17d ago
95 if err != nil || tip == "" {
96 return false
97 }
98 entries, err := g.LsTreeR(tip)
99 if err != nil {
100 return false
101 }
c5902451
Keenan Nemetz
16d ago
102 prefix := metadata.ReleasesDir + "/" + tag + "/"
948a6d7d
Keenan Nemetz
17d ago
103 for _, e := range entries {
104 if strings.HasPrefix(e.Name, prefix) {
105 return true
106 }
107 }
108 return false
109 }