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/web/release_actions.go

Line attribution on main · 206 line(s)

ec343013
Keenan Nemetz
15d ago
1 package web
2
3 // Creating a release is creating an annotated tag, so the tag remains the whole
4 // release. The web form exists to make the notes, the target commit, and the
5 // tagger explicit; it does not introduce a second release record. Editing notes
6 // moves the tag as a new tag object, which the release rules allow only until a
7 // file has been attached.
8
9 import (
10 "net/http"
11 "strings"
12
d37d301f
Keenan Nemetz
17h ago
13 "rodent.software/vole/forge/repo"
14 "rodent.software/vole/metadata"
ec343013
Keenan Nemetz
15d ago
15 )
16
17 const maxReleaseNoteBytes = 64 << 10
18
19 type releaseFormView struct {
20 Page
21 Name string
22 Tag string
23 Target string
24 Notes string
25 Editing bool
26 Locked bool
27 Refs []string
28 Error string
29 }
30
bca9ec24
Keenan Nemetz
9d ago
31 // refreshReleaseEvents brings the feed's release rows up to date after the
32 // browser writes a tag directly on the server. A failure is logged, never
33 // reported over the release that already exists.
34 func (w *Web) refreshReleaseEvents(m *repo.Meta) {
35 if w.Index == nil {
36 return
37 }
38 if err := w.Index.RefreshTags(m); err != nil && w.Logger != nil {
39 w.Logger("web: refresh release events for %s: %v", m.Name, err)
40 }
41 }
42
c717d4eb
Keenan Nemetz
13d ago
43 // canManageReleases is the read-side half of the release writes: the pages that
44 // offer the form and the handlers that accept it ask the same capability.
ec343013
Keenan Nemetz
15d ago
45 func (w *Web) canManageReleases(r *http.Request, m *repo.Meta) bool {
c717d4eb
Keenan Nemetz
13d ago
46 acl, err := m.ACL()
47 if err != nil {
ec343013
Keenan Nemetz
15d ago
48 return false
49 }
c717d4eb
Keenan Nemetz
13d ago
50 return canMaintain(acl, w.accountName(r))
ec343013
Keenan Nemetz
15d ago
51 }
52
53 func (w *Web) releaseFormView(r *http.Request, m *repo.Meta, editing bool, tag, target, notes, msg string) releaseFormView {
54 return releaseFormView{
55 Page: w.page(r, m.Name+" · release"), Name: m.Name,
56 Tag: tag, Target: target, Notes: notes, Editing: editing,
57 Refs: compareRefs(m.Git()), Error: msg,
58 }
59 }
60
61 func (w *Web) ReleaseNewPage(rw http.ResponseWriter, r *http.Request) {
62 m, ok := w.open(rw, r)
63 if !ok {
64 return
65 }
66 if _, ok := w.requireHTMLUser(rw, r); !ok {
67 return
68 }
69 if !w.canManageReleases(r, m) {
70 w.notFound(rw, r)
71 return
72 }
73 w.render(rw, "release_form", w.releaseFormView(r, m, false, "", m.DefaultBranch, "", ""), 0)
74 }
75
76 func (w *Web) CreateRelease(rw http.ResponseWriter, r *http.Request) {
c717d4eb
Keenan Nemetz
13d ago
77 ctx, ok := w.beginWrite(rw, r, maxSmallForm)
ec343013
Keenan Nemetz
15d ago
78 if !ok {
79 return
80 }
c717d4eb
Keenan Nemetz
13d ago
81 m := ctx.Repo
82 if !canMaintain(ctx.ACL, ctx.Name) {
ec343013
Keenan Nemetz
15d ago
83 w.notFound(rw, r)
84 return
85 }
86 tag := strings.TrimSpace(r.PostFormValue("tag"))
87 target := strings.TrimSpace(r.PostFormValue("target"))
88 notes := strings.TrimRight(r.PostFormValue("notes"), "\n")
89 view := w.releaseFormView(r, m, false, tag, target, notes, "")
90 fail := func(msg string) {
91 view.Error = msg
92 w.render(rw, "release_form", view, http.StatusBadRequest)
93 }
94
95 g := m.Git()
96 if tag == "new" || tag == "edit" {
97 fail("That tag name is reserved by the release pages.")
98 return
99 }
100 if !g.ValidTag(tag) {
101 fail("Choose a valid tag name.")
102 return
103 }
104 if strings.TrimSpace(notes) == "" {
105 fail("A release needs notes; they are the tag message.")
106 return
107 }
108 if len(notes) > maxReleaseNoteBytes {
109 fail("Release notes are limited to 64 KiB.")
110 return
111 }
112 commit, _ := g.RevParse(target)
113 if commit == "" {
114 fail("That target revision does not exist.")
115 return
116 }
117 if refs, err := g.ListRefs("refs/tags/"); err == nil {
118 if _, exists := refs["refs/tags/"+tag]; exists {
119 fail("That tag already exists. A release is immutable; choose a new tag name.")
120 return
121 }
122 }
c717d4eb
Keenan Nemetz
13d ago
123 if err := g.WriteAnnotatedTag(tag, commit, notes, ctx.Name, false); err != nil {
ec343013
Keenan Nemetz
15d ago
124 fail(err.Error())
125 return
126 }
bca9ec24
Keenan Nemetz
9d ago
127 w.refreshReleaseEvents(m)
b1499c72
Keenan Nemetz
13d ago
128 w.audit("web.release.create", ctx.Name, m.Name, tag)
ed03b3d1
Keenan Nemetz
4h ago
129 http.Redirect(rw, r, "/"+m.Name+"/releases/"+pathURL(tag), http.StatusSeeOther)
ec343013
Keenan Nemetz
15d ago
130 }
131
132 func (w *Web) ReleaseEditPage(rw http.ResponseWriter, r *http.Request) {
133 m, ok := w.open(rw, r)
134 if !ok {
135 return
136 }
137 if _, ok := w.requireHTMLUser(rw, r); !ok {
138 return
139 }
140 if !w.canManageReleases(r, m) {
141 w.notFound(rw, r)
142 return
143 }
144 tag := strings.TrimSpace(r.URL.Query().Get("tag"))
145 t, err := m.Git().Tag(tag)
146 if err != nil {
147 w.notFound(rw, r)
148 return
149 }
150 view := w.releaseFormView(r, m, true, t.Name, t.Commit, t.Message, "")
151 if !t.Annotated {
152 view.Locked = true
153 view.Error = "Lightweight tags have no notes to edit."
154 } else if metadata.TagWithArtifacts(m.Git(), t.Name) {
155 view.Locked = true
156 view.Error = "This release has files attached, so it cannot be edited. Attach a new file or publish a new release instead."
157 }
158 w.render(rw, "release_form", view, 0)
159 }
160
161 func (w *Web) UpdateRelease(rw http.ResponseWriter, r *http.Request) {
c717d4eb
Keenan Nemetz
13d ago
162 ctx, ok := w.beginWrite(rw, r, maxSmallForm)
ec343013
Keenan Nemetz
15d ago
163 if !ok {
164 return
165 }
c717d4eb
Keenan Nemetz
13d ago
166 m := ctx.Repo
167 if !canMaintain(ctx.ACL, ctx.Name) {
ec343013
Keenan Nemetz
15d ago
168 w.notFound(rw, r)
169 return
170 }
171 tag := strings.TrimSpace(r.PostFormValue("tag"))
172 notes := strings.TrimRight(r.PostFormValue("notes"), "\n")
173 t, err := m.Git().Tag(tag)
174 if err != nil {
175 w.notFound(rw, r)
176 return
177 }
178 view := w.releaseFormView(r, m, true, t.Name, t.Commit, notes, "")
179 fail := func(msg string) {
180 view.Error = msg
181 w.render(rw, "release_form", view, http.StatusBadRequest)
182 }
183 if !t.Annotated {
184 fail("Lightweight tags have no notes to edit.")
185 return
186 }
187 if metadata.TagWithArtifacts(m.Git(), t.Name) {
188 fail("This release has files attached, so it cannot be edited.")
189 return
190 }
191 if strings.TrimSpace(notes) == "" {
192 fail("A release needs notes; they are the tag message.")
193 return
194 }
195 if len(notes) > maxReleaseNoteBytes {
196 fail("Release notes are limited to 64 KiB.")
197 return
198 }
c717d4eb
Keenan Nemetz
13d ago
199 if err := m.Git().WriteAnnotatedTag(t.Name, t.Commit, notes, ctx.Name, true); err != nil {
ec343013
Keenan Nemetz
15d ago
200 fail(err.Error())
201 return
202 }
bca9ec24
Keenan Nemetz
9d ago
203 w.refreshReleaseEvents(m)
b1499c72
Keenan Nemetz
13d ago
204 w.audit("web.release.edit", ctx.Name, m.Name, t.Name)
ed03b3d1
Keenan Nemetz
4h ago
205 http.Redirect(rw, r, "/"+m.Name+"/releases/"+pathURL(t.Name), http.StatusSeeOther)
ec343013
Keenan Nemetz
15d ago
206 }