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/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.

Close linked issues when a change request lands

183d48e55e14b0355db4268275893274433ce8ee · Keenan Nemetz · 14d ago · parent 097b2110

Close linked issues when a change request lands

A change request can now name the issues it closes. Landing writes closed
onto each of them in the same commit that marks the request merged, so the
merge and the closure cannot be separated: a request cannot be merged with
its issues still open, and an issue cannot be closed by a merge that did
not happen.

The link lives in request.md as a list of full issue ids, which is what
makes it reviewable: it is part of the file a reviewer reads, not a
reference buried in prose. Ids are validated when the request is written,
including that the issue exists, so a typo fails while the author is still
looking at the command instead of at the landing they have waited for.

`vole change new --closes <id>` (repeatable, comma lists accepted) sets it,
`vole change merge` reports what it closed, and the browser form takes the
same field. The request page links each issue, and the merge answer names
the ones it closed.

An issue that is already closed, or that is no longer there, is left alone:
the promise is about what a merge does, not permission to overrule somebody
who closed it deliberately. Re-marking an already-merged request closes
nothing, so a retried landing is harmless.

Closes #01M5XG8WYZ1566KZFC73WQWGX3.

forge/landing/land.go +18−7

@@ -45,6 +45,9 @@ type Plan struct {
45 45 // Approval is the request and approvers that authorise the landing, nil when the
46 46 // branch needs no approval (unprotected, or the actor is an admin).
47 47 Approval *metadata.Merge
48 // ClosedIssues is filled in by Land: the issues the request linked were closed in
49 // the same commit that marked it merged.
50 ClosedIssues []string
48 51 }
49 52
50 53 // ErrLandedUnmarked is returned when the branch moved but the request could not be
@@ -93,9 +96,11 @@ func Land(g Gate, id, actor string, now time.Time) (*Plan, error) {
93 96 if plan.Request.State == "merged" && plan.Request.MergedHead != "" {
94 97 return plan, nil
95 98 }
96 if err := marked(g, id, plan.Head, actor, now); err != nil {
99 closed, err := marked(g, id, plan.Head, actor, now)
100 if err != nil {
97 101 return plan, &ErrLandedUnmarked{NewTip: plan.OldTip, Err: err}
98 102 }
103 plan.ClosedIssues = closed
99 104 return plan, nil
100 105 }
101 106 ok, err := g.Repo.UpdateRefCAS(g.Ref(plan.Base), plan.NewTip, plan.OldTip)
@@ -115,9 +120,11 @@ func Land(g Gate, id, actor string, now time.Time) (*Plan, error) {
115 120 g.Audit(fmt.Sprintf("merge %s by %s (head %s)", g.Ref(plan.Base), actor, format.SHA(plan.NewTip)))
116 121 }
117 122 }
118 if err := marked(g, id, plan.NewTip, actor, now); err != nil {
123 closed, err := marked(g, id, plan.NewTip, actor, now)
124 if err != nil {
119 125 return plan, &ErrLandedUnmarked{NewTip: plan.NewTip, Err: err}
120 126 }
127 plan.ClosedIssues = closed
121 128 return plan, nil
122 129 }
123 130 return nil, lastRace
@@ -206,12 +213,16 @@ func Prepare(g Gate, id, actor string, now time.Time) (*Plan, error) {
206 213
207 214 // marked writes the merged state and turns a validation problem into an error, so a
208 215 // landing never leaves a request file the metadata reader would refuse.
209 func marked(g Gate, id, head, actor string, now time.Time) error {
210 if _, _, err := metadata.MarkMerged(g.Repo, id, head, actor, now); err != nil {
211 return err
216 func marked(g Gate, id, head, actor string, now time.Time) ([]string, error) {
217 _, closed, err := metadata.MarkMerged(g.Repo, id, head, actor, now)
218 if err != nil {
219 return nil, err
212 220 }
213 221 if problems := metadata.ValidateRequests(g.Repo, metadata.Ref); len(problems) > 0 {
214 return fmt.Errorf("the request tree is invalid after marking it merged: %s", strings.Join(problems, "; "))
222 return nil, fmt.Errorf("the request tree is invalid after marking it merged: %s", strings.Join(problems, "; "))
223 }
224 if problems := metadata.ValidateIssues(g.Repo, metadata.Ref); len(problems) > 0 {
225 return nil, fmt.Errorf("the issue tree is invalid after closing the linked issues: %s", strings.Join(problems, "; "))
215 226 }
216 return nil
227 return closed, nil
217 228 }