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.
Show state changes and actor/assignee filters in activity
8e93a8f0e42847734facadaa8a56f857ed1a1565 · Keenan Nemetz · 15d ago · parent 2973f727
Show state changes and actor/assignee filters in activity Issue state changes now write a canonical "issue: <id> <state>" subject, the same shape requests already used, so the feed can read closed, reopened, and merged events from the metadata log without diffing historical files. The CLI and the web share metadata.SetIssueState. The repository feed filters by type, actor, and assignee. Filters survive type changes and HTMX swaps, and are applied before the 100-event limit so an older matching event is not hidden by newer unrelated ones.
metadata/activity.go +74
| @@ -0,0 +1,74 @@ | |||
| 1 | package metadata | ||
| 2 | |||
| 3 | // State changes are recoverable from the metadata log because SetIssueState and | ||
| 4 | // SetRequestState write a canonical subject: the resource, its id, and the new | ||
| 5 | // state. Reading subjects rather than diffing every issue file keeps the | ||
| 6 | // activity feed to one git call and leaves the file format alone. | ||
| 7 | |||
| 8 | import ( | ||
| 9 | "strings" | ||
| 10 | "time" | ||
| 11 | |||
| 12 | "github.com/rodent-software/vole/core/git" | ||
| 13 | voleid "github.com/rodent-software/vole/core/id" | ||
| 14 | ) | ||
| 15 | |||
| 16 | // StateChange is one closed, reopened, or merged event from the metadata branch. | ||
| 17 | type StateChange struct { | ||
| 18 | Kind string // issue | request | ||
| 19 | ID string | ||
| 20 | State string // open | closed | merged | ||
| 21 | Actor string | ||
| 22 | Time time.Time | ||
| 23 | } | ||
| 24 | |||
| 25 | // LoadStateChanges reads at most n metadata commits and returns the state | ||
| 26 | // changes among them, newest first. | ||
| 27 | func LoadStateChanges(g *git.Repo, n int) ([]StateChange, error) { | ||
| 28 | commits, err := g.Log(Ref, n) | ||
| 29 | if err != nil { | ||
| 30 | return nil, err | ||
| 31 | } | ||
| 32 | out := make([]StateChange, 0, len(commits)/4) | ||
| 33 | for _, c := range commits { | ||
| 34 | kind, id, state, ok := parseStateSubject(c.Subject) | ||
| 35 | if !ok { | ||
| 36 | continue | ||
| 37 | } | ||
| 38 | out = append(out, StateChange{ | ||
| 39 | Kind: kind, ID: id, State: state, | ||
| 40 | Actor: c.Author, Time: time.Unix(c.When, 0).UTC(), | ||
| 41 | }) | ||
| 42 | } | ||
| 43 | return out, nil | ||
| 44 | } | ||
| 45 | |||
| 46 | func parseStateSubject(subject string) (kind, id, state string, ok bool) { | ||
| 47 | fields := strings.Fields(subject) | ||
| 48 | if len(fields) != 3 { | ||
| 49 | return "", "", "", false | ||
| 50 | } | ||
| 51 | switch fields[0] { | ||
| 52 | case "issue:": | ||
| 53 | kind = "issue" | ||
| 54 | case "request:": | ||
| 55 | kind = "request" | ||
| 56 | default: | ||
| 57 | return "", "", "", false | ||
| 58 | } | ||
| 59 | id, state = fields[1], fields[2] | ||
| 60 | if !voleid.Valid(id) { | ||
| 61 | return "", "", "", false | ||
| 62 | } | ||
| 63 | switch kind { | ||
| 64 | case "issue": | ||
| 65 | if state != "open" && state != "closed" { | ||
| 66 | return "", "", "", false | ||
| 67 | } | ||
| 68 | case "request": | ||
| 69 | if state != "open" && state != "closed" && state != "merged" { | ||
| 70 | return "", "", "", false | ||
| 71 | } | ||
| 72 | } | ||
| 73 | return kind, id, state, true | ||
| 74 | } | ||