a code review tool

feat(tui): track review completion and show green diamond for approved changes

+64 -27
+21 -7
lib/tui/log_view.ml
··· 18 18 ~change_prefix_len 19 19 ~commit_prefix_len 20 20 ~selected 21 - ~review_count 21 + ~pending_reviews 22 + ~total_reviews 22 23 = 23 24 let change = row.change in 24 25 let c color = Catppuccin.color ~flavor color in ··· 28 29 let space = text [] " " in 29 30 let is_active col = col >= 0 && col < Array.length row.active && row.active.(col) in 30 31 (* Node character *) 32 + let fully_reviewed = total_reviews > 0 && pending_reviews = 0 in 31 33 let node = 32 34 if change.current_working_copy 33 35 then text [ Attr.fg (c Green); Attr.bold ] "@" 36 + else if fully_reviewed 37 + then text [ Attr.fg (c Green) ] "\xe2\x97\x86" 34 38 else if change.immutable 35 39 then text [ Attr.fg (c Overlay1) ] "\xe2\x97\x86" 36 40 else text [ Attr.fg (c Overlay1) ] "\xe2\x97\x8b" ··· 71 75 else text [ Attr.fg (c Mauve) ] (" " ^ change.bookmarks) 72 76 in 73 77 let review_badge = 74 - if review_count > 0 78 + if pending_reviews > 0 75 79 then 76 80 text 77 81 [ Attr.fg (c Blue) ] 78 - (sprintf " [%d review%s]" review_count (if review_count = 1 then "" else "s")) 82 + (sprintf " [%d review%s]" pending_reviews (if pending_reviews = 1 then "" else "s")) 79 83 else View.none 80 84 in 81 85 let line1 = ··· 163 167 (List.map changes ~f:(fun (c : Jj.Change.t) -> 164 168 let open Lwt.Syntax in 165 169 let* reqs = Lens_backend.Store.pending_requests store c.change_id in 166 - Lwt.return (c.change_id, List.length reqs))))) 170 + let* apps = Lens_backend.Store.approvals store c.change_id in 171 + let total = List.length reqs in 172 + let pending = 173 + List.count reqs ~f:(fun (r : Lens_backend.Store.Request.t) -> 174 + not 175 + (List.exists apps ~f:(fun (a : Lens_backend.Store.Approval.t) -> 176 + User_id.equal a.user_id r.user_id 177 + && Commit_id.equal a.commit_id c.commit_id))) 178 + in 179 + Lwt.return (c.change_id, (pending, total)))))) 167 180 graph 168 181 in 169 182 let refresh_reviews = ··· 190 203 and flavor = Catppuccin.flavor graph in 191 204 let change_views = 192 205 List.mapi rows ~f:(fun i (row : Graph_layout.Row.t) -> 193 - let review_count = 206 + let pending_reviews, total_reviews = 194 207 List.Assoc.find review_counts ~equal:Change_id.equal row.change.change_id 195 - |> Option.value ~default:0 208 + |> Option.value ~default:(0, 0) 196 209 in 197 210 render_change_line 198 211 ~flavor ··· 210 223 changes 211 224 ~f:Jj.Change.commit_id) 212 225 ~selected:(i = selected_idx) 213 - ~review_count) 226 + ~pending_reviews 227 + ~total_reviews) 214 228 in 215 229 View.vcat change_views 216 230 in
+43 -20
lib/tui/review_view.ml
··· 7 7 ; refresh : unit Effect.t Bonsai.t 8 8 } 9 9 10 - let render_reviews 11 - ~(flavor : Catppuccin.Flavor.t) 12 - (requests : Lens_backend.Store.Request.t list) 13 - = 10 + type review_status = 11 + { user_id : User_id.t 12 + ; approved : bool 13 + } 14 + 15 + let render_reviews ~(flavor : Catppuccin.Flavor.t) (statuses : review_status list) = 14 16 let c color = Catppuccin.color ~flavor color in 15 - match requests with 16 - | [] -> View.text ~attrs:[ Attr.fg (c Subtext0) ] "No pending reviews" 17 - | requests -> 18 - let reviewers = 19 - List.map requests ~f:(fun (r : Lens_backend.Store.Request.t) -> 20 - Lens_backend.User_id.to_string r.user_id) 21 - |> String.concat ~sep:", " 17 + match statuses with 18 + | [] -> View.text ~attrs:[ Attr.fg (c Subtext0) ] "No review requests" 19 + | statuses -> 20 + let reviewer_views = 21 + List.mapi statuses ~f:(fun i status -> 22 + let name = User_id.to_string status.user_id in 23 + let sep = if i > 0 then [ View.text ~attrs:[ Attr.fg (c Subtext0) ] ", " ] else [] in 24 + let reviewer = 25 + if status.approved 26 + then 27 + [ View.text ~attrs:[ Attr.fg (c Green) ] name 28 + ; View.text ~attrs:[ Attr.fg (c Green) ] " \xe2\x9c\x93" 29 + ] 30 + else [ View.text ~attrs:[ Attr.fg (c Mauve) ] name ] 31 + in 32 + sep @ reviewer) 33 + |> List.concat 22 34 in 23 35 View.hcat 24 - [ View.text ~attrs:[ Attr.fg (c Subtext0) ] "Pending reviews: " 25 - ; View.text ~attrs:[ Attr.fg (c Mauve) ] reviewers 26 - ] 36 + ([ View.text ~attrs:[ Attr.fg (c Subtext0) ] "Reviews: " ] @ reviewer_views) 27 37 ;; 28 38 29 39 let component ~selected_change ~(store : Lens_backend.Store.t) (graph @ local) = ··· 32 42 let%arr selected_change and generation in 33 43 selected_change, generation 34 44 in 35 - let pending_requests = 45 + let review_data = 36 46 Bonsai.Edge.Poll.effect_on_change 37 47 Bonsai.Edge.Poll.Starting.empty 38 48 poll_input ··· 48 58 Effect.of_lwt_thunk (fun () -> 49 59 Lens_backend.Store.pending_requests store change.change_id) 50 60 in 51 - return (Some reqs)) 61 + let%bind apps = 62 + Effect.of_lwt_thunk (fun () -> 63 + Lens_backend.Store.approvals store change.change_id) 64 + in 65 + let statuses = 66 + List.map reqs ~f:(fun (r : Lens_backend.Store.Request.t) -> 67 + let approved = 68 + List.exists apps ~f:(fun (a : Lens_backend.Store.Approval.t) -> 69 + User_id.equal a.user_id r.user_id 70 + && Commit_id.equal a.commit_id change.commit_id) 71 + in 72 + { user_id = r.user_id; approved }) 73 + in 74 + return (Some statuses)) 52 75 graph 53 76 in 54 77 let view = 55 - match%sub pending_requests with 78 + match%sub review_data with 56 79 | None -> Bonsai.return View.none 57 80 | Some None -> Bonsai.return View.none 58 - | Some (Some requests) -> 59 - let%arr requests 81 + | Some (Some statuses) -> 82 + let%arr statuses 60 83 and flavor = Catppuccin.flavor graph in 61 - render_reviews ~flavor requests 84 + render_reviews ~flavor statuses 62 85 in 63 86 let refresh = 64 87 let%arr set_generation in