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.

more rendering fixes

+292 -196
+292 -196
jj_tui/lib/render_jj_graph.ml
··· 152 152 |> List.sort_uniq String.compare 153 153 ;; 154 154 155 + type resolved_revs = { 156 + source_ids : string list 157 + ; target_ids : string list 158 + ; source_set : StringSet.t 159 + } 160 + 161 + let resolve_sources_targets 162 + ~(nodes : node list) 163 + ~(sources : string list) 164 + ~(targets : string list) : resolved_revs 165 + = 166 + let source_ids = resolve_revs nodes sources in 167 + let target_ids = resolve_revs nodes targets in 168 + let source_set = StringSet.of_list source_ids in 169 + { source_ids; target_ids; source_set } 170 + ;; 171 + 172 + let validate_preview_cycles 173 + ~(mode : preview_mode) 174 + ~(ancestors_of : string -> StringSet.t) 175 + ~(source_ids : string list) 176 + ~(target_ids : string list) : string option 177 + = 178 + let invalid_target target_id = 179 + List.exists 180 + (fun source_id -> 181 + if source_id = target_id 182 + then true 183 + else ( 184 + let source_ancestors = ancestors_of source_id in 185 + let target_ancestors = ancestors_of target_id in 186 + match mode with 187 + | `Insert_before -> 188 + StringSet.mem target_id source_ancestors 189 + | `Insert_after | `Add_after -> 190 + StringSet.mem source_id target_ancestors)) 191 + source_ids 192 + in 193 + if List.exists invalid_target target_ids 194 + then Some "Preview blocked: cycle detected" 195 + else None 196 + ;; 197 + 198 + let preview_id_for ?source_id ?target_id ~label () = 199 + match source_id, target_id with 200 + | Some source_id, _ -> 201 + Printf.sprintf "preview:%s" source_id 202 + | None, Some target_id -> 203 + Printf.sprintf "preview:%s:%s" label target_id 204 + | None, None -> 205 + Printf.sprintf "preview:%s" label 206 + ;; 207 + 208 + let make_preview_clone ~source_node ~preview_id ~description = 209 + { 210 + source_node with 211 + commit_id = preview_id 212 + ; change_id = preview_id 213 + ; description 214 + ; is_preview = true 215 + } 216 + ;; 217 + 218 + let select_insertion_anchor 219 + ~(mode : preview_mode) 220 + ~(target_ids : string list) 221 + ~(nodes_filtered : node list) 222 + = 223 + (* To keep graph order stable, pick the first/last selected target as the single 224 + insertion anchor. Insert-after/add-after previews appear before the parent 225 + (child-first ordering), while insert-before previews appear after. *) 226 + let fallback = List.hd target_ids in 227 + let first_target_id = 228 + List.find_map 229 + (fun n -> if List.mem n.commit_id target_ids then Some n.commit_id else None) 230 + nodes_filtered 231 + |> Option.value ~default:fallback 232 + in 233 + let last_target_id = 234 + nodes_filtered 235 + |> List.fold_left 236 + (fun acc n -> if List.mem n.commit_id target_ids then Some n.commit_id else acc) 237 + None 238 + |> Option.value ~default:fallback 239 + in 240 + let preview_before = 241 + match mode with `Insert_after | `Add_after -> true | _ -> false 242 + in 243 + let preview_after = match mode with `Insert_before -> true | _ -> false in 244 + let insertion_target_id = if preview_after then last_target_id else first_target_id in 245 + insertion_target_id, preview_before, preview_after 246 + ;; 247 + 248 + let insert_preview_ids_once 249 + ~(nodes_filtered : node list) 250 + ~(insertion_target_id : string) 251 + ~(preview_ids : string list) 252 + ~(preview_before : bool) 253 + ~(preview_after : bool) 254 + = 255 + (* Preserve topological log order: children appear before parents. *) 256 + let inserted = ref false in 257 + let ordered_ids_rev = 258 + List.fold_left 259 + (fun acc n -> 260 + let id = n.commit_id in 261 + if (not !inserted) && id = insertion_target_id 262 + then ( 263 + inserted := true; 264 + if preview_before 265 + then id :: List.rev_append preview_ids acc 266 + else if preview_after 267 + then List.rev_append preview_ids (id :: acc) 268 + else id :: acc) 269 + else id :: acc) 270 + [] 271 + nodes_filtered 272 + in 273 + List.rev ordered_ids_rev 274 + ;; 275 + 155 276 let build_parent_map (nodes : node list) = 156 277 let map = Hashtbl.create (List.length nodes) in 157 278 List.iter 158 - (fun n -> 159 - Hashtbl.replace map n.commit_id (List.map (fun p -> p.commit_id) n.parents)) 279 + (fun n -> Hashtbl.replace map n.commit_id (List.map (fun p -> p.commit_id) n.parents)) 160 280 nodes; 161 281 map 162 282 ;; ··· 167 287 (fun child_id parent_ids -> 168 288 List.iter 169 289 (fun parent_id -> 170 - let existing = Option.value (Hashtbl.find_opt children parent_id) ~default:[] in 290 + let existing = 291 + Option.value (Hashtbl.find_opt children parent_id) ~default:[] 292 + in 171 293 Hashtbl.replace children parent_id (child_id :: existing)) 172 294 parent_ids) 173 295 parent_map; 174 296 children 175 297 ;; 176 298 299 + let build_filtered_maps ~(nodes : node list) ~(source_set : StringSet.t) = 300 + let nodes_filtered = 301 + nodes |> List.filter (fun n -> not (StringSet.mem n.commit_id source_set)) 302 + in 303 + let parent_map = Hashtbl.create (List.length nodes_filtered) in 304 + List.iter 305 + (fun n -> 306 + let parents = 307 + n.parents 308 + |> List.map (fun p -> p.commit_id) 309 + |> List.filter (fun id -> not (StringSet.mem id source_set)) 310 + in 311 + Hashtbl.replace parent_map n.commit_id parents) 312 + nodes_filtered; 313 + let children_map = build_children_map parent_map in 314 + nodes_filtered, parent_map, children_map 315 + ;; 316 + 177 317 let descendants_of ~children_map ~sources = 178 318 let visited = Hashtbl.create (List.length sources * 2) in 179 319 let queue = Queue.create () in ··· 198 338 visited |> Hashtbl.to_seq_keys |> List.of_seq 199 339 ;; 200 340 201 - 202 341 let build_ancestors parent_map = 203 342 let cache = Hashtbl.create (Hashtbl.length parent_map) in 204 343 let rec ancestors id = ··· 221 360 ancestors 222 361 ;; 223 362 363 + (** [expand_preview_sources ~mode ~sources ~targets nodes] 364 + Expands a list of "sources" of a rebase preview, given the preview source mode, 365 + the starting sources and targets, and the list of graph [nodes]. 366 + 367 + The purpose of this function is to determine, based on user actions, which commits 368 + should be highlighted or affected by a rebase preview operation in the commit graph UI. 369 + 370 + - For [`Revisions] mode, the expansion consists of just [sources] themselves. 371 + - For [`Source] mode, it includes all descendants of [sources] (i.e., each source and all its children recursively). 372 + - For [`Branch] mode, it computes the entire branch: the "base" is the set of ancestors of [sources] but not ancestors of [targets]; 373 + then it includes all descendants of this base set. This produces the same set of commits as would be affected by a `jj rebase -b ...`. 374 + 375 + The function returns all commit ids in [nodes] which are in the computed set according to the chosen mode. 376 + *) 224 377 let expand_preview_sources 225 378 ~(mode : preview_source_mode) 226 379 ~(sources : string list) 227 380 ~(targets : string list) 228 381 (nodes : node list) : string list 229 382 = 230 - if sources = [] then [] 383 + if sources = [] 384 + then [] 231 385 else ( 232 386 let parent_map = build_parent_map nodes in 233 387 let children_map = build_children_map parent_map in ··· 287 441 let parent_map_all = build_parent_map nodes in 288 442 let children_map_all = build_children_map parent_map_all in 289 443 let ancestors_of = build_ancestors parent_map_all in 290 - let source_ids = resolve_revs nodes sources in 291 - let target_ids = resolve_revs nodes targets in 292 - let source_set = StringSet.of_list source_ids in 293 - let invalid = ref None in 294 - let invalid_target target_id = 295 - List.exists 296 - (fun source_id -> 297 - if source_id = target_id 298 - then true 299 - else ( 300 - let source_ancestors = ancestors_of source_id in 301 - let target_ancestors = ancestors_of target_id in 302 - match mode with 303 - | `Insert_before -> 304 - StringSet.mem target_id source_ancestors 305 - | `Insert_after | `Add_after -> 306 - StringSet.mem source_id target_ancestors)) 307 - source_ids 444 + let { source_ids; target_ids; source_set } = 445 + resolve_sources_targets ~nodes ~sources ~targets 308 446 in 309 - List.iter (fun target_id -> 310 - if invalid_target target_id 311 - then invalid := Some "Preview blocked: cycle detected") target_ids; 312 - if !invalid <> None 313 - then nodes, !invalid 314 - else ( 315 - let nodes_filtered = 316 - nodes |> List.filter (fun n -> not (StringSet.mem n.commit_id source_set)) 447 + match validate_preview_cycles ~mode ~ancestors_of ~source_ids ~target_ids with 448 + | Some msg -> 449 + nodes, Some msg 450 + | None -> 451 + let nodes_filtered, parent_map, children_map = 452 + build_filtered_maps ~nodes ~source_set 317 453 in 318 - let parent_map = Hashtbl.create (List.length nodes_filtered) in 319 - List.iter 320 - (fun n -> 321 - let parents = 322 - n.parents 323 - |> List.map (fun p -> p.commit_id) 324 - |> List.filter (fun id -> not (StringSet.mem id source_set)) 325 - in 326 - Hashtbl.replace parent_map n.commit_id parents) 327 - nodes_filtered; 328 - let children_map = build_children_map parent_map in 329 454 let base_nodes = Hashtbl.create (List.length nodes_filtered) in 330 455 List.iter (fun n -> Hashtbl.replace base_nodes n.commit_id n) nodes_filtered; 331 456 let heads = 332 457 source_ids 333 458 |> List.filter (fun id -> 334 - let children = 335 - Option.value (Hashtbl.find_opt children_map_all id) ~default:[] 336 - in 459 + let children = Option.value (Hashtbl.find_opt children_map_all id) ~default:[] in 337 460 not (List.exists (fun child -> StringSet.mem child source_set) children)) 338 461 in 339 462 let source_order = ··· 344 467 let preview_map = Hashtbl.create (List.length source_ids) in 345 468 List.iter 346 469 (fun source_id -> 347 - let preview_id = Printf.sprintf "preview:%s" source_id in 470 + let preview_id = preview_id_for ~label:"preview" ~source_id () in 348 471 let source_node = List.find (fun n -> n.commit_id = source_id) nodes in 349 472 let preview_node = 350 - { 351 - source_node with 352 - commit_id = preview_id 353 - ; change_id = preview_id 354 - ; description = "preview: " ^ source_node.description 355 - ; is_preview = true 356 - } 473 + make_preview_clone 474 + ~source_node 475 + ~preview_id 476 + ~description:("preview: " ^ source_node.description) 357 477 in 358 478 Hashtbl.replace base_nodes preview_id preview_node; 359 479 Hashtbl.replace preview_map source_id preview_id) ··· 375 495 source_ids 376 496 |> List.filter (fun id -> 377 497 let source_node = List.find (fun n -> n.commit_id = id) nodes in 378 - not (List.exists (fun p -> StringSet.mem p.commit_id source_set) source_node.parents)) 498 + not 499 + (List.exists 500 + (fun p -> StringSet.mem p.commit_id source_set) 501 + source_node.parents)) 379 502 in 380 503 let root_preview_ids = root_ids |> List.map (fun id -> Hashtbl.find preview_map id) in 381 504 let head_preview_ids = heads |> List.map (fun id -> Hashtbl.find preview_map id) in ··· 417 540 List.iter 418 541 (fun preview_id -> Hashtbl.replace parent_map preview_id target_ids) 419 542 root_preview_ids); 420 - let preview_before = match mode with `Insert_after | `Add_after -> true | _ -> false in 421 - let preview_after = match mode with `Insert_before -> true | _ -> false in 422 - let first_target_id = 423 - List.find_map 424 - (fun n -> if List.mem n.commit_id target_ids then Some n.commit_id else None) 425 - nodes_filtered 426 - |> Option.value ~default:(List.hd target_ids) 543 + let insertion_target_id, preview_before, preview_after = 544 + select_insertion_anchor ~mode ~target_ids ~nodes_filtered 427 545 in 428 - let last_target_id = 429 - nodes_filtered 430 - |> List.fold_left 431 - (fun acc n -> 432 - if List.mem n.commit_id target_ids then Some n.commit_id else acc) 433 - None 434 - |> Option.value ~default:(List.hd target_ids) 546 + let preview_ids = 547 + source_order |> List.map (fun source_id -> Hashtbl.find preview_map source_id) 435 548 in 436 - let insertion_target_id = 437 - if preview_after then last_target_id else first_target_id 549 + let ordered_ids = 550 + insert_preview_ids_once 551 + ~nodes_filtered 552 + ~insertion_target_id 553 + ~preview_ids 554 + ~preview_before 555 + ~preview_after 438 556 in 439 - let inserted = ref false in 440 - let ordered_ids_rev = 441 - List.fold_left 442 - (fun acc n -> 443 - let id = n.commit_id in 444 - if (not !inserted) && id = insertion_target_id 445 - then ( 446 - inserted := true; 447 - let preview_ids = 448 - source_order |> List.map (fun source_id -> Hashtbl.find preview_map source_id) 449 - in 450 - if preview_before 451 - then id :: (List.rev_append preview_ids acc) 452 - else if preview_after 453 - then (List.rev_append preview_ids (id :: acc)) 454 - else id :: acc) 455 - else id :: acc) 456 - [] 457 - nodes_filtered 458 - in 459 - let ordered_ids = List.rev ordered_ids_rev in 460 557 let final_nodes = Hashtbl.create (List.length ordered_ids) in 461 558 let rec build_node id = 462 559 match Hashtbl.find_opt final_nodes id with ··· 464 561 node 465 562 | None -> 466 563 let base = Hashtbl.find base_nodes id in 467 - let parent_ids = Option.value (Hashtbl.find_opt parent_map id) ~default:[] in 564 + let parent_ids = 565 + Option.value (Hashtbl.find_opt parent_map id) ~default:[] 566 + |> List.filter (fun parent_id -> Hashtbl.mem base_nodes parent_id) 567 + in 468 568 let parents = List.map build_node parent_ids in 469 569 let node = { base with parents } in 470 570 Hashtbl.replace final_nodes id node; 471 571 node 472 572 in 473 573 let nodes = List.map build_node ordered_ids in 474 - nodes, !invalid) 574 + nodes, None 475 575 ;; 476 576 477 577 let apply_rebase_preview ··· 480 580 ~(targets : string list) 481 581 (nodes : node list) : node list * string option 482 582 = 483 - if sources = [] || targets = [] then nodes, None 583 + if sources = [] || targets = [] 584 + then nodes, None 484 585 else ( 485 - let source_ids = resolve_revs nodes sources in 486 - let target_ids = resolve_revs nodes targets in 586 + let { source_ids; target_ids; source_set } = 587 + resolve_sources_targets ~nodes ~sources ~targets 588 + in 487 589 if source_ids = [] || target_ids = [] 488 590 then nodes, None 591 + else if List.length source_ids > 1 592 + then apply_rebase_preview_multi ~mode ~sources ~targets nodes 489 593 else ( 490 - if List.length source_ids > 1 491 - then apply_rebase_preview_multi ~mode ~sources ~targets nodes 492 - else ( 493 594 let parent_map_all = build_parent_map nodes in 595 + let children_map_all = build_children_map parent_map_all in 494 596 let ancestors_of = build_ancestors parent_map_all in 495 - let removed_set = StringSet.of_list source_ids in 496 - let nodes_filtered = 497 - nodes |> List.filter (fun n -> not (StringSet.mem n.commit_id removed_set)) 597 + let nodes_filtered, parent_map, children_map = 598 + build_filtered_maps ~nodes ~source_set 599 + in 600 + let () = 601 + let dedupe_preserve_order items = 602 + let seen = Hashtbl.create (List.length items) in 603 + List.filter 604 + (fun item -> 605 + if Hashtbl.mem seen item 606 + then false 607 + else ( 608 + Hashtbl.add seen item (); 609 + true)) 610 + items 611 + in 612 + List.iter 613 + (fun source_id -> 614 + let source_parents = 615 + Option.value (Hashtbl.find_opt parent_map_all source_id) ~default:[] 616 + |> List.filter (fun id -> not (StringSet.mem id source_set)) 617 + in 618 + let children = 619 + Option.value (Hashtbl.find_opt children_map_all source_id) ~default:[] 620 + in 621 + List.iter 622 + (fun child_id -> 623 + if Hashtbl.mem parent_map child_id 624 + then ( 625 + let child_parents = 626 + Option.value (Hashtbl.find_opt parent_map_all child_id) ~default:[] 627 + |> List.filter (fun id -> not (StringSet.mem id source_set)) 628 + in 629 + let updated = 630 + child_parents 631 + |> List.concat_map (fun parent_id -> 632 + if parent_id = source_id then source_parents else [ parent_id ]) 633 + |> dedupe_preserve_order 634 + in 635 + Hashtbl.replace parent_map child_id updated)) 636 + children) 637 + source_ids 498 638 in 499 - let parent_map = Hashtbl.create (List.length nodes_filtered) in 500 - List.iter 501 - (fun n -> 502 - let parents = 503 - n.parents 504 - |> List.map (fun p -> p.commit_id) 505 - |> List.filter (fun id -> not (StringSet.mem id removed_set)) 506 - in 507 - Hashtbl.replace parent_map n.commit_id parents) 508 - nodes_filtered; 509 - let children_map = build_children_map parent_map in 510 - let invalid = ref None in 511 639 let preview_by_target = Hashtbl.create (List.length target_ids) in 512 640 let base_nodes = 513 641 Hashtbl.create (List.length nodes_filtered + List.length target_ids) 514 642 in 515 643 List.iter (fun n -> Hashtbl.replace base_nodes n.commit_id n) nodes_filtered; 516 - let invalid_target target_id = 517 - List.exists 518 - (fun source_id -> 519 - if source_id = target_id 520 - then true 521 - else ( 522 - let source_ancestors = ancestors_of source_id in 523 - let target_ancestors = ancestors_of target_id in 524 - match mode with 525 - | `Insert_before -> 526 - StringSet.mem target_id source_ancestors 527 - | `Insert_after | `Add_after -> 528 - StringSet.mem source_id target_ancestors)) 529 - source_ids 530 - in 531 - List.iter 532 - (fun target_id -> 533 - if invalid_target target_id 534 - then invalid := Some "Preview blocked: cycle detected") 535 - target_ids; 536 - if !invalid <> None 537 - then nodes_filtered, !invalid 538 - else ( 644 + match validate_preview_cycles ~mode ~ancestors_of ~source_ids ~target_ids with 645 + | Some msg -> 646 + nodes_filtered, Some msg 647 + | None -> 539 648 let () = 540 649 if List.length target_ids > 1 541 650 then ( ··· 570 679 let without_target = 571 680 List.filter (fun id -> id <> target_id) child_parents 572 681 in 573 - Hashtbl.replace parent_map child_id (without_target @ [ preview_id ])) 682 + Hashtbl.replace 683 + parent_map 684 + child_id 685 + (without_target @ [ preview_id ])) 574 686 children) 575 687 target_ids 576 688 | `Add_after -> 577 689 Hashtbl.replace parent_map preview_id target_ids); 578 - let first_target_id = 579 - List.find_map 580 - (fun n -> 581 - if List.mem n.commit_id target_ids then Some n.commit_id else None) 582 - nodes_filtered 583 - |> Option.value ~default:(List.hd target_ids) 584 - in 585 - let last_target_id = 586 - nodes_filtered 587 - |> List.fold_left 588 - (fun acc n -> 589 - if List.mem n.commit_id target_ids then Some n.commit_id else acc) 590 - None 591 - |> Option.value ~default:(List.hd target_ids) 592 - in 593 - let insertion_target_id = 594 - match mode with 595 - | `Insert_before -> 596 - last_target_id 597 - | `Insert_after | `Add_after -> 598 - first_target_id 690 + let insertion_target_id, _, _ = 691 + select_insertion_anchor ~mode ~target_ids ~nodes_filtered 599 692 in 600 693 Hashtbl.replace preview_by_target insertion_target_id preview_id) 601 694 else ( ··· 603 696 if not (Hashtbl.mem parent_map target_id) 604 697 then () 605 698 else ( 606 - let preview_id = Printf.sprintf "preview:%s" target_id in 699 + let preview_id = preview_id_for ~label:"preview" ~target_id () in 607 700 if not (Hashtbl.mem preview_by_target target_id) 608 701 then ( 609 702 let label = "preview" in ··· 643 736 in 644 737 List.iter add_preview_for_target target_ids) 645 738 in 646 - (* Order must follow topological log order: children appear before parents. *) 647 - let preview_before = match mode with `Insert_after | `Add_after -> true | _ -> false in 648 - let preview_after = match mode with `Insert_before -> true | _ -> false in 649 - let ordered_ids_rev = 650 - List.fold_left 651 - (fun acc n -> 652 - let id = n.commit_id in 653 - match Hashtbl.find_opt preview_by_target id with 654 - | Some preview_id when preview_before -> 655 - id :: preview_id :: acc 656 - | Some preview_id when preview_after -> 657 - preview_id :: id :: acc 658 - | Some _ -> 659 - id :: acc 660 - | None -> 661 - id :: acc) 662 - [] 663 - nodes_filtered 664 - in 665 - let ordered_ids = List.rev ordered_ids_rev in 666 - let final_nodes = Hashtbl.create (List.length ordered_ids) in 667 - let rec build_node id = 668 - match Hashtbl.find_opt final_nodes id with 669 - | Some node -> 670 - node 671 - | None -> 672 - let base = Hashtbl.find base_nodes id in 673 - let parent_ids = Option.value (Hashtbl.find_opt parent_map id) ~default:[] in 674 - let parents = List.map build_node parent_ids in 675 - let node = { base with parents } in 676 - Hashtbl.replace final_nodes id node; 677 - node 678 - in 679 - let nodes = List.map build_node ordered_ids in 680 - nodes, !invalid)))) 739 + (* Order must follow topological log order: children appear before parents. *) 740 + let insertion_target_id, preview_before, preview_after = 741 + select_insertion_anchor ~mode ~target_ids ~nodes_filtered 742 + in 743 + let preview_ids = 744 + match Hashtbl.find_opt preview_by_target insertion_target_id with 745 + | Some preview_id -> 746 + [ preview_id ] 747 + | None -> 748 + [] 749 + in 750 + let ordered_ids = 751 + insert_preview_ids_once 752 + ~nodes_filtered 753 + ~insertion_target_id 754 + ~preview_ids 755 + ~preview_before 756 + ~preview_after 757 + in 758 + let final_nodes = Hashtbl.create (List.length ordered_ids) in 759 + let rec build_node id = 760 + match Hashtbl.find_opt final_nodes id with 761 + | Some node -> 762 + node 763 + | None -> 764 + let base = Hashtbl.find base_nodes id in 765 + let parent_ids = 766 + Option.value (Hashtbl.find_opt parent_map id) ~default:[] 767 + |> List.filter (fun parent_id -> Hashtbl.mem base_nodes parent_id) 768 + in 769 + let parents = List.map build_node parent_ids in 770 + let node = { base with parents } in 771 + Hashtbl.replace final_nodes id node; 772 + node 773 + in 774 + let nodes = List.map build_node ordered_ids in 775 + nodes, None)) 681 776 ;; 682 777 683 778 (** Insert a preview node after the specified commit. ··· 1583 1678 then ( 1584 1679 Buffer.add_string term_buf2 glyphs.(Glyph.termination); 1585 1680 term_images2 1586 - := Notty.I.string Notty.A.empty glyphs.(Glyph.termination) :: !term_images2) 1681 + := Notty.I.string Notty.A.empty glyphs.(Glyph.termination) 1682 + :: !term_images2) 1587 1683 else ( 1588 1684 let pad_glyph = pad_line_to_glyph row.pad_lines.(i) in 1589 1685 Buffer.add_string term_buf2 glyphs.(pad_glyph);