terminal user interface to jujutsu. Focused on speed and clarity
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix node rendering bug showing extra lines

+772 -97
+2 -70
jj_tui/bin/graph_view.ml
··· 18 18 19 19 (* Use the library's render function for commit content *) 20 20 let render_commit_content = Commit_render.render_commit_content 21 - 22 - (** Group rows by their owning node. Each group is (node_row, continuation_rows). 23 - Each NodeRow starts a new group containing it and all following non-NodeRows until 24 - the next NodeRow. *) 25 - let group_rows_by_node (rows : Render_jj_graph.graph_row_output list) : 26 - (Render_jj_graph.graph_row_output * Render_jj_graph.graph_row_output list) list 27 - = 28 - let open Render_jj_graph in 29 - let rec loop acc current_group = function 30 - | [] -> 31 - List.rev (match current_group with Some g -> g :: acc | None -> acc) 32 - | row :: rest -> 33 - (match row.row_type with 34 - | NodeRow -> 35 - let acc = match current_group with Some g -> g :: acc | None -> acc in 36 - loop acc (Some (row, [])) rest 37 - | _ -> 38 - (match current_group with 39 - | Some (node_row, conts) -> 40 - loop acc (Some (node_row, conts @ [ row ])) rest 41 - | None -> 42 - (* Orphan row, shouldn't happen, but skip it *) 43 - loop acc None rest)) 44 - in 45 - loop [] None rows 46 - ;; 47 - 48 - (** Render a node group by distributing content lines across available rows. 49 - Returns a list of (row, rendered_image) pairs. *) 50 - let render_node_group 51 - ((node_row, continuation_rows) : 52 - Render_jj_graph.graph_row_output * Render_jj_graph.graph_row_output list) 53 - ~(render_content : Render_jj_graph.node -> Notty.image list) : 54 - (Render_jj_graph.graph_row_output * Notty.image) list 55 - = 56 - let open Notty in 57 - let open Render_jj_graph in 58 - let content_lines = render_content node_row.node in 59 - let available_rows = node_row :: continuation_rows in 60 - let result = ref [] in 61 - (* Distribute content lines across available rows *) 62 - List.iteri 63 - (fun i row -> 64 - let graph_img = row.graph_image in 65 - let combined = 66 - if i < List.length content_lines 67 - then I.hcat [ graph_img; List.nth content_lines i ] 68 - else graph_img 69 - in 70 - result := (row, combined) :: !result) 71 - available_rows; 72 - (* If content needs more lines than available, add synthetic continuation rows *) 73 - if List.length content_lines > List.length available_rows 74 - then ( 75 - let node_glyphs = [ "○"; "@"; "◌"; "◆"; "×" ] in 76 - let synthetic_graph = 77 - let chars = node_row.graph_chars in 78 - let replaced = ref chars in 79 - List.iter 80 - (fun glyph -> 81 - replaced := Str.global_replace (Str.regexp_string glyph) "│" !replaced) 82 - node_glyphs; 83 - I.string A.empty !replaced 84 - in 85 - for i = List.length available_rows to List.length content_lines - 1 do 86 - let line_img = List.nth content_lines i in 87 - result := (node_row, I.hcat [ synthetic_graph; line_img ]) :: !result 88 - done); 89 - List.rev !result 90 - ;; 21 + let group_rows_by_node = Graph_row_layout.group_rows_by_node 22 + let render_node_group = Graph_row_layout.render_node_group 91 23 92 24 let bookmark_select_prompt get_bookmark_list name func = 93 25 Selection_prompt
+164
jj_tui/lib/graph_row_layout.ml
··· 1 + open Notty 2 + 3 + (** Group rows by their owning node, preserving graph-only rows that can appear 4 + before a visible node after elision. Native jj emits those rows before the node, 5 + so the layout layer needs to keep that ordering instead of forcing everything to 6 + trail the node row. *) 7 + 8 + type node_group = { 9 + pre_rows : Render_jj_graph.graph_row_output list 10 + ; node_row : Render_jj_graph.graph_row_output 11 + ; continuation_rows : Render_jj_graph.graph_row_output list 12 + } 13 + 14 + let group_rows_by_node_raw (rows : Render_jj_graph.graph_row_output list) : 15 + node_group list 16 + = 17 + let open Render_jj_graph in 18 + let rec loop acc pending_rows current_group = function 19 + | [] -> 20 + let acc = match current_group with Some g -> g :: acc | None -> acc in 21 + List.rev acc 22 + | row :: rest -> 23 + (match row.row_type with 24 + | NodeRow -> 25 + let acc = match current_group with Some g -> g :: acc | None -> acc in 26 + loop 27 + acc 28 + [] 29 + (Some { pre_rows = pending_rows; node_row = row; continuation_rows = [] }) 30 + rest 31 + | _ -> 32 + (match current_group with 33 + | Some ({ continuation_rows; _ } as group) -> 34 + loop 35 + acc 36 + pending_rows 37 + (Some { group with continuation_rows = continuation_rows @ [ row ] }) 38 + rest 39 + | None -> 40 + (* Native jj can emit graph-only rows like `│` or `~` before the next 41 + visible node after an elision gap. Keep them and attach them before 42 + that node instead of dropping or reordering them. *) 43 + loop acc (pending_rows @ [ row ]) None rest)) 44 + in 45 + loop [] [] None rows 46 + ;; 47 + 48 + let contains_str s substr = 49 + try 50 + let _ = Str.search_forward (Str.regexp_string substr) s 0 in 51 + true 52 + with 53 + | Not_found -> 54 + false 55 + ;; 56 + 57 + let has_leading_branch_node (row : Render_jj_graph.graph_row_output) = 58 + (contains_str row.graph_chars "○" 59 + || contains_str row.graph_chars "@" 60 + || contains_str row.graph_chars "◌" 61 + || contains_str row.graph_chars "◆" 62 + || contains_str row.graph_chars "×") 63 + && contains_str row.graph_chars "│" 64 + ;; 65 + 66 + let is_plain_vertical_pad (row : Render_jj_graph.graph_row_output) = 67 + row.row_type = Render_jj_graph.PadRow && String.trim row.graph_chars = "│" 68 + ;; 69 + 70 + let is_branch_continuation_pad (row : Render_jj_graph.graph_row_output) = 71 + row.row_type = Render_jj_graph.PadRow && String.trim row.graph_chars = "│ │" 72 + ;; 73 + 74 + let normalize_join_rows (groups : node_group list) : node_group list = 75 + let rec loop acc = function 76 + | ({ continuation_rows = prev_conts; _ } as prev_group) 77 + :: ({ node_row = next_node; continuation_rows = next_conts; _ } as next_group) 78 + :: rest -> 79 + (match List.rev prev_conts with 80 + | (trailing_link : Render_jj_graph.graph_row_output) :: prev_rev_rest 81 + when trailing_link.row_type = Render_jj_graph.LinkRow 82 + && has_leading_branch_node next_node 83 + && List.exists is_plain_vertical_pad next_conts 84 + && not (List.exists is_branch_continuation_pad next_conts) -> 85 + let prev_group = 86 + { prev_group with continuation_rows = List.rev prev_rev_rest } 87 + in 88 + let next_group = 89 + { next_group with continuation_rows = trailing_link :: next_conts } 90 + in 91 + loop (prev_group :: acc) (next_group :: rest) 92 + | _ -> 93 + loop (prev_group :: acc) (next_group :: rest)) 94 + | [ group ] -> 95 + List.rev (group :: acc) 96 + | [] -> 97 + List.rev acc 98 + in 99 + loop [] groups 100 + ;; 101 + 102 + let group_rows_by_node rows = rows |> group_rows_by_node_raw |> normalize_join_rows 103 + 104 + let render_node_group 105 + ({ pre_rows; node_row; continuation_rows } : node_group) 106 + ~(render_content : Render_jj_graph.node -> Notty.image list) : 107 + (Render_jj_graph.graph_row_output * Notty.image) list 108 + = 109 + let content_lines = render_content node_row.node in 110 + let content_rows, trailing_graph_only_rows = 111 + let available_rows = node_row :: continuation_rows in 112 + available_rows 113 + |> List.partition (fun (row : Render_jj_graph.graph_row_output) -> 114 + row.row_type <> Render_jj_graph.TermRow) 115 + in 116 + let result = ref [] in 117 + (* Distribute content lines across node/link/pad rows only. Term rows such as 118 + `~ (elided revisions)` must remain graph-only so commit descriptions stay on 119 + a vertical continuation instead of being glued to the elision marker. *) 120 + List.iteri 121 + (fun i (row : Render_jj_graph.graph_row_output) -> 122 + let combined = 123 + if i < List.length content_lines 124 + then I.hcat [ row.graph_image; List.nth content_lines i ] 125 + else row.graph_image 126 + in 127 + result := (row, combined) :: !result) 128 + content_rows; 129 + (* When native jj doesn't provide enough content-bearing rows for a two-line 130 + commit, keep the description visually attached by replacing the node glyph 131 + with a vertical line before appending any graph-only term rows. *) 132 + if List.length content_lines > List.length content_rows 133 + then ( 134 + let node_glyphs = [ "○"; "@"; "◌"; "◆"; "×" ] in 135 + let synthetic_graph = 136 + let chars = node_row.graph_chars in 137 + let replaced = ref chars in 138 + List.iter 139 + (fun glyph -> 140 + replaced := Str.global_replace (Str.regexp_string glyph) "│" !replaced) 141 + node_glyphs; 142 + I.string A.empty !replaced 143 + in 144 + for i = List.length content_rows to List.length content_lines - 1 do 145 + let line_img = List.nth content_lines i in 146 + result := (node_row, I.hcat [ synthetic_graph; line_img ]) :: !result 147 + done); 148 + pre_rows 149 + |> List.iter (fun (row : Render_jj_graph.graph_row_output) -> 150 + result := (row, row.graph_image) :: !result); 151 + trailing_graph_only_rows 152 + |> List.iter (fun (row : Render_jj_graph.graph_row_output) -> 153 + result := (row, row.graph_image) :: !result); 154 + let rendered_rows = List.rev !result in 155 + let is_node_row ((row, _img) : Render_jj_graph.graph_row_output * Notty.image) = 156 + row == node_row 157 + in 158 + match List.find_opt is_node_row rendered_rows with 159 + | None -> 160 + rendered_rows 161 + | Some node_entry -> 162 + let other_rows = List.filter (fun entry -> not (is_node_row entry)) rendered_rows in 163 + node_entry :: other_rows 164 + ;;
+27 -27
jj_tui/lib/process_wrappers.ml
··· 163 163 let native_graph_output ?revset limit = 164 164 let args = [ "log"; "-T"; native_graph_template; "--limit"; string_of_int limit ] in 165 165 let args = match revset with Some r -> args @ [ "-r"; r ] | None -> args in 166 - jj_no_log args ~color:false 166 + jj_no_log args 167 167 ;; 168 168 169 169 let line_before_marker line marker = ··· 182 182 search 0 183 183 ;; 184 184 185 - let make_graph_row_output ~graph_chars ~row_type () = 186 - let open Notty in 185 + let make_graph_row_output ~graph_raw ~graph_chars ~row_type () = 187 186 Render_jj_graph. 188 187 { 189 188 graph_chars 190 - ; graph_image = I.string A.empty graph_chars 189 + ; graph_image = AnsiReverse.colored_string graph_raw 191 190 ; node = Render_jj_graph.make_elided_node () 192 191 ; row_type 193 192 } ··· 202 201 List.fold_left 203 202 (fun (acc, current_group) line -> 204 203 match line_before_marker line node_row_marker with 205 - | Some graph_chars -> 204 + | Some graph_raw -> 206 205 let acc = flush_group current_group acc in 206 + let graph_chars = remove_ansi graph_raw in 207 207 let node_row = 208 - make_graph_row_output ~graph_chars ~row_type:Render_jj_graph.NodeRow () 208 + make_graph_row_output 209 + ~graph_raw 210 + ~graph_chars 211 + ~row_type:Render_jj_graph.NodeRow 212 + () 209 213 in 210 214 acc, Some { node_row; continuation_rows = [] } 211 215 | None -> 212 - let graph_chars = remove_ansi line in 213 - if graph_chars = "" 216 + let graph_raw = 217 + match line_before_marker line info_row_marker with 218 + | Some graph_raw -> 219 + graph_raw 220 + | None -> 221 + line 222 + in 223 + let graph_chars = remove_ansi graph_raw in 224 + if String.trim graph_chars = "" 214 225 then acc, current_group 215 226 else ( 216 - let row_type = 217 - match line_before_marker line info_row_marker with 218 - | Some _ -> 219 - Render_jj_graph.PadRow 220 - | None -> 221 - Render_jj_graph.classify_row_type graph_chars 222 - in 223 - let graph_chars = 224 - match line_before_marker line info_row_marker with 225 - | Some chars -> 226 - chars 227 - | None -> 228 - graph_chars 229 - in 227 + let row_type = Render_jj_graph.classify_row_type graph_chars in 230 228 match current_group with 231 229 | Some group -> 232 - let row = make_graph_row_output ~graph_chars ~row_type () in 230 + let row = make_graph_row_output ~graph_raw ~graph_chars ~row_type () in 233 231 ( acc 234 232 , Some 235 233 { group with continuation_rows = group.continuation_rows @ [ row ] } 236 234 ) 237 235 | None -> 238 - let node_row = make_graph_row_output ~graph_chars ~row_type () in 236 + let node_row = 237 + make_graph_row_output ~graph_raw ~graph_chars ~row_type () 238 + in 239 239 acc, Some { node_row; continuation_rows = [] })) 240 240 ([], None) 241 241 lines ··· 247 247 if List.length groups <> List.length nodes 248 248 then None 249 249 else ( 250 - let rows_rev = 250 + let rows = 251 251 List.fold_left2 252 252 (fun acc group node -> 253 253 let node_row : Render_jj_graph.graph_row_output = ··· 258 258 (fun (row : Render_jj_graph.graph_row_output) -> { row with node }) 259 259 group.continuation_rows 260 260 in 261 - List.rev_append (List.rev (node_row :: continuation_rows)) acc) 261 + acc @ (node_row :: continuation_rows)) 262 262 [] 263 263 groups 264 264 nodes 265 265 in 266 - Some (List.rev rows_rev)) 266 + Some rows) 267 267 ;; 268 268 269 269 let get_graph_info node_template revset_arg limit =
+579
jj_tui/test/lib/graph_row_grouping.ml
··· 1 + open Jj_tui 2 + 3 + module Test_native_graph = Process_wrappers.Make (struct 4 + let jj_no_log ?get_stderr:_ ?snapshot:_ ?color:_ _ = failwith "unused" 5 + end) 6 + 7 + (** These tests exercise the shared row-layout implementation used by graph_view so 8 + the post-elision branch regression is locked down end-to-end. *) 9 + 10 + let test_node ?(description = "desc") commit_id = 11 + let open Render_jj_graph in 12 + { 13 + parents = [] 14 + ; creation_time = Int64.zero 15 + ; working_copy = false 16 + ; immutable = false 17 + ; wip = false 18 + ; change_id = commit_id 19 + ; commit_id 20 + ; description 21 + ; bookmarks = [] 22 + ; author_email = "test@example.com" 23 + ; author_timestamp = "2024-01-01" 24 + ; empty = false 25 + ; hidden = false 26 + ; divergent = false 27 + ; conflict = false 28 + ; is_preview = false 29 + ; change_id_prefix = commit_id 30 + ; change_id_rest = "" 31 + ; commit_id_prefix = "deadbeef" 32 + ; commit_id_rest = "" 33 + } 34 + ;; 35 + 36 + let test_row ?(row_type = Render_jj_graph.PadRow) node graph_chars = 37 + Render_jj_graph. 38 + { 39 + graph_chars 40 + ; graph_image = Notty.I.string Notty.A.empty graph_chars 41 + ; node 42 + ; row_type 43 + } 44 + ;; 45 + 46 + let render_content_lines node = 47 + [ 48 + Notty.I.string 49 + Notty.A.empty 50 + (node.Render_jj_graph.change_id 51 + ^ " test@example.com 2024-01-01 " 52 + ^ node.commit_id_prefix) 53 + ; Notty.I.string Notty.A.empty node.Render_jj_graph.description 54 + ] 55 + ;; 56 + 57 + let render_image_to_string img = 58 + let trim_right s = 59 + let rec last_non_space i = 60 + if i < 0 61 + then -1 62 + else if Char.equal s.[i] ' ' || Char.equal s.[i] '\n' || Char.equal s.[i] '\r' 63 + then last_non_space (i - 1) 64 + else i 65 + in 66 + let last = last_non_space (String.length s - 1) in 67 + if last < 0 then "" else String.sub s 0 (last + 1) 68 + in 69 + let buf = Buffer.create 256 in 70 + Notty.Render.to_buffer buf Notty.Cap.dumb (0, 0) (120, 1) img; 71 + Buffer.contents buf |> trim_right 72 + ;; 73 + 74 + let print_group i (group : Graph_row_layout.node_group) = 75 + let node_row : Render_jj_graph.graph_row_output = group.node_row in 76 + let pre_rows : Render_jj_graph.graph_row_output list = group.pre_rows in 77 + let continuation_rows : Render_jj_graph.graph_row_output list = 78 + group.continuation_rows 79 + in 80 + Printf.printf "Group %d node=%s\n" i node_row.node.commit_id; 81 + pre_rows 82 + |> List.iter (fun (row : Render_jj_graph.graph_row_output) -> 83 + Printf.printf " pre=%S\n" row.graph_chars); 84 + Printf.printf " head=%S\n" node_row.graph_chars; 85 + continuation_rows 86 + |> List.iter (fun (row : Render_jj_graph.graph_row_output) -> 87 + Printf.printf " cont=%S\n" row.graph_chars) 88 + ;; 89 + 90 + let rows_with_join_before_child () = 91 + let top = 92 + test_node ~description:"ensure commit actually puts the rev in the right place" "top" 93 + in 94 + let child = test_node ~description:"show conflicts correctly" "child" in 95 + [ 96 + test_row ~row_type:Render_jj_graph.NodeRow top "◆ " 97 + ; test_row ~row_type:Render_jj_graph.LinkRow top "├─╯ " 98 + ; test_row ~row_type:Render_jj_graph.NodeRow child "│ ○ " 99 + ; test_row ~row_type:Render_jj_graph.PadRow child "│ " 100 + ] 101 + ;; 102 + 103 + let rows_with_elided_termination_case () = 104 + let mmnxzuyv = test_node ~description:"show conflicts correctly" "mmnxzuyv" in 105 + let upnslvuv = 106 + test_node ~description:"make bookmarks render origin if need" "upnslvuv" 107 + in 108 + let upnslvuv_2 = 109 + test_node ~description:"make bookmarks render origin if needed" "upnslvuv/2" 110 + in 111 + let lpztppmx = 112 + test_node ~description:"fix elided revisions bug during rebase" "lpztppmx" 113 + in 114 + let vxkltmxw = test_node ~description:"(empty) (no description set)" "vxkltmxw" in 115 + let mwqvkttl = test_node ~description:"(empty) (no description set)" "mwqvkttl" in 116 + let qqsnmuzr_2 = 117 + test_node 118 + ~description:"fix preview mode bugs (I think this broke the update loop somehow)" 119 + "qqsnmuzr/2" 120 + in 121 + let tkozwuzw = test_node ~description:"enable preview mode (5.2 codex)" "tkozwuzw" in 122 + let nkwwwlnw = test_node ~description:"rewrite" "nkwwwlnw" in 123 + let voywlxnk = test_node ~description:"(no description set)" "voywlxnk" in 124 + let wrrtuusr = test_node ~description:"(no description set)" "wrrtuusr" in 125 + let vvnqynuv = test_node ~description:"(no description set)" "vvnqynuv" in 126 + let noszsqtm = 127 + test_node 128 + ~description:"remove aarch64 linux because it doesn't seem to work" 129 + "noszsqtm" 130 + in 131 + [ 132 + test_row ~row_type:Render_jj_graph.NodeRow mmnxzuyv "│ ○ " 133 + ; test_row ~row_type:Render_jj_graph.LinkRow mmnxzuyv "├─╯ " 134 + ; test_row ~row_type:Render_jj_graph.NodeRow upnslvuv "◆ " 135 + ; test_row ~row_type:Render_jj_graph.PadRow upnslvuv "│ " 136 + ; test_row ~row_type:Render_jj_graph.NodeRow upnslvuv_2 "│ ○ " 137 + ; test_row ~row_type:Render_jj_graph.LinkRow upnslvuv_2 "├─╯ " 138 + ; test_row ~row_type:Render_jj_graph.NodeRow lpztppmx "◆ " 139 + ; test_row ~row_type:Render_jj_graph.PadRow lpztppmx "│ " 140 + ; test_row ~row_type:Render_jj_graph.TermRow lpztppmx "~ (elided revisions)" 141 + ; test_row ~row_type:Render_jj_graph.NodeRow vxkltmxw "│ ○ " 142 + ; test_row ~row_type:Render_jj_graph.PadRow vxkltmxw "│ │ " 143 + ; test_row ~row_type:Render_jj_graph.NodeRow mwqvkttl "│ ○ " 144 + ; test_row ~row_type:Render_jj_graph.LinkRow mwqvkttl "├─╯ " 145 + ; test_row ~row_type:Render_jj_graph.NodeRow qqsnmuzr_2 "│ ○ " 146 + ; test_row ~row_type:Render_jj_graph.LinkRow qqsnmuzr_2 "├─╯ " 147 + ; test_row ~row_type:Render_jj_graph.NodeRow tkozwuzw "◆ " 148 + ; test_row ~row_type:Render_jj_graph.PadRow tkozwuzw "│ " 149 + ; test_row ~row_type:Render_jj_graph.TermRow tkozwuzw "~ (elided revisions)" 150 + ; test_row ~row_type:Render_jj_graph.NodeRow nkwwwlnw "│ ○ " 151 + ; test_row ~row_type:Render_jj_graph.PadRow nkwwwlnw "│ │ " 152 + ; test_row ~row_type:Render_jj_graph.NodeRow voywlxnk "│ ○ " 153 + ; test_row ~row_type:Render_jj_graph.PadRow voywlxnk "│ │ " 154 + ; test_row ~row_type:Render_jj_graph.NodeRow wrrtuusr "│ ○ " 155 + ; test_row ~row_type:Render_jj_graph.PadRow wrrtuusr "│ │ " 156 + ; test_row ~row_type:Render_jj_graph.NodeRow vvnqynuv "│ ○ " 157 + ; test_row ~row_type:Render_jj_graph.LinkRow vvnqynuv "├─╯ " 158 + ; test_row ~row_type:Render_jj_graph.NodeRow noszsqtm "◆ " 159 + ; test_row ~row_type:Render_jj_graph.PadRow noszsqtm "│ " 160 + ; test_row ~row_type:Render_jj_graph.TermRow noszsqtm "~" 161 + ] 162 + ;; 163 + 164 + let rows_with_synthetic_termination_case () = 165 + let mmnxzuyv = test_node ~description:"show conflicts correctly" "mmnxzuyv" in 166 + let upnslvuv = 167 + test_node ~description:"make bookmarks render origin if need" "upnslvuv" 168 + in 169 + let upnslvuv_2 = 170 + test_node ~description:"make bookmarks render origin if needed" "upnslvuv/2" 171 + in 172 + let lpztppmx = 173 + test_node ~description:"fix elided revisions bug during rebase" "lpztppmx" 174 + in 175 + let vxkltmxw = test_node ~description:"(empty) (no description set)" "vxkltmxw" in 176 + let mwqvkttl = test_node ~description:"(empty) (no description set)" "mwqvkttl" in 177 + [ 178 + test_row ~row_type:Render_jj_graph.NodeRow mmnxzuyv "│ ○ " 179 + ; test_row ~row_type:Render_jj_graph.LinkRow mmnxzuyv "├─╯ " 180 + ; test_row ~row_type:Render_jj_graph.NodeRow upnslvuv "◆ " 181 + ; test_row ~row_type:Render_jj_graph.PadRow upnslvuv "│ " 182 + ; test_row ~row_type:Render_jj_graph.NodeRow upnslvuv_2 "│ ○ " 183 + ; test_row ~row_type:Render_jj_graph.LinkRow upnslvuv_2 "├─╯ " 184 + ; test_row ~row_type:Render_jj_graph.NodeRow lpztppmx "│ ◆ " 185 + ; test_row ~row_type:Render_jj_graph.TermRow lpztppmx "~ (elided revisions)" 186 + ; test_row ~row_type:Render_jj_graph.NodeRow vxkltmxw "│ ○ " 187 + ; test_row ~row_type:Render_jj_graph.PadRow vxkltmxw "│ │ " 188 + ; test_row ~row_type:Render_jj_graph.NodeRow mwqvkttl "│ ○ " 189 + ; test_row ~row_type:Render_jj_graph.LinkRow mwqvkttl "├─╯ " 190 + ] 191 + ;; 192 + 193 + let rows_with_extra_trailing_vertical_bug () = 194 + let lpztppmx = 195 + test_node ~description:"fix elided revisions bug during rebase" "lpztppmx" 196 + in 197 + let vxkltmxw = test_node ~description:"(empty) (no description set)" "vxkltmxw" in 198 + let mwqvkttl = test_node ~description:"(empty) (no description set)" "mwqvkttl" in 199 + let qqsnmuzr_2 = 200 + test_node 201 + ~description:"fix preview mode bugs (I think this broke the update loop somehow)" 202 + "qqsnmuzr/2" 203 + in 204 + let tkozwuzw = test_node ~description:"enable preview mode (5.2 codex)" "tkozwuzw" in 205 + let nkwwwlnw = test_node ~description:"rewrite" "nkwwwlnw" in 206 + let voywlxnk = test_node ~description:"(no description set)" "voywlxnk" in 207 + let wrrtuusr = test_node ~description:"(no description set)" "wrrtuusr" in 208 + let vvnqynuv = test_node ~description:"(no description set)" "vvnqynuv" in 209 + let noszsqtm = 210 + test_node 211 + ~description:"remove aarch64 linux because it doesn't seem to work" 212 + "noszsqtm" 213 + in 214 + [ 215 + test_row ~row_type:Render_jj_graph.PadRow lpztppmx "│ " 216 + ; test_row ~row_type:Render_jj_graph.TermRow lpztppmx "~ (elided revisions)" 217 + ; test_row ~row_type:Render_jj_graph.NodeRow lpztppmx "◆ " 218 + ; test_row ~row_type:Render_jj_graph.PadRow lpztppmx "│ │ " 219 + ; test_row ~row_type:Render_jj_graph.NodeRow vxkltmxw "│ ○ " 220 + ; test_row ~row_type:Render_jj_graph.LinkRow vxkltmxw "├─╯ " 221 + ; test_row ~row_type:Render_jj_graph.NodeRow mwqvkttl "│ ○ " 222 + ; test_row ~row_type:Render_jj_graph.PadRow mwqvkttl "│ │ " 223 + ; test_row ~row_type:Render_jj_graph.NodeRow qqsnmuzr_2 "│ ○ " 224 + ; test_row ~row_type:Render_jj_graph.LinkRow qqsnmuzr_2 "├─╯ " 225 + ; test_row ~row_type:Render_jj_graph.PadRow tkozwuzw "│ " 226 + ; test_row ~row_type:Render_jj_graph.TermRow tkozwuzw "~ (elided revisions)" 227 + ; test_row ~row_type:Render_jj_graph.NodeRow tkozwuzw "◆ " 228 + ; test_row ~row_type:Render_jj_graph.PadRow tkozwuzw "│ │ " 229 + ; test_row ~row_type:Render_jj_graph.NodeRow nkwwwlnw "│ ○ " 230 + ; test_row ~row_type:Render_jj_graph.PadRow nkwwwlnw "│ │ " 231 + ; test_row ~row_type:Render_jj_graph.NodeRow voywlxnk "│ ○ " 232 + ; test_row ~row_type:Render_jj_graph.PadRow voywlxnk "│ │ " 233 + ; test_row ~row_type:Render_jj_graph.NodeRow wrrtuusr "│ ○ " 234 + ; test_row ~row_type:Render_jj_graph.PadRow wrrtuusr "│ │ " 235 + ; test_row ~row_type:Render_jj_graph.NodeRow vvnqynuv "│ ○ " 236 + ; test_row ~row_type:Render_jj_graph.LinkRow vvnqynuv "├─╯ " 237 + ; test_row ~row_type:Render_jj_graph.PadRow noszsqtm "│ " 238 + ; test_row ~row_type:Render_jj_graph.TermRow noszsqtm "~" 239 + ; test_row ~row_type:Render_jj_graph.NodeRow noszsqtm "◆ " 240 + ; test_row ~row_type:Render_jj_graph.PadRow noszsqtm "│ " 241 + ] 242 + ;; 243 + 244 + let parsed_rows_with_extra_trailing_vertical_bug () = 245 + let lpztppmx = 246 + test_node ~description:"fix elided revisions bug during rebase" "lpztppmx" 247 + in 248 + let vxkltmxw = test_node ~description:"(empty) (no description set)" "vxkltmxw" in 249 + let mwqvkttl = test_node ~description:"(empty) (no description set)" "mwqvkttl" in 250 + let qqsnmuzr_2 = 251 + test_node 252 + ~description:"fix preview mode bugs (I think this broke the update loop somehow)" 253 + "qqsnmuzr/2" 254 + in 255 + let tkozwuzw = test_node ~description:"enable preview mode (5.2 codex)" "tkozwuzw" in 256 + let nkwwwlnw = test_node ~description:"rewrite" "nkwwwlnw" in 257 + let voywlxnk = test_node ~description:"(no description set)" "voywlxnk" in 258 + let wrrtuusr = test_node ~description:"(no description set)" "wrrtuusr" in 259 + let vvnqynuv = test_node ~description:"(no description set)" "vvnqynuv" in 260 + let noszsqtm = 261 + test_node 262 + ~description:"remove aarch64 linux because it doesn't seem to work" 263 + "noszsqtm" 264 + in 265 + let raw_output = 266 + {|◆ @@NODE@@ 267 + │ @@INFO@@ 268 + ~ (elided revisions) 269 + │ ○ @@NODE@@ 270 + │ │ @@INFO@@ 271 + │ ○ @@NODE@@ 272 + ├─╯ @@INFO@@ 273 + │ ○ @@NODE@@ 274 + ├─╯ @@INFO@@ 275 + ◆ @@NODE@@ 276 + │ @@INFO@@ 277 + ~ (elided revisions) 278 + │ ○ @@NODE@@ 279 + │ │ @@INFO@@ 280 + │ ○ @@NODE@@ 281 + │ │ @@INFO@@ 282 + │ ○ @@NODE@@ 283 + │ │ @@INFO@@ 284 + │ ○ @@NODE@@ 285 + ├─╯ @@INFO@@ 286 + ◆ @@NODE@@ 287 + │ @@INFO@@ 288 + ~|} 289 + in 290 + let nodes = 291 + [ 292 + lpztppmx 293 + ; vxkltmxw 294 + ; mwqvkttl 295 + ; qqsnmuzr_2 296 + ; tkozwuzw 297 + ; nkwwwlnw 298 + ; voywlxnk 299 + ; wrrtuusr 300 + ; vvnqynuv 301 + ; noszsqtm 302 + ] 303 + in 304 + raw_output 305 + |> Test_native_graph.parse_native_graph_groups 306 + |> Test_native_graph.attach_nodes_to_native_groups ~nodes 307 + |> Option.get 308 + ;; 309 + 310 + let%expect_test "grouping_repro_for_join_row_before_child_node" = 311 + let groups = rows_with_join_before_child () |> Graph_row_layout.group_rows_by_node in 312 + List.iteri print_group groups; 313 + [%expect 314 + {| 315 + Group 0 node=top 316 + head="\226\151\134 " 317 + Group 1 node=child 318 + head="\226\148\130 \226\151\139 " 319 + cont="\226\148\156\226\148\128\226\149\175 " 320 + cont="\226\148\130 " 321 + |}] 322 + ;; 323 + 324 + let%expect_test "rendering_repro_for_join_row_before_child_node" = 325 + let groups = rows_with_join_before_child () |> Graph_row_layout.group_rows_by_node in 326 + groups 327 + |> List.iter (fun group -> 328 + Graph_row_layout.render_node_group group ~render_content:render_content_lines 329 + |> List.iter (fun ((row : Render_jj_graph.graph_row_output), img) -> 330 + Printf.printf "%S => %S\n" row.graph_chars (render_image_to_string img))); 331 + [%expect 332 + {| 333 + "\226\151\134 " => "\226\151\134 top test@example.com 2024-01-01 deadbeef" 334 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 child test@example.com 2024-01-01 deadbeef" 335 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 show conflicts correctly" 336 + "\226\148\130 " => "\226\148\130" 337 + |}] 338 + ;; 339 + 340 + let%expect_test "grouping_repro_for_trailing_vertical_on_terminating_branch" = 341 + let groups = 342 + rows_with_elided_termination_case () |> Graph_row_layout.group_rows_by_node 343 + in 344 + List.iteri print_group groups; 345 + [%expect 346 + {| 347 + Group 0 node=mmnxzuyv 348 + head="\226\148\130 \226\151\139 " 349 + cont="\226\148\156\226\148\128\226\149\175 " 350 + Group 1 node=upnslvuv 351 + head="\226\151\134 " 352 + cont="\226\148\130 " 353 + Group 2 node=upnslvuv/2 354 + head="\226\148\130 \226\151\139 " 355 + cont="\226\148\156\226\148\128\226\149\175 " 356 + Group 3 node=lpztppmx 357 + head="\226\151\134 " 358 + cont="\226\148\130 " 359 + cont="~ (elided revisions)" 360 + Group 4 node=vxkltmxw 361 + head="\226\148\130 \226\151\139 " 362 + cont="\226\148\130 \226\148\130 " 363 + Group 5 node=mwqvkttl 364 + head="\226\148\130 \226\151\139 " 365 + cont="\226\148\156\226\148\128\226\149\175 " 366 + Group 6 node=qqsnmuzr/2 367 + head="\226\148\130 \226\151\139 " 368 + cont="\226\148\156\226\148\128\226\149\175 " 369 + Group 7 node=tkozwuzw 370 + head="\226\151\134 " 371 + cont="\226\148\130 " 372 + cont="~ (elided revisions)" 373 + Group 8 node=nkwwwlnw 374 + head="\226\148\130 \226\151\139 " 375 + cont="\226\148\130 \226\148\130 " 376 + Group 9 node=voywlxnk 377 + head="\226\148\130 \226\151\139 " 378 + cont="\226\148\130 \226\148\130 " 379 + Group 10 node=wrrtuusr 380 + head="\226\148\130 \226\151\139 " 381 + cont="\226\148\130 \226\148\130 " 382 + Group 11 node=vvnqynuv 383 + head="\226\148\130 \226\151\139 " 384 + cont="\226\148\156\226\148\128\226\149\175 " 385 + Group 12 node=noszsqtm 386 + head="\226\151\134 " 387 + cont="\226\148\130 " 388 + cont="~" 389 + |}] 390 + ;; 391 + 392 + let%expect_test "rendering_repro_for_trailing_vertical_on_terminating_branch" = 393 + let groups = 394 + rows_with_elided_termination_case () |> Graph_row_layout.group_rows_by_node 395 + in 396 + groups 397 + |> List.iter (fun group -> 398 + Graph_row_layout.render_node_group group ~render_content:render_content_lines 399 + |> List.iter (fun ((row : Render_jj_graph.graph_row_output), img) -> 400 + Printf.printf "%S => %S\n" row.graph_chars (render_image_to_string img))); 401 + [%expect 402 + {| 403 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 mmnxzuyv test@example.com 2024-01-01 deadbeef" 404 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 show conflicts correctly" 405 + "\226\151\134 " => "\226\151\134 upnslvuv test@example.com 2024-01-01 deadbeef" 406 + "\226\148\130 " => "\226\148\130 make bookmarks render origin if need" 407 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 upnslvuv/2 test@example.com 2024-01-01 deadbeef" 408 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 make bookmarks render origin if needed" 409 + "\226\151\134 " => "\226\151\134 lpztppmx test@example.com 2024-01-01 deadbeef" 410 + "\226\148\130 " => "\226\148\130 fix elided revisions bug during rebase" 411 + "~ (elided revisions)" => "~ (elided revisions)" 412 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 vxkltmxw test@example.com 2024-01-01 deadbeef" 413 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (empty) (no description set)" 414 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 mwqvkttl test@example.com 2024-01-01 deadbeef" 415 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 (empty) (no description set)" 416 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 qqsnmuzr/2 test@example.com 2024-01-01 deadbeef" 417 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 fix preview mode bugs (I think this broke the update loop somehow)" 418 + "\226\151\134 " => "\226\151\134 tkozwuzw test@example.com 2024-01-01 deadbeef" 419 + "\226\148\130 " => "\226\148\130 enable preview mode (5.2 codex)" 420 + "~ (elided revisions)" => "~ (elided revisions)" 421 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 nkwwwlnw test@example.com 2024-01-01 deadbeef" 422 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 rewrite" 423 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 voywlxnk test@example.com 2024-01-01 deadbeef" 424 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (no description set)" 425 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 wrrtuusr test@example.com 2024-01-01 deadbeef" 426 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (no description set)" 427 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 vvnqynuv test@example.com 2024-01-01 deadbeef" 428 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 (no description set)" 429 + "\226\151\134 " => "\226\151\134 noszsqtm test@example.com 2024-01-01 deadbeef" 430 + "\226\148\130 " => "\226\148\130 remove aarch64 linux because it doesn't seem to work" 431 + "~" => "~" 432 + |}] 433 + ;; 434 + 435 + let%expect_test "rendering_repro_for_synthetic_terminating_branch_with_extra_vertical" = 436 + let groups = 437 + rows_with_synthetic_termination_case () |> Graph_row_layout.group_rows_by_node 438 + in 439 + groups 440 + |> List.iter (fun group -> 441 + Graph_row_layout.render_node_group group ~render_content:render_content_lines 442 + |> List.iter (fun ((row : Render_jj_graph.graph_row_output), img) -> 443 + Printf.printf "%S => %S\n" row.graph_chars (render_image_to_string img))); 444 + [%expect 445 + {| 446 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 mmnxzuyv test@example.com 2024-01-01 deadbeef" 447 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 show conflicts correctly" 448 + "\226\151\134 " => "\226\151\134 upnslvuv test@example.com 2024-01-01 deadbeef" 449 + "\226\148\130 " => "\226\148\130 make bookmarks render origin if need" 450 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 upnslvuv/2 test@example.com 2024-01-01 deadbeef" 451 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 make bookmarks render origin if needed" 452 + "\226\148\130 \226\151\134 " => "\226\148\130 \226\151\134 lpztppmx test@example.com 2024-01-01 deadbeef" 453 + "~ (elided revisions)" => "~ (elided revisions)" 454 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 vxkltmxw test@example.com 2024-01-01 deadbeef" 455 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (empty) (no description set)" 456 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 mwqvkttl test@example.com 2024-01-01 deadbeef" 457 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 (empty) (no description set)" 458 + |}] 459 + ;; 460 + 461 + let%expect_test "rendering_repro_for_extra_trailing_vertical_after_elision" = 462 + let groups = 463 + rows_with_extra_trailing_vertical_bug () |> Graph_row_layout.group_rows_by_node 464 + in 465 + groups 466 + |> List.iter (fun group -> 467 + Graph_row_layout.render_node_group group ~render_content:render_content_lines 468 + |> List.iter (fun ((row : Render_jj_graph.graph_row_output), img) -> 469 + Printf.printf "%S => %S\n" row.graph_chars (render_image_to_string img))); 470 + [%expect 471 + {| 472 + "\226\151\134 " => "\226\151\134 lpztppmx test@example.com 2024-01-01 deadbeef" 473 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 fix elided revisions bug during rebase" 474 + "\226\148\130 " => "\226\148\130" 475 + "~ (elided revisions)" => "~ (elided revisions)" 476 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 vxkltmxw test@example.com 2024-01-01 deadbeef" 477 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 (empty) (no description set)" 478 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 mwqvkttl test@example.com 2024-01-01 deadbeef" 479 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (empty) (no description set)" 480 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 qqsnmuzr/2 test@example.com 2024-01-01 deadbeef" 481 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 fix preview mode bugs (I think this broke the update loop somehow)" 482 + "\226\148\130 " => "\226\148\130" 483 + "~ (elided revisions)" => "~ (elided revisions)" 484 + "\226\151\134 " => "\226\151\134 tkozwuzw test@example.com 2024-01-01 deadbeef" 485 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 enable preview mode (5.2 codex)" 486 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 nkwwwlnw test@example.com 2024-01-01 deadbeef" 487 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 rewrite" 488 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 voywlxnk test@example.com 2024-01-01 deadbeef" 489 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (no description set)" 490 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 wrrtuusr test@example.com 2024-01-01 deadbeef" 491 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (no description set)" 492 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 vvnqynuv test@example.com 2024-01-01 deadbeef" 493 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 (no description set)" 494 + "\226\148\130 " => "\226\148\130" 495 + "~" => "~" 496 + "\226\151\134 " => "\226\151\134 noszsqtm test@example.com 2024-01-01 deadbeef" 497 + "\226\148\130 " => "\226\148\130 remove aarch64 linux because it doesn't seem to work" 498 + |}] 499 + ;; 500 + 501 + let%expect_test "parsed_rendering_repro_for_extra_trailing_vertical_after_elision" = 502 + let groups = 503 + parsed_rows_with_extra_trailing_vertical_bug () |> Graph_row_layout.group_rows_by_node 504 + in 505 + groups 506 + |> List.iter (fun group -> 507 + Graph_row_layout.render_node_group group ~render_content:render_content_lines 508 + |> List.iter (fun ((row : Render_jj_graph.graph_row_output), img) -> 509 + Printf.printf "%S => %S\n" row.graph_chars (render_image_to_string img))); 510 + [%expect 511 + {| 512 + "\226\151\134 " => "\226\151\134 lpztppmx test@example.com 2024-01-01 deadbeef" 513 + "\226\148\130 " => "\226\148\130 fix elided revisions bug during rebase" 514 + "~ (elided revisions)" => "~ (elided revisions)" 515 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 vxkltmxw test@example.com 2024-01-01 deadbeef" 516 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (empty) (no description set)" 517 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 mwqvkttl test@example.com 2024-01-01 deadbeef" 518 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 (empty) (no description set)" 519 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 qqsnmuzr/2 test@example.com 2024-01-01 deadbeef" 520 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 fix preview mode bugs (I think this broke the update loop somehow)" 521 + "\226\151\134 " => "\226\151\134 tkozwuzw test@example.com 2024-01-01 deadbeef" 522 + "\226\148\130 " => "\226\148\130 enable preview mode (5.2 codex)" 523 + "~ (elided revisions)" => "~ (elided revisions)" 524 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 nkwwwlnw test@example.com 2024-01-01 deadbeef" 525 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 rewrite" 526 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 voywlxnk test@example.com 2024-01-01 deadbeef" 527 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (no description set)" 528 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 wrrtuusr test@example.com 2024-01-01 deadbeef" 529 + "\226\148\130 \226\148\130 " => "\226\148\130 \226\148\130 (no description set)" 530 + "\226\148\130 \226\151\139 " => "\226\148\130 \226\151\139 vvnqynuv test@example.com 2024-01-01 deadbeef" 531 + "\226\148\156\226\148\128\226\149\175 " => "\226\148\156\226\148\128\226\149\175 (no description set)" 532 + "\226\151\134 " => "\226\151\134 noszsqtm test@example.com 2024-01-01 deadbeef" 533 + "\226\148\130 " => "\226\148\130 remove aarch64 linux because it doesn't seem to work" 534 + "~" => "~" 535 + |}] 536 + ;; 537 + 538 + let%expect_test "parsed_grouping_repro_for_extra_trailing_vertical_after_elision" = 539 + let groups = 540 + parsed_rows_with_extra_trailing_vertical_bug () |> Graph_row_layout.group_rows_by_node 541 + in 542 + List.iteri print_group groups; 543 + [%expect 544 + {| 545 + Group 0 node=lpztppmx 546 + head="\226\151\134 " 547 + cont="\226\148\130 " 548 + cont="~ (elided revisions)" 549 + Group 1 node=vxkltmxw 550 + head="\226\148\130 \226\151\139 " 551 + cont="\226\148\130 \226\148\130 " 552 + Group 2 node=mwqvkttl 553 + head="\226\148\130 \226\151\139 " 554 + cont="\226\148\156\226\148\128\226\149\175 " 555 + Group 3 node=qqsnmuzr/2 556 + head="\226\148\130 \226\151\139 " 557 + cont="\226\148\156\226\148\128\226\149\175 " 558 + Group 4 node=tkozwuzw 559 + head="\226\151\134 " 560 + cont="\226\148\130 " 561 + cont="~ (elided revisions)" 562 + Group 5 node=nkwwwlnw 563 + head="\226\148\130 \226\151\139 " 564 + cont="\226\148\130 \226\148\130 " 565 + Group 6 node=voywlxnk 566 + head="\226\148\130 \226\151\139 " 567 + cont="\226\148\130 \226\148\130 " 568 + Group 7 node=wrrtuusr 569 + head="\226\148\130 \226\151\139 " 570 + cont="\226\148\130 \226\148\130 " 571 + Group 8 node=vvnqynuv 572 + head="\226\148\130 \226\151\139 " 573 + cont="\226\148\156\226\148\128\226\149\175 " 574 + Group 9 node=noszsqtm 575 + head="\226\151\134 " 576 + cont="\226\148\130 " 577 + cont="~" 578 + |}] 579 + ;;