···492492 conflicts;
493493 (List.rev !unresolved, !applied)
494494495495+(** Check whether any checkout or monorepo prefix is dirty. Returns
496496+ [Ok ()] when clean, [Error (Dirty_state _)] otherwise. *)
497497+let check_dirty ~sw ~fs_t ~config pkgs =
498498+ Log.info (fun m -> m "Checking status of %d packages" (List.length pkgs));
499499+ let statuses = Status.compute_all ~sw ~fs:fs_t ~config pkgs in
500500+ let dirty_checkouts =
501501+ List.filter Status.has_local_changes statuses
502502+ |> List.map (fun s -> s.Status.package)
503503+ in
504504+ let monorepo = Config.Paths.monorepo config in
505505+ let mono_repo = Git.Repository.open_repo ~sw ~fs:fs_t monorepo in
506506+ let dirty_in_mono =
507507+ if Git.Repository.is_dirty mono_repo then
508508+ List.filter
509509+ (fun pkg ->
510510+ let prefix = Package.subtree_prefix pkg in
511511+ Ctx.is_directory ~fs:fs_t Fpath.(monorepo / prefix))
512512+ pkgs
513513+ else []
514514+ in
515515+ let dirty = dirty_checkouts @ dirty_in_mono in
516516+ if dirty <> [] then Error (Ctx.Dirty_state dirty) else Ok ()
517517+518518+(** Collect outer conflict records from pull results and attempt auto-resolution
519519+ if enabled. Returns the list of unresolved conflict paths. *)
520520+let resolve_conflicts ~sw ~fs_t ~config ~auto ~inner_conflict_paths results =
521521+ let outer_conflict_records =
522522+ List.concat_map
523523+ (fun r ->
524524+ List.map
525525+ (fun (c : Git.Merge.conflict) ->
526526+ ( r.repo_name ^ "/" ^ c.path,
527527+ { c with path = r.repo_name ^ "/" ^ c.path } ))
528528+ r.conflicts)
529529+ results
530530+ in
531531+ let outer_conflicts = List.map fst outer_conflict_records in
532532+ let all_conflicts = inner_conflict_paths @ outer_conflicts in
533533+ match auto with
534534+ | None -> all_conflicts
535535+ | Some auto_opts ->
536536+ let monorepo = Config.Paths.monorepo config in
537537+ let conflict_records = List.map snd outer_conflict_records in
538538+ let unresolved, applied =
539539+ auto_resolve_conflicts ~sw ~fs:fs_t ~monorepo ~auto:auto_opts
540540+ conflict_records
541541+ in
542542+ if applied > 0 then
543543+ Log.app (fun m ->
544544+ m " ✓ auto-resolved %d conflict%s" applied
545545+ (if applied = 1 then "" else "s"));
546546+ inner_conflict_paths @ unresolved
547547+548548+(** Write post-pull metadata files or return a conflict error. *)
549549+let finalize_pull ~proc ~fs_t ~config ~all_pkgs unresolved =
550550+ if unresolved = [] then begin
551551+ Init.write_readme ~proc ~fs:fs_t ~config all_pkgs;
552552+ Init.write_claude_md ~proc ~fs:fs_t ~config;
553553+ Init.write_dune_project ~proc ~fs:fs_t ~config all_pkgs;
554554+ Ok ()
555555+ end
556556+ else
557557+ Error
558558+ (Ctx.Pull_conflict
559559+ {
560560+ paths = unresolved;
561561+ hint =
562562+ "Edit the conflicted files under mono/, stage the resolution \
563563+ with 'git add', 'git commit', and run 'monopam push' again.";
564564+ })
565565+566566+(** Load sources.toml for the monorepo, returning [None] for legacy
567567+ workspaces without one. *)
568568+let load_sources ~fs_t ~config =
569569+ let monorepo = Config.Paths.monorepo config in
570570+ let sources_path = Fpath.(monorepo / "sources.toml") in
571571+ match Sources_registry.load ~fs:(fs_t :> _ Eio.Path.t) sources_path with
572572+ | Ok s -> Some s
573573+ | Error _ -> None
574574+495575let run ~sw ~proc ~fs ~config ?(packages = []) ?opam_repo_url ?auto () =
496576 let ( let* ) = Result.bind in
497577 let fs_t = Ctx.fs_typed fs in
···510590 in
511591 if pkgs = [] && packages <> [] then
512592 Error (Ctx.Package_not_found (List.hd packages))
513513- else begin
514514- Log.info (fun m -> m "Checking status of %d packages" (List.length pkgs));
515515- let statuses = Status.compute_all ~sw ~fs:fs_t ~config pkgs in
516516- let dirty_checkouts =
517517- List.filter Status.has_local_changes statuses
518518- |> List.map (fun s -> s.Status.package)
593593+ else
594594+ let* () = check_dirty ~sw ~fs_t ~config pkgs in
595595+ let inner_conflict_paths = mono_entries ~sw ~proc ~fs ~config in
596596+ let sources = load_sources ~fs_t ~config in
597597+ let repos = Ctx.unique_repos pkgs in
598598+ Log.info (fun m ->
599599+ m "Cloning/fetching %d unique repositories" (List.length repos));
600600+ let* checkout_results = clone_repos ~sw ~proc ~fs:fs_t ~config repos in
601601+ Log.info (fun m -> m "Processing %d unique subtrees" (List.length repos));
602602+ let* results =
603603+ process_subtrees ~sw ~proc ~fs ~config ?sources repos checkout_results
519604 in
520520- (* Refuse to pull when the mono itself has uncommitted changes that
521521- overlap any package's prefix. Pull writes the merged tree back to
522522- the working directory, which would silently overwrite WIP edits. *)
523523- let monorepo = Config.Paths.monorepo config in
524524- let mono_repo = Git.Repository.open_repo ~sw ~fs:fs_t monorepo in
525525- let dirty_in_mono =
526526- if Git.Repository.is_dirty mono_repo then
527527- List.filter
528528- (fun pkg ->
529529- let prefix = Package.subtree_prefix pkg in
530530- Ctx.is_directory ~fs:fs_t Fpath.(monorepo / prefix))
531531- pkgs
532532- else []
605605+ log_pull_results results;
606606+ let unresolved =
607607+ resolve_conflicts ~sw ~fs_t ~config ~auto ~inner_conflict_paths results
533608 in
534534- let dirty = dirty_checkouts @ dirty_in_mono in
535535- if dirty <> [] then Error (Ctx.Dirty_state dirty)
536536- else begin
537537- (* Pull mono inner subtrees first (depth-first); collect any
538538- conflicts from the inner merges so they're reported alongside
539539- outer conflicts at the end. *)
540540- let inner_conflict_paths = mono_entries ~sw ~proc ~fs ~config in
541541- (* Load sources.toml so [subtree] can honor per-entry [path]
542542- overrides. For legacy workspaces without sources.toml the
543543- load returns an empty registry; the path field is always
544544- [None] in that case, which matches today's whole-repo
545545- behaviour. *)
546546- let sources =
547547- let monorepo = Config.Paths.monorepo config in
548548- let sources_path = Fpath.(monorepo / "sources.toml") in
549549- match Sources_registry.load ~fs:(fs_t :> _ Eio.Path.t) sources_path with
550550- | Ok s -> Some s
551551- | Error _ -> None
552552- in
553553- let repos = Ctx.unique_repos pkgs in
554554- Log.info (fun m ->
555555- m "Cloning/fetching %d unique repositories" (List.length repos));
556556- let* checkout_results = clone_repos ~sw ~proc ~fs:fs_t ~config repos in
557557- Log.info (fun m -> m "Processing %d unique subtrees" (List.length repos));
558558- let* results =
559559- process_subtrees ~sw ~proc ~fs ~config ?sources repos checkout_results
560560- in
561561- log_pull_results results;
562562- (* Collect conflicts BEFORE running Init.write_* so we can short
563563- circuit cleanly. The write_* helpers spawn git commit
564564- subprocesses whose output is interleaved with our logs;
565565- leaving them out on the conflict path keeps stdout
566566- deterministic for the user (and for cram tests grepping the
567567- output). *)
568568- let outer_conflict_records =
569569- List.concat_map
570570- (fun r ->
571571- List.map
572572- (fun (c : Git.Merge.conflict) ->
573573- ( r.repo_name ^ "/" ^ c.path,
574574- { c with path = r.repo_name ^ "/" ^ c.path } ))
575575- r.conflicts)
576576- results
577577- in
578578- let outer_conflicts = List.map fst outer_conflict_records in
579579- let all_conflicts = inner_conflict_paths @ outer_conflicts in
580580- (* If --auto is enabled, try to resolve content conflicts via the AI
581581- resolver before deciding the final state. *)
582582- let unresolved_after_auto =
583583- match auto with
584584- | None -> all_conflicts
585585- | Some auto_opts ->
586586- let monorepo = Config.Paths.monorepo config in
587587- let conflict_records = List.map snd outer_conflict_records in
588588- let unresolved, applied =
589589- auto_resolve_conflicts ~sw ~fs:fs_t ~monorepo ~auto:auto_opts
590590- conflict_records
591591- in
592592- if applied > 0 then
593593- Log.app (fun m ->
594594- m " ✓ auto-resolved %d conflict%s" applied
595595- (if applied = 1 then "" else "s"));
596596- inner_conflict_paths @ unresolved
597597- in
598598- if unresolved_after_auto = [] then begin
599599- Init.write_readme ~proc ~fs:fs_t ~config all_pkgs;
600600- Init.write_claude_md ~proc ~fs:fs_t ~config;
601601- Init.write_dune_project ~proc ~fs:fs_t ~config all_pkgs
602602- end;
603603- if unresolved_after_auto <> [] then
604604- Error
605605- (Ctx.Pull_conflict
606606- {
607607- paths = unresolved_after_auto;
608608- hint =
609609- "Edit the conflicted files under mono/, stage the resolution \
610610- with 'git add', 'git commit', and run 'monopam push' again.";
611611- })
612612- else Ok ()
613613- end
614614- end
609609+ finalize_pull ~proc ~fs_t ~config ~all_pkgs unresolved