···228228 in
229229 let run package remote skip_push skip_pull () =
230230 Eio_main.run @@ fun env ->
231231+ Eio.Switch.run @@ fun sw ->
231232 with_config env @@ fun config ->
232233 let fs = Eio.Stdenv.fs env in
233234 let proc = Eio.Stdenv.process_mgr env in
235235+ let xdg = Xdge.create fs "monopam" in
234236 match
235235- Monopam.sync ~proc ~fs ~config ?package ~remote ~skip_push ~skip_pull ()
237237+ Monopam.sync ~sw ~env ~proc ~fs ~config ~xdg ?package ~remote ~skip_push
238238+ ~skip_pull ()
236239 with
237240 | Ok summary ->
238241 if summary.errors = [] then `Ok ()
···10691072 in
10701073 let run package json no_sync quiet () =
10711074 Eio_main.run @@ fun env ->
10751075+ Eio.Switch.run @@ fun sw ->
10721076 with_config env @@ fun config ->
10731077 with_verse_config env @@ fun verse_config ->
10741078 let fs = Eio.Stdenv.fs env in
10751079 let proc = Eio.Stdenv.process_mgr env in
10761080 let clock = Eio.Stdenv.clock env in
10811081+ let xdg = Xdge.create fs "monopam" in
10771082 (* Run sync before analysis unless --no-sync is specified *)
10781083 if (not no_sync) && not quiet then begin
10791084 Fmt.pr "Syncing workspace before analysis...@.";
10801080- match Monopam.sync ~proc ~fs ~config ?package () with
10851085+ match Monopam.sync ~sw ~env ~proc ~fs ~config ~xdg ?package () with
10811086 | Ok _summary -> ()
10821087 | Error e ->
10831088 Fmt.pr "Warning: sync failed: %a@." Monopam.pp_error_with_hint e;
···10851090 end
10861091 else if not no_sync then begin
10871092 (* Quiet mode but still sync - just don't print progress *)
10881088- let _ = Monopam.sync ~proc ~fs ~config ?package () in
10931093+ let _ = Monopam.sync ~sw ~env ~proc ~fs ~config ~xdg ?package () in
10891094 ()
10901095 end;
10911096 let report =
+72-21
lib/fork_join.ml
···459459 let branch = Verse_config.default_branch in
460460461461 (* Gather discovery information *)
462462- let mono_exists = Git_cli.Subtree.exists ~fs ~repo:monorepo ~prefix in
462462+ let mono_exists = Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix in
463463 let src_exists = is_directory ~fs src_path in
464464 let has_subtree_hist =
465465 if mono_exists then
···612612 let src_path = Fpath.(checkouts / name) in
613613614614 (* Gather discovery information *)
615615- let subtree_exists = Git_cli.Subtree.exists ~fs ~repo:monorepo ~prefix in
615615+ let subtree_exists = Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix in
616616 let src_exists = is_directory ~fs src_path in
617617 let local_is_repo =
618618 if is_local then begin
···754754 let src_path = Fpath.(checkouts / name) in
755755756756 (* Gather discovery information *)
757757- let subtree_exists = Git_cli.Subtree.exists ~fs ~repo:monorepo ~prefix in
757757+ let subtree_exists = Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix in
758758 let src_exists = is_directory ~fs src_path in
759759 let src_is_repo =
760760 if src_exists then Git_cli.is_repo ~proc ~fs src_path else false
···828828 Git_cli.clone ~proc ~fs ~url:(Uri.of_string url) ~branch dest
829829 |> Result.map_error (fun e -> Git_error e)
830830 | Git_subtree_split { repo; prefix } ->
831831- Git_cli.Subtree.split ~proc ~fs ~repo ~prefix ()
832832- |> Result.map (fun commit -> state.split_commit <- Some commit)
833833- |> Result.map_error (fun e -> Git_error e)
831831+ let repo_path = Fpath.to_string repo in
832832+ let git_repo = Git.Repository.open_repo ~fs repo_path in
833833+ (match Git.Repository.read_ref git_repo "HEAD" with
834834+ | None -> Error (Git_error (Git_cli.Io_error "no HEAD ref found"))
835835+ | Some head -> (
836836+ match Git.Subtree.split git_repo ~prefix ~head () with
837837+ | Ok None ->
838838+ Error (Git_error (Git_cli.Subtree_prefix_missing prefix))
839839+ | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
840840+ | Ok (Some split_hash) ->
841841+ state.split_commit <- Some (Git.Hash.to_hex split_hash);
842842+ Ok ()))
834843 | Git_subtree_add { repo; prefix; url; branch } ->
835835- Git_cli.Subtree.add ~proc ~fs ~repo ~prefix ~url ~branch ()
836836- |> Result.map_error (fun e -> Git_error e)
844844+ (* Fetch the branch first to get the commit *)
845845+ (match Git_cli.fetch_url ~proc ~fs ~repo ~url ~branch () with
846846+ | Error e -> Error (Git_error e)
847847+ | Ok hash_hex ->
848848+ let repo_path = Fpath.to_string repo in
849849+ let git_repo = Git.Repository.open_repo ~fs repo_path in
850850+ let commit = Git.Hash.of_hex hash_hex in
851851+ let user =
852852+ Git.User.make ~name:"monopam" ~email:"monopam@localhost"
853853+ ~date:(Int64.of_float (Unix.time ())) ()
854854+ in
855855+ let message =
856856+ Fmt.str "Add '%s' from %s\n\ngit-subtree-dir: %s\n" prefix
857857+ (Uri.to_string url) prefix
858858+ in
859859+ (match
860860+ Git.Subtree.add git_repo ~prefix ~commit ~author:user
861861+ ~committer:user ~message ()
862862+ with
863863+ | Ok _ -> Ok ()
864864+ | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))))
837865 | Git_add_remote { repo; name; url } ->
838866 Git_cli.add_remote ~proc ~fs ~name ~url repo
839867 |> Result.map_error (fun e -> Git_error e)
···945973 let subtree_path = Fpath.(monorepo / prefix) in
946974 let src_path = Fpath.(checkouts / name) in
947975 (* Validate: mono/<name>/ must exist *)
948948- if not (Git_cli.Subtree.exists ~fs ~repo:monorepo ~prefix) then
976976+ if not (Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix) then
949977 Error (Subtree_not_found name) (* Validate: src/<name>/ must not exist *)
950978 else if is_directory ~fs src_path then Error (Src_already_exists name)
951979 else begin
···963991 }
964992 else begin
965993 (* Split the subtree to get history *)
966966- match Git_cli.Subtree.split ~proc ~fs ~repo:monorepo ~prefix () with
967967- | Error e -> Error (Git_error e)
968968- | Ok split_commit -> (
994994+ let repo_path = Fpath.to_string monorepo in
995995+ let git_repo = Git.Repository.open_repo ~fs repo_path in
996996+ match Git.Repository.read_ref git_repo "HEAD" with
997997+ | None -> Error (Git_error (Git_cli.Io_error "no HEAD ref found"))
998998+ | Some head -> (
999999+ match Git.Subtree.split git_repo ~prefix ~head () with
10001000+ | Ok None ->
10011001+ Error (Git_error (Git_cli.Subtree_prefix_missing prefix))
10021002+ | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
10031003+ | Ok (Some split_hash) ->
10041004+ let split_commit = Git.Hash.to_hex split_hash in
10051005+ (
9691006 (* Ensure src/ exists *)
9701007 ensure_dir ~fs checkouts;
9711008 (* Initialize new git repo at src/<name>/ *)
···10551092 src_path;
10561093 push_url;
10571094 packages_created = packages;
10581058- })))))
10951095+ }))))))
10591096 end
10601097 end
10611098···10671104 let subtree_path = Fpath.(monorepo / prefix) in
10681105 let src_path = Fpath.(checkouts / name) in
10691106 (* Validate: mono/<name>/ must not exist *)
10701070- if Git_cli.Subtree.exists ~fs ~repo:monorepo ~prefix then
11071107+ if Git_cli.subtree_prefix_exists ~fs ~repo:monorepo ~prefix then
10711108 Error (Subtree_already_exists name)
10721109 else if dry_run then
10731110 Ok
···10871124 match Git_cli.clone ~proc ~fs ~url:uri ~branch src_path with
10881125 | Error e -> Error (Git_error e)
10891126 | Ok () -> (
10901090- (* Add subtree to monorepo *)
10911091- match
10921092- Git_cli.Subtree.add ~proc ~fs ~repo:monorepo ~prefix ~url:uri ~branch
10931093- ()
10941094- with
11271127+ (* Add subtree to monorepo - first fetch to get the commit *)
11281128+ match Git_cli.fetch_url ~proc ~fs ~repo:monorepo ~url:uri ~branch () with
10951129 | Error e -> Error (Git_error e)
10961096- | Ok () ->
11301130+ | Ok hash_hex ->
11311131+ let repo_path = Fpath.to_string monorepo in
11321132+ let git_repo = Git.Repository.open_repo ~fs repo_path in
11331133+ let commit = Git.Hash.of_hex hash_hex in
11341134+ let user =
11351135+ Git.User.make ~name:"monopam" ~email:"monopam@localhost"
11361136+ ~date:(Int64.of_float (Unix.time ())) ()
11371137+ in
11381138+ let message =
11391139+ Fmt.str "Add '%s' from %s\n\ngit-subtree-dir: %s\n" prefix url
11401140+ prefix
11411141+ in
11421142+ (match
11431143+ Git.Subtree.add git_repo ~prefix ~commit ~author:user
11441144+ ~committer:user ~message ()
11451145+ with
11461146+ | Error (`Msg msg) -> Error (Git_error (Git_cli.Io_error msg))
11471147+ | Ok _ ->
10971148 (* Find .opam files in the new subtree *)
10981149 let packages = find_opam_files ~fs subtree_path in
10991150 (* Only update sources.toml if there's an upstream to track *)
···11371188 upstream_url = upstream;
11381189 packages_added = packages;
11391190 from_handle = None;
11401140- })
11911191+ }))
11411192 end
1142119311431194let join_from_verse ~proc ~fs ~config ~verse_config ~package ~handle ~fork_url
+15
lib/git_cli.ml
···667667let branch_rename ~proc ~fs ~new_name path =
668668 let cwd = path_to_eio ~fs path in
669669 run_git_ok ~proc ~cwd [ "branch"; "-M"; new_name ] |> Result.map ignore
670670+671671+let ls_remote_head ~proc ~fs ?(remote = "origin") ?(branch = "main") path =
672672+ let cwd = path_to_eio ~fs path in
673673+ match
674674+ run_git_ok ~proc ~cwd
675675+ [ "ls-remote"; "--heads"; remote; Printf.sprintf "refs/heads/%s" branch ]
676676+ with
677677+ | Error _ -> None
678678+ | Ok output ->
679679+ if String.trim output = "" then None
680680+ else
681681+ (* Output format: "hash\trefs/heads/branch" *)
682682+ match String.split_on_char '\t' (String.trim output) with
683683+ | hash :: _ -> Some hash
684684+ | [] -> None
+49-70
lib/git_cli.mli
···167167 @param remote Remote name (default: "origin")
168168 @param branch Branch to compare (default: current branch) *)
169169170170-(** {1 Subtree Operations} *)
171171-172172-(** Operations for git subtree management in the monorepo. *)
173173-module Subtree : sig
174174- val add :
175175- proc:_ Eio.Process.mgr ->
176176- fs:Eio.Fs.dir_ty Eio.Path.t ->
177177- repo:Fpath.t ->
178178- prefix:string ->
179179- url:Uri.t ->
180180- branch:string ->
181181- unit ->
182182- (unit, error) result
183183- (** [add ~proc ~fs ~repo ~prefix ~url ~branch ()] adds a new subtree to the
184184- repository.
185185-186186- @param repo Path to the monorepo
187187- @param prefix Subdirectory for the subtree
188188- @param url Git remote URL for the subtree source
189189- @param branch Branch to add *)
190190-191191- val pull :
192192- proc:_ Eio.Process.mgr ->
193193- fs:Eio.Fs.dir_ty Eio.Path.t ->
194194- repo:Fpath.t ->
195195- prefix:string ->
196196- url:Uri.t ->
197197- branch:string ->
198198- unit ->
199199- (unit, error) result
200200- (** [pull ~proc ~fs ~repo ~prefix ~url ~branch ()] pulls updates from the
201201- remote into the subtree.
202202-203203- @param repo Path to the monorepo
204204- @param prefix Subdirectory of the subtree
205205- @param url Git remote URL
206206- @param branch Branch to pull *)
207207-208208- val push :
209209- proc:_ Eio.Process.mgr ->
210210- fs:Eio.Fs.dir_ty Eio.Path.t ->
211211- repo:Fpath.t ->
212212- prefix:string ->
213213- url:Uri.t ->
214214- branch:string ->
215215- unit ->
216216- (unit, error) result
217217- (** [push ~proc ~fs ~repo ~prefix ~url ~branch ()] pushes subtree changes to
218218- the remote.
170170+(** {1 Subtree Helper Operations} *)
219171220220- This extracts commits that affected the subtree and pushes them to the
221221- specified remote/branch.
172172+val fetch_url :
173173+ proc:_ Eio.Process.mgr ->
174174+ fs:Eio.Fs.dir_ty Eio.Path.t ->
175175+ repo:Fpath.t ->
176176+ url:Uri.t ->
177177+ branch:string ->
178178+ unit ->
179179+ (string, error) result
180180+(** [fetch_url ~proc ~fs ~repo ~url ~branch ()] fetches a branch from a URL
181181+ and returns the commit hash of FETCH_HEAD.
222182223223- @param repo Path to the monorepo
224224- @param prefix Subdirectory of the subtree
225225- @param url Git remote URL
226226- @param branch Branch to push to *)
183183+ @param repo Path to the local repository
184184+ @param url Git remote URL to fetch from
185185+ @param branch Branch to fetch *)
227186228228- val split :
229229- proc:_ Eio.Process.mgr ->
230230- fs:Eio.Fs.dir_ty Eio.Path.t ->
231231- repo:Fpath.t ->
232232- prefix:string ->
233233- unit ->
234234- (string, error) result
235235- (** [split ~proc ~fs ~repo ~prefix ()] extracts commits for a subtree into a
236236- standalone branch.
187187+val push_refspec :
188188+ proc:_ Eio.Process.mgr ->
189189+ fs:Eio.Fs.dir_ty Eio.Path.t ->
190190+ repo:Fpath.t ->
191191+ url:Uri.t ->
192192+ refspec:string ->
193193+ unit ->
194194+ (unit, error) result
195195+(** [push_refspec ~proc ~fs ~repo ~url ~refspec ()] pushes a refspec to a URL.
237196238238- Returns the commit hash of the split branch head. *)
197197+ @param repo Path to the local repository
198198+ @param url Git remote URL to push to
199199+ @param refspec Git refspec (e.g. "hash:refs/heads/branch") *)
239200240240- val exists :
241241- fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> bool
242242- (** [exists ~fs ~repo ~prefix] returns true if the subtree prefix directory
243243- exists in the repository. *)
244244-end
201201+val subtree_prefix_exists :
202202+ fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> bool
203203+(** [subtree_prefix_exists ~fs ~repo ~prefix] returns true if the subtree
204204+ prefix directory exists in the repository. *)
245205246206(** {1 Initialization} *)
247207···656616 (unit, error) result
657617(** [branch_rename ~proc ~fs ~new_name path] renames the current branch to
658618 [new_name] in the repository at [path]. Uses [git branch -M]. *)
619619+620620+(** {1 Remote Queries} *)
621621+622622+val ls_remote_head :
623623+ proc:_ Eio.Process.mgr ->
624624+ fs:Eio.Fs.dir_ty Eio.Path.t ->
625625+ ?remote:string ->
626626+ ?branch:string ->
627627+ Fpath.t ->
628628+ string option
629629+(** [ls_remote_head ~proc ~fs ?remote ?branch path] queries the remote for the
630630+ HEAD ref of a branch without fetching any objects.
631631+632632+ This is much faster than [fetch] and can be used to check if there are any
633633+ new commits to fetch. Returns [None] if the branch doesn't exist or the
634634+ remote is unreachable.
635635+636636+ @param remote Remote name (default: "origin")
637637+ @param branch Branch name (default: "main") *)
+223-80
lib/monopam.ml
···21212222module Log = (val Logs.src_log src : Logs.LOG)
23232424+(* Timing helper for benchmarking phases *)
2525+let time_phase name f =
2626+ let t0 = Unix.gettimeofday () in
2727+ let result = f () in
2828+ let t1 = Unix.gettimeofday () in
2929+ Log.debug (fun m -> m "[TIMING] %s: %.3fs" name (t1 -. t0));
3030+ result
3131+2432type error =
2533 | Config_error of string
2634 | Repo_error of Opam_repo.error
···13841392 end
1385139313861394(* Fetch a single checkout - safe for parallel execution *)
13871387-let fetch_checkout_safe ~proc ~fs ~config pkg =
13951395+(** Remote HEAD cache with O(1) in-memory lookup and disk persistence.
13961396+ File format: one entry per line "url:branch hash timestamp" *)
13971397+module Remote_cache : sig
13981398+ type t
13991399+14001400+ val create : xdg:Xdge.t -> now:(unit -> float) -> t
14011401+ val get : t -> url:Uri.t -> branch:string -> string option
14021402+ val set : t -> url:Uri.t -> branch:string -> hash:string -> unit
14031403+end = struct
14041404+ type entry = { hash : string; expires : float }
14051405+ type t = { tbl : (string, entry) Hashtbl.t; xdg : Xdge.t; now : unit -> float }
14061406+14071407+ let ttl = 300.0 (* 5 minutes *)
14081408+ let filename = "remote-heads"
14091409+ let cache_file xdg = Eio.Path.(Xdge.cache_dir xdg / filename)
14101410+ let make_key url branch = Fmt.str "%s:%s" (Uri.to_string url) branch
14111411+14121412+ let parse_line line =
14131413+ match String.split_on_char ' ' line with
14141414+ | [ key; hash; ts ] -> Some (key, hash, float_of_string ts)
14151415+ | _ -> None
14161416+14171417+ let load_from_disk xdg =
14181418+ let tbl = Hashtbl.create 32 in
14191419+ (try
14201420+ Eio.Path.load (cache_file xdg)
14211421+ |> String.split_on_char '\n'
14221422+ |> List.iter (fun line ->
14231423+ match parse_line line with
14241424+ | Some (key, hash, ts) ->
14251425+ Hashtbl.replace tbl key { hash; expires = ts +. ttl }
14261426+ | None -> ())
14271427+ with _ -> ());
14281428+ tbl
14291429+14301430+ let save_to_disk t =
14311431+ let now = t.now () in
14321432+ let lines =
14331433+ Hashtbl.fold
14341434+ (fun key entry acc ->
14351435+ if entry.expires > now then
14361436+ let ts = entry.expires -. ttl in
14371437+ Fmt.str "%s %s %.0f" key entry.hash ts :: acc
14381438+ else acc)
14391439+ t.tbl []
14401440+ in
14411441+ let content = String.concat "\n" lines ^ "\n" in
14421442+ try Eio.Path.save ~create:(`Or_truncate 0o644) (cache_file t.xdg) content
14431443+ with _ -> ()
14441444+14451445+ let create ~xdg ~now =
14461446+ let tbl = load_from_disk xdg in
14471447+ { tbl; xdg; now }
14481448+14491449+ let get t ~url ~branch =
14501450+ let key = make_key url branch in
14511451+ let now = t.now () in
14521452+ match Hashtbl.find_opt t.tbl key with
14531453+ | Some entry when entry.expires > now ->
14541454+ Log.debug (fun m -> m "Cache hit for %s (expires in %.0fs)" key (entry.expires -. now));
14551455+ Some entry.hash
14561456+ | Some entry ->
14571457+ Log.debug (fun m -> m "Cache expired for %s (%.0fs ago)" key (now -. entry.expires));
14581458+ None
14591459+ | None ->
14601460+ Log.debug (fun m -> m "Cache miss for %s (not found)" key);
14611461+ None
14621462+14631463+ let set t ~url ~branch ~hash =
14641464+ let key = make_key url branch in
14651465+ let expires = t.now () +. ttl in
14661466+ Hashtbl.replace t.tbl key { hash; expires };
14671467+ save_to_disk t
14681468+end
14691469+14701470+let fetch_checkout_safe ~sw ~env ~proc ~fs ~config ~cache ~get_session pkg =
14711471+ let repo = Package.repo_name pkg in
13881472 let checkouts_root = Config.Paths.checkouts config in
13891473 let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
13901474 let branch = get_branch ~config pkg in
13911391- (* Get commits behind before fetching *)
13921392- let behind_before =
13931393- match Git_cli.ahead_behind ~proc ~fs ~branch checkout_dir with
13941394- | Ok ab -> ab.behind
13951395- | Error _ -> 0
14751475+ let remote_url = Package.dev_repo pkg in
14761476+ let local_head =
14771477+ Git_cli.rev_parse ~proc ~fs ~rev:(Fmt.str "origin/%s" branch) checkout_dir
14781478+ |> Result.to_option
13961479 in
13971397- Log.info (fun m -> m "Fetching %s (all remotes)" (Package.repo_name pkg));
13981398- match Git_cli.fetch_all ~proc ~fs checkout_dir with
13991399- | Error e -> Error e
14001400- | Ok () ->
14011401- (* Get commits behind after fetching *)
14021402- let behind_after =
14031403- match Git_cli.ahead_behind ~proc ~fs ~branch checkout_dir with
14041404- | Ok ab -> ab.behind
14051405- | Error _ -> 0
14801480+ (* Check if we can skip fetch entirely *)
14811481+ let remote_matches_local hash =
14821482+ match local_head with Some h -> hash = h | None -> false
14831483+ in
14841484+ (* Step 1: Try cached remote HEAD - O(1) hashtbl lookup *)
14851485+ match Remote_cache.get cache ~url:remote_url ~branch with
14861486+ | Some cached when remote_matches_local cached ->
14871487+ Log.debug (fun m -> m "Skipping fetch for %s (cached)" repo);
14881488+ Ok 0
14891489+ | _ -> (
14901490+ (* Step 2: Query remote HEAD via HTTP (lazily creates session) *)
14911491+ let remote =
14921492+ time_phase (Fmt.str "ls-remote:%s" repo) (fun () ->
14931493+ Git.Remote.get_remote_head ~session:(get_session ()) ~sw ~env remote_url ~branch)
14061494 in
14071407- Ok (behind_after - behind_before)
14951495+ Option.iter
14961496+ (fun h ->
14971497+ Remote_cache.set cache ~url:remote_url ~branch ~hash:(Git.Hash.to_hex h))
14981498+ remote;
14991499+ match remote with
15001500+ | Some h when remote_matches_local (Git.Hash.to_hex h) ->
15011501+ Log.debug (fun m -> m "Skipping fetch for %s (remote unchanged)" repo);
15021502+ Ok 0
15031503+ | _ ->
15041504+ (* Step 3: Do full git fetch *)
15051505+ let get_behind () =
15061506+ Git_cli.ahead_behind ~proc ~fs ~branch checkout_dir
15071507+ |> Result.map (fun (ab : Git_cli.ahead_behind) -> ab.behind)
15081508+ |> Result.value ~default:0
15091509+ in
15101510+ let behind_before = get_behind () in
15111511+ Log.info (fun m -> m "Fetching %s (all remotes)" repo);
15121512+ Git_cli.fetch_all ~proc ~fs checkout_dir
15131513+ |> Result.map (fun () -> get_behind () - behind_before))
1408151414091515(* Merge checkout to latest - must be sequential *)
14101516let merge_checkout_safe ~proc ~fs ~config pkg =
···16431749 Error (Git_error e)
16441750 end)))
1645175116461646-let sync ~proc ~fs ~config ?package ?(remote = false) ?(skip_push = false)
16471647- ?(skip_pull = false) () =
17521752+let sync ~sw ~env ~proc ~fs ~config ~xdg ?package ?(remote = false)
17531753+ ?(skip_push = false) ?(skip_pull = false) ?(skip_verse = false) () =
16481754 let fs_t = fs_typed fs in
17551755+ (* Create remote HEAD cache with O(1) lookup - loaded once, persisted on updates *)
17561756+ let cache = Remote_cache.create ~xdg ~now:(fun () -> Eio.Time.now env#clock) in
17571757+ (* Lazily create HTTP session to avoid TLS cert loading if cache hits *)
17581758+ let session_ref = ref None in
17591759+ let get_session () =
17601760+ match !session_ref with
17611761+ | Some s -> s
17621762+ | None ->
17631763+ let s = Requests.create ~sw env in
17641764+ session_ref := Some s;
17651765+ s
17661766+ in
1649176716501650- (* Step 0: Sync verse members if verse config exists and not skipping pull *)
16511651- (if not skip_pull then
17681768+ (* Step 0: Sync verse members if verse config exists and not skipping
17691769+ Skip verse sync when syncing a specific package for faster operations *)
17701770+ let should_skip_verse = skip_pull || skip_verse || Option.is_some package in
17711771+ (if not should_skip_verse then
16521772 match Verse_config.load ~fs:fs_t () with
16531773 | Error _ -> () (* No verse config = skip *)
16541774 | Ok verse_config -> (
16551775 Log.app (fun m -> m "Syncing verse members...");
16561656- match Verse.pull ~proc ~fs:fs_t ~config:verse_config () with
16571657- | Ok () -> ()
16581658- | Error e -> Log.warn (fun m -> m "Verse sync: %a" Verse.pp_error e)));
17761776+ time_phase "verse-sync" (fun () ->
17771777+ match Verse.pull ~proc ~fs:fs_t ~config:verse_config () with
17781778+ | Ok () -> ()
17791779+ | Error e ->
17801780+ Log.warn (fun m -> m "Verse sync: %a" Verse.pp_error e))));
1659178116601782 (* Clone from verse registry if repos don't exist *)
16611783 match clone_from_verse_if_needed ~proc ~fs:fs_t ~config () with
16621784 | Error e -> Error e
16631785 | Ok () -> (
16641664- (* Update the opam repo first - clone if needed *)
17861786+ (* Update the opam repo first - clone if needed
17871787+ Skip when syncing a single package for faster operations *)
16651788 let opam_repo = Config.Paths.opam_repo config in
16661666- if (not skip_pull) && Git_cli.is_repo ~proc ~fs:fs_t opam_repo then begin
17891789+ let skip_opam_repo = Option.is_some package in
17901790+ if (not skip_pull) && (not skip_opam_repo) && Git_cli.is_repo ~proc ~fs:fs_t opam_repo then begin
16671791 Log.info (fun m -> m "Updating opam repo at %a" Fpath.pp opam_repo);
16681668- let result =
16691669- let ( let* ) = Result.bind in
16701670- let* () = Git_cli.fetch ~proc ~fs:fs_t opam_repo in
16711671- Git_cli.merge_ff ~proc ~fs:fs_t opam_repo
16721672- in
16731673- match result with
16741674- | Ok () -> ()
16751675- | Error e ->
16761676- Log.warn (fun m ->
16771677- m "Failed to update opam repo: %a" Git_cli.pp_error e)
17921792+ time_phase "opam-repo-fetch" (fun () ->
17931793+ let result =
17941794+ let ( let* ) = Result.bind in
17951795+ let* () = Git_cli.fetch ~proc ~fs:fs_t opam_repo in
17961796+ Git_cli.merge_ff ~proc ~fs:fs_t opam_repo
17971797+ in
17981798+ match result with
17991799+ | Ok () -> ()
18001800+ | Error e ->
18011801+ Log.warn (fun m ->
18021802+ m "Failed to update opam repo: %a" Git_cli.pp_error e))
16781803 end;
16791804 (* Ensure directories exist *)
16801805 ensure_checkouts_dir ~fs:fs_t ~config;
16811806 match ensure_monorepo_initialized ~proc ~fs:fs_t ~config with
16821807 | Error e -> Error e
16831808 | Ok () ->
16841684- (* Check for uncommitted changes in monorepo *)
16851685- let monorepo = Config.Paths.monorepo config in
16861686- if Git_cli.is_dirty ~proc ~fs:fs_t monorepo then begin
16871687- Log.err (fun m -> m "Monorepo has uncommitted changes");
16881688- Error Monorepo_dirty
16891689- end
16901690- else begin
16911691- (* Regenerate opam-repo from monorepo to ensure URLs are up to date *)
16921692- regenerate_opam_repo ~fs:(fs_t :> _ Eio.Path.t) ~config ();
16931693- match discover_packages ~fs:(fs_t :> _ Eio.Path.t) ~config () with
16941694- | Error e -> Error e
16951695- | Ok all_pkgs ->
16961696- let pkgs =
16971697- match package with
16981698- | None -> all_pkgs
16991699- | Some name ->
17001700- List.filter (fun p -> Package.name p = name) all_pkgs
18091809+ (* Regenerate opam-repo from monorepo to ensure URLs are up to date *)
18101810+ (* Skip when syncing a single package for faster operations *)
18111811+ (if Option.is_none package then
18121812+ time_phase "regenerate-opam-repo" (fun () ->
18131813+ regenerate_opam_repo ~fs:(fs_t :> _ Eio.Path.t) ~config ()));
18141814+ (match
18151815+ time_phase "discover-packages" (fun () ->
18161816+ discover_packages ~fs:(fs_t :> _ Eio.Path.t) ~config ())
18171817+ with
18181818+ | Error e -> Error e
18191819+ | Ok all_pkgs ->
18201820+ let pkgs =
18211821+ match package with
18221822+ | None -> all_pkgs
18231823+ | Some name ->
18241824+ List.filter (fun p -> Package.name p = name) all_pkgs
18251825+ in
18261826+ if pkgs = [] && package <> None then
18271827+ Error (Package_not_found (Option.get package))
18281828+ else begin
18291829+ (* Step 1: Validate - check for dirty state in packages being synced *)
18301830+ Log.info (fun m ->
18311831+ m "Checking status of %d packages" (List.length pkgs));
18321832+ let statuses =
18331833+ time_phase "compute-status" (fun () ->
18341834+ Status.compute_all ~proc ~fs:fs_t ~config pkgs)
18351835+ in
18361836+ let dirty =
18371837+ List.filter Status.has_local_changes statuses
18381838+ |> List.map (fun s -> s.Status.package)
17011839 in
17021702- if pkgs = [] && package <> None then
17031703- Error (Package_not_found (Option.get package))
18401840+ if dirty <> [] then Error (Dirty_state dirty)
17041841 else begin
17051705- (* Step 1: Validate - check for dirty state *)
17061706- Log.info (fun m ->
17071707- m "Checking status of %d packages" (List.length pkgs));
17081708- let statuses =
17091709- Status.compute_all ~proc ~fs:fs_t ~config pkgs
17101710- in
17111711- let dirty =
17121712- List.filter Status.has_local_changes statuses
17131713- |> List.map (fun s -> s.Status.package)
17141714- in
17151715- if dirty <> [] then Error (Dirty_state dirty)
17161716- else begin
17171842 let repos = unique_repos pkgs in
17181843 let total = List.length repos in
17191844 Log.app (fun m -> m "Syncing %d repositories..." total);
···18181943 Log.app (fun m ->
18191944 m " Fetching from remotes (parallel)...");
18201945 let fetch_results =
19461946+ time_phase "fetch-phase" (fun () ->
18211947 Eio.Fiber.List.map ~max_fibers:4
18221948 (fun pkg ->
18231949 let repo_name = Package.repo_name pkg in
18241950 (* First ensure checkout exists *)
18251951 match
18261826- ensure_checkout_safe ~proc ~fs:fs_t ~config pkg
19521952+ time_phase (Printf.sprintf "ensure-checkout:%s" repo_name) (fun () ->
19531953+ ensure_checkout_safe ~proc ~fs:fs_t ~config pkg)
18271954 with
18281955 | Error e ->
18291956 Error { repo_name; phase = `Fetch; error = e }
···18311958 if was_cloned then Ok (repo_name, true, 0)
18321959 else
18331960 match
18341834- fetch_checkout_safe ~proc ~fs:fs_t ~config
18351835- pkg
19611961+ time_phase (Printf.sprintf "fetch:%s" repo_name) (fun () ->
19621962+ fetch_checkout_safe ~sw ~env ~proc ~fs:fs_t
19631963+ ~config ~cache ~get_session pkg)
18361964 with
18371965 | Error e ->
18381966 Error
···18431971 }
18441972 | Ok commits ->
18451973 Ok (repo_name, false, commits)))
18461846- repos
19741974+ repos)
18471975 in
18481976 let fetch_errs, fetch_successes =
18491977 List.partition_map
···18862014 (* Step 4: Merge phase - fast-forward merge checkouts (SEQUENTIAL) *)
18872015 Log.app (fun m -> m " Merging checkouts...");
18882016 let merge_errs = ref [] in
20172017+ time_phase "merge-phase" (fun () ->
18892018 List.iter
18902019 (fun pkg ->
18912020 match
18921892- merge_checkout_safe ~proc ~fs:fs_t ~config pkg
20212021+ time_phase (Printf.sprintf "merge:%s" (Package.repo_name pkg)) (fun () ->
20222022+ merge_checkout_safe ~proc ~fs:fs_t ~config pkg)
18932023 with
18942024 | Ok () -> ()
18952025 | Error e ->
···19002030 error = e;
19012031 }
19022032 :: !merge_errs)
19031903- successfully_fetched;
20332033+ successfully_fetched);
1904203419052035 (* Step 5: Subtree phase - pull subtrees into monorepo (SEQUENTIAL) *)
19062036 (* Check if monorepo has local modifications first *)
···19722102 in
1973210319742104 (* Step 5.5: Verse remotes - update and fetch from verse members *)
21052105+ (* Skip when syncing a single package for faster operations *)
19752106 (* Only operate on successfully fetched repos to avoid missing directory errors *)
19761976- (match Verse_config.load ~fs:(fs_t :> _ Eio.Path.t) () with
21072107+ (if Option.is_some package then
21082108+ Log.debug (fun m -> m "Skipping verse remotes (single package sync)")
21092109+ else
21102110+ match Verse_config.load ~fs:(fs_t :> _ Eio.Path.t) () with
19772111 | Error _ -> () (* No verse config, skip verse remotes *)
19782112 | Ok verse_config ->
21132113+ time_phase "sync-verse-remotes" (fun () ->
19792114 sync_verse_remotes ~proc ~fs:fs_t ~config ~verse_config
19801980- successfully_fetched_repos;
21152115+ successfully_fetched_repos);
19812116 (* Fetch from verse remotes in parallel *)
19822117 Log.app (fun m -> m " Fetching from verse remotes...");
21182118+ time_phase "fetch-verse-remotes" (fun () ->
19832119 Eio.Fiber.List.iter ~max_fibers:4
19842120 (fun pkg ->
19852121 fetch_verse_remotes ~proc ~fs:fs_t ~config pkg)
19861986- successfully_fetched_repos);
21222122+ successfully_fetched_repos));
1987212319882124 (* Step 6: Finalize - write README.md, CLAUDE.md, and dune-project (SEQUENTIAL) *)
21252125+ (* Skip when syncing a single package for faster operations *)
21262126+ (if Option.is_some package then
21272127+ Log.debug (fun m -> m "Skipping finalize (single package sync)")
21282128+ else begin
19892129 Log.app (fun m ->
19902130 m " Writing README.md, CLAUDE.md, and dune-project...");
19911991- write_readme ~proc ~fs:fs_t ~config all_pkgs;
19921992- write_claude_md ~proc ~fs:fs_t ~config;
19931993- write_dune_project ~proc ~fs:fs_t ~config all_pkgs;
21312131+ time_phase "write-readme" (fun () ->
21322132+ write_readme ~proc ~fs:fs_t ~config all_pkgs);
21332133+ time_phase "write-claude-md" (fun () ->
21342134+ write_claude_md ~proc ~fs:fs_t ~config);
21352135+ time_phase "write-dune-project" (fun () ->
21362136+ write_dune_project ~proc ~fs:fs_t ~config all_pkgs)
21372137+ end);
1994213819952139 (* Step 7: Remote phase - push to upstream remotes if --remote (LIMITED PARALLEL) *)
19962140 (* Only push repos that were successfully fetched *)
···2059220320602204 Ok summary
20612205 end
20622062- end
20632063- end)
22062206+ end))
2064220720652208(* Opam metadata sync: copy .opam files from monorepo subtrees to opam-repo *)
20662209
+8-1
lib/monopam.mli
···166166(** [pp_sync_summary] formats a sync summary. *)
167167168168val sync :
169169+ sw:Eio.Switch.t ->
170170+ env:< clock : _ Eio.Time.clock
171171+ ; net : _ Eio.Net.t
172172+ ; fs : Eio.Fs.dir_ty Eio.Path.t
173173+ ; .. > ->
169174 proc:_ Eio.Process.mgr ->
170175 fs:Eio.Fs.dir_ty Eio.Path.t ->
171176 config:Config.t ->
177177+ xdg:Xdge.t ->
172178 ?package:string ->
173179 ?remote:bool ->
174180 ?skip_push:bool ->
175181 ?skip_pull:bool ->
182182+ ?skip_verse:bool ->
176183 unit ->
177184 (sync_summary, error) result
178178-(** [sync ~proc ~fs ~config ?package ?remote ?skip_push ?skip_pull ()]
185185+(** [sync ~sw ~env ~proc ~fs ~config ~xdg ?package ?remote ?skip_push ?skip_pull ?skip_verse ()]
179186 synchronizes the monorepo with upstream repositories.
180187181188 This is the primary command for all sync operations. It performs both push
+1-1
lib/status.ml
···5050 | _ -> Missing
5151 in
5252 let subtree =
5353- if Git_cli.Subtree.exists ~fs:fs_t ~repo:monorepo ~prefix then Present
5353+ if Git_cli.subtree_prefix_exists ~fs:fs_t ~repo:monorepo ~prefix then Present
5454 else Not_added
5555 in
5656 (* Compute subtree sync state: compare tree content between monorepo subtree and checkout.