···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
101101+ match
102102+ Monopam.Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path
103103+ with
102104 | Ok s -> Some s
103105 | Error _ -> None
104106 in
···447449 [
448450 `S Manpage.s_description;
449451 `P
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.";
452452+ "Creates a new monopam workspace for monorepo development. The \
453453+ workspace lets you manage your own monorepo and optionally browse and \
454454+ track other developers' monorepos.";
453455 `S "WORKSPACE STRUCTURE";
454456 `P
455457 "The init command creates the following directory structure at the \
···476478 handle = \"yourname.bsky.social\"";
477479 `S "HANDLE VALIDATION";
478480 `P
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).";
481481+ "The handle you provide identifies you in the community. It should be \
482482+ a valid domain name (e.g., yourname.bsky.social or your-domain.com).";
482483 `S "REGISTRY";
483484 `P
484485 "The registry is a git repository containing an opamverse.toml file \
···589590 [
590591 `S Manpage.s_description;
591592 `P
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.";
593593+ "Fork a package from a verse member's opam repository into your \
594594+ workspace. This creates entries in your opam-repo with your fork URL \
595595+ as the dev-repo.";
594596 `P
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.";
597597+ "The command finds all packages sharing the same git repository and \
598598+ forks them together. For example, if you fork 'cohttp', it will also \
599599+ fork cohttp-eio, cohttp-lwt, etc.";
598600 `S "WHAT IT DOES";
599601 `P "For the specified package:";
600600- `I ("1.", "Looks up the package in <handle>'s opam-repo (verse/<handle>-opam/)");
602602+ `I
603603+ ( "1.",
604604+ "Looks up the package in <handle>'s opam-repo (verse/<handle>-opam/)"
605605+ );
601606 `I ("2.", "Finds all packages from the same git repository");
602607 `I ("3.", "Creates entries in your opam-repo with your fork URL");
603608 `P "After forking:";
604604- `I ("1.", "Commit the new opam files: $(b,cd opam-repo && git add -A && git commit)");
609609+ `I
610610+ ( "1.",
611611+ "Commit the new opam files: $(b,cd opam-repo && git add -A && git \
612612+ commit)" );
605613 `I ("2.", "Run $(b,monopam sync) to pull the fork into your monorepo");
606614 `S "PREREQUISITES";
607615 `P "Before forking:";
608608- `I ("-", "Run $(b,monopam verse pull <handle>) to sync the member's opam-repo");
616616+ `I
617617+ ( "-",
618618+ "Run $(b,monopam verse pull <handle>) to sync the member's opam-repo"
619619+ );
609620 `I ("-", "Create a fork of the repository on GitHub/GitLab/etc.");
610621 `S Manpage.s_examples;
611622 `P "Fork a package from a verse member:";
612612- `Pre "monopam fork http2 --from sadiq.bsky.social --url git@github.com:me/http2.git";
623623+ `Pre
624624+ "monopam fork http2 --from sadiq.bsky.social --url \
625625+ git@github.com:me/http2.git";
613626 `P "Preview what would be forked (multi-package repos):";
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";
627627+ `Pre
628628+ "monopam fork cohttp --from avsm.bsky.social --url \
629629+ git@github.com:me/cohttp.git --dry-run\n\
630630+ Would fork 5 packages from cohttp repository:\n\
631631+ \ cohttp\n\
632632+ \ cohttp-eio\n\
633633+ \ cohttp-lwt\n\
634634+ \ cohttp-async\n\
635635+ \ cohttp-mirage";
621636 `P "After forking, commit and sync:";
622622- `Pre "cd opam-repo && git add -A && git commit -m \"Fork cohttp\"\n\
623623- monopam sync";
637637+ `Pre
638638+ "cd opam-repo && git add -A && git commit -m \"Fork cohttp\"\n\
639639+ monopam sync";
624640 `S "ERRORS";
625641 `P
626626- "The command will fail if any package from the source repo already exists \
627627- in your opam-repo. Remove conflicting packages first with:";
642642+ "The command will fail if any package from the source repo already \
643643+ exists in your opam-repo. Remove conflicting packages first with:";
628644 `Pre "rm -rf opam-repo/packages/<package-name>";
629645 ]
630646 in
···635651 in
636652 let from_arg =
637653 let doc = "Verse member handle to fork from (e.g., 'avsm.bsky.social')" in
638638- Arg.(required & opt (some string) None & info [ "from" ] ~docv:"HANDLE" ~doc)
654654+ Arg.(
655655+ required & opt (some string) None & info [ "from" ] ~docv:"HANDLE" ~doc)
639656 in
640657 let url_arg =
641658 let doc = "Git URL of your fork (e.g., 'git@github.com:you/repo.git')" in
···650667 with_verse_config env @@ fun config ->
651668 let fs = Eio.Stdenv.fs env in
652669 let proc = Eio.Stdenv.process_mgr env in
653653- match Monopam.Verse.fork ~proc ~fs ~config ~handle ~package ~fork_url ~dry_run () with
670670+ match
671671+ Monopam.Verse.fork ~proc ~fs ~config ~handle ~package ~fork_url ~dry_run
672672+ ()
673673+ with
654674 | Ok result ->
655675 if dry_run then begin
656676 Fmt.pr "Would fork %d package(s) from %s:@."
657657- (List.length result.packages_forked) result.source_handle;
677677+ (List.length result.packages_forked)
678678+ result.source_handle;
658679 List.iter (fun p -> Fmt.pr " %s@." p) result.packages_forked
659659- end else begin
680680+ end
681681+ else begin
660682 (* Update sources.toml with fork information *)
661683 let mono_path = Monopam.Verse_config.mono_path config in
662684 let sources_path = Fpath.(mono_path / "sources.toml") in
663685 let sources =
664664- match Monopam.Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path with
686686+ match
687687+ Monopam.Sources_registry.load
688688+ ~fs:(fs :> _ Eio.Path.t)
689689+ sources_path
690690+ with
665691 | Ok s -> s
666692 | Error _ -> Monopam.Sources_registry.empty
667693 in
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
694694+ let entry =
695695+ Monopam.Sources_registry.
696696+ {
697697+ url = result.fork_url;
698698+ upstream = Some result.upstream_url;
699699+ branch = None;
700700+ reason = Some (Fmt.str "Forked from %s" result.source_handle);
701701+ origin = Some Join;
702702+ (* Forked from verse = joined *)
703703+ }
704704+ in
705705+ let sources =
706706+ Monopam.Sources_registry.add sources ~subtree:result.subtree_name
707707+ entry
708708+ in
709709+ (match
710710+ Monopam.Sources_registry.save
711711+ ~fs:(fs :> _ Eio.Path.t)
712712+ sources_path sources
713713+ with
677714 | Ok () ->
678678- Fmt.pr "Updated sources.toml with fork entry for %s@." result.subtree_name
715715+ Fmt.pr "Updated sources.toml with fork entry for %s@."
716716+ result.subtree_name
679717 | Error msg ->
680718 Fmt.epr "Warning: Failed to update sources.toml: %s@." msg);
681719 Fmt.pr "Forked %d package(s): %a@."
682720 (List.length result.packages_forked)
683683- Fmt.(list ~sep:(any ", ") string) result.packages_forked;
721721+ Fmt.(list ~sep:(any ", ") string)
722722+ result.packages_forked;
684723 Fmt.pr "@.Next steps:@.";
685685- Fmt.pr " 1. cd opam-repo && git add -A && git commit -m \"Fork ...\"@.";
724724+ Fmt.pr
725725+ " 1. cd opam-repo && git add -A && git commit -m \"Fork ...\"@.";
686726 Fmt.pr " 2. monopam sync@."
687727 end;
688728 `Ok ()
···690730 Fmt.epr "Error: %a@." Monopam.Verse.pp_error_with_hint e;
691731 `Error (false, "fork failed")
692732 in
693693- Cmd.v info Term.(ret (const run $ package_arg $ from_arg $ url_arg $ dry_run_arg $ logging_term))
733733+ Cmd.v info
734734+ Term.(
735735+ ret
736736+ (const run $ package_arg $ from_arg $ url_arg $ dry_run_arg
737737+ $ logging_term))
694738695739let verse_cmd =
696740 let doc = "Verse member operations" in
···699743 `S Manpage.s_description;
700744 `P
701745 "Commands for working with verse community members. The verse system \
702702- enables federated collaboration across multiple developers' monorepos.";
746746+ enables federated collaboration across multiple developers' \
747747+ monorepos.";
703748 `P
704749 "Members are identified by handles - typically domain names like \
705750 'yourname.bsky.social' or 'your-domain.com'.";
706751 `S "NOTE";
707752 `P
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.";
753753+ "The $(b,monopam init) command creates your workspace and $(b,monopam \
754754+ sync) automatically syncs verse members. These commands are for \
755755+ additional verse-specific operations.";
711756 `S "COMMANDS";
712757 `I ("members", "List all members in the community registry");
713713- `I ("fork <pkg> --from <handle> --url <url>", "Fork a package from a verse member");
758758+ `I
759759+ ( "fork <pkg> --from <handle> --url <url>",
760760+ "Fork a package from a verse member" );
714761 `S Manpage.s_examples;
715762 `P "List all community members:";
716763 `Pre "monopam verse members";
717764 `P "Fork a package from another member:";
718718- `Pre "monopam verse fork cohttp --from avsm.bsky.social --url git@github.com:me/cohttp.git";
765765+ `Pre
766766+ "monopam verse fork cohttp --from avsm.bsky.social --url \
767767+ git@github.com:me/cohttp.git";
719768 ]
720769 in
721770 let info = Cmd.info "verse" ~doc ~man in
722722- Cmd.group info
723723- [
724724- verse_members_cmd;
725725- verse_fork_cmd;
726726- ]
771771+ Cmd.group info [ verse_members_cmd; verse_fork_cmd ]
727772728773(* Diff command *)
729774···733778 [
734779 `S Manpage.s_description;
735780 `P
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.";
781781+ "Shows commit diffs from verse members for repositories where they \
782782+ have commits you don't have. This helps you see what changes are \
783783+ available from collaborators.";
739784 `S "OUTPUT";
740740- `P "First shows the verse status summary, then for each repository where \
741741- a verse member is ahead:";
785785+ `P
786786+ "First shows the verse status summary, then for each repository where \
787787+ a verse member is ahead:";
742788 `I ("Repository name", "With the handle and relationship");
743789 `I ("Commits", "List of commits they have that you don't (max 20)");
744790 `S "RELATIONSHIPS";
745791 `I ("+N", "They have N commits you don't have");
746792 `I ("+N/-M", "Diverged: they have N new commits, you have M new commits");
747793 `S "CACHING";
748748- `P "Remote fetches are cached for 1 hour to improve performance. \
749749- Use $(b,--refresh) to force fresh fetches from all remotes.";
794794+ `P
795795+ "Remote fetches are cached for 1 hour to improve performance. Use \
796796+ $(b,--refresh) to force fresh fetches from all remotes.";
750797 `S Manpage.s_examples;
751798 `P "Show diffs for all repos needing attention (uses cache):";
752799 `Pre "monopam diff";
···762809 in
763810 let info = Cmd.info "diff" ~doc ~man in
764811 let arg =
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
812812+ let doc =
813813+ "Repository name or commit SHA. If a 7+ character hex string, shows the \
814814+ patch for that commit. Otherwise filters to that repository. If not \
815815+ specified, shows diffs for all repos needing attention."
816816+ in
768817 Arg.(value & pos 0 (some string) None & info [] ~docv:"REPO|SHA" ~doc)
769818 in
770819 let refresh_arg =
771771- let doc = "Force fresh fetches from all remotes, ignoring the 1-hour cache." in
820820+ let doc =
821821+ "Force fresh fetches from all remotes, ignoring the 1-hour cache."
822822+ in
772823 Arg.(value & flag & info [ "refresh"; "r" ] ~doc)
773824 in
774825 let patch_arg =
···783834 let proc = Eio.Stdenv.process_mgr env in
784835 (* Check if arg looks like a commit SHA *)
785836 match arg with
786786- | Some sha when Monopam.is_commit_sha sha ->
837837+ | Some sha when Monopam.is_commit_sha sha -> (
787838 (* Show patch for specific commit *)
788788- (match Monopam.diff_show_commit ~proc ~fs ~config ~verse_config ~sha ~refresh () with
839839+ match
840840+ Monopam.diff_show_commit ~proc ~fs ~config ~verse_config ~sha ~refresh
841841+ ()
842842+ with
789843 | Some info ->
790790- let short_hash = String.sub info.commit_hash 0 (min 7 (String.length info.commit_hash)) in
844844+ let short_hash =
845845+ String.sub info.commit_hash 0
846846+ (min 7 (String.length info.commit_hash))
847847+ in
791848 Fmt.pr "%a %s (%s/%s)@.@.%s@."
792792- Fmt.(styled `Yellow string) short_hash
793793- info.commit_subject
794794- info.commit_repo info.commit_handle
849849+ Fmt.(styled `Yellow string)
850850+ short_hash info.commit_subject info.commit_repo info.commit_handle
795851 info.commit_patch;
796852 `Ok ()
797853 | None ->
798854 Fmt.epr "Commit %s not found in any verse diff@." sha;
799855 `Error (false, "commit not found"))
800856 | repo ->
801801- let result = Monopam.diff ~proc ~fs ~config ~verse_config ?repo ~refresh ~patch () in
857857+ let result =
858858+ Monopam.diff ~proc ~fs ~config ~verse_config ?repo ~refresh ~patch ()
859859+ in
802860 Fmt.pr "%a" (Monopam.pp_diff_result ~show_patch:patch) result;
803861 `Ok ()
804862 in
805805- Cmd.v info Term.(ret (const run $ arg $ refresh_arg $ patch_arg $ logging_term))
863863+ Cmd.v info
864864+ Term.(ret (const run $ arg $ refresh_arg $ patch_arg $ logging_term))
806865807866(* Pull command - pull from verse members *)
808867···822881 `I ("3.", "$(b,monopam sync) - Sync changes into your monorepo");
823882 `S "MERGING BEHAVIOR";
824883 `P "When you're behind (they have commits you don't):";
825825- `I ("Fast-forward", "If your branch has no new commits, a fast-forward merge is used.");
884884+ `I
885885+ ( "Fast-forward",
886886+ "If your branch has no new commits, a fast-forward merge is used." );
826887 `P "When branches have diverged (both have new commits):";
827888 `I ("Merge commit", "A merge commit is created to combine the histories.");
828889 `S Manpage.s_examples;
···836897 in
837898 let info = Cmd.info "pull" ~doc ~man in
838899 let handle_arg =
839839- let doc = "The verse member handle to pull from (e.g., avsm.bsky.social)." in
900900+ let doc =
901901+ "The verse member handle to pull from (e.g., avsm.bsky.social)."
902902+ in
840903 Arg.(required & pos 0 (some string) None & info [] ~docv:"HANDLE" ~doc)
841904 in
842905 let repo_arg =
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
906906+ let doc =
907907+ "Optional repository to pull from. If not specified, pulls from all \
908908+ repositories where the handle has commits you don't have."
909909+ in
845910 Arg.(value & pos 1 (some string) None & info [] ~docv:"REPO" ~doc)
846911 in
847912 let refresh_arg =
848848- let doc = "Force fresh fetches from all remotes, ignoring the 1-hour cache." in
913913+ let doc =
914914+ "Force fresh fetches from all remotes, ignoring the 1-hour cache."
915915+ in
849916 Arg.(value & flag & info [ "refresh"; "r" ] ~doc)
850917 in
851918 let run handle repo refresh () =
···854921 with_verse_config env @@ fun verse_config ->
855922 let fs = Eio.Stdenv.fs env in
856923 let proc = Eio.Stdenv.process_mgr env in
857857- match Monopam.pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo ~refresh () with
924924+ match
925925+ Monopam.pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo
926926+ ~refresh ()
927927+ with
858928 | Ok result ->
859929 Fmt.pr "%a" Monopam.pp_handle_pull_result result;
860930 if result.repos_failed <> [] then
···864934 `Ok ()
865935 end
866936 else begin
867867- Fmt.pr "@.Run $(b,monopam sync) to merge changes into your monorepo.@.";
937937+ Fmt.pr
938938+ "@.Run $(b,monopam sync) to merge changes into your monorepo.@.";
868939 `Ok ()
869940 end
870941 | Error e ->
871942 Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e;
872943 `Error (false, "pull failed")
873944 in
874874- Cmd.v info Term.(ret (const run $ handle_arg $ repo_arg $ refresh_arg $ logging_term))
945945+ Cmd.v info
946946+ Term.(ret (const run $ handle_arg $ repo_arg $ refresh_arg $ logging_term))
875947876948(* Cherrypick command *)
877949···881953 [
882954 `S Manpage.s_description;
883955 `P
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.";
956956+ "Applies a specific commit from a verse member's fork to your local \
957957+ checkout. Use $(b,monopam diff) to see available commits and their \
958958+ hashes.";
886959 `S "WORKFLOW";
887960 `P "The typical workflow for cherry-picking specific commits:";
888961 `I ("1.", "$(b,monopam diff) - See available commits with their hashes");
···899972 in
900973 let info = Cmd.info "cherrypick" ~doc ~man in
901974 let sha_arg =
902902- let doc = "The commit SHA (or prefix) to cherry-pick. Must be at least 7 characters." in
975975+ let doc =
976976+ "The commit SHA (or prefix) to cherry-pick. Must be at least 7 \
977977+ characters."
978978+ in
903979 Arg.(required & pos 0 (some string) None & info [] ~docv:"SHA" ~doc)
904980 in
905981 let refresh_arg =
906906- let doc = "Force fresh fetches from all remotes, ignoring the 1-hour cache." in
982982+ let doc =
983983+ "Force fresh fetches from all remotes, ignoring the 1-hour cache."
984984+ in
907985 Arg.(value & flag & info [ "refresh"; "r" ] ~doc)
908986 in
909987 let run sha refresh () =
···912990 with_verse_config env @@ fun verse_config ->
913991 let fs = Eio.Stdenv.fs env in
914992 let proc = Eio.Stdenv.process_mgr env in
915915- match Monopam.cherrypick ~proc ~fs ~config ~verse_config ~sha ~refresh () with
993993+ match
994994+ Monopam.cherrypick ~proc ~fs ~config ~verse_config ~sha ~refresh ()
995995+ with
916996 | Ok result ->
917997 Fmt.pr "%a" Monopam.pp_cherrypick_result result;
918998 Fmt.pr "Run $(b,monopam sync) to merge changes into your monorepo.@.";
···9821062 in
9831063 let quiet_arg =
9841064 let doc =
985985- "Quiet mode for cron jobs. Only output if issues are found. \
986986- Exit code reflects health status (0=healthy, 1=warning, 2=critical)."
10651065+ "Quiet mode for cron jobs. Only output if issues are found. Exit code \
10661066+ reflects health status (0=healthy, 1=warning, 2=critical)."
9871067 in
9881068 Arg.(value & flag & info [ "quiet"; "q" ] ~doc)
9891069 in
···9951075 let proc = Eio.Stdenv.process_mgr env in
9961076 let clock = Eio.Stdenv.clock env in
9971077 (* Run sync before analysis unless --no-sync is specified *)
998998- if not no_sync && not quiet then begin
10781078+ if (not no_sync) && not quiet then begin
9991079 Fmt.pr "Syncing workspace before analysis...@.";
10001080 match Monopam.sync ~proc ~fs ~config ?package () with
10011081 | Ok _summary -> ()
···10051085 end
10061086 else if not no_sync then begin
10071087 (* Quiet mode but still sync - just don't print progress *)
10081008- let _ = Monopam.sync ~proc ~fs ~config ?package () in ()
10881088+ let _ = Monopam.sync ~proc ~fs ~config ?package () in
10891089+ ()
10091090 end;
10101091 let report =
10111092 Monopam.Doctor.analyze ~proc ~fs ~config ~verse_config ~clock ?package
···10291110 end
10301111 in
10311112 Cmd.v info
10321032- Term.(ret (const run $ package_arg $ json_arg $ no_sync_arg $ quiet_arg $ logging_term))
11131113+ Term.(
11141114+ ret
11151115+ (const run $ package_arg $ json_arg $ no_sync_arg $ quiet_arg
11161116+ $ logging_term))
1033111710341118(* Feature commands *)
10351119···10431127 [
10441128 `S Manpage.s_description;
10451129 `P
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.";
11301130+ "Creates a git worktree at $(b,root/work/<name>) with a new branch \
11311131+ named $(b,<name>). This allows parallel development on separate \
11321132+ branches, useful for having multiple Claude instances working on \
11331133+ different features.";
10491134 `S "HOW IT WORKS";
10501135 `P "The command:";
10511136 `I ("1.", "Creates the $(b,work/) directory if it doesn't exist");
···10531138 `I ("3.", "Checks out a new branch named $(b,<name>)");
10541139 `S Manpage.s_examples;
10551140 `P "Create a feature worktree:";
10561056- `Pre "monopam feature add my-feature\n\
10571057- cd work/my-feature\n\
10581058- # Now you can work here independently";
11411141+ `Pre
11421142+ "monopam feature add my-feature\n\
11431143+ cd work/my-feature\n\
11441144+ # Now you can work here independently";
10591145 `P "Have multiple Claudes work in parallel:";
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";
11461146+ `Pre
11471147+ "# Terminal 1\n\
11481148+ monopam feature add auth-system\n\
11491149+ cd work/auth-system && claude\n\n\
11501150+ # Terminal 2\n\
11511151+ monopam feature add api-refactor\n\
11521152+ cd work/api-refactor && claude";
10661153 ]
10671154 in
10681155 let info = Cmd.info "add" ~doc ~man in
···10731160 let proc = Eio.Stdenv.process_mgr env in
10741161 match Monopam.Feature.add ~proc ~fs ~config:verse_config ~name () with
10751162 | Ok entry ->
10761076- Fmt.pr "Created feature worktree '%s' at %a@." entry.name Fpath.pp entry.path;
11631163+ Fmt.pr "Created feature worktree '%s' at %a@." entry.name Fpath.pp
11641164+ entry.path;
10771165 Fmt.pr "@.To start working:@.";
10781166 Fmt.pr " cd %a@." Fpath.pp entry.path;
10791167 `Ok ()
···11101198 with_verse_config env @@ fun verse_config ->
11111199 let fs = Eio.Stdenv.fs env in
11121200 let proc = Eio.Stdenv.process_mgr env in
11131113- match Monopam.Feature.remove ~proc ~fs ~config:verse_config ~name ~force () with
12011201+ match
12021202+ Monopam.Feature.remove ~proc ~fs ~config:verse_config ~name ~force ()
12031203+ with
11141204 | Ok () ->
11151205 Fmt.pr "Removed feature worktree '%s'.@." name;
11161206 `Ok ()
···11181208 Fmt.epr "Error: %a@." Monopam.Feature.pp_error_with_hint e;
11191209 `Error (false, "feature remove failed")
11201210 in
11211121- Cmd.v info Term.(ret (const run $ feature_name_arg $ force_arg $ logging_term))
12111211+ Cmd.v info
12121212+ Term.(ret (const run $ feature_name_arg $ force_arg $ logging_term))
1122121311231214let feature_list_cmd =
11241215 let doc = "List all feature worktrees" in
···11371228 let fs = Eio.Stdenv.fs env in
11381229 let proc = Eio.Stdenv.process_mgr env in
11391230 let entries = Monopam.Feature.list ~proc ~fs ~config:verse_config () in
11401140- if entries = [] then
11411141- Fmt.pr "No feature worktrees found.@."
12311231+ if entries = [] then Fmt.pr "No feature worktrees found.@."
11421232 else begin
11431233 Fmt.pr "Feature worktrees:@.";
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
12341234+ List.iter
12351235+ (fun entry ->
12361236+ Fmt.pr " %s -> %a (branch: %s)@." entry.Monopam.Feature.name Fpath.pp
12371237+ entry.Monopam.Feature.path entry.Monopam.Feature.branch)
12381238+ entries
11501239 end;
11511240 `Ok ()
11521241 in
···11631252 working on different features simultaneously.";
11641253 `S "WORKSPACE STRUCTURE";
11651254 `P "Feature worktrees are created in the $(b,work/) directory:";
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- └── ...";
12551255+ `Pre
12561256+ "root/\n\
12571257+ ├── mono/ # Main monorepo\n\
12581258+ ├── work/\n\
12591259+ │ ├── feature-a/ # Worktree on branch 'feature-a'\n\
12601260+ │ └── feature-b/ # Worktree on branch 'feature-b'\n\
12611261+ └── ...";
11721262 `S "COMMANDS";
11731263 `I ("add <name>", "Create a new feature worktree");
11741264 `I ("remove <name>", "Remove a feature worktree");
11751265 `I ("list", "List all feature worktrees");
11761266 `S "WORKFLOW";
11771267 `P "Typical workflow for parallel development:";
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";
12681268+ `Pre
12691269+ "# Create feature worktrees\n\
12701270+ monopam feature add auth-system\n\
12711271+ monopam feature add api-cleanup\n\n\
12721272+ # Work in each worktree independently\n\
12731273+ cd work/auth-system && claude\n\
12741274+ cd work/api-cleanup && claude\n\n\
12751275+ # When done, merge branches back to main\n\
12761276+ cd mono\n\
12771277+ git merge auth-system\n\
12781278+ git merge api-cleanup\n\n\
12791279+ # Clean up worktrees\n\
12801280+ monopam feature remove auth-system\n\
12811281+ monopam feature remove api-cleanup";
11911282 ]
11921283 in
11931284 let info = Cmd.info "feature" ~doc ~man in
···12091300 .devcontainer configuration, it will be created automatically.";
12101301 `P
12111302 "This is the recommended way to get started with monopam. The \
12121212- devcontainer provides a consistent environment with OCaml, opam, \
12131213- and all required tools pre-installed.";
13031303+ devcontainer provides a consistent environment with OCaml, opam, and \
13041304+ all required tools pre-installed.";
12141305 `S "WHAT IT DOES";
12151306 `P "For a new directory (no .devcontainer/):";
12161307 `I ("1.", "Creates the target directory if needed");
···12221313 `I ("1.", "Starts the devcontainer if not running");
12231314 `I ("2.", "Opens an interactive shell inside the container");
12241315 `S Manpage.s_options;
12251225- `P "Use $(b,--url) to specify a custom devcontainer.json URL if you want \
12261226- to use a different base configuration.";
13161316+ `P
13171317+ "Use $(b,--url) to specify a custom devcontainer.json URL if you want \
13181318+ to use a different base configuration.";
12271319 `S Manpage.s_examples;
12281320 `P "Create a new devcontainer workspace:";
12291321 `Pre "monopam devcontainer ~/my-ocaml-project";
12301322 `P "Enter an existing devcontainer:";
12311323 `Pre "monopam devcontainer ~/my-ocaml-project";
12321324 `P "Use a custom devcontainer.json:";
12331233- `Pre "monopam devcontainer --url https://example.com/devcontainer.json ~/project";
13251325+ `Pre
13261326+ "monopam devcontainer --url https://example.com/devcontainer.json \
13271327+ ~/project";
12341328 ]
12351329 in
12361330 let info = Cmd.info "devcontainer" ~doc ~man in
···12391333 Arg.(required & pos 0 (some string) None & info [] ~docv:"PATH" ~doc)
12401334 in
12411335 let url_arg =
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)
13361336+ let doc =
13371337+ "URL to fetch devcontainer.json from. Defaults to the \
13381338+ claude-ocaml-devcontainer template."
13391339+ in
13401340+ Arg.(
13411341+ value
13421342+ & opt string default_devcontainer_url
13431343+ & info [ "url" ] ~docv:"URL" ~doc)
12441344 in
12451345 let run path url () =
12461346 (* Resolve to absolute path *)
12471347 let abs_path =
12481248- if Filename.is_relative path then
12491249- Filename.concat (Sys.getcwd ()) path
13481348+ if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path
12501349 else path
12511350 in
12521351 let devcontainer_dir = Filename.concat abs_path ".devcontainer" in
12531253- let devcontainer_json = Filename.concat devcontainer_dir "devcontainer.json" in
13521352+ let devcontainer_json =
13531353+ Filename.concat devcontainer_dir "devcontainer.json"
13541354+ in
12541355 (* Check if .devcontainer exists *)
12551255- let needs_init = not (Sys.file_exists devcontainer_dir && Sys.is_directory devcontainer_dir) in
13561356+ let needs_init =
13571357+ not (Sys.file_exists devcontainer_dir && Sys.is_directory devcontainer_dir)
13581358+ in
12561359 if needs_init then begin
12571360 Fmt.pr "Initializing devcontainer in %s...@." abs_path;
12581361 (* Create directories *)
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, _, _) -> ());
13621362+ (try Unix.mkdir abs_path 0o755
13631363+ with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
13641364+ (try Unix.mkdir devcontainer_dir 0o755
13651365+ with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
12611366 (* Fetch devcontainer.json using curl *)
12621367 Fmt.pr "Fetching devcontainer.json from %s...@." url;
12631263- let curl_cmd = Printf.sprintf "curl -fsSL '%s' -o '%s'" url devcontainer_json in
13681368+ let curl_cmd =
13691369+ Printf.sprintf "curl -fsSL '%s' -o '%s'" url devcontainer_json
13701370+ in
12641371 let ret = Sys.command curl_cmd in
12651372 if ret <> 0 then begin
12661266- Fmt.epr "Error: Failed to fetch devcontainer.json (curl exit code %d)@." ret;
13731373+ Fmt.epr "Error: Failed to fetch devcontainer.json (curl exit code %d)@."
13741374+ ret;
12671375 exit 1
12681376 end;
12691377 Fmt.pr "Created %s@." devcontainer_json;
12701378 (* Build and start the devcontainer *)
12711379 Fmt.pr "Building devcontainer (this may take a while on first run)...@.";
12721272- let up_cmd = Printf.sprintf "npx @devcontainers/cli up --workspace-folder '%s' --remove-existing-container" abs_path in
13801380+ let up_cmd =
13811381+ Printf.sprintf
13821382+ "npx @devcontainers/cli up --workspace-folder '%s' \
13831383+ --remove-existing-container"
13841384+ abs_path
13851385+ in
12731386 let ret = Sys.command up_cmd in
12741387 if ret <> 0 then begin
12751388 Fmt.epr "Error: Failed to start devcontainer (exit code %d)@." ret;
···12781391 end;
12791392 (* Exec into the devcontainer *)
12801393 Fmt.pr "Entering devcontainer...@.";
12811281- let exec_cmd = Printf.sprintf "npx @devcontainers/cli exec --workspace-folder '%s' bash -l" abs_path in
13941394+ let exec_cmd =
13951395+ Printf.sprintf
13961396+ "npx @devcontainers/cli exec --workspace-folder '%s' bash -l" abs_path
13971397+ in
12821398 let ret = Sys.command exec_cmd in
12831399 if ret <> 0 then
12841400 `Error (false, Printf.sprintf "devcontainer exec failed with code %d" ret)
12851285- else
12861286- `Ok ()
14011401+ else `Ok ()
12871402 in
12881403 Cmd.v info Term.(ret (const run $ path_arg $ url_arg $ logging_term))
12891404···13161431 with the extracted history, then re-adds mono/<name>/ as a subtree.";
13171432 `S "FORK MODES";
13181433 `P "The fork command handles two scenarios:";
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.");
14341434+ `I
14351435+ ( "Subtree with history",
14361436+ "For subtrees added via $(b,git subtree add) or $(b,monopam join), \
14371437+ the command uses $(b,git subtree split) to extract the full commit \
14381438+ history into the new repository." );
14391439+ `I
14401440+ ( "Fresh package",
14411441+ "For packages created directly in mono/ without subtree history, the \
14421442+ command copies the files and creates an initial commit. This is \
14431443+ useful for new packages you've developed locally." );
13251444 `S "WHAT IT DOES";
13261445 `P "The fork command performs a complete workflow in one step:";
13271446 `I ("1.", "Analyzes mono/<name>/ to detect fork mode");
13281447 `I ("2.", "Builds an action plan and shows discovery details");
13291448 `I ("3.", "Prompts for confirmation (use $(b,--yes) to skip)");
13301449 `I ("4.", "Creates a new git repo at src/<name>/");
13311331- `I ("5.", "Extracts history (subtree split) or copies files (fresh package)");
14501450+ `I
14511451+ ( "5.",
14521452+ "Extracts history (subtree split) or copies files (fresh package)" );
13321453 `I ("6.", "Removes mono/<name>/ from git and commits");
13331454 `I ("7.", "Re-adds mono/<name>/ as a proper subtree from src/<name>/");
13341455 `I ("8.", "Updates sources.toml with $(b,origin = \"fork\")");
···13811502 let mono_path = Monopam.Config.mono_path config in
13821503 let subtree_path = Fpath.(mono_path / name) in
13831504 let knot = Monopam.Config.knot config in
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 *)
15051505+ let suggested =
15061506+ Monopam.Fork_join.suggest_push_url ~fs ~knot subtree_path
15071507+ in
15081508+ if yes || dry_run then suggested
15091509+ (* Use suggested or None without prompting *)
13871510 else begin
13881511 match suggested with
13891389- | Some default_url ->
15121512+ | Some default_url -> (
13901513 Fmt.pr "Remote push URL [%s]: %!" default_url;
13911391- (match prompt_string "" with
13921392- | None -> Some default_url (* User pressed enter, use default *)
13931393- | Some entered -> Some entered)
15141514+ match prompt_string "" with
15151515+ | None -> Some default_url (* User pressed enter, use default *)
15161516+ | Some entered -> Some entered)
13941517 | None ->
13951518 Fmt.pr "Remote push URL (leave empty to skip): %!";
13961519 prompt_string ""
13971520 end
13981521 in
13991522 (* Build the plan *)
14001400- match Monopam.Fork_join.plan_fork ~proc ~fs ~config ~name ?push_url:url ~dry_run () with
15231523+ match
15241524+ Monopam.Fork_join.plan_fork ~proc ~fs ~config ~name ?push_url:url ~dry_run
15251525+ ()
15261526+ with
14011527 | Error e ->
14021528 Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
14031529 `Error (false, "fork failed")
···14051531 (* Print discovery and actions *)
14061532 Fmt.pr "Analyzing fork request for '%s'...@.@." name;
14071533 Fmt.pr "Discovery:@.%a@." Monopam.Fork_join.pp_discovery plan.discovery;
14081408- (match url with
14091409- | Some u -> Fmt.pr " Remote URL: %s@." u
14101410- | None -> ());
15341534+ (match url with Some u -> Fmt.pr " Remote URL: %s@." u | None -> ());
14111535 Fmt.pr "@.Actions to perform:@.";
14121412- List.iteri (fun i action ->
14131413- Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action
14141414- ) plan.actions;
15361536+ List.iteri
15371537+ (fun i action ->
15381538+ Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action)
15391539+ plan.actions;
14151540 Fmt.pr "@.";
14161541 (* Prompt for confirmation unless --yes or --dry-run *)
14171542 let proceed =
14181543 if dry_run then begin
14191544 Fmt.pr "(dry-run mode - no changes will be made)@.";
14201545 true
14211421- end else if yes then
14221422- true
14231423- else
14241424- confirm "Proceed?"
15461546+ end
15471547+ else if yes then true
15481548+ else confirm "Proceed?"
14251549 in
14261550 if not proceed then begin
14271551 Fmt.pr "Cancelled.@.";
14281552 `Ok ()
14291429- end else begin
15531553+ end
15541554+ else begin
14301555 (* Execute the plan *)
14311556 match Monopam.Fork_join.execute_fork_plan ~proc ~fs plan with
14321557 | Ok result ->
···14351560 Fmt.pr "@.Next steps:@.";
14361561 Fmt.pr " 1. Review the new repo: cd src/%s@." result.name;
14371562 match url with
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>@."
15631563+ | Some _ ->
15641564+ Fmt.pr " 2. Push to remote: git push -u origin main@."
15651565+ | None ->
15661566+ Fmt.pr " 2. Add a remote: git remote add origin <url>@."
14401567 end;
14411568 `Ok ()
14421569 | Error e ->
···14441571 `Error (false, "fork failed")
14451572 end
14461573 in
14471447- Cmd.v info Term.(ret (const run $ name_arg $ url_arg $ dry_run_arg $ yes_arg $ logging_term))
15741574+ Cmd.v info
15751575+ Term.(
15761576+ ret (const run $ name_arg $ url_arg $ dry_run_arg $ yes_arg $ logging_term))
1448157714491578(* Join command *)
14501579···14591588 `S "JOIN MODES";
14601589 `P "The join command handles multiple scenarios:";
14611590 `I ("URL join", "Clone from a git URL and add as subtree (default).");
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).");
15911591+ `I
15921592+ ( "Local directory join",
15931593+ "Import from a local filesystem path. If the path is a git repo, \
15941594+ uses it directly. If not, initializes a new repo." );
15951595+ `I
15961596+ ( "Verse join",
15971597+ "Join from a verse member's repository using $(b,--from)." );
14651598 `S "WHAT IT DOES";
14661599 `P "The join command:";
14671600 `I ("1.", "Analyzes the source (URL or local path)");
···14721605 `I ("6.", "Updates sources.toml with $(b,origin = \"join\")");
14731606 `S "JOINING FROM VERSE";
14741607 `P "To join a package from a verse member, use $(b,--from):";
14751475- `Pre "monopam join --from avsm.bsky.social --url git@github.com:me/cohttp.git cohttp";
16081608+ `Pre
16091609+ "monopam join --from avsm.bsky.social --url \
16101610+ git@github.com:me/cohttp.git cohttp";
14761611 `P "This will:";
14771612 `I ("-", "Look up the package in their opam-repo");
14781613 `I ("-", "Find all packages from the same git repository");
···14931628 `P "Join with a custom name using --as:";
14941629 `Pre "monopam join https://github.com/someone/some-lib --as my-lib";
14951630 `P "Join with upstream tracking (for forks):";
14961496- `Pre "monopam join https://github.com/me/cohttp --upstream https://github.com/mirage/cohttp";
16311631+ `Pre
16321632+ "monopam join https://github.com/me/cohttp --upstream \
16331633+ https://github.com/mirage/cohttp";
14971634 `P "Join from a verse member:";
14981498- `Pre "monopam join cohttp --from avsm.bsky.social --url git@github.com:me/cohttp.git";
16351635+ `Pre
16361636+ "monopam join cohttp --from avsm.bsky.social --url \
16371637+ git@github.com:me/cohttp.git";
14991638 `P "Preview what would be done:";
15001639 `Pre "monopam join https://github.com/someone/lib --dry-run";
15011640 `P "Join without confirmation:";
···15371676 let fs = Eio.Stdenv.fs env in
15381677 let proc = Eio.Stdenv.process_mgr env in
15391678 match from with
15401540- | Some handle ->
16791679+ | Some handle -> (
15411680 (* Join from verse member - requires --url for your fork *)
15421681 (* Uses legacy API as it involves verse-specific operations *)
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 ->
16821682+ match fork_url with
16831683+ | None ->
16841684+ Fmt.epr "Error: --url is required when using --from@.";
16851685+ `Error (false, "--url required")
16861686+ | Some fork_url -> (
16871687+ match
16881688+ Monopam.Fork_join.join_from_verse ~proc ~fs ~config
16891689+ ~verse_config:config ~package:url_or_pkg ~handle ~fork_url
16901690+ ~dry_run ()
16911691+ with
16921692+ | Ok result ->
16931693+ if dry_run then begin
16941694+ Fmt.pr "Would join '%s' from %s:@." result.name
16951695+ (Option.value ~default:"verse" result.from_handle);
16961696+ Fmt.pr " Source: %s@." result.source_url;
16971697+ Option.iter
16981698+ (fun u -> Fmt.pr " Upstream: %s@." u)
16991699+ result.upstream_url;
17001700+ Fmt.pr " Packages: %a@."
17011701+ Fmt.(list ~sep:(any ", ") string)
17021702+ result.packages_added
17031703+ end
17041704+ else begin
17051705+ Fmt.pr "%a@." Monopam.Fork_join.pp_join_result result;
17061706+ Fmt.pr "@.Next steps:@.";
17071707+ Fmt.pr
17081708+ " 1. Commit the opam changes: cd opam-repo && git add -A \
17091709+ && git commit@.";
17101710+ Fmt.pr " 2. Run $(b,monopam sync) to synchronize@."
17111711+ end;
17121712+ `Ok ()
17131713+ | Error e ->
17141714+ Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
17151715+ `Error (false, "join failed")))
17161716+ | None -> (
15671717 (* Normal join from URL or local path - use plan-based workflow *)
15681718 let source = match fork_url with Some u -> u | None -> url_or_pkg in
15691569- let name = match fork_url with Some _ -> Some url_or_pkg | None -> as_name in
17191719+ let name =
17201720+ match fork_url with Some _ -> Some url_or_pkg | None -> as_name
17211721+ in
15701722 (* Build the plan *)
15711571- match Monopam.Fork_join.plan_join ~proc ~fs ~config ~source ?name ?upstream ~dry_run () with
17231723+ match
17241724+ Monopam.Fork_join.plan_join ~proc ~fs ~config ~source ?name ?upstream
17251725+ ~dry_run ()
17261726+ with
15721727 | Error e ->
15731728 Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
15741729 `Error (false, "join failed")
···15811736 (if is_local then "local directory" else "remote URL");
15821737 Fmt.pr "%a" Monopam.Fork_join.pp_discovery plan.discovery;
15831738 Fmt.pr "@.Actions to perform:@.";
15841584- List.iteri (fun i action ->
15851585- Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action
15861586- ) plan.actions;
17391739+ List.iteri
17401740+ (fun i action ->
17411741+ Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action)
17421742+ plan.actions;
15871743 Fmt.pr "@.";
15881744 (* Prompt for confirmation unless --yes or --dry-run *)
15891745 let proceed =
15901746 if dry_run then begin
15911747 Fmt.pr "(dry-run mode - no changes will be made)@.";
15921748 true
15931593- end else if yes then
15941594- true
15951595- else
15961596- confirm "Proceed?"
17491749+ end
17501750+ else if yes then true
17511751+ else confirm "Proceed?"
15971752 in
15981753 if not proceed then begin
15991754 Fmt.pr "Cancelled.@.";
16001755 `Ok ()
16011601- end else begin
17561756+ end
17571757+ else begin
16021758 (* Execute the plan *)
16031759 match Monopam.Fork_join.execute_join_plan ~proc ~fs plan with
16041760 | Ok result ->
···16111767 | Error e ->
16121768 Fmt.epr "Error: %a@." Monopam.Fork_join.pp_error_with_hint e;
16131769 `Error (false, "join failed")
16141614- end
17701770+ end)
16151771 in
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))
17721772+ Cmd.v info
17731773+ Term.(
17741774+ ret
17751775+ (const run $ url_or_pkg_arg $ as_arg $ upstream_arg $ from_arg
17761776+ $ fork_url_arg $ dry_run_arg $ yes_arg $ logging_term))
1617177716181778(* Rejoin command *)
16191779···16411801 `I ("1.", "Verifies src/<name>/ exists and is a git repo");
16421802 `I ("2.", "Verifies mono/<name>/ does not exist");
16431803 `I ("3.", "Prompts for confirmation (use $(b,--yes) to skip)");
16441644- `I ("4.", "Uses $(b,git subtree add) to bring src/<name>/ into mono/<name>/");
18041804+ `I
18051805+ ( "4.",
18061806+ "Uses $(b,git subtree add) to bring src/<name>/ into mono/<name>/" );
16451807 `S Manpage.s_examples;
16461808 `P "Re-add a package from src/:";
16471809 `Pre "monopam rejoin my-lib";
···16791841 Fmt.pr "Analyzing rejoin request for '%s'...@.@." name;
16801842 Fmt.pr "Discovery:@.%a@." Monopam.Fork_join.pp_discovery plan.discovery;
16811843 Fmt.pr "@.Actions to perform:@.";
16821682- List.iteri (fun i action ->
16831683- Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action
16841684- ) plan.actions;
18441844+ List.iteri
18451845+ (fun i action ->
18461846+ Fmt.pr " %d. %a@." (i + 1) Monopam.Fork_join.pp_action action)
18471847+ plan.actions;
16851848 Fmt.pr "@.";
16861849 (* Prompt for confirmation unless --yes or --dry-run *)
16871850 let proceed =
16881851 if dry_run then begin
16891852 Fmt.pr "(dry-run mode - no changes will be made)@.";
16901853 true
16911691- end else if yes then
16921692- true
16931693- else
16941694- confirm "Proceed?"
18541854+ end
18551855+ else if yes then true
18561856+ else confirm "Proceed?"
16951857 in
16961858 if not proceed then begin
16971859 Fmt.pr "Cancelled.@.";
16981860 `Ok ()
16991699- end else begin
18611861+ end
18621862+ else begin
17001863 (* Execute the plan *)
17011864 match Monopam.Fork_join.execute_join_plan ~proc ~fs plan with
17021865 | Ok result ->
···17121875 `Error (false, "rejoin failed")
17131876 end
17141877 in
17151715- Cmd.v info Term.(ret (const run $ name_arg $ dry_run_arg $ yes_arg $ logging_term))
18781878+ Cmd.v info
18791879+ Term.(ret (const run $ name_arg $ dry_run_arg $ yes_arg $ logging_term))
1716188017171881(* Site command *)
17181882···17221886 [
17231887 `S Manpage.s_description;
17241888 `P
17251725- "Generates a static index.html file that maps the monoverse, showing all \
17261726- verse members, their packages, and the relationships between them.";
18891889+ "Generates a static index.html file that maps the monoverse, showing \
18901890+ all verse members, their packages, and the relationships between \
18911891+ them.";
17271892 `S "OUTPUT";
17281893 `P "The generated site includes:";
17291729- `I ("Members", "All verse members with links to their monorepo and opam repos");
18941894+ `I
18951895+ ( "Members",
18961896+ "All verse members with links to their monorepo and opam repos" );
17301897 `I ("Summary", "Overview of common libraries and member-specific packages");
17311898 `I ("Repository Details", "Each shared repo with packages and fork status");
17321899 `S "FORK STATUS";
···17541921 let info = Cmd.info "site" ~doc ~man in
17551922 let output_arg =
17561923 let doc = "Output file path. Defaults to mono/index.html." in
17571757- Arg.(value & opt (some string) None & info [ "o"; "output" ] ~docv:"FILE" ~doc)
19241924+ Arg.(
19251925+ value & opt (some string) None & info [ "o"; "output" ] ~docv:"FILE" ~doc)
17581926 in
17591927 let stdout_arg =
17601928 let doc = "Print HTML to stdout instead of writing to file." in
17611929 Arg.(value & flag & info [ "stdout" ] ~doc)
17621930 in
17631931 let status_arg =
17641764- let doc = "Include fork status (ahead/behind) for each repository. \
17651765- This fetches from remotes and may be slower." in
19321932+ let doc =
19331933+ "Include fork status (ahead/behind) for each repository. This fetches \
19341934+ from remotes and may be slower."
19351935+ in
17661936 Arg.(value & flag & info [ "status"; "s" ] ~doc)
17671937 in
17681938 let run output to_stdout with_status () =
···17741944 (* Pull/clone registry to get latest metadata *)
17751945 Fmt.pr "Syncing registry...@.";
17761946 let registry =
17771777- match Monopam.Verse_registry.clone_or_pull ~proc ~fs:(fs :> _ Eio.Path.t) ~config:verse_config () with
19471947+ match
19481948+ Monopam.Verse_registry.clone_or_pull ~proc
19491949+ ~fs:(fs :> _ Eio.Path.t)
19501950+ ~config:verse_config ()
19511951+ with
17781952 | Ok r -> r
17791953 | Error msg ->
17801954 Fmt.epr "Warning: Could not sync registry: %s@." msg;
17811781- Monopam.Verse_registry.{ name = "opamverse"; description = None; members = [] }
19551955+ Monopam.Verse_registry.
19561956+ { name = "opamverse"; description = None; members = [] }
17821957 in
17831958 (* Compute forks if --status is requested *)
17841959 let forks =
17851960 if with_status then begin
17861961 Fmt.pr "Computing fork status...@.";
17871787- Some (Monopam.Forks.compute ~proc ~fs:(fs :> _ Eio.Path.t)
17881788- ~verse_config ~monopam_config ())
17891789- end else None
19621962+ Some
19631963+ (Monopam.Forks.compute ~proc
19641964+ ~fs:(fs :> _ Eio.Path.t)
19651965+ ~verse_config ~monopam_config ())
19661966+ end
19671967+ else None
17901968 in
17911969 if to_stdout then begin
17921792- let html = Monopam.Site.generate ~fs:(fs :> _ Eio.Path.t) ~config:verse_config ?forks ~registry () in
19701970+ let html =
19711971+ Monopam.Site.generate
19721972+ ~fs:(fs :> _ Eio.Path.t)
19731973+ ~config:verse_config ?forks ~registry ()
19741974+ in
17931975 print_string html;
17941976 `Ok ()
17951795- end else begin
19771977+ end
19781978+ else begin
17961979 let output_path =
17971980 match output with
17981981 | Some p -> (
17991982 match Fpath.of_string p with
18001983 | Ok fp -> fp
18011984 | Error (`Msg _) -> Fpath.v p)
18021802- | None -> Fpath.(Monopam.Verse_config.mono_path verse_config / "index.html")
19851985+ | None ->
19861986+ Fpath.(Monopam.Verse_config.mono_path verse_config / "index.html")
18031987 in
18041804- match Monopam.Site.write ~fs:(fs :> _ Eio.Path.t) ~config:verse_config ?forks ~registry ~output_path () with
19881988+ match
19891989+ Monopam.Site.write
19901990+ ~fs:(fs :> _ Eio.Path.t)
19911991+ ~config:verse_config ?forks ~registry ~output_path ()
19921992+ with
18051993 | Ok () ->
18061994 Fmt.pr "Site generated: %a@." Fpath.pp output_path;
18071995 `Ok ()
···18101998 `Error (false, "site generation failed")
18111999 end
18122000 in
18131813- Cmd.v info Term.(ret (const run $ output_arg $ stdout_arg $ status_arg $ logging_term))
20012001+ Cmd.v info
20022002+ Term.(ret (const run $ output_arg $ stdout_arg $ status_arg $ logging_term))
1814200318152004(* Main command group *)
18162005···18282017 pre-installed.";
18292018 `S "QUICK START";
18302019 `P "Start by creating a devcontainer workspace:";
18311831- `Pre
18321832- "monopam devcontainer ~/tangled";
20202020+ `Pre "monopam devcontainer ~/tangled";
18332021 `P "Inside the devcontainer, initialize your workspace:";
18341834- `Pre
18351835- "cd ~/tangled\n\
18361836- monopam init --handle yourname.bsky.social\n\
18371837- cd mono";
20222022+ `Pre "cd ~/tangled\nmonopam init --handle yourname.bsky.social\ncd mono";
18382023 `P "Daily workflow:";
18392024 `Pre
18402025 "cd ~/tangled/mono\n\
···19132098 in
19142099 let info = Cmd.info "monopam" ~version:"%%VERSION%%" ~doc ~man in
19152100 Cmd.group info
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 ]
21012101+ [
21022102+ init_cmd;
21032103+ status_cmd;
21042104+ diff_cmd;
21052105+ pull_cmd;
21062106+ cherrypick_cmd;
21072107+ sync_cmd;
21082108+ changes_cmd;
21092109+ opam_cmd;
21102110+ doctor_cmd;
21112111+ verse_cmd;
21122112+ feature_cmd;
21132113+ fork_cmd;
21142114+ join_cmd;
21152115+ rejoin_cmd;
21162116+ devcontainer_cmd;
21172117+ site_cmd;
21182118+ ]
1917211919182120let () = exit (Cmd.eval main_cmd)
+35-22
lib/config.ml
···11(** Unified configuration for monopam.
2233- Configuration is stored in TOML format at ~/.config/monopam/opamverse.toml *)
33+ Configuration is stored in TOML format at ~/.config/monopam/opamverse.toml
44+*)
4556let app_name = "monopam"
67···2223(** {1 Paths Configuration} *)
23242425type paths = {
2525- mono : string; (** Monorepo directory (default: "mono") *)
2626- src : string; (** Source checkouts directory (default: "src") *)
2626+ mono : string; (** Monorepo directory (default: "mono") *)
2727+ src : string; (** Source checkouts directory (default: "src") *)
2728 verse : string; (** Verse directory (default: "verse") *)
2829}
2930···8687let xdg_cache_home () =
8788 match Sys.getenv_opt "XDG_CACHE_HOME" with
8889 | Some dir when dir <> "" -> Fpath.v dir
8989- | _ ->
9090+ | _ -> (
9091 match Sys.getenv_opt "HOME" with
9192 | Some home -> Fpath.(v home / ".cache")
9292- | None -> Fpath.v "/tmp"
9393+ | None -> Fpath.v "/tmp")
93949495let config_dir () = Fpath.(xdg_config_home () / app_name)
9596let data_dir () = Fpath.(xdg_data_home () / app_name)
···99100100101(** {1 Construction} *)
101102102102-(** Derive knot (git push server) from handle.
103103- E.g., "anil.recoil.org" -> "git.recoil.org" *)
103103+(** Derive knot (git push server) from handle. E.g., "anil.recoil.org" ->
104104+ "git.recoil.org" *)
104105let default_knot_from_handle handle =
105106 match String.index_opt handle '.' with
106107 | None -> "git." ^ handle (* fallback *)
···109110 "git." ^ domain
110111111112let create ~root ~handle ?knot ?(packages = []) ?(paths = default_paths) () =
112112- let knot = match knot with Some k -> k | None -> default_knot_from_handle handle in
113113+ let knot =
114114+ match knot with Some k -> k | None -> default_knot_from_handle handle
115115+ in
113116 { root; handle; knot; packages; paths }
114117115118let with_package_override t ~name ?branch:branch_opt () =
···145148 Tomlt.(
146149 Table.(
147150 obj (fun mono src verse ->
148148- { mono = Option.value ~default:default_paths.mono mono;
149149- src = Option.value ~default:default_paths.src src;
150150- verse = Option.value ~default:default_paths.verse verse })
151151+ {
152152+ mono = Option.value ~default:default_paths.mono mono;
153153+ src = Option.value ~default:default_paths.src src;
154154+ verse = Option.value ~default:default_paths.verse verse;
155155+ })
151156 |> opt_mem "mono" string ~enc:(fun p -> Some p.mono)
152157 |> opt_mem "src" string ~enc:(fun p -> Some p.src)
153158 |> opt_mem "verse" string ~enc:(fun p -> Some p.verse)
···194199 Tomlt.(
195200 Table.(
196201 obj (fun pkgs -> pkgs)
197197- |> keep_unknown ~enc:(fun pkgs -> pkgs)
198198- (Mems.assoc Package_config.codec)
202202+ |> keep_unknown ~enc:(fun pkgs -> pkgs) (Mems.assoc Package_config.codec)
199203 |> finish))
200204201205let codec : t Tomlt.t =
···205209 let packages = Option.value ~default:[] packages in
206210 let paths = Option.value ~default:default_paths paths in
207211 let knot = Option.value ~default:default_knot identity.i_knot in
208208- { root = workspace.w_root; handle = identity.i_handle; knot; packages; paths })
212212+ {
213213+ root = workspace.w_root;
214214+ handle = identity.i_handle;
215215+ knot;
216216+ packages;
217217+ paths;
218218+ })
209219 |> mem "workspace" workspace_codec ~enc:(fun t -> { w_root = t.root })
210210- |> mem "identity" identity_codec ~enc:(fun t -> { i_handle = t.handle; i_knot = Some t.knot })
211211- |> opt_mem "packages" packages_table_codec
212212- ~enc:(fun t -> if t.packages = [] then None else Some t.packages)
213213- |> opt_mem "paths" paths_codec
214214- ~enc:(fun t -> if t.paths = default_paths then None else Some t.paths)
220220+ |> mem "identity" identity_codec ~enc:(fun t ->
221221+ { i_handle = t.handle; i_knot = Some t.knot })
222222+ |> opt_mem "packages" packages_table_codec ~enc:(fun t ->
223223+ if t.packages = [] then None else Some t.packages)
224224+ |> opt_mem "paths" paths_codec ~enc:(fun t ->
225225+ if t.paths = default_paths then None else Some t.paths)
215226 |> finish))
216227217228(** {1 Validation} *)
···250261 | `Regular_file -> (
251262 try Ok (Tomlt_eio.decode_path_exn codec ~fs path_str) with
252263 | Failure msg -> Error (Printf.sprintf "Invalid config: %s" msg)
253253- | exn -> Error (Printf.sprintf "Error loading config: %s" (Printexc.to_string exn)))
264264+ | exn ->
265265+ Error
266266+ (Printf.sprintf "Error loading config: %s" (Printexc.to_string exn))
267267+ )
254268 | _ -> Error (Printf.sprintf "Config file not found: %s" path_str)
255269 | exception _ -> Error (Printf.sprintf "Config file not found: %s" path_str)
256270···273287 @[<hov 2>identity:@ handle=%s@ knot=%s@]@,\
274288 @[<hov 2>paths:@ mono=%s@ src=%s@ verse=%s@]@,\
275289 packages=%d@]"
276276- Fpath.pp t.root t.handle t.knot
277277- t.paths.mono t.paths.src t.paths.verse
290290+ Fpath.pp t.root t.handle t.knot t.paths.mono t.paths.src t.paths.verse
278291 (List.length t.packages)
+13-11
lib/config.mli
···11(** Unified configuration for monopam.
2233- Configuration is stored in TOML format at [~/.config/monopam/opamverse.toml].
33+ Configuration is stored in TOML format at
44+ [~/.config/monopam/opamverse.toml].
4556 The config stores:
67 - Workspace root and custom paths
···2425 (** [branch t] returns the branch override for this package, if set. *)
2526end
26272828+type paths = {
2929+ mono : string; (** Monorepo directory (default: "mono") *)
3030+ src : string; (** Source checkouts directory (default: "src") *)
3131+ verse : string; (** Verse directory (default: "verse") *)
3232+}
2733(** Configurable paths within the workspace.
28342935 By default, paths are:
···3238 - [verse = "verse"] - verse directory
33393440 Set [mono = "."] to have packages at the root level. *)
3535-type paths = {
3636- mono : string; (** Monorepo directory (default: "mono") *)
3737- src : string; (** Source checkouts directory (default: "src") *)
3838- verse : string; (** Verse directory (default: "verse") *)
3939-}
40414142val default_paths : paths
4243(** Default paths configuration. *)
···5354(** [handle t] returns the user's handle. *)
54555556val knot : t -> string
5656-(** [knot t] returns the git push server hostname (e.g., "git.recoil.org").
5757- Used for converting tangled URLs to SSH push URLs. *)
5757+(** [knot t] returns the git push server hostname (e.g., "git.recoil.org"). Used
5858+ for converting tangled URLs to SSH push URLs. *)
58595960val paths : t -> paths
6061(** [paths t] returns the paths configuration. *)
···129130 ?paths:paths ->
130131 unit ->
131132 t
132132-(** [create ~root ~handle ?knot ?packages ?paths ()] creates a new configuration.
133133+(** [create ~root ~handle ?knot ?packages ?paths ()] creates a new
134134+ configuration.
133135134136 @param root Workspace root directory (absolute path)
135137 @param handle User's handle
···138140 @param paths Optional custom paths configuration *)
139141140142val with_package_override : t -> name:string -> ?branch:string -> unit -> t
141141-(** [with_package_override t ~name ?branch ()] returns a new config
142142- with overrides for the named package. *)
143143+(** [with_package_override t ~name ?branch ()] returns a new config with
144144+ overrides for the named package. *)
143145144146(** {1 Validation} *)
145147
+1-4
lib/doctor.ml
···11321132(** Health status for cron-job style exit codes *)
11331133type health = Healthy | Warning | Critical
1134113411351135-let health_to_exit_code = function
11361136- | Healthy -> 0
11371137- | Warning -> 1
11381138- | Critical -> 2
11351135+let health_to_exit_code = function Healthy -> 0 | Warning -> 1 | Critical -> 2
1139113611401137(** Compute overall health status from a report.
11411138 - Critical: has critical/high priority issues or warnings
+14-19
lib/dune_project.ml
···33type source_info =
44 | Github of { user : string; repo : string }
55 | Gitlab of { user : string; repo : string }
66- | Tangled of { host : string; repo : string } (** tangled.org style sources *)
66+ | Tangled of { host : string; repo : string }
77+ (** tangled.org style sources *)
78 | Uri of { url : string; branch : string option }
89910type t = {
···1617module Sexp = Sexplib0.Sexp
17181819(** Extract string from a Sexp.Atom, or None if it's a List *)
1919-let atom_string = function
2020- | Sexp.Atom s -> Some s
2121- | Sexp.List _ -> None
2020+let atom_string = function Sexp.Atom s -> Some s | Sexp.List _ -> None
22212322(** Parse source stanza: (source (github user/repo)) or (source (uri "url")) *)
2423let parse_source_inner sexp =
···3635 match String.index_opt host_repo '/' with
3736 | Some i ->
3837 let host = String.sub host_repo 0 i in
3939- let repo = String.sub host_repo (i + 1) (String.length host_repo - i - 1) in
3838+ let repo =
3939+ String.sub host_repo (i + 1) (String.length host_repo - i - 1)
4040+ in
4041 Some (Tangled { host; repo })
4142 | None -> None)
4243 | Sexp.List [ Sexp.Atom "uri"; Sexp.Atom url ] ->
4344 (* Check for branch in URI fragment *)
4445 let uri = Uri.of_string url in
4546 let branch = Uri.fragment uri in
4646- let url_without_fragment =
4747- Uri.with_fragment uri None |> Uri.to_string
4848- in
4747+ let url_without_fragment = Uri.with_fragment uri None |> Uri.to_string in
4948 Some (Uri { url = url_without_fragment; branch })
5049 | Sexp.Atom url ->
5150 (* Single atom URL (unlikely but handle it) *)
5251 let uri = Uri.of_string url in
5352 let branch = Uri.fragment uri in
5454- let url_without_fragment =
5555- Uri.with_fragment uri None |> Uri.to_string
5656- in
5353+ let url_without_fragment = Uri.with_fragment uri None |> Uri.to_string in
5754 Some (Uri { url = url_without_fragment; branch })
5855 | _ -> None
5956···9087let parse content =
9188 match Parsexp.Many.parse_string content with
9289 | Error err ->
9393- Error (Printf.sprintf "S-expression parse error: %s"
9494- (Parsexp.Parse_error.message err))
9090+ Error
9191+ (Printf.sprintf "S-expression parse error: %s"
9292+ (Parsexp.Parse_error.message err))
9593 | Ok sexps -> (
9694 match find_string_field "name" sexps with
9795 | None -> Error "dune-project missing (name ...) stanza"
···112110113111(** Ensure URL ends with .git *)
114112let ensure_git_suffix url =
115115- if String.ends_with ~suffix:".git" url then url
116116- else url ^ ".git"
113113+ if String.ends_with ~suffix:".git" url then url else url ^ ".git"
117114118115let dev_repo_url t =
119116 match t.source with
···124121 | Some (Tangled { host; repo }) ->
125122 (* Tangled sources: https://tangled.sh/@handle/repo *)
126123 Ok (Printf.sprintf "git+https://tangled.sh/@%s/%s.git" host repo)
127127- | Some (Uri { url; _ }) ->
128128- Ok (normalize_git_url (ensure_git_suffix url))
124124+ | Some (Uri { url; _ }) -> Ok (normalize_git_url (ensure_git_suffix url))
129125 | None -> (
130126 match t.homepage with
131131- | Some homepage ->
132132- Ok (normalize_git_url (ensure_git_suffix homepage))
127127+ | Some homepage -> Ok (normalize_git_url (ensure_git_suffix homepage))
133128 | None ->
134129 Error
135130 (Printf.sprintf
+7-6
lib/dune_project.mli
···11(** Dune project file parsing.
2233- Parse dune-project s-expressions to extract package metadata needed
44- for generating opam-repo entries. *)
33+ Parse dune-project s-expressions to extract package metadata needed for
44+ 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. *)
1413type t = {
1514 name : string; (** Project name from (name ...) stanza *)
1615 source : source_info option; (** Source from (source ...) stanza *)
1716 homepage : string option; (** Homepage from (homepage ...) stanza *)
1817 packages : string list; (** Package names from (package (name ...)) stanzas *)
1918}
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., "git+https://...").
2727+ Returns a URL suitable for the opam dev-repo field (e.g.,
2828+ "git+https://...").
28292930 URL derivation logic:
3031 - [Github {user; repo}] -> "git+https://github.com/user/repo.git"
···3435 - Neither source nor homepage -> Error *)
35363637val url_with_branch : t -> (string, string) result
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").
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").
39404041 Branch derivation:
4142 - [Uri {url; branch = Some b}] -> url#b
+26-20
lib/feature.ml
···1313let error_hint = function
1414 | Git_error _ -> Some "Check that the monorepo is properly initialized"
1515 | Feature_exists name ->
1616- Some (Printf.sprintf "Run 'monopam feature remove %s' first if you want to recreate it" name)
1616+ Some
1717+ (Printf.sprintf
1818+ "Run 'monopam feature remove %s' first if you want to recreate it"
1919+ name)
1720 | Feature_not_found name ->
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"
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"
20282129let pp_error_with_hint ppf e =
2230 pp_error ppf e;
···2432 | Some hint -> Fmt.pf ppf "@.Hint: %s" hint
2533 | None -> ()
26342727-type entry = {
2828- name : string;
2929- path : Fpath.t;
3030- branch : string;
3131-}
3535+type entry = { name : string; path : Fpath.t; branch : string }
32363337let pp_entry ppf e =
3438 Fmt.pf ppf "%s -> %a (branch: %s)" e.name Fpath.pp e.path e.branch
···5155 let work_eio = Eio.Path.(fs / Fpath.to_string work_dir) in
5256 (try Eio.Path.mkdirs ~perm:0o755 work_eio with Eio.Io _ -> ());
5357 (* Create the worktree with a new branch *)
5454- match Git.Worktree.add ~proc ~fs ~repo:mono ~path:wt_path ~branch:name () with
5858+ match
5959+ Git.Worktree.add ~proc ~fs ~repo:mono ~path:wt_path ~branch:name ()
6060+ with
5561 | Error e -> Error (Git_error e)
5662 | Ok () -> Ok { name; path = wt_path; branch = name }
5763 end
···7278 let work_dir = work_path config in
7379 let all_worktrees = Git.Worktree.list ~proc ~fs mono in
7480 (* Filter to only worktrees under work/ directory *)
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
8181+ List.filter_map
8282+ (fun (wt : Git.Worktree.entry) ->
8383+ (* Check if this worktree is under the work directory *)
8484+ let wt_str = Fpath.to_string wt.path in
8585+ let work_str = Fpath.to_string work_dir in
8686+ if String.starts_with ~prefix:work_str wt_str then
8787+ let name = Fpath.basename wt.path in
8888+ let branch = Option.value ~default:name wt.branch in
8989+ Some { name; path = wt.path; branch }
9090+ else None)
9191+ all_worktrees
+1-1
lib/feature.mli
···1818val pp_error_with_hint : error Fmt.t
1919(** [pp_error_with_hint] formats errors with a helpful hint. *)
20202121-(** A feature worktree entry. *)
2221type entry = {
2322 name : string; (** Feature name *)
2423 path : Fpath.t; (** Path to the worktree *)
2524 branch : string; (** Branch name *)
2625}
2626+(** A feature worktree entry. *)
27272828val pp_entry : entry Fmt.t
2929(** [pp_entry] formats a feature entry. *)
+540-344
lib/fork_join.ml
···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 } (** 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 }
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 }
3037 | Git_add_all of Fpath.t
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 }
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+ }
34463535-(** Discovery information gathered during planning *)
3647type discovery = {
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 *)
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 *)
4354}
5555+(** Discovery information gathered during planning *)
44564545-(** A complete action plan *)
4657type 'a action_plan = {
4747- discovery: discovery;
4848- actions: action list;
4949- result: 'a; (** What we'll return on success *)
5050- dry_run: bool;
5858+ discovery : discovery;
5959+ actions : action list;
6060+ result : 'a; (** What we'll return on success *)
6161+ dry_run : bool;
5162}
6363+(** A complete action plan *)
52645365let pp_error ppf = function
5466 | Config_error msg -> Fmt.pf ppf "Configuration error: %s" msg
5567 | 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
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
5872 | Src_not_found name -> Fmt.pf ppf "Source checkout not found: src/%s" name
5959- | Subtree_already_exists name -> Fmt.pf ppf "Subtree already exists in monorepo: mono/%s" name
7373+ | Subtree_already_exists name ->
7474+ Fmt.pf ppf "Subtree already exists in monorepo: mono/%s" name
6075 | No_opam_files name -> Fmt.pf ppf "No .opam files found in subtree: %s" name
6176 | Verse_error e -> Fmt.pf ppf "Verse error: %a" Verse.pp_error e
6277 | User_cancelled -> Fmt.pf ppf "Operation cancelled by user"
···7085 | Subtree_not_found name ->
7186 Some (Fmt.str "Check that mono/%s exists in your monorepo" name)
7287 | Src_already_exists name ->
7373- Some (Fmt.str "Remove or rename src/%s first, or choose a different name" name)
8888+ Some
8989+ (Fmt.str "Remove or rename src/%s first, or choose a different name"
9090+ name)
7491 | Src_not_found name ->
7592 Some (Fmt.str "Run 'monopam fork %s' first to create src/%s" name name)
7693 | Subtree_already_exists name ->
7777- Some (Fmt.str "Remove mono/%s first, or use a different name with --as" name)
9494+ Some
9595+ (Fmt.str "Remove mono/%s first, or use a different name with --as" name)
7896 | No_opam_files name ->
7997 Some (Fmt.str "Add a .opam file to mono/%s before forking" name)
8098 | Verse_error e -> Verse.error_hint e
···83101(** {1 Pretty Printers for Actions and Discovery} *)
8410285103let pp_action ppf = function
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
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
92107 | Git_config { repo = _; key; value } ->
93108 Fmt.pf ppf "Set git config %s = %s" key value
94109 | Git_clone { url; dest; branch } ->
···96111 | Git_subtree_split { repo = _; prefix } ->
97112 Fmt.pf ppf "Split subtree history for '%s'" prefix
98113 | Git_subtree_add { repo = _; prefix; url; branch } ->
9999- Fmt.pf ppf "Add subtree '%s' from %s (branch: %s)" prefix (Uri.to_string url) branch
114114+ Fmt.pf ppf "Add subtree '%s' from %s (branch: %s)" prefix
115115+ (Uri.to_string url) branch
100116 | Git_add_remote { repo = _; name; url } ->
101117 Fmt.pf ppf "Add remote '%s' -> %s" name url
102118 | Git_push_ref { repo = _; target; ref_spec } ->
···107123 Fmt.pf ppf "Rename current branch to '%s'" new_name
108124 | Copy_directory { src; dest } ->
109125 Fmt.pf ppf "Copy files from %a to %a" Fpath.pp src Fpath.pp dest
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
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
114128 | Git_rm { repo = _; path; recursive = _ } ->
115129 Fmt.pf ppf "Remove '%s' from git" path
116130 | Update_sources_toml { path = _; name; entry = _ } ->
···125139 Fmt.pf ppf " Subtree history: %s@,"
126140 (if d.has_subtree_history then "present" else "none (fresh package)");
127141 (match d.remote_accessible with
128128- | None -> ()
129129- | Some true -> Fmt.pf ppf " Remote accessible: yes@,"
130130- | Some false -> Fmt.pf ppf " Remote accessible: no@,");
142142+ | None -> ()
143143+ | Some true -> Fmt.pf ppf " Remote accessible: yes@,"
144144+ | Some false -> Fmt.pf ppf " Remote accessible: no@,");
131145 (match d.local_path_is_repo with
132132- | None -> ()
133133- | Some true -> Fmt.pf ppf " Is git repo: yes@,"
134134- | Some false -> Fmt.pf ppf " Is git repo: no@,");
146146+ | None -> ()
147147+ | Some true -> Fmt.pf ppf " Is git repo: yes@,"
148148+ | Some false -> Fmt.pf ppf " Is git repo: no@,");
135149 if d.opam_files <> [] then
136136- Fmt.pf ppf " Packages found: %a@," Fmt.(list ~sep:(any ", ") string) d.opam_files;
150150+ Fmt.pf ppf " Packages found: %a@,"
151151+ Fmt.(list ~sep:(any ", ") string)
152152+ d.opam_files;
137153 Fmt.pf ppf "@]"
138154139139-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)@,";
155155+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)@,";
146163 Fmt.pf ppf "@,Expected result:@, %a@]" pp_result plan.result
147164148165let pp_error_with_hint ppf e =
···170187let pp_fork_result ppf (r : fork_result) =
171188 (* Only truncate if it looks like a git SHA (40 hex chars), otherwise show full string *)
172189 let commit_display =
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
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
175195 then String.sub r.split_commit 0 7
176196 else r.split_commit
177197 in
178198 Fmt.pf ppf "@[<v>Forked subtree '%s':@, Split commit: %s@, Local repo: %a@,"
179199 r.name commit_display Fpath.pp r.src_path;
180200 (match r.push_url with
181181- | Some url -> Fmt.pf ppf " Push URL: %s@," url
182182- | None -> ());
201201+ | Some url -> Fmt.pf ppf " Push URL: %s@," url
202202+ | None -> ());
183203 if r.packages_created <> [] then
184184- Fmt.pf ppf " Packages: %a@]" Fmt.(list ~sep:(any ", ") string) r.packages_created
185185- else
186186- Fmt.pf ppf "@]"
204204+ Fmt.pf ppf " Packages: %a@]"
205205+ Fmt.(list ~sep:(any ", ") string)
206206+ r.packages_created
207207+ else Fmt.pf ppf "@]"
187208188209let pp_join_result ppf (r : join_result) =
189189- Fmt.pf ppf "@[<v>Joined repository '%s':@, Source: %s@,"
190190- r.name r.source_url;
210210+ Fmt.pf ppf "@[<v>Joined repository '%s':@, Source: %s@," r.name r.source_url;
191211 (match r.upstream_url with
192192- | Some url -> Fmt.pf ppf " Upstream: %s@," url
193193- | None -> ());
212212+ | Some url -> Fmt.pf ppf " Upstream: %s@," url
213213+ | None -> ());
194214 (match r.from_handle with
195195- | Some h -> Fmt.pf ppf " From verse: %s@," h
196196- | None -> ());
215215+ | Some h -> Fmt.pf ppf " From verse: %s@," h
216216+ | None -> ());
197217 if r.packages_added <> [] then
198198- Fmt.pf ppf " Packages: %a@]" Fmt.(list ~sep:(any ", ") string) r.packages_added
199199- else
200200- Fmt.pf ppf "@]"
218218+ Fmt.pf ppf " Packages: %a@]"
219219+ Fmt.(list ~sep:(any ", ") string)
220220+ r.packages_added
221221+ else Fmt.pf ppf "@]"
201222202223(** Helper to check if a path is a directory *)
203224let is_directory ~fs path =
···236257 | Some "tangled.org" | Some "tangled.sh" -> true
237258 | _ -> false
238259239239-(** Convert a dev-repo URL to a push URL (SSH format for github/gitlab/tangled) *)
260260+(** Convert a dev-repo URL to a push URL (SSH format for github/gitlab/tangled)
261261+*)
240262let url_to_push_url ?knot url =
241263 (* Strip git+ prefix if present *)
242264 let url =
···302324 (* For SSH URLs like git@github.com:user/repo.git *)
303325 if String.starts_with ~prefix:"git@" url then
304326 match String.index_opt url ':' with
305305- | Some i ->
327327+ | Some i -> (
306328 let path = String.sub url (i + 1) (String.length url - i - 1) in
307329 (* path is like "user/repo.git" or "handle/repo" *)
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)
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)
319341 | None -> false
320342 else
321343 (* For HTTPS URLs like https://github.com/user/repo.git *)
···351373 let content = Eio.Path.load eio_path in
352374 match Dune_project.parse content with
353375 | Error _ -> None
354354- | Ok dune_proj ->
376376+ | Ok dune_proj -> (
355377 match Dune_project.dev_repo_url dune_proj with
356378 | Error _ -> None
357357- | Ok dev_repo -> Some (url_to_push_url ?knot dev_repo)
379379+ | Ok dev_repo -> Some (url_to_push_url ?knot dev_repo))
358380 with Eio.Io _ -> None
359381360382(** Extract name from URL (last path component without .git suffix) *)
···362384 let uri = Uri.of_string url in
363385 let path = Uri.path uri in
364386 (* Remove leading slash and .git suffix *)
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
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
371397 (* Get last component *)
372398 match String.rindex_opt path '/' with
373399 | Some i -> String.sub path (i + 1) (String.length path - i - 1)
···378404(** Determine if input is a local path or URL *)
379405let is_local_path s =
380406 (* It's a URL if it starts with a scheme or looks like SSH URL *)
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)
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)
387414388415(** Copy a directory tree recursively *)
389416let copy_directory ~fs ~src ~dest =
···393420 match Eio.Path.kind ~follow:false src_path with
394421 | `Directory ->
395422 (try Eio.Path.mkdirs ~perm:0o755 dest_path with Eio.Io _ -> ());
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)
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)
404432 | `Regular_file ->
405433 let content = Eio.Path.load src_path in
406434 Eio.Path.save ~create:(`Or_truncate 0o644) dest_path content
407407- | `Symbolic_link ->
435435+ | `Symbolic_link -> (
408436 (* Read symlink target and recreate it *)
409437 let target = Eio.Path.read_link src_path in
410410- (try Unix.symlink target (snd dest_path) with _ -> ())
411411- | _ -> () (* Skip other file types *)
438438+ try Unix.symlink target (snd dest_path) with _ -> ())
439439+ | _ -> () (* Skip other file types *)
412440 | exception _ -> ()
413441 in
414442 copy_rec src_eio dest_eio
···417445418446(** Build a fork plan - handles both subtree and fresh package scenarios.
419447420420- 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>/
448448+ 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>/
424451425452 This ensures the subtree relationship is properly established for sync. *)
426453let plan_fork ~proc ~fs ~config ~name ?push_url ?(dry_run = false) () =
···435462 let mono_exists = Git.Subtree.exists ~fs ~repo:monorepo ~prefix in
436463 let src_exists = is_directory ~fs src_path in
437464 let has_subtree_hist =
438438- if mono_exists then Git.has_subtree_history ~proc ~fs ~repo:monorepo ~prefix ()
465465+ if mono_exists then
466466+ Git.has_subtree_history ~proc ~fs ~repo:monorepo ~prefix ()
439467 else false
440468 in
441469 let opam_files =
442442- if mono_exists then find_opam_files ~fs subtree_path
443443- else []
470470+ if mono_exists then find_opam_files ~fs subtree_path else []
444471 in
445472446446- 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
473473+ 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
454484455485 (* Validation *)
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)
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)
462489 else begin
463490 (* Build actions for complete fork workflow:
464491 1. Create src/<name>/ with content
···472499 Git_subtree_split { repo = monorepo; prefix };
473500 Git_init src_path;
474501 (* Allow pushing to checked-out branch (for monopam sync) *)
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" };
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+ };
478516 Git_checkout { repo = src_path; branch };
479517 ]
480518 else
···484522 Create_directory src_path;
485523 Git_init src_path;
486524 (* Allow pushing to checked-out branch (for monopam sync) *)
487487- Git_config { repo = src_path; key = "receive.denyCurrentBranch"; value = "updateInstead" };
525525+ Git_config
526526+ {
527527+ repo = src_path;
528528+ key = "receive.denyCurrentBranch";
529529+ value = "updateInstead";
530530+ };
488531 Git_branch_rename { repo = src_path; new_name = branch };
489532 Copy_directory { src = subtree_path; dest = src_path };
490533 Git_add_all src_path;
491491- Git_commit { repo = src_path; message = Fmt.str "Initial commit of %s" name };
534534+ Git_commit
535535+ { repo = src_path; message = Fmt.str "Initial commit of %s" name };
492536 ]
493537 in
494538495539 (* Add remote if push_url provided *)
496496- let remote_actions = match push_url with
540540+ let remote_actions =
541541+ match push_url with
497542 | Some url -> [ Git_add_remote { repo = src_path; name = "origin"; url } ]
498543 | None -> []
499544 in
500545501546 (* Remove from mono and re-add as subtree *)
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
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
507561508562 (* Update sources.toml only if push_url is a true fork (different namespace) *)
509563 let handle = Verse_config.handle config in
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 *)
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 *)
525584 | None -> []
526585 in
527586528528- let actions = create_src_actions @ remote_actions @ rejoin_actions @ sources_actions in
587587+ let actions =
588588+ create_src_actions @ remote_actions @ rejoin_actions @ sources_actions
589589+ in
529590530530- 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
591591+ 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
537601538602 Ok { discovery; actions; result; dry_run }
539603 end
···555619 match Fpath.of_string source with
556620 | Ok path -> Some (Git.is_repo ~proc ~fs path)
557621 | Error _ -> Some false
558558- end else None
622622+ end
623623+ else None
559624 in
560625561561- 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
626626+ let discovery =
627627+ {
628628+ mono_exists = subtree_exists;
629629+ src_exists;
630630+ has_subtree_history = false;
631631+ remote_accessible = None;
632632+ opam_files = [];
633633+ (* Will be discovered after join *)
634634+ local_path_is_repo = local_is_repo;
635635+ }
636636+ in
569637570638 (* Validation *)
571571- if subtree_exists then
572572- Error (Subtree_already_exists name)
639639+ if subtree_exists then Error (Subtree_already_exists name)
573640 else begin
574641 let branch = Verse_config.default_branch in
575642 let actions =
···584651 [
585652 Create_directory checkouts;
586653 Copy_directory { src = local_path; dest = src_path };
587587- Git_subtree_add { repo = monorepo; prefix; url = Uri.of_string (Fpath.to_string src_path); branch };
654654+ Git_subtree_add
655655+ {
656656+ repo = monorepo;
657657+ prefix;
658658+ url = Uri.of_string (Fpath.to_string src_path);
659659+ branch;
660660+ };
588661 ]
589662 else
590663 (* Local directory without git - init and commit first *)
···594667 Git_init src_path;
595668 Copy_directory { src = local_path; dest = src_path };
596669 Git_add_all src_path;
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 };
670670+ Git_commit
671671+ {
672672+ repo = src_path;
673673+ message = Fmt.str "Initial commit of %s" name;
674674+ };
675675+ Git_branch_rename { repo = src_path; new_name = branch };
676676+ (* Ensure branch is named correctly *)
677677+ Git_subtree_add
678678+ {
679679+ repo = monorepo;
680680+ prefix;
681681+ url = Uri.of_string (Fpath.to_string src_path);
682682+ branch;
683683+ };
600684 ]
601601- end else begin
685685+ end
686686+ else begin
602687 (* Join from URL (existing behavior) *)
603688 let url_uri = Uri.of_string source in
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
689689+ let base_actions =
690690+ [
691691+ Create_directory checkouts;
692692+ Git_clone { url = source; dest = src_path; branch };
693693+ Git_subtree_add { repo = monorepo; prefix; url = url_uri; branch };
694694+ ]
695695+ in
696696+ let sources_actions =
697697+ match upstream with
610698 | Some _ ->
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- }]
699699+ [
700700+ Update_sources_toml
701701+ {
702702+ path = Fpath.(monorepo / "sources.toml");
703703+ name;
704704+ entry =
705705+ Sources_registry.
706706+ {
707707+ url = normalize_git_url source;
708708+ upstream = Option.map normalize_git_url upstream;
709709+ branch = Some branch;
710710+ reason = None;
711711+ origin = Some Join;
712712+ };
713713+ };
714714+ ]
622715 | None -> []
623716 in
624717 base_actions @ sources_actions
···634727 else []
635728 in
636729637637- let result = {
638638- name;
639639- source_url = source;
640640- upstream_url = upstream;
641641- packages_added = opam_preview;
642642- from_handle = None;
643643- } in
730730+ let result =
731731+ {
732732+ name;
733733+ source_url = source;
734734+ upstream_url = upstream;
735735+ packages_added = opam_preview;
736736+ from_handle = None;
737737+ }
738738+ in
644739645645- Ok { discovery = { discovery with opam_files = opam_preview }; actions; result; dry_run }
740740+ Ok
741741+ {
742742+ discovery = { discovery with opam_files = opam_preview };
743743+ actions;
744744+ result;
745745+ dry_run;
746746+ }
646747 end
647748648749(** Build a rejoin plan - add existing src/<name> back into mono/<name> *)
···655756 (* Gather discovery information *)
656757 let subtree_exists = Git.Subtree.exists ~fs ~repo:monorepo ~prefix in
657758 let src_exists = is_directory ~fs src_path in
658658- let src_is_repo = if src_exists then Git.is_repo ~proc ~fs src_path else false in
759759+ let src_is_repo =
760760+ if src_exists then Git.is_repo ~proc ~fs src_path else false
761761+ in
659762 let opam_files = if src_exists then find_opam_files ~fs src_path else [] in
660763661661- 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
764764+ let discovery =
765765+ {
766766+ mono_exists = subtree_exists;
767767+ src_exists;
768768+ has_subtree_history = false;
769769+ remote_accessible = None;
770770+ opam_files;
771771+ local_path_is_repo = Some src_is_repo;
772772+ }
773773+ in
669774670775 (* Validation *)
671671- if subtree_exists then
672672- Error (Subtree_already_exists name)
673673- else if not src_exists then
674674- Error (Src_not_found name)
776776+ if subtree_exists then Error (Subtree_already_exists name)
777777+ else if not src_exists then Error (Src_not_found name)
675778 else if not src_is_repo then
676676- Error (Config_error (Fmt.str "src/%s exists but is not a git repository" name))
779779+ Error
780780+ (Config_error (Fmt.str "src/%s exists but is not a git repository" name))
677781 else begin
678782 let branch = Verse_config.default_branch in
679679- let actions = [
680680- Git_subtree_add { repo = monorepo; prefix; url = Uri.of_string (Fpath.to_string src_path); branch };
681681- ] in
783783+ let actions =
784784+ [
785785+ Git_subtree_add
786786+ {
787787+ repo = monorepo;
788788+ prefix;
789789+ url = Uri.of_string (Fpath.to_string src_path);
790790+ branch;
791791+ };
792792+ ]
793793+ in
682794683683- 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
795795+ let result =
796796+ {
797797+ name;
798798+ source_url = Fpath.to_string src_path;
799799+ upstream_url = None;
800800+ packages_added = opam_files;
801801+ from_handle = None;
802802+ }
803803+ in
690804691805 Ok { discovery; actions; result; dry_run }
692806 end
693807694808(** {1 Plan Execution} *)
695809810810+type exec_state = { mutable split_commit : string option }
696811(** State tracked during plan execution *)
697697-type exec_state = {
698698- mutable split_commit: string option;
699699-}
700812701813(** Execute a single action *)
702814let execute_action ~proc ~fs ~state action =
···710822 | Git_init path ->
711823 Git.init ~proc ~fs path |> Result.map_error (fun e -> Git_error e)
712824 | Git_config { repo; key; value } ->
713713- Git.config ~proc ~fs ~key ~value repo |> Result.map_error (fun e -> Git_error e)
825825+ Git.config ~proc ~fs ~key ~value repo
826826+ |> Result.map_error (fun e -> Git_error e)
714827 | Git_clone { url; dest; branch } ->
715828 Git.clone ~proc ~fs ~url:(Uri.of_string url) ~branch dest
716829 |> Result.map_error (fun e -> Git_error e)
···728841 (* Replace SPLIT_COMMIT placeholder with actual commit if available *)
729842 let ref_spec =
730843 match state.split_commit with
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
844844+ | Some commit ->
845845+ String.concat ""
846846+ (String.split_on_char 'S'
847847+ (String.concat commit (String.split_on_char 'S' ref_spec)))
848848+ |> fun s ->
849849+ if String.starts_with ~prefix:"PLIT_COMMIT" s then
850850+ Option.value ~default:ref_spec state.split_commit
851851+ ^ String.sub s 11 (String.length s - 11)
852852+ else s
735853 | None -> ref_spec
736854 in
737855 (* Better replacement: look for SPLIT_COMMIT literal *)
738856 let ref_spec =
739857 match state.split_commit with
740858 | Some commit ->
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)
859859+ if
860860+ String.length ref_spec >= 12
861861+ && String.sub ref_spec 0 12 = "SPLIT_COMMIT"
862862+ then commit ^ String.sub ref_spec 12 (String.length ref_spec - 12)
743863 else ref_spec
744864 | None -> ref_spec
745865 in
···755875 copy_directory ~fs ~src ~dest;
756876 Ok ()
757877 | Git_add_all path ->
758758- Git.add_all ~proc ~fs path
759759- |> Result.map_error (fun e -> Git_error e)
878878+ Git.add_all ~proc ~fs path |> Result.map_error (fun e -> Git_error e)
760879 | Git_commit { repo; message } ->
761880 Git.commit ~proc ~fs ~message repo
762881 |> Result.map_error (fun e -> Git_error e)
763882 | Git_rm { repo; path; recursive } ->
764883 Git.rm ~proc ~fs ~recursive repo path
765884 |> Result.map_error (fun e -> Git_error e)
766766- | Update_sources_toml { path; name; entry } ->
885885+ | Update_sources_toml { path; name; entry } -> (
767886 let sources =
768887 match Sources_registry.load ~fs:(fs :> _ Eio.Path.t) path with
769888 | Ok s -> s
770889 | Error _ -> Sources_registry.empty
771890 in
772891 let sources = Sources_registry.add sources ~subtree:name entry in
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)))
892892+ match Sources_registry.save ~fs:(fs :> _ Eio.Path.t) path sources with
893893+ | Ok () -> Ok ()
894894+ | Error msg ->
895895+ Error (Config_error (Fmt.str "Failed to update sources.toml: %s" msg))
896896+ )
776897777898(** Execute a complete fork action plan *)
778899let execute_fork_plan ~proc ~fs plan =
779779- if plan.dry_run then
780780- Ok plan.result
900900+ if plan.dry_run then Ok plan.result
781901 else begin
782902 let state = { split_commit = None } in
783903 let rec run_actions = function
784904 | [] -> Ok ()
785785- | action :: rest ->
905905+ | action :: rest -> (
786906 match execute_action ~proc ~fs ~state action with
787907 | Error e -> Error e
788788- | Ok () -> run_actions rest
908908+ | Ok () -> run_actions rest)
789909 in
790910 match run_actions plan.actions with
791911 | Error e -> Error e
···801921802922(** Execute a complete join action plan *)
803923let execute_join_plan ~proc ~fs plan =
804804- if plan.dry_run then
805805- Ok plan.result
924924+ if plan.dry_run then Ok plan.result
806925 else begin
807926 let state = { split_commit = None } in
808927 let rec run_actions = function
809928 | [] -> Ok ()
810810- | action :: rest ->
929929+ | action :: rest -> (
811930 match execute_action ~proc ~fs ~state action with
812931 | Error e -> Error e
813813- | Ok () -> run_actions rest
932932+ | Ok () -> run_actions rest)
814933 in
815934 match run_actions plan.actions with
816935 | Error e -> Error e
···827946 let src_path = Fpath.(checkouts / name) in
828947 (* Validate: mono/<name>/ must exist *)
829948 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)
949949+ Error (Subtree_not_found name) (* Validate: src/<name>/ must not exist *)
950950+ else if is_directory ~fs src_path then Error (Src_already_exists name)
834951 else begin
835952 (* Find .opam files in subtree *)
836953 let packages = find_opam_files ~fs subtree_path in
837837- if packages = [] then
838838- Error (No_opam_files name)
954954+ if packages = [] then Error (No_opam_files name)
839955 else if dry_run then
840840- Ok { name; split_commit = "(dry-run)"; src_path; push_url; packages_created = packages }
956956+ Ok
957957+ {
958958+ name;
959959+ split_commit = "(dry-run)";
960960+ src_path;
961961+ push_url;
962962+ packages_created = packages;
963963+ }
841964 else begin
842965 (* Split the subtree to get history *)
843966 match Git.Subtree.split ~proc ~fs ~repo:monorepo ~prefix () with
844967 | Error e -> Error (Git_error e)
845845- | Ok split_commit ->
968968+ | Ok split_commit -> (
846969 (* Ensure src/ exists *)
847970 ensure_dir ~fs checkouts;
848971 (* Initialize new git repo at src/<name>/ *)
849972 match Git.init ~proc ~fs src_path with
850973 | Error e -> Error (Git_error e)
851851- | Ok () ->
974974+ | Ok () -> (
852975 (* Add 'origin' remote pointing to monorepo path temporarily *)
853976 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
896896- | Ok () -> ()
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 }))
977977+ match
978978+ Git.add_remote ~proc ~fs ~name:"mono" ~url:mono_str src_path
979979+ with
980980+ | Error e -> Error (Git_error e)
981981+ | Ok () -> (
982982+ (* Push split commit to local repo *)
983983+ let ref_spec = split_commit ^ ":refs/heads/main" in
984984+ match
985985+ Git.push_ref ~proc ~fs ~repo:monorepo
986986+ ~target:(Fpath.to_string src_path) ~ref_spec ()
987987+ with
988988+ | Error e -> Error (Git_error e)
989989+ | Ok () -> (
990990+ (* Checkout main branch *)
991991+ match Git.checkout ~proc ~fs ~branch:"main" src_path with
992992+ | Error e -> Error (Git_error e)
993993+ | Ok () -> (
994994+ (* Set push URL if provided *)
995995+ let push_result =
996996+ match push_url with
997997+ | Some url -> (
998998+ match
999999+ Git.add_remote ~proc ~fs ~name:"origin" ~url
10001000+ src_path
10011001+ with
10021002+ | Error e -> Error (Git_error e)
10031003+ | Ok () -> Ok ())
10041004+ | None -> Ok ()
10051005+ in
10061006+ match push_result with
10071007+ | Error _ as e -> e
10081008+ | Ok () ->
10091009+ (* Only update sources.toml if there's a push URL *)
10101010+ (match push_url with
10111011+ | Some url -> (
10121012+ let sources_path =
10131013+ Fpath.(monorepo / "sources.toml")
10141014+ in
10151015+ let sources =
10161016+ match
10171017+ Sources_registry.load
10181018+ ~fs:(fs :> _ Eio.Path.t)
10191019+ sources_path
10201020+ with
10211021+ | Ok s -> s
10221022+ | Error _ -> Sources_registry.empty
10231023+ in
10241024+ let entry =
10251025+ Sources_registry.
10261026+ {
10271027+ url = normalize_git_url url;
10281028+ upstream = None;
10291029+ branch = Some "main";
10301030+ reason = None;
10311031+ origin = Some Fork;
10321032+ }
10331033+ in
10341034+ let sources =
10351035+ Sources_registry.add sources ~subtree:name
10361036+ entry
10371037+ in
10381038+ match
10391039+ Sources_registry.save
10401040+ ~fs:(fs :> _ Eio.Path.t)
10411041+ sources_path sources
10421042+ with
10431043+ | Ok () -> ()
10441044+ | Error msg ->
10451045+ Logs.warn (fun m ->
10461046+ m "Failed to update sources.toml: %s"
10471047+ msg))
10481048+ | None -> ());
10491049+ Ok
10501050+ {
10511051+ name;
10521052+ split_commit;
10531053+ src_path;
10541054+ push_url;
10551055+ packages_created = packages;
10561056+ })))))
9001057 end
9011058 end
9021059···9111068 if Git.Subtree.exists ~fs ~repo:monorepo ~prefix then
9121069 Error (Subtree_already_exists name)
9131070 else if dry_run then
914914- Ok { name; source_url = url; upstream_url = upstream; packages_added = []; from_handle = None }
10711071+ Ok
10721072+ {
10731073+ name;
10741074+ source_url = url;
10751075+ upstream_url = upstream;
10761076+ packages_added = [];
10771077+ from_handle = None;
10781078+ }
9151079 else begin
9161080 (* Ensure src/ exists *)
9171081 ensure_dir ~fs checkouts;
···9201084 let uri = Uri.of_string url in
9211085 match Git.clone ~proc ~fs ~url:uri ~branch src_path with
9221086 | Error e -> Error (Git_error e)
923923- | Ok () ->
10871087+ | Ok () -> (
9241088 (* Add subtree to monorepo *)
925925- match Git.Subtree.add ~proc ~fs ~repo:monorepo ~prefix ~url:uri ~branch () with
10891089+ match
10901090+ Git.Subtree.add ~proc ~fs ~repo:monorepo ~prefix ~url:uri ~branch ()
10911091+ with
9261092 | Error e -> Error (Git_error e)
9271093 | Ok () ->
9281094 (* Find .opam files in the new subtree *)
9291095 let packages = find_opam_files ~fs subtree_path in
9301096 (* Only update sources.toml if there's an upstream to track *)
9311097 (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 }
10981098+ | Some _ -> (
10991099+ let sources_path = Fpath.(monorepo / "sources.toml") in
11001100+ let sources =
11011101+ match
11021102+ Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path
11031103+ with
11041104+ | Ok s -> s
11051105+ | Error _ -> Sources_registry.empty
11061106+ in
11071107+ let entry =
11081108+ Sources_registry.
11091109+ {
11101110+ url = normalize_git_url url;
11111111+ upstream = Option.map normalize_git_url upstream;
11121112+ branch = Some branch;
11131113+ reason = None;
11141114+ origin = Some Join;
11151115+ }
11161116+ in
11171117+ let sources =
11181118+ Sources_registry.add sources ~subtree:name entry
11191119+ in
11201120+ match
11211121+ Sources_registry.save
11221122+ ~fs:(fs :> _ Eio.Path.t)
11231123+ sources_path sources
11241124+ with
11251125+ | Ok () -> ()
11261126+ | Error msg ->
11271127+ Logs.warn (fun m ->
11281128+ m "Failed to update sources.toml: %s" msg))
11291129+ | None -> ());
11301130+ Ok
11311131+ {
11321132+ name;
11331133+ source_url = url;
11341134+ upstream_url = upstream;
11351135+ packages_added = packages;
11361136+ from_handle = None;
11371137+ })
9521138 end
9531139954954-let join_from_verse ~proc ~fs ~config ~verse_config ~package ~handle ~fork_url ?(dry_run = false) () =
11401140+let join_from_verse ~proc ~fs ~config ~verse_config ~package ~handle ~fork_url
11411141+ ?(dry_run = false) () =
9551142 (* First use verse fork to set up the opam entries *)
956956- match Verse.fork ~proc ~fs ~config:verse_config ~handle ~package ~fork_url ~dry_run () with
11431143+ match
11441144+ Verse.fork ~proc ~fs ~config:verse_config ~handle ~package ~fork_url
11451145+ ~dry_run ()
11461146+ with
9571147 | Error e -> Error (Verse_error e)
9581148 | Ok fork_result ->
9591149 if dry_run then
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- }
11501150+ Ok
11511151+ {
11521152+ name = fork_result.subtree_name;
11531153+ source_url = fork_url;
11541154+ upstream_url = Some fork_result.upstream_url;
11551155+ packages_added = fork_result.packages_forked;
11561156+ from_handle = Some handle;
11571157+ }
9671158 else begin
9681159 (* Now join the repository *)
9691160 let name = fork_result.subtree_name in
970970- match join ~proc ~fs ~config ~url:fork_url ~name ~upstream:fork_result.upstream_url ~dry_run () with
11611161+ match
11621162+ join ~proc ~fs ~config ~url:fork_url ~name
11631163+ ~upstream:fork_result.upstream_url ~dry_run ()
11641164+ with
9711165 | Error e -> Error e
9721166 | Ok join_result ->
973973- Ok { join_result with
974974- packages_added = fork_result.packages_forked;
975975- from_handle = Some handle;
976976- }
11671167+ Ok
11681168+ {
11691169+ join_result with
11701170+ packages_added = fork_result.packages_forked;
11711171+ from_handle = Some handle;
11721172+ }
9771173 end
+77-66
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:
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 *)
99+ 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 *)
15131614(** {1 Error Types} *)
1715···4240 | Check_remote_exists of string (** URL - informational check *)
4341 | Create_directory of Fpath.t
4442 | Git_init of 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 }
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 }
5459 | Git_add_all of Fpath.t
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 }
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+ }
58685959-(** Discovery information gathered during planning *)
6069type discovery = {
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 *)
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 *)
6776}
7777+(** Discovery information gathered during planning *)
68786969-(** A complete action plan *)
7079type 'a action_plan = {
7171- discovery: discovery;
7272- actions: action list;
7373- result: 'a; (** What we'll return on success *)
7474- dry_run: bool;
8080+ discovery : discovery;
8181+ actions : action list;
8282+ result : 'a; (** What we'll return on success *)
8383+ dry_run : bool;
7584}
8585+(** A complete action plan *)
76867787val pp_action : action Fmt.t
7888(** [pp_action] formats a single action. *)
···8999(** [is_local_path s] returns true if [s] looks like a local filesystem path
90100 rather than a URL. *)
911019292-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
102102+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
95106 be found and converted to SSH push format, [None] otherwise.
961079797- @param knot Optional git push server for tangled URLs (default: git.recoil.org) *)
108108+ @param knot
109109+ Optional git push server for tangled URLs (default: git.recoil.org) *)
9811099111(** {1 Result Types} *)
100112101101-(** Result of a fork operation. *)
102113type fork_result = {
103114 name : string; (** Subtree/repository name *)
104115 split_commit : string; (** Git commit SHA from subtree split *)
···106117 push_url : string option; (** Remote push URL if provided *)
107118 packages_created : string list; (** Package names from .opam files *)
108119}
120120+(** Result of a fork operation. *)
109121110122val pp_fork_result : fork_result Fmt.t
111123(** [pp_fork_result] formats a fork result. *)
112124113113-(** Result of a join operation. *)
114125type join_result = {
115126 name : string; (** Subtree/repository name *)
116127 source_url : string; (** URL the repository was cloned from *)
···118129 packages_added : string list; (** Package names from .opam files *)
119130 from_handle : string option; (** Verse handle if joined from verse *)
120131}
132132+(** Result of a join operation. *)
121133122134val pp_join_result : join_result Fmt.t
123135(** [pp_join_result] formats a join result. *)
···133145 ?dry_run:bool ->
134146 unit ->
135147 (fork_result action_plan, error) result
136136-(** [plan_fork ~proc ~fs ~config ~name ?push_url ?dry_run ()] builds a fork plan.
148148+(** [plan_fork ~proc ~fs ~config ~name ?push_url ?dry_run ()] builds a fork
149149+ plan.
137150138151 This analyzes the current state and builds a list of actions to:
139152 - For subtrees with history: split subtree, create repo, push history
···155168 ?dry_run:bool ->
156169 unit ->
157170 (join_result action_plan, error) result
158158-(** [plan_join ~proc ~fs ~config ~source ?name ?upstream ?dry_run ()] builds a join plan.
171171+(** [plan_join ~proc ~fs ~config ~source ?name ?upstream ?dry_run ()] builds a
172172+ join plan.
159173160160- This analyzes the source (URL or local path) and builds a list of actions to:
174174+ This analyzes the source (URL or local path) and builds a list of actions
175175+ to:
161176 - For URLs: clone repo, add subtree
162177 - For local directories: copy/init repo, add subtree
163178164179 The plan can be displayed to the user and executed with [execute_join_plan].
165180166181 @param source Git URL or local filesystem path to join
167167- @param name Override the subtree directory name (default: derived from source)
182182+ @param name
183183+ Override the subtree directory name (default: derived from source)
168184 @param upstream Original upstream URL if this is your fork
169185 @param dry_run If true, mark plan as dry-run (execute will skip actions) *)
170186···178194 (join_result action_plan, error) result
179195(** [plan_rejoin ~proc ~fs ~config ~name ?dry_run ()] builds a rejoin plan.
180196181181- 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.
197197+ 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.
183200184201 Requires:
185202 - src/<name>/ must exist and be a git repository
···199216 (fork_result, error) result
200217(** [execute_fork_plan ~proc ~fs plan] executes a fork action plan.
201218202202- 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. *)
219219+ 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. *)
205222206223val execute_join_plan :
207224 proc:_ Eio.Process.mgr ->
···227244(** [fork ~proc ~fs ~config ~name ?push_url ?dry_run ()] splits a monorepo
228245 subtree into its own repository.
229246230230- 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
247247+ 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
238252239253 @param name Name of the subtree to fork (directory name under mono/)
240254 @param push_url Optional remote URL to add as origin for pushing
···255269(** [join ~proc ~fs ~config ~url ?name ?upstream ?dry_run ()] brings an external
256270 repository into the monorepo.
257271258258- 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
272272+ 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
265276266277 @param url Git URL to clone from
267278 @param name Override the subtree directory name (default: derived from URL)
268268- @param upstream Original upstream URL if this is your fork of another project
279279+ @param upstream
280280+ Original upstream URL if this is your fork of another project
269281 @param dry_run If true, validate and report what would be done *)
270282271283val join_from_verse :
···282294(** [join_from_verse ~proc ~fs ~config ~verse_config ~package ~handle ~fork_url
283295 ?dry_run ()] joins a package from a verse member's repository.
284296285285- 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
297297+ 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
290301291302 @param verse_config Verse configuration (for accessing verse/ directory)
292303 @param package Package name to look up
+71-45
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 (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- | _ -> ())
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+ | _ -> ())
4850 pairs
4951 end
5052 with _ -> ()
···6062 ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir)));
6163 (* Write cache as JSON *)
6264 Out_channel.with_open_text path (fun oc ->
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")
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")
7174 with _ -> ()
72757376(** Check if a fetch is needed for a cache key *)
···326329 (* Generic git.<domain>: pattern - convert git@git.<domain>:path to https://git.<domain>/path *)
327330 match String.index_opt s ':' with
328331 | Some colon_pos ->
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
332332+ let host = String.sub s 4 (colon_pos - 4) in
333333+ (* "git.<domain>" *)
334334+ let path =
335335+ String.sub s (colon_pos + 1) (String.length s - colon_pos - 1)
336336+ in
331337 "https://" ^ host ^ "/" ^ path
332338 | None -> s
333339 else s
···397403 if not (needs_fetch ~refresh ~timeout:default_cache_timeout cache_key) then begin
398404 Log.debug (fun m -> m "Skipping fetch for %a (cached)" Fpath.pp path);
399405 ()
400400- end else begin
406406+ end
407407+ else begin
401408 let cwd = Eio.Path.(fs / Fpath.to_string path) in
402402- let cmd = ["git"; "fetch"; "--quiet"] in
403403- Log.debug (fun m -> m "Running: %s (in %a)" (String.concat " " cmd) Fpath.pp path);
409409+ let cmd = [ "git"; "fetch"; "--quiet" ] in
410410+ Log.debug (fun m ->
411411+ m "Running: %s (in %a)" (String.concat " " cmd) Fpath.pp path);
404412 Eio.Switch.run @@ fun sw ->
405405- let child = Eio.Process.spawn proc ~sw ~cwd
413413+ let child =
414414+ Eio.Process.spawn proc ~sw ~cwd
406415 ~stdout:(Eio.Flow.buffer_sink (Buffer.create 16))
407416 ~stderr:(Eio.Flow.buffer_sink (Buffer.create 16))
408417 cmd
···412421 | _ -> Log.debug (fun m -> m "Failed to fetch %a" Fpath.pp path)
413422 end
414423415415-(** Scan all verse opam repos and build a map: repo_basename -> [(handle, url, [packages])] *)
424424+(** Scan all verse opam repos and build a map: repo_basename ->
425425+ [(handle, url, [packages])] *)
416426let scan_all_verse_opam_repos ~proc ~fs ~verse_path ~refresh () =
417427 let eio_verse = Eio.Path.(fs / Fpath.to_string verse_path) in
418428 let entries = try Eio.Path.read_dir eio_verse with _ -> [] in
419429 (* Find opam repo directories (ending in -opam) *)
420420- let opam_dirs = List.filter (fun name -> String.ends_with ~suffix:"-opam" name) entries in
430430+ let opam_dirs =
431431+ List.filter (fun name -> String.ends_with ~suffix:"-opam" name) entries
432432+ in
421433 (* Fetch each opam repo first (respecting cache unless refresh) *)
422434 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)
435435+ List.iter
436436+ (fun opam_dir ->
437437+ let opam_path = Fpath.(verse_path / opam_dir) in
438438+ fetch_verse_opam_repo ~proc ~fs ~refresh opam_path)
426439 opam_dirs;
427440 (* Build map: repo_basename -> [(handle, url, [packages])] *)
428441 let repo_map = Hashtbl.create 64 in
···510523511524(** Fetch a remote (with caching) *)
512525let fetch_remote ~proc ~fs ~repo ~remote ~refresh () =
513513- let cache_key = Printf.sprintf "checkout/%s/%s" (Fpath.to_string repo) remote in
526526+ let cache_key =
527527+ Printf.sprintf "checkout/%s/%s" (Fpath.to_string repo) remote
528528+ in
514529 if not (needs_fetch ~refresh ~timeout:default_cache_timeout cache_key) then 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
530530+ Log.debug (fun m ->
531531+ m "Skipping fetch for %s in %a (cached)" remote Fpath.pp repo);
532532+ Ok () (* Return Ok since we have cached data *)
533533+ end
534534+ else begin
518535 let cwd = Eio.Path.(fs / Fpath.to_string repo) in
519519- let cmd = ["git"; "fetch"; remote] in
536536+ let cmd = [ "git"; "fetch"; remote ] in
520537 Log.info (fun m -> m "Fetching %s in %a" remote Fpath.pp repo);
521521- Log.debug (fun m -> m "Running: %s (in %a)" (String.concat " " cmd) Fpath.pp repo);
538538+ Log.debug (fun m ->
539539+ m "Running: %s (in %a)" (String.concat " " cmd) Fpath.pp repo);
522540 Eio.Switch.run @@ fun sw ->
523523- let child = Eio.Process.spawn proc ~sw ~cwd
541541+ let child =
542542+ Eio.Process.spawn proc ~sw ~cwd
524543 ~stdout:(Eio.Flow.buffer_sink (Buffer.create 256))
525544 ~stderr:(Eio.Flow.buffer_sink (Buffer.create 256))
526545 cmd
527546 in
528547 match Eio.Process.await child with
529529- | `Exited 0 -> record_fetch cache_key; Ok ()
548548+ | `Exited 0 ->
549549+ record_fetch cache_key;
550550+ Ok ()
530551 | _ -> Error "Failed to fetch remote"
531552 end
532553···623644 Diverged { common_ancestor = base; my_ahead; their_ahead }))
624645625646(** Compute fork analysis for all repos *)
626626-let compute ~proc ~fs ~verse_config ~monopam_config ?(refresh=false) () =
647647+let compute ~proc ~fs ~verse_config ~monopam_config ?(refresh = false) () =
627648 let verse_path = Verse_config.verse_path verse_config in
628649 let opam_repo_path = Config.Paths.opam_repo monopam_config in
629650 let checkouts_path = Config.Paths.checkouts monopam_config in
···634655635656 (* Scan verse opam repos *)
636657 Log.info (fun m -> m "Scanning verse opam repos");
637637- let verse_repos = scan_all_verse_opam_repos ~proc ~fs ~verse_path ~refresh () in
658658+ let verse_repos =
659659+ scan_all_verse_opam_repos ~proc ~fs ~verse_path ~refresh ()
660660+ in
638661639662 (* Build combined list of all repo names *)
640663 let all_repos = Hashtbl.create 64 in
···687710 ~name:remote_name ~url:src.url ())
688711 end;
689712 (* Fetch remote (respecting cache unless refresh) *)
690690- match fetch_remote ~proc ~fs ~repo:checkout_path ~remote:remote_name ~refresh () with
713713+ match
714714+ fetch_remote ~proc ~fs ~repo:checkout_path
715715+ ~remote:remote_name ~refresh ()
716716+ with
691717 | Error _ -> Not_fetched
692718 | Ok () ->
693719 (* Compare refs *)
+6-8
lib/forks.mli
···7676 ?refresh:bool ->
7777 unit ->
7878 t
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
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
85838686- Fetches are cached for 1 hour by default. Use [~refresh:true] to force
8787- fresh fetches from all remotes. *)
8484+ Fetches are cached for 1 hour by default. Use [~refresh:true] to force fresh
8585+ fetches from all remotes. *)
+72-41
lib/git.ml
···6868let retryable_error_patterns =
6969 [
7070 (* HTTP 5xx errors *)
7171- "500"; "502"; "503"; "504"; "HTTP 5"; "http 5";
7272- "Internal Server Error"; "Bad Gateway"; "Service Unavailable"; "Gateway Timeout";
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";
7381 (* RPC failures (common git smart HTTP errors) *)
7474- "RPC failed"; "curl"; "unexpected disconnect";
7575- "the remote end hung up"; "early EOF";
8282+ "RPC failed";
8383+ "curl";
8484+ "unexpected disconnect";
8585+ "the remote end hung up";
8686+ "early EOF";
7687 (* Connection errors *)
7777- "Connection refused"; "Connection reset"; "Connection timed out";
7878- "Could not resolve host"; "Failed to connect";
7979- "Network is unreachable"; "Temporary failure";
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";
8095 ]
81968297(** Check if an error is a retryable HTTP server error (5xx) or network error *)
8398let is_retryable_error result =
8499 let stderr = result.stderr in
85100 String.length stderr > 0
8686- && List.exists (fun needle -> string_contains ~needle stderr) retryable_error_patterns
101101+ && List.exists
102102+ (fun needle -> string_contains ~needle stderr)
103103+ retryable_error_patterns
871048888-(** 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 =
105105+(** 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 =
91110 let rec attempt n delay_ms =
92111 let result = run_git ~proc ~cwd args in
93112 if result.exit_code = 0 then Ok result.stdout
94113 else if n < max_retries && is_retryable_error result then begin
95114 (* Log the retry *)
96115 Logs.warn (fun m ->
9797- m "Git command failed with retryable error, retrying in %dms (%d/%d): %s"
116116+ m
117117+ "Git command failed with retryable error, retrying in %dms \
118118+ (%d/%d): %s"
98119 delay_ms (n + 1) max_retries result.stderr);
99120 (* Sleep before retry - convert ms to seconds for Unix.sleepf *)
100121 Unix.sleepf (float_of_int delay_ms /. 1000.0);
···139160 let cwd = Eio.Path.(fs / Fpath.to_string parent) in
140161 let target_name = Fpath.basename target in
141162 let url_str = Uri.to_string url in
142142- run_git_ok_with_retry ~proc ~cwd [ "clone"; "--branch"; branch; url_str; target_name ]
163163+ run_git_ok_with_retry ~proc ~cwd
164164+ [ "clone"; "--branch"; branch; url_str; target_name ]
143165 |> Result.map ignore
144166145167let fetch ~proc ~fs ?(remote = "origin") path =
···261283 | Some b -> b
262284 | None -> Option.value ~default:"main" (current_branch ~proc ~fs path)
263285 in
264264- run_git_ok_with_retry ~proc ~cwd [ "push"; remote; branch ] |> Result.map ignore
286286+ run_git_ok_with_retry ~proc ~cwd [ "push"; remote; branch ]
287287+ |> Result.map ignore
265288266289let push_ref ~proc ~fs ~repo ~target ~ref_spec () =
267290 let cwd = path_to_eio ~fs repo in
···383406 let cwd = path_to_eio ~fs repo_path in
384407 run_git_ok ~proc ~cwd [ "show"; "--patch"; "--stat"; commit ]
385408386386-(** 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. *)
409409+(** Parse a subtree merge/squash commit message to extract the upstream commit
410410+ range. Messages look like: "Squashed 'prefix/' changes from abc123..def456"
411411+ or "Squashed 'prefix/' content from commit abc123" Returns the end commit
412412+ (most recent) if found. *)
390413let parse_subtree_message subject =
391414 (* Helper to extract hex commit hash starting at position *)
392415 let extract_hex s start =
···471494(** {1 Worktree Operations} *)
472495473496module Worktree = struct
474474- type entry = {
475475- path : Fpath.t;
476476- head : string;
477477- branch : string option;
478478- }
497497+ type entry = { path : Fpath.t; head : string; branch : string option }
479498480499 let add ~proc ~fs ~repo ~path ~branch () =
481500 let cwd = path_to_eio ~fs repo in
482501 let path_str = Fpath.to_string path in
483483- run_git_ok ~proc ~cwd
484484- [ "worktree"; "add"; "-b"; branch; path_str ]
502502+ run_git_ok ~proc ~cwd [ "worktree"; "add"; "-b"; branch; path_str ]
485503 |> Result.map ignore
486504487505 let remove ~proc ~fs ~repo ~path ~force () =
···506524 HEAD abc123...
507525 branch refs/heads/branchname (or detached) *)
508526 let lines = String.split_on_char '\n' output in
509509- let rec parse_entries acc current_path current_head current_branch = function
510510- | [] ->
527527+ let rec parse_entries acc current_path current_head current_branch =
528528+ function
529529+ | [] -> (
511530 (* Finalize last entry if we have one *)
512512- (match current_path, current_head with
531531+ match (current_path, current_head) with
513532 | Some p, Some h ->
514514- let entry = { path = p; head = h; branch = current_branch } in
533533+ let entry =
534534+ { path = p; head = h; branch = current_branch }
535535+ in
515536 List.rev (entry :: acc)
516537 | _ -> List.rev acc)
517517- | "" :: rest ->
538538+ | "" :: rest -> (
518539 (* End of entry block *)
519519- (match current_path, current_head with
540540+ match (current_path, current_head) with
520541 | Some p, Some h ->
521521- let entry = { path = p; head = h; branch = current_branch } in
542542+ let entry =
543543+ { path = p; head = h; branch = current_branch }
544544+ in
522545 parse_entries (entry :: acc) None None None rest
523546 | _ -> parse_entries acc None None None rest)
524547 | line :: rest ->
525548 if String.starts_with ~prefix:"worktree " line then
526549 let path_str = String.sub line 9 (String.length line - 9) in
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)
550550+ match Fpath.of_string path_str with
551551+ | Ok p ->
552552+ parse_entries acc (Some p) current_head current_branch
553553+ rest
554554+ | Error _ ->
555555+ parse_entries acc current_path current_head current_branch
556556+ rest
530557 else if String.starts_with ~prefix:"HEAD " line then
531558 let head = String.sub line 5 (String.length line - 5) in
532559 parse_entries acc current_path (Some head) current_branch rest
···535562 (* Extract branch name from refs/heads/... *)
536563 let branch =
537564 if String.starts_with ~prefix:"refs/heads/" branch_ref then
538538- Some (String.sub branch_ref 11 (String.length branch_ref - 11))
539539- else
540540- Some branch_ref
565565+ Some
566566+ (String.sub branch_ref 11
567567+ (String.length branch_ref - 11))
568568+ else Some branch_ref
541569 in
542570 parse_entries acc current_path current_head branch rest
543571 else if line = "detached" then
544572 parse_entries acc current_path current_head None rest
545573 else
546546- parse_entries acc current_path current_head current_branch rest
574574+ parse_entries acc current_path current_head current_branch
575575+ rest
547576 in
548577 parse_entries [] None None None lines
549578···556585 let cwd = path_to_eio ~fs path in
557586 run_git_ok ~proc ~cwd [ "cherry-pick"; commit ] |> Result.map ignore
558587559559-let merge ~proc ~fs ~ref_name ?(ff_only=false) path =
588588+let merge ~proc ~fs ~ref_name ?(ff_only = false) path =
560589 let cwd = path_to_eio ~fs path in
561561- let args = ["merge"] @ (if ff_only then ["--ff-only"] else []) @ [ref_name] in
590590+ let args =
591591+ [ "merge" ] @ (if ff_only then [ "--ff-only" ] else []) @ [ ref_name ]
592592+ in
562593 run_git_ok ~proc ~cwd args |> Result.map ignore
563594564595(** {1 Diff Operations} *)
+18-15
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
132132- and resets the local branch to match the remote.
131131+(** [fetch_and_reset ~proc ~fs ?remote ~branch path] fetches from the remote and
132132+ 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
···490490491491(** Operations for git worktree management. *)
492492module Worktree : sig
493493- (** A git worktree entry. *)
494493 type entry = {
495494 path : Fpath.t; (** Absolute path to the worktree *)
496495 head : string; (** HEAD commit hash *)
497496 branch : string option; (** Branch name if not detached *)
498497 }
498498+ (** A git worktree entry. *)
499499500500 val add :
501501 proc:_ Eio.Process.mgr ->
···539539 repo:Fpath.t ->
540540 path:Fpath.t ->
541541 bool
542542- (** [exists ~proc ~fs ~repo ~path] returns true if a worktree exists at [path]. *)
542542+ (** [exists ~proc ~fs ~repo ~path] returns true if a worktree exists at
543543+ [path]. *)
543544end
544545545546(** {1 Cherry-pick Operations} *)
···550551 commit:string ->
551552 Fpath.t ->
552553 (unit, error) result
553553-(** [cherry_pick ~proc ~fs ~commit path] applies a single commit to the current branch.
554554+(** [cherry_pick ~proc ~fs ~commit path] applies a single commit to the current
555555+ branch.
554556555557 @param commit The commit hash to cherry-pick
556558 @param path Path to the repository *)
···562564 ?ff_only:bool ->
563565 Fpath.t ->
564566 (unit, error) result
565565-(** [merge ~proc ~fs ~ref_name ?ff_only path] merges a ref into the current branch.
567567+(** [merge ~proc ~fs ~ref_name ?ff_only path] merges a ref into the current
568568+ branch.
566569567570 @param ref_name The ref to merge (e.g., "verse/handle/main")
568571 @param ff_only If true, only allow fast-forward merges (default: false)
···610613 message:string ->
611614 Fpath.t ->
612615 (unit, error) result
613613-(** [commit ~proc ~fs ~message path] creates a commit with the given message
614614- in the repository at [path]. *)
616616+(** [commit ~proc ~fs ~message path] creates a commit with the given message in
617617+ the repository at [path]. *)
615618616619val rm :
617620 proc:_ Eio.Process.mgr ->
···620623 Fpath.t ->
621624 string ->
622625 (unit, error) result
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
626626+(** [rm ~proc ~fs ~recursive path target] removes [target] from the git index in
627627+ the repository at [path]. If [recursive] is true, removes directories
625628 recursively (git rm -r). *)
626629627630val config :
···641644 prefix:string ->
642645 unit ->
643646 bool
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. *)
647647+(** [has_subtree_history ~proc ~fs ~repo ~prefix ()] returns true if the prefix
648648+ has subtree commit history (i.e., was added via git subtree add). Returns
649649+ false for fresh local packages that were never part of a subtree. *)
647650648651val branch_rename :
649652 proc:_ Eio.Process.mgr ->
···651654 new_name:string ->
652655 Fpath.t ->
653656 (unit, error) result
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]. *)
657657+(** [branch_rename ~proc ~fs ~new_name path] renames the current branch to
658658+ [new_name] in the repository at [path]. Uses [git branch -M]. *)
+715-556
lib/monopam.ml
···4646*)
4747let error_hint = function
4848 | Config_error _ ->
4949- Some
5050- "Run 'monopam init --handle <your-handle>' to create a workspace."
4949+ Some "Run 'monopam init --handle <your-handle>' to create a workspace."
5150 | Repo_error (Opam_repo.No_dev_repo _) ->
5251 Some
5352 "Add a 'dev-repo' field to the package's opam file pointing to a git \
···7776 "Commit changes in the monorepo first: cd mono && git add -A && git \
7877 commit"
7978 | Monorepo_dirty ->
8080- Some "Commit or stash your changes first: git status && git add -A && git commit"
7979+ Some
8080+ "Commit or stash your changes first: git status && git add -A && git \
8181+ commit"
8182 | Package_not_found _ ->
8283 Some "Check available packages: ls opam-repo/packages/"
8384 | Claude_error msg when String.starts_with ~prefix:"Failed to decode" msg ->
···187188 with Eio.Io _ -> [])
188189 repos
189190190190-(** Information about a package discovered from the monorepo. *)
191191type monorepo_package = {
192192 pkg_name : string;
193193 subtree : string;
···195195 url_src : string;
196196 opam_content : string;
197197}
198198+(** Information about a package discovered from the monorepo. *)
198199199199-(** 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) () =
200200+(** Discover packages from monorepo subtrees by parsing dune-project files. If
201201+ [sources] is provided, it overrides the dev-repo URL for matching subtrees.
202202+*)
203203+let discover_packages_from_monorepo ~fs ~config
204204+ ?(sources = Sources_registry.empty) () =
202205 let fs = fs_typed fs in
203206 let monorepo = Config.Paths.monorepo config in
204207 let monorepo_eio = Eio.Path.(fs / Fpath.to_string monorepo) in
···215218 with Eio.Io _ -> []
216219 in
217220218218- Log.debug (fun m -> m "Found %d subdirectories in monorepo" (List.length subdirs));
221221+ Log.debug (fun m ->
222222+ m "Found %d subdirectories in monorepo" (List.length subdirs));
219223220224 (* Process each subdirectory *)
221225 let packages, errors =
···229233 | `Regular_file -> (
230234 (* Parse dune-project *)
231235 let content =
232232- try Some (Eio.Path.load dune_project_path)
233233- with Eio.Io _ -> None
236236+ try Some (Eio.Path.load dune_project_path) with Eio.Io _ -> None
234237 in
235238 match content with
236239 | None -> (pkgs, errs)
···254257 1. Explicit sources.toml entry for this subtree
255258 2. dune-project source/homepage
256259 3. sources.toml default_url_base + subtree name *)
257257- let sources_override = Sources_registry.find sources ~subtree in
260260+ let sources_override =
261261+ Sources_registry.find sources ~subtree
262262+ in
258263259264 let derive_from_dune () =
260265 match
···270275 match Sources_registry.derive_url sources ~subtree with
271276 | Some dev_repo ->
272277 Log.debug (fun m ->
273273- m "Using default_url_base for %s: %s" subtree dev_repo);
278278+ m "Using default_url_base for %s: %s" subtree
279279+ dev_repo);
274280 Some (dev_repo, dev_repo ^ "#main")
275281 | None -> None
276282 in
···286292 | None -> (
287293 (* Try to get branch from dune-project, default to main *)
288294 match dune_proj.source with
289289- | Some (Dune_project.Uri { branch = Some b; _ }) -> b
295295+ | Some (Dune_project.Uri { branch = Some b; _ })
296296+ ->
297297+ b
290298 | _ -> "main")
291299 in
292300 Log.debug (fun m ->
293293- m "Using sources.toml entry for %s: %s" subtree dev_repo);
301301+ m "Using sources.toml entry for %s: %s" subtree
302302+ dev_repo);
294303 Some (dev_repo, dev_repo ^ "#" ^ branch)
295304 | None -> (
296305 match derive_from_dune () with
···300309 | Some result -> Some result
301310 | None ->
302311 Log.warn (fun m ->
303303- m "Cannot derive dev-repo for %s (no source in dune-project or sources.toml)" subtree);
312312+ m
313313+ "Cannot derive dev-repo for %s (no \
314314+ source in dune-project or \
315315+ sources.toml)"
316316+ subtree);
304317 None))
305318 in
306319 match dev_repo_and_url with
307320 | None -> (pkgs, "Cannot derive dev-repo" :: errs)
308321 | Some (dev_repo, url_src) ->
309322 Log.debug (fun m ->
310310- m "Found %d opam files in %s" (List.length opam_files)
311311- subtree);
323323+ m "Found %d opam files in %s"
324324+ (List.length opam_files) subtree);
312325 (* Transform each opam file *)
313326 let new_pkgs =
314327 List.filter_map
···326339 ~dev_repo ~url_src
327340 in
328341 Some
329329- { pkg_name; subtree; dev_repo; url_src; opam_content }
342342+ {
343343+ pkg_name;
344344+ subtree;
345345+ dev_repo;
346346+ url_src;
347347+ opam_content;
348348+ }
330349 with Eio.Io _ -> None)
331350 opam_files
332351 in
···335354 (* No dune-project, skip *)
336355 Log.debug (fun m -> m "No dune-project in %s, skipping" subtree);
337356 (pkgs, errs)
338338- | exception Eio.Io _ ->
339339- (pkgs, errs))
357357+ | exception Eio.Io _ -> (pkgs, errs))
340358 ([], []) subdirs
341359 in
342360···805823806824(** Convert a clone URL to a push URL.
807825 - GitHub HTTPS URLs are converted to SSH format
808808- - Tangled URLs (tangled.org/tangled.sh) are converted to SSH format using the knot server
826826+ - Tangled URLs (tangled.org/tangled.sh) are converted to SSH format using
827827+ the knot server
809828 - Other URLs are returned unchanged
810810- @param knot Git push server hostname. Defaults to git.recoil.org if not provided. *)
829829+830830+ @param knot
831831+ Git push server hostname. Defaults to git.recoil.org if not provided. *)
811832let url_to_push_url ?knot uri =
812833 let scheme = Uri.scheme uri in
813834 let host = Uri.host uri in
···897918 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
898919 let url = Uri.of_string (Fpath.to_string checkout_dir) in
899920 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);
921921+ Log.info (fun m ->
922922+ m "Pulling subtree %s from %a" prefix Fpath.pp checkout_dir);
901923 match Git.Subtree.pull ~proc ~fs ~repo:monorepo ~prefix ~url ~branch () with
902924 | Ok () -> Ok false (* not newly added *)
903925 | Error e -> Error (Git_error e)
904926 end
905927 else begin
906906- Log.info (fun m -> m "Adding subtree %s from %a" prefix Fpath.pp checkout_dir);
928928+ Log.info (fun m ->
929929+ m "Adding subtree %s from %a" prefix Fpath.pp checkout_dir);
907930 match Git.Subtree.add ~proc ~fs ~repo:monorepo ~prefix ~url ~branch () with
908931 | Ok () -> Ok true (* newly added *)
909932 | Error e -> Error (Git_error e)
···11351158 This preserves commit identity, ensuring round-trips converge. *)
11361159 let checkout_url = Uri.of_string (Fpath.to_string checkout_dir) in
11371160 Log.info (fun m -> m "Subtree push %s -> %a" prefix Fpath.pp checkout_dir);
11381138- let* () = Git.Subtree.push ~proc ~fs ~repo:monorepo ~prefix ~url:checkout_url ~branch () in
11611161+ let* () =
11621162+ Git.Subtree.push ~proc ~fs ~repo:monorepo ~prefix ~url:checkout_url
11631163+ ~branch ()
11641164+ in
11391165 Ok ()
11401166 end
11411167···12871313 Eio.Switch.run (fun sw ->
12881314 let child =
12891315 Eio.Process.spawn proc ~sw ~cwd
12901290- [ "git"; "config"; "receive.denyCurrentBranch"; "updateInstead" ]
13161316+ [
13171317+ "git"; "config"; "receive.denyCurrentBranch"; "updateInstead";
13181318+ ]
12911319 in
12921320 ignore (Eio.Process.await child));
12931321 Ok (true, 0)
···14691497 List.iter
14701498 (fun pkg ->
14711499 let pkg_dir =
14721472- Fpath.(opam_repo / "packages" / pkg.pkg_name / (pkg.pkg_name ^ ".dev"))
15001500+ Fpath.(
15011501+ opam_repo / "packages" / pkg.pkg_name / (pkg.pkg_name ^ ".dev"))
14731502 in
14741503 let dst_path = Eio.Path.(fs / Fpath.to_string pkg_dir / "opam") in
14751504 let dst_content = read_file_opt dst_path in
···14811510 end)
14821511 pkgs;
14831512 if !updated > 0 then
14841484- Log.info (fun m -> m "Regenerated %d opam-repo entries from monorepo" !updated)
15131513+ Log.info (fun m ->
15141514+ m "Regenerated %d opam-repo entries from monorepo" !updated)
1485151514861486-(** 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. *)
15161516+(** Clone monorepo and opam-repo from verse registry if they don't exist
15171517+ locally. This enables `monopam sync` to work in a fresh devcontainer. *)
14881518let clone_from_verse_if_needed ~proc ~fs ~config () =
14891519 let monorepo = Config.Paths.monorepo config in
14901520 let opam_repo = Config.Paths.opam_repo config in
···14981528 match Verse_config.load ~fs () with
14991529 | Error _ ->
15001530 (* No verse config - can't clone from registry *)
15011501- Log.debug (fun m -> m "No verse config found, will initialize fresh repos");
15311531+ Log.debug (fun m ->
15321532+ m "No verse config found, will initialize fresh repos");
15021533 Ok ()
15031503- | Ok verse_config ->
15341534+ | Ok verse_config -> (
15041535 let handle = Verse_config.handle verse_config in
15051536 Log.info (fun m -> m "Found verse config for handle: %s" handle);
15061537 (* Load registry to look up URLs *)
15071507- match Verse_registry.clone_or_pull ~proc ~fs ~config:verse_config () with
15381538+ match
15391539+ Verse_registry.clone_or_pull ~proc ~fs ~config:verse_config ()
15401540+ with
15081541 | Error msg ->
15091542 Log.warn (fun m -> m "Could not load verse registry: %s" msg);
15101510- Ok () (* Continue without cloning - will init fresh *)
15111511- | Ok registry ->
15431543+ Ok () (* Continue without cloning - will init fresh *)
15441544+ | Ok registry -> (
15121545 match Verse_registry.find_member registry ~handle with
15131546 | None ->
15141547 Log.warn (fun m -> m "Handle %s not found in registry" handle);
15151548 Ok ()
15161516- | Some member ->
15491549+ | Some member -> (
15171550 (* Clone monorepo if needed *)
15181551 let result =
15191552 if monorepo_exists then Ok ()
15201553 else begin
15211521- Log.app (fun m -> m "Cloning monorepo from %s..." member.monorepo);
15541554+ Log.app (fun m ->
15551555+ m "Cloning monorepo from %s..." member.monorepo);
15221556 let url = Uri.of_string member.monorepo in
15231523- let branch = Option.value ~default:"main" member.monorepo_branch in
15571557+ let branch =
15581558+ Option.value ~default:"main" member.monorepo_branch
15591559+ in
15241560 match Git.clone ~proc ~fs ~url ~branch monorepo with
15251561 | Ok () ->
15261562 Log.app (fun m -> m "Monorepo cloned successfully");
15271563 Ok ()
15281564 | Error e ->
15291529- Log.err (fun m -> m "Failed to clone monorepo: %a" Git.pp_error e);
15651565+ Log.err (fun m ->
15661566+ m "Failed to clone monorepo: %a" Git.pp_error e);
15301567 Error (Git_error e)
15311568 end
15321569 in
···15361573 (* Clone opam-repo if needed *)
15371574 if opam_repo_exists then Ok ()
15381575 else begin
15391539- Log.app (fun m -> m "Cloning opam-repo from %s..." member.opamrepo);
15761576+ Log.app (fun m ->
15771577+ m "Cloning opam-repo from %s..." member.opamrepo);
15401578 let url = Uri.of_string member.opamrepo in
15411541- let branch = Option.value ~default:"main" member.opamrepo_branch in
15791579+ let branch =
15801580+ Option.value ~default:"main" member.opamrepo_branch
15811581+ in
15421582 match Git.clone ~proc ~fs ~url ~branch opam_repo with
15431583 | Ok () ->
15441584 Log.app (fun m -> m "Opam-repo cloned successfully");
15451585 Ok ()
15461586 | Error e ->
15471547- Log.err (fun m -> m "Failed to clone opam-repo: %a" Git.pp_error e);
15871587+ Log.err (fun m ->
15881588+ m "Failed to clone opam-repo: %a" Git.pp_error e);
15481589 Error (Git_error e)
15491549- end
15901590+ end)))
1550159115511592let sync ~proc ~fs ~config ?package ?(remote = false) ?(skip_push = false)
15521593 ?(skip_pull = false) () =
···1554159515551596 (* Step 0: Sync verse members if verse config exists and not skipping pull *)
15561597 (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));
15981598+ match Verse_config.load ~fs:fs_t () with
15991599+ | Error _ -> () (* No verse config = skip *)
16001600+ | Ok verse_config -> (
16011601+ Log.app (fun m -> m "Syncing verse members...");
16021602+ match Verse.pull ~proc ~fs:fs_t ~config:verse_config () with
16031603+ | Ok () -> ()
16041604+ | Error e -> Log.warn (fun m -> m "Verse sync: %a" Verse.pp_error e)));
1564160515651606 (* Clone from verse registry if repos don't exist *)
15661607 match clone_from_verse_if_needed ~proc ~fs:fs_t ~config () with
15671608 | 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
15881609 | Ok () -> (
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
16101610+ (* Update the opam repo first - clone if needed *)
16111611+ let opam_repo = Config.Paths.opam_repo config in
16121612+ if (not skip_pull) && Git.is_repo ~proc ~fs:fs_t opam_repo then begin
16131613+ Log.info (fun m -> m "Updating opam repo at %a" Fpath.pp opam_repo);
16141614+ let result =
16151615+ let ( let* ) = Result.bind in
16161616+ let* () = Git.fetch ~proc ~fs:fs_t opam_repo in
16171617+ Git.merge_ff ~proc ~fs:fs_t opam_repo
16181618+ in
16191619+ match result with
16201620+ | Ok () -> ()
16211621+ | Error e ->
16221622+ Log.warn (fun m ->
16231623+ m "Failed to update opam repo: %a" Git.pp_error e)
16241624+ end;
16251625+ (* Ensure directories exist *)
16261626+ ensure_checkouts_dir ~fs:fs_t ~config;
16271627+ match ensure_monorepo_initialized ~proc ~fs:fs_t ~config with
15991628 | Error e -> Error e
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))
16291629+ | Ok () ->
16301630+ (* Check for uncommitted changes in monorepo *)
16311631+ let monorepo = Config.Paths.monorepo config in
16321632+ if Git.is_dirty ~proc ~fs:fs_t monorepo then begin
16331633+ Log.err (fun m -> m "Monorepo has uncommitted changes");
16341634+ Error Monorepo_dirty
16351635+ end
16081636 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
16311631- in
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
16471647-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
16371637+ (* Regenerate opam-repo from monorepo to ensure URLs are up to date *)
16381638+ regenerate_opam_repo ~fs:(fs_t :> _ Eio.Path.t) ~config ();
16391639+ match discover_packages ~fs:(fs_t :> _ Eio.Path.t) ~config () with
16401640+ | Error e -> Error e
16411641+ | Ok all_pkgs ->
16421642+ let pkgs =
16431643+ match package with
16441644+ | None -> all_pkgs
16451645+ | Some name ->
16461646+ List.filter (fun p -> Package.name p = name) all_pkgs
16471647+ in
16481648+ if pkgs = [] && package <> None then
16491649+ Error (Package_not_found (Option.get package))
16571650 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
16511651+ (* Step 1: Validate - check for dirty state *)
16521652+ Log.info (fun m ->
16531653+ m "Checking status of %d packages" (List.length pkgs));
16541654+ let statuses =
16551655+ Status.compute_all ~proc ~fs:fs_t ~config pkgs
16781656 in
16791679- let skipped_ok =
16801680- List.map (fun pkg -> Ok (Package.repo_name pkg)) to_skip
16571657+ let dirty =
16581658+ List.filter Status.has_local_changes statuses
16591659+ |> List.map (fun s -> s.Status.package)
16811660 in
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
16611661+ if dirty <> [] then Error (Dirty_state dirty)
16621662+ else begin
16631663+ let repos = unique_repos pkgs in
16641664+ let total = List.length repos in
16651665+ Log.app (fun m -> m "Syncing %d repositories..." total);
1690166616911691- (* 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
16671667+ (* Build status lookup for optimization *)
16681668+ let status_by_name =
16691669+ List.map
16701670+ (fun s -> (Package.name s.Status.package, s))
16711671+ statuses
16721672+ in
16731673+ let sync_needs_push = function
16741674+ | Status.Subtree_ahead _ | Status.Trees_differ -> true
16751675+ | Status.In_sync | Status.Subtree_behind _
16761676+ | Status.Unknown ->
16771677+ false
16781678+ in
16791679+ let needs_push pkg =
16801680+ List.assoc_opt (Package.name pkg) status_by_name
16811681+ |> Option.fold ~none:true ~some:(fun s ->
16821682+ sync_needs_push s.Status.subtree_sync)
16831683+ in
16841684+ let sync_needs_pull = function
16851685+ | Status.Subtree_behind _ | Status.Trees_differ -> true
16861686+ | Status.In_sync | Status.Subtree_ahead _ | Status.Unknown
16871687+ ->
16881688+ false
16891689+ in
16901690+ let needs_pull pkg =
16911691+ List.assoc_opt (Package.name pkg) status_by_name
16921692+ |> Option.fold ~none:true ~some:(fun s ->
16931693+ sync_needs_pull s.Status.subtree_sync)
16941694+ in
16951695+16961696+ (* Step 2: Push phase - export monorepo changes to checkouts (PARALLEL) *)
16971697+ (* git subtree push is read-only on the monorepo, so safe to parallelize *)
16981698+ (* OPTIMIZATION: skip packages already in sync *)
16991699+ let push_results =
17001700+ if skip_push then begin
17011701+ Log.app (fun m ->
17021702+ m " Skipping push to checkouts (--skip-push)");
17031703+ List.map (fun pkg -> Ok (Package.repo_name pkg)) repos
17041704+ end
17051705+ else begin
17061706+ let to_push, to_skip =
17071707+ List.partition needs_push repos
17081708+ in
17091709+ Log.app (fun m ->
17101710+ m
17111711+ " Pushing monorepo changes to checkouts \
17121712+ (parallel)...");
17131713+ if to_skip <> [] then
17141714+ Log.app (fun m ->
17151715+ m " Skipping %d already-synced packages"
17161716+ (List.length to_skip));
17171717+ (* Local git subtree push - no parallelism limit needed *)
17181718+ let pushed =
17191719+ Eio.Fiber.List.map
17201720+ (fun pkg ->
17211721+ let repo_name = Package.repo_name pkg in
17221722+ Log.info (fun m ->
17231723+ m "Push to checkout: %s" repo_name);
17241724+ match push_one ~proc ~fs ~config pkg with
17251725+ | Ok () -> Ok repo_name
17261726+ | Error (Git_error e) ->
17271727+ Error
17281728+ {
17291729+ repo_name;
17301730+ phase = `Push_checkout;
17311731+ error = e;
17321732+ }
17331733+ | Error _ -> Ok repo_name)
17341734+ to_push
17351735+ in
17361736+ let skipped_ok =
17371737+ List.map
17381738+ (fun pkg -> Ok (Package.repo_name pkg))
17391739+ to_skip
17401740+ in
17411741+ pushed @ skipped_ok
17421742+ end
17431743+ in
17441744+ let push_errors =
17451745+ List.filter_map
17461746+ (function Error e -> Some e | Ok _ -> None)
17471747+ push_results
17481748+ in
17491749+17501750+ (* Steps 3-5: Pull phases (fetch, merge, subtree) - skip if --skip-pull *)
17511751+ let ( fetch_errors,
17521752+ unchanged_count,
17531753+ total_commits_pulled,
17541754+ merge_errors,
17551755+ subtree_errors,
17561756+ successfully_fetched_repos ) =
17571757+ if skip_pull then begin
17581758+ Log.app (fun m ->
17591759+ m " Skipping pull from remotes (--skip-pull)");
17601760+ ([], List.length repos, 0, ref [], ref [], repos)
17611761+ end
17621762+ else begin
17631763+ (* Step 3: Fetch phase - clone/fetch from remotes (PARALLEL) *)
17641764+ Log.app (fun m ->
17651765+ m " Fetching from remotes (parallel)...");
17661766+ let fetch_results =
17671767+ Eio.Fiber.List.map ~max_fibers:4
17681768+ (fun pkg ->
17691769+ let repo_name = Package.repo_name pkg in
17701770+ (* First ensure checkout exists *)
17191771 match
17201720- fetch_checkout_safe ~proc ~fs:fs_t ~config pkg
17721772+ ensure_checkout_safe ~proc ~fs:fs_t ~config pkg
17211773 with
17221774 | Error e ->
17231775 Error { repo_name; phase = `Fetch; error = e }
17241724- | Ok commits -> Ok (repo_name, false, commits)))
17251725- repos
17261726- in
17271727- let fetch_errs, fetch_successes =
17281728- List.partition_map
17291729- (function Error e -> Left e | Ok r -> Right r)
17301730- fetch_results
17311731- in
17321732- let cloned =
17331733- List.filter (fun (_, c, _) -> c) fetch_successes
17341734- in
17351735- let updated =
17361736- List.filter
17371737- (fun (_, c, commits) -> (not c) && commits > 0)
17381738- fetch_successes
17391739- in
17401740- let unchanged =
17411741- List.length fetch_successes
17421742- - List.length cloned - List.length updated
17431743- in
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);
17761776+ | Ok (was_cloned, _) -> (
17771777+ if was_cloned then Ok (repo_name, true, 0)
17781778+ else
17791779+ match
17801780+ fetch_checkout_safe ~proc ~fs:fs_t ~config
17811781+ pkg
17821782+ with
17831783+ | Error e ->
17841784+ Error
17851785+ {
17861786+ repo_name;
17871787+ phase = `Fetch;
17881788+ error = e;
17891789+ }
17901790+ | Ok commits ->
17911791+ Ok (repo_name, false, commits)))
17921792+ repos
17931793+ in
17941794+ let fetch_errs, fetch_successes =
17951795+ List.partition_map
17961796+ (function Error e -> Left e | Ok r -> Right r)
17971797+ fetch_results
17981798+ in
17991799+ let cloned =
18001800+ List.filter (fun (_, c, _) -> c) fetch_successes
18011801+ in
18021802+ let updated =
18031803+ List.filter
18041804+ (fun (_, c, commits) -> (not c) && commits > 0)
18051805+ fetch_successes
18061806+ in
18071807+ let unchanged =
18081808+ List.length fetch_successes
18091809+ - List.length cloned - List.length updated
18101810+ in
18111811+ let commits_pulled =
18121812+ List.fold_left
18131813+ (fun acc (_, _, c) -> acc + c)
18141814+ 0 fetch_successes
18151815+ in
18161816+ Log.app (fun m ->
18171817+ m " Pulled: %d cloned, %d updated, %d unchanged"
18181818+ (List.length cloned) (List.length updated)
18191819+ unchanged);
1752182017531753- (* 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
17611761- in
18211821+ (* Filter repos to only those that were successfully fetched *)
18221822+ let success_names =
18231823+ List.map (fun (name, _, _) -> name) fetch_successes
18241824+ in
18251825+ let successfully_fetched =
18261826+ List.filter
18271827+ (fun pkg ->
18281828+ List.mem (Package.repo_name pkg) success_names)
18291829+ repos
18301830+ in
18311831+18321832+ (* Step 4: Merge phase - fast-forward merge checkouts (SEQUENTIAL) *)
18331833+ Log.app (fun m -> m " Merging checkouts...");
18341834+ let merge_errs = ref [] in
18351835+ List.iter
18361836+ (fun pkg ->
18371837+ match
18381838+ merge_checkout_safe ~proc ~fs:fs_t ~config pkg
18391839+ with
18401840+ | Ok () -> ()
18411841+ | Error e ->
18421842+ merge_errs :=
18431843+ {
18441844+ repo_name = Package.repo_name pkg;
18451845+ phase = `Merge;
18461846+ error = e;
18471847+ }
18481848+ :: !merge_errs)
18491849+ successfully_fetched;
18501850+18511851+ (* Step 5: Subtree phase - pull subtrees into monorepo (SEQUENTIAL) *)
18521852+ (* Check if monorepo has local modifications first *)
18531853+ let monorepo = Config.Paths.monorepo config in
18541854+ let monorepo_dirty =
18551855+ Git.is_dirty ~proc ~fs:fs_t monorepo
18561856+ in
18571857+ let subtree_errs = ref [] in
18581858+ if monorepo_dirty then begin
18591859+ Log.warn (fun m ->
18601860+ m
18611861+ "Monorepo has uncommitted changes, skipping \
18621862+ subtree pulls");
18631863+ Log.app (fun m ->
18641864+ m
18651865+ " Skipping subtree updates (local \
18661866+ modifications)...")
18671867+ end
18681868+ else begin
18691869+ (* OPTIMIZATION: skip packages already in sync *)
18701870+ (* But always pull repos that received commits from fetch *)
18711871+ let repos_updated_by_fetch =
18721872+ List.filter_map
18731873+ (fun (name, was_cloned, commits) ->
18741874+ if was_cloned || commits > 0 then Some name
18751875+ else None)
18761876+ fetch_successes
18771877+ in
18781878+ let needs_pull_after_fetch pkg =
18791879+ needs_pull pkg
18801880+ || List.mem (Package.repo_name pkg)
18811881+ repos_updated_by_fetch
18821882+ in
18831883+ let to_pull, to_skip =
18841884+ List.partition needs_pull_after_fetch
18851885+ successfully_fetched
18861886+ in
18871887+ Log.app (fun m -> m " Updating subtrees...");
18881888+ if to_skip <> [] then
18891889+ Log.app (fun m ->
18901890+ m " Skipping %d already-synced subtrees"
18911891+ (List.length to_skip));
18921892+ let pull_count = List.length to_pull in
18931893+ List.iteri
18941894+ (fun i pkg ->
18951895+ Log.info (fun m ->
18961896+ m "[%d/%d] Subtree %s" (i + 1) pull_count
18971897+ (Package.subtree_prefix pkg));
18981898+ match pull_subtree ~proc ~fs ~config pkg with
18991899+ | Ok _ -> ()
19001900+ | Error (Git_error e) ->
19011901+ subtree_errs :=
19021902+ {
19031903+ repo_name = Package.repo_name pkg;
19041904+ phase = `Subtree;
19051905+ error = e;
19061906+ }
19071907+ :: !subtree_errs
19081908+ | Error _ -> ())
19091909+ to_pull
19101910+ end;
19111911+ ( fetch_errs,
19121912+ unchanged,
19131913+ commits_pulled,
19141914+ merge_errs,
19151915+ subtree_errs,
19161916+ successfully_fetched )
19171917+ end
19181918+ in
1762191917631763- (* 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;
19201920+ (* Step 5.5: Verse remotes - update and fetch from verse members *)
19211921+ (* Only operate on successfully fetched repos to avoid missing directory errors *)
19221922+ (match Verse_config.load ~fs:(fs_t :> _ Eio.Path.t) () with
19231923+ | Error _ -> () (* No verse config, skip verse remotes *)
19241924+ | Ok verse_config ->
19251925+ sync_verse_remotes ~proc ~fs:fs_t ~config ~verse_config
19261926+ successfully_fetched_repos;
19271927+ (* Fetch from verse remotes in parallel *)
19281928+ Log.app (fun m -> m " Fetching from verse remotes...");
19291929+ Eio.Fiber.List.iter ~max_fibers:4
19301930+ (fun pkg ->
19311931+ fetch_verse_remotes ~proc ~fs:fs_t ~config pkg)
19321932+ successfully_fetched_repos);
1779193317801780- (* 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");
19341934+ (* Step 6: Finalize - write README.md, CLAUDE.md, and dune-project (SEQUENTIAL) *)
17901935 Log.app (fun m ->
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
19361936+ m " Writing README.md, CLAUDE.md, and dune-project...");
19371937+ write_readme ~proc ~fs:fs_t ~config all_pkgs;
19381938+ write_claude_md ~proc ~fs:fs_t ~config;
19391939+ write_dune_project ~proc ~fs:fs_t ~config all_pkgs;
19401940+19411941+ (* Step 7: Remote phase - push to upstream remotes if --remote (LIMITED PARALLEL) *)
19421942+ (* Only push repos that were successfully fetched *)
19431943+ let remote_errors =
19441944+ if remote then begin
19451945+ Log.app (fun m -> m " Pushing to upstream remotes...");
19461946+ (* Limit to 2 concurrent pushes to avoid overwhelming remotes *)
19471947+ let push_results =
19481948+ Eio.Fiber.List.map ~max_fibers:2
19491949+ (fun pkg ->
19501950+ let repo_name = Package.repo_name pkg in
19511951+ match
19521952+ push_remote_safe ~proc ~fs:fs_t ~config pkg
19531953+ with
19541954+ | Error e ->
19551955+ Error
19561956+ {
19571957+ repo_name;
19581958+ phase = `Push_remote;
19591959+ error = e;
19601960+ }
19611961+ | Ok () ->
19621962+ Log.app (fun m -> m " Pushed %s" repo_name);
19631963+ Ok repo_name)
19641964+ successfully_fetched_repos
19651965+ in
19661966+ let errors, successes =
19671967+ List.partition_map
19681968+ (function Error e -> Left e | Ok r -> Right r)
19691969+ push_results
19701970+ in
19711971+ Log.app (fun m ->
19721972+ m " Pushed: %d repos to upstream"
19731973+ (List.length successes));
19741974+ errors
19751975+ end
19761976+ else []
18011977 in
18021802- let needs_pull_after_fetch pkg =
18031803- needs_pull pkg
18041804- || List.mem (Package.repo_name pkg) repos_updated_by_fetch
19781978+19791979+ (* Collect all errors *)
19801980+ let all_errors =
19811981+ push_errors @ fetch_errors @ !merge_errors
19821982+ @ !subtree_errors @ remote_errors
18051983 in
18061806- let to_pull, to_skip =
18071807- List.partition needs_pull_after_fetch successfully_fetched
19841984+ let summary =
19851985+ {
19861986+ repos_synced =
19871987+ List.length repos - List.length all_errors;
19881988+ repos_unchanged = unchanged_count;
19891989+ commits_pulled = total_commits_pulled;
19901990+ commits_pushed = 0;
19911991+ (* TODO: track this *)
19921992+ errors = all_errors;
19931993+ }
18081994 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
18321832- end;
18331833- ( fetch_errs,
18341834- unchanged,
18351835- commits_pulled,
18361836- merge_errs,
18371837- subtree_errs,
18381838- successfully_fetched )
18391839- end
18401840- in
1841199518421842- (* 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);
18531853-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;
19961996+ (* Print summary *)
19971997+ Log.app (fun m ->
19981998+ m "@.Summary: %d synced, %d errors" summary.repos_synced
19991999+ (List.length summary.errors));
20002000+ if summary.errors <> [] then
20012001+ List.iter
20022002+ (fun e ->
20032003+ Log.warn (fun m -> m " %a" pp_sync_failure e))
20042004+ summary.errors;
1860200518611861- (* 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
18781878- in
18791879- let errors, successes =
18801880- List.partition_map
18811881- (function Error e -> Left e | Ok r -> Right r)
18821882- push_results
18831883- in
18841884- Log.app (fun m ->
18851885- m " Pushed: %d repos to upstream" (List.length successes));
18861886- errors
20062006+ Ok summary
20072007+ end
18872008 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)
20092009+ end)
1920201019212011(* Opam metadata sync: copy .opam files from monorepo subtrees to opam-repo *)
19222012···19792069 | Ok s ->
19802070 let count = List.length (Sources_registry.to_list s) in
19812071 if count > 0 then
19821982- Log.info (fun m -> m "Loaded %d source overrides from sources.toml" count);
20722072+ Log.info (fun m ->
20732073+ m "Loaded %d source overrides from sources.toml" count);
19832074 s
19842075 | Error msg ->
19852076 Log.warn (fun m -> m "Failed to load sources.toml: %s" msg);
···19872078 in
1988207919892080 (* Discover packages from monorepo *)
19901990- match discover_packages_from_monorepo ~fs:(fs :> _ Eio.Path.t) ~config ~sources () with
20812081+ match
20822082+ discover_packages_from_monorepo ~fs:(fs :> _ Eio.Path.t) ~config ~sources ()
20832083+ with
19912084 | Error e -> Error e
19922085 | Ok all_pkgs ->
19932086 (* Filter to specific package/subtree if requested *)
···20122105 (fun pkg ->
20132106 (* Destination: opam-repo/packages/<name>/<name>.dev/opam *)
20142107 let pkg_dir =
20152015- Fpath.(opam_repo / "packages" / pkg.pkg_name / (pkg.pkg_name ^ ".dev"))
21082108+ Fpath.(
21092109+ opam_repo / "packages" / pkg.pkg_name / (pkg.pkg_name ^ ".dev"))
20162110 in
20172111 let dst_path = Eio.Path.(fs / Fpath.to_string pkg_dir / "opam") in
20182112···2043213720442138 (* Find and delete orphaned packages *)
20452139 let generated_names =
20462046- List.map (fun p -> p.pkg_name) pkgs
20472047- |> List.sort_uniq String.compare
21402140+ List.map (fun p -> p.pkg_name) pkgs |> List.sort_uniq String.compare
20482141 in
20492142 let existing_packages = list_opam_repo_packages ~fs ~config in
20502143 let orphaned =
···20702163 {
20712164 synced = List.rev !synced;
20722165 unchanged = List.rev !unchanged;
20732073- missing = []; (* No longer used in generation-based approach *)
21662166+ missing = [];
21672167+ (* No longer used in generation-based approach *)
20742168 orphaned = deleted;
20752169 }
20762170 in
···26002694 handle : string;
26012695 relationship : Forks.relationship;
26022696 commits : Git.log_entry list;
26032603- patches : (string * string) list; (* hash -> patch content *)
26972697+ patches : (string * string) list; (* hash -> patch content *)
26042698}
2605269926062606-type diff_result = {
26072607- entries : diff_entry list;
26082608- forks : Forks.t;
26092609-}
27002700+type diff_result = { entries : diff_entry list; forks : Forks.t }
2610270126112702let pp_diff_entry ~show_patch ppf entry =
26122703 let n_commits = List.length entry.commits in
26132704 Fmt.pf ppf "@[<v 2>%a %s (%a, %d commit%s):@,"
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 -> ())
27052705+ Fmt.(styled `Bold string)
27062706+ entry.repo_name entry.handle Forks.pp_relationship entry.relationship
27072707+ n_commits
27082708+ (if n_commits = 1 then "" else "s");
27092709+ List.iter
27102710+ (fun (c : Git.log_entry) ->
27112711+ let short_hash = String.sub c.hash 0 (min 7 (String.length c.hash)) in
27122712+ Fmt.pf ppf " %a %s %a@,"
27132713+ Fmt.(styled `Yellow string)
27142714+ short_hash c.subject
27152715+ Fmt.(styled `Faint string)
27162716+ c.author;
27172717+ if show_patch then
27182718+ match List.assoc_opt c.hash entry.patches with
27192719+ | Some patch -> Fmt.pf ppf "@,%s@," patch
27202720+ | None -> ())
26282721 entry.commits;
26292722 Fmt.pf ppf "@]"
26302723···26342727 (* Then show diffs for each entry *)
26352728 if result.entries <> [] then begin
26362729 Fmt.pf ppf "@[<v>%a@]@."
26372637- Fmt.(list ~sep:(any "@,@,") (pp_diff_entry ~show_patch)) result.entries
27302730+ Fmt.(list ~sep:(any "@,@,") (pp_diff_entry ~show_patch))
27312731+ result.entries
26382732 end
2639273326402734(** Check if a string looks like a git commit hash (7+ hex chars) *)
26412735let is_commit_sha s =
26422642- String.length s >= 7 &&
26432643- String.for_all (function '0'..'9' | 'a'..'f' | 'A'..'F' -> true | _ -> false) s
27362736+ String.length s >= 7
27372737+ && String.for_all
27382738+ (function '0' .. '9' | 'a' .. 'f' | 'A' .. 'F' -> true | _ -> false)
27392739+ s
2644274026452645-let diff ~proc ~fs ~config ~verse_config ?repo ?(refresh=false) ?(patch=false) () =
27412741+let diff ~proc ~fs ~config ~verse_config ?repo ?(refresh = false)
27422742+ ?(patch = false) () =
26462743 let checkouts_path = Config.Paths.checkouts config in
2647274426482745 (* Compute fork analysis *)
26492649- let forks = Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh () in
27462746+ let forks =
27472747+ Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh ()
27482748+ in
2650274926512750 (* Filter repos if specific one requested *)
26522652- let repos_to_check = match repo with
27512751+ let repos_to_check =
27522752+ match repo with
26532753 | None -> forks.repos
26542754 | Some name -> List.filter (fun r -> r.Forks.repo_name = name) forks.repos
26552755 in
2656275626572757 (* For each repo with actionable status, get commits *)
26582758 let 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)
27592759+ List.filter_map
27602760+ (fun (r : Forks.repo_analysis) ->
27612761+ (* Find actionable verse sources *)
27622762+ let actionable =
27632763+ List.filter
27642764+ (fun (_, _, rel) ->
27652765+ match rel with
27662766+ | Forks.I_am_behind _ -> true
27672767+ | Forks.Diverged _ -> true
27682768+ | _ -> false)
27692769+ r.verse_sources
27702770+ in
27712771+ match actionable with
27722772+ | [] -> None
27732773+ | sources -> (
27742774+ (* Get commits for each actionable source *)
27752775+ let entries =
27762776+ List.filter_map
27772777+ (fun (handle, _src, rel) ->
27782778+ let checkout_path = Fpath.(checkouts_path / r.repo_name) in
27792779+ if not (Git.is_repo ~proc ~fs checkout_path) then None
27802780+ else begin
27812781+ let remote_name = "verse/" ^ handle in
27822782+ let my_ref = "origin/main" in
27832783+ let their_ref = remote_name ^ "/main" in
27842784+ (* Get commits they have that I don't *)
27852785+ match
27862786+ Git.log_range ~proc ~fs ~base:my_ref ~tip:their_ref
27872787+ ~max_count:20 checkout_path
27882788+ with
27892789+ | Error _ -> None
27902790+ | Ok commits when commits = [] -> None
27912791+ | Ok commits ->
27922792+ (* Fetch patches if requested *)
27932793+ let patches =
27942794+ if patch then
27952795+ List.filter_map
27962796+ (fun (c : Git.log_entry) ->
27972797+ match
27982798+ Git.show_patch ~proc ~fs ~commit:c.hash
27992799+ checkout_path
28002800+ with
28012801+ | Ok p -> Some (c.hash, p)
28022802+ | Error _ -> None)
28032803+ commits
28042804+ else []
28052805+ in
28062806+ Some
28072807+ {
28082808+ repo_name = r.repo_name;
28092809+ handle;
28102810+ relationship = rel;
28112811+ commits;
28122812+ patches;
28132813+ }
28142814+ end)
28152815+ sources
28162816+ in
28172817+ match entries with [] -> None | _ -> Some entries))
27012818 repos_to_check
27022819 |> List.flatten
27032820 in
27042821 { entries; forks }
2705282227062706-(** Result of looking up a specific commit *)
27072823type commit_info = {
27082824 commit_repo : string;
27092825 commit_handle : string;
···27122828 commit_author : string;
27132829 commit_patch : string;
27142830}
28312831+(** Result of looking up a specific commit *)
2715283227162833(** Show patch for a specific commit SHA from diff output *)
27172717-let diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?(refresh=false) () =
28342834+let diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?(refresh = false) ()
28352835+ =
27182836 let checkouts_path = Config.Paths.checkouts config in
2719283727202838 (* Compute fork analysis to find which repo has this commit *)
27212721- let forks = Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh () in
28392839+ let forks =
28402840+ Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh ()
28412841+ in
2722284227232843 (* Search through repos for this commit *)
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
28442844+ let result =
28452845+ List.find_map
28462846+ (fun (r : Forks.repo_analysis) ->
28472847+ let checkout_path = Fpath.(checkouts_path / r.repo_name) in
28482848+ if not (Git.is_repo ~proc ~fs checkout_path) then None
28492849+ else
28502850+ (* Check each verse source *)
28512851+ List.find_map
28522852+ (fun (handle, _src, rel) ->
28532853+ match rel with
28542854+ | Forks.I_am_behind _ | Forks.Diverged _ -> (
28552855+ let remote_name = "verse/" ^ handle in
28562856+ let my_ref = "origin/main" in
28572857+ let their_ref = remote_name ^ "/main" in
28582858+ (* Get commits they have that I don't *)
28592859+ match
28602860+ Git.log_range ~proc ~fs ~base:my_ref ~tip:their_ref
28612861+ ~max_count:50 checkout_path
28622862+ with
28632863+ | Error _ -> None
28642864+ | Ok commits -> (
28652865+ (* Check if our sha matches any commit *)
28662866+ let matching =
28672867+ List.find_opt
28682868+ (fun (c : Git.log_entry) ->
28692869+ String.starts_with ~prefix:sha c.hash
28702870+ || String.starts_with
28712871+ ~prefix:(String.lowercase_ascii sha)
28722872+ (String.lowercase_ascii c.hash))
28732873+ commits
28742874+ in
28752875+ match matching with
28762876+ | None -> None
28772877+ | Some c -> (
28782878+ match
28792879+ Git.show_patch ~proc ~fs ~commit:c.hash
28802880+ checkout_path
28812881+ with
28822882+ | Ok patch ->
28832883+ Some
28842884+ {
28852885+ commit_repo = r.repo_name;
28862886+ commit_handle = handle;
28872887+ commit_hash = c.hash;
28882888+ commit_subject = c.subject;
28892889+ commit_author = c.author;
28902890+ commit_patch = patch;
28912891+ }
28922892+ | Error _ -> None)))
28932893+ | _ -> None)
28942894+ r.verse_sources)
28952895+ forks.repos
27612896 in
27622897 result
27632898···27722907let pp_handle_pull_result ppf result =
27732908 if result.repos_pulled <> [] then begin
27742909 Fmt.pf ppf "@[<v>%a@," Fmt.(styled `Bold string) "Pulled:";
27752775- List.iter (fun (repo, count) ->
27762776- Fmt.pf ppf " %s: %d commits@," repo count)
29102910+ List.iter
29112911+ (fun (repo, count) -> Fmt.pf ppf " %s: %d commits@," repo count)
27772912 result.repos_pulled;
27782913 Fmt.pf ppf "@]"
27792914 end;
27802915 if result.repos_skipped <> [] then
27812916 Fmt.pf ppf "%a %s@,"
27822782- Fmt.(styled `Faint string) "Skipped:"
29172917+ Fmt.(styled `Faint string)
29182918+ "Skipped:"
27832919 (String.concat ", " result.repos_skipped);
27842920 if result.repos_failed <> [] then begin
27852921 Fmt.pf ppf "@[<v>%a@," Fmt.(styled `Red string) "Failed:";
27862786- List.iter (fun (repo, err) ->
27872787- Fmt.pf ppf " %s: %s@," repo err)
29222922+ List.iter
29232923+ (fun (repo, err) -> Fmt.pf ppf " %s: %s@," repo err)
27882924 result.repos_failed;
27892925 Fmt.pf ppf "@]"
27902926 end
2791292727922792-let pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo ?(refresh=false) () =
29282928+let pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo
29292929+ ?(refresh = false) () =
27932930 let checkouts_path = Config.Paths.checkouts config in
2794293127952932 (* Compute fork analysis *)
27962796- let forks = Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh () in
29332933+ let forks =
29342934+ Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh ()
29352935+ in
2797293627982937 (* Filter repos if specific one requested *)
27992799- let repos_to_check = match repo with
29382938+ let repos_to_check =
29392939+ match repo with
28002940 | None -> forks.repos
28012941 | Some name -> List.filter (fun r -> r.Forks.repo_name = name) forks.repos
28022942 in
···28062946 let repos_skipped = ref [] in
28072947 let repos_failed = ref [] in
2808294828092809- 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)
29492949+ List.iter
29502950+ (fun (r : Forks.repo_analysis) ->
29512951+ (* Check if this handle has commits for this repo *)
29522952+ let handle_source =
29532953+ List.find_opt (fun (h, _, _) -> h = handle) r.verse_sources
29542954+ in
29552955+ match handle_source with
29562956+ | None ->
29572957+ (* Handle doesn't have this repo *)
29582958+ ()
29592959+ | Some (_, _, rel) ->
29602960+ let checkout_path = Fpath.(checkouts_path / r.repo_name) in
29612961+ if not (Git.is_repo ~proc ~fs checkout_path) then
29622962+ repos_skipped := r.repo_name :: !repos_skipped
29632963+ else begin
29642964+ match rel with
29652965+ | Forks.Same_url | Forks.Same_commit | Forks.I_am_ahead _ ->
29662966+ repos_skipped := r.repo_name :: !repos_skipped
29672967+ | Forks.Not_fetched | Forks.Unrelated ->
29682968+ repos_skipped := r.repo_name :: !repos_skipped
29692969+ | Forks.I_am_behind count -> (
29702970+ (* Merge their changes *)
29712971+ let remote_ref = "verse/" ^ handle ^ "/main" in
29722972+ match
29732973+ Git.merge ~proc ~fs ~ref_name:remote_ref ~ff_only:true
29742974+ checkout_path
29752975+ with
29762976+ | Ok () -> repos_pulled := (r.repo_name, count) :: !repos_pulled
29772977+ | Error e ->
29782978+ repos_failed :=
29792979+ (r.repo_name, Fmt.str "%a" Git.pp_error e)
29802980+ :: !repos_failed)
29812981+ | Forks.Diverged { their_ahead; _ } -> (
29822982+ (* Merge their changes (may create a merge commit) *)
29832983+ let remote_ref = "verse/" ^ handle ^ "/main" in
29842984+ match
29852985+ Git.merge ~proc ~fs ~ref_name:remote_ref checkout_path
29862986+ with
29872987+ | Ok () ->
29882988+ repos_pulled := (r.repo_name, their_ahead) :: !repos_pulled
29892989+ | Error e ->
29902990+ repos_failed :=
29912991+ (r.repo_name, Fmt.str "%a" Git.pp_error e)
29922992+ :: !repos_failed)
29932993+ end)
28432994 repos_to_check;
2844299528452845- Ok {
28462846- repos_pulled = List.rev !repos_pulled;
28472847- repos_skipped = List.rev !repos_skipped;
28482848- repos_failed = List.rev !repos_failed;
28492849- }
29962996+ Ok
29972997+ {
29982998+ repos_pulled = List.rev !repos_pulled;
29992999+ repos_skipped = List.rev !repos_skipped;
30003000+ repos_failed = List.rev !repos_failed;
30013001+ }
2850300228513003(* ==================== Cherry-pick ==================== *)
28523004···28573009}
2858301028593011let pp_cherrypick_result ppf result =
28602860- let short_hash = String.sub result.commit_hash 0 (min 7 (String.length result.commit_hash)) in
30123012+ let short_hash =
30133013+ String.sub result.commit_hash 0 (min 7 (String.length result.commit_hash))
30143014+ in
28613015 Fmt.pf ppf "Cherry-picked %a %s into %s@."
28622862- Fmt.(styled `Yellow string) short_hash
28632863- result.commit_subject
28642864- result.repo_name
30163016+ Fmt.(styled `Yellow string)
30173017+ short_hash result.commit_subject result.repo_name
2865301828662866-let cherrypick ~proc ~fs ~config ~verse_config ~sha ?(refresh=false) () =
30193019+let cherrypick ~proc ~fs ~config ~verse_config ~sha ?(refresh = false) () =
28673020 let checkouts_path = Config.Paths.checkouts config in
2868302128693022 (* First, find the commit *)
28703023 match diff_show_commit ~proc ~fs ~config ~verse_config ~sha ~refresh () with
28713024 | None ->
28722872- Error (Config_error (Printf.sprintf "Commit %s not found in any verse diff" sha))
30253025+ Error
30263026+ (Config_error
30273027+ (Printf.sprintf "Commit %s not found in any verse diff" sha))
28733028 | Some info ->
28743029 let checkout_path = Fpath.(checkouts_path / info.commit_repo) in
28753030 if not (Git.is_repo ~proc ~fs checkout_path) then
28762876- Error (Config_error (Printf.sprintf "No checkout for repository %s" info.commit_repo))
30313031+ Error
30323032+ (Config_error
30333033+ (Printf.sprintf "No checkout for repository %s" info.commit_repo))
28773034 else begin
28782878- match Git.cherry_pick ~proc ~fs ~commit:info.commit_hash checkout_path with
30353035+ match
30363036+ Git.cherry_pick ~proc ~fs ~commit:info.commit_hash checkout_path
30373037+ with
28793038 | Ok () ->
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)
30393039+ Ok
30403040+ {
30413041+ repo_name = info.commit_repo;
30423042+ commit_hash = info.commit_hash;
30433043+ commit_subject = info.commit_subject;
30443044+ }
30453045+ | Error e -> Error (Git_error e)
28873046 end
+49-45
lib/monopam.mli
···221221(** [sync_opam_files ~proc ~fs ~config ?package ()] generates opam-repo entries
222222 from monorepo dune-project files.
223223224224- 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
224224+ For each subtree directory in the monorepo: 1. Parses the dune-project to
225225+ extract source/homepage URL 2. For each .opam file in the subtree:
226226+ - Transforms it by removing dune-generated comment
227227+ - Adds dev-repo and url fields derived from dune-project
228228+ - Writes to opam-repo/packages/<name>/<name>.dev/opam 3. Deletes any
229229+ orphaned packages in opam-repo not found in monorepo 4. Stages and commits
230230+ changes in opam-repo
232231233232 This is a generation-based approach - opam-repo is derived entirely from
234233 monorepo dune-project and .opam files.
···312311 @param config Monopam configuration
313312 @param pkgs List of packages discovered from the opam overlay *)
314313315315-(** Information about a package discovered from the monorepo. *)
316314type monorepo_package = {
317315 pkg_name : string; (** Package name (from .opam filename) *)
318316 subtree : string; (** Subtree directory name *)
···320318 url_src : string; (** url src with branch (e.g., "git+https://...#main") *)
321319 opam_content : string; (** Transformed opam file content ready to write *)
322320}
321321+(** Information about a package discovered from the monorepo. *)
323322324323val discover_packages_from_monorepo :
325324 fs:Eio.Fs.dir_ty Eio.Path.t ->
···330329(** [discover_packages_from_monorepo ~fs ~config ?sources ()] scans monorepo
331330 subtrees and discovers packages from dune-project files.
332331333333- 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
332332+ For each subdirectory of the monorepo with a dune-project file: 1. Checks
333333+ sources.toml for URL override 2. Falls back to dune-project source/homepage
334334+ URL 3. For each .opam file in that directory, transforms it with dev-repo
335335+ and url
337336338337 @param fs Eio filesystem
339338 @param config Monopam configuration
···411410412411(** {1 Diff} *)
413412414414-(** A diff entry for a single repository showing commits from a verse member. *)
415413type diff_entry = {
416414 repo_name : string;
417415 handle : string;
···419417 commits : Git.log_entry list;
420418 patches : (string * string) list; (** hash -> patch content *)
421419}
420420+(** A diff entry for a single repository showing commits from a verse member. *)
422421422422+type diff_result = { entries : diff_entry list; forks : Forks.t }
423423(** Result of computing diffs for repos needing attention. *)
424424-type diff_result = {
425425- entries : diff_entry list;
426426- forks : Forks.t;
427427-}
428424429425val pp_diff_entry : show_patch:bool -> diff_entry Fmt.t
430430-(** [pp_diff_entry ~show_patch] formats a single diff entry.
431431- If [show_patch] is true, includes the patch content for each commit. *)
426426+(** [pp_diff_entry ~show_patch] formats a single diff entry. If [show_patch] is
427427+ true, includes the patch content for each commit. *)
432428433429val pp_diff_result : show_patch:bool -> diff_result Fmt.t
434430(** [pp_diff_result ~show_patch] formats the full diff result. *)
435431436432val is_commit_sha : string -> bool
437437-(** [is_commit_sha s] returns true if [s] looks like a git commit hash
438438- (7+ hexadecimal characters). *)
433433+(** [is_commit_sha s] returns true if [s] looks like a git commit hash (7+
434434+ hexadecimal characters). *)
439435440436val diff :
441437 proc:_ Eio.Process.mgr ->
···447443 ?patch:bool ->
448444 unit ->
449445 diff_result
450450-(** [diff ~proc ~fs ~config ~verse_config ?repo ?refresh ?patch ()] computes and displays diffs
451451- for repositories that need attention from verse members.
446446+(** [diff ~proc ~fs ~config ~verse_config ?repo ?refresh ?patch ()] computes and
447447+ displays diffs for repositories that need attention from verse members.
452448453449 For each repository where a verse member is ahead (I_am_behind or Diverged),
454450 retrieves the commit log showing what commits they have that you don't.
···462458 @param verse_config Verse configuration
463459 @param repo Optional specific repository to show diff for
464460 @param refresh If true, force fresh fetches ignoring cache (default: false)
465465- @param patch If true, fetch and include patch content for each commit (default: false) *)
461461+ @param patch
462462+ If true, fetch and include patch content for each commit (default: false)
463463+*)
466464467467-(** Result of looking up a specific commit *)
468465type commit_info = {
469466 commit_repo : string;
470467 commit_handle : string;
···473470 commit_author : string;
474471 commit_patch : string;
475472}
473473+(** Result of looking up a specific commit *)
476474477475val diff_show_commit :
478476 proc:_ Eio.Process.mgr ->
···483481 ?refresh:bool ->
484482 unit ->
485483 commit_info option
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.
484484+(** [diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?refresh ()] finds
485485+ and shows the patch for a specific commit SHA from the diff output.
488486489487 Searches through all repos with actionable verse sources to find a commit
490490- matching the given SHA prefix. Returns [Some commit_info] if found, [None] otherwise.
488488+ matching the given SHA prefix. Returns [Some commit_info] if found, [None]
489489+ otherwise.
491490492491 @param sha Commit SHA prefix (7+ characters) to look up *)
493492494493(** {1 Pull from Verse Members} *)
495494496496-(** Result of pulling from a handle. *)
497495type handle_pull_result = {
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 *)
496496+ repos_pulled : (string * int) list;
497497+ (** (repo_name, commit_count) for each repo pulled *)
498498+ repos_skipped : string list;
499499+ (** Repos skipped (already in sync or no checkout) *)
500500+ repos_failed : (string * string) list;
501501+ (** (repo_name, error_message) for failures *)
501502}
503503+(** Result of pulling from a handle. *)
502504503505val pp_handle_pull_result : handle_pull_result Fmt.t
504506(** [pp_handle_pull_result] formats a pull result. *)
···516518(** [pull_from_handle ~proc ~fs ~config ~verse_config ~handle ?repo ?refresh ()]
517519 pulls commits from a verse member's forks into your local checkouts.
518520519519- 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]
521521+ For each repository where the handle has commits you don't have: 1. Merges
522522+ their commits into your checkout's main branch 2. The changes are then ready
523523+ to be synced to the monorepo via [sync]
522524523523- If [repo] is specified, only pulls from that repository.
524524- Otherwise, pulls from all repositories where the handle is ahead.
525525+ If [repo] is specified, only pulls from that repository. Otherwise, pulls
526526+ from all repositories where the handle is ahead.
525527526528 @param handle The verse member handle (e.g., "avsm.bsky.social")
527529 @param repo Optional specific repository to pull from
528528- @param refresh If true, force fresh fetches ignoring cache (default: false) *)
530530+ @param refresh If true, force fresh fetches ignoring cache (default: false)
531531+*)
529532530533(** {1 Cherry-pick} *)
531534532532-(** Result of cherry-picking a commit. *)
533535type cherrypick_result = {
534536 repo_name : string;
535537 commit_hash : string;
536538 commit_subject : string;
537539}
540540+(** Result of cherry-picking a commit. *)
538541539542val pp_cherrypick_result : cherrypick_result Fmt.t
540543(** [pp_cherrypick_result] formats a cherry-pick result. *)
···548551 ?refresh:bool ->
549552 unit ->
550553 (cherrypick_result, error) result
551551-(** [cherrypick ~proc ~fs ~config ~verse_config ~sha ?refresh ()]
552552- applies a specific commit from a verse member's fork to your local checkout.
554554+(** [cherrypick ~proc ~fs ~config ~verse_config ~sha ?refresh ()] applies a
555555+ specific commit from a verse member's fork to your local checkout.
553556554557 Finds the commit in the verse diff output and cherry-picks it into the
555555- appropriate local checkout. The changes are then ready to be synced to
556556- the monorepo via [sync].
558558+ appropriate local checkout. The changes are then ready to be synced to the
559559+ monorepo via [sync].
557560558561 @param sha Commit SHA prefix (7+ characters) to cherry-pick
559559- @param refresh If true, force fresh fetches ignoring cache (default: false) *)
562562+ @param refresh If true, force fresh fetches ignoring cache (default: false)
563563+*)
+16-9
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) with Eio.Io _ as e -> Error (Io_error (Printexc.to_string e))
191191+ try Ok (Eio.Path.load eio_path)
192192+ with Eio.Io _ as e -> Error (Io_error (Printexc.to_string e))
192193193193-(** Replace dev-repo line in content. Looks for 'dev-repo: "..."' and replaces the URL. *)
194194+(** Replace dev-repo line in content. Looks for 'dev-repo: "..."' and replaces
195195+ the URL. *)
194196let replace_dev_repo_line content ~new_url =
195197 let lines = String.split_on_char '\n' content in
196198 let dev_repo_url =
···215217 let url_src =
216218 let base =
217219 if String.starts_with ~prefix:"git@" new_url then "git+" ^ new_url
218218- else if String.starts_with ~prefix:"https://" new_url then "git+" ^ new_url
220220+ else if String.starts_with ~prefix:"https://" new_url then
221221+ "git+" ^ new_url
219222 else if String.starts_with ~prefix:"git+" new_url then new_url
220223 else "git+" ^ new_url
221224 in
···239242 else
240243 (* Skip this line, it's part of the old url block *)
241244 process rest true acc
242242- else if trimmed = "url {" || String.starts_with ~prefix:"url {" trimmed then
245245+ else if trimmed = "url {" || String.starts_with ~prefix:"url {" trimmed
246246+ then
243247 (* Start of url block *)
244248 if String.ends_with ~suffix:"}" trimmed then
245249 (* Single-line url block *)
···252256 in
253257 String.concat "\n" (process lines false [])
254258255255-(** 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"). *)
259259+(** 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"). *)
257262let replace_dev_repo_url content ~new_url =
258263 let content = replace_dev_repo_line content ~new_url in
259264 let content = replace_url_section content ~new_url in
260265 content
261266262262-(** Write an opam package to the opam-repo overlay.
263263- Creates the directory structure: packages/<name>/<name.version>/opam *)
267267+(** Write an opam package to the opam-repo overlay. Creates the directory
268268+ structure: packages/<name>/<name.version>/opam *)
264269let write_package ~fs ~repo_path ~name ~version ~content =
265265- let pkg_dir = Fpath.(repo_path / "packages" / name / (name ^ "." ^ version)) in
270270+ let pkg_dir =
271271+ Fpath.(repo_path / "packages" / name / (name ^ "." ^ version))
272272+ in
266273 let opam_path = Fpath.(pkg_dir / "opam") in
267274 let eio_pkg_dir = Eio.Path.(fs / Fpath.to_string pkg_dir) in
268275 let eio_opam_path = Eio.Path.(fs / Fpath.to_string opam_path) in
+8-5
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 items. *)
9393+(** [find_dev_repo items] extracts the dev-repo field from parsed opam file
9494+ items. *)
94959596(** {1 Writing Packages} *)
9697···100101val replace_dev_repo_url : string -> new_url:string -> string
101102(** [replace_dev_repo_url content ~new_url] replaces the dev-repo and url fields
102103 in an opam file content with a new git URL. The new URL should be a plain
103103- git URL (e.g., "git@github.com:user/repo.git" or "https://github.com/user/repo.git"). *)
104104+ git URL (e.g., "git@github.com:user/repo.git" or
105105+ "https://github.com/user/repo.git"). *)
104106105107val write_package :
106108 fs:_ Eio.Path.t ->
···109111 version:string ->
110112 content:string ->
111113 (unit, error) result
112112-(** [write_package ~fs ~repo_path ~name ~version ~content] writes an opam package
113113- to the opam-repo overlay.
114114+(** [write_package ~fs ~repo_path ~name ~version ~content] writes an opam
115115+ package to the opam-repo overlay.
114116115117 Creates the directory structure: [packages/<name>/<name.version>/opam] *)
116118117119val package_exists : fs:_ Eio.Path.t -> repo_path:Fpath.t -> name:string -> bool
118118-(** [package_exists ~fs ~repo_path ~name] checks if a package exists in the opam-repo. *)
120120+(** [package_exists ~fs ~repo_path ~name] checks if a package exists in the
121121+ opam-repo. *)
+2-5
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
3636- process rest false acc
3535+ if String.starts_with ~prefix:"}" trimmed then process rest false acc
3736 else process rest true acc
3837 else if trimmed = "url {" || String.starts_with ~prefix:"url {" trimmed
3938 then
···72717372 (* Step 4: Append dev-repo and url section *)
7473 let dev_repo_line = Printf.sprintf {|dev-repo: "%s"|} dev_repo in
7575- let url_section =
7676- Printf.sprintf "url {\n src: \"%s\"\n}" url_src
7777- in
7474+ let url_section = Printf.sprintf "url {\n src: \"%s\"\n}" url_src in
7875 content ^ "\n" ^ dev_repo_line ^ "\n" ^ url_section ^ "\n"
+4-2
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 file.
1010+(** [transform ~content ~dev_repo ~url_src] transforms a dune-generated opam
1111+ file.
11121213 - Removes the "# This file is generated by dune" comment if present
1314 - Adds or replaces the [dev-repo] field with [dev_repo]
1415 - Adds or replaces the [url { src: "..." }] section with [url_src]
15161617 @param content The original opam file content
1717- @param dev_repo The dev-repo URL (e.g., "git+https://github.com/user/repo.git")
1818+ @param dev_repo
1919+ The dev-repo URL (e.g., "git+https://github.com/user/repo.git")
1820 @param url_src The url src URL with branch (e.g., "git+https://...#main") *)
+361-240
lib/site.ml
···11(** Generate a static HTML site representing the monoverse map. *)
2233-(** Information about a package in the verse *)
43type pkg_info = {
54 name : string;
65 synopsis : string option;
···98 owners : string list; (** List of handles that have this package *)
109 depends : string list; (** Package dependencies *)
1110}
1111+(** Information about a package in the verse *)
12121313-(** Information about a repository (group of packages) *)
1413type repo_info = {
1514 ri_name : string;
1615 ri_dev_repo : string;
1716 ri_packages : pkg_info list;
1818- ri_owners : string list; (** All handles that have any package from this repo *)
1919- ri_fork_status : (string * Forks.relationship) list; (** (handle, relationship) *)
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) *)
2021 ri_dep_count : int; (** Number of dependencies (for sorting) *)
2122}
2323+(** Information about a repository (group of packages) *)
22242323-(** Information about a verse member *)
2425type member_info = {
2526 handle : string;
2627 display_name : string; (** Name to display (from registry or handle) *)
···2930 package_count : int;
3031 unique_packages : string list; (** Packages unique to this member *)
3132}
3333+(** Information about a verse member *)
32343333-(** Aggregated site data *)
3435type site_data = {
3536 local_handle : string;
3637 registry_name : string;
···4041 unique_repos : repo_info list; (** Repos unique to one member *)
4142 all_packages : pkg_info list; (** All packages *)
4243}
4444+(** Aggregated site data *)
43454446(** Scan a member's opam repo and return package info *)
4547let scan_member_packages ~fs opam_repo_path =
4648 let pkgs, _errors = Opam_repo.scan_all ~fs opam_repo_path in
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
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
57605861(** Check if a directory exists *)
5962let dir_exists ~fs path =
···7780 in
78817982 (* Build a map: package name -> list of (handle, pkg_info) *)
8080- let pkg_map : (string, (string * pkg_info) list) Hashtbl.t = Hashtbl.create 256 in
8383+ let pkg_map : (string, (string * pkg_info) list) Hashtbl.t =
8484+ Hashtbl.create 256
8585+ in
81868287 (* Add local packages *)
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;
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;
87938894 let registry_name = registry.Verse_registry.name in
8995 let registry_description = registry.Verse_registry.description in
90969197 (* Build handle -> display name lookup *)
9298 let handle_to_name = Hashtbl.create 16 in
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;
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;
9710498105 (* Get tracked handles from verse directory, excluding local handle *)
99106 let tracked_handles =
···102109 try
103110 Eio.Path.read_dir eio_path
104111 |> List.filter (fun name ->
105105- not (String.ends_with ~suffix:"-opam" name) &&
106106- name <> local_handle &&
107107- dir_exists ~fs Fpath.(verse_path / name))
112112+ (not (String.ends_with ~suffix:"-opam" name))
113113+ && name <> local_handle
114114+ && dir_exists ~fs Fpath.(verse_path / name))
108115 with Eio.Io _ -> []
109116 else []
110117 in
111118112119 (* Scan each tracked member's opam repo *)
113120 let member_infos =
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
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
139153 in
140154141155 (* Add local member info *)
···157171158172 (* Build final package list with owners *)
159173 let all_packages =
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 []
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 []
176189 |> List.sort (fun a b -> String.compare a.name b.name)
177190 in
178191179192 (* Build set of all package names for dependency counting *)
180193 let all_pkg_names =
181181- List.fold_left (fun s p -> Hashtbl.replace s p.name (); s)
194194+ List.fold_left
195195+ (fun s p ->
196196+ Hashtbl.replace s p.name ();
197197+ s)
182198 (Hashtbl.create 256) all_packages
183199 in
184200185201 (* Group packages by repo *)
186202 let repos_map : (string, pkg_info list) Hashtbl.t = Hashtbl.create 64 in
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;
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;
191210192211 (* Build forks status lookup from forks data if provided *)
193193- let forks_by_repo : (string, (string * Forks.relationship) list) Hashtbl.t = Hashtbl.create 64 in
212212+ let forks_by_repo : (string, (string * Forks.relationship) list) Hashtbl.t =
213213+ Hashtbl.create 64
214214+ in
194215 (match forks with
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 -> ());
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 -> ());
201225202226 (* Build repo_info list with dependency counts *)
203227 let all_repos =
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 []
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 []
226255 (* Sort by dependency count descending (apps with most deps first), then by name *)
227256 |> List.sort (fun a b ->
228257 let cmp = compare b.ri_dep_count a.ri_dep_count in
···230259 in
231260232261 (* Separate common and unique repos *)
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
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
235268236269 (* Compute unique packages per member *)
237270 let unique_by_handle = Hashtbl.create 32 in
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;
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;
245281246282 (* Update member infos with unique packages *)
247283 let update_member m =
248248- let unique = try Hashtbl.find unique_by_handle m.handle with Not_found -> [] in
284284+ let unique =
285285+ try Hashtbl.find unique_by_handle m.handle with Not_found -> []
286286+ in
249287 { m with unique_packages = List.sort String.compare unique }
250288 in
251289252290 let all_members = local_member :: member_infos in
253291 let members = List.map update_member all_members in
254292255255- { local_handle; registry_name; registry_description; members; common_repos; unique_repos; all_packages }
293293+ {
294294+ local_handle;
295295+ registry_name;
296296+ registry_description;
297297+ members;
298298+ common_repos;
299299+ unique_repos;
300300+ all_packages;
301301+ }
256302257303(** Escape HTML special characters *)
258304let html_escape s =
259305 let buf = Buffer.create (String.length s) in
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;
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;
267314 Buffer.contents buf
268315269316(** External link SVG icon *)
···276323 | Forks.Same_commit -> "sync"
277324 | Forks.I_am_ahead n -> Printf.sprintf "+%d" n
278325 | Forks.I_am_behind n -> Printf.sprintf "-%d" n
279279- | Forks.Diverged { my_ahead; their_ahead; _ } -> Printf.sprintf "+%d/-%d" my_ahead their_ahead
326326+ | Forks.Diverged { my_ahead; their_ahead; _ } ->
327327+ Printf.sprintf "+%d/-%d" my_ahead their_ahead
280328 | Forks.Unrelated -> "unrel"
281329 | Forks.Not_fetched -> "?"
282330···288336 (* Build member lookups *)
289337 let member_urls = Hashtbl.create 16 in
290338 let member_names = Hashtbl.create 16 in
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;
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;
295344296345 (* Helper to get display name for handle *)
297346 let get_name handle =
298347 try Hashtbl.find member_names handle with Not_found -> handle
299348 in
300349301301- add {|<!DOCTYPE html>
350350+ add
351351+ {|<!DOCTYPE html>
302352<html lang="en">
303353<head>
304354<meta charset="UTF-8">
305355<meta name="viewport" content="width=device-width, initial-scale=1.0">
306356<title>|};
307357 add (html_escape data.registry_name);
308308- add {|</title>
358358+ add
359359+ {|</title>
309360<style>
310361* { margin: 0; padding: 0; box-sizing: border-box; }
311362body { font: 10pt/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #333; max-width: 900px; margin: 0 auto; padding: 12px; }
···365416 (* Title and description *)
366417 add (Printf.sprintf "<h1>%s</h1>\n" (html_escape data.registry_name));
367418 (match data.registry_description with
368368- | Some desc -> add (Printf.sprintf "<div class=\"subtitle\">%s</div>\n" (html_escape desc))
369369- | None -> add "<div class=\"subtitle\"></div>\n");
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");
370423371424 (* Intro section *)
372372- add {|<div class="intro">
425425+ add
426426+ {|<div class="intro">
373427This is an experiment in large-scale agentic coding using OCaml and OxCaml, where we're building environments to exchange vibe code at scale.
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>.
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>.
376435</div>
377436|};
378437379438 (* Members section *)
380439 add "<div class=\"section\">\n<h2>Members</h2>\n<div class=\"members\">\n";
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;
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;
401473 add "</div>\n</div>\n";
402474403475 (* Summary section *)
404476 add "<div class=\"section\">\n";
405477 add "<div class=\"summary\">\n";
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));
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));
409486 add "<div class=\"summary-list\">\n";
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;
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;
414496 add "</div>\n</div>\n";
415497416498 (* Member-specific summary *)
417417- let members_with_unique = List.filter (fun m -> m.unique_packages <> []) data.members in
499499+ let members_with_unique =
500500+ List.filter (fun m -> m.unique_packages <> []) data.members
501501+ in
418502 if members_with_unique <> [] then begin
419503 add "<div class=\"summary\">\n";
420504 add "<div class=\"summary-title\">Member-Specific Packages</div>\n";
421505 add "<div class=\"unique-section\">\n";
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;
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;
431520 add "</div>\n</div>\n"
432521 end;
433522 add "</div>\n";
···436525 if data.common_repos <> [] then begin
437526 add "<div class=\"section\">\n<h2>Repository Details</h2>\n";
438527439439- 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";
528528+ 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";
445541446446- (* 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";
542542+ (* 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";
451547452452- (* 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;
548548+ (* 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;
465565466466- (* 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 -> ("", "")
566566+ (* 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)
480574 in
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;
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;
507621508508- add "</div>\n"
509509- ) data.common_repos;
622622+ add "</div>\n")
623623+ data.common_repos;
510624511625 add "</div>\n"
512626 end;
···514628 (* Footer with generation date *)
515629 let now = Unix.gettimeofday () in
516630 let tm = Unix.gmtime now in
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));
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));
521642522643 add "</body>\n</html>\n";
523644 Buffer.contents buf
+14-11
lib/site.mli
···7788(** {1 Types} *)
991010-(** Information about a package in the verse *)
1110type pkg_info = {
1211 name : string;
1312 synopsis : string option;
···1615 owners : string list; (** List of handles that have this package *)
1716 depends : string list; (** Package dependencies *)
1817}
1818+(** Information about a package in the verse *)
19192020-(** Information about a repository (group of packages) *)
2120type repo_info = {
2221 ri_name : string;
2322 ri_dev_repo : string;
2423 ri_packages : pkg_info list;
2525- ri_owners : string list; (** All handles that have any package from this repo *)
2626- ri_fork_status : (string * Forks.relationship) list; (** (handle, relationship) *)
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) *)
2728 ri_dep_count : int; (** Number of dependencies (for sorting) *)
2829}
3030+(** Information about a repository (group of packages) *)
29313030-(** Information about a verse member *)
3132type member_info = {
3233 handle : string;
3334 display_name : string; (** Name to display (from registry or handle) *)
···3637 package_count : int;
3738 unique_packages : string list; (** Packages unique to this member *)
3839}
4040+(** Information about a verse member *)
39414040-(** Aggregated site data *)
4142type site_data = {
4243 local_handle : string;
4344 registry_name : string;
···4748 unique_repos : repo_info list; (** Repos unique to one member *)
4849 all_packages : pkg_info list; (** All packages *)
4950}
5151+(** Aggregated site data *)
50525153(** {1 Generation} *)
5254···5759 registry:Verse_registry.t ->
5860 unit ->
5961 site_data
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,
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,
6264 includes fork status information for each repository. *)
63656466val generate :
···6870 registry:Verse_registry.t ->
6971 unit ->
7072 string
7171-(** [generate ~fs ~config ?forks ~registry ()] generates the HTML content for the site. *)
7373+(** [generate ~fs ~config ?forks ~registry ()] generates the HTML content for
7474+ the site. *)
72757376val write :
7477 fs:Eio.Fs.dir_ty Eio.Path.t ->
···7881 output_path:Fpath.t ->
7982 unit ->
8083 (unit, string) result
8181-(** [write ~fs ~config ?forks ~registry ~output_path ()] generates and writes the site
8282- to the specified output path. *)
8484+(** [write ~fs ~config ?forks ~registry ~output_path ()] generates and writes
8585+ the site to the specified output path. *)
+23-27
lib/sources_registry.ml
···1010 origin : origin option;
1111}
12121313-type t = {
1414- default_url_base : string option;
1515- entries : (string * entry) list;
1616-}
1313+type t = { default_url_base : string option; entries : (string * entry) list }
17141815let empty = { default_url_base = None; entries = [] }
1919-2016let default_url_base t = t.default_url_base
2121-2222-let with_default_url_base t base =
2323- { t with default_url_base = Some base }
2424-1717+let with_default_url_base t base = { t with default_url_base = Some base }
2518let find t ~subtree = List.assoc_opt subtree t.entries
26192720let derive_url t ~subtree =
···2922 | Some entry -> Some entry.url
3023 | None ->
3124 (* Use default_url_base to construct URL from subtree name *)
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
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
40344135let add t ~subtree entry =
4236 { t with entries = (subtree, entry) :: List.remove_assoc subtree t.entries }
43374444-let remove t ~subtree =
4545- { t with entries = List.remove_assoc subtree t.entries }
4646-3838+let remove t ~subtree = { t with entries = List.remove_assoc subtree t.entries }
4739let to_list t = t.entries
4848-4940let of_list entries = { default_url_base = None; entries }
50415142(* TOML structure:
···6657 ~dec:(function
6758 | "fork" -> Fork
6859 | "join" -> Join
6969- | s -> failwith (Printf.sprintf "Invalid origin: %s (expected 'fork' or 'join')" s))
6060+ | s ->
6161+ failwith
6262+ (Printf.sprintf "Invalid origin: %s (expected 'fork' or 'join')" s))
7063 ~enc:(function Fork -> "fork" | Join -> "join")
7164 Tomlt.string
72657366let entry_codec : entry Tomlt.t =
7467 Tomlt.(
7568 Table.(
7676- obj (fun url upstream branch reason origin -> { url; upstream; branch; reason; origin })
6969+ obj (fun url upstream branch reason origin ->
7070+ { url; upstream; branch; reason; origin })
7771 |> mem "url" string ~enc:(fun e -> e.url)
7872 |> opt_mem "upstream" string ~enc:(fun e -> e.upstream)
7973 |> opt_mem "branch" string ~enc:(fun e -> e.branch)
···8478let codec : t Tomlt.t =
8579 Tomlt.(
8680 Table.(
8787- obj (fun default_url_base entries ->
8888- { default_url_base; entries })
8181+ obj (fun default_url_base entries -> { default_url_base; entries })
8982 |> opt_mem "default_url_base" string ~enc:(fun t -> t.default_url_base)
9083 |> keep_unknown ~enc:(fun t -> t.entries) (Mems.assoc entry_codec)
9184 |> finish))
···9891 | `Regular_file -> (
9992 try Ok (Tomlt_eio.decode_path_exn codec ~fs path_str) with
10093 | Failure msg -> Error (Printf.sprintf "Invalid sources.toml: %s" msg)
101101- | exn -> Error (Printf.sprintf "Error loading sources.toml: %s" (Printexc.to_string exn)))
102102- | _ -> Ok empty (* File doesn't exist, return empty registry *)
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 *)
10399 | exception _ -> Ok empty
104100105101let save ~fs path t =
+15-13
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
44- the dev-repo URL differs from what's declared in dune-project.
55- This is typically used for:
33+ 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:
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
1111- to derive URLs for subtrees without explicit entries:
1010+ The registry also supports a [default_url_base] field that is used to derive
1111+ 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 (** Created via [monopam join] - external repo brought into monorepo *)
2121+ | Join
2222+ (** Created via [monopam join] - external repo brought into monorepo *)
22232323-(** A source entry for a subtree. *)
2424type entry = {
2525- url : string; (** Our dev-repo URL (e.g., "git+https://github.com/avsm/braid") *)
2525+ url : string;
2626+ (** Our dev-repo URL (e.g., "git+https://github.com/avsm/braid") *)
2627 upstream : string option; (** Original upstream URL if this is a fork *)
2728 branch : string option; (** Override branch (default: main) *)
2829 reason : string option; (** Why we have a custom source *)
2930 origin : origin option; (** How this entry was created *)
3031}
3232+(** A source entry for a subtree. *)
31333434+type t
3235(** The sources registry - maps subtree names to source entries. *)
3333-type t
34363537val empty : t
3638(** Empty registry. *)
···4547(** [find t ~subtree] looks up the source entry for a subtree. *)
46484749val derive_url : t -> subtree:string -> string option
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. *)
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. *)
50525153val add : t -> subtree:string -> entry -> t
5254(** [add t ~subtree entry] adds or replaces an entry. *)
···6163(** [of_list entries] creates a registry from an association list. *)
62646365val load : fs:_ Eio.Path.t -> Fpath.t -> (t, string) result
6464-(** [load ~fs path] loads a sources.toml file. Returns empty registry
6565- if file doesn't exist. *)
6666+(** [load ~fs path] loads a sources.toml file. Returns empty registry if file
6767+ doesn't exist. *)
66686769val save : fs:_ Eio.Path.t -> Fpath.t -> t -> (unit, string) result
6870(** [save ~fs path t] writes the registry to a TOML file. *)
+34-22
lib/status.ml
···160160 Fmt.pf ppf "@[<h>%-20s checkout: %a subtree: %a@]" (Package.name t.package)
161161 pp_checkout_status t.checkout pp_subtree_status t.subtree
162162163163-(** Extract handle from a tangled.org URL like "git+https://tangled.org/handle/repo" *)
163163+(** Extract handle from a tangled.org URL like
164164+ "git+https://tangled.org/handle/repo" *)
164165let extract_handle_from_url url =
165165- let url = if String.starts_with ~prefix:"git+" url then
166166- String.sub url 4 (String.length url - 4)
167167- else url in
166166+ let url =
167167+ if String.starts_with ~prefix:"git+" url then
168168+ String.sub url 4 (String.length url - 4)
169169+ else url
170170+ in
168171 let uri = Uri.of_string url in
169172 match Uri.host uri with
170170- | Some "tangled.org" ->
173173+ | Some "tangled.org" -> (
171174 let path = Uri.path uri in
172175 (* Path is like "/handle/repo" - extract first component *)
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)
176176+ let path =
177177+ if String.length path > 0 && path.[0] = '/' then
178178+ String.sub path 1 (String.length path - 1)
179179+ else path
180180+ in
181181+ match String.index_opt path '/' with
182182+ | Some i -> Some (String.sub path 0 i)
183183+ | None -> Some path)
179184 | _ -> None
180185181186(** Format origin indicator from sources registry entry *)
···184189 | None -> ()
185190 | Some Sources_registry.{ origin = Some Sources_registry.Fork; _ } ->
186191 Fmt.pf ppf " %a" Fmt.(styled `Magenta string) "^"
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:")
192192+ | Some
193193+ Sources_registry.
194194+ { origin = Some Sources_registry.Join; upstream = Some url; _ } -> (
195195+ match extract_handle_from_url url with
196196+ | Some handle ->
197197+ (* Abbreviate handle - take first part before dot, max 8 chars *)
198198+ let abbrev =
199199+ match String.index_opt handle '.' with
200200+ | Some i -> String.sub handle 0 i
201201+ | None -> handle
202202+ in
203203+ let abbrev =
204204+ if String.length abbrev > 8 then String.sub abbrev 0 8 else abbrev
205205+ in
206206+ Fmt.pf ppf " %a"
207207+ Fmt.(styled `Cyan (fun ppf s -> pf ppf "v:%s" s))
208208+ abbrev
209209+ | None -> Fmt.pf ppf " %a" Fmt.(styled `Cyan string) "v:")
198210 | Some Sources_registry.{ origin = Some Sources_registry.Join; _ } ->
199211 Fmt.pf ppf " %a" Fmt.(styled `Cyan string) "v:"
200212 | Some _ -> ()
+5-4
lib/status.mli
···113113(** [pp] formats a single package status. *)
114114115115val pp_compact : ?sources:Sources_registry.t -> t Fmt.t
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). *)
116116+(** [pp_compact ?sources] formats a single package status in compact form with
117117+ colors. If [sources] is provided, displays origin indicators (^ for fork,
118118+ v:handle for join). *)
118119119120val pp_summary : ?sources:Sources_registry.t -> t list Fmt.t
120120-(** [pp_summary ?sources] formats a summary of all package statuses.
121121- If [sources] is provided, displays origin indicators for each package. *)
121121+(** [pp_summary ?sources] formats a summary of all package statuses. If
122122+ [sources] is provided, displays origin indicators for each package. *)
+92-40
lib/verse.ml
···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 (** List of conflicting package names *)
99+ | Package_already_exists of string list
1010+ (** List of conflicting package names *)
1011 | Opam_repo_error of Opam_repo.error
11121213let pp_error ppf = function
···2021 Fmt.pf ppf "Package %s not found in %s's opam repo" pkg handle
2122 | Package_already_exists pkgs ->
2223 Fmt.pf ppf "Packages already exist in your opam repo: %a"
2323- Fmt.(list ~sep:comma string) pkgs
2424+ Fmt.(list ~sep:comma string)
2525+ pkgs
2426 | Opam_repo_error e -> Fmt.pf ppf "Opam repo error: %a" Opam_repo.pp_error e
25272628let error_hint = function
2729 | Config_error _ ->
2828- Some
2929- "Run 'monopam init --handle <your-handle>' to create a workspace."
3030+ Some "Run 'monopam init --handle <your-handle>' to create a workspace."
3031 | Git_error (Git.Dirty_worktree _) ->
3132 Some "Commit or stash your changes first: git status"
3233 | Git_error (Git.Command_failed (cmd, _))
···4546 | Workspace_exists _ ->
4647 Some "Use a different directory, or remove the existing workspace."
4748 | Not_a_workspace _ ->
4848- Some "Run 'monopam init --handle <your-handle>' to create a workspace here."
4949+ Some
5050+ "Run 'monopam init --handle <your-handle>' to create a workspace here."
4951 | Package_not_found (pkg, handle) ->
5050- Some (Fmt.str "Run 'monopam verse pull %s' to sync their opam repo, then check package name: %s" handle pkg)
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)
5157 | Package_already_exists 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)))
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)))
5462 | Opam_repo_error _ -> None
55635664let pp_error_with_hint ppf e =
···277285 | Error msg -> Error (Registry_error msg)
278286 | Ok registry -> Ok registry.members
279287280280-(** 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. *)
288288+(** Clone or fetch+reset a single git repo. Returns Ok true if cloned, Ok false
289289+ if reset. Uses fetch+reset instead of pull since verse repos should not have
290290+ local changes. *)
282291let clone_or_reset_repo ~proc ~fs ~url ~branch path =
283292 if Git.is_repo ~proc ~fs path then begin
284293 match Git.fetch_and_reset ~proc ~fs ~branch path with
···320329 (* Clone or fetch+reset monorepo *)
321330 Logs.info (fun m -> m "Syncing %s monorepo" h);
322331 let mono_branch =
323323- Option.value ~default:Verse_config.default_branch member.monorepo_branch
332332+ Option.value ~default:Verse_config.default_branch
333333+ member.monorepo_branch
324334 in
325335 let mono_result =
326336 clone_or_reset_repo ~proc ~fs ~url:member.monorepo
···342352 (* Clone or fetch+reset opam repo *)
343353 Logs.info (fun m -> m "Syncing %s opam repo" h);
344354 let opam_branch =
345345- Option.value ~default:Verse_config.default_branch member.opamrepo_branch
355355+ Option.value ~default:Verse_config.default_branch
356356+ member.opamrepo_branch
346357 in
347358 let opam_result =
348359 clone_or_reset_repo ~proc ~fs ~url:member.opamrepo
···414425 tracked_handles;
415426 subtree_map
416427417417-(** Result of a fork operation. *)
418428type fork_result = {
419429 packages_forked : string list; (** Package names that were forked *)
420430 source_handle : string; (** Handle of the verse member we forked from *)
421431 fork_url : string; (** URL of the fork *)
422432 upstream_url : string; (** Original dev-repo URL (upstream) *)
423423- subtree_name : string; (** Name for the subtree directory (derived from fork URL) *)
433433+ subtree_name : string;
434434+ (** Name for the subtree directory (derived from fork URL) *)
424435}
436436+(** Result of a fork operation. *)
425437426438(** Extract subtree name from a URL (last path component without .git suffix) *)
427439let subtree_name_from_url url =
428440 let uri = Uri.of_string url in
429441 let path = Uri.path uri in
430442 (* Remove leading slash and .git suffix *)
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
443443+ let path =
444444+ if String.length path > 0 && path.[0] = '/' then
445445+ String.sub path 1 (String.length path - 1)
446446+ else path
447447+ in
448448+ let path =
449449+ if String.ends_with ~suffix:".git" path then
450450+ String.sub path 0 (String.length path - 4)
451451+ else path
452452+ in
437453 (* Get last component *)
438454 match String.rindex_opt path '/' with
439455 | Some i -> String.sub path (i + 1) (String.length path - i - 1)
440456 | None -> path
441457442458let pp_fork_result ppf r =
443443- Fmt.pf ppf "@[<v>Forked %d package(s) from %s:@, @[<v>%a@]@,Fork URL: %s@,Upstream: %s@,Subtree: %s@]"
459459+ Fmt.pf ppf
460460+ "@[<v>Forked %d package(s) from %s:@,\
461461+ \ @[<v>%a@]@,\
462462+ Fork URL: %s@,\
463463+ Upstream: %s@,\
464464+ Subtree: %s@]"
444465 (List.length r.packages_forked)
445466 r.source_handle
446446- Fmt.(list ~sep:cut string) r.packages_forked
447447- r.fork_url
448448- r.upstream_url
449449- r.subtree_name
467467+ Fmt.(list ~sep:cut string)
468468+ r.packages_forked r.fork_url r.upstream_url r.subtree_name
450469451470(** Fork a package from a verse member's opam repo into your workspace.
452471···465484 (* Ensure the member exists and their opam-repo is synced *)
466485 match Verse_registry.clone_or_pull ~proc ~fs ~config () with
467486 | Error msg -> Error (Registry_error msg)
468468- | Ok registry ->
487487+ | Ok registry -> (
469488 match Verse_registry.find_member registry ~handle with
470489 | None -> Error (Member_not_found handle)
471471- | Some _member ->
490490+ | Some _member -> (
472491 let verse_path = Verse_config.verse_path config in
473492 let member_opam_repo = Fpath.(verse_path / (handle ^ "-opam")) in
474493 (* Check if their opam repo exists locally *)
475494 if not (is_directory ~fs member_opam_repo) then
476476- Error (Config_error (Fmt.str "Member's opam repo not synced. Run: monopam verse pull %s" handle))
495495+ Error
496496+ (Config_error
497497+ (Fmt.str
498498+ "Member's opam repo not synced. Run: monopam verse pull %s"
499499+ handle))
477500 else
478501 (* Scan their opam repo to find the package *)
479502 let pkgs, _errors = Opam_repo.scan_all ~fs member_opam_repo in
···493516 let user_opam_repo = Verse_config.opam_repo_path config in
494517 let conflicts =
495518 List.filter
496496- (fun name -> Opam_repo.package_exists ~fs ~repo_path:user_opam_repo ~name)
519519+ (fun name ->
520520+ Opam_repo.package_exists ~fs ~repo_path:user_opam_repo
521521+ ~name)
497522 pkg_names
498523 in
499499- if conflicts <> [] then
500500- Error (Package_already_exists conflicts)
524524+ if conflicts <> [] then Error (Package_already_exists conflicts)
501525 else if dry_run then
502526 (* Dry run - just report what would be done *)
503503- Ok { packages_forked = pkg_names; source_handle = handle; fork_url; upstream_url; subtree_name }
527527+ Ok
528528+ {
529529+ packages_forked = pkg_names;
530530+ source_handle = handle;
531531+ fork_url;
532532+ upstream_url;
533533+ subtree_name;
534534+ }
504535 else begin
505536 (* Fork each package *)
506537 let results =
···509540 let name = Package.name p in
510541 let version = Package.version p in
511542 let opam_path =
512512- Fpath.(member_opam_repo / "packages" / name / (name ^ "." ^ version) / "opam")
543543+ Fpath.(
544544+ member_opam_repo / "packages" / name
545545+ / (name ^ "." ^ version)
546546+ / "opam")
513547 in
514548 match Opam_repo.read_opam_file ~fs opam_path with
515549 | Error e -> Error (Opam_repo_error e)
516516- | Ok content ->
550550+ | Ok content -> (
517551 (* Replace dev-repo and url with fork URL *)
518518- let new_content = Opam_repo.replace_dev_repo_url content ~new_url:fork_url in
552552+ let new_content =
553553+ Opam_repo.replace_dev_repo_url content
554554+ ~new_url:fork_url
555555+ in
519556 (* Write to user's opam-repo *)
520520- match Opam_repo.write_package ~fs ~repo_path:user_opam_repo ~name ~version ~content:new_content with
557557+ match
558558+ Opam_repo.write_package ~fs
559559+ ~repo_path:user_opam_repo ~name ~version
560560+ ~content:new_content
561561+ with
521562 | Error e -> Error (Opam_repo_error e)
522522- | Ok () -> Ok name)
563563+ | Ok () -> Ok name))
523564 related_pkgs
524565 in
525566 (* Check for errors *)
526567 match List.find_opt Result.is_error results with
527568 | Some (Error e) -> Error e
528569 | _ ->
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
570570+ let forked_names =
571571+ List.filter_map
572572+ (function Ok n -> Some n | Error _ -> None)
573573+ results
574574+ in
575575+ Ok
576576+ {
577577+ packages_forked = forked_names;
578578+ source_handle = handle;
579579+ fork_url;
580580+ upstream_url;
581581+ subtree_name;
582582+ }
583583+ end))
+7-4
lib/verse.mli
···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 (** Package not found in member's repo: (package, handle) *)
1616- | Package_already_exists of string list (** Packages already exist in user's opam repo *)
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 *)
1719 | Opam_repo_error of Opam_repo.error (** Error reading/writing opam files *)
18201921val pp_error : error Fmt.t
···149151150152(** {1 Forking} *)
151153152152-(** Result of a fork operation. *)
153154type fork_result = {
154155 packages_forked : string list; (** Package names that were forked *)
155156 source_handle : string; (** Handle of the verse member we forked from *)
156157 fork_url : string; (** URL of the fork *)
157158 upstream_url : string; (** Original dev-repo URL (upstream) *)
158158- subtree_name : string; (** Name for the subtree directory (derived from fork URL) *)
159159+ subtree_name : string;
160160+ (** Name for the subtree directory (derived from fork URL) *)
159161}
162162+(** Result of a fork operation. *)
160163161164val pp_fork_result : fork_result Fmt.t
162165(** [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.
44- All functionality has been unified into Config. *)
33+ This module is kept for backwards compatibility. All functionality has been
44+ unified into Config. *)
5566include Config
7788-(** Legacy type alias for package overrides *)
98type package_override = Config.Package_config.t
99+(** Legacy type alias for package overrides *)
+3-3
lib/verse_config.mli
···11(** Verse_config is now an alias for Config.
2233- This module is kept for backwards compatibility.
44- All functionality has been unified into Config.
33+ This module is kept for backwards compatibility. All functionality has been
44+ unified into Config.
5566 @deprecated Use {!Config} directly. *)
7788include module type of Config
991010+type package_override = Config.Package_config.t
1011(** Legacy type alias for package overrides.
1112 @deprecated Use {!Config.Package_config.t} instead. *)
1212-type package_override = Config.Package_config.t
+17-9
lib/verse_registry.ml
···66 opamrepo : string;
77 opamrepo_branch : string option;
88}
99+910type t = { name : string; description : string option; members : member list }
10111112let default_url = "https://tangled.org/eeg.cl.cam.ac.uk/opamverse"
···21222223(** Encode a URL with optional branch suffix. *)
2324let encode_url_with_branch url branch =
2424- match branch with
2525- | None -> url
2626- | Some b -> url ^ "#" ^ b
2525+ match branch with None -> url | Some b -> url ^ "#" ^ b
27262827let pp_member ppf m =
2928 let mono_str = encode_url_with_branch m.monorepo m.monorepo_branch in
3029 let opam_str = encode_url_with_branch m.opamrepo m.opamrepo_branch in
3130 let name_str = match m.name with Some n -> n | None -> m.handle in
3232- Fmt.pf ppf "@[<hov 2>%s (%s) ->@ mono:%s@ opam:%s@]" name_str m.handle mono_str opam_str
3131+ Fmt.pf ppf "@[<hov 2>%s (%s) ->@ mono:%s@ opam:%s@]" name_str m.handle
3232+ 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)) t.description
3636+ Fmt.(option (fun ppf s -> pf ppf "@,description: %s" s))
3737+ t.description
3738 Fmt.(list ~sep:cut pp_member)
3839 t.members
3940···5657 { handle; name; monorepo; monorepo_branch; opamrepo; opamrepo_branch })
5758 |> mem "handle" string ~enc:(fun (m : member) -> m.handle)
5859 |> opt_mem "name" string ~enc:(fun (m : member) -> m.name)
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)
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)
6164 |> finish))
62656366type registry_info = { r_name : string; r_description : string option }
···7477 Tomlt.(
7578 Table.(
7679 obj (fun registry members ->
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 })
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 })
7987 |> opt_mem "members" (list member_codec) ~enc:(fun t ->
8088 match t.members with [] -> None | ms -> Some ms)
8189 |> finish))
+6-4
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; (** Optional branch for monorepo (from URL#branch) *)
1212+ monorepo_branch : string option;
1313+ (** Optional branch for monorepo (from URL#branch) *)
1314 opamrepo : string; (** Git URL of the member's opam overlay repository *)
1414- opamrepo_branch : string option; (** Optional branch for opam repo (from URL#branch) *)
1515+ opamrepo_branch : string option;
1616+ (** Optional branch for opam repo (from URL#branch) *)
1517}
1618(** A registry member entry.
17191818- URLs may include a [#branch] suffix to specify a non-default branch.
1919- For example, ["https://github.com/user/repo#develop"]. *)
2020+ URLs may include a [#branch] suffix to specify a non-default branch. For
2121+ example, ["https://github.com/user/repo#develop"]. *)
20222123type t = {
2224 name : string; (** Registry name *)