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.
server/web/request_view.go +86
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "sort" | |
| 11 | 11 | "strings" | |
| 12 | 12 | "time" | |
| 13 | "unicode/utf8" | ||
| 13 | 14 | ||
| 14 | 15 | "github.com/rodent-software/vole/ci/runs" | |
| 15 | 16 | "github.com/rodent-software/vole/core/git" | |
| @@ -410,6 +411,91 @@ func (w *Web) requestLinkedIssues(m *repo.Meta, req *metadata.Request, nums map[ | |||
| 410 | 411 | return out | |
| 411 | 412 | } | |
| 412 | 413 | ||
| 414 | // requestIssueSearchLimit is how many matches the link picker offers. It is a | ||
| 415 | // picker beside the linked list, not the issue list: more than a handful makes the | ||
| 416 | // sidebar something to scroll before it is something to choose from. | ||
| 417 | const requestIssueSearchLimit = 8 | ||
| 418 | |||
| 419 | // requestIssueSearchView is the result fragment under the link search box. It is | ||
| 420 | // separate from the Issues section so typing replaces only the results. | ||
| 421 | type requestIssueSearchView struct { | ||
| 422 | Page | ||
| 423 | Name string | ||
| 424 | Request *metadata.Request | ||
| 425 | Query string | ||
| 426 | Results []requestIssue | ||
| 427 | Error string | ||
| 428 | } | ||
| 429 | |||
| 430 | // SearchRequestIssues filters the issues the link picker may add. A term matches | ||
| 431 | // a title, a `#42` number, or a ULID prefix; the linked issues are left out | ||
| 432 | // because offering one again would only produce a refusal. | ||
| 433 | func (w *Web) SearchRequestIssues(rw http.ResponseWriter, r *http.Request) { | ||
| 434 | m, ok := w.open(rw, r) | ||
| 435 | if !ok { | ||
| 436 | return | ||
| 437 | } | ||
| 438 | req, err := metadata.GetRequest(m.Git(), r.PathValue("id")) | ||
| 439 | if err != nil { | ||
| 440 | w.notFound(rw, r) | ||
| 441 | return | ||
| 442 | } | ||
| 443 | query := strings.TrimSpace(r.URL.Query().Get("q")) | ||
| 444 | view := requestIssueSearchView{ | ||
| 445 | Page: w.page(r, ""), Name: m.Name, Request: req, Query: query, | ||
| 446 | } | ||
| 447 | if query == "" || !req.Open() { | ||
| 448 | w.render(rw, "request_issue_search", view, 0) | ||
| 449 | return | ||
| 450 | } | ||
| 451 | if utf8.RuneCountInString(query) > 200 { | ||
| 452 | view.Error = "Search is limited to 200 characters." | ||
| 453 | w.render(rw, "request_issue_search", view, 0) | ||
| 454 | return | ||
| 455 | } | ||
| 456 | if w.Index == nil { | ||
| 457 | view.Error = "Issue search is unavailable right now." | ||
| 458 | w.render(rw, "request_issue_search", view, 0) | ||
| 459 | return | ||
| 460 | } | ||
| 461 | if err := w.Index.Ensure(m); err != nil { | ||
| 462 | if w.Logger != nil { | ||
| 463 | w.Logger("web: issue search index for %s: %v", m.Name, err) | ||
| 464 | } | ||
| 465 | view.Error = "Issue search is unavailable right now." | ||
| 466 | w.render(rw, "request_issue_search", view, 0) | ||
| 467 | return | ||
| 468 | } | ||
| 469 | linked := make(map[string]bool, len(req.Closes)) | ||
| 470 | for _, id := range req.Closes { | ||
| 471 | linked[id] = true | ||
| 472 | } | ||
| 473 | // Ask for the linked issues too, then drop them: a title shared by a linked | ||
| 474 | // issue must not fill the picker with rows that cannot be added. | ||
| 475 | rows, err := w.Index.SearchIssues(m.ID, query, requestIssueSearchLimit+len(linked)) | ||
| 476 | if err != nil { | ||
| 477 | if w.Logger != nil { | ||
| 478 | w.Logger("web: issue search for %s: %v", m.Name, err) | ||
| 479 | } | ||
| 480 | view.Error = "Issue search is unavailable right now." | ||
| 481 | w.render(rw, "request_issue_search", view, 0) | ||
| 482 | return | ||
| 483 | } | ||
| 484 | for _, row := range rows { | ||
| 485 | if linked[row.ID] { | ||
| 486 | continue | ||
| 487 | } | ||
| 488 | view.Results = append(view.Results, requestIssue{ | ||
| 489 | ID: row.ID, Number: row.Number, Title: row.Title, | ||
| 490 | Author: row.Author, Created: row.Created, | ||
| 491 | }) | ||
| 492 | if len(view.Results) == requestIssueSearchLimit { | ||
| 493 | break | ||
| 494 | } | ||
| 495 | } | ||
| 496 | w.render(rw, "request_issue_search", view, 0) | ||
| 497 | } | ||
| 498 | |||
| 413 | 499 | // pendingContributorCI finds the contributor branch whose tip is head and whose | |
| 414 | 500 | // CI still waits for a maintainer. It is keyed by the commit: a branch that has | |
| 415 | 501 | // moved past this request is not what is under review here. | |