package main // progress: how far a design, a milestone, an issue or a record has come, in one call (novox/hq issue 1947). The // knowledge base works a design's state out and nobody writes it (ADR 0424 §1, §2); this verb says it at every // level, in two forms: minimal, one line per child, cheap enough for every five minutes; detailed, the tree of a // lead's report with the pull requests, the held groups and what waits on whom, for about every thirty. // // It keeps no state of its own. It reads, each a request and reply over the bus: the designs' titles and states through // the records module (design.go, cached by hq's commit), their milestones from the issue tracker (milestones.go, // issue 1781), the issues from the issue tracker (`issue-tracker.list`, whose rows // say each issue's delivery and when it last moved), and the deliveries and groups from mesh-delivery. import ( "encoding/json" "errors" "fmt" "regexp" "sort" "strconv" "strings" "time" ) // The two forms of an answer. const ( DetailMinimal = "minimal" DetailDetailed = "detailed" ) // The states, each with the mark a report draws. const ( StateDone = "done" StateMoving = "moving" StateWaiting = "waiting" StateStuck = "stuck" ) var marks = map[string]string{StateDone: "✓", StateMoving: "▶", StateWaiting: "⏸", StateStuck: "✗"} // rank orders the states from best to worst, for a parent taking its children's. var rank = map[string]int{StateDone: 0, StateWaiting: 1, StateMoving: 2, StateStuck: 3} // The most a detailed answer reads one by one, so one call stays one call's cost. const ( MostShows = 30 MostGets = 20 // MostIssuesAsked is the most the tracker's list answers by numbers in one call. MostIssuesAsked = 200 ) // SeatAsk asks another seat's verb or a module's tool over the bus and answers its result (stdio.Ask). type SeatAsk func(key string, body any) (json.RawMessage, error) // ProgressAsk is what progress is asked. type ProgressAsk struct { Of string Detail string Bound int // Since is a window, such as 60m: the answer is then only what changed in it (progress_since.go). Since string } // Item is one thing whose progress is said: a design, a milestone, an issue, a pull request. type Item struct { ID string `json:"id"` State string `json:"state"` Title string `json:"title,omitempty"` Done int `json:"done,omitempty"` Total int `json:"total,omitempty"` Moved *time.Time `json:"moved,omitempty"` Why string `json:"why,omitempty"` Children []Item `json:"children,omitempty"` } // Progress is the answer. type Progress struct { Of string `json:"of"` Detail string `json:"detail"` State string `json:"state"` Tree string `json:"tree"` Items []Item `json:"items,omitempty"` Warnings []string `json:"warnings,omitempty"` Source string `json:"source"` ReadAt time.Time `json:"read-at"` Cut string `json:"cut,omitempty"` // Since and Changes are said for an answer about a window: how long it is and how many changes it holds. Since string `json:"since,omitempty"` Changes int `json:"changes,omitempty"` // scope is what the answer is about, kept for a window's answer; nil without since. scope *sinceScope } // trackerRow is a row of `issue-tracker.list`. type trackerRow struct { Number int `json:"number"` Status string `json:"status"` Title string `json:"title"` Needs string `json:"needs"` Ready bool `json:"ready"` Delivery string `json:"delivery"` Worker string `json:"worker"` Since *time.Time `json:"since"` Stale bool `json:"stale"` Abandoned bool `json:"abandoned"` Moved *time.Time `json:"moved"` } // deliveryRow is a row of `mesh-delivery.deliveries`, with the issues `mesh-delivery.show` names for it. type deliveryRow struct { ID string `json:"id"` Number int `json:"number"` State string `json:"state"` Since time.Time `json:"since"` Waits string `json:"waits"` Group string `json:"group"` Issues []int `json:"-"` Title string `json:"-"` } // groupRow is a row of `mesh-delivery.groups`. type groupRow struct { ID string `json:"id"` State string `json:"state"` Why string `json:"why"` Members []string `json:"members"` } var ( designOf = regexp.MustCompile(`^design\s+([0-9]{1,3})$`) milestoneOf = regexp.MustCompile(`(?i)^milestone\s+([0-9]{1,3})/(.+)$`) issueOf = regexp.MustCompile(`^issue\s+([0-9]{1,6})$`) recordOf = regexp.MustCompile(`(?i)^(ADR|decision|research)\s+([0-9]{1,4})$`) ) // Progress says how far what `of` names has come. func (k *KB) Progress(a ProgressAsk) (*Progress, error) { detail := strings.TrimSpace(a.Detail) if detail == "" { detail = DetailMinimal } if detail != DetailMinimal && detail != DetailDetailed { return nil, fmt.Errorf("detail is %s or %s, not %q", DetailMinimal, DetailDetailed, a.Detail) } if k.Seats == nil { return nil, errors.New("the knowledge base reaches no other seat here") } window, err := parseSince(a.Since) if err != nil { return nil, err } of := strings.Join(strings.Fields(a.Of), " ") p := &Progress{Of: of, Detail: detail, ReadAt: k.now()} if window > 0 { p.scope = &sinceScope{rows: map[int]trackerRow{}} } switch { case strings.EqualFold(of, "mesh"): err = k.progressMesh(p) case designOf.MatchString(strings.ToLower(of)): n, _ := strconv.Atoi(designOf.FindStringSubmatch(strings.ToLower(of))[1]) err = k.progressDesign(p, n, "") case milestoneOf.MatchString(of): m := milestoneOf.FindStringSubmatch(of) n, _ := strconv.Atoi(m[1]) err = k.progressDesign(p, n, m[2]) case issueOf.MatchString(strings.ToLower(of)): n, _ := strconv.Atoi(issueOf.FindStringSubmatch(strings.ToLower(of))[1]) err = k.progressIssue(p, n) case recordOf.MatchString(of): m := recordOf.FindStringSubmatch(of) n, _ := strconv.Atoi(m[2]) kind := "ADR" if strings.EqualFold(m[1], "research") { kind = "research" } err = k.progressRecord(p, kind, n) default: return nil, fmt.Errorf("of names mesh, design , milestone / (such as milestone 47/R6), issue , "+ "decision (or ADR ) or research , not %q", a.Of) } if err != nil { return nil, err } if window > 0 { k.progressSince(p, window) } // A cut takes the warnings from the last: the calls that failed are said before the answers that did not parse. callsFirst(p.Warnings) fitProgress(p, boundOf(a.Bound)) return p, nil } // issueRows asks the tracker for these issues, in chunks it takes, and answers them by number with the numbers it // does not know. func (k *KB) issueRows(numbers []int) (map[int]trackerRow, []int, error) { rows := map[int]trackerRow{} var missing []int for from := 0; from < len(numbers); from += MostIssuesAsked { chunk := numbers[from:min(from+MostIssuesAsked, len(numbers))] raw, err := k.Seats("seat:issue-tracker.list", map[string]any{"numbers": chunk, "limit": len(chunk)}) if err != nil { return nil, nil, fmt.Errorf("the issue tracker does not answer: %v", err) } var l struct { Rows []trackerRow `json:"rows"` Missing []int `json:"missing"` } if err := json.Unmarshal(raw, &l); err != nil { return nil, nil, fmt.Errorf("issue-tracker.list answered something else: %v", err) } for _, r := range l.Rows { rows[r.Number] = r } missing = append(missing, l.Missing...) } return rows, missing, nil } // issueItem is one issue's state, from its tracker row (1947/AC3). func issueItem(r trackerRow) (Item, bool) { it := Item{ID: strconv.Itoa(r.Number), Title: r.Title, Moved: r.Moved} if it.Moved == nil { it.Moved = r.Since } held := strings.HasPrefix(r.Delivery, "held") || strings.Contains(r.Delivery, " failed") switch { case r.Status == "wontfix": return it, false case r.Status == "resolved": // Done only when its fix is delivered, or it has nothing to deliver (ADR 0424 §2, §4; 1947/AC3). switch d := r.Delivery; { case d == "": it.State, it.Why = StateDone, "resolved, nothing to deliver" case strings.HasPrefix(d, "delivered"): it.State, it.Why = StateDone, "resolved, delivered" case held || strings.Contains(d, " stopped"): it.State, it.Why = StateStuck, "resolved · "+d case strings.HasPrefix(d, "fix merged, not delivered"): it.State, it.Why = StateMoving, "resolved · "+d default: it.State, it.Why = StateWaiting, "resolved · "+d } case r.Abandoned || r.Stale: it.State, it.Why = StateStuck, fmt.Sprintf("%s, worker %s stale since %s", r.Status, r.Worker, when(r.Since)) case held: it.State, it.Why = StateStuck, r.Status+" · "+r.Delivery case r.Status == "diagnosing" || r.Status == "fixing": it.State, it.Why = StateMoving, fmt.Sprintf("%s since %s (%s)", r.Status, when(r.Since), r.Worker) if r.Delivery != "" { it.Why += " · " + r.Delivery } case r.Needs == "decision" || r.Needs == "operator": it.State, it.Why = StateWaiting, r.Status+", needs "+map[string]string{"decision": "a decision", "operator": "the operator"}[r.Needs] case !r.Ready: it.State, it.Why = StateWaiting, r.Status+", waits for what it depends on" default: it.State, it.Why = StateWaiting, r.Status+", ready" } return it, true } // rollUp gives a parent its children's state: stuck when one is, else moving when one moves, else waiting when one // waits, else done; done and total counted; moved the latest of theirs (1947/AC3). func rollUp(parent *Item, children []Item) { parent.State, parent.Done, parent.Total = StateDone, 0, len(children) if len(children) == 0 { parent.State, parent.Why = StateWaiting, "no issues yet" return } worst := StateDone for _, c := range children { if c.State == StateDone { parent.Done++ } if rank[c.State] > rank[worst] { worst = c.State } if c.Moved != nil && (parent.Moved == nil || c.Moved.After(*parent.Moved)) { m := *c.Moved parent.Moved = &m } } parent.State = worst } // milestoneItems is each milestone of a design as an item with its issues as children. func milestoneItems(d *Design, rows map[int]trackerRow, missing map[int]bool) ([]Item, []string) { var items []Item var warn []string var unanswered []string for _, m := range d.Milestones { mi := Item{ID: m.ID, Title: m.Outcome} var kids []Item for _, n := range m.Issues { if missing[n] { warn = append(warn, fmt.Sprintf("milestone %s names %d, which is not an issue", m.ID, n)) continue } r, ok := rows[n] if !ok { unanswered = append(unanswered, strconv.Itoa(n)) continue } if it, counts := issueItem(r); counts { kids = append(kids, it) } } rollUp(&mi, kids) if m.Status == "closed" && len(kids) == 0 { mi.State, mi.Why = StateDone, "closed: "+m.Reason } mi.Children = kids items = append(items, mi) } if len(unanswered) > 0 { warn = append(warn, fmt.Sprintf("the tracker neither answered nor named missing %d issues the milestones name, left out of the counts: %s", len(unanswered), strings.Join(unanswered, ", "))) } return items, warn } // progressDesign says a design's milestones, or with milestone one milestone's issues. func (k *KB) progressDesign(p *Progress, number int, milestone string) error { ds, commit, err := k.designs(false, number) if err != nil { return err } if ds, err = k.withMilestones(ds, number); err != nil { return err } d := ds[0] if d.Unread != "" { p.Warnings = append(p.Warnings, d.Unread) if d.Title == "" { d.Title = fmt.Sprintf("design %d", d.Number) } } if len(d.Milestones) == 0 { return fmt.Errorf("design %d has no milestone records in the issue tracker: seed them from its table with "+ "issue-tracker.import milestones %d", number, number) } p.Source = fmt.Sprintf("the issue tracker's milestone records of design %d, and %s at hq %s", number, d.Path, short(commit)) ms := d if milestone != "" { m := d.milestone(milestone) if m == nil { var ids []string for _, x := range d.Milestones { ids = append(ids, x.ID) } return fmt.Errorf("design %d has no milestone %q; its milestones are %s", number, milestone, strings.Join(ids, ", ")) } ms = &Design{Number: d.Number, Path: d.Path, Title: d.Title, Milestones: []Milestone{*m}} } rows, missingList, err := k.issueRows(ms.issues()) if err != nil { return err } missing := map[int]bool{} for _, n := range missingList { missing[n] = true } items, warn := milestoneItems(ms, rows, missing) p.Warnings = append(p.Warnings, warn...) p.scope.hold(rows, ms) var extra *extras if p.Detail == DetailDetailed && p.scope == nil { if extra, err = k.readExtras(items); err != nil { return err } p.Warnings = append(p.Warnings, extra.warnings...) } top := Item{ID: fmt.Sprintf("design %d", d.Number), Title: d.Title} rollUp(&top, items) top.Children = items if milestone != "" { top = items[0] top.ID = fmt.Sprintf("milestone %d/%s", d.Number, items[0].ID) } p.State = top.State p.Items = []Item{top} p.Tree = renderTree(top, p.Detail, extra, milestone != "") return nil } // progressMesh says every design being built: those with milestones one line each, the rest counted. func (k *KB) progressMesh(p *Progress) error { ds, commit, err := k.designs(true, 0) if err != nil { return err } if ds, err = k.withMilestones(ds, 0); err != nil { return err } p.Source = fmt.Sprintf("the issue tracker's milestone records, and the designs in %s at hq %s", DesignsFolder, short(commit)) p.Warnings = append(p.Warnings, unreadWarnings(ds)...) var with []*Design var without []string for _, d := range ds { if d.Status != "in-progress" { continue } if len(d.Milestones) == 0 { without = append(without, fmt.Sprintf("%02d", d.Number)) continue } with = append(with, d) } var numbers []int for _, d := range with { numbers = append(numbers, d.issues()...) } rows, missingList, err := k.issueRows(numbers) if err != nil { return err } missing := map[int]bool{} for _, n := range missingList { missing[n] = true } p.scope.hold(rows, with...) if p.scope != nil { p.scope.every = true } top := Item{ID: "mesh", Title: "the designs being built"} var kids []Item for _, d := range with { items, _ := milestoneItems(d, rows, missing) di := Item{ID: fmt.Sprintf("design %d", d.Number), Title: d.Title} rollUp(&di, items) if p.Detail == DetailDetailed { di.Children = items } kids = append(kids, di) } rollUp(&top, kids) top.Children = kids p.State = top.State p.Items = []Item{top} var b strings.Builder fmt.Fprintf(&b, "The mesh · %d designs in progress, %d with milestones\n", len(with)+len(without), len(with)) for i, di := range kids { branch := "├── " if i == len(kids)-1 && len(without) == 0 { branch = "└── " } fmt.Fprintf(&b, "%s%s %s · %s — %d of %d milestones done%s\n", branch, marks[di.State], di.ID, di.Title, di.Done, di.Total, movedSay(di.Moved)) } if len(without) > 0 { fmt.Fprintf(&b, "└── %d designs in progress name no milestones (issue 845): %s\n", len(without), strings.Join(without, ", ")) } p.Tree = strings.TrimRight(b.String(), "\n") return nil } // progressIssue says one issue: its state, its pull requests and their deliveries, what it waits for. func (k *KB) progressIssue(p *Progress, n int) error { rows, missing, err := k.issueRows([]int{n}) if err != nil { return err } r, ok := rows[n] if len(missing) > 0 || !ok { return fmt.Errorf("%d is not an issue the tracker holds", n) } it, counts := issueItem(r) if !counts { it.State, it.Why = StateDone, "wontfix" } it.ID = fmt.Sprintf("issue %d", n) p.Source = "the issue tracker and mesh-delivery" if p.scope != nil { p.scope.hold(rows) p.State, p.Items = it.State, []Item{it} return nil } extra, err := k.readExtras([]Item{{Children: []Item{{ID: strconv.Itoa(n), State: StateMoving}}}}) if err != nil { return err } p.Warnings = append(p.Warnings, extra.warnings...) p.State = it.State p.Items = []Item{it} var b strings.Builder fmt.Fprintf(&b, "%s issue %d · %s — %s%s\n", marks[it.State], n, it.Title, it.Why, movedSay(it.Moved)) if g := extra.gets[n]; g != nil { fmt.Fprintf(&b, "├── %d notes; last %s\n", g.notes, g.last) if len(g.waitsFor) > 0 { fmt.Fprintf(&b, "├── depends on %s\n", strings.Join(g.waitsFor, ", ")) } } lines := extra.pullLines(n) if len(lines) == 0 { b.WriteString("└── no pull request open or ended in the last day names it\n") } for i, l := range lines { branch := "├── " if i == len(lines)-1 { branch = "└── " } b.WriteString(branch + l + "\n") } p.Tree = strings.TrimRight(b.String(), "\n") return nil } // progressRecord says what builds a decision record or a research effort: the designs that name it and the issues // linked to it. func (k *KB) progressRecord(p *Progress, kind string, n int) error { key := Key{KindDecision, n} name := fmt.Sprintf("ADR %04d", n) if kind == "research" { key, name = Key{KindResearch, n}, fmt.Sprintf("research %03d", n) } g, err := k.Get(GetAsk{Kind: key.Kind, Number: key.Number, Bound: LeastBound}) if err != nil { return err } ds, commit, err := k.designs(true, 0) if err != nil { return err } p.Source = fmt.Sprintf("the knowledge base, the designs at hq %s and the issue tracker's links", short(commit)) p.Warnings = append(p.Warnings, unreadWarnings(ds)...) var designs []string for _, d := range ds { for _, dn := range d.Decisions { if dn == name { designs = append(designs, fmt.Sprintf("design %d (%s)", d.Number, d.Status)) } } } var kids []Item if kind == "ADR" { raw, err := k.Seats("seat:issue-tracker.list", map[string]any{"linked": fmt.Sprintf("decision:%04d", n), "limit": MostIssuesAsked}) if err != nil { return fmt.Errorf("the issue tracker does not answer: %v", err) } var l struct { Rows []trackerRow `json:"rows"` Matched int `json:"matched"` } if err := json.Unmarshal(raw, &l); err != nil { return fmt.Errorf("issue-tracker.list answered something else: %v", err) } if l.Matched > len(l.Rows) { p.Warnings = append(p.Warnings, fmt.Sprintf("%d issues are linked to %s; the first %d are counted", l.Matched, name, len(l.Rows))) } for _, r := range l.Rows { if p.scope != nil { p.scope.rows[r.Number] = r } if it, counts := issueItem(r); counts { kids = append(kids, it) } } } top := Item{ID: name, Title: g.Title} rollUp(&top, kids) top.Children = kids if len(kids) == 0 { top.Why = "no issue is linked to it" } p.State = top.State p.Items = []Item{top} var b strings.Builder fmt.Fprintf(&b, "%s · %s — %s\n", name, g.Title, g.Status) for _, x := range g.Retracted { fmt.Fprintf(&b, "├── retracted %s by %s at %s: %s; not in force\n", x.What, x.By, x.At, x.Why) } if g.Retracting != "" { b.WriteString("├── " + g.Retracting + "\n") } if len(designs) == 0 { b.WriteString("├── no design names it\n") } else { fmt.Fprintf(&b, "├── named by %s\n", strings.Join(designs, ", ")) } if kind != "ADR" { b.WriteString("└── issues link decisions, not research efforts: see what it became (knowledge-base.links)\n") } else if len(kids) == 0 { b.WriteString("└── no issue is linked to it (issue-tracker.link, kind decision)\n") } else { fmt.Fprintf(&b, "└── %s %d of %d linked issues done%s\n", marks[top.State], top.Done, top.Total, movedSay(top.Moved)) if p.Detail == DetailDetailed { for i, c := range kids { branch := " ├── " if i == len(kids)-1 { branch = " └── " } fmt.Fprintf(&b, "%s%s %s %s — %s%s\n", branch, marks[c.State], c.ID, c.Title, c.Why, movedSay(c.Moved)) } } } p.Tree = strings.TrimRight(b.String(), "\n") return nil } // extras is what a detailed answer reads beyond the rows: the open deliveries with their issues, the groups, and // each issue not done read whole. type extras struct { deliveries []deliveryRow groups map[string]groupRow gets map[int]*issueGot warnings []string } type issueGot struct { notes int last string waitsFor []string } // final says a delivery's state says nothing more will happen to it. func final(state string) bool { return state == "merged" || state == "closed" || state == "delivered" } // readExtras reads the deliveries not final with the issues each names, the groups, and the issues not done. func (k *KB) readExtras(items []Item) (*extras, error) { x := &extras{groups: map[string]groupRow{}, gets: map[int]*issueGot{}} raw, err := k.Seats("seat:mesh-delivery.deliveries", map[string]any{}) if err != nil { return nil, fmt.Errorf("mesh-delivery does not answer: %v", err) } var dl struct { Deliveries []deliveryRow `json:"deliveries"` } if err := json.Unmarshal(raw, &dl); err != nil { return nil, fmt.Errorf("mesh-delivery.deliveries answered something else: %v", err) } shown := 0 for _, d := range dl.Deliveries { if final(d.State) { continue } if shown == MostShows { x.warnings = append(x.warnings, fmt.Sprintf("more than %d deliveries are open; the rest are not read", MostShows)) break } shown++ raw, err := k.Seats("seat:mesh-delivery.show", map[string]any{"id": d.ID}) if err != nil { x.warnings = append(x.warnings, fmt.Sprintf("mesh-delivery.show %s: %v", d.ID, err)) continue } var s struct { Delivery struct { Issues []int `json:"issues"` Title string `json:"title"` } `json:"delivery"` } if err := json.Unmarshal(raw, &s); err != nil { x.warnings = append(x.warnings, fmt.Sprintf("mesh-delivery.show %s answered something else: %v", d.ID, err)) } d.Issues, d.Title = s.Delivery.Issues, s.Delivery.Title x.deliveries = append(x.deliveries, d) } raw, err = k.Seats("seat:mesh-delivery.groups", map[string]any{}) if err != nil { return nil, fmt.Errorf("mesh-delivery does not answer: %v", err) } var gs []groupRow if err := json.Unmarshal(raw, &gs); err != nil { return nil, fmt.Errorf("mesh-delivery.groups answered something else: %v", err) } for _, g := range gs { x.groups[g.ID] = g } got := 0 for _, m := range items { for _, c := range m.Children { if c.State == StateDone { continue } n, _ := strconv.Atoi(c.ID) if got == MostGets { x.warnings = append(x.warnings, fmt.Sprintf("more than %d issues are not done; the rest are not read whole", MostGets)) return x, nil } got++ raw, err := k.Seats("seat:issue-tracker.get", map[string]any{"number": n, "brief": true}) if err != nil { x.warnings = append(x.warnings, fmt.Sprintf("issue-tracker.get %d: %v", n, err)) continue } var g struct { Notes []struct { At time.Time `json:"at"` By string `json:"by"` Text string `json:"text"` } `json:"notes"` Left int `json:"notes-left-out"` Links []struct { Kind string `json:"kind"` Target string `json:"target"` } `json:"links"` } if err := json.Unmarshal(raw, &g); err != nil { x.warnings = append(x.warnings, fmt.Sprintf("issue-tracker.get %d answered something else: %v", n, err)) continue } ig := &issueGot{notes: len(g.Notes) + g.Left, last: "none"} if len(g.Notes) > 0 { last := g.Notes[len(g.Notes)-1] ig.last = fmt.Sprintf("%s by %s", when(&last.At), last.By) } for _, l := range g.Links { if l.Kind == "depends-on" { ig.waitsFor = append(ig.waitsFor, l.Target) } } x.gets[n] = ig } } return x, nil } // pullLines is one line per open delivery naming issue n, and per group it is in. func (x *extras) pullLines(n int) []string { if x == nil { return nil } var out []string groups := map[string]bool{} for _, d := range x.deliveries { named := false for _, i := range d.Issues { if i == n { named = true } } if !named { continue } state := StateWaiting if strings.HasPrefix(d.State, "held") || strings.Contains(d.Waits, "a person") { state = StateStuck } else if d.State == "checking" || d.State == "merging" || d.State == "delivering" { state = StateMoving } line := fmt.Sprintf("%s %s %s since %s", marks[state], d.ID, d.State, when(&d.Since)) if d.Waits != "" { line += ", waits for " + d.Waits } out = append(out, line) if d.Group != "" { groups[d.Group] = true } } var gids []string for g := range groups { gids = append(gids, g) } sort.Strings(gids) for _, id := range gids { g, ok := x.groups[id] if !ok { continue } mark := marks[StateMoving] if g.State == "held" { mark = marks[StateStuck] } out = append(out, fmt.Sprintf("%s group %s %s: %s", mark, g.ID, g.State, g.Why)) } return out } // renderTree draws a design's or a milestone's tree: minimal, one line per milestone with the done ones folded into // one line; detailed, each milestone not done with its issues, their pull requests and groups. func renderTree(top Item, detail string, x *extras, oneMilestone bool) string { var b strings.Builder if oneMilestone { fmt.Fprintf(&b, "%s %s · %s — %d of %d done%s\n", marks[top.State], top.ID, top.Title, top.Done, top.Total, movedSay(top.Moved)) writeIssues(&b, "", top.Children, detail, x) return strings.TrimRight(b.String(), "\n") } fmt.Fprintf(&b, "%s %s · %s — %d of %d milestones done%s\n", marks[top.State], top.ID, top.Title, top.Done, top.Total, movedSay(top.Moved)) var done []string issuesDone := 0 var open []Item for _, m := range top.Children { if m.State == StateDone { done = append(done, m.ID) issuesDone += m.Total continue } open = append(open, m) } if len(done) > 0 { branch := "├── " if len(open) == 0 { branch = "└── " } fmt.Fprintf(&b, "%s%s %d milestones done (%d issues): %s\n", branch, marks[StateDone], len(done), issuesDone, strings.Join(done, ", ")) } for i, m := range open { branch, under := "├── ", "│ " if i == len(open)-1 { branch, under = "└── ", " " } left := notDone(m.Children) if m.Total == 0 { left = m.Why } if detail == DetailMinimal { fmt.Fprintf(&b, "%s%s %s %d/%d %s%s\n", branch, marks[m.State], m.ID, m.Done, m.Total, left, movedSay(m.Moved)) continue } fmt.Fprintf(&b, "%s%s %s · %s — %d of %d done%s\n", branch, marks[m.State], m.ID, clip(m.Title, 90), m.Done, m.Total, movedSay(m.Moved)) writeIssues(&b, under, m.Children, detail, x) } return strings.TrimRight(b.String(), "\n") } // writeIssues draws a milestone's issues: minimal, those not done; detailed, every issue, and under each not done // its pull requests, groups and what it depends on. func writeIssues(b *strings.Builder, indent string, kids []Item, detail string, x *extras) { var show []Item for _, c := range kids { if detail == DetailDetailed || c.State != StateDone { show = append(show, c) } } for i, c := range show { branch, under := "├── ", "│ " if i == len(show)-1 { branch, under = "└── ", " " } fmt.Fprintf(b, "%s%s%s %s %s — %s%s\n", indent, branch, marks[c.State], c.ID, clip(c.Title, 70), c.Why, movedSay(c.Moved)) if detail != DetailDetailed || c.State == StateDone || x == nil { continue } n, _ := strconv.Atoi(c.ID) var lines []string if g := x.gets[n]; g != nil { if len(g.waitsFor) > 0 { lines = append(lines, "depends on "+strings.Join(g.waitsFor, ", ")) } lines = append(lines, fmt.Sprintf("%d notes; last %s", g.notes, g.last)) } lines = append(lines, x.pullLines(n)...) for j, l := range lines { br := "├── " if j == len(lines)-1 { br = "└── " } fmt.Fprintf(b, "%s%s%s%s\n", indent, under, br, l) } } } // notDone is a short say of the issues of a milestone not done, for its one line. func notDone(kids []Item) string { var parts []string for _, c := range kids { if c.State != StateDone { parts = append(parts, c.ID+" "+c.Why) } } if len(parts) > 3 { return fmt.Sprintf("%s; and %d more", strings.Join(parts[:3], "; "), len(parts)-3) } return strings.Join(parts, "; ") } // when is a time as the operator reads it: local, its day and its hour. func when(t *time.Time) string { if t == nil || t.IsZero() { return "an unknown time" } return t.Local().Format("01-02 15:04") } func movedSay(t *time.Time) string { if t == nil { return "" } return " · moved " + when(t) } // callsFirst puts the warnings of calls that failed before those of answers that did not parse, in place. func callsFirst(w []string) { sort.SliceStable(w, func(i, j int) bool { return !strings.Contains(w[i], " answered something else") && strings.Contains(w[j], " answered something else") }) } // fitProgress keeps an answer within its bound: the items go first, then the tree's last lines, saying what was cut. // A minimal answer never carries the items: its tree says the same, one line per milestone, so leaving them out is how // the minimal form is made (the items of design 47 alone passed 8192 bytes) and no cut. A detailed answer's items, and // everything else that goes, are said in cut. func fitProgress(p *Progress, bound int) { if p.Detail == DetailMinimal && p.Tree != "" { p.Items = nil } if size(p) <= bound { return } const raise = ": raise bound (at most 65536) or ask a smaller of, such as one milestone" p.Cut = "the items, some warnings and the tree's last lines" + raise var cut []string if len(p.Items) > 0 { p.Items = nil cut = append(cut, "the items (the tree says the same)") } warned := len(p.Warnings) for size(p) > bound && len(p.Warnings) > 0 { p.Warnings = p.Warnings[:len(p.Warnings)-1] } if len(p.Warnings) < warned { cut = append(cut, fmt.Sprintf("the last %d of %d warnings", warned-len(p.Warnings), warned)) } lines := strings.Split(p.Tree, "\n") dropped := 0 for size(p) > bound && len(lines) > 1 { lines = lines[:len(lines)-1] dropped++ p.Tree = strings.Join(lines, "\n") } if dropped > 0 { cut = append(cut, fmt.Sprintf("the tree's last %d lines", dropped)) } if exact := strings.Join(cut, "; ") + raise; len(exact) <= len(p.Cut) { p.Cut = exact } }