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.
Search for an issue when linking it to a change
838f282eebf7e68f61318eae93d03263df1341df · Keenan Nemetz · 10d ago · parent 267feec2
Search for an issue when linking it to a change The link field only accepted an id that the author already knew. It is now a search box over the issue index that matches a title, a #number, or a ULID prefix. A matching issue is a button: picking one posts the same link write and replaces the Issues section with the new list. Issues already linked are left out of the results.
forge/index/issues.go +52−3
| @@ -2,6 +2,7 @@ package index | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "database/sql" | |
| 5 | "strconv" | ||
| 5 | 6 | "strings" | |
| 6 | 7 | "time" | |
| 7 | 8 | ||
| @@ -266,11 +267,15 @@ type Filter struct { | |||
| 266 | 267 | Query string | |
| 267 | 268 | } | |
| 268 | 269 | ||
| 269 | func (s *Store) Issues(repoID string, f Filter) ([]IssueRow, error) { | ||
| 270 | q := `SELECT i.id, i.title, i.state, i.author, i.created, i.labels, i.assignees, i.comments, | ||
| 270 | // issueSelect is the row every issue list scans: the indexed issue with its | ||
| 271 | // `#42` alias from the numbering table. | ||
| 272 | const issueSelect = `SELECT i.id, i.title, i.state, i.author, i.created, i.labels, i.assignees, i.comments, | ||
| 271 | 273 | COALESCE(n.number, 0) | |
| 272 | 274 | FROM issues i | |
| 273 | LEFT JOIN entity_numbers n ON n.repo_id = i.repo_id AND n.id = i.id | ||
| 275 | LEFT JOIN entity_numbers n ON n.repo_id = i.repo_id AND n.id = i.id` | ||
| 276 | |||
| 277 | func (s *Store) Issues(repoID string, f Filter) ([]IssueRow, error) { | ||
| 278 | q := issueSelect + ` | ||
| 274 | 279 | WHERE i.repo_id=?` | |
| 275 | 280 | args := []any{repoID} | |
| 276 | 281 | if f.State != "" && f.State != "all" { | |
| @@ -297,6 +302,50 @@ func (s *Store) Issues(repoID string, f Filter) ([]IssueRow, error) { | |||
| 297 | 302 | return nil, err | |
| 298 | 303 | } | |
| 299 | 304 | defer rows.Close() | |
| 305 | return scanIssueRows(rows) | ||
| 306 | } | ||
| 307 | |||
| 308 | // SearchIssues finds the issues a link picker should offer. A term matches a | ||
| 309 | // title as a substring, and a number or ULID as a typed reference: `42` and | ||
| 310 | // `#42` find issue 42, and a ULID or its prefix matches from the start. Exact | ||
| 311 | // references sort ahead of title matches so the issue someone named is first. | ||
| 312 | func (s *Store) SearchIssues(repoID, term string, limit int) ([]IssueRow, error) { | ||
| 313 | term = strings.TrimSpace(term) | ||
| 314 | if term == "" { | ||
| 315 | return nil, nil | ||
| 316 | } | ||
| 317 | if limit <= 0 { | ||
| 318 | limit = 8 | ||
| 319 | } | ||
| 320 | ref := strings.TrimPrefix(term, "#") | ||
| 321 | number := 0 | ||
| 322 | if n, err := strconv.Atoi(ref); err == nil && n > 0 { | ||
| 323 | number = n | ||
| 324 | } | ||
| 325 | q := issueSelect + ` | ||
| 326 | WHERE i.repo_id=? AND (i.title LIKE ? OR i.id LIKE ?` | ||
| 327 | args := []any{repoID, "%" + term + "%", ref + "%"} | ||
| 328 | if number > 0 { | ||
| 329 | q += ` OR n.number=?` | ||
| 330 | args = append(args, number) | ||
| 331 | } | ||
| 332 | q += `) | ||
| 333 | ORDER BY CASE | ||
| 334 | WHEN i.id=? THEN 0 | ||
| 335 | WHEN n.number=? THEN 1 | ||
| 336 | WHEN i.id LIKE ? THEN 2 | ||
| 337 | ELSE 3 END, i.id DESC | ||
| 338 | LIMIT ?` | ||
| 339 | args = append(args, ref, number, ref+"%", limit) | ||
| 340 | rows, err := s.db.Query(q, args...) | ||
| 341 | if err != nil { | ||
| 342 | return nil, err | ||
| 343 | } | ||
| 344 | defer rows.Close() | ||
| 345 | return scanIssueRows(rows) | ||
| 346 | } | ||
| 347 | |||
| 348 | func scanIssueRows(rows *sql.Rows) ([]IssueRow, error) { | ||
| 300 | 349 | var out []IssueRow | |
| 301 | 350 | for rows.Next() { | |
| 302 | 351 | var r IssueRow | |