Monorepo management for opam overlays
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add opam metadata sync command

Implements `monopam opam sync` to synchronize .opam files from monorepo
subtrees to the opam-repo overlay. Local metadata always wins over
opam-repo metadata.

Features:
- sync_opam_files function in monopam.ml
- opam_sync_result type tracking synced/unchanged/missing packages
- CLI command under `monopam opam sync [PACKAGE]`
- Auto-stages and commits changes in opam-repo

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+203 -1
+61 -1
bin/main.ml
··· 309 309 (const run $ package_arg $ daily $ weeks $ days $ history $ dry_run 310 310 $ no_aggregate $ logging_term)) 311 311 312 + (* Opam commands *) 313 + 314 + let opam_sync_cmd = 315 + let doc = "Sync opam files from monorepo to opam-repo" in 316 + let man = 317 + [ 318 + `S Manpage.s_description; 319 + `P 320 + "Copies .opam files from monorepo subtrees to the opam-repo overlay. \ 321 + This ensures your opam overlay reflects any changes you made to \ 322 + .opam files in the monorepo."; 323 + `S "HOW IT WORKS"; 324 + `P "For each package in your opam overlay:"; 325 + `I ("1.", "Reads the .opam file from the monorepo subtree (e.g., mono/eio/eio.opam)"); 326 + `I ("2.", "Compares with the opam-repo version (e.g., opam-repo/packages/eio/eio.dev/opam)"); 327 + `I ("3.", "If different, copies monorepo → opam-repo"); 328 + `I ("4.", "Stages and commits changes in opam-repo"); 329 + `S "PRECEDENCE"; 330 + `P "Local always wins: the monorepo version is the source of truth."; 331 + `S Manpage.s_examples; 332 + `P "Sync all packages:"; 333 + `Pre "monopam opam sync"; 334 + `P "Sync a specific package:"; 335 + `Pre "monopam opam sync eio"; 336 + ] 337 + in 338 + let info = Cmd.info "sync" ~doc ~man in 339 + let run package () = 340 + Eio_main.run @@ fun env -> 341 + with_config env @@ fun config -> 342 + let fs = Eio.Stdenv.fs env in 343 + let proc = Eio.Stdenv.process_mgr env in 344 + match Monopam.sync_opam_files ~proc ~fs ~config ?package () with 345 + | Ok result -> 346 + if result.synced = [] then 347 + Fmt.pr "All opam files already in sync.@." 348 + else 349 + Fmt.pr "Synced %d opam files.@." (List.length result.synced); 350 + `Ok () 351 + | Error e -> 352 + Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 353 + `Error (false, "opam sync failed") 354 + in 355 + Cmd.v info Term.(ret (const run $ package_arg $ logging_term)) 356 + 357 + let opam_cmd = 358 + let doc = "Opam metadata management" in 359 + let man = 360 + [ 361 + `S Manpage.s_description; 362 + `P 363 + "Commands for managing opam metadata between your monorepo and the \ 364 + opam-repo overlay."; 365 + `S "COMMANDS"; 366 + `I ("sync", "Sync .opam files from monorepo subtrees to opam-repo"); 367 + ] 368 + in 369 + let info = Cmd.info "opam" ~doc ~man in 370 + Cmd.group info [ opam_sync_cmd ] 371 + 312 372 (* Verse commands *) 313 373 314 374 (* Helper to load verse config from XDG *) ··· 752 812 in 753 813 let info = Cmd.info "monopam" ~version:"%%VERSION%%" ~doc ~man in 754 814 Cmd.group info 755 - [ status_cmd; sync_cmd; changes_cmd; verse_cmd ] 815 + [ status_cmd; sync_cmd; changes_cmd; opam_cmd; verse_cmd ] 756 816 757 817 let () = exit (Cmd.eval main_cmd)
+106
lib/monopam.ml
··· 1439 1439 end 1440 1440 end) 1441 1441 1442 + (* Opam metadata sync: copy .opam files from monorepo subtrees to opam-repo *) 1443 + 1444 + type opam_sync_result = { 1445 + synced : string list; (* packages that were updated *) 1446 + unchanged : string list; (* packages that were already in sync *) 1447 + missing : string list; (* packages where monorepo has no .opam file *) 1448 + } 1449 + 1450 + let pp_opam_sync_result ppf r = 1451 + Fmt.pf ppf "Synced: %d, Unchanged: %d, Missing: %d" 1452 + (List.length r.synced) (List.length r.unchanged) (List.length r.missing) 1453 + 1454 + (* Read file contents safely, returning None if file doesn't exist *) 1455 + let read_file_opt path = 1456 + try Some (Eio.Path.load path) 1457 + with Eio.Io _ -> None 1458 + 1459 + (* Sync a single package's opam file from monorepo to opam-repo *) 1460 + let sync_opam_file ~proc ~fs ~config pkg = 1461 + let monorepo = Config.Paths.monorepo config in 1462 + let opam_repo = Config.Paths.opam_repo config in 1463 + let name = Package.name pkg in 1464 + let subtree_prefix = Package.subtree_prefix pkg in 1465 + let version = Package.version pkg in 1466 + 1467 + (* Source: monorepo/<subtree>/<name>.opam *) 1468 + let src_path = Eio.Path.(fs / Fpath.to_string monorepo / subtree_prefix / (name ^ ".opam")) in 1469 + 1470 + (* Destination: opam-repo/packages/<name>/<name>.<version>/opam *) 1471 + let pkg_dir = Fpath.(opam_repo / "packages" / name / (name ^ "." ^ version)) in 1472 + let dst_path = Eio.Path.(fs / Fpath.to_string pkg_dir / "opam") in 1473 + 1474 + match read_file_opt src_path with 1475 + | None -> 1476 + (* No opam file in monorepo subtree *) 1477 + `Missing name 1478 + | Some src_content -> 1479 + let dst_content = read_file_opt dst_path in 1480 + if Some src_content = dst_content then 1481 + `Unchanged name 1482 + else begin 1483 + (* Create destination directory if needed *) 1484 + let pkg_dir_eio = Eio.Path.(fs / Fpath.to_string pkg_dir) in 1485 + (try mkdirs pkg_dir_eio with _ -> ()); 1486 + (* Write the opam file *) 1487 + Log.info (fun m -> m "Syncing %s.opam to opam-repo" name); 1488 + Eio.Path.save ~create:(`Or_truncate 0o644) dst_path src_content; 1489 + (* Stage the change *) 1490 + let opam_repo_eio = Eio.Path.(fs / Fpath.to_string opam_repo) in 1491 + let rel_path = Printf.sprintf "packages/%s/%s.%s/opam" name name version in 1492 + Eio.Switch.run (fun sw -> 1493 + let child = 1494 + Eio.Process.spawn proc ~sw ~cwd:opam_repo_eio 1495 + [ "git"; "add"; rel_path ] 1496 + in 1497 + ignore (Eio.Process.await child)); 1498 + `Synced name 1499 + end 1500 + 1501 + (* Sync opam files for all packages *) 1502 + let sync_opam_files ~proc ~fs ~config ?package () = 1503 + let fs = fs_typed fs in 1504 + match discover_packages ~fs:(fs :> _ Eio.Path.t) ~config () with 1505 + | Error e -> Error e 1506 + | Ok all_pkgs -> 1507 + let pkgs = 1508 + match package with 1509 + | None -> all_pkgs 1510 + | Some name -> List.filter (fun p -> Package.name p = name) all_pkgs 1511 + in 1512 + if pkgs = [] && package <> None then 1513 + Error (Package_not_found (Option.get package)) 1514 + else begin 1515 + Log.app (fun m -> m "Syncing opam files for %d packages..." (List.length pkgs)); 1516 + let synced = ref [] in 1517 + let unchanged = ref [] in 1518 + let missing = ref [] in 1519 + List.iter (fun pkg -> 1520 + match sync_opam_file ~proc ~fs ~config pkg with 1521 + | `Synced name -> synced := name :: !synced 1522 + | `Unchanged name -> unchanged := name :: !unchanged 1523 + | `Missing name -> missing := name :: !missing) 1524 + pkgs; 1525 + let result = { 1526 + synced = List.rev !synced; 1527 + unchanged = List.rev !unchanged; 1528 + missing = List.rev !missing; 1529 + } in 1530 + (* Commit if there were changes *) 1531 + if result.synced <> [] then begin 1532 + let opam_repo = Config.Paths.opam_repo config in 1533 + let opam_repo_eio = Eio.Path.(fs / Fpath.to_string opam_repo) in 1534 + let msg = Printf.sprintf "Sync opam files from monorepo (%d packages)" 1535 + (List.length result.synced) in 1536 + Eio.Switch.run (fun sw -> 1537 + let child = 1538 + Eio.Process.spawn proc ~sw ~cwd:opam_repo_eio 1539 + [ "git"; "commit"; "-m"; msg ] 1540 + in 1541 + ignore (Eio.Process.await child)); 1542 + Log.app (fun m -> m "Committed opam sync: %s" msg) 1543 + end; 1544 + Log.app (fun m -> m "%a" pp_opam_sync_result result); 1545 + Ok result 1546 + end 1547 + 1442 1548 let add ~proc ~fs ~config ~package () = 1443 1549 let fs_t = fs_typed fs in 1444 1550 ensure_checkouts_dir ~fs:fs_t ~config;
+36
lib/monopam.mli
··· 187 187 @param skip_push If true, skip pushing monorepo changes to checkouts 188 188 @param skip_pull If true, skip fetching and pulling from remotes *) 189 189 190 + (** {2 Opam Metadata Sync} *) 191 + 192 + (** Result of syncing opam files from monorepo to opam-repo. *) 193 + type opam_sync_result = { 194 + synced : string list; (** Packages that were updated *) 195 + unchanged : string list; (** Packages that were already in sync *) 196 + missing : string list; (** Packages where monorepo has no .opam file *) 197 + } 198 + 199 + val pp_opam_sync_result : opam_sync_result Fmt.t 200 + (** [pp_opam_sync_result] formats an opam sync result. *) 201 + 202 + val sync_opam_files : 203 + proc:_ Eio.Process.mgr -> 204 + fs:Eio.Fs.dir_ty Eio.Path.t -> 205 + config:Config.t -> 206 + ?package:string -> 207 + unit -> 208 + (opam_sync_result, error) result 209 + (** [sync_opam_files ~proc ~fs ~config ?package ()] synchronizes .opam files 210 + from monorepo subtrees to the opam-repo overlay. 211 + 212 + For each package (or the specified package): 213 + 1. Reads the .opam file from the monorepo subtree 214 + 2. Compares with the opam-repo version 215 + 3. If different, copies monorepo → opam-repo (local always wins) 216 + 4. Stages and commits changes in opam-repo 217 + 218 + This ensures that the opam overlay reflects any changes made to .opam 219 + files in the monorepo. 220 + 221 + @param proc Eio process manager 222 + @param fs Eio filesystem 223 + @param config Monopam configuration 224 + @param package Optional specific package to sync *) 225 + 190 226 (** {2 Package Management} *) 191 227 192 228 val add :