···8787 let doc = "Show all repos including those not in your workspace." in
8888 Arg.(value & flag & info [ "all"; "a" ] ~doc)
8989 in
9090- let forks_arg =
9191- let doc = "Include fork analysis from verse members (slower)." in
9292- Arg.(value & flag & info [ "forks"; "f" ] ~doc)
9393- in
9494- (* Helper: abbreviate handle to first part before dot, max 4 chars *)
9595- let abbrev_handle h =
9696- match String.split_on_char '.' h with
9797- | first :: _ ->
9898- if String.length first <= 4 then first else String.sub first 0 3
9999- | [] -> h
100100- in
101101- (* Helper: load sources.toml *)
102102- let load_sources ~fs ~config =
103103- let sources_path =
104104- Fpath.(Monopam.Config.Paths.monorepo config / "sources.toml")
105105- in
106106- match
107107- Monopam.Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path
108108- with
109109- | Ok s -> Some s
110110- | Error _ -> None
111111- in
112112- (* Helper: print unregistered opam files if any *)
113113- let print_unregistered ~fs ~config pkgs =
114114- let unregistered = Monopam.find_unregistered_opam_files ~fs ~config pkgs in
115115- if unregistered <> [] then begin
116116- let handle_abbrev =
117117- match Monopam.Verse_config.load ~fs () with
118118- | Ok vc -> abbrev_handle (Monopam.Verse_config.handle vc)
119119- | Error _ -> "local"
120120- in
121121- Fmt.pr "%a %a\n"
122122- Fmt.(styled `Bold string)
123123- "Unregistered:"
124124- Fmt.(styled `Faint int)
125125- (List.length unregistered);
126126- List.iter
127127- (fun (_r, p) ->
128128- Fmt.pr " %-22s %a\n" p
129129- Fmt.(styled `Faint (fun ppf s -> pf ppf "%s~" s))
130130- handle_abbrev)
131131- unregistered
132132- end
133133- in
134134- (* Helper: run fork analysis if requested *)
135135- let print_forks ~proc ~fs ~config ~show_all =
136136- match Monopam.Verse_config.load ~fs () with
137137- | Error _ -> ()
138138- | Ok verse_config ->
139139- let forks =
140140- Monopam.Forks.compute ~proc ~fs ~verse_config ~monopam_config:config
141141- ()
142142- in
143143- if forks.repos <> [] then
144144- Fmt.pr "%a" (Monopam.Forks.pp_summary' ~show_all) forks
145145- in
146146- let run show_all show_forks () =
9090+ let run show_all () =
14791 Eio_main.run @@ fun env ->
14892 with_config env @@ fun config ->
14993 let fs = Eio.Stdenv.fs env in
15094 let proc = Eio.Stdenv.process_mgr env in
15195 match Monopam.status ~proc ~fs ~config () with
15296 | Ok statuses ->
153153- let sources = load_sources ~fs ~config in
9797+ (* Load sources.toml for origin indicators *)
9898+ let sources =
9999+ let mono_path = Monopam.Config.Paths.monorepo config in
100100+ let sources_path = Fpath.(mono_path / "sources.toml") in
101101+ match Monopam.Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path with
102102+ | Ok s -> Some s
103103+ | Error _ -> None
104104+ in
154105 Fmt.pr "%a" (Monopam.Status.pp_summary ?sources) statuses;
106106+ (* Check for unregistered opam files *)
155107 (match Monopam.discover_packages ~fs ~config () with
156156- | Ok pkgs -> print_unregistered ~fs ~config pkgs
108108+ | Ok pkgs ->
109109+ let unregistered =
110110+ Monopam.find_unregistered_opam_files ~fs ~config pkgs
111111+ in
112112+ if unregistered <> [] then begin
113113+ (* Get local handle abbreviation *)
114114+ let handle_abbrev =
115115+ match Monopam.Verse_config.load ~fs () with
116116+ | Ok vc -> (
117117+ let h = Monopam.Verse_config.handle vc in
118118+ match String.split_on_char '.' h with
119119+ | first :: _ ->
120120+ if String.length first <= 4 then first
121121+ else String.sub first 0 3
122122+ | [] -> h)
123123+ | Error _ -> "local"
124124+ in
125125+ Fmt.pr "%a %a\n"
126126+ Fmt.(styled `Bold string)
127127+ "Unregistered:"
128128+ Fmt.(styled `Faint int)
129129+ (List.length unregistered);
130130+ List.iter
131131+ (fun (_r, p) ->
132132+ Fmt.pr " %-22s %a\n" p
133133+ Fmt.(styled `Faint (fun ppf s -> pf ppf "%s~" s))
134134+ handle_abbrev)
135135+ unregistered
136136+ end
157137 | Error _ -> ());
158158- if show_forks then print_forks ~proc ~fs ~config ~show_all;
138138+ (* Fork analysis *)
139139+ (match Monopam.Verse_config.load ~fs () with
140140+ | Error _ -> ()
141141+ | Ok verse_config ->
142142+ let forks =
143143+ Monopam.Forks.compute ~proc ~fs ~verse_config
144144+ ~monopam_config:config ()
145145+ in
146146+ if forks.repos <> [] then
147147+ Fmt.pr "%a" (Monopam.Forks.pp_summary' ~show_all) forks);
159148 `Ok ()
160149 | Error e ->
161150 Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e;
162151 `Error (false, "status failed")
163152 in
164164- Cmd.v info Term.(ret (const run $ all_arg $ forks_arg $ logging_term))
153153+ Cmd.v info Term.(ret (const run $ all_arg $ logging_term))
165154166155(* Sync command *)
167156···237226 in
238227 let run package remote skip_push skip_pull () =
239228 Eio_main.run @@ fun env ->
240240- Eio.Switch.run @@ fun sw ->
241229 with_config env @@ fun config ->
242230 let fs = Eio.Stdenv.fs env in
243231 let proc = Eio.Stdenv.process_mgr env in
244244- let xdg = Xdge.create fs "monopam" in
245232 match
246246- Monopam.sync ~sw ~env ~proc ~fs ~config ~xdg ?package ~remote ~skip_push
247247- ~skip_pull ()
233233+ Monopam.sync ~proc ~fs ~config ?package ~remote ~skip_push ~skip_pull ()
248234 with
249235 | Ok summary ->
250236 if summary.errors = [] then `Ok ()
···461447 [
462448 `S Manpage.s_description;
463449 `P
464464- "Creates a new monopam workspace for monorepo development. The \
465465- workspace lets you manage your own monorepo and optionally browse and \
466466- track other developers' monorepos.";
450450+ "Creates a new monopam workspace for monorepo development. The workspace \
451451+ lets you manage your own monorepo and optionally browse and track other \
452452+ developers' monorepos.";
467453 `S "WORKSPACE STRUCTURE";
468454 `P
469455 "The init command creates the following directory structure at the \
···490476 handle = \"yourname.bsky.social\"";
491477 `S "HANDLE VALIDATION";
492478 `P
493493- "The handle you provide identifies you in the community. It should be \
494494- a valid domain name (e.g., yourname.bsky.social or your-domain.com).";
479479+ "The handle you provide identifies you in the community. \
480480+ It should be a valid domain name (e.g., yourname.bsky.social or \
481481+ your-domain.com).";
495482 `S "REGISTRY";
496483 `P
497484 "The registry is a git repository containing an opamverse.toml file \
···602589 [
603590 `S Manpage.s_description;
604591 `P
605605- "Fork a package from a verse member's opam repository into your \
606606- workspace. This creates entries in your opam-repo with your fork URL \
607607- as the dev-repo.";
592592+ "Fork a package from a verse member's opam repository into your workspace. \
593593+ This creates entries in your opam-repo with your fork URL as the dev-repo.";
608594 `P
609609- "The command finds all packages sharing the same git repository and \
610610- forks them together. For example, if you fork 'cohttp', it will also \
611611- fork cohttp-eio, cohttp-lwt, etc.";
595595+ "The command finds all packages sharing the same git repository and forks \
596596+ them together. For example, if you fork 'cohttp', it will also fork \
597597+ cohttp-eio, cohttp-lwt, etc.";
612598 `S "WHAT IT DOES";
613599 `P "For the specified package:";
614614- `I
615615- ( "1.",
616616- "Looks up the package in <handle>'s opam-repo (verse/<handle>-opam/)"
617617- );
600600+ `I ("1.", "Looks up the package in <handle>'s opam-repo (verse/<handle>-opam/)");
618601 `I ("2.", "Finds all packages from the same git repository");
619602 `I ("3.", "Creates entries in your opam-repo with your fork URL");
620603 `P "After forking:";
621621- `I
622622- ( "1.",
623623- "Commit the new opam files: $(b,cd opam-repo && git add -A && git \
624624- commit)" );
604604+ `I ("1.", "Commit the new opam files: $(b,cd opam-repo && git add -A && git commit)");
625605 `I ("2.", "Run $(b,monopam sync) to pull the fork into your monorepo");
626606 `S "PREREQUISITES";
627607 `P "Before forking:";
628628- `I
629629- ( "-",
630630- "Run $(b,monopam verse pull <handle>) to sync the member's opam-repo"
631631- );
608608+ `I ("-", "Run $(b,monopam verse pull <handle>) to sync the member's opam-repo");
632609 `I ("-", "Create a fork of the repository on GitHub/GitLab/etc.");
633610 `S Manpage.s_examples;
634611 `P "Fork a package from a verse member:";
635635- `Pre
636636- "monopam fork http2 --from sadiq.bsky.social --url \
637637- git@github.com:me/http2.git";
612612+ `Pre "monopam fork http2 --from sadiq.bsky.social --url git@github.com:me/http2.git";
638613 `P "Preview what would be forked (multi-package repos):";
639639- `Pre
640640- "monopam fork cohttp --from avsm.bsky.social --url \
641641- git@github.com:me/cohttp.git --dry-run\n\
642642- Would fork 5 packages from cohttp repository:\n\
643643- \ cohttp\n\
644644- \ cohttp-eio\n\
645645- \ cohttp-lwt\n\
646646- \ cohttp-async\n\
647647- \ cohttp-mirage";
614614+ `Pre "monopam fork cohttp --from avsm.bsky.social --url git@github.com:me/cohttp.git --dry-run\n\
615615+ Would fork 5 packages from cohttp repository:\n\
616616+ \ cohttp\n\
617617+ \ cohttp-eio\n\
618618+ \ cohttp-lwt\n\
619619+ \ cohttp-async\n\
620620+ \ cohttp-mirage";
648621 `P "After forking, commit and sync:";
649649- `Pre
650650- "cd opam-repo && git add -A && git commit -m \"Fork cohttp\"\n\
651651- monopam sync";
622622+ `Pre "cd opam-repo && git add -A && git commit -m \"Fork cohttp\"\n\
623623+ monopam sync";
652624 `S "ERRORS";
653625 `P
654654- "The command will fail if any package from the source repo already \
655655- exists in your opam-repo. Remove conflicting packages first with:";
626626+ "The command will fail if any package from the source repo already exists \
627627+ in your opam-repo. Remove conflicting packages first with:";
656628 `Pre "rm -rf opam-repo/packages/<package-name>";
657629 ]
658630 in
···663635 in
664636 let from_arg =
665637 let doc = "Verse member handle to fork from (e.g., 'avsm.bsky.social')" in
666666- Arg.(
667667- required & opt (some string) None & info [ "from" ] ~docv:"HANDLE" ~doc)
638638+ Arg.(required & opt (some string) None & info [ "from" ] ~docv:"HANDLE" ~doc)
668639 in
669640 let url_arg =
670641 let doc = "Git URL of your fork (e.g., 'git@github.com:you/repo.git')" in
···679650 with_verse_config env @@ fun config ->
680651 let fs = Eio.Stdenv.fs env in
681652 let proc = Eio.Stdenv.process_mgr env in
682682- match
683683- Monopam.Verse.fork ~proc ~fs ~config ~handle ~package ~fork_url ~dry_run
684684- ()
685685- with
653653+ match Monopam.Verse.fork ~proc ~fs ~config ~handle ~package ~fork_url ~dry_run () with
686654 | Ok result ->
687655 if dry_run then begin
688656 Fmt.pr "Would fork %d package(s) from %s:@."
689689- (List.length result.packages_forked)
690690- result.source_handle;
657657+ (List.length result.packages_forked) result.source_handle;
691658 List.iter (fun p -> Fmt.pr " %s@." p) result.packages_forked
692692- end
693693- else begin
659659+ end else begin
694660 (* Update sources.toml with fork information *)
695661 let mono_path = Monopam.Verse_config.mono_path config in
696662 let sources_path = Fpath.(mono_path / "sources.toml") in
697663 let sources =
698698- match
699699- Monopam.Sources_registry.load
700700- ~fs:(fs :> _ Eio.Path.t)
701701- sources_path
702702- with
664664+ match Monopam.Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path with
703665 | Ok s -> s
704666 | Error _ -> Monopam.Sources_registry.empty
705667 in
706706- let entry =
707707- Monopam.Sources_registry.
708708- {
709709- url = result.fork_url;
710710- upstream = Some result.upstream_url;
711711- branch = None;
712712- reason = Some (Fmt.str "Forked from %s" result.source_handle);
713713- origin = Some Join;
714714- (* Forked from verse = joined *)
715715- }
716716- in
717717- let sources =
718718- Monopam.Sources_registry.add sources ~subtree:result.subtree_name
719719- entry
720720- in
721721- (match
722722- Monopam.Sources_registry.save
723723- ~fs:(fs :> _ Eio.Path.t)
724724- sources_path sources
725725- with
668668+ let entry = Monopam.Sources_registry.{
669669+ url = result.fork_url;
670670+ upstream = Some result.upstream_url;
671671+ branch = None;
672672+ reason = Some (Fmt.str "Forked from %s" result.source_handle);
673673+ origin = Some Join; (* Forked from verse = joined *)
674674+ } in
675675+ let sources = Monopam.Sources_registry.add sources ~subtree:result.subtree_name entry in
676676+ (match Monopam.Sources_registry.save ~fs:(fs :> _ Eio.Path.t) sources_path sources with
726677 | Ok () ->
727727- Fmt.pr "Updated sources.toml with fork entry for %s@."
728728- result.subtree_name
678678+ Fmt.pr "Updated sources.toml with fork entry for %s@." result.subtree_name
729679 | Error msg ->
730680 Fmt.epr "Warning: Failed to update sources.toml: %s@." msg);
731681 Fmt.pr "Forked %d package(s): %a@."
732682 (List.length result.packages_forked)
733733- Fmt.(list ~sep:(any ", ") string)
734734- result.packages_forked;
683683+ Fmt.(list ~sep:(any ", ") string) result.packages_forked;
735684 Fmt.pr "@.Next steps:@.";
736736- Fmt.pr
737737- " 1. cd opam-repo && git add -A && git commit -m \"Fork ...\"@.";
685685+ Fmt.pr " 1. cd opam-repo && git add -A && git commit -m \"Fork ...\"@.";
738686 Fmt.pr " 2. monopam sync@."
739687 end;
740688 `Ok ()
···742690 Fmt.epr "Error: %a@." Monopam.Verse.pp_error_with_hint e;
743691 `Error (false, "fork failed")
744692 in
745745- Cmd.v info
746746- Term.(
747747- ret
748748- (const run $ package_arg $ from_arg $ url_arg $ dry_run_arg
749749- $ logging_term))
693693+ Cmd.v info Term.(ret (const run $ package_arg $ from_arg $ url_arg $ dry_run_arg $ logging_term))
750694751695let verse_cmd =
752696 let doc = "Verse member operations" in
···755699 `S Manpage.s_description;
756700 `P
757701 "Commands for working with verse community members. The verse system \
758758- enables federated collaboration across multiple developers' \
759759- monorepos.";
702702+ enables federated collaboration across multiple developers' monorepos.";
760703 `P
761704 "Members are identified by handles - typically domain names like \
762705 'yourname.bsky.social' or 'your-domain.com'.";
763706 `S "NOTE";
764707 `P
765765- "The $(b,monopam init) command creates your workspace and $(b,monopam \
766766- sync) automatically syncs verse members. These commands are for \
767767- additional verse-specific operations.";
708708+ "The $(b,monopam init) command creates your workspace and \
709709+ $(b,monopam sync) automatically syncs verse members. These commands \
710710+ are for additional verse-specific operations.";
768711 `S "COMMANDS";
769712 `I ("members", "List all members in the community registry");
770770- `I
771771- ( "fork <pkg> --from <handle> --url <url>",
772772- "Fork a package from a verse member" );
713713+ `I ("fork <pkg> --from <handle> --url <url>", "Fork a package from a verse member");
773714 `S Manpage.s_examples;
774715 `P "List all community members:";
775716 `Pre "monopam verse members";
776717 `P "Fork a package from another member:";
777777- `Pre
778778- "monopam verse fork cohttp --from avsm.bsky.social --url \
779779- git@github.com:me/cohttp.git";
718718+ `Pre "monopam verse fork cohttp --from avsm.bsky.social --url git@github.com:me/cohttp.git";
780719 ]
781720 in
782721 let info = Cmd.info "verse" ~doc ~man in
783783- Cmd.group info [ verse_members_cmd; verse_fork_cmd ]
722722+ Cmd.group info
723723+ [
724724+ verse_members_cmd;
725725+ verse_fork_cmd;
726726+ ]
784727785728(* Diff command *)
786729···790733 [
791734 `S Manpage.s_description;
792735 `P
793793- "Shows commit diffs from verse members for repositories where they \
794794- have commits you don't have. This helps you see what changes are \
795795- available from collaborators.";
736736+ "Shows commit diffs from verse members for repositories where they have \
737737+ commits you don't have. This helps you see what changes are available \
738738+ from collaborators.";
796739 `S "OUTPUT";
797797- `P
798798- "First shows the verse status summary, then for each repository where \
799799- a verse member is ahead:";
740740+ `P "First shows the verse status summary, then for each repository where \
741741+ a verse member is ahead:";
800742 `I ("Repository name", "With the handle and relationship");
801743 `I ("Commits", "List of commits they have that you don't (max 20)");
802744 `S "RELATIONSHIPS";
803745 `I ("+N", "They have N commits you don't have");
804746 `I ("+N/-M", "Diverged: they have N new commits, you have M new commits");
805747 `S "CACHING";
806806- `P
807807- "Remote fetches are cached for 1 hour to improve performance. Use \
808808- $(b,--refresh) to force fresh fetches from all remotes.";
748748+ `P "Remote fetches are cached for 1 hour to improve performance. \
749749+ Use $(b,--refresh) to force fresh fetches from all remotes.";
809750 `S Manpage.s_examples;
810751 `P "Show diffs for all repos needing attention (uses cache):";
811752 `Pre "monopam diff";
···821762 in
822763 let info = Cmd.info "diff" ~doc ~man in
823764 let arg =
824824- let doc =
825825- "Repository name or commit SHA. If a 7+ character hex string, shows the \
826826- patch for that commit. Otherwise filters to that repository. If not \
827827- specified, shows diffs for all repos needing attention."
828828- in
765765+ let doc = "Repository name or commit SHA. If a 7+ character hex string, shows \
766766+ the patch for that commit. Otherwise filters to that repository. \
767767+ If not specified, shows diffs for all repos needing attention." in
829768 Arg.(value & pos 0 (some string) None & info [] ~docv:"REPO|SHA" ~doc)
830769 in
831770 let refresh_arg =
832832- let doc =
833833- "Force fresh fetches from all remotes, ignoring the 1-hour cache."
834834- in
771771+ let doc = "Force fresh fetches from all remotes, ignoring the 1-hour cache." in
835772 Arg.(value & flag & info [ "refresh"; "r" ] ~doc)
836773 in
837774 let patch_arg =
···846783 let proc = Eio.Stdenv.process_mgr env in
847784 (* Check if arg looks like a commit SHA *)
848785 match arg with
849849- | Some sha when Monopam.is_commit_sha sha -> (
786786+ | Some sha when Monopam.is_commit_sha sha ->
850787 (* Show patch for specific commit *)
851851- match
852852- Monopam.diff_show_commit ~proc ~fs ~config ~verse_config ~sha ~refresh
853853- ()
854854- with
788788+ (match Monopam.diff_show_commit ~proc ~fs ~config ~verse_config ~sha ~refresh () with
855789 | Some info ->
856856- let short_hash =
857857- String.sub info.commit_hash 0
858858- (min 7 (String.length info.commit_hash))
859859- in
790790+ let short_hash = String.sub info.commit_hash 0 (min 7 (String.length info.commit_hash)) in
860791 Fmt.pr "%a %s (%s/%s)@.@.%s@."
861861- Fmt.(styled `Yellow string)
862862- short_hash info.commit_subject info.commit_repo info.commit_handle
792792+ Fmt.(styled `Yellow string) short_hash
793793+ info.commit_subject
794794+ info.commit_repo info.commit_handle
863795 info.commit_patch;
864796 `Ok ()
865797 | None ->
866798 Fmt.epr "Commit %s not found in any verse diff@." sha;
867799 `Error (false, "commit not found"))
868800 | repo ->
869869- let result =
870870- Monopam.diff ~proc ~fs ~config ~verse_config ?repo ~refresh ~patch ()
871871- in
801801+ let result = Monopam.diff ~proc ~fs ~config ~verse_config ?repo ~refresh ~patch () in
872802 Fmt.pr "%a" (Monopam.pp_diff_result ~show_patch:patch) result;
873803 `Ok ()
874804 in
875875- Cmd.v info
876876- Term.(ret (const run $ arg $ refresh_arg $ patch_arg $ logging_term))
805805+ Cmd.v info Term.(ret (const run $ arg $ refresh_arg $ patch_arg $ logging_term))
877806878807(* Pull command - pull from verse members *)
879808···893822 `I ("3.", "$(b,monopam sync) - Sync changes into your monorepo");
894823 `S "MERGING BEHAVIOR";
895824 `P "When you're behind (they have commits you don't):";
896896- `I
897897- ( "Fast-forward",
898898- "If your branch has no new commits, a fast-forward merge is used." );
825825+ `I ("Fast-forward", "If your branch has no new commits, a fast-forward merge is used.");
899826 `P "When branches have diverged (both have new commits):";
900827 `I ("Merge commit", "A merge commit is created to combine the histories.");
901828 `S Manpage.s_examples;
···909836 in
910837 let info = Cmd.info "pull" ~doc ~man in
911838 let handle_arg =
912912- let doc =
913913- "The verse member handle to pull from (e.g., avsm.bsky.social)."
914914- in
839839+ let doc = "The verse member handle to pull from (e.g., avsm.bsky.social)." in
915840 Arg.(required & pos 0 (some string) None & info [] ~docv:"HANDLE" ~doc)
916841 in
917842 let repo_arg =
918918- let doc =
919919- "Optional repository to pull from. If not specified, pulls from all \
920920- repositories where the handle has commits you don't have."
921921- in
843843+ let doc = "Optional repository to pull from. If not specified, pulls from all \
844844+ repositories where the handle has commits you don't have." in
922845 Arg.(value & pos 1 (some string) None & info [] ~docv:"REPO" ~doc)
923846 in
924847 let refresh_arg =
925925- let doc =
926926- "Force fresh fetches from all remotes, ignoring the 1-hour cache."
927927- in
848848+ let doc = "Force fresh fetches from all remotes, ignoring the 1-hour cache." in
928849 Arg.(value & flag & info [ "refresh"; "r" ] ~doc)
929850 in
930851 let run handle repo refresh () =
···933854 with_verse_config env @@ fun verse_config ->
934855 let fs = Eio.Stdenv.fs env in
935856 let proc = Eio.Stdenv.process_mgr env in
936936- match
937937- Monopam.pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo
938938- ~refresh ()
939939- with
857857+ match Monopam.pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo ~refresh () with
940858 | Ok result ->
941859 Fmt.pr "%a" Monopam.pp_handle_pull_result result;
942860 if result.repos_failed <> [] then
···946864 `Ok ()
947865 end
948866 else begin
949949- Fmt.pr
950950- "@.Run $(b,monopam sync) to merge changes into your monorepo.@.";
867867+ Fmt.pr "@.Run $(b,monopam sync) to merge changes into your monorepo.@.";
951868 `Ok ()
952869 end
953870 | Error e ->
954871 Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e;
955872 `Error (false, "pull failed")
956873 in
957957- Cmd.v info
958958- Term.(ret (const run $ handle_arg $ repo_arg $ refresh_arg $ logging_term))
874874+ Cmd.v info Term.(ret (const run $ handle_arg $ repo_arg $ refresh_arg $ logging_term))
959875960876(* Cherrypick command *)
961877···965881 [
966882 `S Manpage.s_description;
967883 `P
968968- "Applies a specific commit from a verse member's fork to your local \
969969- checkout. Use $(b,monopam diff) to see available commits and their \
970970- hashes.";
884884+ "Applies a specific commit from a verse member's fork to your local checkout. \
885885+ Use $(b,monopam diff) to see available commits and their hashes.";
971886 `S "WORKFLOW";
972887 `P "The typical workflow for cherry-picking specific commits:";
973888 `I ("1.", "$(b,monopam diff) - See available commits with their hashes");
···984899 in
985900 let info = Cmd.info "cherrypick" ~doc ~man in
986901 let sha_arg =
987987- let doc =
988988- "The commit SHA (or prefix) to cherry-pick. Must be at least 7 \
989989- characters."
990990- in
902902+ let doc = "The commit SHA (or prefix) to cherry-pick. Must be at least 7 characters." in
991903 Arg.(required & pos 0 (some string) None & info [] ~docv:"SHA" ~doc)
992904 in
993905 let refresh_arg =
994994- let doc =
995995- "Force fresh fetches from all remotes, ignoring the 1-hour cache."
996996- in
906906+ let doc = "Force fresh fetches from all remotes, ignoring the 1-hour cache." in
997907 Arg.(value & flag & info [ "refresh"; "r" ] ~doc)
998908 in
999909 let run sha refresh () =
···1002912 with_verse_config env @@ fun verse_config ->
1003913 let fs = Eio.Stdenv.fs env in
1004914 let proc = Eio.Stdenv.process_mgr env in
10051005- match
10061006- Monopam.cherrypick ~proc ~fs ~config ~verse_config ~sha ~refresh ()
10071007- with
915915+ match Monopam.cherrypick ~proc ~fs ~config ~verse_config ~sha ~refresh () with
1008916 | Ok result ->
1009917 Fmt.pr "%a" Monopam.pp_cherrypick_result result;
1010918 Fmt.pr "Run $(b,monopam sync) to merge changes into your monorepo.@.";
···1074982 in
1075983 let quiet_arg =
1076984 let doc =
10771077- "Quiet mode for cron jobs. Only output if issues are found. Exit code \
10781078- reflects health status (0=healthy, 1=warning, 2=critical)."
985985+ "Quiet mode for cron jobs. Only output if issues are found. \
986986+ Exit code reflects health status (0=healthy, 1=warning, 2=critical)."
1079987 in
1080988 Arg.(value & flag & info [ "quiet"; "q" ] ~doc)
1081989 in
1082990 let run package json no_sync quiet () =
1083991 Eio_main.run @@ fun env ->
10841084- Eio.Switch.run @@ fun sw ->
1085992 with_config env @@ fun config ->
1086993 with_verse_config env @@ fun verse_config ->
1087994 let fs = Eio.Stdenv.fs env in
1088995 let proc = Eio.Stdenv.process_mgr env in
1089996 let clock = Eio.Stdenv.clock env in
10901090- let xdg = Xdge.create fs "monopam" in
1091997 (* Run sync before analysis unless --no-sync is specified *)
10921092- if (not no_sync) && not quiet then begin
998998+ if not no_sync && not quiet then begin
1093999 Fmt.pr "Syncing workspace before analysis...@.";
10941094- match Monopam.sync ~sw ~env ~proc ~fs ~config ~xdg ?package () with
10001000+ match Monopam.sync ~proc ~fs ~config ?package () with
10951001 | Ok _summary -> ()
10961002 | Error e ->
10971003 Fmt.pr "Warning: sync failed: %a@." Monopam.pp_error_with_hint e;
···10991005 end
11001006 else if not no_sync then begin
11011007 (* Quiet mode but still sync - just don't print progress *)
11021102- let _ = Monopam.sync ~sw ~env ~proc ~fs ~config ~xdg ?package () in
11031103- ()
10081008+ let _ = Monopam.sync ~proc ~fs ~config ?package () in ()
11041009 end;
11051010 let report =
11061011 Monopam.Doctor.analyze ~proc ~fs ~config ~verse_config ~clock ?package
···11241029 end
11251030 in
11261031 Cmd.v info
11271127- Term.(
11281128- ret
11291129- (const run $ package_arg $ json_arg $ no_sync_arg $ quiet_arg
11301130- $ logging_term))
10321032+ Term.(ret (const run $ package_arg $ json_arg $ no_sync_arg $ quiet_arg $ logging_term))
1131103311321034(* Feature commands *)
11331035···11411043 [
11421044 `S Manpage.s_description;
11431045 `P
11441144- "Creates a git worktree at $(b,root/work/<name>) with a new branch \
11451145- named $(b,<name>). This allows parallel development on separate \
11461146- branches, useful for having multiple Claude instances working on \
11471147- different features.";
10461046+ "Creates a git worktree at $(b,root/work/<name>) with a new branch named \
10471047+ $(b,<name>). This allows parallel development on separate branches, \
10481048+ useful for having multiple Claude instances working on different features.";
11481049 `S "HOW IT WORKS";
11491050 `P "The command:";
11501051 `I ("1.", "Creates the $(b,work/) directory if it doesn't exist");
···11521053 `I ("3.", "Checks out a new branch named $(b,<name>)");
11531054 `S Manpage.s_examples;
11541055 `P "Create a feature worktree:";
11551155- `Pre
11561156- "monopam feature add my-feature\n\
11571157- cd work/my-feature\n\
11581158- # Now you can work here independently";
10561056+ `Pre "monopam feature add my-feature\n\
10571057+ cd work/my-feature\n\
10581058+ # Now you can work here independently";
11591059 `P "Have multiple Claudes work in parallel:";
11601160- `Pre
11611161- "# Terminal 1\n\
11621162- monopam feature add auth-system\n\
11631163- cd work/auth-system && claude\n\n\
11641164- # Terminal 2\n\
11651165- monopam feature add api-refactor\n\
11661166- cd work/api-refactor && claude";
10601060+ `Pre "# Terminal 1\n\
10611061+ monopam feature add auth-system\n\
10621062+ cd work/auth-system && claude\n\n\
10631063+ # Terminal 2\n\
10641064+ monopam feature add api-refactor\n\
10651065+ cd work/api-refactor && claude";
11671066 ]
11681067 in
11691068 let info = Cmd.info "add" ~doc ~man in
···11741073 let proc = Eio.Stdenv.process_mgr env in
11751074 match Monopam.Feature.add ~proc ~fs ~config:verse_config ~name () with
11761075 | Ok entry ->
11771177- Fmt.pr "Created feature worktree '%s' at %a@." entry.name Fpath.pp
11781178- entry.path;
10761076+ Fmt.pr "Created feature worktree '%s' at %a@." entry.name Fpath.pp entry.path;
11791077 Fmt.pr "@.To start working:@.";
11801078 Fmt.pr " cd %a@." Fpath.pp entry.path;
11811079 `Ok ()
···12121110 with_verse_config env @@ fun verse_config ->
12131111 let fs = Eio.Stdenv.fs env in
12141112 let proc = Eio.Stdenv.process_mgr env in
12151215- match
12161216- Monopam.Feature.remove ~proc ~fs ~config:verse_config ~name ~force ()
12171217- with
11131113+ match Monopam.Feature.remove ~proc ~fs ~config:verse_config ~name ~force () with
12181114 | Ok () ->
12191115 Fmt.pr "Removed feature worktree '%s'.@." name;
12201116 `Ok ()
···12221118 Fmt.epr "Error: %a@." Monopam.Feature.pp_error_with_hint e;
12231119 `Error (false, "feature remove failed")
12241120 in
12251225- Cmd.v info
12261226- Term.(ret (const run $ feature_name_arg $ force_arg $ logging_term))
11211121+ Cmd.v info Term.(ret (const run $ feature_name_arg $ force_arg $ logging_term))
1227112212281123let feature_list_cmd =
12291124 let doc = "List all feature worktrees" in
···12421137 let fs = Eio.Stdenv.fs env in
12431138 let proc = Eio.Stdenv.process_mgr env in
12441139 let entries = Monopam.Feature.list ~proc ~fs ~config:verse_config () in
12451245- if entries = [] then Fmt.pr "No feature worktrees found.@."
11401140+ if entries = [] then
11411141+ Fmt.pr "No feature worktrees found.@."
12461142 else begin
12471143 Fmt.pr "Feature worktrees:@.";
12481248- List.iter
12491249- (fun entry ->
12501250- Fmt.pr " %s -> %a (branch: %s)@." entry.Monopam.Feature.name Fpath.pp
12511251- entry.Monopam.Feature.path entry.Monopam.Feature.branch)
12521252- entries
11441144+ List.iter (fun entry ->
11451145+ Fmt.pr " %s -> %a (branch: %s)@."
11461146+ entry.Monopam.Feature.name
11471147+ Fpath.pp entry.Monopam.Feature.path
11481148+ entry.Monopam.Feature.branch
11491149+ ) entries
12531150 end;
12541151 `Ok ()
12551152 in
···12661163 working on different features simultaneously.";
12671164 `S "WORKSPACE STRUCTURE";
12681165 `P "Feature worktrees are created in the $(b,work/) directory:";
12691269- `Pre
12701270- "root/\n\
12711271- ├── mono/ # Main monorepo\n\
12721272- ├── work/\n\
12731273- │ ├── feature-a/ # Worktree on branch 'feature-a'\n\
12741274- │ └── feature-b/ # Worktree on branch 'feature-b'\n\
12751275- └── ...";
11661166+ `Pre "root/\n\
11671167+ ├── mono/ # Main monorepo\n\
11681168+ ├── work/\n\
11691169+ │ ├── feature-a/ # Worktree on branch 'feature-a'\n\
11701170+ │ └── feature-b/ # Worktree on branch 'feature-b'\n\
11711171+ └── ...";
12761172 `S "COMMANDS";
12771173 `I ("add <name>", "Create a new feature worktree");
12781174 `I ("remove <name>", "Remove a feature worktree");
12791175 `I ("list", "List all feature worktrees");
12801176 `S "WORKFLOW";
12811177 `P "Typical workflow for parallel development:";
12821282- `Pre
12831283- "# Create feature worktrees\n\
12841284- monopam feature add auth-system\n\
12851285- monopam feature add api-cleanup\n\n\
12861286- # Work in each worktree independently\n\
12871287- cd work/auth-system && claude\n\
12881288- cd work/api-cleanup && claude\n\n\
12891289- # When done, merge branches back to main\n\
12901290- cd mono\n\
12911291- git merge auth-system\n\
12921292- git merge api-cleanup\n\n\
12931293- # Clean up worktrees\n\
12941294- monopam feature remove auth-system\n\
12951295- monopam feature remove api-cleanup";
11781178+ `Pre "# Create feature worktrees\n\
11791179+ monopam feature add auth-system\n\
11801180+ monopam feature add api-cleanup\n\n\
11811181+ # Work in each worktree independently\n\
11821182+ cd work/auth-system && claude\n\
11831183+ cd work/api-cleanup && claude\n\n\
11841184+ # When done, merge branches back to main\n\
11851185+ cd mono\n\
11861186+ git merge auth-system\n\
11871187+ git merge api-cleanup\n\n\
11881188+ # Clean up worktrees\n\
11891189+ monopam feature remove auth-system\n\
11901190+ monopam feature remove api-cleanup";
12961191 ]
12971192 in
12981193 let info = Cmd.info "feature" ~doc ~man in
···13141209 .devcontainer configuration, it will be created automatically.";
13151210 `P
13161211 "This is the recommended way to get started with monopam. The \
13171317- devcontainer provides a consistent environment with OCaml, opam, and \
13181318- all required tools pre-installed.";
12121212+ devcontainer provides a consistent environment with OCaml, opam, \
12131213+ and all required tools pre-installed.";
13191214 `S "WHAT IT DOES";
13201215 `P "For a new directory (no .devcontainer/):";
13211216 `I ("1.", "Creates the target directory if needed");
···13271222 `I ("1.", "Starts the devcontainer if not running");
13281223 `I ("2.", "Opens an interactive shell inside the container");
13291224 `S Manpage.s_options;
13301330- `P
13311331- "Use $(b,--url) to specify a custom devcontainer.json URL if you want \
13321332- to use a different base configuration.";
12251225+ `P "Use $(b,--url) to specify a custom devcontainer.json URL if you want \
12261226+ to use a different base configuration.";
13331227 `S Manpage.s_examples;
13341228 `P "Create a new devcontainer workspace:";
13351229 `Pre "monopam devcontainer ~/my-ocaml-project";
13361230 `P "Enter an existing devcontainer:";
13371231 `Pre "monopam devcontainer ~/my-ocaml-project";
13381232 `P "Use a custom devcontainer.json:";
13391339- `Pre
13401340- "monopam devcontainer --url https://example.com/devcontainer.json \
13411341- ~/project";
12331233+ `Pre "monopam devcontainer --url https://example.com/devcontainer.json ~/project";
13421234 ]
13431235 in
13441236 let info = Cmd.info "devcontainer" ~doc ~man in
···13471239 Arg.(required & pos 0 (some string) None & info [] ~docv:"PATH" ~doc)
13481240 in
13491241 let url_arg =
13501350- let doc =
13511351- "URL to fetch devcontainer.json from. Defaults to the \
13521352- claude-ocaml-devcontainer template."
13531353- in
13541354- Arg.(
13551355- value
13561356- & opt string default_devcontainer_url
13571357- & info [ "url" ] ~docv:"URL" ~doc)
12421242+ let doc = "URL to fetch devcontainer.json from. Defaults to the claude-ocaml-devcontainer template." in
12431243+ Arg.(value & opt string default_devcontainer_url & info ["url"] ~docv:"URL" ~doc)
13581244 in
13591245 let run path url () =
13601246 (* Resolve to absolute path *)
13611247 let abs_path =
13621362- if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path
12481248+ if Filename.is_relative path then
12491249+ Filename.concat (Sys.getcwd ()) path
13631250 else path
13641251 in
13651252 let devcontainer_dir = Filename.concat abs_path ".devcontainer" in
13661366- let devcontainer_json =
13671367- Filename.concat devcontainer_dir "devcontainer.json"
13681368- in
12531253+ let devcontainer_json = Filename.concat devcontainer_dir "devcontainer.json" in
13691254 (* Check if .devcontainer exists *)
13701370- let needs_init =
13711371- not (Sys.file_exists devcontainer_dir && Sys.is_directory devcontainer_dir)
13721372- in
12551255+ let needs_init = not (Sys.file_exists devcontainer_dir && Sys.is_directory devcontainer_dir) in
13731256 if needs_init then begin
13741257 Fmt.pr "Initializing devcontainer in %s...@." abs_path;
13751258 (* Create directories *)
13761376- (try Unix.mkdir abs_path 0o755
13771377- with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
13781378- (try Unix.mkdir devcontainer_dir 0o755
13791379- with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
12591259+ (try Unix.mkdir abs_path 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
12601260+ (try Unix.mkdir devcontainer_dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
13801261 (* Fetch devcontainer.json using curl *)
13811262 Fmt.pr "Fetching devcontainer.json from %s...@." url;
13821382- let curl_cmd =
13831383- Printf.sprintf "curl -fsSL '%s' -o '%s'" url devcontainer_json
13841384- in
12631263+ let curl_cmd = Printf.sprintf "curl -fsSL '%s' -o '%s'" url devcontainer_json in
13851264 let ret = Sys.command curl_cmd in
13861265 if ret <> 0 then begin
13871387- Fmt.epr "Error: Failed to fetch devcontainer.json (curl exit code %d)@."
13881388- ret;
12661266+ Fmt.epr "Error: Failed to fetch devcontainer.json (curl exit code %d)@." ret;
13891267 exit 1
13901268 end;
13911269 Fmt.pr "Created %s@." devcontainer_json;
13921270 (* Build and start the devcontainer *)
13931271 Fmt.pr "Building devcontainer (this may take a while on first run)...@.";
13941394- let up_cmd =
13951395- Printf.sprintf
13961396- "npx @devcontainers/cli up --workspace-folder '%s' \
13971397- --remove-existing-container"
13981398- abs_path
13991399- in
12721272+ let up_cmd = Printf.sprintf "npx @devcontainers/cli up --workspace-folder '%s' --remove-existing-container" abs_path in
14001273 let ret = Sys.command up_cmd in
14011274 if ret <> 0 then begin
14021275 Fmt.epr "Error: Failed to start devcontainer (exit code %d)@." ret;
···14051278 end;
14061279 (* Exec into the devcontainer *)
14071280 Fmt.pr "Entering devcontainer...@.";
14081408- let exec_cmd =
14091409- Printf.sprintf
14101410- "npx @devcontainers/cli exec --workspace-folder '%s' bash -l" abs_path
14111411- in
12811281+ let exec_cmd = Printf.sprintf "npx @devcontainers/cli exec --workspace-folder '%s' bash -l" abs_path in
14121282 let ret = Sys.command exec_cmd in
14131283 if ret <> 0 then
14141284 `Error (false, Printf.sprintf "devcontainer exec failed with code %d" ret)
14151415- else `Ok ()
12851285+ else
12861286+ `Ok ()
14161287 in
14171288 Cmd.v info Term.(ret (const run $ path_arg $ url_arg $ logging_term))
14181289···14451316 with the extracted history, then re-adds mono/<name>/ as a subtree.";
14461317 `S "FORK MODES";
14471318 `P "The fork command handles two scenarios:";
14481448- `I
14491449- ( "Subtree with history",
14501450- "For subtrees added via $(b,git subtree add) or $(b,monopam join), \
14511451- the command uses $(b,git subtree split) to extract the full commit \
14521452- history into the new repository." );
14531453- `I
14541454- ( "Fresh package",
14551455- "For packages created directly in mono/ without subtree history, the \
14561456- command copies the files and creates an initial commit. This is \
14571457- useful for new packages you've developed locally." );
13191319+ `I ("Subtree with history", "For subtrees added via $(b,git subtree add) or \
13201320+ $(b,monopam join), the command uses $(b,git subtree split) to extract \
13211321+ the full commit history into the new repository.");
13221322+ `I ("Fresh package", "For packages created directly in mono/ without subtree \
13231323+ history, the command copies the files and creates an initial commit. \
13241324+ This is useful for new packages you've developed locally.");
14581325 `S "WHAT IT DOES";
14591326 `P "The fork command performs a complete workflow in one step:";
14601327 `I ("1.", "Analyzes mono/<name>/ to detect fork mode");
14611328 `I ("2.", "Builds an action plan and shows discovery details");
14621329 `I ("3.", "Prompts for confirmation (use $(b,--yes) to skip)");
14631330 `I ("4.", "Creates a new git repo at src/<name>/");
14641464- `I
14651465- ( "5.",
14661466- "Extracts history (subtree split) or copies files (fresh package)" );
13311331+ `I ("5.", "Extracts history (subtree split) or copies files (fresh package)");
14671332 `I ("6.", "Removes mono/<name>/ from git and commits");
14681333 `I ("7.", "Re-adds mono/<name>/ as a proper subtree from src/<name>/");
14691334 `I ("8.", "Updates sources.toml with $(b,origin = \"fork\")");
···15161381 let mono_path = Monopam.Config.mono_path config in
15171382 let subtree_path = Fpath.(mono_path / name) in
15181383 let knot = Monopam.Config.knot config in
15191519- let suggested =
15201520- Monopam.Fork_join.suggest_push_url ~fs ~knot subtree_path
15211521- in
15221522- if yes || dry_run then suggested
15231523- (* Use suggested or None without prompting *)
13841384+ let suggested = Monopam.Fork_join.suggest_push_url ~fs ~knot subtree_path in
13851385+ if yes || dry_run then
13861386+ suggested (* Use suggested or None without prompting *)
15241387 else begin
15251388 match suggested with
15261526- | Some default_url -> (
13891389+ | Some default_url ->
15271390 Fmt.pr "Remote push URL [%s]: %!" default_url;
15281528- match prompt_string "" with
15291529- | None -> Some default_url (* User pressed enter, use default *)
15301530- | Some entered -> Some entered)
13911391+ (match prompt_string "" with
13921392+ | None -> Some default_url (* User pressed enter, use default *)
13931393+ | Some entered -> Some entered)
15311394 | None ->
15321395 Fmt.pr "Remote push URL (leave empty to skip): %!";
15331396 prompt_string ""
15341397 end
15351398 in
15361399 (* Build the plan *)
15371537- match
15381538- Monopam.Fork_join.plan_fork ~proc ~fs ~config ~name ?push_url:url ~dry_run
15391539- ()
15401540- with
14001400+ match Monopam.Fork_join.plan_fork ~proc ~fs ~config ~name ?push_url:url ~dry_run () with
15411401 | Error e ->
15421402 Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
15431403 `Error (false, "fork failed")
···15451405 (* Print discovery and actions *)
15461406 Fmt.pr "Analyzing fork request for '%s'...@.@." name;
15471407 Fmt.pr "Discovery:@.%a@." Monopam.Fork_join.pp_discovery plan.discovery;
15481548- (match url with Some u -> Fmt.pr " Remote URL: %s@." u | None -> ());
14081408+ (match url with
14091409+ | Some u -> Fmt.pr " Remote URL: %s@." u
14101410+ | None -> ());
15491411 Fmt.pr "@.Actions to perform:@.";
15501550- List.iteri
15511551- (fun i action ->
15521552- Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action)
15531553- plan.actions;
14121412+ List.iteri (fun i action ->
14131413+ Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action
14141414+ ) plan.actions;
15541415 Fmt.pr "@.";
15551416 (* Prompt for confirmation unless --yes or --dry-run *)
15561417 let proceed =
15571418 if dry_run then begin
15581419 Fmt.pr "(dry-run mode - no changes will be made)@.";
15591420 true
15601560- end
15611561- else if yes then true
15621562- else confirm "Proceed?"
14211421+ end else if yes then
14221422+ true
14231423+ else
14241424+ confirm "Proceed?"
15631425 in
15641426 if not proceed then begin
15651427 Fmt.pr "Cancelled.@.";
15661428 `Ok ()
15671567- end
15681568- else begin
14291429+ end else begin
15691430 (* Execute the plan *)
15701431 match Monopam.Fork_join.execute_fork_plan ~proc ~fs plan with
15711432 | Ok result ->
···15741435 Fmt.pr "@.Next steps:@.";
15751436 Fmt.pr " 1. Review the new repo: cd src/%s@." result.name;
15761437 match url with
15771577- | Some _ ->
15781578- Fmt.pr " 2. Push to remote: git push -u origin main@."
15791579- | None ->
15801580- Fmt.pr " 2. Add a remote: git remote add origin <url>@."
14381438+ | Some _ -> Fmt.pr " 2. Push to remote: git push -u origin main@."
14391439+ | None -> Fmt.pr " 2. Add a remote: git remote add origin <url>@."
15811440 end;
15821441 `Ok ()
15831442 | Error e ->
···15851444 `Error (false, "fork failed")
15861445 end
15871446 in
15881588- Cmd.v info
15891589- Term.(
15901590- ret (const run $ name_arg $ url_arg $ dry_run_arg $ yes_arg $ logging_term))
14471447+ Cmd.v info Term.(ret (const run $ name_arg $ url_arg $ dry_run_arg $ yes_arg $ logging_term))
1591144815921449(* Join command *)
15931450···16021459 `S "JOIN MODES";
16031460 `P "The join command handles multiple scenarios:";
16041461 `I ("URL join", "Clone from a git URL and add as subtree (default).");
16051605- `I
16061606- ( "Local directory join",
16071607- "Import from a local filesystem path. If the path is a git repo, \
16081608- uses it directly. If not, initializes a new repo." );
16091609- `I
16101610- ( "Verse join",
16111611- "Join from a verse member's repository using $(b,--from)." );
14621462+ `I ("Local directory join", "Import from a local filesystem path. If the \
14631463+ path is a git repo, uses it directly. If not, initializes a new repo.");
14641464+ `I ("Verse join", "Join from a verse member's repository using $(b,--from).");
16121465 `S "WHAT IT DOES";
16131466 `P "The join command:";
16141467 `I ("1.", "Analyzes the source (URL or local path)");
···16191472 `I ("6.", "Updates sources.toml with $(b,origin = \"join\")");
16201473 `S "JOINING FROM VERSE";
16211474 `P "To join a package from a verse member, use $(b,--from):";
16221622- `Pre
16231623- "monopam join --from avsm.bsky.social --url \
16241624- git@github.com:me/cohttp.git cohttp";
14751475+ `Pre "monopam join --from avsm.bsky.social --url git@github.com:me/cohttp.git cohttp";
16251476 `P "This will:";
16261477 `I ("-", "Look up the package in their opam-repo");
16271478 `I ("-", "Find all packages from the same git repository");
···16421493 `P "Join with a custom name using --as:";
16431494 `Pre "monopam join https://github.com/someone/some-lib --as my-lib";
16441495 `P "Join with upstream tracking (for forks):";
16451645- `Pre
16461646- "monopam join https://github.com/me/cohttp --upstream \
16471647- https://github.com/mirage/cohttp";
14961496+ `Pre "monopam join https://github.com/me/cohttp --upstream https://github.com/mirage/cohttp";
16481497 `P "Join from a verse member:";
16491649- `Pre
16501650- "monopam join cohttp --from avsm.bsky.social --url \
16511651- git@github.com:me/cohttp.git";
14981498+ `Pre "monopam join cohttp --from avsm.bsky.social --url git@github.com:me/cohttp.git";
16521499 `P "Preview what would be done:";
16531500 `Pre "monopam join https://github.com/someone/lib --dry-run";
16541501 `P "Join without confirmation:";
···16901537 let fs = Eio.Stdenv.fs env in
16911538 let proc = Eio.Stdenv.process_mgr env in
16921539 match from with
16931693- | Some handle -> (
15401540+ | Some handle ->
16941541 (* Join from verse member - requires --url for your fork *)
16951542 (* Uses legacy API as it involves verse-specific operations *)
16961696- match fork_url with
16971697- | None ->
16981698- Fmt.epr "Error: --url is required when using --from@.";
16991699- `Error (false, "--url required")
17001700- | Some fork_url -> (
17011701- match
17021702- Monopam.Fork_join.join_from_verse ~proc ~fs ~config
17031703- ~verse_config:config ~package:url_or_pkg ~handle ~fork_url
17041704- ~dry_run ()
17051705- with
17061706- | Ok result ->
17071707- if dry_run then begin
17081708- Fmt.pr "Would join '%s' from %s:@." result.name
17091709- (Option.value ~default:"verse" result.from_handle);
17101710- Fmt.pr " Source: %s@." result.source_url;
17111711- Option.iter
17121712- (fun u -> Fmt.pr " Upstream: %s@." u)
17131713- result.upstream_url;
17141714- Fmt.pr " Packages: %a@."
17151715- Fmt.(list ~sep:(any ", ") string)
17161716- result.packages_added
17171717- end
17181718- else begin
17191719- Fmt.pr "%a@." Monopam.Fork_join.pp_join_result result;
17201720- Fmt.pr "@.Next steps:@.";
17211721- Fmt.pr
17221722- " 1. Commit the opam changes: cd opam-repo && git add -A \
17231723- && git commit@.";
17241724- Fmt.pr " 2. Run $(b,monopam sync) to synchronize@."
17251725- end;
17261726- `Ok ()
17271727- | Error e ->
17281728- Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
17291729- `Error (false, "join failed")))
17301730- | None -> (
15431543+ (match fork_url with
15441544+ | None ->
15451545+ Fmt.epr "Error: --url is required when using --from@.";
15461546+ `Error (false, "--url required")
15471547+ | Some fork_url ->
15481548+ match Monopam.Fork_join.join_from_verse ~proc ~fs ~config ~verse_config:config
15491549+ ~package:url_or_pkg ~handle ~fork_url ~dry_run () with
15501550+ | Ok result ->
15511551+ if dry_run then begin
15521552+ Fmt.pr "Would join '%s' from %s:@." result.name (Option.value ~default:"verse" result.from_handle);
15531553+ Fmt.pr " Source: %s@." result.source_url;
15541554+ Option.iter (fun u -> Fmt.pr " Upstream: %s@." u) result.upstream_url;
15551555+ Fmt.pr " Packages: %a@." Fmt.(list ~sep:(any ", ") string) result.packages_added
15561556+ end else begin
15571557+ Fmt.pr "%a@." Monopam.Fork_join.pp_join_result result;
15581558+ Fmt.pr "@.Next steps:@.";
15591559+ Fmt.pr " 1. Commit the opam changes: cd opam-repo && git add -A && git commit@.";
15601560+ Fmt.pr " 2. Run $(b,monopam sync) to synchronize@."
15611561+ end;
15621562+ `Ok ()
15631563+ | Error e ->
15641564+ Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
15651565+ `Error (false, "join failed"))
15661566+ | None ->
17311567 (* Normal join from URL or local path - use plan-based workflow *)
17321568 let source = match fork_url with Some u -> u | None -> url_or_pkg in
17331733- let name =
17341734- match fork_url with Some _ -> Some url_or_pkg | None -> as_name
17351735- in
15691569+ let name = match fork_url with Some _ -> Some url_or_pkg | None -> as_name in
17361570 (* Build the plan *)
17371737- match
17381738- Monopam.Fork_join.plan_join ~proc ~fs ~config ~source ?name ?upstream
17391739- ~dry_run ()
17401740- with
15711571+ match Monopam.Fork_join.plan_join ~proc ~fs ~config ~source ?name ?upstream ~dry_run () with
17411572 | Error e ->
17421573 Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
17431574 `Error (false, "join failed")
···17501581 (if is_local then "local directory" else "remote URL");
17511582 Fmt.pr "%a" Monopam.Fork_join.pp_discovery plan.discovery;
17521583 Fmt.pr "@.Actions to perform:@.";
17531753- List.iteri
17541754- (fun i action ->
17551755- Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action)
17561756- plan.actions;
15841584+ List.iteri (fun i action ->
15851585+ Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action
15861586+ ) plan.actions;
17571587 Fmt.pr "@.";
17581588 (* Prompt for confirmation unless --yes or --dry-run *)
17591589 let proceed =
17601590 if dry_run then begin
17611591 Fmt.pr "(dry-run mode - no changes will be made)@.";
17621592 true
17631763- end
17641764- else if yes then true
17651765- else confirm "Proceed?"
15931593+ end else if yes then
15941594+ true
15951595+ else
15961596+ confirm "Proceed?"
17661597 in
17671598 if not proceed then begin
17681599 Fmt.pr "Cancelled.@.";
17691600 `Ok ()
17701770- end
17711771- else begin
16011601+ end else begin
17721602 (* Execute the plan *)
17731603 match Monopam.Fork_join.execute_join_plan ~proc ~fs plan with
17741604 | Ok result ->
···17811611 | Error e ->
17821612 Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
17831613 `Error (false, "join failed")
17841784- end)
16141614+ end
17851615 in
17861786- Cmd.v info
17871787- Term.(
17881788- ret
17891789- (const run $ url_or_pkg_arg $ as_arg $ upstream_arg $ from_arg
17901790- $ fork_url_arg $ dry_run_arg $ yes_arg $ logging_term))
16161616+ Cmd.v info Term.(ret (const run $ url_or_pkg_arg $ as_arg $ upstream_arg $ from_arg $ fork_url_arg $ dry_run_arg $ yes_arg $ logging_term))
1791161717921618(* Rejoin command *)
17931619···18151641 `I ("1.", "Verifies src/<name>/ exists and is a git repo");
18161642 `I ("2.", "Verifies mono/<name>/ does not exist");
18171643 `I ("3.", "Prompts for confirmation (use $(b,--yes) to skip)");
18181818- `I
18191819- ( "4.",
18201820- "Uses $(b,git subtree add) to bring src/<name>/ into mono/<name>/" );
16441644+ `I ("4.", "Uses $(b,git subtree add) to bring src/<name>/ into mono/<name>/");
18211645 `S Manpage.s_examples;
18221646 `P "Re-add a package from src/:";
18231647 `Pre "monopam rejoin my-lib";
···18551679 Fmt.pr "Analyzing rejoin request for '%s'...@.@." name;
18561680 Fmt.pr "Discovery:@.%a@." Monopam.Fork_join.pp_discovery plan.discovery;
18571681 Fmt.pr "@.Actions to perform:@.";
18581858- List.iteri
18591859- (fun i action ->
18601860- Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action)
18611861- plan.actions;
16821682+ List.iteri (fun i action ->
16831683+ Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action
16841684+ ) plan.actions;
18621685 Fmt.pr "@.";
18631686 (* Prompt for confirmation unless --yes or --dry-run *)
18641687 let proceed =
18651688 if dry_run then begin
18661689 Fmt.pr "(dry-run mode - no changes will be made)@.";
18671690 true
18681868- end
18691869- else if yes then true
18701870- else confirm "Proceed?"
16911691+ end else if yes then
16921692+ true
16931693+ else
16941694+ confirm "Proceed?"
18711695 in
18721696 if not proceed then begin
18731697 Fmt.pr "Cancelled.@.";
18741698 `Ok ()
18751875- end
18761876- else begin
16991699+ end else begin
18771700 (* Execute the plan *)
18781701 match Monopam.Fork_join.execute_join_plan ~proc ~fs plan with
18791702 | Ok result ->
···18891712 `Error (false, "rejoin failed")
18901713 end
18911714 in
18921892- Cmd.v info
18931893- Term.(ret (const run $ name_arg $ dry_run_arg $ yes_arg $ logging_term))
17151715+ Cmd.v info Term.(ret (const run $ name_arg $ dry_run_arg $ yes_arg $ logging_term))
1894171618951717(* Site command *)
18961718···19001722 [
19011723 `S Manpage.s_description;
19021724 `P
19031903- "Generates a static index.html file that maps the monoverse, showing \
19041904- all verse members, their packages, and the relationships between \
19051905- them.";
17251725+ "Generates a static index.html file that maps the monoverse, showing all \
17261726+ verse members, their packages, and the relationships between them.";
19061727 `S "OUTPUT";
19071728 `P "The generated site includes:";
19081908- `I
19091909- ( "Members",
19101910- "All verse members with links to their monorepo and opam repos" );
17291729+ `I ("Members", "All verse members with links to their monorepo and opam repos");
19111730 `I ("Summary", "Overview of common libraries and member-specific packages");
19121731 `I ("Repository Details", "Each shared repo with packages and fork status");
19131732 `S "FORK STATUS";
···19351754 let info = Cmd.info "site" ~doc ~man in
19361755 let output_arg =
19371756 let doc = "Output file path. Defaults to mono/index.html." in
19381938- Arg.(
19391939- value & opt (some string) None & info [ "o"; "output" ] ~docv:"FILE" ~doc)
17571757+ Arg.(value & opt (some string) None & info [ "o"; "output" ] ~docv:"FILE" ~doc)
19401758 in
19411759 let stdout_arg =
19421760 let doc = "Print HTML to stdout instead of writing to file." in
19431761 Arg.(value & flag & info [ "stdout" ] ~doc)
19441762 in
19451763 let status_arg =
19461946- let doc =
19471947- "Include fork status (ahead/behind) for each repository. This fetches \
19481948- from remotes and may be slower."
19491949- in
17641764+ let doc = "Include fork status (ahead/behind) for each repository. \
17651765+ This fetches from remotes and may be slower." in
19501766 Arg.(value & flag & info [ "status"; "s" ] ~doc)
19511767 in
19521768 let run output to_stdout with_status () =
···19581774 (* Pull/clone registry to get latest metadata *)
19591775 Fmt.pr "Syncing registry...@.";
19601776 let registry =
19611961- match
19621962- Monopam.Verse_registry.clone_or_pull ~proc
19631963- ~fs:(fs :> _ Eio.Path.t)
19641964- ~config:verse_config ()
19651965- with
17771777+ match Monopam.Verse_registry.clone_or_pull ~proc ~fs:(fs :> _ Eio.Path.t) ~config:verse_config () with
19661778 | Ok r -> r
19671779 | Error msg ->
19681780 Fmt.epr "Warning: Could not sync registry: %s@." msg;
19691969- Monopam.Verse_registry.
19701970- { name = "opamverse"; description = None; members = [] }
17811781+ Monopam.Verse_registry.{ name = "opamverse"; description = None; members = [] }
19711782 in
19721783 (* Compute forks if --status is requested *)
19731784 let forks =
19741785 if with_status then begin
19751786 Fmt.pr "Computing fork status...@.";
19761976- Some
19771977- (Monopam.Forks.compute ~proc
19781978- ~fs:(fs :> _ Eio.Path.t)
19791979- ~verse_config ~monopam_config ())
19801980- end
19811981- else None
17871787+ Some (Monopam.Forks.compute ~proc ~fs:(fs :> _ Eio.Path.t)
17881788+ ~verse_config ~monopam_config ())
17891789+ end else None
19821790 in
19831791 if to_stdout then begin
19841984- let html =
19851985- Monopam.Site.generate
19861986- ~fs:(fs :> _ Eio.Path.t)
19871987- ~config:verse_config ?forks ~registry ()
19881988- in
17921792+ let html = Monopam.Site.generate ~fs:(fs :> _ Eio.Path.t) ~config:verse_config ?forks ~registry () in
19891793 print_string html;
19901794 `Ok ()
19911991- end
19921992- else begin
17951795+ end else begin
19931796 let output_path =
19941797 match output with
19951798 | Some p -> (
19961799 match Fpath.of_string p with
19971800 | Ok fp -> fp
19981801 | Error (`Msg _) -> Fpath.v p)
19991999- | None ->
20002000- Fpath.(Monopam.Verse_config.mono_path verse_config / "index.html")
18021802+ | None -> Fpath.(Monopam.Verse_config.mono_path verse_config / "index.html")
20011803 in
20022002- match
20032003- Monopam.Site.write
20042004- ~fs:(fs :> _ Eio.Path.t)
20052005- ~config:verse_config ?forks ~registry ~output_path ()
20062006- with
18041804+ match Monopam.Site.write ~fs:(fs :> _ Eio.Path.t) ~config:verse_config ?forks ~registry ~output_path () with
20071805 | Ok () ->
20081806 Fmt.pr "Site generated: %a@." Fpath.pp output_path;
20091807 `Ok ()
···20121810 `Error (false, "site generation failed")
20131811 end
20141812 in
20152015- Cmd.v info
20162016- Term.(ret (const run $ output_arg $ stdout_arg $ status_arg $ logging_term))
18131813+ Cmd.v info Term.(ret (const run $ output_arg $ stdout_arg $ status_arg $ logging_term))
2017181420181815(* Main command group *)
20191816···20311828 pre-installed.";
20321829 `S "QUICK START";
20331830 `P "Start by creating a devcontainer workspace:";
20342034- `Pre "monopam devcontainer ~/tangled";
18311831+ `Pre
18321832+ "monopam devcontainer ~/tangled";
20351833 `P "Inside the devcontainer, initialize your workspace:";
20362036- `Pre "cd ~/tangled\nmonopam init --handle yourname.bsky.social\ncd mono";
18341834+ `Pre
18351835+ "cd ~/tangled\n\
18361836+ monopam init --handle yourname.bsky.social\n\
18371837+ cd mono";
20371838 `P "Daily workflow:";
20381839 `Pre
20391840 "cd ~/tangled/mono\n\
···21121913 in
21131914 let info = Cmd.info "monopam" ~version:"%%VERSION%%" ~doc ~man in
21141915 Cmd.group info
21152115- [
21162116- init_cmd;
21172117- status_cmd;
21182118- diff_cmd;
21192119- pull_cmd;
21202120- cherrypick_cmd;
21212121- sync_cmd;
21222122- changes_cmd;
21232123- opam_cmd;
21242124- doctor_cmd;
21252125- verse_cmd;
21262126- feature_cmd;
21272127- fork_cmd;
21282128- join_cmd;
21292129- rejoin_cmd;
21302130- devcontainer_cmd;
21312131- site_cmd;
21322132- ]
19161916+ [ init_cmd; status_cmd; diff_cmd; pull_cmd; cherrypick_cmd; sync_cmd; changes_cmd; opam_cmd; doctor_cmd; verse_cmd; feature_cmd; fork_cmd; join_cmd; rejoin_cmd; devcontainer_cmd; site_cmd ]
2133191721342134-let () =
21352135- Memtrace.trace_if_requested ~context:"monopam" ();
21362136- exit (Cmd.eval main_cmd)
19181918+let () = exit (Cmd.eval main_cmd)
···33type source_info =
44 | Github of { user : string; repo : string }
55 | Gitlab of { user : string; repo : string }
66- | Tangled of { host : string; repo : string }
77- (** tangled.org style sources *)
66+ | Tangled of { host : string; repo : string } (** tangled.org style sources *)
87 | Uri of { url : string; branch : string option }
98109type t = {
···1716module Sexp = Sexplib0.Sexp
18171918(** Extract string from a Sexp.Atom, or None if it's a List *)
2020-let atom_string = function Sexp.Atom s -> Some s | Sexp.List _ -> None
1919+let atom_string = function
2020+ | Sexp.Atom s -> Some s
2121+ | Sexp.List _ -> None
21222223(** Parse source stanza: (source (github user/repo)) or (source (uri "url")) *)
2324let parse_source_inner sexp =
···3536 match String.index_opt host_repo '/' with
3637 | Some i ->
3738 let host = String.sub host_repo 0 i in
3838- let repo =
3939- String.sub host_repo (i + 1) (String.length host_repo - i - 1)
4040- in
3939+ let repo = String.sub host_repo (i + 1) (String.length host_repo - i - 1) in
4140 Some (Tangled { host; repo })
4241 | None -> None)
4342 | Sexp.List [ Sexp.Atom "uri"; Sexp.Atom url ] ->
4443 (* Check for branch in URI fragment *)
4544 let uri = Uri.of_string url in
4645 let branch = Uri.fragment uri in
4747- let url_without_fragment = Uri.with_fragment uri None |> Uri.to_string in
4646+ let url_without_fragment =
4747+ Uri.with_fragment uri None |> Uri.to_string
4848+ in
4849 Some (Uri { url = url_without_fragment; branch })
4950 | Sexp.Atom url ->
5051 (* Single atom URL (unlikely but handle it) *)
5152 let uri = Uri.of_string url in
5253 let branch = Uri.fragment uri in
5353- let url_without_fragment = Uri.with_fragment uri None |> Uri.to_string in
5454+ let url_without_fragment =
5555+ Uri.with_fragment uri None |> Uri.to_string
5656+ in
5457 Some (Uri { url = url_without_fragment; branch })
5558 | _ -> None
5659···8790let parse content =
8891 match Parsexp.Many.parse_string content with
8992 | Error err ->
9090- Error
9191- (Printf.sprintf "S-expression parse error: %s"
9292- (Parsexp.Parse_error.message err))
9393+ Error (Printf.sprintf "S-expression parse error: %s"
9494+ (Parsexp.Parse_error.message err))
9395 | Ok sexps -> (
9496 match find_string_field "name" sexps with
9597 | None -> Error "dune-project missing (name ...) stanza"
···110112111113(** Ensure URL ends with .git *)
112114let ensure_git_suffix url =
113113- if String.ends_with ~suffix:".git" url then url else url ^ ".git"
115115+ if String.ends_with ~suffix:".git" url then url
116116+ else url ^ ".git"
114117115118let dev_repo_url t =
116119 match t.source with
···121124 | Some (Tangled { host; repo }) ->
122125 (* Tangled sources: https://tangled.sh/@handle/repo *)
123126 Ok (Printf.sprintf "git+https://tangled.sh/@%s/%s.git" host repo)
124124- | Some (Uri { url; _ }) -> Ok (normalize_git_url (ensure_git_suffix url))
127127+ | Some (Uri { url; _ }) ->
128128+ Ok (normalize_git_url (ensure_git_suffix url))
125129 | None -> (
126130 match t.homepage with
127127- | Some homepage -> Ok (normalize_git_url (ensure_git_suffix homepage))
131131+ | Some homepage ->
132132+ Ok (normalize_git_url (ensure_git_suffix homepage))
128133 | None ->
129134 Error
130135 (Printf.sprintf
+6-7
lib/dune_project.mli
···11(** Dune project file parsing.
2233- Parse dune-project s-expressions to extract package metadata needed for
44- generating opam-repo entries. *)
33+ Parse dune-project s-expressions to extract package metadata needed
44+ for generating opam-repo entries. *)
5566(** Source information from dune-project. *)
77type source_info =
···1010 | Tangled of { host : string; repo : string } (** tangled.sh style sources *)
1111 | Uri of { url : string; branch : string option }
12121313+(** Parsed dune-project file. *)
1314type t = {
1415 name : string; (** Project name from (name ...) stanza *)
1516 source : source_info option; (** Source from (source ...) stanza *)
1617 homepage : string option; (** Homepage from (homepage ...) stanza *)
1718 packages : string list; (** Package names from (package (name ...)) stanzas *)
1819}
1919-(** Parsed dune-project file. *)
20202121val parse : string -> (t, string) result
2222(** [parse content] parses a dune-project file content and extracts metadata.
···24242525val dev_repo_url : t -> (string, string) result
2626(** [dev_repo_url t] derives the dev-repo URL from the parsed dune-project.
2727- Returns a URL suitable for the opam dev-repo field (e.g.,
2828- "git+https://...").
2727+ Returns a URL suitable for the opam dev-repo field (e.g., "git+https://...").
29283029 URL derivation logic:
3130 - [Github {user; repo}] -> "git+https://github.com/user/repo.git"
···3534 - Neither source nor homepage -> Error *)
36353736val url_with_branch : t -> (string, string) result
3838-(** [url_with_branch t] derives the URL with branch fragment for the opam url
3939- section. Returns a URL with #branch suffix (e.g., "git+https://...#main").
3737+(** [url_with_branch t] derives the URL with branch fragment for the opam url section.
3838+ Returns a URL with #branch suffix (e.g., "git+https://...#main").
40394140 Branch derivation:
4241 - [Uri {url; branch = Some b}] -> url#b
+26-34
lib/feature.ml
···11type error =
22- | Git_error of Git_cli.error
22+ | Git_error of Git.error
33 | Feature_exists of string
44 | Feature_not_found of string
55 | Config_error of string
6677let pp_error ppf = function
88- | Git_error e -> Fmt.pf ppf "Git error: %a" Git_cli.pp_error e
88+ | Git_error e -> Fmt.pf ppf "Git error: %a" Git.pp_error e
99 | Feature_exists name -> Fmt.pf ppf "Feature '%s' already exists" name
1010 | Feature_not_found name -> Fmt.pf ppf "Feature '%s' not found" name
1111 | Config_error msg -> Fmt.pf ppf "Configuration error: %s" msg
···1313let error_hint = function
1414 | Git_error _ -> Some "Check that the monorepo is properly initialized"
1515 | Feature_exists name ->
1616- Some
1717- (Printf.sprintf
1818- "Run 'monopam feature remove %s' first if you want to recreate it"
1919- name)
1616+ Some (Printf.sprintf "Run 'monopam feature remove %s' first if you want to recreate it" name)
2017 | Feature_not_found name ->
2121- Some
2222- (Printf.sprintf
2323- "Run 'monopam feature list' to see available features, or 'monopam \
2424- feature add %s' to create it"
2525- name)
2626- | Config_error _ ->
2727- Some "Run 'monopam init' to create a workspace configuration"
1818+ Some (Printf.sprintf "Run 'monopam feature list' to see available features, or 'monopam feature add %s' to create it" name)
1919+ | Config_error _ -> Some "Run 'monopam init' to create a workspace configuration"
28202921let pp_error_with_hint ppf e =
3022 pp_error ppf e;
···3224 | Some hint -> Fmt.pf ppf "@.Hint: %s" hint
3325 | None -> ()
34263535-type entry = { name : string; path : Fpath.t; branch : string }
2727+type entry = {
2828+ name : string;
2929+ path : Fpath.t;
3030+ branch : string;
3131+}
36323733let pp_entry ppf e =
3834 Fmt.pf ppf "%s -> %a (branch: %s)" e.name Fpath.pp e.path e.branch
···4844 let work_dir = work_path config in
4945 let wt_path = feature_path config name in
5046 (* Check if feature already exists *)
5151- if Git_cli.Worktree.exists ~proc ~fs ~repo:mono ~path:wt_path then
4747+ if Git.Worktree.exists ~proc ~fs ~repo:mono ~path:wt_path then
5248 Error (Feature_exists name)
5349 else begin
5450 (* Ensure work directory exists *)
5551 let work_eio = Eio.Path.(fs / Fpath.to_string work_dir) in
5652 (try Eio.Path.mkdirs ~perm:0o755 work_eio with Eio.Io _ -> ());
5753 (* Create the worktree with a new branch *)
5858- match
5959- Git_cli.Worktree.add ~proc ~fs ~repo:mono ~path:wt_path ~branch:name ()
6060- with
5454+ match Git.Worktree.add ~proc ~fs ~repo:mono ~path:wt_path ~branch:name () with
6155 | Error e -> Error (Git_error e)
6256 | Ok () -> Ok { name; path = wt_path; branch = name }
6357 end
···6660 let mono = Verse_config.mono_path config in
6761 let wt_path = feature_path config name in
6862 (* Check if feature exists *)
6969- if not (Git_cli.Worktree.exists ~proc ~fs ~repo:mono ~path:wt_path) then
6363+ if not (Git.Worktree.exists ~proc ~fs ~repo:mono ~path:wt_path) then
7064 Error (Feature_not_found name)
7165 else
7272- match
7373- Git_cli.Worktree.remove ~proc ~fs ~repo:mono ~path:wt_path ~force ()
7474- with
6666+ match Git.Worktree.remove ~proc ~fs ~repo:mono ~path:wt_path ~force () with
7567 | Error e -> Error (Git_error e)
7668 | Ok () -> Ok ()
77697870let list ~proc ~fs ~config () =
7971 let mono = Verse_config.mono_path config in
8072 let work_dir = work_path config in
8181- let all_worktrees = Git_cli.Worktree.list ~proc ~fs mono in
7373+ let all_worktrees = Git.Worktree.list ~proc ~fs mono in
8274 (* Filter to only worktrees under work/ directory *)
8383- List.filter_map
8484- (fun (wt : Git_cli.Worktree.entry) ->
8585- (* Check if this worktree is under the work directory *)
8686- let wt_str = Fpath.to_string wt.path in
8787- let work_str = Fpath.to_string work_dir in
8888- if String.starts_with ~prefix:work_str wt_str then
8989- let name = Fpath.basename wt.path in
9090- let branch = Option.value ~default:name wt.branch in
9191- Some { name; path = wt.path; branch }
9292- else None)
9393- all_worktrees
7575+ List.filter_map (fun (wt : Git.Worktree.entry) ->
7676+ (* Check if this worktree is under the work directory *)
7777+ let wt_str = Fpath.to_string wt.path in
7878+ let work_str = Fpath.to_string work_dir in
7979+ if String.starts_with ~prefix:work_str wt_str then
8080+ let name = Fpath.basename wt.path in
8181+ let branch = Option.value ~default:name wt.branch in
8282+ Some { name; path = wt.path; branch }
8383+ else
8484+ None
8585+ ) all_worktrees
+2-2
lib/feature.mli
···7788(** Errors from feature operations. *)
99type error =
1010- | Git_error of Git_cli.error (** Git operation error *)
1010+ | Git_error of Git.error (** Git operation error *)
1111 | Feature_exists of string (** Feature worktree already exists *)
1212 | Feature_not_found of string (** Feature worktree does not exist *)
1313 | Config_error of string (** Configuration error *)
···1818val pp_error_with_hint : error Fmt.t
1919(** [pp_error_with_hint] formats errors with a helpful hint. *)
20202121+(** A feature worktree entry. *)
2122type entry = {
2223 name : string; (** Feature name *)
2324 path : Fpath.t; (** Path to the worktree *)
2425 branch : string; (** Branch name *)
2526}
2626-(** A feature worktree entry. *)
27272828val pp_entry : entry Fmt.t
2929(** [pp_entry] formats a feature entry. *)
+383-643
lib/fork_join.ml
···2233type error =
44 | Config_error of string
55- | Git_error of Git_cli.error
55+ | Git_error of Git.error
66 | Subtree_not_found of string
77 | Src_already_exists of string
88 | Src_not_found of string
···1818 | Check_remote_exists of string (** URL - informational check *)
1919 | Create_directory of Fpath.t
2020 | Git_init of Fpath.t
2121- | Git_config of { repo : Fpath.t; key : string; value : string }
2222- (** Set git config *)
2323- | Git_clone of { url : string; dest : Fpath.t; branch : string }
2424- | Git_subtree_split of { repo : Fpath.t; prefix : string }
2525- | Git_subtree_add of {
2626- repo : Fpath.t;
2727- prefix : string;
2828- url : Uri.t;
2929- branch : string;
3030- }
3131- | Git_add_remote of { repo : Fpath.t; name : string; url : string }
3232- | Git_push_ref of { repo : Fpath.t; target : string; ref_spec : string }
3333- | Git_checkout of { repo : Fpath.t; branch : string }
3434- | Git_branch_rename of { repo : Fpath.t; new_name : string }
3535- (** Rename current branch *)
3636- | Copy_directory of { src : Fpath.t; dest : Fpath.t }
2121+ | Git_config of { repo: Fpath.t; key: string; value: string } (** Set git config *)
2222+ | Git_clone of { url: string; dest: Fpath.t; branch: string }
2323+ | Git_subtree_split of { repo: Fpath.t; prefix: string }
2424+ | Git_subtree_add of { repo: Fpath.t; prefix: string; url: Uri.t; branch: string }
2525+ | Git_add_remote of { repo: Fpath.t; name: string; url: string }
2626+ | Git_push_ref of { repo: Fpath.t; target: string; ref_spec: string }
2727+ | Git_checkout of { repo: Fpath.t; branch: string }
2828+ | Git_branch_rename of { repo: Fpath.t; new_name: string } (** Rename current branch *)
2929+ | Copy_directory of { src: Fpath.t; dest: Fpath.t }
3730 | Git_add_all of Fpath.t
3838- | Git_commit of { repo : Fpath.t; message : string }
3939- | Git_rm of { repo : Fpath.t; path : string; recursive : bool }
4040- (** Remove file/dir from git *)
4141- | Update_sources_toml of {
4242- path : Fpath.t;
4343- name : string;
4444- entry : Sources_registry.entry;
4545- }
3131+ | Git_commit of { repo: Fpath.t; message: string }
3232+ | Git_rm of { repo: Fpath.t; path: string; recursive: bool } (** Remove file/dir from git *)
3333+ | Update_sources_toml of { path: Fpath.t; name: string; entry: Sources_registry.entry }
46343535+(** Discovery information gathered during planning *)
4736type discovery = {
4848- mono_exists : bool;
4949- src_exists : bool;
5050- has_subtree_history : bool; (** Can we git subtree split? *)
5151- remote_accessible : bool option; (** None = not checked, Some = result *)
5252- opam_files : string list;
5353- local_path_is_repo : bool option; (** For join from local dir *)
3737+ mono_exists: bool;
3838+ src_exists: bool;
3939+ has_subtree_history: bool; (** Can we git subtree split? *)
4040+ remote_accessible: bool option; (** None = not checked, Some = result *)
4141+ opam_files: string list;
4242+ local_path_is_repo: bool option; (** For join from local dir *)
5443}
5555-(** Discovery information gathered during planning *)
56444545+(** A complete action plan *)
5746type 'a action_plan = {
5858- discovery : discovery;
5959- actions : action list;
6060- result : 'a; (** What we'll return on success *)
6161- dry_run : bool;
4747+ discovery: discovery;
4848+ actions: action list;
4949+ result: 'a; (** What we'll return on success *)
5050+ dry_run: bool;
6251}
6363-(** A complete action plan *)
64526553let pp_error ppf = function
6654 | Config_error msg -> Fmt.pf ppf "Configuration error: %s" msg
6767- | Git_error e -> Fmt.pf ppf "Git error: %a" Git_cli.pp_error e
6868- | Subtree_not_found name ->
6969- Fmt.pf ppf "Subtree not found in monorepo: %s" name
7070- | Src_already_exists name ->
7171- Fmt.pf ppf "Source checkout already exists: src/%s" name
5555+ | Git_error e -> Fmt.pf ppf "Git error: %a" Git.pp_error e
5656+ | Subtree_not_found name -> Fmt.pf ppf "Subtree not found in monorepo: %s" name
5757+ | Src_already_exists name -> Fmt.pf ppf "Source checkout already exists: src/%s" name
7258 | Src_not_found name -> Fmt.pf ppf "Source checkout not found: src/%s" name
7373- | Subtree_already_exists name ->
7474- Fmt.pf ppf "Subtree already exists in monorepo: mono/%s" name
5959+ | Subtree_already_exists name -> Fmt.pf ppf "Subtree already exists in monorepo: mono/%s" name
7560 | No_opam_files name -> Fmt.pf ppf "No .opam files found in subtree: %s" name
7661 | Verse_error e -> Fmt.pf ppf "Verse error: %a" Verse.pp_error e
7762 | User_cancelled -> Fmt.pf ppf "Operation cancelled by user"
···7964let error_hint = function
8065 | Config_error _ ->
8166 Some "Run 'monopam init --handle <your-handle>' to create a workspace."
8282- | Git_error (Git_cli.Dirty_worktree _) ->
6767+ | Git_error (Git.Dirty_worktree _) ->
8368 Some "Commit or stash your changes first: git status"
8469 | Git_error _ -> None
8570 | Subtree_not_found name ->
8671 Some (Fmt.str "Check that mono/%s exists in your monorepo" name)
8772 | Src_already_exists name ->
8888- Some
8989- (Fmt.str "Remove or rename src/%s first, or choose a different name"
9090- name)
7373+ Some (Fmt.str "Remove or rename src/%s first, or choose a different name" name)
9174 | Src_not_found name ->
9275 Some (Fmt.str "Run 'monopam fork %s' first to create src/%s" name name)
9376 | Subtree_already_exists name ->
9494- Some
9595- (Fmt.str "Remove mono/%s first, or use a different name with --as" name)
7777+ Some (Fmt.str "Remove mono/%s first, or use a different name with --as" name)
9678 | No_opam_files name ->
9779 Some (Fmt.str "Add a .opam file to mono/%s before forking" name)
9880 | Verse_error e -> Verse.error_hint e
···10183(** {1 Pretty Printers for Actions and Discovery} *)
1028410385let pp_action ppf = function
104104- | Check_remote_exists url -> Fmt.pf ppf "Check remote accessible: %s" url
105105- | Create_directory path -> Fmt.pf ppf "Create directory: %a" Fpath.pp path
106106- | Git_init path -> Fmt.pf ppf "Initialize git repository: %a" Fpath.pp path
8686+ | Check_remote_exists url ->
8787+ Fmt.pf ppf "Check remote accessible: %s" url
8888+ | Create_directory path ->
8989+ Fmt.pf ppf "Create directory: %a" Fpath.pp path
9090+ | Git_init path ->
9191+ Fmt.pf ppf "Initialize git repository: %a" Fpath.pp path
10792 | Git_config { repo = _; key; value } ->
10893 Fmt.pf ppf "Set git config %s = %s" key value
10994 | Git_clone { url; dest; branch } ->
···11196 | Git_subtree_split { repo = _; prefix } ->
11297 Fmt.pf ppf "Split subtree history for '%s'" prefix
11398 | Git_subtree_add { repo = _; prefix; url; branch } ->
114114- Fmt.pf ppf "Add subtree '%s' from %s (branch: %s)" prefix
115115- (Uri.to_string url) branch
9999+ Fmt.pf ppf "Add subtree '%s' from %s (branch: %s)" prefix (Uri.to_string url) branch
116100 | Git_add_remote { repo = _; name; url } ->
117101 Fmt.pf ppf "Add remote '%s' -> %s" name url
118102 | Git_push_ref { repo = _; target; ref_spec } ->
···123107 Fmt.pf ppf "Rename current branch to '%s'" new_name
124108 | Copy_directory { src; dest } ->
125109 Fmt.pf ppf "Copy files from %a to %a" Fpath.pp src Fpath.pp dest
126126- | Git_add_all path -> Fmt.pf ppf "Stage all changes in %a" Fpath.pp path
127127- | Git_commit { repo = _; message } -> Fmt.pf ppf "Create commit: %s" message
110110+ | Git_add_all path ->
111111+ Fmt.pf ppf "Stage all changes in %a" Fpath.pp path
112112+ | Git_commit { repo = _; message } ->
113113+ Fmt.pf ppf "Create commit: %s" message
128114 | Git_rm { repo = _; path; recursive = _ } ->
129115 Fmt.pf ppf "Remove '%s' from git" path
130116 | Update_sources_toml { path = _; name; entry = _ } ->
···139125 Fmt.pf ppf " Subtree history: %s@,"
140126 (if d.has_subtree_history then "present" else "none (fresh package)");
141127 (match d.remote_accessible with
142142- | None -> ()
143143- | Some true -> Fmt.pf ppf " Remote accessible: yes@,"
144144- | Some false -> Fmt.pf ppf " Remote accessible: no@,");
128128+ | None -> ()
129129+ | Some true -> Fmt.pf ppf " Remote accessible: yes@,"
130130+ | Some false -> Fmt.pf ppf " Remote accessible: no@,");
145131 (match d.local_path_is_repo with
146146- | None -> ()
147147- | Some true -> Fmt.pf ppf " Is git repo: yes@,"
148148- | Some false -> Fmt.pf ppf " Is git repo: no@,");
132132+ | None -> ()
133133+ | Some true -> Fmt.pf ppf " Is git repo: yes@,"
134134+ | Some false -> Fmt.pf ppf " Is git repo: no@,");
149135 if d.opam_files <> [] then
150150- Fmt.pf ppf " Packages found: %a@,"
151151- Fmt.(list ~sep:(any ", ") string)
152152- d.opam_files;
136136+ Fmt.pf ppf " Packages found: %a@," Fmt.(list ~sep:(any ", ") string) d.opam_files;
153137 Fmt.pf ppf "@]"
154138155155-let pp_action_plan : type a. a Fmt.t -> a action_plan Fmt.t =
156156- fun pp_result ppf plan ->
157157- Fmt.pf ppf "@[<v>Discovery:@,%a@,@,Actions to perform:@," pp_discovery
158158- plan.discovery;
159159- List.iteri
160160- (fun i action -> Fmt.pf ppf " %d. %a@," (i + 1) pp_action action)
161161- plan.actions;
162162- if plan.dry_run then Fmt.pf ppf "@,(dry-run mode - no changes will be made)@,";
139139+let pp_action_plan : type a. a Fmt.t -> a action_plan Fmt.t = fun pp_result ppf plan ->
140140+ Fmt.pf ppf "@[<v>Discovery:@,%a@,@,Actions to perform:@," pp_discovery plan.discovery;
141141+ List.iteri (fun i action ->
142142+ Fmt.pf ppf " %d. %a@," (i + 1) pp_action action
143143+ ) plan.actions;
144144+ if plan.dry_run then
145145+ Fmt.pf ppf "@,(dry-run mode - no changes will be made)@,";
163146 Fmt.pf ppf "@,Expected result:@, %a@]" pp_result plan.result
164147165148let pp_error_with_hint ppf e =
···187170let pp_fork_result ppf (r : fork_result) =
188171 (* Only truncate if it looks like a git SHA (40 hex chars), otherwise show full string *)
189172 let commit_display =
190190- if
191191- String.length r.split_commit = 40
192192- && String.for_all
193193- (fun c -> (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))
194194- r.split_commit
173173+ if String.length r.split_commit = 40 &&
174174+ String.for_all (fun c -> (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) r.split_commit
195175 then String.sub r.split_commit 0 7
196176 else r.split_commit
197177 in
198178 Fmt.pf ppf "@[<v>Forked subtree '%s':@, Split commit: %s@, Local repo: %a@,"
199179 r.name commit_display Fpath.pp r.src_path;
200180 (match r.push_url with
201201- | Some url -> Fmt.pf ppf " Push URL: %s@," url
202202- | None -> ());
181181+ | Some url -> Fmt.pf ppf " Push URL: %s@," url
182182+ | None -> ());
203183 if r.packages_created <> [] then
204204- Fmt.pf ppf " Packages: %a@]"
205205- Fmt.(list ~sep:(any ", ") string)
206206- r.packages_created
207207- else Fmt.pf ppf "@]"
184184+ Fmt.pf ppf " Packages: %a@]" Fmt.(list ~sep:(any ", ") string) r.packages_created
185185+ else
186186+ Fmt.pf ppf "@]"
208187209188let pp_join_result ppf (r : join_result) =
210210- Fmt.pf ppf "@[<v>Joined repository '%s':@, Source: %s@," r.name r.source_url;
189189+ Fmt.pf ppf "@[<v>Joined repository '%s':@, Source: %s@,"
190190+ r.name r.source_url;
211191 (match r.upstream_url with
212212- | Some url -> Fmt.pf ppf " Upstream: %s@," url
213213- | None -> ());
192192+ | Some url -> Fmt.pf ppf " Upstream: %s@," url
193193+ | None -> ());
214194 (match r.from_handle with
215215- | Some h -> Fmt.pf ppf " From verse: %s@," h
216216- | None -> ());
195195+ | Some h -> Fmt.pf ppf " From verse: %s@," h
196196+ | None -> ());
217197 if r.packages_added <> [] then
218218- Fmt.pf ppf " Packages: %a@]"
219219- Fmt.(list ~sep:(any ", ") string)
220220- r.packages_added
221221- else Fmt.pf ppf "@]"
198198+ Fmt.pf ppf " Packages: %a@]" Fmt.(list ~sep:(any ", ") string) r.packages_added
199199+ else
200200+ Fmt.pf ppf "@]"
222201223202(** Helper to check if a path is a directory *)
224203let is_directory ~fs path =
···257236 | Some "tangled.org" | Some "tangled.sh" -> true
258237 | _ -> false
259238260260-(** Convert a dev-repo URL to a push URL (SSH format for github/gitlab/tangled)
261261-*)
239239+(** Convert a dev-repo URL to a push URL (SSH format for github/gitlab/tangled) *)
262240let url_to_push_url ?knot url =
263241 (* Strip git+ prefix if present *)
264242 let url =
···324302 (* For SSH URLs like git@github.com:user/repo.git *)
325303 if String.starts_with ~prefix:"git@" url then
326304 match String.index_opt url ':' with
327327- | Some i -> (
305305+ | Some i ->
328306 let path = String.sub url (i + 1) (String.length url - i - 1) in
329307 (* path is like "user/repo.git" or "handle/repo" *)
330330- match String.index_opt path '/' with
331331- | Some j ->
332332- let user = String.sub path 0 j in
333333- (* Handle may be like "avsm" or "avsm.bsky.social" - compare first component *)
334334- let handle_first =
335335- match String.index_opt handle '.' with
336336- | Some k -> String.sub handle 0 k
337337- | None -> handle
338338- in
339339- String.equal user handle_first || String.equal user handle
340340- | None -> false)
308308+ (match String.index_opt path '/' with
309309+ | Some j ->
310310+ let user = String.sub path 0 j in
311311+ (* Handle may be like "avsm" or "avsm.bsky.social" - compare first component *)
312312+ let handle_first =
313313+ match String.index_opt handle '.' with
314314+ | Some k -> String.sub handle 0 k
315315+ | None -> handle
316316+ in
317317+ String.equal user handle_first || String.equal user handle
318318+ | None -> false)
341319 | None -> false
342320 else
343321 (* For HTTPS URLs like https://github.com/user/repo.git *)
···373351 let content = Eio.Path.load eio_path in
374352 match Dune_project.parse content with
375353 | Error _ -> None
376376- | Ok dune_proj -> (
354354+ | Ok dune_proj ->
377355 match Dune_project.dev_repo_url dune_proj with
378356 | Error _ -> None
379379- | Ok dev_repo -> Some (url_to_push_url ?knot dev_repo))
357357+ | Ok dev_repo -> Some (url_to_push_url ?knot dev_repo)
380358 with Eio.Io _ -> None
381359382360(** Extract name from URL (last path component without .git suffix) *)
···384362 let uri = Uri.of_string url in
385363 let path = Uri.path uri in
386364 (* Remove leading slash and .git suffix *)
387387- let path =
388388- if String.length path > 0 && path.[0] = '/' then
389389- String.sub path 1 (String.length path - 1)
390390- else path
391391- in
392392- let path =
393393- if String.ends_with ~suffix:".git" path then
394394- String.sub path 0 (String.length path - 4)
395395- else path
396396- in
365365+ let path = if String.length path > 0 && path.[0] = '/' then
366366+ String.sub path 1 (String.length path - 1)
367367+ else path in
368368+ let path = if String.ends_with ~suffix:".git" path then
369369+ String.sub path 0 (String.length path - 4)
370370+ else path in
397371 (* Get last component *)
398372 match String.rindex_opt path '/' with
399373 | Some i -> String.sub path (i + 1) (String.length path - i - 1)
···404378(** Determine if input is a local path or URL *)
405379let is_local_path s =
406380 (* It's a URL if it starts with a scheme or looks like SSH URL *)
407407- not
408408- (String.starts_with ~prefix:"http://" s
409409- || String.starts_with ~prefix:"https://" s
410410- || String.starts_with ~prefix:"git://" s
411411- || String.starts_with ~prefix:"git@" s
412412- || String.starts_with ~prefix:"ssh://" s
413413- || String.starts_with ~prefix:"git+" s)
381381+ not (String.starts_with ~prefix:"http://" s ||
382382+ String.starts_with ~prefix:"https://" s ||
383383+ String.starts_with ~prefix:"git://" s ||
384384+ String.starts_with ~prefix:"git@" s ||
385385+ String.starts_with ~prefix:"ssh://" s ||
386386+ String.starts_with ~prefix:"git+" s)
414387415388(** Copy a directory tree recursively *)
416389let copy_directory ~fs ~src ~dest =
···420393 match Eio.Path.kind ~follow:false src_path with
421394 | `Directory ->
422395 (try Eio.Path.mkdirs ~perm:0o755 dest_path with Eio.Io _ -> ());
423423- List.iter
424424- (fun name ->
425425- (* Skip .git directory to avoid copying git internals *)
426426- if name <> ".git" then begin
427427- let src_child = Eio.Path.(src_path / name) in
428428- let dest_child = Eio.Path.(dest_path / name) in
429429- copy_rec src_child dest_child
430430- end)
431431- (Eio.Path.read_dir src_path)
396396+ List.iter (fun name ->
397397+ (* Skip .git directory to avoid copying git internals *)
398398+ if name <> ".git" then begin
399399+ let src_child = Eio.Path.(src_path / name) in
400400+ let dest_child = Eio.Path.(dest_path / name) in
401401+ copy_rec src_child dest_child
402402+ end
403403+ ) (Eio.Path.read_dir src_path)
432404 | `Regular_file ->
433405 let content = Eio.Path.load src_path in
434406 Eio.Path.save ~create:(`Or_truncate 0o644) dest_path content
435435- | `Symbolic_link -> (
407407+ | `Symbolic_link ->
436408 (* Read symlink target and recreate it *)
437409 let target = Eio.Path.read_link src_path in
438438- try Unix.symlink target (snd dest_path) with _ -> ())
439439- | _ -> () (* Skip other file types *)
410410+ (try Unix.symlink target (snd dest_path) with _ -> ())
411411+ | _ -> () (* Skip other file types *)
440412 | exception _ -> ()
441413 in
442414 copy_rec src_eio dest_eio
···445417446418(** Build a fork plan - handles both subtree and fresh package scenarios.
447419448448- The fork workflow: 1. Create src/<name>/ with the package content (split or
449449- copy) 2. Remove mono/<name>/ from git 3. Re-add mono/<name>/ as a proper
450450- subtree from src/<name>/
420420+ The fork workflow:
421421+ 1. Create src/<name>/ with the package content (split or copy)
422422+ 2. Remove mono/<name>/ from git
423423+ 3. Re-add mono/<name>/ as a proper subtree from src/<name>/
451424452425 This ensures the subtree relationship is properly established for sync. *)
453426let plan_fork ~proc ~fs ~config ~name ?push_url ?(dry_run = false) () =
···459432 let branch = Verse_config.default_branch in
460433461434 (* Gather discovery information *)
462462- let mono_exists = Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix in
435435+ let mono_exists = Git.Subtree.exists ~fs ~repo:monorepo ~prefix in
463436 let src_exists = is_directory ~fs src_path in
464437 let has_subtree_hist =
465465- if mono_exists then
466466- Git_cli.has_subtree_history ~proc ~fs ~repo:monorepo ~prefix ()
438438+ if mono_exists then Git.has_subtree_history ~proc ~fs ~repo:monorepo ~prefix ()
467439 else false
468440 in
469441 let opam_files =
470470- if mono_exists then find_opam_files ~fs subtree_path else []
442442+ if mono_exists then find_opam_files ~fs subtree_path
443443+ else []
471444 in
472445473473- let discovery =
474474- {
475475- mono_exists;
476476- src_exists;
477477- has_subtree_history = has_subtree_hist;
478478- remote_accessible = None;
479479- (* Could check if push_url is accessible *)
480480- opam_files;
481481- local_path_is_repo = None;
482482- }
483483- in
446446+ let discovery = {
447447+ mono_exists;
448448+ src_exists;
449449+ has_subtree_history = has_subtree_hist;
450450+ remote_accessible = None; (* Could check if push_url is accessible *)
451451+ opam_files;
452452+ local_path_is_repo = None;
453453+ } in
484454485455 (* Validation *)
486486- if not mono_exists then Error (Subtree_not_found name)
487487- else if src_exists then Error (Src_already_exists name)
488488- else if opam_files = [] then Error (No_opam_files name)
456456+ if not mono_exists then
457457+ Error (Subtree_not_found name)
458458+ else if src_exists then
459459+ Error (Src_already_exists name)
460460+ else if opam_files = [] then
461461+ Error (No_opam_files name)
489462 else begin
490463 (* Build actions for complete fork workflow:
491464 1. Create src/<name>/ with content
···499472 Git_subtree_split { repo = monorepo; prefix };
500473 Git_init src_path;
501474 (* Allow pushing to checked-out branch (for monopam sync) *)
502502- Git_config
503503- {
504504- repo = src_path;
505505- key = "receive.denyCurrentBranch";
506506- value = "updateInstead";
507507- };
508508- Git_add_remote
509509- { repo = src_path; name = "mono"; url = Fpath.to_string monorepo };
510510- Git_push_ref
511511- {
512512- repo = monorepo;
513513- target = Fpath.to_string src_path;
514514- ref_spec = "SPLIT_COMMIT:refs/heads/main";
515515- };
475475+ Git_config { repo = src_path; key = "receive.denyCurrentBranch"; value = "updateInstead" };
476476+ Git_add_remote { repo = src_path; name = "mono"; url = Fpath.to_string monorepo };
477477+ Git_push_ref { repo = monorepo; target = Fpath.to_string src_path; ref_spec = "SPLIT_COMMIT:refs/heads/main" };
516478 Git_checkout { repo = src_path; branch };
517479 ]
518480 else
···522484 Create_directory src_path;
523485 Git_init src_path;
524486 (* Allow pushing to checked-out branch (for monopam sync) *)
525525- Git_config
526526- {
527527- repo = src_path;
528528- key = "receive.denyCurrentBranch";
529529- value = "updateInstead";
530530- };
487487+ Git_config { repo = src_path; key = "receive.denyCurrentBranch"; value = "updateInstead" };
531488 Git_branch_rename { repo = src_path; new_name = branch };
532489 Copy_directory { src = subtree_path; dest = src_path };
533490 Git_add_all src_path;
534534- Git_commit
535535- { repo = src_path; message = Fmt.str "Initial commit of %s" name };
491491+ Git_commit { repo = src_path; message = Fmt.str "Initial commit of %s" name };
536492 ]
537493 in
538494539495 (* Add remote if push_url provided *)
540540- let remote_actions =
541541- match push_url with
496496+ let remote_actions = match push_url with
542497 | Some url -> [ Git_add_remote { repo = src_path; name = "origin"; url } ]
543498 | None -> []
544499 in
545500546501 (* Remove from mono and re-add as subtree *)
547547- let rejoin_actions =
548548- [
549549- Git_rm { repo = monorepo; path = prefix; recursive = true };
550550- Git_commit
551551- { repo = monorepo; message = Fmt.str "Remove %s for fork" name };
552552- Git_subtree_add
553553- {
554554- repo = monorepo;
555555- prefix;
556556- url = Uri.of_string (Fpath.to_string src_path);
557557- branch;
558558- };
559559- ]
560560- in
502502+ let rejoin_actions = [
503503+ Git_rm { repo = monorepo; path = prefix; recursive = true };
504504+ Git_commit { repo = monorepo; message = Fmt.str "Remove %s for fork" name };
505505+ Git_subtree_add { repo = monorepo; prefix; url = Uri.of_string (Fpath.to_string src_path); branch };
506506+ ] in
561507562508 (* Update sources.toml only if push_url is a true fork (different namespace) *)
563509 let handle = Verse_config.handle config in
564564- let sources_actions =
565565- match push_url with
566566- | Some url when not (is_own_namespace ~handle url) ->
567567- [
568568- Update_sources_toml
569569- {
570570- path = Fpath.(monorepo / "sources.toml");
571571- name;
572572- entry =
573573- Sources_registry.
574574- {
575575- url = normalize_git_url url;
576576- upstream = None;
577577- branch = Some branch;
578578- reason = None;
579579- origin = Some Fork;
580580- };
581581- };
582582- ]
583583- | Some _ -> [] (* Own namespace - no sources.toml entry needed *)
510510+ let sources_actions = match push_url with
511511+ | Some url when not (is_own_namespace ~handle url) -> [
512512+ Update_sources_toml {
513513+ path = Fpath.(monorepo / "sources.toml");
514514+ name;
515515+ entry = Sources_registry.{
516516+ url = normalize_git_url url;
517517+ upstream = None;
518518+ branch = Some branch;
519519+ reason = None;
520520+ origin = Some Fork;
521521+ };
522522+ };
523523+ ]
524524+ | Some _ -> [] (* Own namespace - no sources.toml entry needed *)
584525 | None -> []
585526 in
586527587587- let actions =
588588- create_src_actions @ remote_actions @ rejoin_actions @ sources_actions
589589- in
528528+ let actions = create_src_actions @ remote_actions @ rejoin_actions @ sources_actions in
590529591591- let result =
592592- {
593593- name;
594594- split_commit =
595595- (if has_subtree_hist then "(will be computed)" else "(fresh package)");
596596- src_path;
597597- push_url;
598598- packages_created = opam_files;
599599- }
600600- in
530530+ let result = {
531531+ name;
532532+ split_commit = if has_subtree_hist then "(will be computed)" else "(fresh package)";
533533+ src_path;
534534+ push_url;
535535+ packages_created = opam_files;
536536+ } in
601537602538 Ok { discovery; actions; result; dry_run }
603539 end
···612548 let src_path = Fpath.(checkouts / name) in
613549614550 (* Gather discovery information *)
615615- let subtree_exists =
616616- Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix
617617- in
551551+ let subtree_exists = Git.Subtree.exists ~fs ~repo:monorepo ~prefix in
618552 let src_exists = is_directory ~fs src_path in
619553 let local_is_repo =
620554 if is_local then begin
621555 match Fpath.of_string source with
622622- | Ok path -> Some (Git_cli.is_repo ~proc ~fs path)
556556+ | Ok path -> Some (Git.is_repo ~proc ~fs path)
623557 | Error _ -> Some false
624624- end
625625- else None
558558+ end else None
626559 in
627560628628- let discovery =
629629- {
630630- mono_exists = subtree_exists;
631631- src_exists;
632632- has_subtree_history = false;
633633- remote_accessible = None;
634634- opam_files = [];
635635- (* Will be discovered after join *)
636636- local_path_is_repo = local_is_repo;
637637- }
638638- in
561561+ let discovery = {
562562+ mono_exists = subtree_exists;
563563+ src_exists;
564564+ has_subtree_history = false;
565565+ remote_accessible = None;
566566+ opam_files = []; (* Will be discovered after join *)
567567+ local_path_is_repo = local_is_repo;
568568+ } in
639569640570 (* Validation *)
641641- if subtree_exists then Error (Subtree_already_exists name)
571571+ if subtree_exists then
572572+ Error (Subtree_already_exists name)
642573 else begin
643574 let branch = Verse_config.default_branch in
644575 let actions =
···653584 [
654585 Create_directory checkouts;
655586 Copy_directory { src = local_path; dest = src_path };
656656- Git_subtree_add
657657- {
658658- repo = monorepo;
659659- prefix;
660660- url = Uri.of_string (Fpath.to_string src_path);
661661- branch;
662662- };
587587+ Git_subtree_add { repo = monorepo; prefix; url = Uri.of_string (Fpath.to_string src_path); branch };
663588 ]
664589 else
665590 (* Local directory without git - init and commit first *)
···669594 Git_init src_path;
670595 Copy_directory { src = local_path; dest = src_path };
671596 Git_add_all src_path;
672672- Git_commit
673673- {
674674- repo = src_path;
675675- message = Fmt.str "Initial commit of %s" name;
676676- };
677677- Git_branch_rename { repo = src_path; new_name = branch };
678678- (* Ensure branch is named correctly *)
679679- Git_subtree_add
680680- {
681681- repo = monorepo;
682682- prefix;
683683- url = Uri.of_string (Fpath.to_string src_path);
684684- branch;
685685- };
597597+ Git_commit { repo = src_path; message = Fmt.str "Initial commit of %s" name };
598598+ Git_branch_rename { repo = src_path; new_name = branch }; (* Ensure branch is named correctly *)
599599+ Git_subtree_add { repo = monorepo; prefix; url = Uri.of_string (Fpath.to_string src_path); branch };
686600 ]
687687- end
688688- else begin
601601+ end else begin
689602 (* Join from URL (existing behavior) *)
690603 let url_uri = Uri.of_string source in
691691- let base_actions =
692692- [
693693- Create_directory checkouts;
694694- Git_clone { url = source; dest = src_path; branch };
695695- Git_subtree_add { repo = monorepo; prefix; url = url_uri; branch };
696696- ]
697697- in
698698- let sources_actions =
699699- match upstream with
604604+ let base_actions = [
605605+ Create_directory checkouts;
606606+ Git_clone { url = source; dest = src_path; branch };
607607+ Git_subtree_add { repo = monorepo; prefix; url = url_uri; branch };
608608+ ] in
609609+ let sources_actions = match upstream with
700610 | Some _ ->
701701- [
702702- Update_sources_toml
703703- {
704704- path = Fpath.(monorepo / "sources.toml");
705705- name;
706706- entry =
707707- Sources_registry.
708708- {
709709- url = normalize_git_url source;
710710- upstream = Option.map normalize_git_url upstream;
711711- branch = Some branch;
712712- reason = None;
713713- origin = Some Join;
714714- };
715715- };
716716- ]
611611+ [Update_sources_toml {
612612+ path = Fpath.(monorepo / "sources.toml");
613613+ name;
614614+ entry = Sources_registry.{
615615+ url = normalize_git_url source;
616616+ upstream = Option.map normalize_git_url upstream;
617617+ branch = Some branch;
618618+ reason = None;
619619+ origin = Some Join;
620620+ };
621621+ }]
717622 | None -> []
718623 in
719624 base_actions @ sources_actions
···729634 else []
730635 in
731636732732- let result =
733733- {
734734- name;
735735- source_url = source;
736736- upstream_url = upstream;
737737- packages_added = opam_preview;
738738- from_handle = None;
739739- }
740740- in
637637+ let result = {
638638+ name;
639639+ source_url = source;
640640+ upstream_url = upstream;
641641+ packages_added = opam_preview;
642642+ from_handle = None;
643643+ } in
741644742742- Ok
743743- {
744744- discovery = { discovery with opam_files = opam_preview };
745745- actions;
746746- result;
747747- dry_run;
748748- }
645645+ Ok { discovery = { discovery with opam_files = opam_preview }; actions; result; dry_run }
749646 end
750647751648(** Build a rejoin plan - add existing src/<name> back into mono/<name> *)
···756653 let src_path = Fpath.(checkouts / name) in
757654758655 (* Gather discovery information *)
759759- let subtree_exists =
760760- Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix
761761- in
656656+ let subtree_exists = Git.Subtree.exists ~fs ~repo:monorepo ~prefix in
762657 let src_exists = is_directory ~fs src_path in
763763- let src_is_repo =
764764- if src_exists then Git_cli.is_repo ~proc ~fs src_path else false
765765- in
658658+ let src_is_repo = if src_exists then Git.is_repo ~proc ~fs src_path else false in
766659 let opam_files = if src_exists then find_opam_files ~fs src_path else [] in
767660768768- let discovery =
769769- {
770770- mono_exists = subtree_exists;
771771- src_exists;
772772- has_subtree_history = false;
773773- remote_accessible = None;
774774- opam_files;
775775- local_path_is_repo = Some src_is_repo;
776776- }
777777- in
661661+ let discovery = {
662662+ mono_exists = subtree_exists;
663663+ src_exists;
664664+ has_subtree_history = false;
665665+ remote_accessible = None;
666666+ opam_files;
667667+ local_path_is_repo = Some src_is_repo;
668668+ } in
778669779670 (* Validation *)
780780- if subtree_exists then Error (Subtree_already_exists name)
781781- else if not src_exists then Error (Src_not_found name)
671671+ if subtree_exists then
672672+ Error (Subtree_already_exists name)
673673+ else if not src_exists then
674674+ Error (Src_not_found name)
782675 else if not src_is_repo then
783783- Error
784784- (Config_error (Fmt.str "src/%s exists but is not a git repository" name))
676676+ Error (Config_error (Fmt.str "src/%s exists but is not a git repository" name))
785677 else begin
786678 let branch = Verse_config.default_branch in
787787- let actions =
788788- [
789789- Git_subtree_add
790790- {
791791- repo = monorepo;
792792- prefix;
793793- url = Uri.of_string (Fpath.to_string src_path);
794794- branch;
795795- };
796796- ]
797797- in
679679+ let actions = [
680680+ Git_subtree_add { repo = monorepo; prefix; url = Uri.of_string (Fpath.to_string src_path); branch };
681681+ ] in
798682799799- let result =
800800- {
801801- name;
802802- source_url = Fpath.to_string src_path;
803803- upstream_url = None;
804804- packages_added = opam_files;
805805- from_handle = None;
806806- }
807807- in
683683+ let result = {
684684+ name;
685685+ source_url = Fpath.to_string src_path;
686686+ upstream_url = None;
687687+ packages_added = opam_files;
688688+ from_handle = None;
689689+ } in
808690809691 Ok { discovery; actions; result; dry_run }
810692 end
811693812694(** {1 Plan Execution} *)
813695814814-type exec_state = { mutable split_commit : string option }
815696(** State tracked during plan execution *)
697697+type exec_state = {
698698+ mutable split_commit: string option;
699699+}
816700817701(** Execute a single action *)
818702let execute_action ~proc ~fs ~state action =
···824708 ensure_dir ~fs path;
825709 Ok ()
826710 | Git_init path ->
827827- Git_cli.init ~proc ~fs path |> Result.map_error (fun e -> Git_error e)
711711+ Git.init ~proc ~fs path |> Result.map_error (fun e -> Git_error e)
828712 | Git_config { repo; key; value } ->
829829- Git_cli.config ~proc ~fs ~key ~value repo
830830- |> Result.map_error (fun e -> Git_error e)
713713+ Git.config ~proc ~fs ~key ~value repo |> Result.map_error (fun e -> Git_error e)
831714 | Git_clone { url; dest; branch } ->
832832- Git_cli.clone ~proc ~fs ~url:(Uri.of_string url) ~branch dest
715715+ Git.clone ~proc ~fs ~url:(Uri.of_string url) ~branch dest
833716 |> Result.map_error (fun e -> Git_error e)
834834- | Git_subtree_split { repo; prefix } -> (
835835- let repo_path = Fpath.to_string repo in
836836- let git_repo = Git.Repository.open_repo ~fs repo_path in
837837- match Git.Repository.read_ref git_repo "HEAD" with
838838- | None -> Error (Git_error (Git_cli.Io_error "no HEAD ref found"))
839839- | Some head -> (
840840- match Git.Subtree.split git_repo ~prefix ~head () with
841841- | Ok None -> Error (Git_error (Git_cli.Subtree_prefix_missing prefix))
842842- | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
843843- | Ok (Some split_hash) ->
844844- state.split_commit <- Some (Git.Hash.to_hex split_hash);
845845- Ok ()))
846846- | Git_subtree_add { repo; prefix; url; branch } -> (
847847- (* Fetch the branch first to get the commit *)
848848- match Git_cli.fetch_url ~proc ~fs ~repo ~url ~branch () with
849849- | Error e -> Error (Git_error e)
850850- | Ok hash_hex -> (
851851- let repo_path = Fpath.to_string repo in
852852- let git_repo = Git.Repository.open_repo ~fs repo_path in
853853- let commit = Git.Hash.of_hex hash_hex in
854854- let user =
855855- Git.User.make ~name:"monopam" ~email:"monopam@localhost"
856856- ~date:(Int64.of_float (Unix.time ()))
857857- ()
858858- in
859859- let message =
860860- Fmt.str "Add '%s' from %s\n\ngit-subtree-dir: %s\n" prefix
861861- (Uri.to_string url) prefix
862862- in
863863- match
864864- Git.Subtree.add git_repo ~prefix ~commit ~author:user
865865- ~committer:user ~message ()
866866- with
867867- | Ok _ -> Ok ()
868868- | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))))
717717+ | Git_subtree_split { repo; prefix } ->
718718+ Git.Subtree.split ~proc ~fs ~repo ~prefix ()
719719+ |> Result.map (fun commit -> state.split_commit <- Some commit)
720720+ |> Result.map_error (fun e -> Git_error e)
721721+ | Git_subtree_add { repo; prefix; url; branch } ->
722722+ Git.Subtree.add ~proc ~fs ~repo ~prefix ~url ~branch ()
723723+ |> Result.map_error (fun e -> Git_error e)
869724 | Git_add_remote { repo; name; url } ->
870870- Git_cli.add_remote ~proc ~fs ~name ~url repo
725725+ Git.add_remote ~proc ~fs ~name ~url repo
871726 |> Result.map_error (fun e -> Git_error e)
872727 | Git_push_ref { repo; target; ref_spec } ->
873728 (* Replace SPLIT_COMMIT placeholder with actual commit if available *)
874729 let ref_spec =
875730 match state.split_commit with
876876- | Some commit ->
877877- String.concat ""
878878- (String.split_on_char 'S'
879879- (String.concat commit (String.split_on_char 'S' ref_spec)))
880880- |> fun s ->
881881- if String.starts_with ~prefix:"PLIT_COMMIT" s then
882882- Option.value ~default:ref_spec state.split_commit
883883- ^ String.sub s 11 (String.length s - 11)
884884- else s
731731+ | Some commit -> String.concat "" (String.split_on_char 'S' (String.concat commit (String.split_on_char 'S' ref_spec)))
732732+ |> fun s -> if String.starts_with ~prefix:"PLIT_COMMIT" s then
733733+ Option.value ~default:ref_spec state.split_commit ^ String.sub s 11 (String.length s - 11)
734734+ else s
885735 | None -> ref_spec
886736 in
887737 (* Better replacement: look for SPLIT_COMMIT literal *)
888738 let ref_spec =
889739 match state.split_commit with
890740 | Some commit ->
891891- if
892892- String.length ref_spec >= 12
893893- && String.sub ref_spec 0 12 = "SPLIT_COMMIT"
894894- then commit ^ String.sub ref_spec 12 (String.length ref_spec - 12)
741741+ if String.length ref_spec >= 12 && String.sub ref_spec 0 12 = "SPLIT_COMMIT" then
742742+ commit ^ String.sub ref_spec 12 (String.length ref_spec - 12)
895743 else ref_spec
896744 | None -> ref_spec
897745 in
898898- Git_cli.push_ref ~proc ~fs ~repo ~target ~ref_spec ()
746746+ Git.push_ref ~proc ~fs ~repo ~target ~ref_spec ()
899747 |> Result.map_error (fun e -> Git_error e)
900748 | Git_checkout { repo; branch } ->
901901- Git_cli.checkout ~proc ~fs ~branch repo
749749+ Git.checkout ~proc ~fs ~branch repo
902750 |> Result.map_error (fun e -> Git_error e)
903751 | Git_branch_rename { repo; new_name } ->
904904- Git_cli.branch_rename ~proc ~fs ~new_name repo
752752+ Git.branch_rename ~proc ~fs ~new_name repo
905753 |> Result.map_error (fun e -> Git_error e)
906754 | Copy_directory { src; dest } ->
907755 copy_directory ~fs ~src ~dest;
908756 Ok ()
909757 | Git_add_all path ->
910910- Git_cli.add_all ~proc ~fs path |> Result.map_error (fun e -> Git_error e)
758758+ Git.add_all ~proc ~fs path
759759+ |> Result.map_error (fun e -> Git_error e)
911760 | Git_commit { repo; message } ->
912912- Git_cli.commit ~proc ~fs ~message repo
761761+ Git.commit ~proc ~fs ~message repo
913762 |> Result.map_error (fun e -> Git_error e)
914763 | Git_rm { repo; path; recursive } ->
915915- Git_cli.rm ~proc ~fs ~recursive repo path
764764+ Git.rm ~proc ~fs ~recursive repo path
916765 |> Result.map_error (fun e -> Git_error e)
917917- | Update_sources_toml { path; name; entry } -> (
766766+ | Update_sources_toml { path; name; entry } ->
918767 let sources =
919768 match Sources_registry.load ~fs:(fs :> _ Eio.Path.t) path with
920769 | Ok s -> s
921770 | Error _ -> Sources_registry.empty
922771 in
923772 let sources = Sources_registry.add sources ~subtree:name entry in
924924- match Sources_registry.save ~fs:(fs :> _ Eio.Path.t) path sources with
925925- | Ok () -> Ok ()
926926- | Error msg ->
927927- Error (Config_error (Fmt.str "Failed to update sources.toml: %s" msg))
928928- )
773773+ (match Sources_registry.save ~fs:(fs :> _ Eio.Path.t) path sources with
774774+ | Ok () -> Ok ()
775775+ | Error msg -> Error (Config_error (Fmt.str "Failed to update sources.toml: %s" msg)))
929776930777(** Execute a complete fork action plan *)
931778let execute_fork_plan ~proc ~fs plan =
932932- if plan.dry_run then Ok plan.result
779779+ if plan.dry_run then
780780+ Ok plan.result
933781 else begin
934782 let state = { split_commit = None } in
935783 let rec run_actions = function
936784 | [] -> Ok ()
937937- | action :: rest -> (
785785+ | action :: rest ->
938786 match execute_action ~proc ~fs ~state action with
939787 | Error e -> Error e
940940- | Ok () -> run_actions rest)
788788+ | Ok () -> run_actions rest
941789 in
942790 match run_actions plan.actions with
943791 | Error e -> Error e
···953801954802(** Execute a complete join action plan *)
955803let execute_join_plan ~proc ~fs plan =
956956- if plan.dry_run then Ok plan.result
804804+ if plan.dry_run then
805805+ Ok plan.result
957806 else begin
958807 let state = { split_commit = None } in
959808 let rec run_actions = function
960809 | [] -> Ok ()
961961- | action :: rest -> (
810810+ | action :: rest ->
962811 match execute_action ~proc ~fs ~state action with
963812 | Error e -> Error e
964964- | Ok () -> run_actions rest)
813813+ | Ok () -> run_actions rest
965814 in
966815 match run_actions plan.actions with
967816 | Error e -> Error e
···977826 let subtree_path = Fpath.(monorepo / prefix) in
978827 let src_path = Fpath.(checkouts / name) in
979828 (* Validate: mono/<name>/ must exist *)
980980- if not (Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix) then
981981- Error (Subtree_not_found name) (* Validate: src/<name>/ must not exist *)
982982- else if is_directory ~fs src_path then Error (Src_already_exists name)
829829+ if not (Git.Subtree.exists ~fs ~repo:monorepo ~prefix) then
830830+ Error (Subtree_not_found name)
831831+ (* Validate: src/<name>/ must not exist *)
832832+ else if is_directory ~fs src_path then
833833+ Error (Src_already_exists name)
983834 else begin
984835 (* Find .opam files in subtree *)
985836 let packages = find_opam_files ~fs subtree_path in
986986- if packages = [] then Error (No_opam_files name)
837837+ if packages = [] then
838838+ Error (No_opam_files name)
987839 else if dry_run then
988988- Ok
989989- {
990990- name;
991991- split_commit = "(dry-run)";
992992- src_path;
993993- push_url;
994994- packages_created = packages;
995995- }
840840+ Ok { name; split_commit = "(dry-run)"; src_path; push_url; packages_created = packages }
996841 else begin
997842 (* Split the subtree to get history *)
998998- let repo_path = Fpath.to_string monorepo in
999999- let git_repo = Git.Repository.open_repo ~fs repo_path in
10001000- match Git.Repository.read_ref git_repo "HEAD" with
10011001- | None -> Error (Git_error (Git_cli.Io_error "no HEAD ref found"))
10021002- | Some head -> (
10031003- match Git.Subtree.split git_repo ~prefix ~head () with
10041004- | Ok None -> Error (Git_error (Git_cli.Subtree_prefix_missing prefix))
10051005- | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
10061006- | Ok (Some split_hash) -> (
10071007- let split_commit = Git.Hash.to_hex split_hash in
10081008- (* Ensure src/ exists *)
10091009- ensure_dir ~fs checkouts;
10101010- (* Initialize new git repo at src/<name>/ *)
10111011- match Git_cli.init ~proc ~fs src_path with
10121012- | Error e -> Error (Git_error e)
10131013- | Ok () -> (
10141014- (* Add 'origin' remote pointing to monorepo path temporarily *)
10151015- let mono_str = Fpath.to_string monorepo in
10161016- match
10171017- Git_cli.add_remote ~proc ~fs ~name:"mono" ~url:mono_str
10181018- src_path
10191019- with
10201020- | Error e -> Error (Git_error e)
10211021- | Ok () -> (
10221022- (* Push split commit to local repo *)
10231023- let ref_spec = split_commit ^ ":refs/heads/main" in
10241024- match
10251025- Git_cli.push_ref ~proc ~fs ~repo:monorepo
10261026- ~target:(Fpath.to_string src_path) ~ref_spec ()
10271027- with
10281028- | Error e -> Error (Git_error e)
10291029- | Ok () -> (
10301030- (* Checkout main branch *)
10311031- match
10321032- Git_cli.checkout ~proc ~fs ~branch:"main" src_path
10331033- with
10341034- | Error e -> Error (Git_error e)
10351035- | Ok () -> (
10361036- (* Set push URL if provided *)
10371037- let push_result =
10381038- match push_url with
10391039- | Some url -> (
10401040- match
10411041- Git_cli.add_remote ~proc ~fs
10421042- ~name:"origin" ~url src_path
10431043- with
10441044- | Error e -> Error (Git_error e)
10451045- | Ok () -> Ok ())
10461046- | None -> Ok ()
10471047- in
10481048- match push_result with
10491049- | Error _ as e -> e
10501050- | Ok () ->
10511051- (* Only update sources.toml if there's a push URL *)
10521052- (match push_url with
10531053- | Some url -> (
10541054- let sources_path =
10551055- Fpath.(monorepo / "sources.toml")
10561056- in
10571057- let sources =
10581058- match
10591059- Sources_registry.load
10601060- ~fs:(fs :> _ Eio.Path.t)
10611061- sources_path
10621062- with
10631063- | Ok s -> s
10641064- | Error _ -> Sources_registry.empty
10651065- in
10661066- let entry =
10671067- Sources_registry.
10681068- {
10691069- url = normalize_git_url url;
10701070- upstream = None;
10711071- branch = Some "main";
10721072- reason = None;
10731073- origin = Some Fork;
10741074- }
10751075- in
10761076- let sources =
10771077- Sources_registry.add sources
10781078- ~subtree:name entry
10791079- in
10801080- match
10811081- Sources_registry.save
10821082- ~fs:(fs :> _ Eio.Path.t)
10831083- sources_path sources
10841084- with
843843+ match Git.Subtree.split ~proc ~fs ~repo:monorepo ~prefix () with
844844+ | Error e -> Error (Git_error e)
845845+ | Ok split_commit ->
846846+ (* Ensure src/ exists *)
847847+ ensure_dir ~fs checkouts;
848848+ (* Initialize new git repo at src/<name>/ *)
849849+ match Git.init ~proc ~fs src_path with
850850+ | Error e -> Error (Git_error e)
851851+ | Ok () ->
852852+ (* Add 'origin' remote pointing to monorepo path temporarily *)
853853+ let mono_str = Fpath.to_string monorepo in
854854+ (match Git.add_remote ~proc ~fs ~name:"mono" ~url:mono_str src_path with
855855+ | Error e -> Error (Git_error e)
856856+ | Ok () ->
857857+ (* Push split commit to local repo *)
858858+ let ref_spec = split_commit ^ ":refs/heads/main" in
859859+ match Git.push_ref ~proc ~fs ~repo:monorepo ~target:(Fpath.to_string src_path) ~ref_spec () with
860860+ | Error e -> Error (Git_error e)
861861+ | Ok () ->
862862+ (* Checkout main branch *)
863863+ (match Git.checkout ~proc ~fs ~branch:"main" src_path with
864864+ | Error e -> Error (Git_error e)
865865+ | Ok () ->
866866+ (* Set push URL if provided *)
867867+ let push_result =
868868+ match push_url with
869869+ | Some url ->
870870+ (match Git.add_remote ~proc ~fs ~name:"origin" ~url src_path with
871871+ | Error e -> Error (Git_error e)
872872+ | Ok () -> Ok ())
873873+ | None -> Ok ()
874874+ in
875875+ match push_result with
876876+ | Error _ as e -> e
877877+ | Ok () ->
878878+ (* Only update sources.toml if there's a push URL *)
879879+ (match push_url with
880880+ | Some url ->
881881+ let sources_path = Fpath.(monorepo / "sources.toml") in
882882+ let sources =
883883+ match Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path with
884884+ | Ok s -> s
885885+ | Error _ -> Sources_registry.empty
886886+ in
887887+ let entry = Sources_registry.{
888888+ url = normalize_git_url url;
889889+ upstream = None;
890890+ branch = Some "main";
891891+ reason = None;
892892+ origin = Some Fork;
893893+ } in
894894+ let sources = Sources_registry.add sources ~subtree:name entry in
895895+ (match Sources_registry.save ~fs:(fs :> _ Eio.Path.t) sources_path sources with
1085896 | Ok () -> ()
10861086- | Error msg ->
10871087- Logs.warn (fun m ->
10881088- m
10891089- "Failed to update \
10901090- sources.toml: %s"
10911091- msg))
10921092- | None -> ());
10931093- Ok
10941094- {
10951095- name;
10961096- split_commit;
10971097- src_path;
10981098- push_url;
10991099- packages_created = packages;
11001100- }))))))
897897+ | Error msg -> Logs.warn (fun m -> m "Failed to update sources.toml: %s" msg))
898898+ | None -> ());
899899+ Ok { name; split_commit; src_path; push_url; packages_created = packages }))
1101900 end
1102901 end
1103902···1109908 let subtree_path = Fpath.(monorepo / prefix) in
1110909 let src_path = Fpath.(checkouts / name) in
1111910 (* Validate: mono/<name>/ must not exist *)
11121112- if Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix then
911911+ if Git.Subtree.exists ~fs ~repo:monorepo ~prefix then
1113912 Error (Subtree_already_exists name)
1114913 else if dry_run then
11151115- Ok
11161116- {
11171117- name;
11181118- source_url = url;
11191119- upstream_url = upstream;
11201120- packages_added = [];
11211121- from_handle = None;
11221122- }
914914+ Ok { name; source_url = url; upstream_url = upstream; packages_added = []; from_handle = None }
1123915 else begin
1124916 (* Ensure src/ exists *)
1125917 ensure_dir ~fs checkouts;
1126918 (* Clone to src/<name>/ *)
1127919 let branch = Verse_config.default_branch in
1128920 let uri = Uri.of_string url in
11291129- match Git_cli.clone ~proc ~fs ~url:uri ~branch src_path with
921921+ match Git.clone ~proc ~fs ~url:uri ~branch src_path with
1130922 | Error e -> Error (Git_error e)
11311131- | Ok () -> (
11321132- (* Add subtree to monorepo - first fetch to get the commit *)
11331133- match
11341134- Git_cli.fetch_url ~proc ~fs ~repo:monorepo ~url:uri ~branch ()
11351135- with
923923+ | Ok () ->
924924+ (* Add subtree to monorepo *)
925925+ match Git.Subtree.add ~proc ~fs ~repo:monorepo ~prefix ~url:uri ~branch () with
1136926 | Error e -> Error (Git_error e)
11371137- | Ok hash_hex -> (
11381138- let repo_path = Fpath.to_string monorepo in
11391139- let git_repo = Git.Repository.open_repo ~fs repo_path in
11401140- let commit = Git.Hash.of_hex hash_hex in
11411141- let user =
11421142- Git.User.make ~name:"monopam" ~email:"monopam@localhost"
11431143- ~date:(Int64.of_float (Unix.time ()))
11441144- ()
11451145- in
11461146- let message =
11471147- Fmt.str "Add '%s' from %s\n\ngit-subtree-dir: %s\n" prefix url
11481148- prefix
11491149- in
11501150- match
11511151- Git.Subtree.add git_repo ~prefix ~commit ~author:user
11521152- ~committer:user ~message ()
11531153- with
11541154- | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
11551155- | Ok _ ->
11561156- (* Find .opam files in the new subtree *)
11571157- let packages = find_opam_files ~fs subtree_path in
11581158- (* Only update sources.toml if there's an upstream to track *)
11591159- (match upstream with
11601160- | Some _ -> (
11611161- let sources_path = Fpath.(monorepo / "sources.toml") in
11621162- let sources =
11631163- match
11641164- Sources_registry.load
11651165- ~fs:(fs :> _ Eio.Path.t)
11661166- sources_path
11671167- with
11681168- | Ok s -> s
11691169- | Error _ -> Sources_registry.empty
11701170- in
11711171- let entry =
11721172- Sources_registry.
11731173- {
11741174- url = normalize_git_url url;
11751175- upstream = Option.map normalize_git_url upstream;
11761176- branch = Some branch;
11771177- reason = None;
11781178- origin = Some Join;
11791179- }
11801180- in
11811181- let sources =
11821182- Sources_registry.add sources ~subtree:name entry
11831183- in
11841184- match
11851185- Sources_registry.save
11861186- ~fs:(fs :> _ Eio.Path.t)
11871187- sources_path sources
11881188- with
11891189- | Ok () -> ()
11901190- | Error msg ->
11911191- Logs.warn (fun m ->
11921192- m "Failed to update sources.toml: %s" msg))
11931193- | None -> ());
11941194- Ok
11951195- {
11961196- name;
11971197- source_url = url;
11981198- upstream_url = upstream;
11991199- packages_added = packages;
12001200- from_handle = None;
12011201- }))
927927+ | Ok () ->
928928+ (* Find .opam files in the new subtree *)
929929+ let packages = find_opam_files ~fs subtree_path in
930930+ (* Only update sources.toml if there's an upstream to track *)
931931+ (match upstream with
932932+ | Some _ ->
933933+ let sources_path = Fpath.(monorepo / "sources.toml") in
934934+ let sources =
935935+ match Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path with
936936+ | Ok s -> s
937937+ | Error _ -> Sources_registry.empty
938938+ in
939939+ let entry = Sources_registry.{
940940+ url = normalize_git_url url;
941941+ upstream = Option.map normalize_git_url upstream;
942942+ branch = Some branch;
943943+ reason = None;
944944+ origin = Some Join;
945945+ } in
946946+ let sources = Sources_registry.add sources ~subtree:name entry in
947947+ (match Sources_registry.save ~fs:(fs :> _ Eio.Path.t) sources_path sources with
948948+ | Ok () -> ()
949949+ | Error msg -> Logs.warn (fun m -> m "Failed to update sources.toml: %s" msg))
950950+ | None -> ());
951951+ Ok { name; source_url = url; upstream_url = upstream; packages_added = packages; from_handle = None }
1202952 end
120395312041204-let join_from_verse ~proc ~fs ~config ~verse_config ~package ~handle ~fork_url
12051205- ?(dry_run = false) () =
954954+let join_from_verse ~proc ~fs ~config ~verse_config ~package ~handle ~fork_url ?(dry_run = false) () =
1206955 (* First use verse fork to set up the opam entries *)
12071207- match
12081208- Verse.fork ~proc ~fs ~config:verse_config ~handle ~package ~fork_url
12091209- ~dry_run ()
12101210- with
956956+ match Verse.fork ~proc ~fs ~config:verse_config ~handle ~package ~fork_url ~dry_run () with
1211957 | Error e -> Error (Verse_error e)
1212958 | Ok fork_result ->
1213959 if dry_run then
12141214- Ok
12151215- {
12161216- name = fork_result.subtree_name;
12171217- source_url = fork_url;
12181218- upstream_url = Some fork_result.upstream_url;
12191219- packages_added = fork_result.packages_forked;
12201220- from_handle = Some handle;
12211221- }
960960+ Ok {
961961+ name = fork_result.subtree_name;
962962+ source_url = fork_url;
963963+ upstream_url = Some fork_result.upstream_url;
964964+ packages_added = fork_result.packages_forked;
965965+ from_handle = Some handle;
966966+ }
1222967 else begin
1223968 (* Now join the repository *)
1224969 let name = fork_result.subtree_name in
12251225- match
12261226- join ~proc ~fs ~config ~url:fork_url ~name
12271227- ~upstream:fork_result.upstream_url ~dry_run ()
12281228- with
970970+ match join ~proc ~fs ~config ~url:fork_url ~name ~upstream:fork_result.upstream_url ~dry_run () with
1229971 | Error e -> Error e
1230972 | Ok join_result ->
12311231- Ok
12321232- {
12331233- join_result with
12341234- packages_added = fork_result.packages_forked;
12351235- from_handle = Some handle;
12361236- }
973973+ Ok { join_result with
974974+ packages_added = fork_result.packages_forked;
975975+ from_handle = Some handle;
976976+ }
1237977 end
+67-78
lib/fork_join.mli
···6677 Both operations update sources.toml to track the origin of each source.
8899- The module supports an action-based workflow where commands: 1. Analyze
1010- current state 2. Build a list of actions with reasoning 3. Display the plan
1111- with discovery details 4. Prompt for confirmation (or skip with [--yes]) 5.
1212- Execute actions sequentially *)
99+ The module supports an action-based workflow where commands:
1010+ 1. Analyze current state
1111+ 2. Build a list of actions with reasoning
1212+ 3. Display the plan with discovery details
1313+ 4. Prompt for confirmation (or skip with [--yes])
1414+ 5. Execute actions sequentially *)
13151416(** {1 Error Types} *)
15171618type error =
1719 | Config_error of string (** Configuration error *)
1818- | Git_error of Git_cli.error (** Git operation failed *)
2020+ | Git_error of Git.error (** Git operation failed *)
1921 | Subtree_not_found of string (** Subtree not found in monorepo *)
2022 | Src_already_exists of string (** Source checkout already exists *)
2123 | Src_not_found of string (** Source checkout not found *)
···4042 | Check_remote_exists of string (** URL - informational check *)
4143 | Create_directory of Fpath.t
4244 | Git_init of Fpath.t
4343- | Git_config of { repo : Fpath.t; key : string; value : string }
4444- (** Set git config *)
4545- | Git_clone of { url : string; dest : Fpath.t; branch : string }
4646- | Git_subtree_split of { repo : Fpath.t; prefix : string }
4747- | Git_subtree_add of {
4848- repo : Fpath.t;
4949- prefix : string;
5050- url : Uri.t;
5151- branch : string;
5252- }
5353- | Git_add_remote of { repo : Fpath.t; name : string; url : string }
5454- | Git_push_ref of { repo : Fpath.t; target : string; ref_spec : string }
5555- | Git_checkout of { repo : Fpath.t; branch : string }
5656- | Git_branch_rename of { repo : Fpath.t; new_name : string }
5757- (** Rename current branch *)
5858- | Copy_directory of { src : Fpath.t; dest : Fpath.t }
4545+ | Git_config of { repo: Fpath.t; key: string; value: string } (** Set git config *)
4646+ | Git_clone of { url: string; dest: Fpath.t; branch: string }
4747+ | Git_subtree_split of { repo: Fpath.t; prefix: string }
4848+ | Git_subtree_add of { repo: Fpath.t; prefix: string; url: Uri.t; branch: string }
4949+ | Git_add_remote of { repo: Fpath.t; name: string; url: string }
5050+ | Git_push_ref of { repo: Fpath.t; target: string; ref_spec: string }
5151+ | Git_checkout of { repo: Fpath.t; branch: string }
5252+ | Git_branch_rename of { repo: Fpath.t; new_name: string } (** Rename current branch *)
5353+ | Copy_directory of { src: Fpath.t; dest: Fpath.t }
5954 | Git_add_all of Fpath.t
6060- | Git_commit of { repo : Fpath.t; message : string }
6161- | Git_rm of { repo : Fpath.t; path : string; recursive : bool }
6262- (** Remove from git *)
6363- | Update_sources_toml of {
6464- path : Fpath.t;
6565- name : string;
6666- entry : Sources_registry.entry;
6767- }
5555+ | Git_commit of { repo: Fpath.t; message: string }
5656+ | Git_rm of { repo: Fpath.t; path: string; recursive: bool } (** Remove from git *)
5757+ | Update_sources_toml of { path: Fpath.t; name: string; entry: Sources_registry.entry }
68585959+(** Discovery information gathered during planning *)
6960type discovery = {
7070- mono_exists : bool; (** Does mono/<name>/ exist? *)
7171- src_exists : bool; (** Does src/<name>/ exist? *)
7272- has_subtree_history : bool; (** Can we git subtree split? *)
7373- remote_accessible : bool option; (** None = not checked, Some = result *)
7474- opam_files : string list; (** Package names found from .opam files *)
7575- local_path_is_repo : bool option; (** For join from local dir *)
6161+ mono_exists: bool; (** Does mono/<name>/ exist? *)
6262+ src_exists: bool; (** Does src/<name>/ exist? *)
6363+ has_subtree_history: bool; (** Can we git subtree split? *)
6464+ remote_accessible: bool option; (** None = not checked, Some = result *)
6565+ opam_files: string list; (** Package names found from .opam files *)
6666+ local_path_is_repo: bool option; (** For join from local dir *)
7667}
7777-(** Discovery information gathered during planning *)
78686969+(** A complete action plan *)
7970type 'a action_plan = {
8080- discovery : discovery;
8181- actions : action list;
8282- result : 'a; (** What we'll return on success *)
8383- dry_run : bool;
7171+ discovery: discovery;
7272+ actions: action list;
7373+ result: 'a; (** What we'll return on success *)
7474+ dry_run: bool;
8475}
8585-(** A complete action plan *)
86768777val pp_action : action Fmt.t
8878(** [pp_action] formats a single action. *)
···9989(** [is_local_path s] returns true if [s] looks like a local filesystem path
10090 rather than a URL. *)
10191102102-val suggest_push_url :
103103- fs:Eio.Fs.dir_ty Eio.Path.t -> ?knot:string -> Fpath.t -> string option
104104-(** [suggest_push_url ~fs ?knot subtree_path] tries to derive a push URL from
105105- the dune-project file in the subtree. Returns [Some url] if a source URL can
9292+val suggest_push_url : fs:Eio.Fs.dir_ty Eio.Path.t -> ?knot:string -> Fpath.t -> string option
9393+(** [suggest_push_url ~fs ?knot subtree_path] tries to derive a push URL from the
9494+ dune-project file in the subtree. Returns [Some url] if a source URL can
10695 be found and converted to SSH push format, [None] otherwise.
10796108108- @param knot
109109- Optional git push server for tangled URLs (default: git.recoil.org) *)
9797+ @param knot Optional git push server for tangled URLs (default: git.recoil.org) *)
1109811199(** {1 Result Types} *)
112100101101+(** Result of a fork operation. *)
113102type fork_result = {
114103 name : string; (** Subtree/repository name *)
115104 split_commit : string; (** Git commit SHA from subtree split *)
···117106 push_url : string option; (** Remote push URL if provided *)
118107 packages_created : string list; (** Package names from .opam files *)
119108}
120120-(** Result of a fork operation. *)
121109122110val pp_fork_result : fork_result Fmt.t
123111(** [pp_fork_result] formats a fork result. *)
124112113113+(** Result of a join operation. *)
125114type join_result = {
126115 name : string; (** Subtree/repository name *)
127116 source_url : string; (** URL the repository was cloned from *)
···129118 packages_added : string list; (** Package names from .opam files *)
130119 from_handle : string option; (** Verse handle if joined from verse *)
131120}
132132-(** Result of a join operation. *)
133121134122val pp_join_result : join_result Fmt.t
135123(** [pp_join_result] formats a join result. *)
···145133 ?dry_run:bool ->
146134 unit ->
147135 (fork_result action_plan, error) result
148148-(** [plan_fork ~proc ~fs ~config ~name ?push_url ?dry_run ()] builds a fork
149149- plan.
136136+(** [plan_fork ~proc ~fs ~config ~name ?push_url ?dry_run ()] builds a fork plan.
150137151138 This analyzes the current state and builds a list of actions to:
152139 - For subtrees with history: split subtree, create repo, push history
···168155 ?dry_run:bool ->
169156 unit ->
170157 (join_result action_plan, error) result
171171-(** [plan_join ~proc ~fs ~config ~source ?name ?upstream ?dry_run ()] builds a
172172- join plan.
158158+(** [plan_join ~proc ~fs ~config ~source ?name ?upstream ?dry_run ()] builds a join plan.
173159174174- This analyzes the source (URL or local path) and builds a list of actions
175175- to:
160160+ This analyzes the source (URL or local path) and builds a list of actions to:
176161 - For URLs: clone repo, add subtree
177162 - For local directories: copy/init repo, add subtree
178163179164 The plan can be displayed to the user and executed with [execute_join_plan].
180165181166 @param source Git URL or local filesystem path to join
182182- @param name
183183- Override the subtree directory name (default: derived from source)
167167+ @param name Override the subtree directory name (default: derived from source)
184168 @param upstream Original upstream URL if this is your fork
185169 @param dry_run If true, mark plan as dry-run (execute will skip actions) *)
186170···194178 (join_result action_plan, error) result
195179(** [plan_rejoin ~proc ~fs ~config ~name ?dry_run ()] builds a rejoin plan.
196180197197- This is used to add an existing src/<name>/ repository back into
198198- mono/<name>/ as a subtree. Useful after forking a package and removing it
199199- from the monorepo.
181181+ This is used to add an existing src/<name>/ repository back into mono/<name>/
182182+ as a subtree. Useful after forking a package and removing it from the monorepo.
200183201184 Requires:
202185 - src/<name>/ must exist and be a git repository
···216199 (fork_result, error) result
217200(** [execute_fork_plan ~proc ~fs plan] executes a fork action plan.
218201219219- Returns the fork result with the actual split commit (if applicable). If the
220220- plan is marked as dry-run, returns the plan's result without executing any
221221- actions. *)
202202+ Returns the fork result with the actual split commit (if applicable).
203203+ If the plan is marked as dry-run, returns the plan's result without
204204+ executing any actions. *)
222205223206val execute_join_plan :
224207 proc:_ Eio.Process.mgr ->
···244227(** [fork ~proc ~fs ~config ~name ?push_url ?dry_run ()] splits a monorepo
245228 subtree into its own repository.
246229247247- This operation: 1. Validates mono/<name>/ exists 2. Validates src/<name>/
248248- does not exist 3. Uses [git subtree split] to extract history 4. Creates a
249249- new git repo at src/<name>/ 5. Pushes the split commit to the new repo 6.
250250- Updates sources.toml with [origin = "fork"] 7. Auto-discovers packages from
251251- .opam files
230230+ This operation:
231231+ 1. Validates mono/<name>/ exists
232232+ 2. Validates src/<name>/ does not exist
233233+ 3. Uses [git subtree split] to extract history
234234+ 4. Creates a new git repo at src/<name>/
235235+ 5. Pushes the split commit to the new repo
236236+ 6. Updates sources.toml with [origin = "fork"]
237237+ 7. Auto-discovers packages from .opam files
252238253239 @param name Name of the subtree to fork (directory name under mono/)
254240 @param push_url Optional remote URL to add as origin for pushing
···269255(** [join ~proc ~fs ~config ~url ?name ?upstream ?dry_run ()] brings an external
270256 repository into the monorepo.
271257272272- This operation: 1. Derives name from URL if not provided 2. Validates
273273- mono/<name>/ does not exist 3. Clones the repository to src/<name>/ 4. Uses
274274- [git subtree add] to bring into monorepo 5. Updates sources.toml with
275275- [origin = "join"] 6. Auto-discovers packages from .opam files
258258+ This operation:
259259+ 1. Derives name from URL if not provided
260260+ 2. Validates mono/<name>/ does not exist
261261+ 3. Clones the repository to src/<name>/
262262+ 4. Uses [git subtree add] to bring into monorepo
263263+ 5. Updates sources.toml with [origin = "join"]
264264+ 6. Auto-discovers packages from .opam files
276265277266 @param url Git URL to clone from
278267 @param name Override the subtree directory name (default: derived from URL)
279279- @param upstream
280280- Original upstream URL if this is your fork of another project
268268+ @param upstream Original upstream URL if this is your fork of another project
281269 @param dry_run If true, validate and report what would be done *)
282270283271val join_from_verse :
···294282(** [join_from_verse ~proc ~fs ~config ~verse_config ~package ~handle ~fork_url
295283 ?dry_run ()] joins a package from a verse member's repository.
296284297297- This combines [Verse.fork] (to set up opam entries) with [join]: 1. Looks up
298298- the package in verse/<handle>-opam/ 2. Finds all packages sharing the same
299299- git repository 3. Creates opam entries pointing to your fork 4. Clones and
300300- adds the subtree
285285+ This combines [Verse.fork] (to set up opam entries) with [join]:
286286+ 1. Looks up the package in verse/<handle>-opam/
287287+ 2. Finds all packages sharing the same git repository
288288+ 3. Creates opam entries pointing to your fork
289289+ 4. Clones and adds the subtree
301290302291 @param verse_config Verse configuration (for accessing verse/ directory)
303292 @param package Package name to look up
+53-167
lib/forks.ml
···3030 if String.length content > 2 then begin
3131 let inner = String.sub content 1 (String.length content - 2) in
3232 let pairs = String.split_on_char ',' inner in
3333- List.iter
3434- (fun pair ->
3535- let pair = String.trim pair in
3636- match String.split_on_char ':' pair with
3737- | [ key; value ] -> (
3838- let key = String.trim key in
3939- let value = String.trim value in
4040- (* Strip quotes from key *)
4141- let key =
4242- if String.length key > 2 && key.[0] = '"' then
4343- String.sub key 1 (String.length key - 2)
4444- else key
4545- in
4646- match float_of_string_opt value with
4747- | Some ts -> Hashtbl.replace fetch_cache key ts
4848- | None -> ())
4949- | _ -> ())
3333+ List.iter (fun pair ->
3434+ let pair = String.trim pair in
3535+ match String.split_on_char ':' pair with
3636+ | [key; value] ->
3737+ let key = String.trim key in
3838+ let value = String.trim value in
3939+ (* Strip quotes from key *)
4040+ let key = if String.length key > 2 && key.[0] = '"' then
4141+ String.sub key 1 (String.length key - 2)
4242+ else key
4343+ in
4444+ (match float_of_string_opt value with
4545+ | Some ts -> Hashtbl.replace fetch_cache key ts
4646+ | None -> ())
4747+ | _ -> ())
5048 pairs
5149 end
5250 with _ -> ()
···6260 ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir)));
6361 (* Write cache as JSON *)
6462 Out_channel.with_open_text path (fun oc ->
6565- output_string oc "{\n";
6666- let first = ref true in
6767- Hashtbl.iter
6868- (fun key ts ->
6969- if not !first then output_string oc ",\n";
7070- first := false;
7171- Printf.fprintf oc " \"%s\": %.0f" key ts)
7272- fetch_cache;
7373- output_string oc "\n}\n")
6363+ output_string oc "{\n";
6464+ let first = ref true in
6565+ Hashtbl.iter (fun key ts ->
6666+ if not !first then output_string oc ",\n";
6767+ first := false;
6868+ Printf.fprintf oc " \"%s\": %.0f" key ts)
6969+ fetch_cache;
7070+ output_string oc "\n}\n")
7471 with _ -> ()
75727673(** Check if a fetch is needed for a cache key *)
···9289 Hashtbl.replace fetch_cache key now;
9390 save_cache ()
94919595-(* ==================== Scan Cache ==================== *)
9696-9797-(** In-memory cache of scanned opam repo results: opam_path -> (pkg_name, url)
9898- list *)
9999-let scan_cache : (string, (string * Uri.t) list) Hashtbl.t = Hashtbl.create 64
100100-101101-(** Scan cache file path *)
102102-let scan_cache_file_path () =
103103- Fpath.(to_string (Verse_config.cache_dir () / "scan-cache.json"))
104104-105105-(** Load scan cache from disk. Uses simple line-based format:
106106- path<TAB>pkg1<TAB>url1<TAB>pkg2<TAB>url2... *)
107107-let load_scan_cache () =
108108- let path = scan_cache_file_path () in
109109- if Sys.file_exists path then begin
110110- try
111111- let lines =
112112- In_channel.with_open_text path (fun ic ->
113113- let rec read acc =
114114- match In_channel.input_line ic with
115115- | Some line -> read (line :: acc)
116116- | None -> List.rev acc
117117- in
118118- read [])
119119- in
120120- List.iter
121121- (fun line ->
122122- match String.split_on_char '\t' line with
123123- | key :: rest when List.length rest >= 2 ->
124124- (* rest is alternating pkg, url, pkg, url, ... *)
125125- let rec parse_pairs acc = function
126126- | pkg :: url :: tail ->
127127- parse_pairs ((pkg, Uri.of_string url) :: acc) tail
128128- | _ -> List.rev acc
129129- in
130130- let pairs = parse_pairs [] rest in
131131- if pairs <> [] then Hashtbl.replace scan_cache key pairs
132132- | _ -> ())
133133- lines;
134134- Log.debug (fun m ->
135135- m "Loaded scan cache with %d entries" (Hashtbl.length scan_cache))
136136- with _ -> ()
137137- end
138138-139139-(** Save scan cache to disk. Uses simple line-based format. *)
140140-let save_scan_cache () =
141141- let path = scan_cache_file_path () in
142142- try
143143- let dir = Filename.dirname path in
144144- if not (Sys.file_exists dir) then
145145- ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir)));
146146- Out_channel.with_open_text path (fun oc ->
147147- Hashtbl.iter
148148- (fun key pairs ->
149149- output_string oc key;
150150- List.iter
151151- (fun (pkg, url) ->
152152- output_char oc '\t';
153153- output_string oc pkg;
154154- output_char oc '\t';
155155- output_string oc (Uri.to_string url))
156156- pairs;
157157- output_char oc '\n')
158158- scan_cache)
159159- with _ -> ()
160160-161161-(** Get cached scan results for a path, or None if not cached *)
162162-let get_cached_scan path =
163163- if Hashtbl.length scan_cache = 0 then load_scan_cache ();
164164- Hashtbl.find_opt scan_cache (Fpath.to_string path)
165165-166166-(** Store scan results in cache *)
167167-let cache_scan path results =
168168- Hashtbl.replace scan_cache (Fpath.to_string path) results;
169169- save_scan_cache ()
170170-17192type repo_source = {
17293 handle : string; (** Member handle or "me" *)
17394 url : Uri.t; (** Normalized git URL *)
···405326 (* Generic git.<domain>: pattern - convert git@git.<domain>:path to https://git.<domain>/path *)
406327 match String.index_opt s ':' with
407328 | Some colon_pos ->
408408- let host = String.sub s 4 (colon_pos - 4) in
409409- (* "git.<domain>" *)
410410- let path =
411411- String.sub s (colon_pos + 1) (String.length s - colon_pos - 1)
412412- in
329329+ let host = String.sub s 4 (colon_pos - 4) in (* "git.<domain>" *)
330330+ let path = String.sub s (colon_pos + 1) (String.length s - colon_pos - 1) in
413331 "https://" ^ host ^ "/" ^ path
414332 | None -> s
415333 else s
···473391 package_names
474392 with _ -> []
475393476476-(** Fetch a verse opam repo (with caching). Returns true if actually fetched. *)
394394+(** Fetch a verse opam repo (with caching) *)
477395let fetch_verse_opam_repo ~proc ~fs ~refresh path =
478396 let cache_key = "verse-opam/" ^ Fpath.to_string path in
479397 if not (needs_fetch ~refresh ~timeout:default_cache_timeout cache_key) then begin
480398 Log.debug (fun m -> m "Skipping fetch for %a (cached)" Fpath.pp path);
481481- false (* Did not fetch *)
482482- end
483483- else begin
399399+ ()
400400+ end else begin
484401 let cwd = Eio.Path.(fs / Fpath.to_string path) in
485485- let cmd = [ "git"; "fetch"; "--quiet" ] in
486486- Log.debug (fun m ->
487487- m "Running: %s (in %a)" (String.concat " " cmd) Fpath.pp path);
402402+ let cmd = ["git"; "fetch"; "--quiet"] in
403403+ Log.debug (fun m -> m "Running: %s (in %a)" (String.concat " " cmd) Fpath.pp path);
488404 Eio.Switch.run @@ fun sw ->
489489- let child =
490490- Eio.Process.spawn proc ~sw ~cwd
405405+ let child = Eio.Process.spawn proc ~sw ~cwd
491406 ~stdout:(Eio.Flow.buffer_sink (Buffer.create 16))
492407 ~stderr:(Eio.Flow.buffer_sink (Buffer.create 16))
493408 cmd
494409 in
495410 match Eio.Process.await child with
496496- | `Exited 0 ->
497497- record_fetch cache_key;
498498- true (* Actually fetched *)
499499- | _ ->
500500- Log.debug (fun m -> m "Failed to fetch %a" Fpath.pp path);
501501- false
411411+ | `Exited 0 -> record_fetch cache_key
412412+ | _ -> Log.debug (fun m -> m "Failed to fetch %a" Fpath.pp path)
502413 end
503414504504-(** Scan all verse opam repos and build a map: repo_basename ->
505505- [(handle, url, [packages])] *)
415415+(** Scan all verse opam repos and build a map: repo_basename -> [(handle, url, [packages])] *)
506416let scan_all_verse_opam_repos ~proc ~fs ~verse_path ~refresh () =
507417 let eio_verse = Eio.Path.(fs / Fpath.to_string verse_path) in
508418 let entries = try Eio.Path.read_dir eio_verse with _ -> [] in
509419 (* Find opam repo directories (ending in -opam) *)
510510- let opam_dirs =
511511- List.filter (fun name -> String.ends_with ~suffix:"-opam" name) entries
512512- in
420420+ let opam_dirs = List.filter (fun name -> String.ends_with ~suffix:"-opam" name) entries in
421421+ (* Fetch each opam repo first (respecting cache unless refresh) *)
513422 Log.info (fun m -> m "Checking %d verse opam repos" (List.length opam_dirs));
423423+ List.iter (fun opam_dir ->
424424+ let opam_path = Fpath.(verse_path / opam_dir) in
425425+ fetch_verse_opam_repo ~proc ~fs ~refresh opam_path)
426426+ opam_dirs;
514427 (* Build map: repo_basename -> [(handle, url, [packages])] *)
515428 let repo_map = Hashtbl.create 64 in
516429 List.iter
···518431 let handle = String.sub opam_dir 0 (String.length opam_dir - 5) in
519432 (* strip -opam *)
520433 let opam_path = Fpath.(verse_path / opam_dir) in
521521- (* Fetch and decide whether to rescan *)
522522- let did_fetch = fetch_verse_opam_repo ~proc ~fs ~refresh opam_path in
523523- (* Use cached scan results unless we fetched or have no cache *)
524524- let pkg_urls =
525525- match (did_fetch, get_cached_scan opam_path) with
526526- | false, Some cached ->
527527- Log.debug (fun m -> m "Using cached scan for %a" Fpath.pp opam_path);
528528- cached
529529- | _ ->
530530- (* Need to scan: either we fetched or no cache exists *)
531531- Log.debug (fun m -> m "Scanning %a" Fpath.pp opam_path);
532532- let results = scan_verse_opam_repo ~fs opam_path in
533533- cache_scan opam_path results;
534534- results
535535- in
434434+ let pkg_urls = scan_verse_opam_repo ~fs opam_path in
536435 (* Group by repo basename *)
537436 let by_repo = Hashtbl.create 16 in
538437 List.iter
···611510612511(** Fetch a remote (with caching) *)
613512let fetch_remote ~proc ~fs ~repo ~remote ~refresh () =
614614- let cache_key =
615615- Printf.sprintf "checkout/%s/%s" (Fpath.to_string repo) remote
616616- in
513513+ let cache_key = Printf.sprintf "checkout/%s/%s" (Fpath.to_string repo) remote in
617514 if not (needs_fetch ~refresh ~timeout:default_cache_timeout cache_key) then begin
618618- Log.debug (fun m ->
619619- m "Skipping fetch for %s in %a (cached)" remote Fpath.pp repo);
620620- Ok () (* Return Ok since we have cached data *)
621621- end
622622- else begin
515515+ Log.debug (fun m -> m "Skipping fetch for %s in %a (cached)" remote Fpath.pp repo);
516516+ Ok () (* Return Ok since we have cached data *)
517517+ end else begin
623518 let cwd = Eio.Path.(fs / Fpath.to_string repo) in
624624- let cmd = [ "git"; "fetch"; remote ] in
519519+ let cmd = ["git"; "fetch"; remote] in
625520 Log.info (fun m -> m "Fetching %s in %a" remote Fpath.pp repo);
626626- Log.debug (fun m ->
627627- m "Running: %s (in %a)" (String.concat " " cmd) Fpath.pp repo);
521521+ Log.debug (fun m -> m "Running: %s (in %a)" (String.concat " " cmd) Fpath.pp repo);
628522 Eio.Switch.run @@ fun sw ->
629629- let child =
630630- Eio.Process.spawn proc ~sw ~cwd
523523+ let child = Eio.Process.spawn proc ~sw ~cwd
631524 ~stdout:(Eio.Flow.buffer_sink (Buffer.create 256))
632525 ~stderr:(Eio.Flow.buffer_sink (Buffer.create 256))
633526 cmd
634527 in
635528 match Eio.Process.await child with
636636- | `Exited 0 ->
637637- record_fetch cache_key;
638638- Ok ()
529529+ | `Exited 0 -> record_fetch cache_key; Ok ()
639530 | _ -> Error "Failed to fetch remote"
640531 end
641532···732623 Diverged { common_ancestor = base; my_ahead; their_ahead }))
733624734625(** Compute fork analysis for all repos *)
735735-let compute ~proc ~fs ~verse_config ~monopam_config ?(refresh = false) () =
626626+let compute ~proc ~fs ~verse_config ~monopam_config ?(refresh=false) () =
736627 let verse_path = Verse_config.verse_path verse_config in
737628 let opam_repo_path = Config.Paths.opam_repo monopam_config in
738629 let checkouts_path = Config.Paths.checkouts monopam_config in
···743634744635 (* Scan verse opam repos *)
745636 Log.info (fun m -> m "Scanning verse opam repos");
746746- let verse_repos =
747747- scan_all_verse_opam_repos ~proc ~fs ~verse_path ~refresh ()
748748- in
637637+ let verse_repos = scan_all_verse_opam_repos ~proc ~fs ~verse_path ~refresh () in
749638750639 (* Build combined list of all repo names *)
751640 let all_repos = Hashtbl.create 64 in
···771660 else begin
772661 (* Check if we have a local checkout *)
773662 let checkout_path = Fpath.(checkouts_path / repo_name) in
774774- let have_checkout = Git_cli.is_repo ~proc ~fs checkout_path in
663663+ let have_checkout = Git.is_repo ~proc ~fs checkout_path in
775664776665 (* Process each verse source *)
777666 let verse_with_rel =
···798687 ~name:remote_name ~url:src.url ())
799688 end;
800689 (* Fetch remote (respecting cache unless refresh) *)
801801- match
802802- fetch_remote ~proc ~fs ~repo:checkout_path
803803- ~remote:remote_name ~refresh ()
804804- with
690690+ match fetch_remote ~proc ~fs ~repo:checkout_path ~remote:remote_name ~refresh () with
805691 | Error _ -> Not_fetched
806692 | Ok () ->
807693 (* Compare refs *)
+8-6
lib/forks.mli
···7676 ?refresh:bool ->
7777 unit ->
7878 t
7979-(** [compute ~proc ~fs ~verse_config ~monopam_config ?refresh ()] performs full
8080- fork analysis by: 1. Scanning my opam repo for dev-repo URLs 2. Scanning all
8181- verse opam repos for dev-repo URLs 3. Adding git remotes to my checkouts for
8282- each member's fork 4. Fetching remotes and comparing commit histories
7979+(** [compute ~proc ~fs ~verse_config ~monopam_config ?refresh ()] performs full fork
8080+ analysis by:
8181+ 1. Scanning my opam repo for dev-repo URLs
8282+ 2. Scanning all verse opam repos for dev-repo URLs
8383+ 3. Adding git remotes to my checkouts for each member's fork
8484+ 4. Fetching remotes and comparing commit histories
83858484- Fetches are cached for 1 hour by default. Use [~refresh:true] to force fresh
8585- fetches from all remotes. *)
8686+ Fetches are cached for 1 hour by default. Use [~refresh:true] to force
8787+ fresh fetches from all remotes. *)
+80-110
lib/git_cli.ml
lib/git.ml
···6868let retryable_error_patterns =
6969 [
7070 (* HTTP 5xx errors *)
7171- "500";
7272- "502";
7373- "503";
7474- "504";
7575- "HTTP 5";
7676- "http 5";
7777- "Internal Server Error";
7878- "Bad Gateway";
7979- "Service Unavailable";
8080- "Gateway Timeout";
7171+ "500"; "502"; "503"; "504"; "HTTP 5"; "http 5";
7272+ "Internal Server Error"; "Bad Gateway"; "Service Unavailable"; "Gateway Timeout";
8173 (* RPC failures (common git smart HTTP errors) *)
8282- "RPC failed";
8383- "curl";
8484- "unexpected disconnect";
8585- "the remote end hung up";
8686- "early EOF";
7474+ "RPC failed"; "curl"; "unexpected disconnect";
7575+ "the remote end hung up"; "early EOF";
8776 (* Connection errors *)
8888- "Connection refused";
8989- "Connection reset";
9090- "Connection timed out";
9191- "Could not resolve host";
9292- "Failed to connect";
9393- "Network is unreachable";
9494- "Temporary failure";
7777+ "Connection refused"; "Connection reset"; "Connection timed out";
7878+ "Could not resolve host"; "Failed to connect";
7979+ "Network is unreachable"; "Temporary failure";
9580 ]
96819782(** Check if an error is a retryable HTTP server error (5xx) or network error *)
9883let is_retryable_error result =
9984 let stderr = result.stderr in
10085 String.length stderr > 0
101101- && List.exists
102102- (fun needle -> string_contains ~needle stderr)
103103- retryable_error_patterns
8686+ && List.exists (fun needle -> string_contains ~needle stderr) retryable_error_patterns
10487105105-(** Run a git command with retry logic for network errors. Retries up to
106106- [max_retries] times with exponential backoff starting at [initial_delay_ms].
107107-*)
108108-let run_git_ok_with_retry ~proc ~cwd ?(max_retries = 3)
109109- ?(initial_delay_ms = 2000) args =
8888+(** Run a git command with retry logic for network errors.
8989+ Retries up to [max_retries] times with exponential backoff starting at [initial_delay_ms]. *)
9090+let run_git_ok_with_retry ~proc ~cwd ?(max_retries = 3) ?(initial_delay_ms = 2000) args =
11091 let rec attempt n delay_ms =
11192 let result = run_git ~proc ~cwd args in
11293 if result.exit_code = 0 then Ok result.stdout
11394 else if n < max_retries && is_retryable_error result then begin
11495 (* Log the retry *)
11596 Logs.warn (fun m ->
116116- m
117117- "Git command failed with retryable error, retrying in %dms \
118118- (%d/%d): %s"
9797+ m "Git command failed with retryable error, retrying in %dms (%d/%d): %s"
11998 delay_ms (n + 1) max_retries result.stderr);
12099 (* Sleep before retry - convert ms to seconds for Unix.sleepf *)
121100 Unix.sleepf (float_of_int delay_ms /. 1000.0);
···160139 let cwd = Eio.Path.(fs / Fpath.to_string parent) in
161140 let target_name = Fpath.basename target in
162141 let url_str = Uri.to_string url in
163163- run_git_ok_with_retry ~proc ~cwd
164164- [ "clone"; "--branch"; branch; url_str; target_name ]
142142+ run_git_ok_with_retry ~proc ~cwd [ "clone"; "--branch"; branch; url_str; target_name ]
165143 |> Result.map ignore
166144167145let fetch ~proc ~fs ?(remote = "origin") path =
···224202 Ok { ahead = int_of_string ahead; behind = int_of_string behind }
225203 | _ -> Ok { ahead = 0; behind = 0 })
226204227227-(* Fetch from URL and return the commit hash for the branch *)
228228-let fetch_url ~proc ~fs ~repo ~url ~branch () =
229229- let cwd = path_to_eio ~fs repo in
230230- let url_str = Uri.to_string url in
231231- (* Fetch into FETCH_HEAD *)
232232- match run_git_ok_with_retry ~proc ~cwd [ "fetch"; url_str; branch ] with
233233- | Error e -> Error e
234234- | Ok _ -> (
235235- (* Get the fetched commit hash *)
236236- match run_git_ok ~proc ~cwd [ "rev-parse"; "FETCH_HEAD" ] with
237237- | Error e -> Error e
238238- | Ok hash -> Ok (String.trim hash))
205205+module Subtree = struct
206206+ let exists ~fs ~repo ~prefix =
207207+ let path = Eio.Path.(fs / Fpath.to_string repo / prefix) in
208208+ match Eio.Path.kind ~follow:true path with
209209+ | `Directory -> true
210210+ | _ -> false
211211+ | exception _ -> false
239212240240-let push_refspec ~proc ~fs ~repo ~url ~refspec () =
241241- let cwd = path_to_eio ~fs repo in
242242- let url_str = Uri.to_string url in
243243- run_git_ok_with_retry ~proc ~cwd [ "push"; url_str; refspec ]
244244- |> Result.map ignore
213213+ let add ~proc ~fs ~repo ~prefix ~url ~branch () =
214214+ if exists ~fs ~repo ~prefix then Error (Subtree_prefix_exists prefix)
215215+ else
216216+ let cwd = path_to_eio ~fs repo in
217217+ let url_str = Uri.to_string url in
218218+ run_git_ok_with_retry ~proc ~cwd
219219+ [ "subtree"; "add"; "--prefix"; prefix; url_str; branch; "--squash" ]
220220+ |> Result.map ignore
245221246246-let subtree_prefix_exists ~fs ~repo ~prefix =
247247- let path = Eio.Path.(fs / Fpath.to_string repo / prefix) in
248248- match Eio.Path.kind ~follow:true path with
249249- | `Directory -> true
250250- | _ -> false
251251- | exception _ -> false
222222+ let pull ~proc ~fs ~repo ~prefix ~url ~branch () =
223223+ if not (exists ~fs ~repo ~prefix) then Error (Subtree_prefix_missing prefix)
224224+ else
225225+ let cwd = path_to_eio ~fs repo in
226226+ let url_str = Uri.to_string url in
227227+ run_git_ok_with_retry ~proc ~cwd
228228+ [ "subtree"; "pull"; "--prefix"; prefix; url_str; branch; "--squash" ]
229229+ |> Result.map ignore
230230+231231+ let push ~proc ~fs ~repo ~prefix ~url ~branch () =
232232+ if not (exists ~fs ~repo ~prefix) then Error (Subtree_prefix_missing prefix)
233233+ else
234234+ let cwd = path_to_eio ~fs repo in
235235+ let url_str = Uri.to_string url in
236236+ run_git_ok_with_retry ~proc ~cwd
237237+ [ "subtree"; "push"; "--prefix"; prefix; url_str; branch ]
238238+ |> Result.map ignore
239239+240240+ let split ~proc ~fs ~repo ~prefix () =
241241+ if not (exists ~fs ~repo ~prefix) then Error (Subtree_prefix_missing prefix)
242242+ else
243243+ let cwd = path_to_eio ~fs repo in
244244+ run_git_ok ~proc ~cwd [ "subtree"; "split"; "--prefix"; prefix ]
245245+end
252246253247let init ~proc ~fs path =
254248 let cwd = path_to_eio ~fs (Fpath.parent path) in
···267261 | Some b -> b
268262 | None -> Option.value ~default:"main" (current_branch ~proc ~fs path)
269263 in
270270- run_git_ok_with_retry ~proc ~cwd [ "push"; remote; branch ]
271271- |> Result.map ignore
264264+ run_git_ok_with_retry ~proc ~cwd [ "push"; remote; branch ] |> Result.map ignore
272265273266let push_ref ~proc ~fs ~repo ~target ~ref_spec () =
274267 let cwd = path_to_eio ~fs repo in
···390383 let cwd = path_to_eio ~fs repo_path in
391384 run_git_ok ~proc ~cwd [ "show"; "--patch"; "--stat"; commit ]
392385393393-(** Parse a subtree merge/squash commit message to extract the upstream commit
394394- range. Messages look like: "Squashed 'prefix/' changes from abc123..def456"
395395- or "Squashed 'prefix/' content from commit abc123" Returns the end commit
396396- (most recent) if found. *)
386386+(** Parse a subtree merge/squash commit message to extract the upstream commit range.
387387+ Messages look like: "Squashed 'prefix/' changes from abc123..def456"
388388+ or "Squashed 'prefix/' content from commit abc123"
389389+ Returns the end commit (most recent) if found. *)
397390let parse_subtree_message subject =
398391 (* Helper to extract hex commit hash starting at position *)
399392 let extract_hex s start =
···478471(** {1 Worktree Operations} *)
479472480473module Worktree = struct
481481- type entry = { path : Fpath.t; head : string; branch : string option }
474474+ type entry = {
475475+ path : Fpath.t;
476476+ head : string;
477477+ branch : string option;
478478+ }
482479483480 let add ~proc ~fs ~repo ~path ~branch () =
484481 let cwd = path_to_eio ~fs repo in
485482 let path_str = Fpath.to_string path in
486486- run_git_ok ~proc ~cwd [ "worktree"; "add"; "-b"; branch; path_str ]
483483+ run_git_ok ~proc ~cwd
484484+ [ "worktree"; "add"; "-b"; branch; path_str ]
487485 |> Result.map ignore
488486489487 let remove ~proc ~fs ~repo ~path ~force () =
···508506 HEAD abc123...
509507 branch refs/heads/branchname (or detached) *)
510508 let lines = String.split_on_char '\n' output in
511511- let rec parse_entries acc current_path current_head current_branch =
512512- function
513513- | [] -> (
509509+ let rec parse_entries acc current_path current_head current_branch = function
510510+ | [] ->
514511 (* Finalize last entry if we have one *)
515515- match (current_path, current_head) with
512512+ (match current_path, current_head with
516513 | Some p, Some h ->
517517- let entry =
518518- { path = p; head = h; branch = current_branch }
519519- in
514514+ let entry = { path = p; head = h; branch = current_branch } in
520515 List.rev (entry :: acc)
521516 | _ -> List.rev acc)
522522- | "" :: rest -> (
517517+ | "" :: rest ->
523518 (* End of entry block *)
524524- match (current_path, current_head) with
519519+ (match current_path, current_head with
525520 | Some p, Some h ->
526526- let entry =
527527- { path = p; head = h; branch = current_branch }
528528- in
521521+ let entry = { path = p; head = h; branch = current_branch } in
529522 parse_entries (entry :: acc) None None None rest
530523 | _ -> parse_entries acc None None None rest)
531524 | line :: rest ->
532525 if String.starts_with ~prefix:"worktree " line then
533526 let path_str = String.sub line 9 (String.length line - 9) in
534534- match Fpath.of_string path_str with
535535- | Ok p ->
536536- parse_entries acc (Some p) current_head current_branch
537537- rest
538538- | Error _ ->
539539- parse_entries acc current_path current_head current_branch
540540- rest
527527+ (match Fpath.of_string path_str with
528528+ | Ok p -> parse_entries acc (Some p) current_head current_branch rest
529529+ | Error _ -> parse_entries acc current_path current_head current_branch rest)
541530 else if String.starts_with ~prefix:"HEAD " line then
542531 let head = String.sub line 5 (String.length line - 5) in
543532 parse_entries acc current_path (Some head) current_branch rest
···546535 (* Extract branch name from refs/heads/... *)
547536 let branch =
548537 if String.starts_with ~prefix:"refs/heads/" branch_ref then
549549- Some
550550- (String.sub branch_ref 11
551551- (String.length branch_ref - 11))
552552- else Some branch_ref
538538+ Some (String.sub branch_ref 11 (String.length branch_ref - 11))
539539+ else
540540+ Some branch_ref
553541 in
554542 parse_entries acc current_path current_head branch rest
555543 else if line = "detached" then
556544 parse_entries acc current_path current_head None rest
557545 else
558558- parse_entries acc current_path current_head current_branch
559559- rest
546546+ parse_entries acc current_path current_head current_branch rest
560547 in
561548 parse_entries [] None None None lines
562549···569556 let cwd = path_to_eio ~fs path in
570557 run_git_ok ~proc ~cwd [ "cherry-pick"; commit ] |> Result.map ignore
571558572572-let merge ~proc ~fs ~ref_name ?(ff_only = false) path =
559559+let merge ~proc ~fs ~ref_name ?(ff_only=false) path =
573560 let cwd = path_to_eio ~fs path in
574574- let args =
575575- [ "merge" ] @ (if ff_only then [ "--ff-only" ] else []) @ [ ref_name ]
576576- in
561561+ let args = ["merge"] @ (if ff_only then ["--ff-only"] else []) @ [ref_name] in
577562 run_git_ok ~proc ~cwd args |> Result.map ignore
578563579564(** {1 Diff Operations} *)
···667652let branch_rename ~proc ~fs ~new_name path =
668653 let cwd = path_to_eio ~fs path in
669654 run_git_ok ~proc ~cwd [ "branch"; "-M"; new_name ] |> Result.map ignore
670670-671671-let ls_remote_head ~proc ~fs ?(remote = "origin") ?(branch = "main") path =
672672- let cwd = path_to_eio ~fs path in
673673- match
674674- run_git_ok ~proc ~cwd
675675- [ "ls-remote"; "--heads"; remote; Printf.sprintf "refs/heads/%s" branch ]
676676- with
677677- | Error _ -> None
678678- | Ok output -> (
679679- if String.trim output = "" then None
680680- else
681681- (* Output format: "hash\trefs/heads/branch" *)
682682- match String.split_on_char '\t' (String.trim output) with
683683- | hash :: _ -> Some hash
684684- | [] -> None)
+85-67
lib/git_cli.mli
lib/git.mli
···128128 branch:string ->
129129 Fpath.t ->
130130 (unit, error) result
131131-(** [fetch_and_reset ~proc ~fs ?remote ~branch path] fetches from the remote and
132132- resets the local branch to match the remote.
131131+(** [fetch_and_reset ~proc ~fs ?remote ~branch path] fetches from the remote
132132+ and resets the local branch to match the remote.
133133134134 This is useful for repositories that should not have local changes, as it
135135 discards any local modifications and sets the working tree to exactly match
···167167 @param remote Remote name (default: "origin")
168168 @param branch Branch to compare (default: current branch) *)
169169170170-(** {1 Subtree Helper Operations} *)
170170+(** {1 Subtree Operations} *)
171171172172-val fetch_url :
173173- proc:_ Eio.Process.mgr ->
174174- fs:Eio.Fs.dir_ty Eio.Path.t ->
175175- repo:Fpath.t ->
176176- url:Uri.t ->
177177- branch:string ->
178178- unit ->
179179- (string, error) result
180180-(** [fetch_url ~proc ~fs ~repo ~url ~branch ()] fetches a branch from a URL and
181181- returns the commit hash of FETCH_HEAD.
172172+(** Operations for git subtree management in the monorepo. *)
173173+module Subtree : sig
174174+ val add :
175175+ proc:_ Eio.Process.mgr ->
176176+ fs:Eio.Fs.dir_ty Eio.Path.t ->
177177+ repo:Fpath.t ->
178178+ prefix:string ->
179179+ url:Uri.t ->
180180+ branch:string ->
181181+ unit ->
182182+ (unit, error) result
183183+ (** [add ~proc ~fs ~repo ~prefix ~url ~branch ()] adds a new subtree to the
184184+ repository.
182185183183- @param repo Path to the local repository
184184- @param url Git remote URL to fetch from
185185- @param branch Branch to fetch *)
186186+ @param repo Path to the monorepo
187187+ @param prefix Subdirectory for the subtree
188188+ @param url Git remote URL for the subtree source
189189+ @param branch Branch to add *)
186190187187-val push_refspec :
188188- proc:_ Eio.Process.mgr ->
189189- fs:Eio.Fs.dir_ty Eio.Path.t ->
190190- repo:Fpath.t ->
191191- url:Uri.t ->
192192- refspec:string ->
193193- unit ->
194194- (unit, error) result
195195-(** [push_refspec ~proc ~fs ~repo ~url ~refspec ()] pushes a refspec to a URL.
191191+ val pull :
192192+ proc:_ Eio.Process.mgr ->
193193+ fs:Eio.Fs.dir_ty Eio.Path.t ->
194194+ repo:Fpath.t ->
195195+ prefix:string ->
196196+ url:Uri.t ->
197197+ branch:string ->
198198+ unit ->
199199+ (unit, error) result
200200+ (** [pull ~proc ~fs ~repo ~prefix ~url ~branch ()] pulls updates from the
201201+ remote into the subtree.
196202197197- @param repo Path to the local repository
198198- @param url Git remote URL to push to
199199- @param refspec Git refspec (e.g. "hash:refs/heads/branch") *)
203203+ @param repo Path to the monorepo
204204+ @param prefix Subdirectory of the subtree
205205+ @param url Git remote URL
206206+ @param branch Branch to pull *)
200207201201-val subtree_prefix_exists :
202202- fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> bool
203203-(** [subtree_prefix_exists ~fs ~repo ~prefix] returns true if the subtree prefix
204204- directory exists in the repository. *)
208208+ val push :
209209+ proc:_ Eio.Process.mgr ->
210210+ fs:Eio.Fs.dir_ty Eio.Path.t ->
211211+ repo:Fpath.t ->
212212+ prefix:string ->
213213+ url:Uri.t ->
214214+ branch:string ->
215215+ unit ->
216216+ (unit, error) result
217217+ (** [push ~proc ~fs ~repo ~prefix ~url ~branch ()] pushes subtree changes to
218218+ the remote.
219219+220220+ This extracts commits that affected the subtree and pushes them to the
221221+ specified remote/branch.
222222+223223+ @param repo Path to the monorepo
224224+ @param prefix Subdirectory of the subtree
225225+ @param url Git remote URL
226226+ @param branch Branch to push to *)
227227+228228+ val split :
229229+ proc:_ Eio.Process.mgr ->
230230+ fs:Eio.Fs.dir_ty Eio.Path.t ->
231231+ repo:Fpath.t ->
232232+ prefix:string ->
233233+ unit ->
234234+ (string, error) result
235235+ (** [split ~proc ~fs ~repo ~prefix ()] extracts commits for a subtree into a
236236+ standalone branch.
237237+238238+ Returns the commit hash of the split branch head. *)
239239+240240+ val exists :
241241+ fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> bool
242242+ (** [exists ~fs ~repo ~prefix] returns true if the subtree prefix directory
243243+ exists in the repository. *)
244244+end
205245206246(** {1 Initialization} *)
207247···450490451491(** Operations for git worktree management. *)
452492module Worktree : sig
493493+ (** A git worktree entry. *)
453494 type entry = {
454495 path : Fpath.t; (** Absolute path to the worktree *)
455496 head : string; (** HEAD commit hash *)
456497 branch : string option; (** Branch name if not detached *)
457498 }
458458- (** A git worktree entry. *)
459499460500 val add :
461501 proc:_ Eio.Process.mgr ->
···499539 repo:Fpath.t ->
500540 path:Fpath.t ->
501541 bool
502502- (** [exists ~proc ~fs ~repo ~path] returns true if a worktree exists at
503503- [path]. *)
542542+ (** [exists ~proc ~fs ~repo ~path] returns true if a worktree exists at [path]. *)
504543end
505544506545(** {1 Cherry-pick Operations} *)
···511550 commit:string ->
512551 Fpath.t ->
513552 (unit, error) result
514514-(** [cherry_pick ~proc ~fs ~commit path] applies a single commit to the current
515515- branch.
553553+(** [cherry_pick ~proc ~fs ~commit path] applies a single commit to the current branch.
516554517555 @param commit The commit hash to cherry-pick
518556 @param path Path to the repository *)
···524562 ?ff_only:bool ->
525563 Fpath.t ->
526564 (unit, error) result
527527-(** [merge ~proc ~fs ~ref_name ?ff_only path] merges a ref into the current
528528- branch.
565565+(** [merge ~proc ~fs ~ref_name ?ff_only path] merges a ref into the current branch.
529566530567 @param ref_name The ref to merge (e.g., "verse/handle/main")
531568 @param ff_only If true, only allow fast-forward merges (default: false)
···573610 message:string ->
574611 Fpath.t ->
575612 (unit, error) result
576576-(** [commit ~proc ~fs ~message path] creates a commit with the given message in
577577- the repository at [path]. *)
613613+(** [commit ~proc ~fs ~message path] creates a commit with the given message
614614+ in the repository at [path]. *)
578615579616val rm :
580617 proc:_ Eio.Process.mgr ->
···583620 Fpath.t ->
584621 string ->
585622 (unit, error) result
586586-(** [rm ~proc ~fs ~recursive path target] removes [target] from the git index in
587587- the repository at [path]. If [recursive] is true, removes directories
623623+(** [rm ~proc ~fs ~recursive path target] removes [target] from the git index
624624+ in the repository at [path]. If [recursive] is true, removes directories
588625 recursively (git rm -r). *)
589626590627val config :
···604641 prefix:string ->
605642 unit ->
606643 bool
607607-(** [has_subtree_history ~proc ~fs ~repo ~prefix ()] returns true if the prefix
608608- has subtree commit history (i.e., was added via git subtree add). Returns
609609- false for fresh local packages that were never part of a subtree. *)
644644+(** [has_subtree_history ~proc ~fs ~repo ~prefix ()] returns true if the
645645+ prefix has subtree commit history (i.e., was added via git subtree add).
646646+ Returns false for fresh local packages that were never part of a subtree. *)
610647611648val branch_rename :
612649 proc:_ Eio.Process.mgr ->
···614651 new_name:string ->
615652 Fpath.t ->
616653 (unit, error) result
617617-(** [branch_rename ~proc ~fs ~new_name path] renames the current branch to
618618- [new_name] in the repository at [path]. Uses [git branch -M]. *)
619619-620620-(** {1 Remote Queries} *)
621621-622622-val ls_remote_head :
623623- proc:_ Eio.Process.mgr ->
624624- fs:Eio.Fs.dir_ty Eio.Path.t ->
625625- ?remote:string ->
626626- ?branch:string ->
627627- Fpath.t ->
628628- string option
629629-(** [ls_remote_head ~proc ~fs ?remote ?branch path] queries the remote for the
630630- HEAD ref of a branch without fetching any objects.
631631-632632- This is much faster than [fetch] and can be used to check if there are any
633633- new commits to fetch. Returns [None] if the branch doesn't exist or the
634634- remote is unreachable.
635635-636636- @param remote Remote name (default: "origin")
637637- @param branch Branch name (default: "main") *)
654654+(** [branch_rename ~proc ~fs ~new_name path] renames the current branch
655655+ to [new_name] in the repository at [path]. Uses [git branch -M]. *)
+687-1009
lib/monopam.ml
···11module Config = Config
22module Package = Package
33module Opam_repo = Opam_repo
44-module Git_cli = Git_cli
44+module Git = Git
55module Status = Status
66module Changes = Changes
77module Verse = Verse
···1616module Sources_registry = Sources_registry
1717module Fork_join = Fork_join
1818module Site = Site
1919-module Remote_cache = Remote_cache
20192120let src = Logs.Src.create "monopam" ~doc:"Monopam operations"
22212322module Log = (val Logs.src_log src : Logs.LOG)
24232525-(* Timing helper for benchmarking phases *)
2626-let time_phase name f =
2727- let t0 = Unix.gettimeofday () in
2828- let result = f () in
2929- let t1 = Unix.gettimeofday () in
3030- Log.debug (fun m -> m "[TIMING] %s: %.3fs" name (t1 -. t0));
3131- result
3232-3324type error =
3425 | Config_error of string
3526 | Repo_error of Opam_repo.error
3636- | Git_error of Git_cli.error
2727+ | Git_error of Git.error
3728 | Dirty_state of Package.t list
3829 | Monorepo_dirty
3930 | Package_not_found of string
···4233let pp_error ppf = function
4334 | Config_error msg -> Fmt.pf ppf "Configuration error: %s" msg
4435 | Repo_error e -> Fmt.pf ppf "Repository error: %a" Opam_repo.pp_error e
4545- | Git_error e -> Fmt.pf ppf "Git error: %a" Git_cli.pp_error e
3636+ | Git_error e -> Fmt.pf ppf "Git error: %a" Git.pp_error e
4637 | Dirty_state pkgs ->
4738 Fmt.pf ppf "Dirty packages: %a"
4839 Fmt.(list ~sep:comma (using Package.name string))
···5546*)
5647let error_hint = function
5748 | Config_error _ ->
5858- Some "Run 'monopam init --handle <your-handle>' to create a workspace."
4949+ Some
5050+ "Run 'monopam init --handle <your-handle>' to create a workspace."
5951 | Repo_error (Opam_repo.No_dev_repo _) ->
6052 Some
6153 "Add a 'dev-repo' field to the package's opam file pointing to a git \
···6355 | Repo_error (Opam_repo.Not_git_remote _) ->
6456 Some "The dev-repo must be a git URL (git+https:// or git://)."
6557 | Repo_error _ -> None
6666- | Git_error (Git_cli.Dirty_worktree _) ->
5858+ | Git_error (Git.Dirty_worktree _) ->
6759 Some "Commit or stash your changes first: cd <repo> && git status"
6868- | Git_error (Git_cli.Not_a_repo _) ->
6060+ | Git_error (Git.Not_a_repo _) ->
6961 Some "Run 'monopam sync' to clone missing repositories."
7070- | Git_error (Git_cli.Subtree_prefix_missing _) ->
6262+ | Git_error (Git.Subtree_prefix_missing _) ->
7163 Some "Run 'monopam sync' to set up the subtree."
7272- | Git_error (Git_cli.Remote_not_found _) ->
6464+ | Git_error (Git.Remote_not_found _) ->
7365 Some "Check that the remote is configured: git remote -v"
7474- | Git_error (Git_cli.Branch_not_found _) ->
6666+ | Git_error (Git.Branch_not_found _) ->
7567 Some "Check available branches: git branch -a"
7676- | Git_error (Git_cli.Command_failed (cmd, _))
6868+ | Git_error (Git.Command_failed (cmd, _))
7769 when String.starts_with ~prefix:"git push" cmd ->
7870 Some "Check your network connection and git credentials."
7979- | Git_error (Git_cli.Command_failed (cmd, _))
7171+ | Git_error (Git.Command_failed (cmd, _))
8072 when String.starts_with ~prefix:"git subtree" cmd ->
8173 Some "Run 'monopam status' to check repository state."
8274 | Git_error _ -> None
···8577 "Commit changes in the monorepo first: cd mono && git add -A && git \
8678 commit"
8779 | Monorepo_dirty ->
8888- Some
8989- "Commit or stash your changes first: git status && git add -A && git \
9090- commit"
8080+ Some "Commit or stash your changes first: git status && git add -A && git commit"
9181 | Package_not_found _ ->
9282 Some "Check available packages: ls opam-repo/packages/"
9383 | Claude_error msg when String.starts_with ~prefix:"Failed to decode" msg ->
···146136 let fs = fs_typed fs in
147137 ensure_checkouts_dir ~fs ~config;
148138 discover_packages ~fs:(fs :> _ Eio.Path.t) ~config ()
149149- |> Result.map (Status.compute_all ~fs ~config)
139139+ |> Result.map (Status.compute_all ~proc ~fs ~config)
150140151141(** Find opam files in monorepo subtrees that aren't registered in the overlay.
152142 Returns a list of (subtree_name, unregistered_package_name) pairs. *)
···197187 with Eio.Io _ -> [])
198188 repos
199189190190+(** Information about a package discovered from the monorepo. *)
200191type monorepo_package = {
201192 pkg_name : string;
202193 subtree : string;
···204195 url_src : string;
205196 opam_content : string;
206197}
207207-(** Information about a package discovered from the monorepo. *)
208198209209-(** Discover packages from monorepo subtrees by parsing dune-project files. If
210210- [sources] is provided, it overrides the dev-repo URL for matching subtrees.
211211-*)
212212-let discover_packages_from_monorepo ~fs ~config
213213- ?(sources = Sources_registry.empty) () =
199199+(** Discover packages from monorepo subtrees by parsing dune-project files.
200200+ If [sources] is provided, it overrides the dev-repo URL for matching subtrees. *)
201201+let discover_packages_from_monorepo ~fs ~config ?(sources = Sources_registry.empty) () =
214202 let fs = fs_typed fs in
215203 let monorepo = Config.Paths.monorepo config in
216204 let monorepo_eio = Eio.Path.(fs / Fpath.to_string monorepo) in
···227215 with Eio.Io _ -> []
228216 in
229217230230- Log.debug (fun m ->
231231- m "Found %d subdirectories in monorepo" (List.length subdirs));
218218+ Log.debug (fun m -> m "Found %d subdirectories in monorepo" (List.length subdirs));
232219233220 (* Process each subdirectory *)
234221 let packages, errors =
···242229 | `Regular_file -> (
243230 (* Parse dune-project *)
244231 let content =
245245- try Some (Eio.Path.load dune_project_path) with Eio.Io _ -> None
232232+ try Some (Eio.Path.load dune_project_path)
233233+ with Eio.Io _ -> None
246234 in
247235 match content with
248236 | None -> (pkgs, errs)
···266254 1. Explicit sources.toml entry for this subtree
267255 2. dune-project source/homepage
268256 3. sources.toml default_url_base + subtree name *)
269269- let sources_override =
270270- Sources_registry.find sources ~subtree
271271- in
257257+ let sources_override = Sources_registry.find sources ~subtree in
272258273259 let derive_from_dune () =
274260 match
···284270 match Sources_registry.derive_url sources ~subtree with
285271 | Some dev_repo ->
286272 Log.debug (fun m ->
287287- m "Using default_url_base for %s: %s" subtree
288288- dev_repo);
273273+ m "Using default_url_base for %s: %s" subtree dev_repo);
289274 Some (dev_repo, dev_repo ^ "#main")
290275 | None -> None
291276 in
···301286 | None -> (
302287 (* Try to get branch from dune-project, default to main *)
303288 match dune_proj.source with
304304- | Some (Dune_project.Uri { branch = Some b; _ })
305305- ->
306306- b
289289+ | Some (Dune_project.Uri { branch = Some b; _ }) -> b
307290 | _ -> "main")
308291 in
309292 Log.debug (fun m ->
310310- m "Using sources.toml entry for %s: %s" subtree
311311- dev_repo);
293293+ m "Using sources.toml entry for %s: %s" subtree dev_repo);
312294 Some (dev_repo, dev_repo ^ "#" ^ branch)
313295 | None -> (
314296 match derive_from_dune () with
···318300 | Some result -> Some result
319301 | None ->
320302 Log.warn (fun m ->
321321- m
322322- "Cannot derive dev-repo for %s (no \
323323- source in dune-project or \
324324- sources.toml)"
325325- subtree);
303303+ m "Cannot derive dev-repo for %s (no source in dune-project or sources.toml)" subtree);
326304 None))
327305 in
328306 match dev_repo_and_url with
329307 | None -> (pkgs, "Cannot derive dev-repo" :: errs)
330308 | Some (dev_repo, url_src) ->
331309 Log.debug (fun m ->
332332- m "Found %d opam files in %s"
333333- (List.length opam_files) subtree);
310310+ m "Found %d opam files in %s" (List.length opam_files)
311311+ subtree);
334312 (* Transform each opam file *)
335313 let new_pkgs =
336314 List.filter_map
···348326 ~dev_repo ~url_src
349327 in
350328 Some
351351- {
352352- pkg_name;
353353- subtree;
354354- dev_repo;
355355- url_src;
356356- opam_content;
357357- }
329329+ { pkg_name; subtree; dev_repo; url_src; opam_content }
358330 with Eio.Io _ -> None)
359331 opam_files
360332 in
···363335 (* No dune-project, skip *)
364336 Log.debug (fun m -> m "No dune-project in %s, skipping" subtree);
365337 (pkgs, errs)
366366- | exception Eio.Io _ -> (pkgs, errs))
338338+ | exception Eio.Io _ ->
339339+ (pkgs, errs))
367340 ([], []) subdirs
368341 in
369342···394367 Log.info (fun m ->
395368 m "Cloning %s from %a (branch: %s)" (Package.repo_name pkg) Uri.pp
396369 (Package.dev_repo pkg) branch);
397397- Git_cli.clone ~proc ~fs ~url:(Package.dev_repo pkg) ~branch checkout_dir
370370+ Git.clone ~proc ~fs ~url:(Package.dev_repo pkg) ~branch checkout_dir
398371 in
399372 let is_directory =
400373 match Eio.Path.kind ~follow:true checkout_eio with
···403376 | exception Eio.Io _ -> false
404377 in
405378 if not is_directory then do_clone ()
406406- else if not (Git_cli.is_repo ~proc ~fs checkout_dir) then do_clone ()
379379+ else if not (Git.is_repo ~proc ~fs checkout_dir) then do_clone ()
407380 else begin
408381 Log.info (fun m -> m "Fetching %s" (Package.repo_name pkg));
409409- match Git_cli.fetch ~proc ~fs checkout_dir with
382382+ match Git.fetch ~proc ~fs checkout_dir with
410383 | Error e -> Error e
411384 | Ok () ->
412385 Log.info (fun m -> m "Updating %s to %s" (Package.repo_name pkg) branch);
413413- Git_cli.merge_ff ~proc ~fs ~branch checkout_dir
386386+ Git.merge_ff ~proc ~fs ~branch checkout_dir
414387 end
415388416389(* Group packages by their repository *)
···684657 let monorepo_eio = Eio.Path.(fs / Fpath.to_string monorepo) in
685658 let init_and_commit () =
686659 Log.info (fun m -> m "Initializing monorepo at %a" Fpath.pp monorepo);
687687- match Git_cli.init ~proc ~fs monorepo with
660660+ match Git.init ~proc ~fs monorepo with
688661 | Error e -> Error (Git_error e)
689662 | Ok () -> (
690663 (* Create dune-project file so the monorepo builds *)
···711684 (* Commit *)
712685 Log.debug (fun m -> m "Creating initial commit in monorepo");
713686 match
714714- Git_cli.commit_allow_empty ~proc ~fs
687687+ Git.commit_allow_empty ~proc ~fs
715688 ~message:
716689 "Initial commit with dune-project, CLAUDE.md, and .gitignore"
717690 monorepo
···749722 | _ -> false
750723 | exception Eio.Io _ -> false
751724 in
752752- if is_directory && Git_cli.is_repo ~proc ~fs monorepo then begin
725725+ if is_directory && Git.is_repo ~proc ~fs monorepo then begin
753726 Log.debug (fun m ->
754727 m "Monorepo already initialized at %a" Fpath.pp monorepo);
755728 ensure_file ~filename:"CLAUDE.md" ~content:claude_md_content;
···832805833806(** Convert a clone URL to a push URL.
834807 - GitHub HTTPS URLs are converted to SSH format
835835- - Tangled URLs (tangled.org/tangled.sh) are converted to SSH format using
836836- the knot server
808808+ - Tangled URLs (tangled.org/tangled.sh) are converted to SSH format using the knot server
837809 - Other URLs are returned unchanged
838838-839839- @param knot
840840- Git push server hostname. Defaults to git.recoil.org if not provided. *)
810810+ @param knot Git push server hostname. Defaults to git.recoil.org if not provided. *)
841811let url_to_push_url ?knot uri =
842812 let scheme = Uri.scheme uri in
843813 let host = Uri.host uri in
···926896 (* Pull from local checkout, not remote URL - ensures push/pull use same source *)
927897 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
928898 let url = Uri.of_string (Fpath.to_string checkout_dir) in
929929- let subtree_exists =
930930- Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix
931931- in
932932- (* Fetch from checkout and get commit hash *)
933933- match Git_cli.fetch_url ~proc ~fs ~repo:monorepo ~url ~branch () with
934934- | Error e -> Error (Git_error e)
935935- | Ok hash_hex ->
936936- let repo_path = Fpath.to_string monorepo in
937937- let git_repo = Git.Repository.open_repo ~fs repo_path in
938938- let commit = Git.Hash.of_hex hash_hex in
939939- let user =
940940- Git.User.make ~name:"monopam" ~email:"monopam@localhost"
941941- ~date:(Int64.of_float (Unix.time ()))
942942- ()
943943- in
944944- if subtree_exists then begin
945945- Log.info (fun m ->
946946- m "Pulling subtree %s from %a" prefix Fpath.pp checkout_dir);
947947- let message =
948948- Fmt.str
949949- "Merge '%s/' from %s\n\n\
950950- git-subtree-dir: %s\n\
951951- git-subtree-mainline: %s\n"
952952- prefix (Uri.to_string url) prefix hash_hex
953953- in
954954- match
955955- Git.Subtree.merge git_repo ~prefix ~commit ~author:user
956956- ~committer:user ~message ()
957957- with
958958- | Ok _ -> Ok false (* not newly added *)
959959- | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
960960- end
961961- else begin
962962- Log.info (fun m ->
963963- m "Adding subtree %s from %a" prefix Fpath.pp checkout_dir);
964964- let message =
965965- Fmt.str
966966- "Add '%s/' from %s\n\n\
967967- git-subtree-dir: %s\n\
968968- git-subtree-mainline: %s\n"
969969- prefix (Uri.to_string url) prefix hash_hex
970970- in
971971- match
972972- Git.Subtree.add git_repo ~prefix ~commit ~author:user ~committer:user
973973- ~message ()
974974- with
975975- | Ok _ -> Ok true (* newly added *)
976976- | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
977977- end
899899+ if Git.Subtree.exists ~fs ~repo:monorepo ~prefix then begin
900900+ Log.info (fun m -> m "Pulling subtree %s from %a" prefix Fpath.pp checkout_dir);
901901+ match Git.Subtree.pull ~proc ~fs ~repo:monorepo ~prefix ~url ~branch () with
902902+ | Ok () -> Ok false (* not newly added *)
903903+ | Error e -> Error (Git_error e)
904904+ end
905905+ else begin
906906+ Log.info (fun m -> m "Adding subtree %s from %a" prefix Fpath.pp checkout_dir);
907907+ match Git.Subtree.add ~proc ~fs ~repo:monorepo ~prefix ~url ~branch () with
908908+ | Ok () -> Ok true (* newly added *)
909909+ | Error e -> Error (Git_error e)
910910+ end
978911979912(* Check if checkout exists and is a repo *)
980913let checkout_exists ~proc ~fs ~config pkg =
···982915 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
983916 let checkout_eio = Eio.Path.(fs / Fpath.to_string checkout_dir) in
984917 match Eio.Path.kind ~follow:true checkout_eio with
985985- | `Directory -> Git_cli.is_repo ~proc ~fs checkout_dir
918918+ | `Directory -> Git.is_repo ~proc ~fs checkout_dir
986919 | _ -> false
987920 | exception Eio.Io _ -> false
988921···991924 let checkouts_root = Config.Paths.checkouts config in
992925 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
993926 let branch = get_branch ~config pkg in
994994- match Git_cli.ahead_behind ~proc ~fs ~branch checkout_dir with
927927+ match Git.ahead_behind ~proc ~fs ~branch checkout_dir with
995928 | Ok ab -> ab.behind
996929 | Error _ -> 0
997930···999932 let fs_t = fs_typed fs in
1000933 (* Update the opam repo first - clone if needed *)
1001934 let opam_repo = Config.Paths.opam_repo config in
10021002- if Git_cli.is_repo ~proc ~fs:fs_t opam_repo then begin
935935+ if Git.is_repo ~proc ~fs:fs_t opam_repo then begin
1003936 Log.info (fun m -> m "Updating opam repo at %a" Fpath.pp opam_repo);
1004937 let result =
1005938 let ( let* ) = Result.bind in
10061006- let* () = Git_cli.fetch ~proc ~fs:fs_t opam_repo in
10071007- Git_cli.merge_ff ~proc ~fs:fs_t opam_repo
939939+ let* () = Git.fetch ~proc ~fs:fs_t opam_repo in
940940+ Git.merge_ff ~proc ~fs:fs_t opam_repo
1008941 in
1009942 match result with
1010943 | Ok () -> ()
1011944 | Error e ->
10121012- Log.warn (fun m ->
10131013- m "Failed to update opam repo: %a" Git_cli.pp_error e)
945945+ Log.warn (fun m -> m "Failed to update opam repo: %a" Git.pp_error e)
1014946 end
1015947 else begin
1016948 (* Opam repo doesn't exist - clone it if we have a URL *)
···1020952 m "Cloning opam repo from %s to %a" url Fpath.pp opam_repo);
1021953 let url = Uri.of_string url in
1022954 let branch = Config.default_branch in
10231023- match Git_cli.clone ~proc ~fs:fs_t ~url ~branch opam_repo with
955955+ match Git.clone ~proc ~fs:fs_t ~url ~branch opam_repo with
1024956 | Ok () -> Log.info (fun m -> m "Opam repo cloned successfully")
1025957 | Error e ->
10261026- Log.warn (fun m ->
10271027- m "Failed to clone opam repo: %a" Git_cli.pp_error e))
958958+ Log.warn (fun m -> m "Failed to clone opam repo: %a" Git.pp_error e)
959959+ )
1028960 | None ->
1029961 Log.info (fun m ->
1030962 m "Opam repo at %a does not exist and no URL provided" Fpath.pp
···1048980 else begin
1049981 Log.info (fun m ->
1050982 m "Checking status of %d packages" (List.length pkgs));
10511051- let statuses = Status.compute_all ~fs:fs_t ~config pkgs in
983983+ let statuses = Status.compute_all ~proc ~fs:fs_t ~config pkgs in
1052984 let dirty =
1053985 List.filter Status.has_local_changes statuses
1054986 |> List.map (fun s -> s.Status.package)
···11801112 let checkouts_root = Config.Paths.checkouts config in
11811113 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
11821114 let branch = get_branch ~config pkg in
11831183- if not (Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix) then begin
11151115+ if not (Git.Subtree.exists ~fs ~repo:monorepo ~prefix) then begin
11841116 Log.debug (fun m -> m "Subtree %s not in monorepo, skipping" prefix);
11851117 Ok ()
11861118 end
···11891121 let needs_clone =
11901122 match Eio.Path.kind ~follow:true checkout_eio with
11911123 | exception Eio.Io _ -> true
11921192- | `Directory when Git_cli.is_repo ~proc ~fs checkout_dir -> false
11241124+ | `Directory when Git.is_repo ~proc ~fs checkout_dir -> false
11931125 | _ -> true
11941126 in
11951127 let* () =
···11991131 end
12001132 else Ok ()
12011133 in
12021202- (* Use native subtree split + push to export commits to the checkout.
11341134+ (* Use git subtree push to export commits to the checkout.
12031135 This preserves commit identity, ensuring round-trips converge. *)
12041136 let checkout_url = Uri.of_string (Fpath.to_string checkout_dir) in
12051137 Log.info (fun m -> m "Subtree push %s -> %a" prefix Fpath.pp checkout_dir);
12061206- let repo_path = Fpath.to_string monorepo in
12071207- let git_repo = Git.Repository.open_repo ~fs repo_path in
12081208- match Git.Repository.read_ref git_repo "HEAD" with
12091209- | None -> Error (Git_error (Git_cli.Io_error "no HEAD ref found"))
12101210- | Some head -> (
12111211- match Git.Subtree.split git_repo ~prefix ~head () with
12121212- | Ok None -> Error (Git_error (Git_cli.Subtree_prefix_missing prefix))
12131213- | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
12141214- | Ok (Some split_hash) ->
12151215- let refspec =
12161216- Git.Hash.to_hex split_hash ^ ":refs/heads/" ^ branch
12171217- in
12181218- let* () =
12191219- Git_cli.push_refspec ~proc ~fs ~repo:monorepo ~url:checkout_url
12201220- ~refspec ()
12211221- in
12221222- Ok ())
11381138+ let* () = Git.Subtree.push ~proc ~fs ~repo:monorepo ~prefix ~url:checkout_url ~branch () in
11391139+ Ok ()
12231140 end
1224114112251142let push ~proc ~fs ~config ?package ?(upstream = false) () =
···12391156 else begin
12401157 Log.info (fun m ->
12411158 m "Checking status of %d packages" (List.length pkgs));
12421242- let statuses = Status.compute_all ~fs:fs_t ~config pkgs in
11591159+ let statuses = Status.compute_all ~proc ~fs:fs_t ~config pkgs in
12431160 let dirty =
12441161 List.filter Status.has_local_changes statuses
12451162 |> List.map (fun s -> s.Status.package)
···12801197 m "Pushing %s to %s" (Package.repo_name pkg) push_url);
12811198 (* Set the push URL for origin *)
12821199 (match
12831283- Git_cli.set_push_url ~proc ~fs:fs_t ~url:push_url
12001200+ Git.set_push_url ~proc ~fs:fs_t ~url:push_url
12841201 checkout_dir
12851202 with
12861203 | Ok () -> ()
12871204 | Error e ->
12881205 Log.warn (fun m ->
12891289- m "Failed to set push URL: %a" Git_cli.pp_error e));
12061206+ m "Failed to set push URL: %a" Git.pp_error e));
12901207 match
12911291- Git_cli.push_remote ~proc ~fs:fs_t ~branch checkout_dir
12081208+ Git.push_remote ~proc ~fs:fs_t ~branch checkout_dir
12921209 with
12931210 | Ok () ->
12941211 Log.app (fun m ->
···13131230type sync_failure = {
13141231 repo_name : string;
13151232 phase : sync_phase;
13161316- error : Git_cli.error;
12331233+ error : Git.error;
13171234}
1318123513191236type sync_summary = {
···13321249 | `Push_remote -> Fmt.string ppf "push-remote"
1333125013341251let pp_sync_failure ppf f =
13351335- Fmt.pf ppf "%s (%a): %a" f.repo_name pp_sync_phase f.phase Git_cli.pp_error
12521252+ Fmt.pf ppf "%s (%a): %a" f.repo_name pp_sync_phase f.phase Git.pp_error
13361253 f.error
1337125413381255let pp_sync_summary ppf s =
···13551272 | _ -> false
13561273 | exception Eio.Io _ -> false
13571274 in
13581358- let was_cloned =
13591359- not (is_directory && Git_cli.is_repo ~proc ~fs checkout_dir)
13601360- in
12751275+ let was_cloned = not (is_directory && Git.is_repo ~proc ~fs checkout_dir) in
13611276 if was_cloned then begin
13621277 Log.info (fun m ->
13631278 m "Cloning %s from %a (branch: %s)" (Package.repo_name pkg) Uri.pp
13641279 (Package.dev_repo pkg) branch);
13651280 match
13661366- Git_cli.clone ~proc ~fs ~url:(Package.dev_repo pkg) ~branch checkout_dir
12811281+ Git.clone ~proc ~fs ~url:(Package.dev_repo pkg) ~branch checkout_dir
13671282 with
13681283 | Ok () ->
13691284 (* Configure checkout to accept pushes to current branch.
···13721287 Eio.Switch.run (fun sw ->
13731288 let child =
13741289 Eio.Process.spawn proc ~sw ~cwd
13751375- [
13761376- "git"; "config"; "receive.denyCurrentBranch"; "updateInstead";
13771377- ]
12901290+ [ "git"; "config"; "receive.denyCurrentBranch"; "updateInstead" ]
13781291 in
13791292 ignore (Eio.Process.await child));
13801293 Ok (true, 0)
···13931306 end
1394130713951308(* Fetch a single checkout - safe for parallel execution *)
13961396-13971397-(** Wrapper around Remote_cache that adds disk persistence via XDG cache *)
13981398-module Cached_remote_heads : sig
13991399- type t
14001400-14011401- val create : xdg:Xdge.t -> now:(unit -> float) -> t
14021402- val get : t -> url:Uri.t -> branch:string -> string option
14031403- val set : t -> url:Uri.t -> branch:string -> hash:string -> unit
14041404-end = struct
14051405- type t = { cache : Remote_cache.t; cache_file : Eio.Fs.dir_ty Eio.Path.t }
14061406-14071407- let filename = "remote-heads"
14081408-14091409- let create ~xdg ~now =
14101410- let cache_file = Eio.Path.(Xdge.cache_dir xdg / filename) in
14111411- let content = try Eio.Path.load cache_file with _ -> "" in
14121412- let cache = Remote_cache.create_from_string ~now content in
14131413- { cache; cache_file }
14141414-14151415- let get t = Remote_cache.get t.cache
14161416-14171417- let set t ~url ~branch ~hash =
14181418- Remote_cache.set t.cache ~url ~branch ~hash;
14191419- let content = Remote_cache.to_string t.cache in
14201420- try Eio.Path.save ~create:(`Or_truncate 0o644) t.cache_file content
14211421- with _ -> ()
14221422-end
14231423-14241424-let fetch_checkout_safe ~sw ~env ~proc ~fs ~config ~cache ~get_session pkg =
14251425- let repo = Package.repo_name pkg in
13091309+let fetch_checkout_safe ~proc ~fs ~config pkg =
14261310 let checkouts_root = Config.Paths.checkouts config in
14271311 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
14281312 let branch = get_branch ~config pkg in
14291429- let remote_url = Package.dev_repo pkg in
14301430- let local_head =
14311431- Git_cli.rev_parse ~proc ~fs ~rev:(Fmt.str "origin/%s" branch) checkout_dir
14321432- |> Result.to_option
13131313+ (* Get commits behind before fetching *)
13141314+ let behind_before =
13151315+ match Git.ahead_behind ~proc ~fs ~branch checkout_dir with
13161316+ | Ok ab -> ab.behind
13171317+ | Error _ -> 0
14331318 in
14341434- (* Check if we can skip fetch entirely *)
14351435- let remote_matches_local hash =
14361436- match local_head with Some h -> hash = h | None -> false
14371437- in
14381438- (* Step 1: Try cached remote HEAD - O(1) hashtbl lookup *)
14391439- match Cached_remote_heads.get cache ~url:remote_url ~branch with
14401440- | Some cached when remote_matches_local cached ->
14411441- Log.debug (fun m -> m "Skipping fetch for %s (cached)" repo);
14421442- Ok 0
14431443- | _ -> (
14441444- (* Step 2: Query remote HEAD via HTTP (lazily creates session) *)
14451445- let remote =
14461446- time_phase (Fmt.str "ls-remote:%s" repo) (fun () ->
14471447- Git.Remote.get_remote_head ~session:(get_session ()) ~sw ~env
14481448- remote_url ~branch)
13191319+ Log.info (fun m -> m "Fetching %s (all remotes)" (Package.repo_name pkg));
13201320+ match Git.fetch_all ~proc ~fs checkout_dir with
13211321+ | Error e -> Error e
13221322+ | Ok () ->
13231323+ (* Get commits behind after fetching *)
13241324+ let behind_after =
13251325+ match Git.ahead_behind ~proc ~fs ~branch checkout_dir with
13261326+ | Ok ab -> ab.behind
13271327+ | Error _ -> 0
14491328 in
14501450- Option.iter
14511451- (fun h ->
14521452- Cached_remote_heads.set cache ~url:remote_url ~branch
14531453- ~hash:(Git.Hash.to_hex h))
14541454- remote;
14551455- match remote with
14561456- | Some h when remote_matches_local (Git.Hash.to_hex h) ->
14571457- Log.debug (fun m -> m "Skipping fetch for %s (remote unchanged)" repo);
14581458- Ok 0
14591459- | _ ->
14601460- (* Step 3: Do full git fetch *)
14611461- let get_behind () =
14621462- Git_cli.ahead_behind ~proc ~fs ~branch checkout_dir
14631463- |> Result.map (fun (ab : Git_cli.ahead_behind) -> ab.behind)
14641464- |> Result.value ~default:0
14651465- in
14661466- let behind_before = get_behind () in
14671467- Log.info (fun m -> m "Fetching %s (all remotes)" repo);
14681468- Git_cli.fetch_all ~proc ~fs checkout_dir
14691469- |> Result.map (fun () -> get_behind () - behind_before))
13291329+ Ok (behind_after - behind_before)
1470133014711331(* Merge checkout to latest - must be sequential *)
14721332let merge_checkout_safe ~proc ~fs ~config pkg =
···14741334 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
14751335 let branch = get_branch ~config pkg in
14761336 Log.info (fun m -> m "Merging %s to %s" (Package.repo_name pkg) branch);
14771477- Git_cli.merge_ff ~proc ~fs ~branch checkout_dir
13371337+ Git.merge_ff ~proc ~fs ~branch checkout_dir
1478133814791339(* Push checkout to remote - safe for parallel execution *)
14801340let push_remote_safe ~proc ~fs ~config pkg =
···14841344 let push_url = url_to_push_url (Package.dev_repo pkg) in
14851345 Log.info (fun m -> m "Pushing %s to %s" (Package.repo_name pkg) push_url);
14861346 (* Set the push URL for origin *)
14871487- (match Git_cli.set_push_url ~proc ~fs ~url:push_url checkout_dir with
13471347+ (match Git.set_push_url ~proc ~fs ~url:push_url checkout_dir with
14881348 | Ok () -> ()
14891489- | Error e ->
14901490- Log.warn (fun m -> m "Failed to set push URL: %a" Git_cli.pp_error e));
14911491- Git_cli.push_remote ~proc ~fs ~branch checkout_dir
13491349+ | Error e -> Log.warn (fun m -> m "Failed to set push URL: %a" Git.pp_error e));
13501350+ Git.push_remote ~proc ~fs ~branch checkout_dir
1492135114931352(* Sanitize handle for use as git remote name *)
14941353let sanitize_remote_name handle =
14951354 (* Replace @ and . with - for valid git remote names *)
14961355 String.map (function '@' | '.' -> '-' | c -> c) handle
1497135614981498-(* Ensure verse remotes for a single repo - fully native git *)
14991499-let ensure_verse_remotes_for_repo ~fs ~config ~verse_subtrees pkg =
13571357+(* Ensure verse remotes for a single repo *)
13581358+let ensure_verse_remotes_for_repo ~proc ~fs ~config ~verse_subtrees pkg =
15001359 let checkouts_root = Config.Paths.checkouts config in
15011360 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
15021502- let checkout_path = Fpath.to_string checkout_dir in
15031361 let repo_name = Package.repo_name pkg in
1504136215051505- (* Only process if checkout exists - use native git *)
15061506- if not (Git.Repository.is_repo ~fs checkout_path) then ()
13631363+ (* Only process if checkout exists *)
13641364+ if not (Git.is_repo ~proc ~fs checkout_dir) then ()
15071365 else begin
15081366 (* Get all verse members who have this repo *)
15091367 let members_with_repo =
15101368 Hashtbl.find_opt verse_subtrees repo_name |> Option.value ~default:[]
15111369 in
1512137015131513- (* Get current remotes - use native git *)
15141514- let repo = Git.Repository.open_repo ~fs checkout_path in
15151515- let current_remotes = Git.Repository.list_remotes repo in
13711371+ (* Get current remotes *)
13721372+ let current_remotes = Git.list_remotes ~proc ~fs checkout_dir in
15161373 let verse_remotes =
15171374 List.filter
15181375 (fun r -> String.starts_with ~prefix:"verse-" r)
15191376 current_remotes
15201377 in
1521137815221522- (* Build set of expected verse remotes with their URLs *)
13791379+ (* Build set of expected verse remotes *)
15231380 let expected_remotes =
15241524- List.filter_map
15251525- (fun (handle, verse_mono_path) ->
15261526- let remote_name = "verse-" ^ sanitize_remote_name handle in
15271527- let verse_src = Fpath.(parent verse_mono_path / "src" / repo_name) in
15281528- if Sys.file_exists (Fpath.to_string verse_src) then
15291529- Some (remote_name, Fpath.to_string verse_src)
15301530- else None)
13811381+ List.map
13821382+ (fun (handle, _) -> "verse-" ^ sanitize_remote_name handle)
15311383 members_with_repo
15321384 in
15331533- let expected_names = List.map fst expected_remotes in
1534138515351535- (* Add/update remotes for verse members - native git *)
13861386+ (* Add/update remotes for verse members *)
15361387 List.iter
15371537- (fun (remote_name, url) ->
15381538- match Git.Repository.ensure_remote repo ~name:remote_name ~url with
15391539- | Ok () ->
15401540- Log.debug (fun m ->
15411541- m "Ensured verse remote %s -> %s" remote_name url)
15421542- | Error (`Msg msg) ->
15431543- Log.warn (fun m ->
15441544- m "Failed to add verse remote %s: %s" remote_name msg))
15451545- expected_remotes;
13881388+ (fun (handle, verse_mono_path) ->
13891389+ let remote_name = "verse-" ^ sanitize_remote_name handle in
13901390+ (* Point to their src/ checkout for this repo *)
13911391+ let verse_src = Fpath.(parent verse_mono_path / "src" / repo_name) in
13921392+ if Sys.file_exists (Fpath.to_string verse_src) then begin
13931393+ let url = Fpath.to_string verse_src in
13941394+ match
13951395+ Git.ensure_remote ~proc ~fs ~name:remote_name ~url checkout_dir
13961396+ with
13971397+ | Ok () ->
13981398+ Log.debug (fun m ->
13991399+ m "Ensured verse remote %s -> %s" remote_name url)
14001400+ | Error e ->
14011401+ Log.warn (fun m ->
14021402+ m "Failed to add verse remote %s: %a" remote_name Git.pp_error
14031403+ e)
14041404+ end)
14051405+ members_with_repo;
1546140615471547- (* Remove outdated verse remotes - native git *)
14071407+ (* Remove outdated verse remotes *)
15481408 List.iter
15491409 (fun remote_name ->
15501550- if not (List.mem remote_name expected_names) then begin
14101410+ if not (List.mem remote_name expected_remotes) then begin
15511411 Log.debug (fun m -> m "Removing outdated verse remote %s" remote_name);
15521552- match Git.Repository.remove_remote repo remote_name with
14121412+ match Git.remove_remote ~proc ~fs ~name:remote_name checkout_dir with
15531413 | Ok () -> ()
15541554- | Error (`Msg msg) ->
14141414+ | Error e ->
15551415 Log.warn (fun m ->
15561556- m "Failed to remove verse remote %s: %s" remote_name msg)
14161416+ m "Failed to remove verse remote %s: %a" remote_name
14171417+ Git.pp_error e)
15571418 end)
15581419 verse_remotes
15591420 end
···15651426 Verse.get_verse_subtrees ~proc ~fs ~config:verse_config ()
15661427 in
15671428 List.iter
15681568- (fun pkg -> ensure_verse_remotes_for_repo ~fs ~config ~verse_subtrees pkg)
14291429+ (fun pkg ->
14301430+ ensure_verse_remotes_for_repo ~proc ~fs ~config ~verse_subtrees pkg)
15691431 repos
1570143215711571-(* Fetch from verse remotes for a repo - uses native git for list_remotes *)
14331433+(* Fetch from verse remotes for a repo *)
15721434let fetch_verse_remotes ~proc ~fs ~config pkg =
15731435 let checkouts_root = Config.Paths.checkouts config in
15741436 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
15751575- let checkout_path = Fpath.to_string checkout_dir in
15761576- let remotes =
15771577- if Git.Repository.is_repo ~fs checkout_path then
15781578- let repo = Git.Repository.open_repo ~fs checkout_path in
15791579- Git.Repository.list_remotes repo
15801580- else []
15811581- in
14371437+ let remotes = Git.list_remotes ~proc ~fs checkout_dir in
15821438 let verse_remotes =
15831439 List.filter (fun r -> String.starts_with ~prefix:"verse-" r) remotes
15841440 in
15851441 List.iter
15861442 (fun remote ->
15871443 Log.debug (fun m -> m "Fetching from verse remote %s" remote);
15881588- match Git_cli.fetch ~proc ~fs ~remote checkout_dir with
14441444+ match Git.fetch ~proc ~fs ~remote checkout_dir with
15891445 | Ok () -> ()
15901446 | Error e ->
15911447 Log.debug (fun m ->
15921592- m "Failed to fetch from %s: %a" remote Git_cli.pp_error e))
14481448+ m "Failed to fetch from %s: %a" remote Git.pp_error e))
15931449 verse_remotes
1594145015951451(* Helper to read file contents, returning None if file doesn't exist *)
···16131469 List.iter
16141470 (fun pkg ->
16151471 let pkg_dir =
16161616- Fpath.(
16171617- opam_repo / "packages" / pkg.pkg_name / (pkg.pkg_name ^ ".dev"))
14721472+ Fpath.(opam_repo / "packages" / pkg.pkg_name / (pkg.pkg_name ^ ".dev"))
16181473 in
16191474 let dst_path = Eio.Path.(fs / Fpath.to_string pkg_dir / "opam") in
16201475 let dst_content = read_file_opt dst_path in
···16261481 end)
16271482 pkgs;
16281483 if !updated > 0 then
16291629- Log.info (fun m ->
16301630- m "Regenerated %d opam-repo entries from monorepo" !updated)
14841484+ Log.info (fun m -> m "Regenerated %d opam-repo entries from monorepo" !updated)
1631148516321632-(** Clone monorepo and opam-repo from verse registry if they don't exist
16331633- locally. This enables `monopam sync` to work in a fresh devcontainer. *)
14861486+(** Clone monorepo and opam-repo from verse registry if they don't exist locally.
14871487+ This enables `monopam sync` to work in a fresh devcontainer. *)
16341488let clone_from_verse_if_needed ~proc ~fs ~config () =
16351489 let monorepo = Config.Paths.monorepo config in
16361490 let opam_repo = Config.Paths.opam_repo config in
16371637- let monorepo_exists = Git_cli.is_repo ~proc ~fs monorepo in
16381638- let opam_repo_exists = Git_cli.is_repo ~proc ~fs opam_repo in
14911491+ let monorepo_exists = Git.is_repo ~proc ~fs monorepo in
14921492+ let opam_repo_exists = Git.is_repo ~proc ~fs opam_repo in
1639149316401494 (* If both exist, nothing to do *)
16411495 if monorepo_exists && opam_repo_exists then Ok ()
···16441498 match Verse_config.load ~fs () with
16451499 | Error _ ->
16461500 (* No verse config - can't clone from registry *)
16471647- Log.debug (fun m ->
16481648- m "No verse config found, will initialize fresh repos");
15011501+ Log.debug (fun m -> m "No verse config found, will initialize fresh repos");
16491502 Ok ()
16501650- | Ok verse_config -> (
15031503+ | Ok verse_config ->
16511504 let handle = Verse_config.handle verse_config in
16521505 Log.info (fun m -> m "Found verse config for handle: %s" handle);
16531506 (* Load registry to look up URLs *)
16541654- match
16551655- Verse_registry.clone_or_pull ~proc ~fs ~config:verse_config ()
16561656- with
15071507+ match Verse_registry.clone_or_pull ~proc ~fs ~config:verse_config () with
16571508 | Error msg ->
16581509 Log.warn (fun m -> m "Could not load verse registry: %s" msg);
16591659- Ok () (* Continue without cloning - will init fresh *)
16601660- | Ok registry -> (
15101510+ Ok () (* Continue without cloning - will init fresh *)
15111511+ | Ok registry ->
16611512 match Verse_registry.find_member registry ~handle with
16621513 | None ->
16631514 Log.warn (fun m -> m "Handle %s not found in registry" handle);
16641515 Ok ()
16651665- | Some member -> (
15161516+ | Some member ->
16661517 (* Clone monorepo if needed *)
16671518 let result =
16681519 if monorepo_exists then Ok ()
16691520 else begin
16701670- Log.app (fun m ->
16711671- m "Cloning monorepo from %s..." member.monorepo);
15211521+ Log.app (fun m -> m "Cloning monorepo from %s..." member.monorepo);
16721522 let url = Uri.of_string member.monorepo in
16731673- let branch =
16741674- Option.value ~default:"main" member.monorepo_branch
16751675- in
16761676- match Git_cli.clone ~proc ~fs ~url ~branch monorepo with
15231523+ let branch = Option.value ~default:"main" member.monorepo_branch in
15241524+ match Git.clone ~proc ~fs ~url ~branch monorepo with
16771525 | Ok () ->
16781526 Log.app (fun m -> m "Monorepo cloned successfully");
16791527 Ok ()
16801528 | Error e ->
16811681- Log.err (fun m ->
16821682- m "Failed to clone monorepo: %a" Git_cli.pp_error e);
15291529+ Log.err (fun m -> m "Failed to clone monorepo: %a" Git.pp_error e);
16831530 Error (Git_error e)
16841531 end
16851532 in
···16891536 (* Clone opam-repo if needed *)
16901537 if opam_repo_exists then Ok ()
16911538 else begin
16921692- Log.app (fun m ->
16931693- m "Cloning opam-repo from %s..." member.opamrepo);
15391539+ Log.app (fun m -> m "Cloning opam-repo from %s..." member.opamrepo);
16941540 let url = Uri.of_string member.opamrepo in
16951695- let branch =
16961696- Option.value ~default:"main" member.opamrepo_branch
16971697- in
16981698- match Git_cli.clone ~proc ~fs ~url ~branch opam_repo with
15411541+ let branch = Option.value ~default:"main" member.opamrepo_branch in
15421542+ match Git.clone ~proc ~fs ~url ~branch opam_repo with
16991543 | Ok () ->
17001544 Log.app (fun m -> m "Opam-repo cloned successfully");
17011545 Ok ()
17021546 | Error e ->
17031703- Log.err (fun m ->
17041704- m "Failed to clone opam-repo: %a" Git_cli.pp_error
17051705- e);
15471547+ Log.err (fun m -> m "Failed to clone opam-repo: %a" Git.pp_error e);
17061548 Error (Git_error e)
17071707- end)))
15491549+ end
1708155017091709-let sync ~sw ~env ~proc ~fs ~config ~xdg ?package ?(remote = false)
17101710- ?(skip_push = false) ?(skip_pull = false) ?(skip_verse = false) () =
15511551+let sync ~proc ~fs ~config ?package ?(remote = false) ?(skip_push = false)
15521552+ ?(skip_pull = false) () =
17111553 let fs_t = fs_typed fs in
17121712- (* Create remote HEAD cache with O(1) lookup - loaded once, persisted on updates *)
17131713- let cache =
17141714- Cached_remote_heads.create ~xdg ~now:(fun () -> Eio.Time.now env#clock)
17151715- in
17161716- (* Domain-safe lazy HTTP session to avoid TLS cert loading if cache hits *)
17171717- let session_atom : Requests.t option Atomic.t = Atomic.make None in
17181718- let get_session () =
17191719- match Atomic.get session_atom with
17201720- | Some s -> s
17211721- | None ->
17221722- let s = Requests.create ~sw env in
17231723- (* CAS to avoid races - if another domain created one, use theirs *)
17241724- if Atomic.compare_and_set session_atom None (Some s) then s
17251725- else Option.get (Atomic.get session_atom)
17261726- in
1727155417281728- (* Step 0: Sync verse members if verse config exists and not skipping
17291729- Skip verse sync when syncing a specific package for faster operations *)
17301730- let should_skip_verse = skip_pull || skip_verse || Option.is_some package in
17311731- (if not should_skip_verse then
17321732- match Verse_config.load ~fs:fs_t () with
17331733- | Error _ -> () (* No verse config = skip *)
17341734- | Ok verse_config ->
17351735- Log.app (fun m -> m "Syncing verse members...");
17361736- time_phase "verse-sync" (fun () ->
17371737- match Verse.pull ~proc ~fs:fs_t ~config:verse_config () with
17381738- | Ok () -> ()
17391739- | Error e ->
17401740- Log.warn (fun m -> m "Verse sync: %a" Verse.pp_error e)));
15551555+ (* Step 0: Sync verse members if verse config exists and not skipping pull *)
15561556+ (if not skip_pull then
15571557+ match Verse_config.load ~fs:fs_t () with
15581558+ | Error _ -> () (* No verse config = skip *)
15591559+ | Ok verse_config ->
15601560+ Log.app (fun m -> m "Syncing verse members...");
15611561+ match Verse.pull ~proc ~fs:fs_t ~config:verse_config () with
15621562+ | Ok () -> ()
15631563+ | Error e -> Log.warn (fun m -> m "Verse sync: %a" Verse.pp_error e));
1741156417421565 (* Clone from verse registry if repos don't exist *)
17431566 match clone_from_verse_if_needed ~proc ~fs:fs_t ~config () with
17441567 | Error e -> Error e
15681568+ | Ok () ->
15691569+15701570+ (* Update the opam repo first - clone if needed *)
15711571+ let opam_repo = Config.Paths.opam_repo config in
15721572+ if (not skip_pull) && Git.is_repo ~proc ~fs:fs_t opam_repo then begin
15731573+ Log.info (fun m -> m "Updating opam repo at %a" Fpath.pp opam_repo);
15741574+ let result =
15751575+ let ( let* ) = Result.bind in
15761576+ let* () = Git.fetch ~proc ~fs:fs_t opam_repo in
15771577+ Git.merge_ff ~proc ~fs:fs_t opam_repo
15781578+ in
15791579+ match result with
15801580+ | Ok () -> ()
15811581+ | Error e ->
15821582+ Log.warn (fun m -> m "Failed to update opam repo: %a" Git.pp_error e)
15831583+ end;
15841584+ (* Ensure directories exist *)
15851585+ ensure_checkouts_dir ~fs:fs_t ~config;
15861586+ match ensure_monorepo_initialized ~proc ~fs:fs_t ~config with
15871587+ | Error e -> Error e
17451588 | Ok () -> (
17461746- (* Update the opam repo first - clone if needed
17471747- Skip when syncing a single package for faster operations *)
17481748- let opam_repo = Config.Paths.opam_repo config in
17491749- let skip_opam_repo = Option.is_some package in
17501750- if
17511751- (not skip_pull) && (not skip_opam_repo)
17521752- && Git_cli.is_repo ~proc ~fs:fs_t opam_repo
17531753- then begin
17541754- Log.info (fun m -> m "Updating opam repo at %a" Fpath.pp opam_repo);
17551755- time_phase "opam-repo-fetch" (fun () ->
17561756- let result =
17571757- let ( let* ) = Result.bind in
17581758- let* () = Git_cli.fetch ~proc ~fs:fs_t opam_repo in
17591759- Git_cli.merge_ff ~proc ~fs:fs_t opam_repo
17601760- in
17611761- match result with
17621762- | Ok () -> ()
17631763- | Error e ->
17641764- Log.warn (fun m ->
17651765- m "Failed to update opam repo: %a" Git_cli.pp_error e))
17661766- end;
17671767- (* Ensure directories exist *)
17681768- ensure_checkouts_dir ~fs:fs_t ~config;
17691769- match ensure_monorepo_initialized ~proc ~fs:fs_t ~config with
15891589+ (* Check for uncommitted changes in monorepo *)
15901590+ let monorepo = Config.Paths.monorepo config in
15911591+ if Git.is_dirty ~proc ~fs:fs_t monorepo then begin
15921592+ Log.err (fun m -> m "Monorepo has uncommitted changes");
15931593+ Error Monorepo_dirty
15941594+ end
15951595+ else begin
15961596+ (* Regenerate opam-repo from monorepo to ensure URLs are up to date *)
15971597+ regenerate_opam_repo ~fs:(fs_t :> _ Eio.Path.t) ~config ();
15981598+ match discover_packages ~fs:(fs_t :> _ Eio.Path.t) ~config () with
17701599 | Error e -> Error e
17711771- | Ok () -> (
17721772- (* Regenerate opam-repo from monorepo to ensure URLs are up to date *)
17731773- (* Skip when syncing a single package for faster operations *)
17741774- if Option.is_none package then
17751775- time_phase "regenerate-opam-repo" (fun () ->
17761776- regenerate_opam_repo ~fs:(fs_t :> _ Eio.Path.t) ~config ());
17771777- match
17781778- time_phase "discover-packages" (fun () ->
17791779- discover_packages ~fs:(fs_t :> _ Eio.Path.t) ~config ())
17801780- with
17811781- | Error e -> Error e
17821782- | Ok all_pkgs ->
17831783- let pkgs =
17841784- match package with
17851785- | None -> all_pkgs
17861786- | Some name ->
17871787- List.filter (fun p -> Package.name p = name) all_pkgs
16001600+ | Ok all_pkgs ->
16011601+ let pkgs =
16021602+ match package with
16031603+ | None -> all_pkgs
16041604+ | Some name -> List.filter (fun p -> Package.name p = name) all_pkgs
16051605+ in
16061606+ if pkgs = [] && package <> None then
16071607+ Error (Package_not_found (Option.get package))
16081608+ else begin
16091609+ (* Step 1: Validate - check for dirty state *)
16101610+ Log.info (fun m ->
16111611+ m "Checking status of %d packages" (List.length pkgs));
16121612+ let statuses = Status.compute_all ~proc ~fs:fs_t ~config pkgs in
16131613+ let dirty =
16141614+ List.filter Status.has_local_changes statuses
16151615+ |> List.map (fun s -> s.Status.package)
16161616+ in
16171617+ if dirty <> [] then Error (Dirty_state dirty)
16181618+ else begin
16191619+ let repos = unique_repos pkgs in
16201620+ let total = List.length repos in
16211621+ Log.app (fun m -> m "Syncing %d repositories..." total);
16221622+16231623+ (* Build status lookup for optimization *)
16241624+ let status_by_name =
16251625+ List.map (fun s -> (Package.name s.Status.package, s)) statuses
16261626+ in
16271627+ let sync_needs_push = function
16281628+ | Status.Subtree_ahead _ | Status.Trees_differ -> true
16291629+ | Status.In_sync | Status.Subtree_behind _ | Status.Unknown ->
16301630+ false
17881631 in
17891789- if pkgs = [] && package <> None then
17901790- Error (Package_not_found (Option.get package))
17911791- else begin
17921792- (* Step 1: Validate - check for dirty state in packages being synced *)
17931793- Log.info (fun m ->
17941794- m "Checking status of %d packages" (List.length pkgs));
17951795- let statuses =
17961796- time_phase "compute-status" (fun () ->
17971797- Status.compute_all ~fs:fs_t ~config pkgs)
17981798- in
17991799- let dirty =
18001800- List.filter Status.has_local_changes statuses
18011801- |> List.map (fun s -> s.Status.package)
18021802- in
18031803- if dirty <> [] then Error (Dirty_state dirty)
18041804- else begin
18051805- let repos = unique_repos pkgs in
18061806- let total = List.length repos in
18071807- Log.app (fun m -> m "Syncing %d repositories..." total);
16321632+ let needs_push pkg =
16331633+ List.assoc_opt (Package.name pkg) status_by_name
16341634+ |> Option.fold ~none:true ~some:(fun s ->
16351635+ sync_needs_push s.Status.subtree_sync)
16361636+ in
16371637+ let sync_needs_pull = function
16381638+ | Status.Subtree_behind _ | Status.Trees_differ -> true
16391639+ | Status.In_sync | Status.Subtree_ahead _ | Status.Unknown ->
16401640+ false
16411641+ in
16421642+ let needs_pull pkg =
16431643+ List.assoc_opt (Package.name pkg) status_by_name
16441644+ |> Option.fold ~none:true ~some:(fun s ->
16451645+ sync_needs_pull s.Status.subtree_sync)
16461646+ in
1808164718091809- (* Build status lookup for optimization *)
18101810- let status_by_name =
18111811- List.map
18121812- (fun s -> (Package.name s.Status.package, s))
18131813- statuses
16481648+ (* Step 2: Push phase - export monorepo changes to checkouts (PARALLEL) *)
16491649+ (* git subtree push is read-only on the monorepo, so safe to parallelize *)
16501650+ (* OPTIMIZATION: skip packages already in sync *)
16511651+ let push_results =
16521652+ if skip_push then begin
16531653+ Log.app (fun m ->
16541654+ m " Skipping push to checkouts (--skip-push)");
16551655+ List.map (fun pkg -> Ok (Package.repo_name pkg)) repos
16561656+ end
16571657+ else begin
16581658+ let to_push, to_skip = List.partition needs_push repos in
16591659+ Log.app (fun m ->
16601660+ m " Pushing monorepo changes to checkouts (parallel)...");
16611661+ if to_skip <> [] then
16621662+ Log.app (fun m ->
16631663+ m " Skipping %d already-synced packages"
16641664+ (List.length to_skip));
16651665+ (* Local git subtree push - no parallelism limit needed *)
16661666+ let pushed =
16671667+ Eio.Fiber.List.map
16681668+ (fun pkg ->
16691669+ let repo_name = Package.repo_name pkg in
16701670+ Log.info (fun m -> m "Push to checkout: %s" repo_name);
16711671+ match push_one ~proc ~fs ~config pkg with
16721672+ | Ok () -> Ok repo_name
16731673+ | Error (Git_error e) ->
16741674+ Error
16751675+ { repo_name; phase = `Push_checkout; error = e }
16761676+ | Error _ -> Ok repo_name)
16771677+ to_push
18141678 in
18151815- let sync_needs_push = function
18161816- | Status.Subtree_ahead _ | Status.Trees_differ -> true
18171817- | Status.In_sync | Status.Subtree_behind _ | Status.Unknown
18181818- ->
18191819- false
16791679+ let skipped_ok =
16801680+ List.map (fun pkg -> Ok (Package.repo_name pkg)) to_skip
18201681 in
18211821- let needs_push pkg =
18221822- List.assoc_opt (Package.name pkg) status_by_name
18231823- |> Option.fold ~none:true ~some:(fun s ->
18241824- sync_needs_push s.Status.subtree_sync)
16821682+ pushed @ skipped_ok
16831683+ end
16841684+ in
16851685+ let push_errors =
16861686+ List.filter_map
16871687+ (function Error e -> Some e | Ok _ -> None)
16881688+ push_results
16891689+ in
16901690+16911691+ (* Steps 3-5: Pull phases (fetch, merge, subtree) - skip if --skip-pull *)
16921692+ let ( fetch_errors,
16931693+ unchanged_count,
16941694+ total_commits_pulled,
16951695+ merge_errors,
16961696+ subtree_errors,
16971697+ successfully_fetched_repos ) =
16981698+ if skip_pull then begin
16991699+ Log.app (fun m ->
17001700+ m " Skipping pull from remotes (--skip-pull)");
17011701+ ([], List.length repos, 0, ref [], ref [], repos)
17021702+ end
17031703+ else begin
17041704+ (* Step 3: Fetch phase - clone/fetch from remotes (PARALLEL) *)
17051705+ Log.app (fun m -> m " Fetching from remotes (parallel)...");
17061706+ let fetch_results =
17071707+ Eio.Fiber.List.map ~max_fibers:4
17081708+ (fun pkg ->
17091709+ let repo_name = Package.repo_name pkg in
17101710+ (* First ensure checkout exists *)
17111711+ match
17121712+ ensure_checkout_safe ~proc ~fs:fs_t ~config pkg
17131713+ with
17141714+ | Error e ->
17151715+ Error { repo_name; phase = `Fetch; error = e }
17161716+ | Ok (was_cloned, _) -> (
17171717+ if was_cloned then Ok (repo_name, true, 0)
17181718+ else
17191719+ match
17201720+ fetch_checkout_safe ~proc ~fs:fs_t ~config pkg
17211721+ with
17221722+ | Error e ->
17231723+ Error { repo_name; phase = `Fetch; error = e }
17241724+ | Ok commits -> Ok (repo_name, false, commits)))
17251725+ repos
18251726 in
18261826- let sync_needs_pull = function
18271827- | Status.Subtree_behind _ | Status.Trees_differ -> true
18281828- | Status.In_sync | Status.Subtree_ahead _ | Status.Unknown
18291829- ->
18301830- false
17271727+ let fetch_errs, fetch_successes =
17281728+ List.partition_map
17291729+ (function Error e -> Left e | Ok r -> Right r)
17301730+ fetch_results
18311731 in
18321832- let needs_pull pkg =
18331833- List.assoc_opt (Package.name pkg) status_by_name
18341834- |> Option.fold ~none:true ~some:(fun s ->
18351835- sync_needs_pull s.Status.subtree_sync)
17321732+ let cloned =
17331733+ List.filter (fun (_, c, _) -> c) fetch_successes
18361734 in
18371837-18381838- (* Step 2: Push phase - export monorepo changes to checkouts (PARALLEL) *)
18391839- (* git subtree push is read-only on the monorepo, so safe to parallelize *)
18401840- (* OPTIMIZATION: skip packages already in sync *)
18411841- let push_results =
18421842- if skip_push then begin
18431843- Log.app (fun m ->
18441844- m " Skipping push to checkouts (--skip-push)");
18451845- List.map (fun pkg -> Ok (Package.repo_name pkg)) repos
18461846- end
18471847- else begin
18481848- let to_push, to_skip = List.partition needs_push repos in
18491849- Log.app (fun m ->
18501850- m
18511851- " Pushing monorepo changes to checkouts \
18521852- (parallel)...");
18531853- if to_skip <> [] then
18541854- Log.app (fun m ->
18551855- m " Skipping %d already-synced packages"
18561856- (List.length to_skip));
18571857- (* Local git subtree push - no parallelism limit needed *)
18581858- let pushed =
18591859- Eio.Fiber.List.map
18601860- (fun pkg ->
18611861- let repo_name = Package.repo_name pkg in
18621862- Log.info (fun m ->
18631863- m "Push to checkout: %s" repo_name);
18641864- match push_one ~proc ~fs ~config pkg with
18651865- | Ok () -> Ok repo_name
18661866- | Error (Git_error e) ->
18671867- Error
18681868- {
18691869- repo_name;
18701870- phase = `Push_checkout;
18711871- error = e;
18721872- }
18731873- | Error _ -> Ok repo_name)
18741874- to_push
18751875- in
18761876- let skipped_ok =
18771877- List.map (fun pkg -> Ok (Package.repo_name pkg)) to_skip
18781878- in
18791879- pushed @ skipped_ok
18801880- end
17351735+ let updated =
17361736+ List.filter
17371737+ (fun (_, c, commits) -> (not c) && commits > 0)
17381738+ fetch_successes
18811739 in
18821882- let push_errors =
18831883- List.filter_map
18841884- (function Error e -> Some e | Ok _ -> None)
18851885- push_results
17401740+ let unchanged =
17411741+ List.length fetch_successes
17421742+ - List.length cloned - List.length updated
18861743 in
18871887-18881888- (* Steps 3-5: Pull phases (fetch, merge, subtree) - skip if --skip-pull *)
18891889- let ( fetch_errors,
18901890- unchanged_count,
18911891- total_commits_pulled,
18921892- merge_errors,
18931893- subtree_errors,
18941894- successfully_fetched_repos ) =
18951895- if skip_pull then begin
18961896- Log.app (fun m ->
18971897- m " Skipping pull from remotes (--skip-pull)");
18981898- ([], List.length repos, 0, ref [], ref [], repos)
18991899- end
19001900- else begin
19011901- (* Step 3: Fetch phase - clone/fetch from remotes (PARALLEL) *)
19021902- Log.app (fun m ->
19031903- m " Fetching from remotes (parallel)...");
19041904- let fetch_results =
19051905- time_phase "fetch-phase" (fun () ->
19061906- Eio.Fiber.List.map ~max_fibers:8
19071907- (fun pkg ->
19081908- let repo_name = Package.repo_name pkg in
19091909- (* First ensure checkout exists *)
19101910- match
19111911- time_phase
19121912- (Printf.sprintf "ensure-checkout:%s"
19131913- repo_name) (fun () ->
19141914- ensure_checkout_safe ~proc ~fs:fs_t
19151915- ~config pkg)
19161916- with
19171917- | Error e ->
19181918- Error
19191919- { repo_name; phase = `Fetch; error = e }
19201920- | Ok (was_cloned, _) -> (
19211921- if was_cloned then Ok (repo_name, true, 0)
19221922- else
19231923- match
19241924- time_phase
19251925- (Printf.sprintf "fetch:%s" repo_name)
19261926- (fun () ->
19271927- fetch_checkout_safe ~sw ~env ~proc
19281928- ~fs:fs_t ~config ~cache
19291929- ~get_session pkg)
19301930- with
19311931- | Error e ->
19321932- Error
19331933- {
19341934- repo_name;
19351935- phase = `Fetch;
19361936- error = e;
19371937- }
19381938- | Ok commits ->
19391939- Ok (repo_name, false, commits)))
19401940- repos)
19411941- in
19421942- let fetch_errs, fetch_successes =
19431943- List.partition_map
19441944- (function Error e -> Left e | Ok r -> Right r)
19451945- fetch_results
19461946- in
19471947- let cloned =
19481948- List.filter (fun (_, c, _) -> c) fetch_successes
19491949- in
19501950- let updated =
19511951- List.filter
19521952- (fun (_, c, commits) -> (not c) && commits > 0)
19531953- fetch_successes
19541954- in
19551955- let unchanged =
19561956- List.length fetch_successes
19571957- - List.length cloned - List.length updated
19581958- in
19591959- let commits_pulled =
19601960- List.fold_left
19611961- (fun acc (_, _, c) -> acc + c)
19621962- 0 fetch_successes
19631963- in
19641964- Log.app (fun m ->
19651965- m " Pulled: %d cloned, %d updated, %d unchanged"
19661966- (List.length cloned) (List.length updated) unchanged);
19671967-19681968- (* Filter repos to only those that were successfully fetched *)
19691969- let success_names =
19701970- List.map (fun (name, _, _) -> name) fetch_successes
19711971- in
19721972- let successfully_fetched =
19731973- List.filter
19741974- (fun pkg ->
19751975- List.mem (Package.repo_name pkg) success_names)
19761976- repos
19771977- in
19781978-19791979- (* Step 4: Merge phase - fast-forward merge checkouts (SEQUENTIAL) *)
19801980- Log.app (fun m -> m " Merging checkouts...");
19811981- let merge_errs = ref [] in
19821982- time_phase "merge-phase" (fun () ->
19831983- List.iter
19841984- (fun pkg ->
19851985- match
19861986- time_phase
19871987- (Printf.sprintf "merge:%s"
19881988- (Package.repo_name pkg))
19891989- (fun () ->
19901990- merge_checkout_safe ~proc ~fs:fs_t ~config
19911991- pkg)
19921992- with
19931993- | Ok () -> ()
19941994- | Error e ->
19951995- merge_errs :=
19961996- {
19971997- repo_name = Package.repo_name pkg;
19981998- phase = `Merge;
19991999- error = e;
20002000- }
20012001- :: !merge_errs)
20022002- successfully_fetched);
17441744+ let commits_pulled =
17451745+ List.fold_left
17461746+ (fun acc (_, _, c) -> acc + c)
17471747+ 0 fetch_successes
17481748+ in
17491749+ Log.app (fun m ->
17501750+ m " Pulled: %d cloned, %d updated, %d unchanged"
17511751+ (List.length cloned) (List.length updated) unchanged);
2003175220042004- (* Step 5: Subtree phase - pull subtrees into monorepo (SEQUENTIAL) *)
20052005- (* Check if monorepo has local modifications first *)
20062006- let monorepo = Config.Paths.monorepo config in
20072007- let monorepo_dirty =
20082008- Git_cli.is_dirty ~proc ~fs:fs_t monorepo
20092009- in
20102010- let subtree_errs = ref [] in
20112011- if monorepo_dirty then begin
20122012- Log.warn (fun m ->
20132013- m
20142014- "Monorepo has uncommitted changes, skipping \
20152015- subtree pulls");
20162016- Log.app (fun m ->
20172017- m
20182018- " Skipping subtree updates (local \
20192019- modifications)...")
20202020- end
20212021- else begin
20222022- (* OPTIMIZATION: skip packages already in sync *)
20232023- (* But always pull repos that received commits from fetch *)
20242024- let repos_updated_by_fetch =
20252025- List.filter_map
20262026- (fun (name, was_cloned, commits) ->
20272027- if was_cloned || commits > 0 then Some name
20282028- else None)
20292029- fetch_successes
20302030- in
20312031- let needs_pull_after_fetch pkg =
20322032- needs_pull pkg
20332033- || List.mem (Package.repo_name pkg)
20342034- repos_updated_by_fetch
20352035- in
20362036- let to_pull, to_skip =
20372037- List.partition needs_pull_after_fetch
20382038- successfully_fetched
20392039- in
20402040- Log.app (fun m -> m " Updating subtrees...");
20412041- if to_skip <> [] then
20422042- Log.app (fun m ->
20432043- m " Skipping %d already-synced subtrees"
20442044- (List.length to_skip));
20452045- let pull_count = List.length to_pull in
20462046- List.iteri
20472047- (fun i pkg ->
20482048- Log.info (fun m ->
20492049- m "[%d/%d] Subtree %s" (i + 1) pull_count
20502050- (Package.subtree_prefix pkg));
20512051- match pull_subtree ~proc ~fs ~config pkg with
20522052- | Ok _ -> ()
20532053- | Error (Git_error e) ->
20542054- subtree_errs :=
20552055- {
20562056- repo_name = Package.repo_name pkg;
20572057- phase = `Subtree;
20582058- error = e;
20592059- }
20602060- :: !subtree_errs
20612061- | Error _ -> ())
20622062- to_pull
20632063- end;
20642064- ( fetch_errs,
20652065- unchanged,
20662066- commits_pulled,
20672067- merge_errs,
20682068- subtree_errs,
20692069- successfully_fetched )
20702070- end
17531753+ (* Filter repos to only those that were successfully fetched *)
17541754+ let success_names =
17551755+ List.map (fun (name, _, _) -> name) fetch_successes
17561756+ in
17571757+ let successfully_fetched =
17581758+ List.filter
17591759+ (fun pkg -> List.mem (Package.repo_name pkg) success_names)
17601760+ repos
20711761 in
2072176220732073- (* Step 5.5: Verse remotes - update and fetch from verse members *)
20742074- (* Skip when syncing a single package for faster operations *)
20752075- (* Only operate on successfully fetched repos to avoid missing directory errors *)
20762076- (if Option.is_some package then
20772077- Log.debug (fun m ->
20782078- m "Skipping verse remotes (single package sync)")
20792079- else
20802080- match Verse_config.load ~fs:(fs_t :> _ Eio.Path.t) () with
20812081- | Error _ -> () (* No verse config, skip verse remotes *)
20822082- | Ok verse_config ->
20832083- time_phase "sync-verse-remotes" (fun () ->
20842084- sync_verse_remotes ~proc ~fs:fs_t ~config
20852085- ~verse_config successfully_fetched_repos);
20862086- (* Fetch from verse remotes in parallel *)
20872087- Log.app (fun m -> m " Fetching from verse remotes...");
20882088- time_phase "fetch-verse-remotes" (fun () ->
20892089- Eio.Fiber.List.iter ~max_fibers:8
20902090- (fun pkg ->
20912091- fetch_verse_remotes ~proc ~fs:fs_t ~config pkg)
20922092- successfully_fetched_repos));
17631763+ (* Step 4: Merge phase - fast-forward merge checkouts (SEQUENTIAL) *)
17641764+ Log.app (fun m -> m " Merging checkouts...");
17651765+ let merge_errs = ref [] in
17661766+ List.iter
17671767+ (fun pkg ->
17681768+ match merge_checkout_safe ~proc ~fs:fs_t ~config pkg with
17691769+ | Ok () -> ()
17701770+ | Error e ->
17711771+ merge_errs :=
17721772+ {
17731773+ repo_name = Package.repo_name pkg;
17741774+ phase = `Merge;
17751775+ error = e;
17761776+ }
17771777+ :: !merge_errs)
17781778+ successfully_fetched;
2093177920942094- (* Step 6: Finalize - write README.md, CLAUDE.md, and dune-project (SEQUENTIAL) *)
20952095- (* Skip when syncing a single package for faster operations *)
20962096- if Option.is_some package then
20972097- Log.debug (fun m ->
20982098- m "Skipping finalize (single package sync)")
20992099- else begin
17801780+ (* Step 5: Subtree phase - pull subtrees into monorepo (SEQUENTIAL) *)
17811781+ (* Check if monorepo has local modifications first *)
17821782+ let monorepo = Config.Paths.monorepo config in
17831783+ let monorepo_dirty = Git.is_dirty ~proc ~fs:fs_t monorepo in
17841784+ let subtree_errs = ref [] in
17851785+ if monorepo_dirty then begin
17861786+ Log.warn (fun m ->
17871787+ m
17881788+ "Monorepo has uncommitted changes, skipping subtree \
17891789+ pulls");
21001790 Log.app (fun m ->
21012101- m " Writing README.md, CLAUDE.md, and dune-project...");
21022102- time_phase "write-readme" (fun () ->
21032103- write_readme ~proc ~fs:fs_t ~config all_pkgs);
21042104- time_phase "write-claude-md" (fun () ->
21052105- write_claude_md ~proc ~fs:fs_t ~config);
21062106- time_phase "write-dune-project" (fun () ->
21072107- write_dune_project ~proc ~fs:fs_t ~config all_pkgs)
17911791+ m " Skipping subtree updates (local modifications)...")
17921792+ end
17931793+ else begin
17941794+ (* OPTIMIZATION: skip packages already in sync *)
17951795+ (* But always pull repos that received commits from fetch *)
17961796+ let repos_updated_by_fetch =
17971797+ List.filter_map
17981798+ (fun (name, was_cloned, commits) ->
17991799+ if was_cloned || commits > 0 then Some name else None)
18001800+ fetch_successes
18011801+ in
18021802+ let needs_pull_after_fetch pkg =
18031803+ needs_pull pkg
18041804+ || List.mem (Package.repo_name pkg) repos_updated_by_fetch
18051805+ in
18061806+ let to_pull, to_skip =
18071807+ List.partition needs_pull_after_fetch successfully_fetched
18081808+ in
18091809+ Log.app (fun m -> m " Updating subtrees...");
18101810+ if to_skip <> [] then
18111811+ Log.app (fun m ->
18121812+ m " Skipping %d already-synced subtrees"
18131813+ (List.length to_skip));
18141814+ let pull_count = List.length to_pull in
18151815+ List.iteri
18161816+ (fun i pkg ->
18171817+ Log.info (fun m ->
18181818+ m "[%d/%d] Subtree %s" (i + 1) pull_count
18191819+ (Package.subtree_prefix pkg));
18201820+ match pull_subtree ~proc ~fs ~config pkg with
18211821+ | Ok _ -> ()
18221822+ | Error (Git_error e) ->
18231823+ subtree_errs :=
18241824+ {
18251825+ repo_name = Package.repo_name pkg;
18261826+ phase = `Subtree;
18271827+ error = e;
18281828+ }
18291829+ :: !subtree_errs
18301830+ | Error _ -> ())
18311831+ to_pull
21081832 end;
18331833+ ( fetch_errs,
18341834+ unchanged,
18351835+ commits_pulled,
18361836+ merge_errs,
18371837+ subtree_errs,
18381838+ successfully_fetched )
18391839+ end
18401840+ in
2109184121102110- (* Step 7: Remote phase - push to upstream remotes if --remote (LIMITED PARALLEL) *)
21112111- (* Only push repos that were successfully fetched *)
21122112- let remote_errors =
21132113- if remote then begin
21142114- Log.app (fun m -> m " Pushing to upstream remotes...");
21152115- (* Limit to 2 concurrent pushes to avoid overwhelming remotes *)
21162116- let push_results =
21172117- Eio.Fiber.List.map ~max_fibers:2
21182118- (fun pkg ->
21192119- let repo_name = Package.repo_name pkg in
21202120- match
21212121- push_remote_safe ~proc ~fs:fs_t ~config pkg
21222122- with
21232123- | Error e ->
21242124- Error
21252125- { repo_name; phase = `Push_remote; error = e }
21262126- | Ok () ->
21272127- Log.app (fun m -> m " Pushed %s" repo_name);
21282128- Ok repo_name)
21292129- successfully_fetched_repos
21302130- in
21312131- let errors, successes =
21322132- List.partition_map
21332133- (function Error e -> Left e | Ok r -> Right r)
21342134- push_results
21352135- in
21362136- Log.app (fun m ->
21372137- m " Pushed: %d repos to upstream"
21382138- (List.length successes));
21392139- errors
21402140- end
21412141- else []
21422142- in
18421842+ (* Step 5.5: Verse remotes - update and fetch from verse members *)
18431843+ (* Only operate on successfully fetched repos to avoid missing directory errors *)
18441844+ (match Verse_config.load ~fs:(fs_t :> _ Eio.Path.t) () with
18451845+ | Error _ -> () (* No verse config, skip verse remotes *)
18461846+ | Ok verse_config ->
18471847+ sync_verse_remotes ~proc ~fs:fs_t ~config ~verse_config successfully_fetched_repos;
18481848+ (* Fetch from verse remotes in parallel *)
18491849+ Log.app (fun m -> m " Fetching from verse remotes...");
18501850+ Eio.Fiber.List.iter ~max_fibers:4
18511851+ (fun pkg -> fetch_verse_remotes ~proc ~fs:fs_t ~config pkg)
18521852+ successfully_fetched_repos);
2143185321442144- (* Collect all errors *)
21452145- let all_errors =
21462146- push_errors @ fetch_errors @ !merge_errors @ !subtree_errors
21472147- @ remote_errors
18541854+ (* Step 6: Finalize - write README.md, CLAUDE.md, and dune-project (SEQUENTIAL) *)
18551855+ Log.app (fun m ->
18561856+ m " Writing README.md, CLAUDE.md, and dune-project...");
18571857+ write_readme ~proc ~fs:fs_t ~config all_pkgs;
18581858+ write_claude_md ~proc ~fs:fs_t ~config;
18591859+ write_dune_project ~proc ~fs:fs_t ~config all_pkgs;
18601860+18611861+ (* Step 7: Remote phase - push to upstream remotes if --remote (LIMITED PARALLEL) *)
18621862+ (* Only push repos that were successfully fetched *)
18631863+ let remote_errors =
18641864+ if remote then begin
18651865+ Log.app (fun m -> m " Pushing to upstream remotes...");
18661866+ (* Limit to 2 concurrent pushes to avoid overwhelming remotes *)
18671867+ let push_results =
18681868+ Eio.Fiber.List.map ~max_fibers:2
18691869+ (fun pkg ->
18701870+ let repo_name = Package.repo_name pkg in
18711871+ match push_remote_safe ~proc ~fs:fs_t ~config pkg with
18721872+ | Error e ->
18731873+ Error { repo_name; phase = `Push_remote; error = e }
18741874+ | Ok () ->
18751875+ Log.app (fun m -> m " Pushed %s" repo_name);
18761876+ Ok repo_name)
18771877+ successfully_fetched_repos
21481878 in
21492149- let summary =
21502150- {
21512151- repos_synced = List.length repos - List.length all_errors;
21522152- repos_unchanged = unchanged_count;
21532153- commits_pulled = total_commits_pulled;
21542154- commits_pushed = 0;
21552155- (* TODO: track this *)
21562156- errors = all_errors;
21572157- }
18791879+ let errors, successes =
18801880+ List.partition_map
18811881+ (function Error e -> Left e | Ok r -> Right r)
18821882+ push_results
21581883 in
21592159-21602160- (* Print summary *)
21611884 Log.app (fun m ->
21622162- m "@.Summary: %d synced, %d errors" summary.repos_synced
21632163- (List.length summary.errors));
21642164- if summary.errors <> [] then
21652165- List.iter
21662166- (fun e -> Log.warn (fun m -> m " %a" pp_sync_failure e))
21672167- summary.errors;
21682168-21692169- Ok summary
18851885+ m " Pushed: %d repos to upstream" (List.length successes));
18861886+ errors
21701887 end
21712171- end))
18881888+ else []
18891889+ in
18901890+18911891+ (* Collect all errors *)
18921892+ let all_errors =
18931893+ push_errors @ fetch_errors @ !merge_errors @ !subtree_errors
18941894+ @ remote_errors
18951895+ in
18961896+ let summary =
18971897+ {
18981898+ repos_synced = List.length repos - List.length all_errors;
18991899+ repos_unchanged = unchanged_count;
19001900+ commits_pulled = total_commits_pulled;
19011901+ commits_pushed = 0;
19021902+ (* TODO: track this *)
19031903+ errors = all_errors;
19041904+ }
19051905+ in
19061906+19071907+ (* Print summary *)
19081908+ Log.app (fun m ->
19091909+ m "@.Summary: %d synced, %d errors" summary.repos_synced
19101910+ (List.length summary.errors));
19111911+ if summary.errors <> [] then
19121912+ List.iter
19131913+ (fun e -> Log.warn (fun m -> m " %a" pp_sync_failure e))
19141914+ summary.errors;
19151915+19161916+ Ok summary
19171917+ end
19181918+ end
19191919+ end)
2172192021731921(* Opam metadata sync: copy .opam files from monorepo subtrees to opam-repo *)
21741922···22311979 | Ok s ->
22321980 let count = List.length (Sources_registry.to_list s) in
22331981 if count > 0 then
22342234- Log.info (fun m ->
22352235- m "Loaded %d source overrides from sources.toml" count);
19821982+ Log.info (fun m -> m "Loaded %d source overrides from sources.toml" count);
22361983 s
22371984 | Error msg ->
22381985 Log.warn (fun m -> m "Failed to load sources.toml: %s" msg);
···22401987 in
2241198822421989 (* Discover packages from monorepo *)
22432243- match
22442244- discover_packages_from_monorepo ~fs:(fs :> _ Eio.Path.t) ~config ~sources ()
22452245- with
19901990+ match discover_packages_from_monorepo ~fs:(fs :> _ Eio.Path.t) ~config ~sources () with
22461991 | Error e -> Error e
22471992 | Ok all_pkgs ->
22481993 (* Filter to specific package/subtree if requested *)
···22672012 (fun pkg ->
22682013 (* Destination: opam-repo/packages/<name>/<name>.dev/opam *)
22692014 let pkg_dir =
22702270- Fpath.(
22712271- opam_repo / "packages" / pkg.pkg_name / (pkg.pkg_name ^ ".dev"))
20152015+ Fpath.(opam_repo / "packages" / pkg.pkg_name / (pkg.pkg_name ^ ".dev"))
22722016 in
22732017 let dst_path = Eio.Path.(fs / Fpath.to_string pkg_dir / "opam") in
22742018···2299204323002044 (* Find and delete orphaned packages *)
23012045 let generated_names =
23022302- List.map (fun p -> p.pkg_name) pkgs |> List.sort_uniq String.compare
20462046+ List.map (fun p -> p.pkg_name) pkgs
20472047+ |> List.sort_uniq String.compare
23032048 in
23042049 let existing_packages = list_opam_repo_packages ~fs ~config in
23052050 let orphaned =
···23252070 {
23262071 synced = List.rev !synced;
23272072 unchanged = List.rev !unchanged;
23282328- missing = [];
23292329- (* No longer used in generation-based approach *)
20732073+ missing = []; (* No longer used in generation-based approach *)
23302074 orphaned = deleted;
23312075 }
23322076 in
···23802124 let fs = fs_typed fs in
23812125 let monorepo = Config.Paths.monorepo config in
23822126 let prefix = package in
23832383- if not (Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix) then Ok ()
21272127+ if not (Git.Subtree.exists ~fs ~repo:monorepo ~prefix) then Ok ()
23842128 else
23852129 let subtree_path = Eio.Path.(fs / Fpath.to_string monorepo / prefix) in
23862130 try
23872131 Eio.Path.rmtree subtree_path;
23882132 Ok ()
23892133 with Eio.Io _ as e ->
23902390- Error (Git_error (Git_cli.Io_error (Printexc.to_string e)))
21342134+ Error (Git_error (Git.Io_error (Printexc.to_string e)))
2391213523922136(* Changes command - generate weekly changelogs using Claude *)
23932137···24592203 let since = week_start ^ " 00:00:00" in
24602204 let until = week_end ^ " 23:59:59" in
24612205 match
24622462- Git_cli.log ~proc ~fs:fs_t ~since ~until
24632463- ~path:repo_name monorepo
22062206+ Git.log ~proc ~fs:fs_t ~since ~until ~path:repo_name
22072207+ monorepo
24642208 with
24652209 | Error e -> Error (Git_error e)
24662210 | Ok commits ->
···25022246 repo_name week_start);
25032247 (* Create new entry *)
25042248 let first_hash =
25052505- (List.hd commits).Git_cli.hash
22492249+ (List.hd commits).Git.hash
25062250 in
25072251 let last_hash =
25082508- (List.hd (List.rev commits)).Git_cli.hash
22522252+ (List.hd (List.rev commits)).Git.hash
25092253 in
25102254 let entry : Changes.weekly_entry =
25112255 {
···26682412 let since = date ^ " 00:00:00" in
26692413 let until = date ^ " 23:59:59" in
26702414 match
26712671- Git_cli.log ~proc ~fs:fs_t ~since ~until
26722672- ~path:repo_name monorepo
24152415+ Git.log ~proc ~fs:fs_t ~since ~until ~path:repo_name
24162416+ monorepo
26732417 with
26742418 | Error e -> Error (Git_error e)
26752419 | Ok commits ->
···27122456 (* Extract unique contributors from commits *)
27132457 let contributors =
27142458 commits
27152715- |> List.map
27162716- (fun (c : Git_cli.log_entry) ->
27172717- c.author)
24592459+ |> List.map (fun (c : Git.log_entry) ->
24602460+ c.author)
27182461 |> List.sort_uniq String.compare
27192462 in
27202463 (* Get repo URL from package dev_repo *)
···27312474 in
27322475 (* Create new entry with hour and timestamp *)
27332476 let first_hash =
27342734- (List.hd commits).Git_cli.hash
24772477+ (List.hd commits).Git.hash
27352478 in
27362479 let last_hash =
27372737- (List.hd (List.rev commits)).Git_cli.hash
24802480+ (List.hd (List.rev commits)).Git.hash
27382481 in
27392482 let _, ((hour, _, _), _) =
27402483 Ptime.to_date_time now_ptime
···28312574 if (not dry_run) && aggregate then begin
28322575 let today = Changes.date_of_ptime now_ptime in
28332576 let git_head =
28342834- match Git_cli.rev_parse ~proc ~fs:fs_t ~rev:"HEAD" monorepo with
25772577+ match Git.rev_parse ~proc ~fs:fs_t ~rev:"HEAD" monorepo with
28352578 | Ok hash -> String.sub hash 0 (min 7 (String.length hash))
28362579 | Error _ -> "unknown"
28372580 in
···28562599 repo_name : string;
28572600 handle : string;
28582601 relationship : Forks.relationship;
28592859- commits : Git_cli.log_entry list;
28602860- patches : (string * string) list; (* hash -> patch content *)
26022602+ commits : Git.log_entry list;
26032603+ patches : (string * string) list; (* hash -> patch content *)
28612604}
2862260528632863-type diff_result = { entries : diff_entry list; forks : Forks.t }
26062606+type diff_result = {
26072607+ entries : diff_entry list;
26082608+ forks : Forks.t;
26092609+}
2864261028652611let pp_diff_entry ~show_patch ppf entry =
28662612 let n_commits = List.length entry.commits in
28672613 Fmt.pf ppf "@[<v 2>%a %s (%a, %d commit%s):@,"
28682868- Fmt.(styled `Bold string)
28692869- entry.repo_name entry.handle Forks.pp_relationship entry.relationship
28702870- n_commits
28712871- (if n_commits = 1 then "" else "s");
28722872- List.iter
28732873- (fun (c : Git_cli.log_entry) ->
28742874- let short_hash = String.sub c.hash 0 (min 7 (String.length c.hash)) in
28752875- Fmt.pf ppf " %a %s %a@,"
28762876- Fmt.(styled `Yellow string)
28772877- short_hash c.subject
28782878- Fmt.(styled `Faint string)
28792879- c.author;
28802880- if show_patch then
28812881- match List.assoc_opt c.hash entry.patches with
28822882- | Some patch -> Fmt.pf ppf "@,%s@," patch
28832883- | None -> ())
26142614+ Fmt.(styled `Bold string) entry.repo_name
26152615+ entry.handle
26162616+ Forks.pp_relationship entry.relationship
26172617+ n_commits (if n_commits = 1 then "" else "s");
26182618+ List.iter (fun (c : Git.log_entry) ->
26192619+ let short_hash = String.sub c.hash 0 (min 7 (String.length c.hash)) in
26202620+ Fmt.pf ppf " %a %s %a@,"
26212621+ Fmt.(styled `Yellow string) short_hash
26222622+ c.subject
26232623+ Fmt.(styled `Faint string) c.author;
26242624+ if show_patch then
26252625+ match List.assoc_opt c.hash entry.patches with
26262626+ | Some patch -> Fmt.pf ppf "@,%s@," patch
26272627+ | None -> ())
28842628 entry.commits;
28852629 Fmt.pf ppf "@]"
28862630···28902634 (* Then show diffs for each entry *)
28912635 if result.entries <> [] then begin
28922636 Fmt.pf ppf "@[<v>%a@]@."
28932893- Fmt.(list ~sep:(any "@,@,") (pp_diff_entry ~show_patch))
28942894- result.entries
26372637+ Fmt.(list ~sep:(any "@,@,") (pp_diff_entry ~show_patch)) result.entries
28952638 end
2896263928972640(** Check if a string looks like a git commit hash (7+ hex chars) *)
28982641let is_commit_sha s =
28992899- String.length s >= 7
29002900- && String.for_all
29012901- (function '0' .. '9' | 'a' .. 'f' | 'A' .. 'F' -> true | _ -> false)
29022902- s
26422642+ String.length s >= 7 &&
26432643+ String.for_all (function '0'..'9' | 'a'..'f' | 'A'..'F' -> true | _ -> false) s
2903264429042904-let diff ~proc ~fs ~config ~verse_config ?repo ?(refresh = false)
29052905- ?(patch = false) () =
26452645+let diff ~proc ~fs ~config ~verse_config ?repo ?(refresh=false) ?(patch=false) () =
29062646 let checkouts_path = Config.Paths.checkouts config in
2907264729082648 (* Compute fork analysis *)
29092909- let forks =
29102910- Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh ()
29112911- in
26492649+ let forks = Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh () in
2912265029132651 (* Filter repos if specific one requested *)
29142914- let repos_to_check =
29152915- match repo with
26522652+ let repos_to_check = match repo with
29162653 | None -> forks.repos
29172654 | Some name -> List.filter (fun r -> r.Forks.repo_name = name) forks.repos
29182655 in
2919265629202657 (* For each repo with actionable status, get commits *)
29212658 let entries =
29222922- List.filter_map
29232923- (fun (r : Forks.repo_analysis) ->
29242924- (* Find actionable verse sources *)
29252925- let actionable =
29262926- List.filter
29272927- (fun (_, _, rel) ->
29282928- match rel with
29292929- | Forks.I_am_behind _ -> true
29302930- | Forks.Diverged _ -> true
29312931- | _ -> false)
29322932- r.verse_sources
29332933- in
29342934- match actionable with
29352935- | [] -> None
29362936- | sources -> (
29372937- (* Get commits for each actionable source *)
29382938- let entries =
29392939- List.filter_map
29402940- (fun (handle, _src, rel) ->
29412941- let checkout_path = Fpath.(checkouts_path / r.repo_name) in
29422942- if not (Git_cli.is_repo ~proc ~fs checkout_path) then None
29432943- else begin
29442944- let remote_name = "verse/" ^ handle in
29452945- let my_ref = "origin/main" in
29462946- let their_ref = remote_name ^ "/main" in
29472947- (* Get commits they have that I don't *)
29482948- match
29492949- Git_cli.log_range ~proc ~fs ~base:my_ref ~tip:their_ref
29502950- ~max_count:20 checkout_path
29512951- with
29522952- | Error _ -> None
29532953- | Ok commits when commits = [] -> None
29542954- | Ok commits ->
29552955- (* Fetch patches if requested *)
29562956- let patches =
29572957- if patch then
29582958- List.filter_map
29592959- (fun (c : Git_cli.log_entry) ->
29602960- match
29612961- Git_cli.show_patch ~proc ~fs ~commit:c.hash
29622962- checkout_path
29632963- with
29642964- | Ok p -> Some (c.hash, p)
29652965- | Error _ -> None)
29662966- commits
29672967- else []
29682968- in
29692969- Some
29702970- {
29712971- repo_name = r.repo_name;
29722972- handle;
29732973- relationship = rel;
29742974- commits;
29752975- patches;
29762976- }
29772977- end)
29782978- sources
29792979- in
29802980- match entries with [] -> None | _ -> Some entries))
26592659+ List.filter_map (fun (r : Forks.repo_analysis) ->
26602660+ (* Find actionable verse sources *)
26612661+ let actionable = List.filter (fun (_, _, rel) ->
26622662+ match rel with
26632663+ | Forks.I_am_behind _ -> true
26642664+ | Forks.Diverged _ -> true
26652665+ | _ -> false)
26662666+ r.verse_sources
26672667+ in
26682668+ match actionable with
26692669+ | [] -> None
26702670+ | sources ->
26712671+ (* Get commits for each actionable source *)
26722672+ let entries = List.filter_map (fun (handle, _src, rel) ->
26732673+ let checkout_path = Fpath.(checkouts_path / r.repo_name) in
26742674+ if not (Git.is_repo ~proc ~fs checkout_path) then None
26752675+ else begin
26762676+ let remote_name = "verse/" ^ handle in
26772677+ let my_ref = "origin/main" in
26782678+ let their_ref = remote_name ^ "/main" in
26792679+ (* Get commits they have that I don't *)
26802680+ match Git.log_range ~proc ~fs ~base:my_ref ~tip:their_ref ~max_count:20 checkout_path with
26812681+ | Error _ -> None
26822682+ | Ok commits when commits = [] -> None
26832683+ | Ok commits ->
26842684+ (* Fetch patches if requested *)
26852685+ let patches =
26862686+ if patch then
26872687+ List.filter_map (fun (c : Git.log_entry) ->
26882688+ match Git.show_patch ~proc ~fs ~commit:c.hash checkout_path with
26892689+ | Ok p -> Some (c.hash, p)
26902690+ | Error _ -> None)
26912691+ commits
26922692+ else []
26932693+ in
26942694+ Some { repo_name = r.repo_name; handle; relationship = rel; commits; patches }
26952695+ end)
26962696+ sources
26972697+ in
26982698+ match entries with
26992699+ | [] -> None
27002700+ | _ -> Some entries)
29812701 repos_to_check
29822702 |> List.flatten
29832703 in
29842704 { entries; forks }
2985270527062706+(** Result of looking up a specific commit *)
29862707type commit_info = {
29872708 commit_repo : string;
29882709 commit_handle : string;
···29912712 commit_author : string;
29922713 commit_patch : string;
29932714}
29942994-(** Result of looking up a specific commit *)
2995271529962716(** Show patch for a specific commit SHA from diff output *)
29972997-let diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?(refresh = false) ()
29982998- =
27172717+let diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?(refresh=false) () =
29992718 let checkouts_path = Config.Paths.checkouts config in
3000271930012720 (* Compute fork analysis to find which repo has this commit *)
30023002- let forks =
30033003- Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh ()
30043004- in
27212721+ let forks = Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh () in
3005272230062723 (* Search through repos for this commit *)
30073007- let result =
30083008- List.find_map
30093009- (fun (r : Forks.repo_analysis) ->
30103010- let checkout_path = Fpath.(checkouts_path / r.repo_name) in
30113011- if not (Git_cli.is_repo ~proc ~fs checkout_path) then None
30123012- else
30133013- (* Check each verse source *)
30143014- List.find_map
30153015- (fun (handle, _src, rel) ->
30163016- match rel with
30173017- | Forks.I_am_behind _ | Forks.Diverged _ -> (
30183018- let remote_name = "verse/" ^ handle in
30193019- let my_ref = "origin/main" in
30203020- let their_ref = remote_name ^ "/main" in
30213021- (* Get commits they have that I don't *)
30223022- match
30233023- Git_cli.log_range ~proc ~fs ~base:my_ref ~tip:their_ref
30243024- ~max_count:50 checkout_path
30253025- with
30263026- | Error _ -> None
30273027- | Ok commits -> (
30283028- (* Check if our sha matches any commit *)
30293029- let matching =
30303030- List.find_opt
30313031- (fun (c : Git_cli.log_entry) ->
30323032- String.starts_with ~prefix:sha c.hash
30333033- || String.starts_with
30343034- ~prefix:(String.lowercase_ascii sha)
30353035- (String.lowercase_ascii c.hash))
30363036- commits
30373037- in
30383038- match matching with
30393039- | None -> None
30403040- | Some c -> (
30413041- match
30423042- Git_cli.show_patch ~proc ~fs ~commit:c.hash
30433043- checkout_path
30443044- with
30453045- | Ok patch ->
30463046- Some
30473047- {
30483048- commit_repo = r.repo_name;
30493049- commit_handle = handle;
30503050- commit_hash = c.hash;
30513051- commit_subject = c.subject;
30523052- commit_author = c.author;
30533053- commit_patch = patch;
30543054- }
30553055- | Error _ -> None)))
30563056- | _ -> None)
30573057- r.verse_sources)
30583058- forks.repos
27242724+ let result = List.find_map (fun (r : Forks.repo_analysis) ->
27252725+ let checkout_path = Fpath.(checkouts_path / r.repo_name) in
27262726+ if not (Git.is_repo ~proc ~fs checkout_path) then None
27272727+ else
27282728+ (* Check each verse source *)
27292729+ List.find_map (fun (handle, _src, rel) ->
27302730+ match rel with
27312731+ | Forks.I_am_behind _ | Forks.Diverged _ ->
27322732+ let remote_name = "verse/" ^ handle in
27332733+ let my_ref = "origin/main" in
27342734+ let their_ref = remote_name ^ "/main" in
27352735+ (* Get commits they have that I don't *)
27362736+ (match Git.log_range ~proc ~fs ~base:my_ref ~tip:their_ref ~max_count:50 checkout_path with
27372737+ | Error _ -> None
27382738+ | Ok commits ->
27392739+ (* Check if our sha matches any commit *)
27402740+ let matching = List.find_opt (fun (c : Git.log_entry) ->
27412741+ String.starts_with ~prefix:sha c.hash ||
27422742+ String.starts_with ~prefix:(String.lowercase_ascii sha) (String.lowercase_ascii c.hash))
27432743+ commits
27442744+ in
27452745+ match matching with
27462746+ | None -> None
27472747+ | Some c ->
27482748+ match Git.show_patch ~proc ~fs ~commit:c.hash checkout_path with
27492749+ | Ok patch -> Some {
27502750+ commit_repo = r.repo_name;
27512751+ commit_handle = handle;
27522752+ commit_hash = c.hash;
27532753+ commit_subject = c.subject;
27542754+ commit_author = c.author;
27552755+ commit_patch = patch;
27562756+ }
27572757+ | Error _ -> None)
27582758+ | _ -> None)
27592759+ r.verse_sources)
27602760+ forks.repos
30592761 in
30602762 result
30612763···30702772let pp_handle_pull_result ppf result =
30712773 if result.repos_pulled <> [] then begin
30722774 Fmt.pf ppf "@[<v>%a@," Fmt.(styled `Bold string) "Pulled:";
30733073- List.iter
30743074- (fun (repo, count) -> Fmt.pf ppf " %s: %d commits@," repo count)
27752775+ List.iter (fun (repo, count) ->
27762776+ Fmt.pf ppf " %s: %d commits@," repo count)
30752777 result.repos_pulled;
30762778 Fmt.pf ppf "@]"
30772779 end;
30782780 if result.repos_skipped <> [] then
30792781 Fmt.pf ppf "%a %s@,"
30803080- Fmt.(styled `Faint string)
30813081- "Skipped:"
27822782+ Fmt.(styled `Faint string) "Skipped:"
30822783 (String.concat ", " result.repos_skipped);
30832784 if result.repos_failed <> [] then begin
30842785 Fmt.pf ppf "@[<v>%a@," Fmt.(styled `Red string) "Failed:";
30853085- List.iter
30863086- (fun (repo, err) -> Fmt.pf ppf " %s: %s@," repo err)
27862786+ List.iter (fun (repo, err) ->
27872787+ Fmt.pf ppf " %s: %s@," repo err)
30872788 result.repos_failed;
30882789 Fmt.pf ppf "@]"
30892790 end
3090279130913091-let pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo
30923092- ?(refresh = false) () =
27922792+let pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo ?(refresh=false) () =
30932793 let checkouts_path = Config.Paths.checkouts config in
3094279430952795 (* Compute fork analysis *)
30963096- let forks =
30973097- Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh ()
30983098- in
27962796+ let forks = Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh () in
3099279731002798 (* Filter repos if specific one requested *)
31013101- let repos_to_check =
31023102- match repo with
27992799+ let repos_to_check = match repo with
31032800 | None -> forks.repos
31042801 | Some name -> List.filter (fun r -> r.Forks.repo_name = name) forks.repos
31052802 in
···31092806 let repos_skipped = ref [] in
31102807 let repos_failed = ref [] in
3111280831123112- List.iter
31133113- (fun (r : Forks.repo_analysis) ->
31143114- (* Check if this handle has commits for this repo *)
31153115- let handle_source =
31163116- List.find_opt (fun (h, _, _) -> h = handle) r.verse_sources
31173117- in
31183118- match handle_source with
31193119- | None ->
31203120- (* Handle doesn't have this repo *)
31213121- ()
31223122- | Some (_, _, rel) ->
31233123- let checkout_path = Fpath.(checkouts_path / r.repo_name) in
31243124- if not (Git_cli.is_repo ~proc ~fs checkout_path) then
31253125- repos_skipped := r.repo_name :: !repos_skipped
31263126- else begin
31273127- match rel with
31283128- | Forks.Same_url | Forks.Same_commit | Forks.I_am_ahead _ ->
31293129- repos_skipped := r.repo_name :: !repos_skipped
31303130- | Forks.Not_fetched | Forks.Unrelated ->
31313131- repos_skipped := r.repo_name :: !repos_skipped
31323132- | Forks.I_am_behind count -> (
31333133- (* Merge their changes *)
31343134- let remote_ref = "verse/" ^ handle ^ "/main" in
31353135- match
31363136- Git_cli.merge ~proc ~fs ~ref_name:remote_ref ~ff_only:true
31373137- checkout_path
31383138- with
31393139- | Ok () -> repos_pulled := (r.repo_name, count) :: !repos_pulled
31403140- | Error e ->
31413141- repos_failed :=
31423142- (r.repo_name, Fmt.str "%a" Git_cli.pp_error e)
31433143- :: !repos_failed)
31443144- | Forks.Diverged { their_ahead; _ } -> (
31453145- (* Merge their changes (may create a merge commit) *)
31463146- let remote_ref = "verse/" ^ handle ^ "/main" in
31473147- match
31483148- Git_cli.merge ~proc ~fs ~ref_name:remote_ref checkout_path
31493149- with
31503150- | Ok () ->
31513151- repos_pulled := (r.repo_name, their_ahead) :: !repos_pulled
31523152- | Error e ->
31533153- repos_failed :=
31543154- (r.repo_name, Fmt.str "%a" Git_cli.pp_error e)
31553155- :: !repos_failed)
31563156- end)
28092809+ List.iter (fun (r : Forks.repo_analysis) ->
28102810+ (* Check if this handle has commits for this repo *)
28112811+ let handle_source = List.find_opt (fun (h, _, _) -> h = handle) r.verse_sources in
28122812+ match handle_source with
28132813+ | None ->
28142814+ (* Handle doesn't have this repo *)
28152815+ ()
28162816+ | Some (_, _, rel) ->
28172817+ let checkout_path = Fpath.(checkouts_path / r.repo_name) in
28182818+ if not (Git.is_repo ~proc ~fs checkout_path) then
28192819+ repos_skipped := r.repo_name :: !repos_skipped
28202820+ else begin
28212821+ match rel with
28222822+ | Forks.Same_url | Forks.Same_commit | Forks.I_am_ahead _ ->
28232823+ repos_skipped := r.repo_name :: !repos_skipped
28242824+ | Forks.Not_fetched | Forks.Unrelated ->
28252825+ repos_skipped := r.repo_name :: !repos_skipped
28262826+ | Forks.I_am_behind count ->
28272827+ (* Merge their changes *)
28282828+ let remote_ref = "verse/" ^ handle ^ "/main" in
28292829+ (match Git.merge ~proc ~fs ~ref_name:remote_ref ~ff_only:true checkout_path with
28302830+ | Ok () ->
28312831+ repos_pulled := (r.repo_name, count) :: !repos_pulled
28322832+ | Error e ->
28332833+ repos_failed := (r.repo_name, Fmt.str "%a" Git.pp_error e) :: !repos_failed)
28342834+ | Forks.Diverged { their_ahead; _ } ->
28352835+ (* Merge their changes (may create a merge commit) *)
28362836+ let remote_ref = "verse/" ^ handle ^ "/main" in
28372837+ (match Git.merge ~proc ~fs ~ref_name:remote_ref checkout_path with
28382838+ | Ok () ->
28392839+ repos_pulled := (r.repo_name, their_ahead) :: !repos_pulled
28402840+ | Error e ->
28412841+ repos_failed := (r.repo_name, Fmt.str "%a" Git.pp_error e) :: !repos_failed)
28422842+ end)
31572843 repos_to_check;
3158284431593159- Ok
31603160- {
31613161- repos_pulled = List.rev !repos_pulled;
31623162- repos_skipped = List.rev !repos_skipped;
31633163- repos_failed = List.rev !repos_failed;
31643164- }
28452845+ Ok {
28462846+ repos_pulled = List.rev !repos_pulled;
28472847+ repos_skipped = List.rev !repos_skipped;
28482848+ repos_failed = List.rev !repos_failed;
28492849+ }
3165285031662851(* ==================== Cherry-pick ==================== *)
31672852···31722857}
3173285831742859let pp_cherrypick_result ppf result =
31753175- let short_hash =
31763176- String.sub result.commit_hash 0 (min 7 (String.length result.commit_hash))
31773177- in
28602860+ let short_hash = String.sub result.commit_hash 0 (min 7 (String.length result.commit_hash)) in
31782861 Fmt.pf ppf "Cherry-picked %a %s into %s@."
31793179- Fmt.(styled `Yellow string)
31803180- short_hash result.commit_subject result.repo_name
28622862+ Fmt.(styled `Yellow string) short_hash
28632863+ result.commit_subject
28642864+ result.repo_name
3181286531823182-let cherrypick ~proc ~fs ~config ~verse_config ~sha ?(refresh = false) () =
28662866+let cherrypick ~proc ~fs ~config ~verse_config ~sha ?(refresh=false) () =
31832867 let checkouts_path = Config.Paths.checkouts config in
3184286831852869 (* First, find the commit *)
31862870 match diff_show_commit ~proc ~fs ~config ~verse_config ~sha ~refresh () with
31872871 | None ->
31883188- Error
31893189- (Config_error
31903190- (Printf.sprintf "Commit %s not found in any verse diff" sha))
28722872+ Error (Config_error (Printf.sprintf "Commit %s not found in any verse diff" sha))
31912873 | Some info ->
31922874 let checkout_path = Fpath.(checkouts_path / info.commit_repo) in
31933193- if not (Git_cli.is_repo ~proc ~fs checkout_path) then
31943194- Error
31953195- (Config_error
31963196- (Printf.sprintf "No checkout for repository %s" info.commit_repo))
28752875+ if not (Git.is_repo ~proc ~fs checkout_path) then
28762876+ Error (Config_error (Printf.sprintf "No checkout for repository %s" info.commit_repo))
31972877 else begin
31983198- match
31993199- Git_cli.cherry_pick ~proc ~fs ~commit:info.commit_hash checkout_path
32003200- with
28782878+ match Git.cherry_pick ~proc ~fs ~commit:info.commit_hash checkout_path with
32012879 | Ok () ->
32023202- Ok
32033203- {
32043204- repo_name = info.commit_repo;
32053205- commit_hash = info.commit_hash;
32063206- commit_subject = info.commit_subject;
32073207- }
32083208- | Error e -> Error (Git_error e)
28802880+ Ok {
28812881+ repo_name = info.commit_repo;
28822882+ commit_hash = info.commit_hash;
28832883+ commit_subject = info.commit_subject;
28842884+ }
28852885+ | Error e ->
28862886+ Error (Git_error e)
32092887 end
+52-65
lib/monopam.mli
···1717 - {!Config} - Configuration management
1818 - {!Package} - Package metadata
1919 - {!Opam_repo} - Opam repository scanning
2020- - {!Git_cli} - Git operations (CLI-based)
2020+ - {!Git} - Git operations
2121 - {!Status} - Status computation *)
22222323(** Re-export modules for convenience. *)
···2525module Config = Config
2626module Package = Package
2727module Opam_repo = Opam_repo
2828-module Git_cli = Git_cli
2828+module Git = Git
2929module Status = Status
3030module Changes = Changes
3131module Verse = Verse
···4040module Sources_registry = Sources_registry
4141module Fork_join = Fork_join
4242module Site = Site
4343-module Remote_cache = Remote_cache
44434544(** {1 High-Level Operations} *)
4645···4847type error =
4948 | Config_error of string (** Configuration error *)
5049 | Repo_error of Opam_repo.error (** Opam repository error *)
5151- | Git_error of Git_cli.error (** Git operation error *)
5050+ | Git_error of Git.error (** Git operation error *)
5251 | Dirty_state of Package.t list
5352 (** Operation blocked due to dirty packages *)
5453 | Monorepo_dirty (** Monorepo has uncommitted changes *)
···144143type sync_failure = {
145144 repo_name : string;
146145 phase : sync_phase;
147147- error : Git_cli.error;
146146+ error : Git.error;
148147}
149148(** A failure during sync for a specific repository. *)
150149···167166(** [pp_sync_summary] formats a sync summary. *)
168167169168val sync :
170170- sw:Eio.Switch.t ->
171171- env:
172172- < clock : _ Eio.Time.clock
173173- ; net : _ Eio.Net.t
174174- ; fs : Eio.Fs.dir_ty Eio.Path.t
175175- ; .. > ->
176169 proc:_ Eio.Process.mgr ->
177170 fs:Eio.Fs.dir_ty Eio.Path.t ->
178171 config:Config.t ->
179179- xdg:Xdge.t ->
180172 ?package:string ->
181173 ?remote:bool ->
182174 ?skip_push:bool ->
183175 ?skip_pull:bool ->
184184- ?skip_verse:bool ->
185176 unit ->
186177 (sync_summary, error) result
187187-(** [sync ~sw ~env ~proc ~fs ~config ~xdg ?package ?remote ?skip_push ?skip_pull
188188- ?skip_verse ()] synchronizes the monorepo with upstream repositories.
178178+(** [sync ~proc ~fs ~config ?package ?remote ?skip_push ?skip_pull ()]
179179+ synchronizes the monorepo with upstream repositories.
189180190181 This is the primary command for all sync operations. It performs both push
191182 and pull operations in the correct order: 1. Validate: check for dirty state
···230221(** [sync_opam_files ~proc ~fs ~config ?package ()] generates opam-repo entries
231222 from monorepo dune-project files.
232223233233- For each subtree directory in the monorepo: 1. Parses the dune-project to
234234- extract source/homepage URL 2. For each .opam file in the subtree:
235235- - Transforms it by removing dune-generated comment
236236- - Adds dev-repo and url fields derived from dune-project
237237- - Writes to opam-repo/packages/<name>/<name>.dev/opam 3. Deletes any
238238- orphaned packages in opam-repo not found in monorepo 4. Stages and commits
239239- changes in opam-repo
224224+ For each subtree directory in the monorepo:
225225+ 1. Parses the dune-project to extract source/homepage URL
226226+ 2. For each .opam file in the subtree:
227227+ - Transforms it by removing dune-generated comment
228228+ - Adds dev-repo and url fields derived from dune-project
229229+ - Writes to opam-repo/packages/<name>/<name>.dev/opam
230230+ 3. Deletes any orphaned packages in opam-repo not found in monorepo
231231+ 4. Stages and commits changes in opam-repo
240232241233 This is a generation-based approach - opam-repo is derived entirely from
242234 monorepo dune-project and .opam files.
···320312 @param config Monopam configuration
321313 @param pkgs List of packages discovered from the opam overlay *)
322314315315+(** Information about a package discovered from the monorepo. *)
323316type monorepo_package = {
324317 pkg_name : string; (** Package name (from .opam filename) *)
325318 subtree : string; (** Subtree directory name *)
···327320 url_src : string; (** url src with branch (e.g., "git+https://...#main") *)
328321 opam_content : string; (** Transformed opam file content ready to write *)
329322}
330330-(** Information about a package discovered from the monorepo. *)
331323332324val discover_packages_from_monorepo :
333325 fs:Eio.Fs.dir_ty Eio.Path.t ->
···338330(** [discover_packages_from_monorepo ~fs ~config ?sources ()] scans monorepo
339331 subtrees and discovers packages from dune-project files.
340332341341- For each subdirectory of the monorepo with a dune-project file: 1. Checks
342342- sources.toml for URL override 2. Falls back to dune-project source/homepage
343343- URL 3. For each .opam file in that directory, transforms it with dev-repo
344344- and url
333333+ For each subdirectory of the monorepo with a dune-project file:
334334+ 1. Checks sources.toml for URL override
335335+ 2. Falls back to dune-project source/homepage URL
336336+ 3. For each .opam file in that directory, transforms it with dev-repo and url
345337346338 @param fs Eio filesystem
347339 @param config Monopam configuration
···419411420412(** {1 Diff} *)
421413414414+(** A diff entry for a single repository showing commits from a verse member. *)
422415type diff_entry = {
423416 repo_name : string;
424417 handle : string;
425418 relationship : Forks.relationship;
426426- commits : Git_cli.log_entry list;
419419+ commits : Git.log_entry list;
427420 patches : (string * string) list; (** hash -> patch content *)
428421}
429429-(** A diff entry for a single repository showing commits from a verse member. *)
430422431431-type diff_result = { entries : diff_entry list; forks : Forks.t }
432423(** Result of computing diffs for repos needing attention. *)
424424+type diff_result = {
425425+ entries : diff_entry list;
426426+ forks : Forks.t;
427427+}
433428434429val pp_diff_entry : show_patch:bool -> diff_entry Fmt.t
435435-(** [pp_diff_entry ~show_patch] formats a single diff entry. If [show_patch] is
436436- true, includes the patch content for each commit. *)
430430+(** [pp_diff_entry ~show_patch] formats a single diff entry.
431431+ If [show_patch] is true, includes the patch content for each commit. *)
437432438433val pp_diff_result : show_patch:bool -> diff_result Fmt.t
439434(** [pp_diff_result ~show_patch] formats the full diff result. *)
440435441436val is_commit_sha : string -> bool
442442-(** [is_commit_sha s] returns true if [s] looks like a git commit hash (7+
443443- hexadecimal characters). *)
437437+(** [is_commit_sha s] returns true if [s] looks like a git commit hash
438438+ (7+ hexadecimal characters). *)
444439445440val diff :
446441 proc:_ Eio.Process.mgr ->
···452447 ?patch:bool ->
453448 unit ->
454449 diff_result
455455-(** [diff ~proc ~fs ~config ~verse_config ?repo ?refresh ?patch ()] computes and
456456- displays diffs for repositories that need attention from verse members.
450450+(** [diff ~proc ~fs ~config ~verse_config ?repo ?refresh ?patch ()] computes and displays diffs
451451+ for repositories that need attention from verse members.
457452458453 For each repository where a verse member is ahead (I_am_behind or Diverged),
459454 retrieves the commit log showing what commits they have that you don't.
···467462 @param verse_config Verse configuration
468463 @param repo Optional specific repository to show diff for
469464 @param refresh If true, force fresh fetches ignoring cache (default: false)
470470- @param patch
471471- If true, fetch and include patch content for each commit (default: false)
472472-*)
465465+ @param patch If true, fetch and include patch content for each commit (default: false) *)
473466467467+(** Result of looking up a specific commit *)
474468type commit_info = {
475469 commit_repo : string;
476470 commit_handle : string;
···479473 commit_author : string;
480474 commit_patch : string;
481475}
482482-(** Result of looking up a specific commit *)
483476484477val diff_show_commit :
485478 proc:_ Eio.Process.mgr ->
···490483 ?refresh:bool ->
491484 unit ->
492485 commit_info option
493493-(** [diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?refresh ()] finds
494494- and shows the patch for a specific commit SHA from the diff output.
486486+(** [diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?refresh ()] finds and shows
487487+ the patch for a specific commit SHA from the diff output.
495488496489 Searches through all repos with actionable verse sources to find a commit
497497- matching the given SHA prefix. Returns [Some commit_info] if found, [None]
498498- otherwise.
490490+ matching the given SHA prefix. Returns [Some commit_info] if found, [None] otherwise.
499491500492 @param sha Commit SHA prefix (7+ characters) to look up *)
501493502494(** {1 Pull from Verse Members} *)
503495496496+(** Result of pulling from a handle. *)
504497type handle_pull_result = {
505505- repos_pulled : (string * int) list;
506506- (** (repo_name, commit_count) for each repo pulled *)
507507- repos_skipped : string list;
508508- (** Repos skipped (already in sync or no checkout) *)
509509- repos_failed : (string * string) list;
510510- (** (repo_name, error_message) for failures *)
498498+ repos_pulled : (string * int) list; (** (repo_name, commit_count) for each repo pulled *)
499499+ repos_skipped : string list; (** Repos skipped (already in sync or no checkout) *)
500500+ repos_failed : (string * string) list; (** (repo_name, error_message) for failures *)
511501}
512512-(** Result of pulling from a handle. *)
513502514503val pp_handle_pull_result : handle_pull_result Fmt.t
515504(** [pp_handle_pull_result] formats a pull result. *)
···527516(** [pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo ?refresh ()]
528517 pulls commits from a verse member's forks into your local checkouts.
529518530530- For each repository where the handle has commits you don't have: 1. Merges
531531- their commits into your checkout's main branch 2. The changes are then ready
532532- to be synced to the monorepo via [sync]
519519+ For each repository where the handle has commits you don't have:
520520+ 1. Merges their commits into your checkout's main branch
521521+ 2. The changes are then ready to be synced to the monorepo via [sync]
533522534534- If [repo] is specified, only pulls from that repository. Otherwise, pulls
535535- from all repositories where the handle is ahead.
523523+ If [repo] is specified, only pulls from that repository.
524524+ Otherwise, pulls from all repositories where the handle is ahead.
536525537526 @param handle The verse member handle (e.g., "avsm.bsky.social")
538527 @param repo Optional specific repository to pull from
539539- @param refresh If true, force fresh fetches ignoring cache (default: false)
540540-*)
528528+ @param refresh If true, force fresh fetches ignoring cache (default: false) *)
541529542530(** {1 Cherry-pick} *)
543531532532+(** Result of cherry-picking a commit. *)
544533type cherrypick_result = {
545534 repo_name : string;
546535 commit_hash : string;
547536 commit_subject : string;
548537}
549549-(** Result of cherry-picking a commit. *)
550538551539val pp_cherrypick_result : cherrypick_result Fmt.t
552540(** [pp_cherrypick_result] formats a cherry-pick result. *)
···560548 ?refresh:bool ->
561549 unit ->
562550 (cherrypick_result, error) result
563563-(** [cherrypick ~proc ~fs ~config ~verse_config ~sha ?refresh ()] applies a
564564- specific commit from a verse member's fork to your local checkout.
551551+(** [cherrypick ~proc ~fs ~config ~verse_config ~sha ?refresh ()]
552552+ applies a specific commit from a verse member's fork to your local checkout.
565553566554 Finds the commit in the verse diff output and cherry-picks it into the
567567- appropriate local checkout. The changes are then ready to be synced to the
568568- monorepo via [sync].
555555+ appropriate local checkout. The changes are then ready to be synced to
556556+ the monorepo via [sync].
569557570558 @param sha Commit SHA prefix (7+ characters) to cherry-pick
571571- @param refresh If true, force fresh fetches ignoring cache (default: false)
572572-*)
559559+ @param refresh If true, force fresh fetches ignoring cache (default: false) *)
+9-16
lib/opam_repo.ml
···188188(** Read the raw content of an opam file. *)
189189let read_opam_file ~fs opam_file_path =
190190 let eio_path = Eio.Path.(fs / Fpath.to_string opam_file_path) in
191191- try Ok (Eio.Path.load eio_path)
192192- with Eio.Io _ as e -> Error (Io_error (Printexc.to_string e))
191191+ try Ok (Eio.Path.load eio_path) with Eio.Io _ as e -> Error (Io_error (Printexc.to_string e))
193192194194-(** Replace dev-repo line in content. Looks for 'dev-repo: "..."' and replaces
195195- the URL. *)
193193+(** Replace dev-repo line in content. Looks for 'dev-repo: "..."' and replaces the URL. *)
196194let replace_dev_repo_line content ~new_url =
197195 let lines = String.split_on_char '\n' content in
198196 let dev_repo_url =
···217215 let url_src =
218216 let base =
219217 if String.starts_with ~prefix:"git@" new_url then "git+" ^ new_url
220220- else if String.starts_with ~prefix:"https://" new_url then
221221- "git+" ^ new_url
218218+ else if String.starts_with ~prefix:"https://" new_url then "git+" ^ new_url
222219 else if String.starts_with ~prefix:"git+" new_url then new_url
223220 else "git+" ^ new_url
224221 in
···242239 else
243240 (* Skip this line, it's part of the old url block *)
244241 process rest true acc
245245- else if trimmed = "url {" || String.starts_with ~prefix:"url {" trimmed
246246- then
242242+ else if trimmed = "url {" || String.starts_with ~prefix:"url {" trimmed then
247243 (* Start of url block *)
248244 if String.ends_with ~suffix:"}" trimmed then
249245 (* Single-line url block *)
···256252 in
257253 String.concat "\n" (process lines false [])
258254259259-(** Replace the dev-repo and url fields in an opam file content with a new git
260260- URL. The new URL should be a plain git URL (e.g.,
261261- "git@github.com:user/repo.git"). *)
255255+(** Replace the dev-repo and url fields in an opam file content with a new git URL.
256256+ The new URL should be a plain git URL (e.g., "git@github.com:user/repo.git"). *)
262257let replace_dev_repo_url content ~new_url =
263258 let content = replace_dev_repo_line content ~new_url in
264259 let content = replace_url_section content ~new_url in
265260 content
266261267267-(** Write an opam package to the opam-repo overlay. Creates the directory
268268- structure: packages/<name>/<name.version>/opam *)
262262+(** Write an opam package to the opam-repo overlay.
263263+ Creates the directory structure: packages/<name>/<name.version>/opam *)
269264let write_package ~fs ~repo_path ~name ~version ~content =
270270- let pkg_dir =
271271- Fpath.(repo_path / "packages" / name / (name ^ "." ^ version))
272272- in
265265+ let pkg_dir = Fpath.(repo_path / "packages" / name / (name ^ "." ^ version)) in
273266 let opam_path = Fpath.(pkg_dir / "opam") in
274267 let eio_pkg_dir = Eio.Path.(fs / Fpath.to_string pkg_dir) in
275268 let eio_opam_path = Eio.Path.(fs / Fpath.to_string opam_path) in
+5-8
lib/opam_repo.mli
···9090(** {1 Low-level Opam File Parsing} *)
91919292val find_dev_repo : OpamParserTypes.FullPos.opamfile_item list -> string option
9393-(** [find_dev_repo items] extracts the dev-repo field from parsed opam file
9494- items. *)
9393+(** [find_dev_repo items] extracts the dev-repo field from parsed opam file items. *)
95949695(** {1 Writing Packages} *)
9796···101100val replace_dev_repo_url : string -> new_url:string -> string
102101(** [replace_dev_repo_url content ~new_url] replaces the dev-repo and url fields
103102 in an opam file content with a new git URL. The new URL should be a plain
104104- git URL (e.g., "git@github.com:user/repo.git" or
105105- "https://github.com/user/repo.git"). *)
103103+ git URL (e.g., "git@github.com:user/repo.git" or "https://github.com/user/repo.git"). *)
106104107105val write_package :
108106 fs:_ Eio.Path.t ->
···111109 version:string ->
112110 content:string ->
113111 (unit, error) result
114114-(** [write_package ~fs ~repo_path ~name ~version ~content] writes an opam
115115- package to the opam-repo overlay.
112112+(** [write_package ~fs ~repo_path ~name ~version ~content] writes an opam package
113113+ to the opam-repo overlay.
116114117115 Creates the directory structure: [packages/<name>/<name.version>/opam] *)
118116119117val package_exists : fs:_ Eio.Path.t -> repo_path:Fpath.t -> name:string -> bool
120120-(** [package_exists ~fs ~repo_path ~name] checks if a package exists in the
121121- opam-repo. *)
118118+(** [package_exists ~fs ~repo_path ~name] checks if a package exists in the opam-repo. *)
+5-2
lib/opam_transform.ml
···3232 let trimmed = String.trim line in
3333 if in_url_block then
3434 (* Inside url { ... }, skip until we see } *)
3535- if String.starts_with ~prefix:"}" trimmed then process rest false acc
3535+ if String.starts_with ~prefix:"}" trimmed then
3636+ process rest false acc
3637 else process rest true acc
3738 else if trimmed = "url {" || String.starts_with ~prefix:"url {" trimmed
3839 then
···71727273 (* Step 4: Append dev-repo and url section *)
7374 let dev_repo_line = Printf.sprintf {|dev-repo: "%s"|} dev_repo in
7474- let url_section = Printf.sprintf "url {\n src: \"%s\"\n}" url_src in
7575+ let url_section =
7676+ Printf.sprintf "url {\n src: \"%s\"\n}" url_src
7777+ in
7578 content ^ "\n" ^ dev_repo_line ^ "\n" ^ url_section ^ "\n"
+2-4
lib/opam_transform.mli
···77 - Add url section with source URL and branch *)
8899val transform : content:string -> dev_repo:string -> url_src:string -> string
1010-(** [transform ~content ~dev_repo ~url_src] transforms a dune-generated opam
1111- file.
1010+(** [transform ~content ~dev_repo ~url_src] transforms a dune-generated opam file.
12111312 - Removes the "# This file is generated by dune" comment if present
1413 - Adds or replaces the [dev-repo] field with [dev_repo]
1514 - Adds or replaces the [url { src: "..." }] section with [url_src]
16151716 @param content The original opam file content
1818- @param dev_repo
1919- The dev-repo URL (e.g., "git+https://github.com/user/repo.git")
1717+ @param dev_repo The dev-repo URL (e.g., "git+https://github.com/user/repo.git")
2018 @param url_src The url src URL with branch (e.g., "git+https://...#main") *)
-91
lib/remote_cache.ml
···11-(* Copyright (c) 2024-2026 Thomas Gazagnaire <thomas@gazagnaire.org>
22-33- Permission to use, copy, modify, and distribute this software for any
44- purpose with or without fee is hereby granted, provided that the above
55- copyright notice and this permission notice appear in all copies.
66-77- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
88- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
99- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1010- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1111- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1212- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1313- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *)
1414-1515-(** Remote HEAD cache with O(1) in-memory lookup and optional disk persistence.
1616-1717- Uses a Hashtbl for O(1) amortized lookup by key (url:branch). Entries expire
1818- after a configurable TTL (default 5 minutes).
1919-2020- File format: one entry per line "url:branch hash timestamp" *)
2121-2222-let src = Logs.Src.create "monopam.remote_cache" ~doc:"Remote HEAD cache"
2323-2424-module Log = (val Logs.src_log src : Logs.LOG)
2525-2626-type entry = { hash : string; expires : float }
2727-type t = { tbl : (string, entry) Hashtbl.t; ttl : float; now : unit -> float }
2828-2929-let default_ttl = 300.0 (* 5 minutes *)
3030-let make_key url branch = Fmt.str "%s:%s" (Uri.to_string url) branch
3131-3232-let parse_line ~ttl line =
3333- match String.split_on_char ' ' line with
3434- | [ key; hash; ts ] ->
3535- let ts = float_of_string ts in
3636- Some (key, { hash; expires = ts +. ttl })
3737- | _ -> None
3838-3939-let load_from_string ~ttl content =
4040- let tbl = Hashtbl.create 32 in
4141- String.split_on_char '\n' content
4242- |> List.iter (fun line ->
4343- match parse_line ~ttl line with
4444- | Some (key, entry) -> Hashtbl.replace tbl key entry
4545- | None -> ());
4646- tbl
4747-4848-let to_string t =
4949- let now = t.now () in
5050- let lines =
5151- Hashtbl.fold
5252- (fun key entry acc ->
5353- if entry.expires > now then
5454- let ts = entry.expires -. t.ttl in
5555- Fmt.str "%s %s %.0f" key entry.hash ts :: acc
5656- else acc)
5757- t.tbl []
5858- in
5959- String.concat "\n" lines ^ "\n"
6060-6161-let create ?(ttl = default_ttl) ~now () =
6262- let tbl = Hashtbl.create 32 in
6363- { tbl; ttl; now }
6464-6565-let create_from_string ?(ttl = default_ttl) ~now content =
6666- let tbl = load_from_string ~ttl content in
6767- { tbl; ttl; now }
6868-6969-let get t ~url ~branch =
7070- let key = make_key url branch in
7171- let now = t.now () in
7272- match Hashtbl.find_opt t.tbl key with
7373- | Some entry when entry.expires > now ->
7474- Log.debug (fun m ->
7575- m "Cache hit for %s (expires in %.0fs)" key (entry.expires -. now));
7676- Some entry.hash
7777- | Some entry ->
7878- Log.debug (fun m ->
7979- m "Cache expired for %s (%.0fs ago)" key (now -. entry.expires));
8080- None
8181- | None ->
8282- Log.debug (fun m -> m "Cache miss for %s (not found)" key);
8383- None
8484-8585-let set t ~url ~branch ~hash =
8686- let key = make_key url branch in
8787- let expires = t.now () +. t.ttl in
8888- Hashtbl.replace t.tbl key { hash; expires }
8989-9090-let size t = Hashtbl.length t.tbl
9191-let clear t = Hashtbl.clear t.tbl
-76
lib/remote_cache.mli
···11-(* Copyright (c) 2024-2026 Thomas Gazagnaire <thomas@gazagnaire.org>
22-33- Permission to use, copy, modify, and distribute this software for any
44- purpose with or without fee is hereby granted, provided that the above
55- copyright notice and this permission notice appear in all copies.
66-77- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
88- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
99- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1010- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1111- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1212- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1313- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *)
1414-1515-(** Remote HEAD cache with O(1) in-memory lookup.
1616-1717- This module provides an in-memory cache for remote git HEAD refs with
1818- time-based expiration. Uses a Hashtbl for O(1) amortized lookup.
1919-2020- {2 Example with mock time}
2121-2222- {[
2323- let time = ref 0.0 in
2424- let now () = !time in
2525- let cache = Remote_cache.create ~ttl:60.0 ~now () in
2626-2727- (* Set a value *)
2828- let url = Uri.of_string "https://github.com/ocaml/ocaml.git" in
2929- Remote_cache.set cache ~url ~branch:"trunk" ~hash:"abc123";
3030-3131- (* Get it back immediately *)
3232- assert (Remote_cache.get cache ~url ~branch:"trunk" = Some "abc123");
3333-3434- (* Advance time past TTL *)
3535- time := 61.0;
3636- assert (Remote_cache.get cache ~url ~branch:"trunk" = None)
3737- ]} *)
3838-3939-type t
4040-(** The cache type. *)
4141-4242-val default_ttl : float
4343-(** Default TTL in seconds (300.0 = 5 minutes). *)
4444-4545-val create : ?ttl:float -> now:(unit -> float) -> unit -> t
4646-(** [create ~ttl ~now ()] creates a new empty cache.
4747-4848- @param ttl Time-to-live in seconds (default {!default_ttl})
4949- @param now Function to get current time in seconds *)
5050-5151-val create_from_string : ?ttl:float -> now:(unit -> float) -> string -> t
5252-(** [create_from_string ~ttl ~now content] creates a cache populated from
5353- serialized content.
5454-5555- @param ttl Time-to-live in seconds (default {!default_ttl})
5656- @param now Function to get current time in seconds
5757- @param content Serialized cache content from {!to_string} *)
5858-5959-val get : t -> url:Uri.t -> branch:string -> string option
6060-(** [get t ~url ~branch] returns the cached hash if present and not expired.
6161- O(1) amortized time complexity. *)
6262-6363-val set : t -> url:Uri.t -> branch:string -> hash:string -> unit
6464-(** [set t ~url ~branch ~hash] adds or updates a cache entry. O(1) amortized
6565- time complexity. *)
6666-6767-val to_string : t -> string
6868-(** [to_string t] serializes the cache to a string for disk persistence. Format:
6969- one entry per line "url:branch hash timestamp". Expired entries are not
7070- included. *)
7171-7272-val size : t -> int
7373-(** [size t] returns the number of entries in the cache. *)
7474-7575-val clear : t -> unit
7676-(** [clear t] removes all entries from the cache. *)
+240-361
lib/site.ml
···11(** Generate a static HTML site representing the monoverse map. *)
2233+(** Information about a package in the verse *)
34type pkg_info = {
45 name : string;
56 synopsis : string option;
···89 owners : string list; (** List of handles that have this package *)
910 depends : string list; (** Package dependencies *)
1011}
1111-(** Information about a package in the verse *)
12121313+(** Information about a repository (group of packages) *)
1314type repo_info = {
1415 ri_name : string;
1516 ri_dev_repo : string;
1617 ri_packages : pkg_info list;
1717- ri_owners : string list;
1818- (** All handles that have any package from this repo *)
1919- ri_fork_status : (string * Forks.relationship) list;
2020- (** (handle, relationship) *)
1818+ ri_owners : string list; (** All handles that have any package from this repo *)
1919+ ri_fork_status : (string * Forks.relationship) list; (** (handle, relationship) *)
2120 ri_dep_count : int; (** Number of dependencies (for sorting) *)
2221}
2323-(** Information about a repository (group of packages) *)
24222323+(** Information about a verse member *)
2524type member_info = {
2625 handle : string;
2726 display_name : string; (** Name to display (from registry or handle) *)
···3029 package_count : int;
3130 unique_packages : string list; (** Packages unique to this member *)
3231}
3333-(** Information about a verse member *)
34323333+(** Aggregated site data *)
3534type site_data = {
3635 local_handle : string;
3736 registry_name : string;
···4140 unique_repos : repo_info list; (** Repos unique to one member *)
4241 all_packages : pkg_info list; (** All packages *)
4342}
4444-(** Aggregated site data *)
45434644(** Scan a member's opam repo and return package info *)
4745let scan_member_packages ~fs opam_repo_path =
4846 let pkgs, _errors = Opam_repo.scan_all ~fs opam_repo_path in
4949- List.map
5050- (fun pkg ->
5151- {
5252- name = Package.name pkg;
5353- synopsis = Package.synopsis pkg;
5454- repo_name = Package.repo_name pkg;
5555- dev_repo = Uri.to_string (Package.dev_repo pkg);
5656- owners = [];
5757- depends = Package.depends pkg;
5858- })
5959- pkgs
4747+ List.map (fun pkg ->
4848+ {
4949+ name = Package.name pkg;
5050+ synopsis = Package.synopsis pkg;
5151+ repo_name = Package.repo_name pkg;
5252+ dev_repo = Uri.to_string (Package.dev_repo pkg);
5353+ owners = [];
5454+ depends = Package.depends pkg;
5555+ }
5656+ ) pkgs
60576158(** Check if a directory exists *)
6259let dir_exists ~fs path =
···8077 in
81788279 (* Build a map: package name -> list of (handle, pkg_info) *)
8383- let pkg_map : (string, (string * pkg_info) list) Hashtbl.t =
8484- Hashtbl.create 256
8585- in
8080+ let pkg_map : (string, (string * pkg_info) list) Hashtbl.t = Hashtbl.create 256 in
86818782 (* Add local packages *)
8888- List.iter
8989- (fun pkg ->
9090- let existing = try Hashtbl.find pkg_map pkg.name with Not_found -> [] in
9191- Hashtbl.replace pkg_map pkg.name ((local_handle, pkg) :: existing))
9292- local_pkgs;
8383+ List.iter (fun pkg ->
8484+ let existing = try Hashtbl.find pkg_map pkg.name with Not_found -> [] in
8585+ Hashtbl.replace pkg_map pkg.name ((local_handle, pkg) :: existing)
8686+ ) local_pkgs;
93879488 let registry_name = registry.Verse_registry.name in
9589 let registry_description = registry.Verse_registry.description in
96909791 (* Build handle -> display name lookup *)
9892 let handle_to_name = Hashtbl.create 16 in
9999- List.iter
100100- (fun (m : Verse_registry.member) ->
101101- let display = match m.name with Some n -> n | None -> m.handle in
102102- Hashtbl.replace handle_to_name m.handle display)
103103- registry.Verse_registry.members;
9393+ List.iter (fun (m : Verse_registry.member) ->
9494+ let display = match m.name with Some n -> n | None -> m.handle in
9595+ Hashtbl.replace handle_to_name m.handle display
9696+ ) registry.Verse_registry.members;
1049710598 (* Get tracked handles from verse directory, excluding local handle *)
10699 let tracked_handles =
···109102 try
110103 Eio.Path.read_dir eio_path
111104 |> List.filter (fun name ->
112112- (not (String.ends_with ~suffix:"-opam" name))
113113- && name <> local_handle
114114- && dir_exists ~fs Fpath.(verse_path / name))
105105+ not (String.ends_with ~suffix:"-opam" name) &&
106106+ name <> local_handle &&
107107+ dir_exists ~fs Fpath.(verse_path / name))
115108 with Eio.Io _ -> []
116109 else []
117110 in
118111119112 (* Scan each tracked member's opam repo *)
120113 let member_infos =
121121- List.filter_map
122122- (fun handle ->
123123- let opam_path = Fpath.(verse_path / (handle ^ "-opam")) in
124124- if dir_exists ~fs opam_path then begin
125125- let pkgs = scan_member_packages ~fs opam_path in
126126- (* Add to package map *)
127127- List.iter
128128- (fun pkg ->
129129- let existing =
130130- try Hashtbl.find pkg_map pkg.name with Not_found -> []
131131- in
132132- Hashtbl.replace pkg_map pkg.name ((handle, pkg) :: existing))
133133- pkgs;
134134- (* Look up member in registry for URLs *)
135135- let member = Verse_registry.find_member registry ~handle in
136136- let display_name =
137137- try Hashtbl.find handle_to_name handle with Not_found -> handle
138138- in
139139- Some
140140- {
141141- handle;
142142- display_name;
143143- monorepo_url =
144144- (match member with Some m -> m.monorepo | None -> "");
145145- opam_url = (match member with Some m -> m.opamrepo | None -> "");
146146- package_count = List.length pkgs;
147147- unique_packages = [];
148148- (* Will be filled in later *)
149149- }
150150- end
151151- else None)
152152- tracked_handles
114114+ List.filter_map (fun handle ->
115115+ let opam_path = Fpath.(verse_path / (handle ^ "-opam")) in
116116+ if dir_exists ~fs opam_path then begin
117117+ let pkgs = scan_member_packages ~fs opam_path in
118118+ (* Add to package map *)
119119+ List.iter (fun pkg ->
120120+ let existing = try Hashtbl.find pkg_map pkg.name with Not_found -> [] in
121121+ Hashtbl.replace pkg_map pkg.name ((handle, pkg) :: existing)
122122+ ) pkgs;
123123+ (* Look up member in registry for URLs *)
124124+ let member = Verse_registry.find_member registry ~handle in
125125+ let display_name =
126126+ try Hashtbl.find handle_to_name handle
127127+ with Not_found -> handle
128128+ in
129129+ Some {
130130+ handle;
131131+ display_name;
132132+ monorepo_url = (match member with Some m -> m.monorepo | None -> "");
133133+ opam_url = (match member with Some m -> m.opamrepo | None -> "");
134134+ package_count = List.length pkgs;
135135+ unique_packages = []; (* Will be filled in later *)
136136+ }
137137+ end else None
138138+ ) tracked_handles
153139 in
154140155141 (* Add local member info *)
···171157172158 (* Build final package list with owners *)
173159 let all_packages =
174174- Hashtbl.fold
175175- (fun _name entries acc ->
176176- match entries with
177177- | [] -> acc
178178- | (_, pkg) :: _ as all ->
179179- let owners = List.map fst all in
180180- (* Pick the best synopsis (first non-None) *)
181181- let synopsis = List.find_map (fun (_, p) -> p.synopsis) all in
182182- (* Merge depends from all sources *)
183183- let depends =
184184- List.concat_map (fun (_, p) -> p.depends) all
185185- |> List.sort_uniq String.compare
186186- in
187187- { pkg with owners; synopsis; depends } :: acc)
188188- pkg_map []
160160+ Hashtbl.fold (fun _name entries acc ->
161161+ match entries with
162162+ | [] -> acc
163163+ | (_, pkg) :: _ as all ->
164164+ let owners = List.map fst all in
165165+ (* Pick the best synopsis (first non-None) *)
166166+ let synopsis =
167167+ List.find_map (fun (_, p) -> p.synopsis) all
168168+ in
169169+ (* Merge depends from all sources *)
170170+ let depends =
171171+ List.concat_map (fun (_, p) -> p.depends) all
172172+ |> List.sort_uniq String.compare
173173+ in
174174+ { pkg with owners; synopsis; depends } :: acc
175175+ ) pkg_map []
189176 |> List.sort (fun a b -> String.compare a.name b.name)
190177 in
191178192179 (* Build set of all package names for dependency counting *)
193180 let all_pkg_names =
194194- List.fold_left
195195- (fun s p ->
196196- Hashtbl.replace s p.name ();
197197- s)
181181+ List.fold_left (fun s p -> Hashtbl.replace s p.name (); s)
198182 (Hashtbl.create 256) all_packages
199183 in
200184201185 (* Group packages by repo *)
202186 let repos_map : (string, pkg_info list) Hashtbl.t = Hashtbl.create 64 in
203203- List.iter
204204- (fun (pkg : pkg_info) ->
205205- let existing =
206206- try Hashtbl.find repos_map pkg.repo_name with Not_found -> []
207207- in
208208- Hashtbl.replace repos_map pkg.repo_name (pkg :: existing))
209209- all_packages;
187187+ List.iter (fun (pkg : pkg_info) ->
188188+ let existing = try Hashtbl.find repos_map pkg.repo_name with Not_found -> [] in
189189+ Hashtbl.replace repos_map pkg.repo_name (pkg :: existing)
190190+ ) all_packages;
210191211192 (* Build forks status lookup from forks data if provided *)
212212- let forks_by_repo : (string, (string * Forks.relationship) list) Hashtbl.t =
213213- Hashtbl.create 64
214214- in
193193+ let forks_by_repo : (string, (string * Forks.relationship) list) Hashtbl.t = Hashtbl.create 64 in
215194 (match forks with
216216- | Some f ->
217217- List.iter
218218- (fun (ra : Forks.repo_analysis) ->
219219- let statuses =
220220- List.map (fun (h, _src, rel) -> (h, rel)) ra.verse_sources
221221- in
222222- Hashtbl.replace forks_by_repo ra.repo_name statuses)
223223- f.Forks.repos
224224- | None -> ());
195195+ | Some f ->
196196+ List.iter (fun (ra : Forks.repo_analysis) ->
197197+ let statuses = List.map (fun (h, _src, rel) -> (h, rel)) ra.verse_sources in
198198+ Hashtbl.replace forks_by_repo ra.repo_name statuses
199199+ ) f.Forks.repos
200200+ | None -> ());
225201226202 (* Build repo_info list with dependency counts *)
227203 let all_repos =
228228- Hashtbl.fold
229229- (fun repo_name pkgs acc ->
230230- let dev_repo = (List.hd pkgs).dev_repo in
231231- let owners =
232232- List.sort_uniq String.compare
233233- (List.concat_map (fun (p : pkg_info) -> p.owners) pkgs)
234234- in
235235- let fork_status =
236236- try Hashtbl.find forks_by_repo repo_name with Not_found -> []
237237- in
238238- (* Count dependencies that are in our package set *)
239239- let dep_count =
240240- List.concat_map (fun (p : pkg_info) -> p.depends) pkgs
241241- |> List.filter (fun d -> Hashtbl.mem all_pkg_names d)
242242- |> List.sort_uniq String.compare
243243- |> List.length
244244- in
245245- {
246246- ri_name = repo_name;
247247- ri_dev_repo = dev_repo;
248248- ri_packages = List.sort (fun a b -> String.compare a.name b.name) pkgs;
249249- ri_owners = owners;
250250- ri_fork_status = fork_status;
251251- ri_dep_count = dep_count;
252252- }
253253- :: acc)
254254- repos_map []
204204+ Hashtbl.fold (fun repo_name pkgs acc ->
205205+ let dev_repo = (List.hd pkgs).dev_repo in
206206+ let owners =
207207+ List.sort_uniq String.compare (List.concat_map (fun (p : pkg_info) -> p.owners) pkgs)
208208+ in
209209+ let fork_status =
210210+ try Hashtbl.find forks_by_repo repo_name with Not_found -> []
211211+ in
212212+ (* Count dependencies that are in our package set *)
213213+ let dep_count =
214214+ List.concat_map (fun (p : pkg_info) -> p.depends) pkgs
215215+ |> List.filter (fun d -> Hashtbl.mem all_pkg_names d)
216216+ |> List.sort_uniq String.compare
217217+ |> List.length
218218+ in
219219+ { ri_name = repo_name;
220220+ ri_dev_repo = dev_repo;
221221+ ri_packages = List.sort (fun a b -> String.compare a.name b.name) pkgs;
222222+ ri_owners = owners;
223223+ ri_fork_status = fork_status;
224224+ ri_dep_count = dep_count } :: acc
225225+ ) repos_map []
255226 (* Sort by dependency count descending (apps with most deps first), then by name *)
256227 |> List.sort (fun a b ->
257228 let cmp = compare b.ri_dep_count a.ri_dep_count in
···259230 in
260231261232 (* Separate common and unique repos *)
262262- let common_repos =
263263- List.filter (fun r -> List.length r.ri_owners > 1) all_repos
264264- in
265265- let unique_repos =
266266- List.filter (fun r -> List.length r.ri_owners = 1) all_repos
267267- in
233233+ let common_repos = List.filter (fun r -> List.length r.ri_owners > 1) all_repos in
234234+ let unique_repos = List.filter (fun r -> List.length r.ri_owners = 1) all_repos in
268235269236 (* Compute unique packages per member *)
270237 let unique_by_handle = Hashtbl.create 32 in
271271- List.iter
272272- (fun (pkg : pkg_info) ->
273273- if List.length pkg.owners = 1 then begin
274274- let handle = List.hd pkg.owners in
275275- let existing =
276276- try Hashtbl.find unique_by_handle handle with Not_found -> []
277277- in
278278- Hashtbl.replace unique_by_handle handle (pkg.name :: existing)
279279- end)
280280- all_packages;
238238+ List.iter (fun (pkg : pkg_info) ->
239239+ if List.length pkg.owners = 1 then begin
240240+ let handle = List.hd pkg.owners in
241241+ let existing = try Hashtbl.find unique_by_handle handle with Not_found -> [] in
242242+ Hashtbl.replace unique_by_handle handle (pkg.name :: existing)
243243+ end
244244+ ) all_packages;
281245282246 (* Update member infos with unique packages *)
283247 let update_member m =
284284- let unique =
285285- try Hashtbl.find unique_by_handle m.handle with Not_found -> []
286286- in
248248+ let unique = try Hashtbl.find unique_by_handle m.handle with Not_found -> [] in
287249 { m with unique_packages = List.sort String.compare unique }
288250 in
289251290252 let all_members = local_member :: member_infos in
291253 let members = List.map update_member all_members in
292254293293- {
294294- local_handle;
295295- registry_name;
296296- registry_description;
297297- members;
298298- common_repos;
299299- unique_repos;
300300- all_packages;
301301- }
255255+ { local_handle; registry_name; registry_description; members; common_repos; unique_repos; all_packages }
302256303257(** Escape HTML special characters *)
304258let html_escape s =
305259 let buf = Buffer.create (String.length s) in
306306- String.iter
307307- (function
308308- | '<' -> Buffer.add_string buf "<"
309309- | '>' -> Buffer.add_string buf ">"
310310- | '&' -> Buffer.add_string buf "&"
311311- | '"' -> Buffer.add_string buf """
312312- | c -> Buffer.add_char buf c)
313313- s;
260260+ String.iter (function
261261+ | '<' -> Buffer.add_string buf "<"
262262+ | '>' -> Buffer.add_string buf ">"
263263+ | '&' -> Buffer.add_string buf "&"
264264+ | '"' -> Buffer.add_string buf """
265265+ | c -> Buffer.add_char buf c
266266+ ) s;
314267 Buffer.contents buf
315268316269(** External link SVG icon *)
···323276 | Forks.Same_commit -> "sync"
324277 | Forks.I_am_ahead n -> Printf.sprintf "+%d" n
325278 | Forks.I_am_behind n -> Printf.sprintf "-%d" n
326326- | Forks.Diverged { my_ahead; their_ahead; _ } ->
327327- Printf.sprintf "+%d/-%d" my_ahead their_ahead
279279+ | Forks.Diverged { my_ahead; their_ahead; _ } -> Printf.sprintf "+%d/-%d" my_ahead their_ahead
328280 | Forks.Unrelated -> "unrel"
329281 | Forks.Not_fetched -> "?"
330282···336288 (* Build member lookups *)
337289 let member_urls = Hashtbl.create 16 in
338290 let member_names = Hashtbl.create 16 in
339339- List.iter
340340- (fun m ->
341341- Hashtbl.replace member_urls m.handle (m.monorepo_url, m.opam_url);
342342- Hashtbl.replace member_names m.handle m.display_name)
343343- data.members;
291291+ List.iter (fun m ->
292292+ Hashtbl.replace member_urls m.handle (m.monorepo_url, m.opam_url);
293293+ Hashtbl.replace member_names m.handle m.display_name
294294+ ) data.members;
344295345296 (* Helper to get display name for handle *)
346297 let get_name handle =
347298 try Hashtbl.find member_names handle with Not_found -> handle
348299 in
349300350350- add
351351- {|<!DOCTYPE html>
301301+ add {|<!DOCTYPE html>
352302<html lang="en">
353303<head>
354304<meta charset="UTF-8">
355305<meta name="viewport" content="width=device-width, initial-scale=1.0">
356306<title>|};
357307 add (html_escape data.registry_name);
358358- add
359359- {|</title>
308308+ add {|</title>
360309<style>
361310* { margin: 0; padding: 0; box-sizing: border-box; }
362311body { font: 10pt/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #333; max-width: 900px; margin: 0 auto; padding: 12px; }
···416365 (* Title and description *)
417366 add (Printf.sprintf "<h1>%s</h1>\n" (html_escape data.registry_name));
418367 (match data.registry_description with
419419- | Some desc ->
420420- add
421421- (Printf.sprintf "<div class=\"subtitle\">%s</div>\n" (html_escape desc))
422422- | None -> add "<div class=\"subtitle\"></div>\n");
368368+ | Some desc -> add (Printf.sprintf "<div class=\"subtitle\">%s</div>\n" (html_escape desc))
369369+ | None -> add "<div class=\"subtitle\"></div>\n");
423370424371 (* Intro section *)
425425- add
426426- {|<div class="intro">
372372+ add {|<div class="intro">
427373This is an experiment in large-scale agentic coding using OCaml and OxCaml, where we're building environments to exchange vibe code at scale.
428428-Managed by <a class="ext" href="https://tangled.org/anil.recoil.org/monopam">monopam|};
429429- add external_link_icon;
430430- add
431431- {|</a>,
432432-with the central registry at <a class="ext" href="https://tangled.org/eeg.cl.cam.ac.uk/opamverse">opamverse|};
433433- add external_link_icon;
434434- add {|</a>.
374374+Managed by <a class="ext" href="https://tangled.org/anil.recoil.org/monopam">monopam|}; add external_link_icon; add {|</a>,
375375+with the central registry at <a class="ext" href="https://tangled.org/eeg.cl.cam.ac.uk/opamverse">opamverse|}; add external_link_icon; add {|</a>.
435376</div>
436377|};
437378438379 (* Members section *)
439380 add "<div class=\"section\">\n<h2>Members</h2>\n<div class=\"members\">\n";
440440- List.iter
441441- (fun m ->
442442- add "<div class=\"member\">\n";
443443- add
444444- (Printf.sprintf
445445- "<div class=\"member-name\"><a href=\"https://%s\">%s</a></div>\n"
446446- (html_escape m.handle)
447447- (html_escape m.display_name));
448448- if m.display_name <> m.handle then
449449- add
450450- (Printf.sprintf "<div class=\"member-handle\">%s</div>\n"
451451- (html_escape m.handle));
452452- add
453453- (Printf.sprintf "<div class=\"member-stats\">%d packages"
454454- m.package_count);
455455- if m.unique_packages <> [] then
456456- add (Printf.sprintf ", %d unique" (List.length m.unique_packages));
457457- add "</div>\n";
458458- if m.monorepo_url <> "" || m.opam_url <> "" then begin
459459- add "<div class=\"member-links\">";
460460- if m.monorepo_url <> "" then
461461- add
462462- (Printf.sprintf "<a class=\"ext\" href=\"%s\">mono%s</a>"
463463- (html_escape m.monorepo_url)
464464- external_link_icon);
465465- if m.opam_url <> "" then
466466- add
467467- (Printf.sprintf "<a class=\"ext\" href=\"%s\">opam%s</a>"
468468- (html_escape m.opam_url) external_link_icon);
469469- add "</div>\n"
470470- end;
471471- add "</div>\n")
472472- data.members;
381381+ List.iter (fun m ->
382382+ add "<div class=\"member\">\n";
383383+ add (Printf.sprintf "<div class=\"member-name\"><a href=\"https://%s\">%s</a></div>\n"
384384+ (html_escape m.handle) (html_escape m.display_name));
385385+ if m.display_name <> m.handle then
386386+ add (Printf.sprintf "<div class=\"member-handle\">%s</div>\n" (html_escape m.handle));
387387+ add (Printf.sprintf "<div class=\"member-stats\">%d packages" m.package_count);
388388+ if m.unique_packages <> [] then
389389+ add (Printf.sprintf ", %d unique" (List.length m.unique_packages));
390390+ add "</div>\n";
391391+ if m.monorepo_url <> "" || m.opam_url <> "" then begin
392392+ add "<div class=\"member-links\">";
393393+ if m.monorepo_url <> "" then
394394+ add (Printf.sprintf "<a class=\"ext\" href=\"%s\">mono%s</a>" (html_escape m.monorepo_url) external_link_icon);
395395+ if m.opam_url <> "" then
396396+ add (Printf.sprintf "<a class=\"ext\" href=\"%s\">opam%s</a>" (html_escape m.opam_url) external_link_icon);
397397+ add "</div>\n"
398398+ end;
399399+ add "</div>\n"
400400+ ) data.members;
473401 add "</div>\n</div>\n";
474402475403 (* Summary section *)
476404 add "<div class=\"section\">\n";
477405 add "<div class=\"summary\">\n";
478478- add
479479- (Printf.sprintf
480480- "<div class=\"summary-title\">Common Libraries (%d repos, %d \
481481- packages)</div>\n"
482482- (List.length data.common_repos)
483483- (List.fold_left
484484- (fun acc r -> acc + List.length r.ri_packages)
485485- 0 data.common_repos));
406406+ add (Printf.sprintf "<div class=\"summary-title\">Common Libraries (%d repos, %d packages)</div>\n"
407407+ (List.length data.common_repos)
408408+ (List.fold_left (fun acc r -> acc + List.length r.ri_packages) 0 data.common_repos));
486409 add "<div class=\"summary-list\">\n";
487487- List.iter
488488- (fun r ->
489489- add
490490- (Printf.sprintf
491491- "<span class=\"summary-item\"><a href=\"#%s\">%s</a> <span \
492492- style=\"color:#888\">(%d)</span></span>\n"
493493- (html_escape r.ri_name) (html_escape r.ri_name)
494494- (List.length r.ri_packages)))
495495- data.common_repos;
410410+ List.iter (fun r ->
411411+ add (Printf.sprintf "<span class=\"summary-item\"><a href=\"#%s\">%s</a> <span style=\"color:#888\">(%d)</span></span>\n"
412412+ (html_escape r.ri_name) (html_escape r.ri_name) (List.length r.ri_packages))
413413+ ) data.common_repos;
496414 add "</div>\n</div>\n";
497415498416 (* Member-specific summary *)
499499- let members_with_unique =
500500- List.filter (fun m -> m.unique_packages <> []) data.members
501501- in
417417+ let members_with_unique = List.filter (fun m -> m.unique_packages <> []) data.members in
502418 if members_with_unique <> [] then begin
503419 add "<div class=\"summary\">\n";
504420 add "<div class=\"summary-title\">Member-Specific Packages</div>\n";
505421 add "<div class=\"unique-section\">\n";
506506- List.iter
507507- (fun m ->
508508- add "<div class=\"unique-member\">\n";
509509- add
510510- (Printf.sprintf
511511- "<span class=\"unique-member-name\"><a \
512512- href=\"https://%s\">%s</a>:</span> "
513513- (html_escape m.handle)
514514- (html_escape m.display_name));
515515- add "<span class=\"unique-list\">";
516516- add (String.concat ", " (List.map html_escape m.unique_packages));
517517- add "</span>\n";
518518- add "</div>\n")
519519- members_with_unique;
422422+ List.iter (fun m ->
423423+ add "<div class=\"unique-member\">\n";
424424+ add (Printf.sprintf "<span class=\"unique-member-name\"><a href=\"https://%s\">%s</a>:</span> "
425425+ (html_escape m.handle) (html_escape m.display_name));
426426+ add "<span class=\"unique-list\">";
427427+ add (String.concat ", " (List.map html_escape m.unique_packages));
428428+ add "</span>\n";
429429+ add "</div>\n"
430430+ ) members_with_unique;
520431 add "</div>\n</div>\n"
521432 end;
522433 add "</div>\n";
···525436 if data.common_repos <> [] then begin
526437 add "<div class=\"section\">\n<h2>Repository Details</h2>\n";
527438528528- List.iter
529529- (fun r ->
530530- add
531531- (Printf.sprintf "<div class=\"repo\" id=\"%s\">\n"
532532- (html_escape r.ri_name));
533533- add "<div class=\"repo-header\">";
534534- add
535535- (Printf.sprintf
536536- "<span class=\"repo-name\"><a class=\"ext\" \
537537- href=\"%s\">%s%s</a></span>"
538538- (html_escape r.ri_dev_repo)
539539- (html_escape r.ri_name) external_link_icon);
540540- add "</div>\n";
439439+ List.iter (fun r ->
440440+ add (Printf.sprintf "<div class=\"repo\" id=\"%s\">\n" (html_escape r.ri_name));
441441+ add "<div class=\"repo-header\">";
442442+ add (Printf.sprintf "<span class=\"repo-name\"><a class=\"ext\" href=\"%s\">%s%s</a></span>"
443443+ (html_escape r.ri_dev_repo) (html_escape r.ri_name) external_link_icon);
444444+ add "</div>\n";
541445542542- (* Packages list - compact with names *)
543543- add "<div class=\"repo-packages\">";
544544- let pkg_names = List.map (fun (p : pkg_info) -> p.name) r.ri_packages in
545545- add (String.concat ", " (List.map html_escape pkg_names));
546546- add "</div>\n";
446446+ (* Packages list - compact with names *)
447447+ add "<div class=\"repo-packages\">";
448448+ let pkg_names = List.map (fun (p : pkg_info) -> p.name) r.ri_packages in
449449+ add (String.concat ", " (List.map html_escape pkg_names));
450450+ add "</div>\n";
547451548548- (* Package descriptions as bullet list *)
549549- let pkg_descs =
550550- List.filter_map
551551- (fun (p : pkg_info) ->
552552- match p.synopsis with Some s -> Some (p.name, s) | None -> None)
553553- r.ri_packages
554554- in
555555- if pkg_descs <> [] then begin
556556- add "<ul class=\"pkg-list\">\n";
557557- List.iter
558558- (fun (name, desc) ->
559559- add
560560- (Printf.sprintf "<li><b>%s</b>: %s</li>\n" (html_escape name)
561561- (html_escape desc)))
562562- pkg_descs;
563563- add "</ul>\n"
564564- end;
452452+ (* Package descriptions as bullet list *)
453453+ let pkg_descs = List.filter_map (fun (p : pkg_info) ->
454454+ match p.synopsis with
455455+ | Some s -> Some (p.name, s)
456456+ | None -> None
457457+ ) r.ri_packages in
458458+ if pkg_descs <> [] then begin
459459+ add "<ul class=\"pkg-list\">\n";
460460+ List.iter (fun (name, desc) ->
461461+ add (Printf.sprintf "<li><b>%s</b>: %s</li>\n" (html_escape name) (html_escape desc))
462462+ ) pkg_descs;
463463+ add "</ul>\n"
464464+ end;
565465566566- (* Forks - at repo level with names *)
567567- if List.length r.ri_owners > 1 then begin
568568- let owner_links =
569569- List.map
570570- (fun h ->
571571- Printf.sprintf "<a href=\"https://%s\">%s</a>" (html_escape h)
572572- (html_escape (get_name h)))
573573- (List.sort String.compare r.ri_owners)
466466+ (* Forks - at repo level with names *)
467467+ if List.length r.ri_owners > 1 then begin
468468+ let owner_links = List.map (fun h ->
469469+ Printf.sprintf "<a href=\"https://%s\">%s</a>" (html_escape h) (html_escape (get_name h))
470470+ ) (List.sort String.compare r.ri_owners) in
471471+ add "<details class=\"repo-forks\">\n";
472472+ add (Printf.sprintf "<summary>%d members (%s)</summary>\n"
473473+ (List.length r.ri_owners)
474474+ (String.concat ", " owner_links));
475475+ add "<div class=\"fork-list\">\n";
476476+ List.iter (fun handle ->
477477+ let mono_url, _opam_url =
478478+ try Hashtbl.find member_urls handle
479479+ with Not_found -> ("", "")
574480 in
575575- add "<details class=\"repo-forks\">\n";
576576- add
577577- (Printf.sprintf "<summary>%d members (%s)</summary>\n"
578578- (List.length r.ri_owners)
579579- (String.concat ", " owner_links));
580580- add "<div class=\"fork-list\">\n";
581581- List.iter
582582- (fun handle ->
583583- let mono_url, _opam_url =
584584- try Hashtbl.find member_urls handle with Not_found -> ("", "")
585585- in
586586- add "<span class=\"fork-item\">";
587587- add
588588- (Printf.sprintf "<a href=\"https://%s\">%s</a>"
589589- (html_escape handle)
590590- (html_escape (get_name handle)));
591591- (* Add status if available *)
592592- (match List.assoc_opt handle r.ri_fork_status with
593593- | Some rel ->
594594- let status_str = format_relationship rel in
595595- let status_class =
596596- match rel with
597597- | Forks.Same_url | Forks.Same_commit -> "sync"
598598- | Forks.I_am_ahead _ -> "ahead"
599599- | Forks.I_am_behind _ -> "behind"
600600- | Forks.Diverged _ -> "diverged"
601601- | _ -> ""
602602- in
603603- if status_class <> "" then
604604- add
605605- (Printf.sprintf "<span class=\"fork-status %s\">%s</span>"
606606- status_class status_str)
607607- else
608608- add
609609- (Printf.sprintf "<span class=\"fork-status\">%s</span>"
610610- status_str)
611611- | None -> ());
612612- if mono_url <> "" then
613613- add
614614- (Printf.sprintf "<a class=\"ext\" href=\"%s/%s\">mono%s</a>"
615615- (html_escape mono_url) (html_escape r.ri_name)
616616- external_link_icon);
617617- add "</span>\n")
618618- (List.sort String.compare r.ri_owners);
619619- add "</div>\n</details>\n"
620620- end;
481481+ add "<span class=\"fork-item\">";
482482+ add (Printf.sprintf "<a href=\"https://%s\">%s</a>" (html_escape handle) (html_escape (get_name handle)));
483483+ (* Add status if available *)
484484+ (match List.assoc_opt handle r.ri_fork_status with
485485+ | Some rel ->
486486+ let status_str = format_relationship rel in
487487+ let status_class =
488488+ match rel with
489489+ | Forks.Same_url | Forks.Same_commit -> "sync"
490490+ | Forks.I_am_ahead _ -> "ahead"
491491+ | Forks.I_am_behind _ -> "behind"
492492+ | Forks.Diverged _ -> "diverged"
493493+ | _ -> ""
494494+ in
495495+ if status_class <> "" then
496496+ add (Printf.sprintf "<span class=\"fork-status %s\">%s</span>" status_class status_str)
497497+ else
498498+ add (Printf.sprintf "<span class=\"fork-status\">%s</span>" status_str)
499499+ | None -> ());
500500+ if mono_url <> "" then
501501+ add (Printf.sprintf "<a class=\"ext\" href=\"%s/%s\">mono%s</a>"
502502+ (html_escape mono_url) (html_escape r.ri_name) external_link_icon);
503503+ add "</span>\n"
504504+ ) (List.sort String.compare r.ri_owners);
505505+ add "</div>\n</details>\n"
506506+ end;
621507622622- add "</div>\n")
623623- data.common_repos;
508508+ add "</div>\n"
509509+ ) data.common_repos;
624510625511 add "</div>\n"
626512 end;
···628514 (* Footer with generation date *)
629515 let now = Unix.gettimeofday () in
630516 let tm = Unix.gmtime now in
631631- let date_str =
632632- Printf.sprintf "%04d-%02d-%02d" (tm.Unix.tm_year + 1900)
633633- (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
634634- in
635635- add
636636- (Printf.sprintf
637637- "<footer>Generated by monopam on %s | %d members | %d repos | %d \
638638- packages</footer>\n"
639639- date_str (List.length data.members)
640640- (List.length data.common_repos + List.length data.unique_repos)
641641- (List.length data.all_packages));
517517+ let date_str = Printf.sprintf "%04d-%02d-%02d"
518518+ (tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday in
519519+ add (Printf.sprintf "<footer>Generated by monopam on %s | %d members | %d repos | %d packages</footer>\n"
520520+ date_str (List.length data.members) (List.length data.common_repos + List.length data.unique_repos) (List.length data.all_packages));
642521643522 add "</body>\n</html>\n";
644523 Buffer.contents buf
+11-14
lib/site.mli
···7788(** {1 Types} *)
991010+(** Information about a package in the verse *)
1011type pkg_info = {
1112 name : string;
1213 synopsis : string option;
···1516 owners : string list; (** List of handles that have this package *)
1617 depends : string list; (** Package dependencies *)
1718}
1818-(** Information about a package in the verse *)
19192020+(** Information about a repository (group of packages) *)
2021type repo_info = {
2122 ri_name : string;
2223 ri_dev_repo : string;
2324 ri_packages : pkg_info list;
2424- ri_owners : string list;
2525- (** All handles that have any package from this repo *)
2626- ri_fork_status : (string * Forks.relationship) list;
2727- (** (handle, relationship) *)
2525+ ri_owners : string list; (** All handles that have any package from this repo *)
2626+ ri_fork_status : (string * Forks.relationship) list; (** (handle, relationship) *)
2827 ri_dep_count : int; (** Number of dependencies (for sorting) *)
2928}
3030-(** Information about a repository (group of packages) *)
31293030+(** Information about a verse member *)
3231type member_info = {
3332 handle : string;
3433 display_name : string; (** Name to display (from registry or handle) *)
···3736 package_count : int;
3837 unique_packages : string list; (** Packages unique to this member *)
3938}
4040-(** Information about a verse member *)
41394040+(** Aggregated site data *)
4241type site_data = {
4342 local_handle : string;
4443 registry_name : string;
···4847 unique_repos : repo_info list; (** Repos unique to one member *)
4948 all_packages : pkg_info list; (** All packages *)
5049}
5151-(** Aggregated site data *)
52505351(** {1 Generation} *)
5452···5957 registry:Verse_registry.t ->
6058 unit ->
6159 site_data
6262-(** [collect_data ~fs ~config ?forks ~registry ()] scans the workspace and verse
6363- members to collect package information for the site. If [forks] is provided,
6060+(** [collect_data ~fs ~config ?forks ~registry ()] scans the workspace and verse members
6161+ to collect package information for the site. If [forks] is provided,
6462 includes fork status information for each repository. *)
65636664val generate :
···7068 registry:Verse_registry.t ->
7169 unit ->
7270 string
7373-(** [generate ~fs ~config ?forks ~registry ()] generates the HTML content for
7474- the site. *)
7171+(** [generate ~fs ~config ?forks ~registry ()] generates the HTML content for the site. *)
75727673val write :
7774 fs:Eio.Fs.dir_ty Eio.Path.t ->
···8178 output_path:Fpath.t ->
8279 unit ->
8380 (unit, string) result
8484-(** [write ~fs ~config ?forks ~registry ~output_path ()] generates and writes
8585- the site to the specified output path. *)
8181+(** [write ~fs ~config ?forks ~registry ~output_path ()] generates and writes the site
8282+ to the specified output path. *)
+27-23
lib/sources_registry.ml
···1010 origin : origin option;
1111}
12121313-type t = { default_url_base : string option; entries : (string * entry) list }
1313+type t = {
1414+ default_url_base : string option;
1515+ entries : (string * entry) list;
1616+}
14171518let empty = { default_url_base = None; entries = [] }
1919+1620let default_url_base t = t.default_url_base
1717-let with_default_url_base t base = { t with default_url_base = Some base }
2121+2222+let with_default_url_base t base =
2323+ { t with default_url_base = Some base }
2424+1825let find t ~subtree = List.assoc_opt subtree t.entries
19262027let derive_url t ~subtree =
···2229 | Some entry -> Some entry.url
2330 | None ->
2431 (* Use default_url_base to construct URL from subtree name *)
2525- Option.map
2626- (fun base ->
2727- let base =
2828- if String.ends_with ~suffix:"/" base then
2929- String.sub base 0 (String.length base - 1)
3030- else base
3131- in
3232- base ^ "/" ^ subtree)
3333- t.default_url_base
3232+ Option.map (fun base ->
3333+ let base =
3434+ if String.ends_with ~suffix:"/" base then
3535+ String.sub base 0 (String.length base - 1)
3636+ else base
3737+ in
3838+ base ^ "/" ^ subtree
3939+ ) t.default_url_base
34403541let add t ~subtree entry =
3642 { t with entries = (subtree, entry) :: List.remove_assoc subtree t.entries }
37433838-let remove t ~subtree = { t with entries = List.remove_assoc subtree t.entries }
4444+let remove t ~subtree =
4545+ { t with entries = List.remove_assoc subtree t.entries }
4646+3947let to_list t = t.entries
4848+4049let of_list entries = { default_url_base = None; entries }
41504251(* TOML structure:
···5766 ~dec:(function
5867 | "fork" -> Fork
5968 | "join" -> Join
6060- | s ->
6161- failwith
6262- (Printf.sprintf "Invalid origin: %s (expected 'fork' or 'join')" s))
6969+ | s -> failwith (Printf.sprintf "Invalid origin: %s (expected 'fork' or 'join')" s))
6370 ~enc:(function Fork -> "fork" | Join -> "join")
6471 Tomlt.string
65726673let entry_codec : entry Tomlt.t =
6774 Tomlt.(
6875 Table.(
6969- obj (fun url upstream branch reason origin ->
7070- { url; upstream; branch; reason; origin })
7676+ obj (fun url upstream branch reason origin -> { url; upstream; branch; reason; origin })
7177 |> mem "url" string ~enc:(fun e -> e.url)
7278 |> opt_mem "upstream" string ~enc:(fun e -> e.upstream)
7379 |> opt_mem "branch" string ~enc:(fun e -> e.branch)
···7884let codec : t Tomlt.t =
7985 Tomlt.(
8086 Table.(
8181- obj (fun default_url_base entries -> { default_url_base; entries })
8787+ obj (fun default_url_base entries ->
8888+ { default_url_base; entries })
8289 |> opt_mem "default_url_base" string ~enc:(fun t -> t.default_url_base)
8390 |> keep_unknown ~enc:(fun t -> t.entries) (Mems.assoc entry_codec)
8491 |> finish))
···9198 | `Regular_file -> (
9299 try Ok (Tomlt_eio.decode_path_exn codec ~fs path_str) with
93100 | Failure msg -> Error (Printf.sprintf "Invalid sources.toml: %s" msg)
9494- | exn ->
9595- Error
9696- (Printf.sprintf "Error loading sources.toml: %s"
9797- (Printexc.to_string exn)))
9898- | _ -> Ok empty (* File doesn't exist, return empty registry *)
101101+ | exn -> Error (Printf.sprintf "Error loading sources.toml: %s" (Printexc.to_string exn)))
102102+ | _ -> Ok empty (* File doesn't exist, return empty registry *)
99103 | exception _ -> Ok empty
100104101105let save ~fs path t =
+13-15
lib/sources_registry.mli
···11(** Sources registry for tracking forked/vendored package URLs.
2233- The sources.toml file in the monorepo root tracks packages where the
44- dev-repo URL differs from what's declared in dune-project. This is typically
55- used for:
33+ The sources.toml file in the monorepo root tracks packages where
44+ the dev-repo URL differs from what's declared in dune-project.
55+ This is typically used for:
66 - Forked packages (our fork URL vs upstream)
77 - Vendored packages (local copy, custom URL)
88 - Packages without source in dune-project
991010- The registry also supports a [default_url_base] field that is used to derive
1111- URLs for subtrees without explicit entries:
1010+ The registry also supports a [default_url_base] field that is used
1111+ to derive URLs for subtrees without explicit entries:
1212 {v
1313 default_url_base = "git+https://tangled.org/anil.recoil.org"
1414 v}
···1818(** How a source entry was created. *)
1919type origin =
2020 | Fork (** Created via [monopam fork] - subtree split from monorepo *)
2121- | Join
2222- (** Created via [monopam join] - external repo brought into monorepo *)
2121+ | Join (** Created via [monopam join] - external repo brought into monorepo *)
23222323+(** A source entry for a subtree. *)
2424type entry = {
2525- url : string;
2626- (** Our dev-repo URL (e.g., "git+https://github.com/avsm/braid") *)
2525+ url : string; (** Our dev-repo URL (e.g., "git+https://github.com/avsm/braid") *)
2726 upstream : string option; (** Original upstream URL if this is a fork *)
2827 branch : string option; (** Override branch (default: main) *)
2928 reason : string option; (** Why we have a custom source *)
3029 origin : origin option; (** How this entry was created *)
3130}
3232-(** A source entry for a subtree. *)
33313232+(** The sources registry - maps subtree names to source entries. *)
3433type t
3535-(** The sources registry - maps subtree names to source entries. *)
36343735val empty : t
3836(** Empty registry. *)
···4745(** [find t ~subtree] looks up the source entry for a subtree. *)
48464947val derive_url : t -> subtree:string -> string option
5050-(** [derive_url t ~subtree] derives a URL for a subtree. First checks for an
5151- explicit entry, then uses default_url_base if set. *)
4848+(** [derive_url t ~subtree] derives a URL for a subtree.
4949+ First checks for an explicit entry, then uses default_url_base if set. *)
52505351val add : t -> subtree:string -> entry -> t
5452(** [add t ~subtree entry] adds or replaces an entry. *)
···6361(** [of_list entries] creates a registry from an association list. *)
64626563val load : fs:_ Eio.Path.t -> Fpath.t -> (t, string) result
6666-(** [load ~fs path] loads a sources.toml file. Returns empty registry if file
6767- doesn't exist. *)
6464+(** [load ~fs path] loads a sources.toml file. Returns empty registry
6565+ if file doesn't exist. *)
68666967val save : fs:_ Eio.Path.t -> Fpath.t -> t -> (unit, string) result
7068(** [save ~fs path t] writes the registry to a TOML file. *)
+96-111
lib/status.ml
···11-type ahead_behind = { ahead : int; behind : int }
22-type checkout_status = Missing | Not_a_repo | Dirty | Clean of ahead_behind
11+type checkout_status =
22+ | Missing
33+ | Not_a_repo
44+ | Dirty
55+ | Clean of Git.ahead_behind
66+37type subtree_status = Not_added | Present
4859(** Sync state between monorepo subtree and local checkout *)
···2327 let dir, _ = fs in
2428 (dir, "")
25292626-(** Check if a directory exists *)
2727-let dir_exists fs path =
2828- let eio_path = Eio.Path.(fs / Fpath.to_string path) in
2929- match Eio.Path.kind ~follow:true eio_path with
3030- | `Directory -> true
3131- | _ -> false
3232- | exception Eio.Io _ -> false
3333-3434-let to_ahead_behind (ab : Git.Repository.ahead_behind) =
3535- { ahead = ab.ahead; behind = ab.behind }
3636-3737-(** Pre-compute all subtree hashes from mono repo's HEAD *)
3838-let get_subtree_hashes ~fs ~monorepo =
3939- let mono_repo = Git.Repository.open_repo ~fs (Fpath.to_string monorepo) in
4040- match Git.Repository.read_ref mono_repo "HEAD" with
4141- | None -> Hashtbl.create 0
4242- | Some commit_hash -> (
4343- match Git.Repository.read mono_repo commit_hash with
4444- | Error _ -> Hashtbl.create 0
4545- | Ok (Git.Value.Commit c) ->
4646- let root_tree_hash = Git.Commit.tree c in
4747- let tbl = Hashtbl.create 128 in
4848- (* Read root tree and cache all subtree hashes *)
4949- (match Git.Repository.read mono_repo root_tree_hash with
5050- | Ok (Git.Value.Tree tree) ->
5151- List.iter
5252- (fun (e : Git.Tree.entry) ->
5353- if e.perm = `Dir then Hashtbl.add tbl e.name e.hash)
5454- (Git.Tree.to_list tree)
5555- | _ -> ());
5656- tbl
5757- | Ok _ -> Hashtbl.create 0)
5858-5959-(** Internal: compute status for a single package with pre-computed subtree
6060- hashes *)
6161-let compute_one ~fs ~checkouts_root ~monorepo ~subtree_hashes pkg =
3030+let compute ~proc ~fs ~config pkg =
3131+ let checkouts_root = Config.Paths.checkouts config in
6232 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
3333+ let monorepo = Config.Paths.monorepo config in
6334 let prefix = Package.subtree_prefix pkg in
6464- let checkout_path = Fpath.to_string checkout_dir in
3535+ let fs_t = fs_typed fs in
3636+ let fs_dir =
3737+ let dir, _ = fs in
3838+ (dir, Fpath.to_string checkout_dir)
3939+ in
6540 let checkout =
6666- if not (dir_exists fs checkout_dir) then Missing
6767- else if not (Git.Repository.is_repo ~fs checkout_path) then Not_a_repo
6868- else
6969- let repo = Git.Repository.open_repo ~fs checkout_path in
7070- if Git.Repository.is_dirty repo then Dirty
7171- else
7272- let branch =
7373- match Git.Repository.current_branch repo with
7474- | Some b -> b
7575- | None -> "main"
7676- in
7777- match Git.Repository.ahead_behind repo ~branch () with
7878- | Some ab -> Clean (to_ahead_behind ab)
7979- | None -> Clean { ahead = 0; behind = 0 }
4141+ match Eio.Path.kind ~follow:true fs_dir with
4242+ | exception Eio.Io _ -> Missing
4343+ | `Directory -> (
4444+ if not (Git.is_repo ~proc ~fs:fs_t checkout_dir) then Not_a_repo
4545+ else if Git.is_dirty ~proc ~fs:fs_t checkout_dir then Dirty
4646+ else
4747+ match Git.ahead_behind ~proc ~fs:fs_t checkout_dir with
4848+ | Ok ab -> Clean ab
4949+ | Error _ -> Clean { ahead = 0; behind = 0 })
5050+ | _ -> Missing
8051 in
8181- let subtree_dir = Fpath.(monorepo / prefix) in
8282- let subtree = if dir_exists fs subtree_dir then Present else Not_added in
5252+ let subtree =
5353+ if Git.Subtree.exists ~fs:fs_t ~repo:monorepo ~prefix then Present
5454+ else Not_added
5555+ in
5656+ (* Compute subtree sync state: compare tree content between monorepo subtree and checkout.
5757+ This is more accurate than commit ancestry because it handles both push and pull directions.
5858+ If the trees match, the content is in sync regardless of how it got there. *)
8359 let subtree_sync =
8460 match (checkout, subtree) with
8561 | (Missing | Not_a_repo | Dirty), _ -> Unknown
8662 | _, Not_added -> Unknown
8763 | Clean _, Present -> (
8888- let checkout_repo = Git.Repository.open_repo ~fs checkout_path in
8989- let subtree_tree = Hashtbl.find_opt subtree_hashes prefix in
6464+ (* Get tree hash of subtree directory in monorepo *)
6565+ let subtree_tree =
6666+ Git.rev_parse ~proc ~fs:fs_t ~rev:("HEAD:" ^ prefix) monorepo
6767+ in
6868+ (* Get tree hash of checkout root *)
9069 let checkout_tree =
9191- Git.Repository.tree_hash_at_path checkout_repo ~rev:"HEAD" ~path:""
7070+ Git.rev_parse ~proc ~fs:fs_t ~rev:"HEAD^{tree}" checkout_dir
9271 in
9372 match (subtree_tree, checkout_tree) with
9494- | Some st, Some ct when Git.Hash.equal st ct -> In_sync
9595- | Some _, Some _ -> Trees_differ
7373+ | Ok st, Ok ct when st = ct -> In_sync
7474+ | Ok _, Ok _ -> (
7575+ (* Trees differ - check commit ancestry to determine direction *)
7676+ let subtree_commit =
7777+ Git.subtree_last_upstream_commit ~proc ~fs:fs_t ~repo:monorepo
7878+ ~prefix ()
7979+ in
8080+ let checkout_head = Git.head_commit ~proc ~fs:fs_t checkout_dir in
8181+ match (subtree_commit, checkout_head) with
8282+ | Some subtree_sha, Ok checkout_sha ->
8383+ if
8484+ Git.is_ancestor ~proc ~fs:fs_t ~repo:checkout_dir
8585+ ~commit1:subtree_sha ~commit2:checkout_sha ()
8686+ then
8787+ (* Checkout has commits not in subtree - need subtree pull *)
8888+ let count =
8989+ Git.count_commits_between ~proc ~fs:fs_t ~repo:checkout_dir
9090+ ~base:subtree_sha ~head:checkout_sha ()
9191+ in
9292+ if count > 0 then Subtree_behind count else Trees_differ
9393+ (* Same commit but trees differ - monorepo has changes *)
9494+ else if
9595+ Git.is_ancestor ~proc ~fs:fs_t ~repo:checkout_dir
9696+ ~commit1:checkout_sha ~commit2:subtree_sha ()
9797+ then
9898+ (* Subtree has content not in checkout - need push *)
9999+ let count =
100100+ Git.count_commits_between ~proc ~fs:fs_t ~repo:checkout_dir
101101+ ~base:checkout_sha ~head:subtree_sha ()
102102+ in
103103+ if count > 0 then Subtree_ahead count else Trees_differ
104104+ else Trees_differ (* Diverged *)
105105+ | _ -> Trees_differ
106106+ (* Trees differ but can't determine ancestry *))
96107 | _ -> Unknown)
97108 in
98109 { package = pkg; checkout; subtree; subtree_sync }
99110100100-let compute ~fs ~config pkg =
101101- let fs_t = fs_typed fs in
102102- let checkouts_root = Config.Paths.checkouts config in
103103- let monorepo = Config.Paths.monorepo config in
104104- let subtree_hashes = get_subtree_hashes ~fs:fs_t ~monorepo in
105105- compute_one ~fs:fs_t ~checkouts_root ~monorepo ~subtree_hashes pkg
106106-107107-let compute_all ~fs ~config packages =
108108- let fs_t = fs_typed fs in
109109- let checkouts_root = Config.Paths.checkouts config in
110110- let monorepo = Config.Paths.monorepo config in
111111- (* Pre-compute all subtree hashes once *)
112112- let subtree_hashes = get_subtree_hashes ~fs:fs_t ~monorepo in
113113- Eio.Fiber.List.map ~max_fibers:8
114114- (compute_one ~fs:fs_t ~checkouts_root ~monorepo ~subtree_hashes)
115115- packages
111111+let compute_all ~proc ~fs ~config packages =
112112+ List.map (compute ~proc ~fs ~config) packages
116113117114let is_checkout_clean t = match t.checkout with Clean _ -> true | _ -> false
118115let has_local_changes t = match t.checkout with Dirty -> true | _ -> false
···163160 Fmt.pf ppf "@[<h>%-20s checkout: %a subtree: %a@]" (Package.name t.package)
164161 pp_checkout_status t.checkout pp_subtree_status t.subtree
165162166166-(** Extract handle from a tangled.org URL like
167167- "git+https://tangled.org/handle/repo" *)
163163+(** Extract handle from a tangled.org URL like "git+https://tangled.org/handle/repo" *)
168164let extract_handle_from_url url =
169169- let url =
170170- if String.starts_with ~prefix:"git+" url then
171171- String.sub url 4 (String.length url - 4)
172172- else url
173173- in
165165+ let url = if String.starts_with ~prefix:"git+" url then
166166+ String.sub url 4 (String.length url - 4)
167167+ else url in
174168 let uri = Uri.of_string url in
175169 match Uri.host uri with
176176- | Some "tangled.org" -> (
170170+ | Some "tangled.org" ->
177171 let path = Uri.path uri in
178172 (* Path is like "/handle/repo" - extract first component *)
179179- let path =
180180- if String.length path > 0 && path.[0] = '/' then
181181- String.sub path 1 (String.length path - 1)
182182- else path
183183- in
184184- match String.index_opt path '/' with
185185- | Some i -> Some (String.sub path 0 i)
186186- | None -> Some path)
173173+ let path = if String.length path > 0 && path.[0] = '/' then
174174+ String.sub path 1 (String.length path - 1)
175175+ else path in
176176+ (match String.index_opt path '/' with
177177+ | Some i -> Some (String.sub path 0 i)
178178+ | None -> Some path)
187179 | _ -> None
188180189181(** Format origin indicator from sources registry entry *)
···192184 | None -> ()
193185 | Some Sources_registry.{ origin = Some Sources_registry.Fork; _ } ->
194186 Fmt.pf ppf " %a" Fmt.(styled `Magenta string) "^"
195195- | Some
196196- Sources_registry.
197197- { origin = Some Sources_registry.Join; upstream = Some url; _ } -> (
198198- match extract_handle_from_url url with
199199- | Some handle ->
200200- (* Abbreviate handle - take first part before dot, max 8 chars *)
201201- let abbrev =
202202- match String.index_opt handle '.' with
203203- | Some i -> String.sub handle 0 i
204204- | None -> handle
205205- in
206206- let abbrev =
207207- if String.length abbrev > 8 then String.sub abbrev 0 8 else abbrev
208208- in
209209- Fmt.pf ppf " %a"
210210- Fmt.(styled `Cyan (fun ppf s -> pf ppf "v:%s" s))
211211- abbrev
212212- | None -> Fmt.pf ppf " %a" Fmt.(styled `Cyan string) "v:")
187187+ | Some Sources_registry.{ origin = Some Sources_registry.Join; upstream = Some url; _ } ->
188188+ (match extract_handle_from_url url with
189189+ | Some handle ->
190190+ (* Abbreviate handle - take first part before dot, max 8 chars *)
191191+ let abbrev = match String.index_opt handle '.' with
192192+ | Some i -> String.sub handle 0 i
193193+ | None -> handle
194194+ in
195195+ let abbrev = if String.length abbrev > 8 then String.sub abbrev 0 8 else abbrev in
196196+ Fmt.pf ppf " %a" Fmt.(styled `Cyan (fun ppf s -> pf ppf "v:%s" s)) abbrev
197197+ | None -> Fmt.pf ppf " %a" Fmt.(styled `Cyan string) "v:")
213198 | Some Sources_registry.{ origin = Some Sources_registry.Join; _ } ->
214199 Fmt.pf ppf " %a" Fmt.(styled `Cyan string) "v:"
215200 | Some _ -> ()
···221206 let entry = Option.bind sources (fun s -> Sources_registry.find s ~subtree) in
222207 (* Helper to print remote sync info *)
223208 let pp_remote ab =
224224- if ab.ahead > 0 && ab.behind > 0 then
209209+ if ab.Git.ahead > 0 && ab.behind > 0 then
225210 Fmt.pf ppf " %a"
226211 Fmt.(styled `Yellow (fun ppf (a, b) -> pf ppf "remote:+%d/-%d" a b))
227212 (ab.ahead, ab.behind)
+30-15
lib/status.mli
···11(** Status computation and display.
2233 This module computes the synchronization status of packages across the three
44- locations: git remote, individual checkout, and monorepo subtree. Uses
55- native OCaml git library for fast in-process operations. *)
44+ locations: git remote, individual checkout, and monorepo subtree. *)
6576(** {1 Types} *)
8799-type ahead_behind = { ahead : int; behind : int }
1010-(** Commits ahead/behind relative to upstream. *)
1111-128(** Status of an individual checkout relative to its remote. *)
139type checkout_status =
1410 | Missing (** Checkout directory does not exist *)
1511 | Not_a_repo (** Directory exists but is not a git repository *)
1612 | Dirty (** Has uncommitted changes *)
1717- | Clean of ahead_behind
1313+ | Clean of Git.ahead_behind
1814 (** Clean with ahead/behind info relative to remote *)
19152016(** Status of a subtree in the monorepo. *)
···44404541(** {1 Status Computation} *)
46424747-val compute : fs:Eio.Fs.dir_ty Eio.Path.t -> config:Config.t -> Package.t -> t
4848-(** [compute ~fs ~config pkg] computes the status of a single package. *)
4343+val compute :
4444+ proc:_ Eio.Process.mgr ->
4545+ fs:Eio.Fs.dir_ty Eio.Path.t ->
4646+ config:Config.t ->
4747+ Package.t ->
4848+ t
4949+(** [compute ~proc ~fs ~config pkg] computes the status of a single package.
5050+5151+ @param proc Eio process manager
5252+ @param fs Eio filesystem
5353+ @param config Monopam configuration
5454+ @param pkg Package to check *)
49555056val compute_all :
5151- fs:Eio.Fs.dir_ty Eio.Path.t -> config:Config.t -> Package.t list -> t list
5252-(** [compute_all ~fs ~config packages] computes status for all packages. *)
5757+ proc:_ Eio.Process.mgr ->
5858+ fs:Eio.Fs.dir_ty Eio.Path.t ->
5959+ config:Config.t ->
6060+ Package.t list ->
6161+ t list
6262+(** [compute_all ~proc ~fs ~config packages] computes status for all packages in
6363+ parallel.
6464+6565+ @param proc Eio process manager
6666+ @param fs Eio filesystem
6767+ @param config Monopam configuration
6868+ @param packages List of packages to check *)
53695470(** {1 Predicates} *)
5571···97113(** [pp] formats a single package status. *)
9811499115val pp_compact : ?sources:Sources_registry.t -> t Fmt.t
100100-(** [pp_compact ?sources] formats a single package status in compact form with
101101- colors. If [sources] is provided, displays origin indicators (^ for fork,
102102- v:handle for join). *)
116116+(** [pp_compact ?sources] formats a single package status in compact form with colors.
117117+ If [sources] is provided, displays origin indicators (^ for fork, v:handle for join). *)
103118104119val pp_summary : ?sources:Sources_registry.t -> t list Fmt.t
105105-(** [pp_summary ?sources] formats a summary of all package statuses. If
106106- [sources] is provided, displays origin indicators for each package. *)
120120+(** [pp_summary ?sources] formats a summary of all package statuses.
121121+ If [sources] is provided, displays origin indicators for each package. *)
+64-119
lib/verse.ml
···11type error =
22 | Config_error of string
33- | Git_error of Git_cli.error
33+ | Git_error of Git.error
44 | Registry_error of string
55 | Member_not_found of string
66 | Workspace_exists of Fpath.t
77 | Not_a_workspace of Fpath.t
88 | Package_not_found of string * string (** (package, handle) *)
99- | Package_already_exists of string list
1010- (** List of conflicting package names *)
99+ | Package_already_exists of string list (** List of conflicting package names *)
1110 | Opam_repo_error of Opam_repo.error
12111312let pp_error ppf = function
1413 | Config_error msg -> Fmt.pf ppf "Configuration error: %s" msg
1515- | Git_error e -> Fmt.pf ppf "Git error: %a" Git_cli.pp_error e
1414+ | Git_error e -> Fmt.pf ppf "Git error: %a" Git.pp_error e
1615 | Registry_error msg -> Fmt.pf ppf "Registry error: %s" msg
1716 | Member_not_found h -> Fmt.pf ppf "Member not in registry: %s" h
1817 | Workspace_exists p -> Fmt.pf ppf "Workspace already exists: %a" Fpath.pp p
···2120 Fmt.pf ppf "Package %s not found in %s's opam repo" pkg handle
2221 | Package_already_exists pkgs ->
2322 Fmt.pf ppf "Packages already exist in your opam repo: %a"
2424- Fmt.(list ~sep:comma string)
2525- pkgs
2323+ Fmt.(list ~sep:comma string) pkgs
2624 | Opam_repo_error e -> Fmt.pf ppf "Opam repo error: %a" Opam_repo.pp_error e
27252826let error_hint = function
2927 | Config_error _ ->
3030- Some "Run 'monopam init --handle <your-handle>' to create a workspace."
3131- | Git_error (Git_cli.Dirty_worktree _) ->
2828+ Some
2929+ "Run 'monopam init --handle <your-handle>' to create a workspace."
3030+ | Git_error (Git.Dirty_worktree _) ->
3231 Some "Commit or stash your changes first: git status"
3333- | Git_error (Git_cli.Command_failed (cmd, _))
3232+ | Git_error (Git.Command_failed (cmd, _))
3433 when String.starts_with ~prefix:"git clone" cmd ->
3534 Some "Check the URL is correct and you have network access."
3636- | Git_error (Git_cli.Command_failed (cmd, _))
3535+ | Git_error (Git.Command_failed (cmd, _))
3736 when String.starts_with ~prefix:"git pull" cmd ->
3837 Some "Check your network connection. Try: git fetch origin"
3938 | Git_error _ -> None
···4645 | Workspace_exists _ ->
4746 Some "Use a different directory, or remove the existing workspace."
4847 | Not_a_workspace _ ->
4949- Some
5050- "Run 'monopam init --handle <your-handle>' to create a workspace here."
4848+ Some "Run 'monopam init --handle <your-handle>' to create a workspace here."
5149 | Package_not_found (pkg, handle) ->
5252- Some
5353- (Fmt.str
5454- "Run 'monopam verse pull %s' to sync their opam repo, then check \
5555- package name: %s"
5656- handle pkg)
5050+ Some (Fmt.str "Run 'monopam verse pull %s' to sync their opam repo, then check package name: %s" handle pkg)
5751 | Package_already_exists pkgs ->
5858- Some
5959- (Fmt.str "Remove conflicting packages first:\n %s"
6060- (String.concat "\n "
6161- (List.map (fun p -> "rm -rf opam-repo/packages/" ^ p) pkgs)))
5252+ Some (Fmt.str "Remove conflicting packages first:\n %s"
5353+ (String.concat "\n " (List.map (fun p -> "rm -rf opam-repo/packages/" ^ p) pkgs)))
6254 | Opam_repo_error _ -> None
63556456let pp_error_with_hint ppf e =
···7365 local_path : Fpath.t;
7466 cloned : bool;
7567 clean : bool option;
7676- ahead_behind : Git_cli.ahead_behind option;
6868+ ahead_behind : Git.ahead_behind option;
7769}
78707971type status = {
···188180 Logs.info (fun m -> m "Cloning monorepo to %a" Fpath.pp mono_path);
189181 let mono_url = Uri.of_string member.monorepo in
190182 match
191191- Git_cli.clone ~proc ~fs ~url:mono_url
183183+ Git.clone ~proc ~fs ~url:mono_url
192184 ~branch:Verse_config.default_branch mono_path
193185 with
194186 | Error e ->
195195- Logs.err (fun m ->
196196- m "Monorepo clone failed: %a" Git_cli.pp_error e);
187187+ Logs.err (fun m -> m "Monorepo clone failed: %a" Git.pp_error e);
197188 Error (Git_error e)
198189 | Ok () -> (
199190 Logs.info (fun m -> m "Monorepo cloned");
···203194 m "Cloning opam repo to %a" Fpath.pp opam_path);
204195 let opam_url = Uri.of_string member.opamrepo in
205196 match
206206- Git_cli.clone ~proc ~fs ~url:opam_url
197197+ Git.clone ~proc ~fs ~url:opam_url
207198 ~branch:Verse_config.default_branch opam_path
208199 with
209200 | Error e ->
210201 Logs.err (fun m ->
211211- m "Opam repo clone failed: %a" Git_cli.pp_error e);
202202+ m "Opam repo clone failed: %a" Git.pp_error e);
212203 Error (Git_error e)
213204 | Ok () -> (
214205 Logs.info (fun m -> m "Opam repo cloned");
···256247 let local_path =
257248 Fpath.(Verse_config.verse_path config / handle)
258249 in
259259- let cloned = Git_cli.is_repo ~proc ~fs local_path in
250250+ let cloned = Git.is_repo ~proc ~fs local_path in
260251 let clean =
261261- if cloned then
262262- Some (not (Git_cli.is_dirty ~proc ~fs local_path))
252252+ if cloned then Some (not (Git.is_dirty ~proc ~fs local_path))
263253 else None
264254 in
265255 let ahead_behind =
266256 if cloned then
267267- match Git_cli.ahead_behind ~proc ~fs local_path with
257257+ match Git.ahead_behind ~proc ~fs local_path with
268258 | Ok ab -> Some ab
269259 | Error _ -> None
270260 else None
···287277 | Error msg -> Error (Registry_error msg)
288278 | Ok registry -> Ok registry.members
289279290290-(** Clone or fetch+reset a single git repo. Returns Ok true if cloned, Ok false
291291- if reset. Uses fetch+reset instead of pull since verse repos should not have
292292- local changes. *)
280280+(** Clone or fetch+reset a single git repo. Returns Ok true if cloned, Ok false if reset.
281281+ Uses fetch+reset instead of pull since verse repos should not have local changes. *)
293282let clone_or_reset_repo ~proc ~fs ~url ~branch path =
294294- if Git_cli.is_repo ~proc ~fs path then begin
295295- match Git_cli.fetch_and_reset ~proc ~fs ~branch path with
283283+ if Git.is_repo ~proc ~fs path then begin
284284+ match Git.fetch_and_reset ~proc ~fs ~branch path with
296285 | Error e -> Error e
297286 | Ok () -> Ok false
298287 end
299288 else begin
300289 let url = Uri.of_string url in
301301- match Git_cli.clone ~proc ~fs ~url ~branch path with
290290+ match Git.clone ~proc ~fs ~url ~branch path with
302291 | Error e -> Error e
303292 | Ok () -> Ok true
304293 end
···322311 let verse_dir = Verse_config.verse_path config in
323312 ensure_dir ~fs verse_dir;
324313 Logs.info (fun m -> m "Syncing %d members" (List.length members));
325325- (* Sync all members in parallel *)
326314 let errors =
327327- Eio.Fiber.List.filter_map ~max_fibers:4
315315+ List.filter_map
328316 (fun (member : Verse_registry.member) ->
329317 let h = member.handle in
330318 let mono_path = Fpath.(verse_dir / h) in
···332320 (* Clone or fetch+reset monorepo *)
333321 Logs.info (fun m -> m "Syncing %s monorepo" h);
334322 let mono_branch =
335335- Option.value ~default:Verse_config.default_branch
336336- member.monorepo_branch
323323+ Option.value ~default:Verse_config.default_branch member.monorepo_branch
337324 in
338325 let mono_result =
339326 clone_or_reset_repo ~proc ~fs ~url:member.monorepo
···349336 None
350337 | Error e ->
351338 Logs.warn (fun m ->
352352- m " Failed %s monorepo: %a" h Git_cli.pp_error e);
353353- Some (Fmt.str "%s monorepo: %a" h Git_cli.pp_error e)
339339+ m " Failed %s monorepo: %a" h Git.pp_error e);
340340+ Some (Fmt.str "%s monorepo: %a" h Git.pp_error e)
354341 in
355342 (* Clone or fetch+reset opam repo *)
356343 Logs.info (fun m -> m "Syncing %s opam repo" h);
357344 let opam_branch =
358358- Option.value ~default:Verse_config.default_branch
359359- member.opamrepo_branch
345345+ Option.value ~default:Verse_config.default_branch member.opamrepo_branch
360346 in
361347 let opam_result =
362348 clone_or_reset_repo ~proc ~fs ~url:member.opamrepo
···372358 None
373359 | Error e ->
374360 Logs.warn (fun m ->
375375- m " Failed %s opam repo: %a" h Git_cli.pp_error e);
376376- Some (Fmt.str "%s opam: %a" h Git_cli.pp_error e)
361361+ m " Failed %s opam repo: %a" h Git.pp_error e);
362362+ Some (Fmt.str "%s opam: %a" h Git.pp_error e)
377363 in
378364 match (mono_err, opam_err) with
379365 | None, None -> None
···382368 members
383369 in
384370 if errors = [] then Ok ()
385385- else Error (Git_error (Git_cli.Io_error (String.concat "; " errors)))
371371+ else Error (Git_error (Git.Io_error (String.concat "; " errors)))
386372 end
387373388374let sync ~proc ~fs ~config () =
···392378(** Scan a monorepo for subtree directories. Returns a list of directory names
393379 that look like subtrees (have commits). *)
394380let scan_subtrees ~proc ~fs monorepo_path =
395395- if not (Git_cli.is_repo ~proc ~fs monorepo_path) then []
381381+ if not (Git.is_repo ~proc ~fs monorepo_path) then []
396382 else
397383 let eio_path = Eio.Path.(fs / Fpath.to_string monorepo_path) in
398384 try
···414400 List.iter
415401 (fun handle ->
416402 let member_mono = Fpath.(verse_path / handle) in
417417- if Git_cli.is_repo ~proc ~fs member_mono then begin
403403+ if Git.is_repo ~proc ~fs member_mono then begin
418404 let subtrees = scan_subtrees ~proc ~fs member_mono in
419405 List.iter
420406 (fun subtree ->
···428414 tracked_handles;
429415 subtree_map
430416417417+(** Result of a fork operation. *)
431418type fork_result = {
432419 packages_forked : string list; (** Package names that were forked *)
433420 source_handle : string; (** Handle of the verse member we forked from *)
434421 fork_url : string; (** URL of the fork *)
435422 upstream_url : string; (** Original dev-repo URL (upstream) *)
436436- subtree_name : string;
437437- (** Name for the subtree directory (derived from fork URL) *)
423423+ subtree_name : string; (** Name for the subtree directory (derived from fork URL) *)
438424}
439439-(** Result of a fork operation. *)
440425441426(** Extract subtree name from a URL (last path component without .git suffix) *)
442427let subtree_name_from_url url =
443428 let uri = Uri.of_string url in
444429 let path = Uri.path uri in
445430 (* Remove leading slash and .git suffix *)
446446- let path =
447447- if String.length path > 0 && path.[0] = '/' then
448448- String.sub path 1 (String.length path - 1)
449449- else path
450450- in
451451- let path =
452452- if String.ends_with ~suffix:".git" path then
453453- String.sub path 0 (String.length path - 4)
454454- else path
455455- in
431431+ let path = if String.length path > 0 && path.[0] = '/' then
432432+ String.sub path 1 (String.length path - 1)
433433+ else path in
434434+ let path = if String.ends_with ~suffix:".git" path then
435435+ String.sub path 0 (String.length path - 4)
436436+ else path in
456437 (* Get last component *)
457438 match String.rindex_opt path '/' with
458439 | Some i -> String.sub path (i + 1) (String.length path - i - 1)
459440 | None -> path
460441461442let pp_fork_result ppf r =
462462- Fmt.pf ppf
463463- "@[<v>Forked %d package(s) from %s:@,\
464464- \ @[<v>%a@]@,\
465465- Fork URL: %s@,\
466466- Upstream: %s@,\
467467- Subtree: %s@]"
443443+ Fmt.pf ppf "@[<v>Forked %d package(s) from %s:@, @[<v>%a@]@,Fork URL: %s@,Upstream: %s@,Subtree: %s@]"
468444 (List.length r.packages_forked)
469445 r.source_handle
470470- Fmt.(list ~sep:cut string)
471471- r.packages_forked r.fork_url r.upstream_url r.subtree_name
446446+ Fmt.(list ~sep:cut string) r.packages_forked
447447+ r.fork_url
448448+ r.upstream_url
449449+ r.subtree_name
472450473451(** Fork a package from a verse member's opam repo into your workspace.
474452···487465 (* Ensure the member exists and their opam-repo is synced *)
488466 match Verse_registry.clone_or_pull ~proc ~fs ~config () with
489467 | Error msg -> Error (Registry_error msg)
490490- | Ok registry -> (
468468+ | Ok registry ->
491469 match Verse_registry.find_member registry ~handle with
492470 | None -> Error (Member_not_found handle)
493493- | Some _member -> (
471471+ | Some _member ->
494472 let verse_path = Verse_config.verse_path config in
495473 let member_opam_repo = Fpath.(verse_path / (handle ^ "-opam")) in
496474 (* Check if their opam repo exists locally *)
497475 if not (is_directory ~fs member_opam_repo) then
498498- Error
499499- (Config_error
500500- (Fmt.str
501501- "Member's opam repo not synced. Run: monopam verse pull %s"
502502- handle))
476476+ Error (Config_error (Fmt.str "Member's opam repo not synced. Run: monopam verse pull %s" handle))
503477 else
504478 (* Scan their opam repo to find the package *)
505479 let pkgs, _errors = Opam_repo.scan_all ~fs member_opam_repo in
···519493 let user_opam_repo = Verse_config.opam_repo_path config in
520494 let conflicts =
521495 List.filter
522522- (fun name ->
523523- Opam_repo.package_exists ~fs ~repo_path:user_opam_repo
524524- ~name)
496496+ (fun name -> Opam_repo.package_exists ~fs ~repo_path:user_opam_repo ~name)
525497 pkg_names
526498 in
527527- if conflicts <> [] then Error (Package_already_exists conflicts)
499499+ if conflicts <> [] then
500500+ Error (Package_already_exists conflicts)
528501 else if dry_run then
529502 (* Dry run - just report what would be done *)
530530- Ok
531531- {
532532- packages_forked = pkg_names;
533533- source_handle = handle;
534534- fork_url;
535535- upstream_url;
536536- subtree_name;
537537- }
503503+ Ok { packages_forked = pkg_names; source_handle = handle; fork_url; upstream_url; subtree_name }
538504 else begin
539505 (* Fork each package *)
540506 let results =
···543509 let name = Package.name p in
544510 let version = Package.version p in
545511 let opam_path =
546546- Fpath.(
547547- member_opam_repo / "packages" / name
548548- / (name ^ "." ^ version)
549549- / "opam")
512512+ Fpath.(member_opam_repo / "packages" / name / (name ^ "." ^ version) / "opam")
550513 in
551514 match Opam_repo.read_opam_file ~fs opam_path with
552515 | Error e -> Error (Opam_repo_error e)
553553- | Ok content -> (
516516+ | Ok content ->
554517 (* Replace dev-repo and url with fork URL *)
555555- let new_content =
556556- Opam_repo.replace_dev_repo_url content
557557- ~new_url:fork_url
558558- in
518518+ let new_content = Opam_repo.replace_dev_repo_url content ~new_url:fork_url in
559519 (* Write to user's opam-repo *)
560560- match
561561- Opam_repo.write_package ~fs
562562- ~repo_path:user_opam_repo ~name ~version
563563- ~content:new_content
564564- with
520520+ match Opam_repo.write_package ~fs ~repo_path:user_opam_repo ~name ~version ~content:new_content with
565521 | Error e -> Error (Opam_repo_error e)
566566- | Ok () -> Ok name))
522522+ | Ok () -> Ok name)
567523 related_pkgs
568524 in
569525 (* Check for errors *)
570526 match List.find_opt Result.is_error results with
571527 | Some (Error e) -> Error e
572528 | _ ->
573573- let forked_names =
574574- List.filter_map
575575- (function Ok n -> Some n | Error _ -> None)
576576- results
577577- in
578578- Ok
579579- {
580580- packages_forked = forked_names;
581581- source_handle = handle;
582582- fork_url;
583583- upstream_url;
584584- subtree_name;
585585- }
586586- end))
529529+ let forked_names = List.filter_map (function Ok n -> Some n | Error _ -> None) results in
530530+ Ok { packages_forked = forked_names; source_handle = handle; fork_url; upstream_url; subtree_name }
531531+ end
+6-9
lib/verse.mli
···7788type error =
99 | Config_error of string (** Configuration loading/saving error *)
1010- | Git_error of Git_cli.error (** Git operation failed *)
1010+ | Git_error of Git.error (** Git operation failed *)
1111 | Registry_error of string (** Registry clone/pull/parse error *)
1212 | Member_not_found of string (** Handle not in registry *)
1313 | Workspace_exists of Fpath.t (** Workspace already initialized *)
1414 | Not_a_workspace of Fpath.t (** Not a opamverse workspace *)
1515- | Package_not_found of string * string
1616- (** Package not found in member's repo: (package, handle) *)
1717- | Package_already_exists of string list
1818- (** Packages already exist in user's opam repo *)
1515+ | Package_not_found of string * string (** Package not found in member's repo: (package, handle) *)
1616+ | Package_already_exists of string list (** Packages already exist in user's opam repo *)
1917 | Opam_repo_error of Opam_repo.error (** Error reading/writing opam files *)
20182119val pp_error : error Fmt.t
···3634 local_path : Fpath.t; (** Local path under verse/ *)
3735 cloned : bool; (** Whether the monorepo is cloned locally *)
3836 clean : bool option; (** Whether the clone is clean (None if not cloned) *)
3939- ahead_behind : Git_cli.ahead_behind option;
3737+ ahead_behind : Git.ahead_behind option;
4038 (** Ahead/behind status (None if not cloned) *)
4139}
4240(** Status of a member's monorepo in the workspace. *)
···151149152150(** {1 Forking} *)
153151152152+(** Result of a fork operation. *)
154153type fork_result = {
155154 packages_forked : string list; (** Package names that were forked *)
156155 source_handle : string; (** Handle of the verse member we forked from *)
157156 fork_url : string; (** URL of the fork *)
158157 upstream_url : string; (** Original dev-repo URL (upstream) *)
159159- subtree_name : string;
160160- (** Name for the subtree directory (derived from fork URL) *)
158158+ subtree_name : string; (** Name for the subtree directory (derived from fork URL) *)
161159}
162162-(** Result of a fork operation. *)
163160164161val pp_fork_result : fork_result Fmt.t
165162(** [pp_fork_result] formats a fork result. *)
+3-3
lib/verse_config.ml
···11(** Verse_config is now an alias for Config.
2233- This module is kept for backwards compatibility. All functionality has been
44- unified into Config. *)
33+ This module is kept for backwards compatibility.
44+ All functionality has been unified into Config. *)
5566include Config
7788-type package_override = Config.Package_config.t
98(** Legacy type alias for package overrides *)
99+type package_override = Config.Package_config.t
+3-3
lib/verse_config.mli
···11(** Verse_config is now an alias for Config.
2233- This module is kept for backwards compatibility. All functionality has been
44- unified into Config.
33+ This module is kept for backwards compatibility.
44+ All functionality has been unified into Config.
5566 @deprecated Use {!Config} directly. *)
7788include module type of Config
991010-type package_override = Config.Package_config.t
1110(** Legacy type alias for package overrides.
1211 @deprecated Use {!Config.Package_config.t} instead. *)
1212+type package_override = Config.Package_config.t
+15-23
lib/verse_registry.ml
···66 opamrepo : string;
77 opamrepo_branch : string option;
88}
99-109type t = { name : string; description : string option; members : member list }
11101211let default_url = "https://tangled.org/eeg.cl.cam.ac.uk/opamverse"
···22212322(** Encode a URL with optional branch suffix. *)
2423let encode_url_with_branch url branch =
2525- match branch with None -> url | Some b -> url ^ "#" ^ b
2424+ match branch with
2525+ | None -> url
2626+ | Some b -> url ^ "#" ^ b
26272728let pp_member ppf m =
2829 let mono_str = encode_url_with_branch m.monorepo m.monorepo_branch in
2930 let opam_str = encode_url_with_branch m.opamrepo m.opamrepo_branch in
3031 let name_str = match m.name with Some n -> n | None -> m.handle in
3131- Fmt.pf ppf "@[<hov 2>%s (%s) ->@ mono:%s@ opam:%s@]" name_str m.handle
3232- mono_str opam_str
3232+ Fmt.pf ppf "@[<hov 2>%s (%s) ->@ mono:%s@ opam:%s@]" name_str m.handle mono_str opam_str
33333434let pp ppf t =
3535 Fmt.pf ppf "@[<v>registry: %s%a@,members:@, @[<v>%a@]@]" t.name
3636- Fmt.(option (fun ppf s -> pf ppf "@,description: %s" s))
3737- t.description
3636+ Fmt.(option (fun ppf s -> pf ppf "@,description: %s" s)) t.description
3837 Fmt.(list ~sep:cut pp_member)
3938 t.members
4039···5756 { handle; name; monorepo; monorepo_branch; opamrepo; opamrepo_branch })
5857 |> mem "handle" string ~enc:(fun (m : member) -> m.handle)
5958 |> opt_mem "name" string ~enc:(fun (m : member) -> m.name)
6060- |> mem "monorepo" string ~enc:(fun (m : member) ->
6161- encode_url_with_branch m.monorepo m.monorepo_branch)
6262- |> mem "opamrepo" string ~enc:(fun (m : member) ->
6363- encode_url_with_branch m.opamrepo m.opamrepo_branch)
5959+ |> mem "monorepo" string ~enc:(fun (m : member) -> encode_url_with_branch m.monorepo m.monorepo_branch)
6060+ |> mem "opamrepo" string ~enc:(fun (m : member) -> encode_url_with_branch m.opamrepo m.opamrepo_branch)
6461 |> finish))
65626663type registry_info = { r_name : string; r_description : string option }
···7774 Tomlt.(
7875 Table.(
7976 obj (fun registry members ->
8080- {
8181- name = registry.r_name;
8282- description = registry.r_description;
8383- members = Option.value ~default:[] members;
8484- })
8585- |> mem "registry" registry_info_codec ~enc:(fun t ->
8686- { r_name = t.name; r_description = t.description })
7777+ { name = registry.r_name; description = registry.r_description; members = Option.value ~default:[] members })
7878+ |> mem "registry" registry_info_codec ~enc:(fun t -> { r_name = t.name; r_description = t.description })
8779 |> opt_mem "members" (list member_codec) ~enc:(fun t ->
8880 match t.members with [] -> None | ms -> Some ms)
8981 |> finish))
···125117 let exists =
126118 let path = Eio.Path.(fs / Fpath.to_string registry_path) in
127119 match Eio.Path.kind ~follow:true path with
128128- | `Directory -> Git_cli.is_repo ~proc ~fs registry_path
120120+ | `Directory -> Git.is_repo ~proc ~fs registry_path
129121 | _ -> false
130122 | exception _ -> false
131123 in
132124 if exists then begin
133125 Logs.info (fun m -> m "Registry exists, pulling updates...");
134126 (* Pull updates, but don't fail if pull fails *)
135135- (match Git_cli.pull ~proc ~fs registry_path with
127127+ (match Git.pull ~proc ~fs registry_path with
136128 | Ok () -> Logs.info (fun m -> m "Registry pull succeeded")
137129 | Error e ->
138130 Logs.warn (fun m ->
139139- m "Registry pull failed: %a (using cached)" Git_cli.pp_error e));
131131+ m "Registry pull failed: %a (using cached)" Git.pp_error e));
140132 Logs.info (fun m -> m "Loading registry from %a" Fpath.pp registry_toml);
141133 load ~fs registry_toml
142134 end
···149141 (* Try to clone the registry *)
150142 let url = Uri.of_string default_url in
151143 let branch = "main" in
152152- match Git_cli.clone ~proc ~fs ~url ~branch registry_path with
144144+ match Git.clone ~proc ~fs ~url ~branch registry_path with
153145 | Ok () ->
154146 Logs.info (fun m -> m "Registry cloned successfully");
155147 load ~fs registry_toml
156148 | Error e ->
157157- Logs.warn (fun m -> m "Registry clone failed: %a" Git_cli.pp_error e);
149149+ Logs.warn (fun m -> m "Registry clone failed: %a" Git.pp_error e);
158150 Logs.info (fun m -> m "Creating empty local registry...");
159151 (* Clone failed - create local registry directory with empty registry *)
160152 let registry_eio = Eio.Path.(fs / Fpath.to_string registry_path) in
161153 (try Eio.Path.mkdirs ~perm:0o755 registry_eio with Eio.Io _ -> ());
162154 (* Initialize as git repo *)
163163- (match Git_cli.init ~proc ~fs registry_path with
155155+ (match Git.init ~proc ~fs registry_path with
164156 | Ok () -> ()
165157 | Error _ -> ());
166158 (* Create empty registry file *)
+4-6
lib/verse_registry.mli
···99 handle : string; (** Tangled handle (e.g., "alice.bsky.social") *)
1010 name : string option; (** Display name (e.g., "Alice Smith") *)
1111 monorepo : string; (** Git URL of the member's monorepo *)
1212- monorepo_branch : string option;
1313- (** Optional branch for monorepo (from URL#branch) *)
1212+ monorepo_branch : string option; (** Optional branch for monorepo (from URL#branch) *)
1413 opamrepo : string; (** Git URL of the member's opam overlay repository *)
1515- opamrepo_branch : string option;
1616- (** Optional branch for opam repo (from URL#branch) *)
1414+ opamrepo_branch : string option; (** Optional branch for opam repo (from URL#branch) *)
1715}
1816(** A registry member entry.
19172020- URLs may include a [#branch] suffix to specify a non-default branch. For
2121- example, ["https://github.com/user/repo#develop"]. *)
1818+ URLs may include a [#branch] suffix to specify a non-default branch.
1919+ For example, ["https://github.com/user/repo#develop"]. *)
22202321type t = {
2422 name : string; (** Registry name *)