Monorepo management for opam overlays
0
fork

Configure Feed

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

monopam: improve split output and make pull/push/split consistent

- split now shows progress bar and lists synced packages
- pull, push, split all accept multiple package names
- Commits use global git config (user.name/email) instead of hardcoded values
- Remove duplicate output in split command
- Add packages_arg for consistent multi-package CLI interface

+143 -79
+4 -4
bin/cmd_pull.ml
··· 27 27 `P "Pull all upstream changes:"; 28 28 `Pre "monopam pull"; 29 29 `P "Pull changes for a specific package:"; 30 - `Pre "monopam pull eio"; 30 + `Pre "monopam pull eio cohttp"; 31 31 ] 32 32 in 33 33 let info = Cmd.info "pull" ~doc ~man in 34 - let run package () = 34 + let run packages () = 35 35 let t0 = Unix.gettimeofday () in 36 36 Eio_main.run @@ fun env -> 37 37 Common.with_config env @@ fun config -> 38 38 let fs = Eio.Stdenv.fs env in 39 39 let proc = Eio.Stdenv.process_mgr env in 40 - match Monopam.pull ~proc ~fs ~config ?package () with 40 + match Monopam.pull ~proc ~fs ~config ~packages () with 41 41 | Ok () -> 42 42 let elapsed = Unix.gettimeofday () -. t0 in 43 43 Fmt.pr "@.%a Monorepo updated in %a.@." Tty.Span.pp ··· 51 51 Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 52 52 `Error (false, "pull failed") 53 53 in 54 - Cmd.v info Term.(ret (const run $ Common.package_arg $ Common.logging_term)) 54 + Cmd.v info Term.(ret (const run $ Common.packages_arg $ Common.logging_term))
+3 -3
bin/cmd_push.ml
··· 57 57 let doc = "Force push to upstream. Required when using --clean." in 58 58 Arg.(value & flag & info [ "force" ] ~doc) 59 59 in 60 - let run package local_only clean force () = 60 + let run packages local_only clean force () = 61 61 let t0 = Unix.gettimeofday () in 62 62 Eio_main.run @@ fun env -> 63 63 Common.with_config env @@ fun config -> 64 64 let fs = Eio.Stdenv.fs env in 65 65 let proc = Eio.Stdenv.process_mgr env in 66 66 match 67 - Monopam.push ~proc ~fs ~config ?package ~upstream:(not local_only) ~clean 67 + Monopam.push ~proc ~fs ~config ~packages ~upstream:(not local_only) ~clean 68 68 ~force () 69 69 with 70 70 | Ok () -> ··· 91 91 Cmd.v info 92 92 Term.( 93 93 ret 94 - (const run $ Common.package_arg $ local_arg $ clean_arg $ force_arg 94 + (const run $ Common.packages_arg $ local_arg $ clean_arg $ force_arg 95 95 $ Common.logging_term))
+6 -6
bin/cmd_split.ml
··· 14 14 ( "2.", 15 15 "Transforms opam files for opam-repo (adds dev-repo, url stanzas)" ); 16 16 `I ("3.", "Writes to opam-repo/packages/<name>/<name>.dev/opam"); 17 - `I ("4.", "Removes orphaned packages not in monorepo"); 17 + `I ("4.", "Removes orphaned packages not in monorepo (when syncing all)"); 18 18 `I ("5.", "Stages and commits changes in opam-repo"); 19 19 `S "EXAMPLE"; 20 - `Pre "monopam split # Sync all packages"; 21 - `Pre "monopam split aos # Sync only aos package"; 20 + `Pre "monopam split # Sync all packages"; 21 + `Pre "monopam split aos clcw # Sync specific packages"; 22 22 `S Manpage.s_see_also; 23 23 `P "$(b,monopam push)(1), $(b,monopam status)(1)"; 24 24 ] 25 25 in 26 26 let info = Cmd.info "split" ~doc ~man in 27 - let run package () = 27 + let run packages () = 28 28 Eio_main.run @@ fun env -> 29 29 Common.with_config env @@ fun config -> 30 30 let fs = Eio.Stdenv.fs env in 31 - match Monopam.sync_opam_files ~fs ~config ?package () with 31 + match Monopam.sync_opam_files ~fs ~config ~packages () with 32 32 | Ok result -> 33 33 Fmt.pr "%a@." Monopam.pp_opam_sync_result result; 34 34 `Ok () ··· 36 36 Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 37 37 `Error (false, "split failed") 38 38 in 39 - Cmd.v info Term.(ret (const run $ Common.package_arg $ Common.logging_term)) 39 + Cmd.v info Term.(ret (const run $ Common.packages_arg $ Common.logging_term))
+4
bin/common.ml
··· 16 16 let doc = "Package name. If not specified, operates on all packages." in 17 17 Arg.(value & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc) 18 18 19 + let packages_arg = 20 + let doc = "Package names. If not specified, operates on all packages." in 21 + Arg.(value & pos_all string [] & info [] ~docv:"PACKAGE" ~doc) 22 + 19 23 (* Load config from opamverse.toml *) 20 24 let load_config env = 21 25 let fs = Eio.Stdenv.fs env in
+12 -6
lib/fork_join.ml
··· 863 863 let git_repo = Git.Repository.open_repo ~fs repo in 864 864 let commit = Git.Hash.of_hex hash_hex in 865 865 let user = 866 - Git.User.v ~name:"monopam" ~email:"monopam@localhost" 867 - ~date:(Int64.of_float (Unix.time ())) 868 - () 866 + match Git_cli.global_git_user () with 867 + | Some u -> u 868 + | None -> 869 + Git.User.v ~name:"monopam" ~email:"monopam@localhost" 870 + ~date:(Int64.of_float (Unix.time ())) 871 + () 869 872 in 870 873 let message = 871 874 Fmt.str "Add '%s' from %s\n\ngit-subtree-dir: %s\n" prefix ··· 1153 1156 let git_repo = Git.Repository.open_repo ~fs monorepo in 1154 1157 let commit = Git.Hash.of_hex hash_hex in 1155 1158 let user = 1156 - Git.User.v ~name:"monopam" ~email:"monopam@localhost" 1157 - ~date:(Int64.of_float (Unix.time ())) 1158 - () 1159 + match Git_cli.global_git_user () with 1160 + | Some u -> u 1161 + | None -> 1162 + Git.User.v ~name:"monopam" ~email:"monopam@localhost" 1163 + ~date:(Int64.of_float (Unix.time ())) 1164 + () 1159 1165 in 1160 1166 let message = 1161 1167 Fmt.str "Add '%s' from %s\n\ngit-subtree-dir: %s\n" prefix url
+16
lib/git_cli.ml
··· 1 1 let src = Logs.Src.create "monopam.git_cli" ~doc:"Git CLI operations" 2 2 3 + (** Read user info from global git config. *) 4 + let global_git_user () = 5 + let read_config key = 6 + let ic = Unix.open_process_in (Printf.sprintf "git config %s" key) in 7 + let value = 8 + try Some (String.trim (input_line ic)) with End_of_file -> None 9 + in 10 + ignore (Unix.close_process_in ic); 11 + value 12 + in 13 + match (read_config "user.name", read_config "user.email") with 14 + | Some name, Some email -> 15 + let date = Int64.of_float (Unix.gettimeofday ()) in 16 + Some (Git.User.v ~name ~email ~date ()) 17 + | _ -> None 18 + 3 19 type cmd_result = { exit_code : int; stdout : string; stderr : string } 4 20 5 21 type error =
+6
lib/git_cli.mli
··· 4 4 and git subtree operations in the monorepo. All operations use Eio for 5 5 process spawning. *) 6 6 7 + (** {1 User configuration} *) 8 + 9 + val global_git_user : unit -> Git.User.t option 10 + (** [global_git_user ()] reads user.name and user.email from global git config. 11 + Returns [None] if either is not configured. *) 12 + 7 13 (** {1 Types} *) 8 14 9 15 type cmd_result = { exit_code : int; stdout : string; stderr : string }
+29 -21
lib/monopam.ml
··· 537 537 |> Result.map_error (fun (`Msg msg) -> Git_error (Git_cli.Io_error msg))) 538 538 (fun () -> 539 539 let user = 540 - Git.User.v ~name:"monopam" ~email:"monopam@localhost" 541 - ~date:(Int64.of_float (Unix.time ())) 542 - () 540 + match Git_cli.global_git_user () with 541 + | Some u -> u 542 + | None -> 543 + Git.User.v ~name:"monopam" ~email:"monopam@localhost" 544 + ~date:(Int64.of_float (Unix.time ())) 545 + () 543 546 in 544 547 Git.Repository.commit_index repo ~author:user ~committer:user 545 548 ~message:"Initial commit with dune-project, CLAUDE.md, and .gitignore" ··· 762 765 let git_repo = Git.Repository.open_repo ~fs monorepo in 763 766 let commit = Git.Hash.of_hex hash_hex in 764 767 let user = 765 - Git.User.v ~name:"monopam" ~email:"monopam@localhost" 766 - ~date:(Int64.of_float (Unix.time ())) 767 - () 768 + match Git_cli.global_git_user () with 769 + | Some u -> u 770 + | None -> 771 + Git.User.v ~name:"monopam" ~email:"monopam@localhost" 772 + ~date:(Int64.of_float (Unix.time ())) 773 + () 768 774 in 769 775 if subtree_exists then begin 770 776 Log.info (fun m -> ··· 823 829 | Some ab -> ab.behind 824 830 | None -> 0 825 831 826 - let pull ~proc ~fs ~config ?package ?opam_repo_url () = 832 + let pull ~proc ~fs ~config ?(packages = []) ?opam_repo_url () = 827 833 let fs_t = fs_typed fs in 828 834 (* Update the opam repo first - clone if needed *) 829 835 let opam_repo = Config.Paths.opam_repo config in ··· 867 873 | Error e -> Error e 868 874 | Ok all_pkgs -> 869 875 let pkgs = 870 - match package with 871 - | None -> all_pkgs 872 - | Some name -> List.filter (fun p -> Package.name p = name) all_pkgs 876 + match packages with 877 + | [] -> all_pkgs 878 + | names -> 879 + List.filter (fun p -> List.mem (Package.name p) names) all_pkgs 873 880 in 874 - if pkgs = [] && package <> None then 875 - Error (Package_not_found (Option.get package)) 881 + if pkgs = [] && packages <> [] then 882 + Error (Package_not_found (List.hd packages)) 876 883 else begin 877 884 Log.info (fun m -> 878 885 m "Checking status of %d packages" (List.length pkgs)); ··· 1074 1081 | Error e -> Error (Git_error e))) 1075 1082 end 1076 1083 1077 - let rec push ~proc ~fs ~config ?package ?(upstream = false) ?(clean = false) 1078 - ?(force = false) () = 1084 + let rec push ~proc ~fs ~config ?(packages = []) ?(upstream = false) 1085 + ?(clean = false) ?(force = false) () = 1079 1086 let fs_t = fs_typed fs in 1080 1087 (* Ensure checkouts directory exists before computing status *) 1081 1088 ensure_checkouts_dir ~fs:fs_t ~config; ··· 1083 1090 | Error e -> Error e 1084 1091 | Ok all_pkgs -> 1085 1092 let pkgs = 1086 - match package with 1087 - | None -> all_pkgs 1088 - | Some name -> List.filter (fun p -> Package.name p = name) all_pkgs 1093 + match packages with 1094 + | [] -> all_pkgs 1095 + | names -> 1096 + List.filter (fun p -> List.mem (Package.name p) names) all_pkgs 1089 1097 in 1090 - if pkgs = [] && package <> None then 1091 - Error (Package_not_found (Option.get package)) 1098 + if pkgs = [] && packages <> [] then 1099 + Error (Package_not_found (List.hd packages)) 1092 1100 else begin 1093 1101 Log.info (fun m -> 1094 1102 m "Checking status of %d packages" (List.length pkgs)); ··· 1367 1375 (* Thin wrappers to extracted modules *) 1368 1376 let pp_opam_sync_result = Opam_sync.pp 1369 1377 1370 - let sync_opam_files ~fs ~config ?package () = 1371 - match Opam_sync.run ~fs:(fs_typed fs) ~config ?package () with 1378 + let sync_opam_files ~fs ~config ?(packages = []) () = 1379 + match Opam_sync.run ~fs:(fs_typed fs) ~config ~packages () with 1372 1380 | Ok result -> Ok result 1373 1381 | Error (`Config_error e) -> Error (Config_error e) 1374 1382
+14 -15
lib/monopam.mli
··· 87 87 proc:_ Eio.Process.mgr -> 88 88 fs:Eio.Fs.dir_ty Eio.Path.t -> 89 89 config:Config.t -> 90 - ?package:string -> 90 + ?packages:string list -> 91 91 ?opam_repo_url:string -> 92 92 unit -> 93 93 (unit, error) result 94 - (** [pull ~proc ~fs ~config ?package ?opam_repo_url ()] pulls updates from 94 + (** [pull ~proc ~fs ~config ?packages ?opam_repo_url ()] pulls updates from 95 95 remotes. 96 96 97 - For each package (or the specified package): 1. Clones or fetches the 97 + For each package (or the specified packages): 1. Clones or fetches the 98 98 individual checkout 2. Adds or pulls the subtree in the monorepo 99 99 100 100 If the opam-repo doesn't exist locally and [opam_repo_url] is provided, ··· 115 115 proc:_ Eio.Process.mgr -> 116 116 fs:Eio.Fs.dir_ty Eio.Path.t -> 117 117 config:Config.t -> 118 - ?package:string -> 118 + ?packages:string list -> 119 119 ?upstream:bool -> 120 120 ?clean:bool -> 121 121 ?force:bool -> 122 122 unit -> 123 123 (unit, error) result 124 - (** [push ~proc ~fs ~config ?package ?upstream ?clean ?force ()] pushes changes 124 + (** [push ~proc ~fs ~config ?packages ?upstream ?clean ?force ()] pushes changes 125 125 from monorepo to checkouts. 126 126 127 - For each package (or the specified package) with changes in the monorepo: 1. 128 - Splits the subtree commits 2. If [~clean:true], removes empty commits from 129 - unrelated subtree merges 3. Pushes to the individual checkout 4. If 127 + For each package (or the specified packages) with changes in the monorepo: 128 + 1. Splits the subtree commits 2. If [~clean:true], removes empty commits 129 + from unrelated subtree merges 3. Pushes to the individual checkout 4. If 130 130 [~upstream:true], also pushes each checkout to its git remote 131 131 132 132 If [~upstream] is false (the default), the user must manually push from ··· 137 137 @param proc Eio process manager 138 138 @param fs Eio filesystem 139 139 @param config Monopam configuration 140 - @param package Optional specific package to push 140 + @param packages Optional list of packages to push (empty = all) 141 141 @param upstream If true, also push checkouts to their git remotes 142 142 @param clean If true, clean history by removing unrelated merge commits 143 143 @param force If true, force push to upstream (use with --clean) *) ··· 175 175 val sync_opam_files : 176 176 fs:Eio.Fs.dir_ty Eio.Path.t -> 177 177 config:Config.t -> 178 - ?package:string -> 178 + ?packages:string list -> 179 179 unit -> 180 180 (opam_sync_result, error) result 181 - (** [sync_opam_files ~fs ~config ?package ()] generates opam-repo entries from 181 + (** [sync_opam_files ~fs ~config ?packages ()] generates opam-repo entries from 182 182 monorepo dune-project files. 183 183 184 184 For each subtree directory in the monorepo: 1. Parses the dune-project to ··· 186 186 - Transforms it by removing dune-generated comment 187 187 - Adds dev-repo and url fields derived from dune-project 188 188 - Writes to opam-repo/packages/<name>/<name>.dev/opam 3. Deletes any 189 - orphaned packages in opam-repo not found in monorepo 4. Stages and commits 190 - changes in opam-repo 189 + orphaned packages in opam-repo not found in monorepo (only when syncing 190 + all packages) 4. Stages and commits changes in opam-repo 191 191 192 192 This is a generation-based approach - opam-repo is derived entirely from 193 193 monorepo dune-project and .opam files. 194 194 195 - @param proc Eio process manager 196 195 @param fs Eio filesystem 197 196 @param config Monopam configuration 198 - @param package Optional specific subtree to sync *) 197 + @param packages Optional list of package/subtree names to sync *) 199 198 200 199 (** {2 Package Management} *) 201 200
+48 -23
lib/opam_sync.ml
··· 12 12 } 13 13 14 14 let pp ppf r = 15 - Fmt.pf ppf "Synced: %d, Unchanged: %d, Missing: %d, Orphaned: %d" 16 - (List.length r.synced) (List.length r.unchanged) (List.length r.missing) 17 - (List.length r.orphaned) 15 + (* Print synced packages if any *) 16 + if r.synced <> [] then begin 17 + Fmt.pf ppf "@[<v>Synced %d package%s:@," (List.length r.synced) 18 + (if List.length r.synced = 1 then "" else "s"); 19 + List.iter (fun name -> Fmt.pf ppf " %s@," name) r.synced; 20 + Fmt.pf ppf "@]" 21 + end; 22 + (* Print orphaned packages if any *) 23 + if r.orphaned <> [] then begin 24 + if r.synced <> [] then Fmt.pf ppf "@,"; 25 + Fmt.pf ppf "@[<v>Removed %d orphaned package%s:@," (List.length r.orphaned) 26 + (if List.length r.orphaned = 1 then "" else "s"); 27 + List.iter (fun name -> Fmt.pf ppf " %s@," name) r.orphaned; 28 + Fmt.pf ppf "@]" 29 + end; 30 + (* Print summary if nothing changed *) 31 + if r.synced = [] && r.orphaned = [] then 32 + Fmt.pf ppf "All %d packages unchanged" (List.length r.unchanged) 18 33 19 34 let read_file_opt path = try Some (Eio.Path.load path) with Eio.Io _ -> None 20 35 ··· 112 127 orphaned; 113 128 orphaned 114 129 115 - let run ~fs ~config ?package () = 130 + let run ~fs ~config ?(packages = []) () = 116 131 let monorepo = Config.Paths.monorepo config in 117 132 let sources = load_sources ~fs ~monorepo in 118 133 match Monorepo_pkg.discover ~fs ~config ~sources () with 119 134 | Error (`Config_error e) -> Error (`Config_error e) 120 135 | Ok all_pkgs -> 121 136 let pkgs = 122 - match package with 123 - | None -> all_pkgs 124 - | Some name -> 137 + match packages with 138 + | [] -> all_pkgs 139 + | names -> 125 140 List.filter 126 141 (fun p -> 127 - Monorepo_pkg.name p = name || Monorepo_pkg.subtree p = name) 142 + List.exists 143 + (fun name -> 144 + Monorepo_pkg.name p = name || Monorepo_pkg.subtree p = name) 145 + names) 128 146 all_pkgs 129 147 in 130 - Log.app (fun m -> 131 - m "Generating opam-repo entries for %d packages..." (List.length pkgs)); 148 + let total = List.length pkgs in 149 + let progress = Tty.Progress.create ~total "Split" in 132 150 let opam_repo = Config.Paths.opam_repo config in 133 - let sync_results = List.map (sync_package ~fs ~opam_repo) pkgs in 151 + let sync_results = 152 + List.mapi 153 + (fun i pkg -> 154 + Tty.Progress.message progress (Monorepo_pkg.name pkg); 155 + Tty.Progress.set progress (i + 1); 156 + sync_package ~fs ~opam_repo pkg) 157 + pkgs 158 + in 159 + Tty.Progress.finish progress; 134 160 let synced, unchanged = 135 161 List.fold_left 136 162 (fun (s, u) r -> ··· 141 167 List.map Monorepo_pkg.name pkgs |> List.sort_uniq String.compare 142 168 in 143 169 let deleted = 144 - if package = None then delete_orphaned ~fs ~config ~generated_names 170 + if packages = [] then delete_orphaned ~fs ~config ~generated_names 145 171 else [] 146 172 in 147 173 let result = ··· 155 181 if result.synced <> [] || result.orphaned <> [] then begin 156 182 let repo = Git.Repository.open_repo ~fs opam_repo in 157 183 let msg = commit_message result in 158 - let now = Int64.of_float (Unix.gettimeofday ()) in 159 - let author = 160 - Git.User.v ~name:"monopam" ~email:"monopam@localhost" ~date:now () 161 - in 162 - match 163 - Git.Repository.commit_index repo ~author ~committer:author 164 - ~message:msg () 165 - with 166 - | Ok _ -> Log.app (fun m -> m "Committed opam sync: %s" msg) 167 - | Error (`Msg e) -> Log.warn (fun m -> m "Failed to commit: %s" e) 184 + match Git_cli.global_git_user () with 185 + | Some user -> ( 186 + match 187 + Git.Repository.commit_index repo ~author:user ~committer:user 188 + ~message:msg () 189 + with 190 + | Ok _ -> () 191 + | Error (`Msg e) -> Log.warn (fun m -> m "Failed to commit: %s" e)) 192 + | None -> 193 + Log.warn (fun m -> m "No git user config found, skipping commit") 168 194 end; 169 - Log.app (fun m -> m "%a" pp result); 170 195 Ok result
+1 -1
lib/opam_sync.mli
··· 12 12 val run : 13 13 fs:Eio.Fs.dir_ty Eio.Path.t -> 14 14 config:Config.t -> 15 - ?package:string -> 15 + ?packages:string list -> 16 16 unit -> 17 17 (t, [> `Config_error of string ]) result