···152152 |> List.sort_uniq String.compare
153153;;
154154155155+type resolved_revs = {
156156+ source_ids : string list
157157+ ; target_ids : string list
158158+ ; source_set : StringSet.t
159159+}
160160+161161+let resolve_sources_targets
162162+ ~(nodes : node list)
163163+ ~(sources : string list)
164164+ ~(targets : string list) : resolved_revs
165165+ =
166166+ let source_ids = resolve_revs nodes sources in
167167+ let target_ids = resolve_revs nodes targets in
168168+ let source_set = StringSet.of_list source_ids in
169169+ { source_ids; target_ids; source_set }
170170+;;
171171+172172+let validate_preview_cycles
173173+ ~(mode : preview_mode)
174174+ ~(ancestors_of : string -> StringSet.t)
175175+ ~(source_ids : string list)
176176+ ~(target_ids : string list) : string option
177177+ =
178178+ let invalid_target target_id =
179179+ List.exists
180180+ (fun source_id ->
181181+ if source_id = target_id
182182+ then true
183183+ else (
184184+ let source_ancestors = ancestors_of source_id in
185185+ let target_ancestors = ancestors_of target_id in
186186+ match mode with
187187+ | `Insert_before ->
188188+ StringSet.mem target_id source_ancestors
189189+ | `Insert_after | `Add_after ->
190190+ StringSet.mem source_id target_ancestors))
191191+ source_ids
192192+ in
193193+ if List.exists invalid_target target_ids
194194+ then Some "Preview blocked: cycle detected"
195195+ else None
196196+;;
197197+198198+let preview_id_for ?source_id ?target_id ~label () =
199199+ match source_id, target_id with
200200+ | Some source_id, _ ->
201201+ Printf.sprintf "preview:%s" source_id
202202+ | None, Some target_id ->
203203+ Printf.sprintf "preview:%s:%s" label target_id
204204+ | None, None ->
205205+ Printf.sprintf "preview:%s" label
206206+;;
207207+208208+let make_preview_clone ~source_node ~preview_id ~description =
209209+ {
210210+ source_node with
211211+ commit_id = preview_id
212212+ ; change_id = preview_id
213213+ ; description
214214+ ; is_preview = true
215215+ }
216216+;;
217217+218218+let select_insertion_anchor
219219+ ~(mode : preview_mode)
220220+ ~(target_ids : string list)
221221+ ~(nodes_filtered : node list)
222222+ =
223223+ (* To keep graph order stable, pick the first/last selected target as the single
224224+ insertion anchor. Insert-after/add-after previews appear before the parent
225225+ (child-first ordering), while insert-before previews appear after. *)
226226+ let fallback = List.hd target_ids in
227227+ let first_target_id =
228228+ List.find_map
229229+ (fun n -> if List.mem n.commit_id target_ids then Some n.commit_id else None)
230230+ nodes_filtered
231231+ |> Option.value ~default:fallback
232232+ in
233233+ let last_target_id =
234234+ nodes_filtered
235235+ |> List.fold_left
236236+ (fun acc n -> if List.mem n.commit_id target_ids then Some n.commit_id else acc)
237237+ None
238238+ |> Option.value ~default:fallback
239239+ in
240240+ let preview_before =
241241+ match mode with `Insert_after | `Add_after -> true | _ -> false
242242+ in
243243+ let preview_after = match mode with `Insert_before -> true | _ -> false in
244244+ let insertion_target_id = if preview_after then last_target_id else first_target_id in
245245+ insertion_target_id, preview_before, preview_after
246246+;;
247247+248248+let insert_preview_ids_once
249249+ ~(nodes_filtered : node list)
250250+ ~(insertion_target_id : string)
251251+ ~(preview_ids : string list)
252252+ ~(preview_before : bool)
253253+ ~(preview_after : bool)
254254+ =
255255+ (* Preserve topological log order: children appear before parents. *)
256256+ let inserted = ref false in
257257+ let ordered_ids_rev =
258258+ List.fold_left
259259+ (fun acc n ->
260260+ let id = n.commit_id in
261261+ if (not !inserted) && id = insertion_target_id
262262+ then (
263263+ inserted := true;
264264+ if preview_before
265265+ then id :: List.rev_append preview_ids acc
266266+ else if preview_after
267267+ then List.rev_append preview_ids (id :: acc)
268268+ else id :: acc)
269269+ else id :: acc)
270270+ []
271271+ nodes_filtered
272272+ in
273273+ List.rev ordered_ids_rev
274274+;;
275275+155276let build_parent_map (nodes : node list) =
156277 let map = Hashtbl.create (List.length nodes) in
157278 List.iter
158158- (fun n ->
159159- Hashtbl.replace map n.commit_id (List.map (fun p -> p.commit_id) n.parents))
279279+ (fun n -> Hashtbl.replace map n.commit_id (List.map (fun p -> p.commit_id) n.parents))
160280 nodes;
161281 map
162282;;
···167287 (fun child_id parent_ids ->
168288 List.iter
169289 (fun parent_id ->
170170- let existing = Option.value (Hashtbl.find_opt children parent_id) ~default:[] in
290290+ let existing =
291291+ Option.value (Hashtbl.find_opt children parent_id) ~default:[]
292292+ in
171293 Hashtbl.replace children parent_id (child_id :: existing))
172294 parent_ids)
173295 parent_map;
174296 children
175297;;
176298299299+let build_filtered_maps ~(nodes : node list) ~(source_set : StringSet.t) =
300300+ let nodes_filtered =
301301+ nodes |> List.filter (fun n -> not (StringSet.mem n.commit_id source_set))
302302+ in
303303+ let parent_map = Hashtbl.create (List.length nodes_filtered) in
304304+ List.iter
305305+ (fun n ->
306306+ let parents =
307307+ n.parents
308308+ |> List.map (fun p -> p.commit_id)
309309+ |> List.filter (fun id -> not (StringSet.mem id source_set))
310310+ in
311311+ Hashtbl.replace parent_map n.commit_id parents)
312312+ nodes_filtered;
313313+ let children_map = build_children_map parent_map in
314314+ nodes_filtered, parent_map, children_map
315315+;;
316316+177317let descendants_of ~children_map ~sources =
178318 let visited = Hashtbl.create (List.length sources * 2) in
179319 let queue = Queue.create () in
···198338 visited |> Hashtbl.to_seq_keys |> List.of_seq
199339;;
200340201201-202341let build_ancestors parent_map =
203342 let cache = Hashtbl.create (Hashtbl.length parent_map) in
204343 let rec ancestors id =
···221360 ancestors
222361;;
223362363363+(** [expand_preview_sources ~mode ~sources ~targets nodes]
364364+ Expands a list of "sources" of a rebase preview, given the preview source mode,
365365+ the starting sources and targets, and the list of graph [nodes].
366366+367367+ The purpose of this function is to determine, based on user actions, which commits
368368+ should be highlighted or affected by a rebase preview operation in the commit graph UI.
369369+370370+ - For [`Revisions] mode, the expansion consists of just [sources] themselves.
371371+ - For [`Source] mode, it includes all descendants of [sources] (i.e., each source and all its children recursively).
372372+ - For [`Branch] mode, it computes the entire branch: the "base" is the set of ancestors of [sources] but not ancestors of [targets];
373373+ 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 ...`.
374374+375375+ The function returns all commit ids in [nodes] which are in the computed set according to the chosen mode.
376376+*)
224377let expand_preview_sources
225378 ~(mode : preview_source_mode)
226379 ~(sources : string list)
227380 ~(targets : string list)
228381 (nodes : node list) : string list
229382 =
230230- if sources = [] then []
383383+ if sources = []
384384+ then []
231385 else (
232386 let parent_map = build_parent_map nodes in
233387 let children_map = build_children_map parent_map in
···287441 let parent_map_all = build_parent_map nodes in
288442 let children_map_all = build_children_map parent_map_all in
289443 let ancestors_of = build_ancestors parent_map_all in
290290- let source_ids = resolve_revs nodes sources in
291291- let target_ids = resolve_revs nodes targets in
292292- let source_set = StringSet.of_list source_ids in
293293- let invalid = ref None in
294294- let invalid_target target_id =
295295- List.exists
296296- (fun source_id ->
297297- if source_id = target_id
298298- then true
299299- else (
300300- let source_ancestors = ancestors_of source_id in
301301- let target_ancestors = ancestors_of target_id in
302302- match mode with
303303- | `Insert_before ->
304304- StringSet.mem target_id source_ancestors
305305- | `Insert_after | `Add_after ->
306306- StringSet.mem source_id target_ancestors))
307307- source_ids
444444+ let { source_ids; target_ids; source_set } =
445445+ resolve_sources_targets ~nodes ~sources ~targets
308446 in
309309- List.iter (fun target_id ->
310310- if invalid_target target_id
311311- then invalid := Some "Preview blocked: cycle detected") target_ids;
312312- if !invalid <> None
313313- then nodes, !invalid
314314- else (
315315- let nodes_filtered =
316316- nodes |> List.filter (fun n -> not (StringSet.mem n.commit_id source_set))
447447+ match validate_preview_cycles ~mode ~ancestors_of ~source_ids ~target_ids with
448448+ | Some msg ->
449449+ nodes, Some msg
450450+ | None ->
451451+ let nodes_filtered, parent_map, children_map =
452452+ build_filtered_maps ~nodes ~source_set
317453 in
318318- let parent_map = Hashtbl.create (List.length nodes_filtered) in
319319- List.iter
320320- (fun n ->
321321- let parents =
322322- n.parents
323323- |> List.map (fun p -> p.commit_id)
324324- |> List.filter (fun id -> not (StringSet.mem id source_set))
325325- in
326326- Hashtbl.replace parent_map n.commit_id parents)
327327- nodes_filtered;
328328- let children_map = build_children_map parent_map in
329454 let base_nodes = Hashtbl.create (List.length nodes_filtered) in
330455 List.iter (fun n -> Hashtbl.replace base_nodes n.commit_id n) nodes_filtered;
331456 let heads =
332457 source_ids
333458 |> List.filter (fun id ->
334334- let children =
335335- Option.value (Hashtbl.find_opt children_map_all id) ~default:[]
336336- in
459459+ let children = Option.value (Hashtbl.find_opt children_map_all id) ~default:[] in
337460 not (List.exists (fun child -> StringSet.mem child source_set) children))
338461 in
339462 let source_order =
···344467 let preview_map = Hashtbl.create (List.length source_ids) in
345468 List.iter
346469 (fun source_id ->
347347- let preview_id = Printf.sprintf "preview:%s" source_id in
470470+ let preview_id = preview_id_for ~label:"preview" ~source_id () in
348471 let source_node = List.find (fun n -> n.commit_id = source_id) nodes in
349472 let preview_node =
350350- {
351351- source_node with
352352- commit_id = preview_id
353353- ; change_id = preview_id
354354- ; description = "preview: " ^ source_node.description
355355- ; is_preview = true
356356- }
473473+ make_preview_clone
474474+ ~source_node
475475+ ~preview_id
476476+ ~description:("preview: " ^ source_node.description)
357477 in
358478 Hashtbl.replace base_nodes preview_id preview_node;
359479 Hashtbl.replace preview_map source_id preview_id)
···375495 source_ids
376496 |> List.filter (fun id ->
377497 let source_node = List.find (fun n -> n.commit_id = id) nodes in
378378- not (List.exists (fun p -> StringSet.mem p.commit_id source_set) source_node.parents))
498498+ not
499499+ (List.exists
500500+ (fun p -> StringSet.mem p.commit_id source_set)
501501+ source_node.parents))
379502 in
380503 let root_preview_ids = root_ids |> List.map (fun id -> Hashtbl.find preview_map id) in
381504 let head_preview_ids = heads |> List.map (fun id -> Hashtbl.find preview_map id) in
···417540 List.iter
418541 (fun preview_id -> Hashtbl.replace parent_map preview_id target_ids)
419542 root_preview_ids);
420420- let preview_before = match mode with `Insert_after | `Add_after -> true | _ -> false in
421421- let preview_after = match mode with `Insert_before -> true | _ -> false in
422422- let first_target_id =
423423- List.find_map
424424- (fun n -> if List.mem n.commit_id target_ids then Some n.commit_id else None)
425425- nodes_filtered
426426- |> Option.value ~default:(List.hd target_ids)
543543+ let insertion_target_id, preview_before, preview_after =
544544+ select_insertion_anchor ~mode ~target_ids ~nodes_filtered
427545 in
428428- let last_target_id =
429429- nodes_filtered
430430- |> List.fold_left
431431- (fun acc n ->
432432- if List.mem n.commit_id target_ids then Some n.commit_id else acc)
433433- None
434434- |> Option.value ~default:(List.hd target_ids)
546546+ let preview_ids =
547547+ source_order |> List.map (fun source_id -> Hashtbl.find preview_map source_id)
435548 in
436436- let insertion_target_id =
437437- if preview_after then last_target_id else first_target_id
549549+ let ordered_ids =
550550+ insert_preview_ids_once
551551+ ~nodes_filtered
552552+ ~insertion_target_id
553553+ ~preview_ids
554554+ ~preview_before
555555+ ~preview_after
438556 in
439439- let inserted = ref false in
440440- let ordered_ids_rev =
441441- List.fold_left
442442- (fun acc n ->
443443- let id = n.commit_id in
444444- if (not !inserted) && id = insertion_target_id
445445- then (
446446- inserted := true;
447447- let preview_ids =
448448- source_order |> List.map (fun source_id -> Hashtbl.find preview_map source_id)
449449- in
450450- if preview_before
451451- then id :: (List.rev_append preview_ids acc)
452452- else if preview_after
453453- then (List.rev_append preview_ids (id :: acc))
454454- else id :: acc)
455455- else id :: acc)
456456- []
457457- nodes_filtered
458458- in
459459- let ordered_ids = List.rev ordered_ids_rev in
460557 let final_nodes = Hashtbl.create (List.length ordered_ids) in
461558 let rec build_node id =
462559 match Hashtbl.find_opt final_nodes id with
···464561 node
465562 | None ->
466563 let base = Hashtbl.find base_nodes id in
467467- let parent_ids = Option.value (Hashtbl.find_opt parent_map id) ~default:[] in
564564+ let parent_ids =
565565+ Option.value (Hashtbl.find_opt parent_map id) ~default:[]
566566+ |> List.filter (fun parent_id -> Hashtbl.mem base_nodes parent_id)
567567+ in
468568 let parents = List.map build_node parent_ids in
469569 let node = { base with parents } in
470570 Hashtbl.replace final_nodes id node;
471571 node
472572 in
473573 let nodes = List.map build_node ordered_ids in
474474- nodes, !invalid)
574574+ nodes, None
475575;;
476576477577let apply_rebase_preview
···480580 ~(targets : string list)
481581 (nodes : node list) : node list * string option
482582 =
483483- if sources = [] || targets = [] then nodes, None
583583+ if sources = [] || targets = []
584584+ then nodes, None
484585 else (
485485- let source_ids = resolve_revs nodes sources in
486486- let target_ids = resolve_revs nodes targets in
586586+ let { source_ids; target_ids; source_set } =
587587+ resolve_sources_targets ~nodes ~sources ~targets
588588+ in
487589 if source_ids = [] || target_ids = []
488590 then nodes, None
591591+ else if List.length source_ids > 1
592592+ then apply_rebase_preview_multi ~mode ~sources ~targets nodes
489593 else (
490490- if List.length source_ids > 1
491491- then apply_rebase_preview_multi ~mode ~sources ~targets nodes
492492- else (
493594 let parent_map_all = build_parent_map nodes in
595595+ let children_map_all = build_children_map parent_map_all in
494596 let ancestors_of = build_ancestors parent_map_all in
495495- let removed_set = StringSet.of_list source_ids in
496496- let nodes_filtered =
497497- nodes |> List.filter (fun n -> not (StringSet.mem n.commit_id removed_set))
597597+ let nodes_filtered, parent_map, children_map =
598598+ build_filtered_maps ~nodes ~source_set
599599+ in
600600+ let () =
601601+ let dedupe_preserve_order items =
602602+ let seen = Hashtbl.create (List.length items) in
603603+ List.filter
604604+ (fun item ->
605605+ if Hashtbl.mem seen item
606606+ then false
607607+ else (
608608+ Hashtbl.add seen item ();
609609+ true))
610610+ items
611611+ in
612612+ List.iter
613613+ (fun source_id ->
614614+ let source_parents =
615615+ Option.value (Hashtbl.find_opt parent_map_all source_id) ~default:[]
616616+ |> List.filter (fun id -> not (StringSet.mem id source_set))
617617+ in
618618+ let children =
619619+ Option.value (Hashtbl.find_opt children_map_all source_id) ~default:[]
620620+ in
621621+ List.iter
622622+ (fun child_id ->
623623+ if Hashtbl.mem parent_map child_id
624624+ then (
625625+ let child_parents =
626626+ Option.value (Hashtbl.find_opt parent_map_all child_id) ~default:[]
627627+ |> List.filter (fun id -> not (StringSet.mem id source_set))
628628+ in
629629+ let updated =
630630+ child_parents
631631+ |> List.concat_map (fun parent_id ->
632632+ if parent_id = source_id then source_parents else [ parent_id ])
633633+ |> dedupe_preserve_order
634634+ in
635635+ Hashtbl.replace parent_map child_id updated))
636636+ children)
637637+ source_ids
498638 in
499499- let parent_map = Hashtbl.create (List.length nodes_filtered) in
500500- List.iter
501501- (fun n ->
502502- let parents =
503503- n.parents
504504- |> List.map (fun p -> p.commit_id)
505505- |> List.filter (fun id -> not (StringSet.mem id removed_set))
506506- in
507507- Hashtbl.replace parent_map n.commit_id parents)
508508- nodes_filtered;
509509- let children_map = build_children_map parent_map in
510510- let invalid = ref None in
511639 let preview_by_target = Hashtbl.create (List.length target_ids) in
512640 let base_nodes =
513641 Hashtbl.create (List.length nodes_filtered + List.length target_ids)
514642 in
515643 List.iter (fun n -> Hashtbl.replace base_nodes n.commit_id n) nodes_filtered;
516516- let invalid_target target_id =
517517- List.exists
518518- (fun source_id ->
519519- if source_id = target_id
520520- then true
521521- else (
522522- let source_ancestors = ancestors_of source_id in
523523- let target_ancestors = ancestors_of target_id in
524524- match mode with
525525- | `Insert_before ->
526526- StringSet.mem target_id source_ancestors
527527- | `Insert_after | `Add_after ->
528528- StringSet.mem source_id target_ancestors))
529529- source_ids
530530- in
531531- List.iter
532532- (fun target_id ->
533533- if invalid_target target_id
534534- then invalid := Some "Preview blocked: cycle detected")
535535- target_ids;
536536- if !invalid <> None
537537- then nodes_filtered, !invalid
538538- else (
644644+ match validate_preview_cycles ~mode ~ancestors_of ~source_ids ~target_ids with
645645+ | Some msg ->
646646+ nodes_filtered, Some msg
647647+ | None ->
539648 let () =
540649 if List.length target_ids > 1
541650 then (
···570679 let without_target =
571680 List.filter (fun id -> id <> target_id) child_parents
572681 in
573573- Hashtbl.replace parent_map child_id (without_target @ [ preview_id ]))
682682+ Hashtbl.replace
683683+ parent_map
684684+ child_id
685685+ (without_target @ [ preview_id ]))
574686 children)
575687 target_ids
576688 | `Add_after ->
577689 Hashtbl.replace parent_map preview_id target_ids);
578578- let first_target_id =
579579- List.find_map
580580- (fun n ->
581581- if List.mem n.commit_id target_ids then Some n.commit_id else None)
582582- nodes_filtered
583583- |> Option.value ~default:(List.hd target_ids)
584584- in
585585- let last_target_id =
586586- nodes_filtered
587587- |> List.fold_left
588588- (fun acc n ->
589589- if List.mem n.commit_id target_ids then Some n.commit_id else acc)
590590- None
591591- |> Option.value ~default:(List.hd target_ids)
592592- in
593593- let insertion_target_id =
594594- match mode with
595595- | `Insert_before ->
596596- last_target_id
597597- | `Insert_after | `Add_after ->
598598- first_target_id
690690+ let insertion_target_id, _, _ =
691691+ select_insertion_anchor ~mode ~target_ids ~nodes_filtered
599692 in
600693 Hashtbl.replace preview_by_target insertion_target_id preview_id)
601694 else (
···603696 if not (Hashtbl.mem parent_map target_id)
604697 then ()
605698 else (
606606- let preview_id = Printf.sprintf "preview:%s" target_id in
699699+ let preview_id = preview_id_for ~label:"preview" ~target_id () in
607700 if not (Hashtbl.mem preview_by_target target_id)
608701 then (
609702 let label = "preview" in
···643736 in
644737 List.iter add_preview_for_target target_ids)
645738 in
646646- (* Order must follow topological log order: children appear before parents. *)
647647- let preview_before = match mode with `Insert_after | `Add_after -> true | _ -> false in
648648- let preview_after = match mode with `Insert_before -> true | _ -> false in
649649- let ordered_ids_rev =
650650- List.fold_left
651651- (fun acc n ->
652652- let id = n.commit_id in
653653- match Hashtbl.find_opt preview_by_target id with
654654- | Some preview_id when preview_before ->
655655- id :: preview_id :: acc
656656- | Some preview_id when preview_after ->
657657- preview_id :: id :: acc
658658- | Some _ ->
659659- id :: acc
660660- | None ->
661661- id :: acc)
662662- []
663663- nodes_filtered
664664- in
665665- let ordered_ids = List.rev ordered_ids_rev in
666666- let final_nodes = Hashtbl.create (List.length ordered_ids) in
667667- let rec build_node id =
668668- match Hashtbl.find_opt final_nodes id with
669669- | Some node ->
670670- node
671671- | None ->
672672- let base = Hashtbl.find base_nodes id in
673673- let parent_ids = Option.value (Hashtbl.find_opt parent_map id) ~default:[] in
674674- let parents = List.map build_node parent_ids in
675675- let node = { base with parents } in
676676- Hashtbl.replace final_nodes id node;
677677- node
678678- in
679679- let nodes = List.map build_node ordered_ids in
680680- nodes, !invalid))))
739739+ (* Order must follow topological log order: children appear before parents. *)
740740+ let insertion_target_id, preview_before, preview_after =
741741+ select_insertion_anchor ~mode ~target_ids ~nodes_filtered
742742+ in
743743+ let preview_ids =
744744+ match Hashtbl.find_opt preview_by_target insertion_target_id with
745745+ | Some preview_id ->
746746+ [ preview_id ]
747747+ | None ->
748748+ []
749749+ in
750750+ let ordered_ids =
751751+ insert_preview_ids_once
752752+ ~nodes_filtered
753753+ ~insertion_target_id
754754+ ~preview_ids
755755+ ~preview_before
756756+ ~preview_after
757757+ in
758758+ let final_nodes = Hashtbl.create (List.length ordered_ids) in
759759+ let rec build_node id =
760760+ match Hashtbl.find_opt final_nodes id with
761761+ | Some node ->
762762+ node
763763+ | None ->
764764+ let base = Hashtbl.find base_nodes id in
765765+ let parent_ids =
766766+ Option.value (Hashtbl.find_opt parent_map id) ~default:[]
767767+ |> List.filter (fun parent_id -> Hashtbl.mem base_nodes parent_id)
768768+ in
769769+ let parents = List.map build_node parent_ids in
770770+ let node = { base with parents } in
771771+ Hashtbl.replace final_nodes id node;
772772+ node
773773+ in
774774+ let nodes = List.map build_node ordered_ids in
775775+ nodes, None))
681776;;
682777683778(** Insert a preview node after the specified commit.
···15831678 then (
15841679 Buffer.add_string term_buf2 glyphs.(Glyph.termination);
15851680 term_images2
15861586- := Notty.I.string Notty.A.empty glyphs.(Glyph.termination) :: !term_images2)
16811681+ := Notty.I.string Notty.A.empty glyphs.(Glyph.termination)
16821682+ :: !term_images2)
15871683 else (
15881684 let pad_glyph = pad_line_to_glyph row.pad_lines.(i) in
15891685 Buffer.add_string term_buf2 glyphs.(pad_glyph);