Monorepo management for opam overlays
0
fork

Configure Feed

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

monopam: nested monorepos auto-detected, three-layer push wired up

Two changes that work together to make nested monorepos invisible
papercut-free:

1. Auto-detect nested monorepos. A subtree is treated as a nested
monorepo iff its directory contains its own sources.toml. The old
`mono = true` flag in the outer sources.toml is gone — no field, no
codec entry, no manual marker. The detection happens in a new
Ctx.nested_monos helper that walks the immediate children of the
monorepo and pairs each "is a monorepo" subtree with its outer
sources entry (if any). push.ml and pull.ml use this helper instead
of Sources_registry.mono_entries.

2. Three-layer depth-first push. The previous implementation pushed
inner subtrees (lib.git) but skipped the middle layer (open-mono.git).
Push now does both: first the inner subtrees of each nested mono,
then a regular subtree push of the nested mono itself to its own
remote. Order is depth-first as the README promises.

Together these mean: a fix in product/open-mono/lib/ flows out to
lib.git AND open-mono.git in a single monopam push, with no flag
to forget.

Removed:
- Sources_registry.entry.mono : bool
- Sources_registry.mono_entries
- Sources_registry.codec_legacy and is_legacy_format
- test_sources_registry test cases for the dropped field

Tests:
- nested_mono.t setup no longer writes mono = true and asserts both
lib.git AND open-mono.git receive the change.
- test_sources_registry / test_pkg / test_deps helpers shrink to
match the smaller record.

+197 -228
-1
bin/cmd_verse.ml
··· 330 330 branch = None; 331 331 reason = Some (Fmt.str "Forked from %s" result.source_handle); 332 332 origin = Some Join; 333 - mono = false; 334 333 ref_ = None; 335 334 path = None; 336 335 }
+25
lib/ctx.ml
··· 148 148 | _ -> false 149 149 | exception _ -> false 150 150 151 + (** Walk every immediate subdirectory of [monorepo] and pick the ones that look 152 + like nested monorepos (= contain a [sources.toml] file). Pair each with its 153 + outer sources entry if any. *) 154 + let nested_monos ~fs ~monorepo ~sources = 155 + let monorepo_eio = Eio.Path.(fs / Fpath.to_string monorepo) in 156 + let entries = try Eio.Path.read_dir monorepo_eio with Eio.Io _ -> [] in 157 + List.filter_map 158 + (fun name -> 159 + if name = ".git" || name = "_build" then None 160 + else 161 + let sub = Fpath.(monorepo / name) in 162 + if not (is_directory ~fs sub) then None 163 + else 164 + let inner_toml = Eio.Path.(monorepo_eio / name / "sources.toml") in 165 + match Eio.Path.kind ~follow:true inner_toml with 166 + | `Regular_file -> 167 + let entry = 168 + Option.bind sources (fun s -> 169 + Sources_registry.find s ~subtree:name) 170 + in 171 + Some (name, entry) 172 + | _ -> None 173 + | exception _ -> None) 174 + entries 175 + 151 176 let normalize_opam_url_string s = 152 177 if String.starts_with ~prefix:"git+" s then 153 178 String.sub s 4 (String.length s - 4)
+13
lib/ctx.mli
··· 59 59 val is_directory : fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> bool 60 60 (** [is_directory ~fs path] returns [true] if [path] is a directory. *) 61 61 62 + val nested_monos : 63 + fs:Eio.Fs.dir_ty Eio.Path.t -> 64 + monorepo:Fpath.t -> 65 + sources:Sources_registry.t option -> 66 + (string * Sources_registry.entry option) list 67 + (** [nested_monos ~fs ~monorepo ~sources] returns the list of subtrees in 68 + [monorepo] that are themselves monorepos, paired with their sources.toml 69 + entry if there is one. 70 + 71 + Detection is purely structural: a subtree is a nested monorepo iff its 72 + directory contains its own [sources.toml] file. No flag in the outer 73 + sources.toml is required (or read). *) 74 + 62 75 val ensure_checkouts_dir : 63 76 fs:Eio.Fs.dir_ty Eio.Path.t -> config:Config.t -> unit 64 77 (** [ensure_checkouts_dir ~fs ~config] creates the checkouts directory. *)
-4
lib/fork_join.ml
··· 475 475 branch = Some branch; 476 476 reason = None; 477 477 origin = Some Fork; 478 - mono = false; 479 478 ref_ = None; 480 479 path = None; 481 480 }; ··· 596 595 branch = Some branch; 597 596 reason = None; 598 597 origin = Some Join; 599 - mono = false; 600 598 ref_ = None; 601 599 path = None; 602 600 }; ··· 936 934 branch = Some "main"; 937 935 reason = None; 938 936 origin = Some Fork; 939 - mono = false; 940 937 ref_ = None; 941 938 path = None; 942 939 } ··· 1047 1044 branch = Some branch; 1048 1045 reason = None; 1049 1046 origin = Some Join; 1050 - mono = false; 1051 1047 ref_ = None; 1052 1048 path = None; 1053 1049 }
-1
lib/import.ml
··· 444 444 branch; 445 445 reason = None; 446 446 origin = None; 447 - mono = false; 448 447 ref_ = Some result.commit; 449 448 path; 450 449 }
+44 -46
lib/pull.ml
··· 356 356 m "Failed to merge mono inner subtree %s: %a" prefix 357 357 Ctx.pp_error_with_hint e)) 358 358 359 - (** Pull inner subtrees for mono=true entries. For each entry in the inner 360 - sources.toml, fetches the checkout and merges at the nested prefix. Inner 361 - subtrees are processed first (depth-first). *) 359 + (** Pull inner subtrees of every nested monorepo found in the workspace. A 360 + subtree is a nested monorepo iff its directory contains a [sources.toml] 361 + file — no flag, no marker required. *) 362 362 let mono_entries ~sw ~proc ~fs ~config = 363 363 let fs_t = Ctx.fs_typed fs in 364 364 let monorepo = Config.Paths.monorepo config in 365 365 let checkouts_root = Config.Paths.checkouts config in 366 366 let sources_path = Fpath.(monorepo / "sources.toml") in 367 - match Sources_registry.load ~fs:(fs_t :> _ Eio.Path.t) sources_path with 368 - | Error _ -> () 369 - | Ok sources -> 370 - let mono = Sources_registry.mono_entries sources in 371 - if mono <> [] then begin 372 - Log.info (fun m -> 373 - m "Processing %d mono entries for inner subtree pull" 374 - (List.length mono)); 375 - List.iter 376 - (fun (mono_name, _mono_entry) -> 377 - let inner_sources_path = 378 - Fpath.(monorepo / mono_name / "sources.toml") 379 - in 380 - match 381 - Sources_registry.load 382 - ~fs:(fs_t :> _ Eio.Path.t) 383 - inner_sources_path 384 - with 385 - | Error msg -> 386 - Log.warn (fun m -> 387 - m "Failed to load %a: %s" Fpath.pp inner_sources_path msg) 388 - | Ok inner_sources -> 389 - let inner_entries = Sources_registry.to_list inner_sources in 390 - List.iter 391 - (fun (inner_name, (inner_entry : Sources_registry.entry)) -> 392 - let nested_prefix = mono_name ^ "/" ^ inner_name in 393 - let branch = 394 - Option.value ~default:"main" inner_entry.branch 395 - in 396 - let checkout_dir = Fpath.(checkouts_root / inner_name) in 397 - let clone_url = 398 - Ctx.normalize_opam_url_string inner_entry.source 399 - in 400 - let fetched = 401 - fetch_or_clone_inner ~proc ~fs_t ~checkout_dir ~clone_url 402 - ~name:inner_name ~label:nested_prefix ~branch 403 - in 404 - if fetched then 405 - merge_inner_subtree ~sw ~proc ~fs_t ~monorepo 406 - ~prefix:nested_prefix ~checkout_dir ~branch) 407 - inner_entries) 408 - mono 409 - end 367 + let outer_sources = 368 + match Sources_registry.load ~fs:(fs_t :> _ Eio.Path.t) sources_path with 369 + | Ok s -> Some s 370 + | Error _ -> None 371 + in 372 + let nested = Ctx.nested_monos ~fs:fs_t ~monorepo ~sources:outer_sources in 373 + if nested <> [] then begin 374 + Log.info (fun m -> 375 + m "Processing %d nested monorepo(s) for inner subtree pull" 376 + (List.length nested)); 377 + List.iter 378 + (fun (mono_name, _entry) -> 379 + let inner_sources_path = 380 + Fpath.(monorepo / mono_name / "sources.toml") 381 + in 382 + match 383 + Sources_registry.load ~fs:(fs_t :> _ Eio.Path.t) inner_sources_path 384 + with 385 + | Error msg -> 386 + Log.warn (fun m -> 387 + m "Failed to load %a: %s" Fpath.pp inner_sources_path msg) 388 + | Ok inner_sources -> 389 + let inner_entries = Sources_registry.to_list inner_sources in 390 + List.iter 391 + (fun (inner_name, (inner_entry : Sources_registry.entry)) -> 392 + let nested_prefix = mono_name ^ "/" ^ inner_name in 393 + let branch = Option.value ~default:"main" inner_entry.branch in 394 + let checkout_dir = Fpath.(checkouts_root / inner_name) in 395 + let clone_url = 396 + Ctx.normalize_opam_url_string inner_entry.source 397 + in 398 + let fetched = 399 + fetch_or_clone_inner ~proc ~fs_t ~checkout_dir ~clone_url 400 + ~name:inner_name ~label:nested_prefix ~branch 401 + in 402 + if fetched then 403 + merge_inner_subtree ~sw ~proc ~fs_t ~monorepo 404 + ~prefix:nested_prefix ~checkout_dir ~branch) 405 + inner_entries) 406 + nested 407 + end 410 408 411 409 let run ~sw ~proc ~fs ~config ?(packages = []) ?opam_repo_url () = 412 410 let ( let* ) = Result.bind in
+58 -18
lib/push.ml
··· 583 583 end) 584 584 inner_entries 585 585 586 - (** Push inner subtrees of mono=true entries. For each entry in the inner 587 - sources.toml, splits at the nested prefix and pushes to a shared checkout. 588 - *) 586 + (** Push the outer subtree of a nested monorepo to the inner mono's own remote. 587 + This is the middle layer of the depth-first push: 588 + 589 + - inner-most: push individual subtrees inside the mono to their own upstream 590 + URLs. Done by [mono_inner] above. 591 + - middle: push the outer monorepo's split at the mono prefix to the inner 592 + mono's URL (e.g. product split at "open-mono" → open-mono.git). This is 593 + what this function does. 594 + - outermost: workspace_repos pushes the outer monorepo to its remote. 595 + 596 + Without this step, the inner mono's git history never receives the outer's 597 + edits and a downstream developer pulling from open-mono.git would not see 598 + them. *) 599 + let push_mono_outer_subtree ~sw ~proc ~fs_t ~monorepo ~checkouts_root ~git_repo 600 + ~clean ~force mono_name mono_entry = 601 + match mono_entry with 602 + | None -> 603 + Log.debug (fun m -> 604 + m "Skipping mono outer subtree %s (no source URL in sources.toml)" 605 + mono_name) 606 + | Some (mono_entry : Sources_registry.entry) -> 607 + if not (Ctx.is_directory ~fs:fs_t Fpath.(monorepo / mono_name)) then 608 + Log.debug (fun m -> 609 + m "Skipping mono outer subtree %s (not in monorepo)" mono_name) 610 + else begin 611 + let checkout_dir = Fpath.(checkouts_root / mono_name) in 612 + let clone_url = Ctx.normalize_opam_url_string mono_entry.source in 613 + let branch = Option.value ~default:"main" mono_entry.branch in 614 + let cloned = 615 + ensure_inner_clone ~proc ~fs_t ~checkout_dir ~clone_url 616 + ~name:mono_name ~label:mono_name ~branch 617 + in 618 + if cloned then 619 + inner_subtree ~sw ~proc ~fs_t ~monorepo ~git_repo ~prefix:mono_name 620 + ~checkout_dir ~name:mono_name ~clean ~force ~branch 621 + end 622 + 623 + (** Push every nested monorepo found in the workspace. A subtree is a nested 624 + monorepo iff its directory contains a [sources.toml] file — no flag, no 625 + marker required. *) 589 626 let mono_entries ~sw ~proc ~fs ~config ~sources ~clean ~force = 590 627 let fs_t = Ctx.fs_typed fs in 591 628 let monorepo = Config.Paths.monorepo config in 592 629 let checkouts_root = Config.Paths.checkouts config in 593 - match sources with 594 - | None -> () 595 - | Some sources -> 596 - let mono = Sources_registry.mono_entries sources in 597 - if mono <> [] then begin 598 - Log.info (fun m -> 599 - m "Processing %d mono entries for inner subtree push" 600 - (List.length mono)); 601 - let git_repo = Git.Repository.open_repo ~sw ~fs:fs_t monorepo in 602 - List.iter 603 - (fun (mono_name, _mono_entry) -> 604 - mono_inner ~sw ~proc ~fs_t ~monorepo ~checkouts_root ~git_repo 605 - ~clean ~force mono_name) 606 - mono 607 - end 630 + let nested = Ctx.nested_monos ~fs:fs_t ~monorepo ~sources in 631 + if nested <> [] then begin 632 + Log.info (fun m -> 633 + m "Processing %d nested monorepo(s) for inner subtree push" 634 + (List.length nested)); 635 + let git_repo = Git.Repository.open_repo ~sw ~fs:fs_t monorepo in 636 + (* Depth-first: inner subtrees first, then the outer mono itself. *) 637 + List.iter 638 + (fun (mono_name, _entry) -> 639 + mono_inner ~sw ~proc ~fs_t ~monorepo ~checkouts_root ~git_repo ~clean 640 + ~force mono_name) 641 + nested; 642 + List.iter 643 + (fun (mono_name, entry) -> 644 + push_mono_outer_subtree ~sw ~proc ~fs_t ~monorepo ~checkouts_root 645 + ~git_repo ~clean ~force mono_name entry) 646 + nested 647 + end 608 648 609 649 let log_missing_repos ~all_pkgs missing = 610 650 if missing <> [] then begin
+6 -71
lib/sources_registry.ml
··· 13 13 branch : string option; 14 14 reason : string option; 15 15 origin : origin option; 16 - mono : bool; 17 16 ref_ : string option; 18 17 path : string option; 19 18 } ··· 57 56 58 57 (* Backward-compat alias *) 59 58 let derive_url = derive_source 60 - let mono_entries t = List.filter (fun (_, entry) -> entry.mono) t.entries 61 59 62 60 let add t ~subtree entry = 63 61 { t with entries = (subtree, entry) :: List.remove_assoc subtree t.entries } ··· 80 78 81 79 [open-mono] 82 80 source = "git+https://github.com/org/open-mono" 83 - mono = true 84 81 ref = "abc123" 82 + # No flag needed: nested monorepos are auto-detected from the 83 + # presence of a sources.toml file inside open-mono/. 85 84 *) 86 85 87 86 let origin_codec : origin Tomlt.t = ··· 93 92 ~enc:(function Fork -> "fork" | Join -> "join") 94 93 Tomlt.string 95 94 96 - (** Decode entry from TOML table, reading both old [url] and new [source] keys 97 - *) 98 95 let entry_codec : entry Tomlt.t = 99 96 Tomlt.( 100 97 Table.( 101 - obj (fun source upstream branch reason entry_origin mono ref_ path -> 98 + obj (fun source upstream branch reason entry_origin ref_ path -> 102 99 { 103 100 source; 104 101 upstream; 105 102 branch; 106 103 reason; 107 104 origin = entry_origin; 108 - mono; 109 105 ref_; 110 106 path; 111 107 }) ··· 114 110 |> opt_mem "branch" string ~enc:(fun (e : entry) -> e.branch) 115 111 |> opt_mem "reason" string ~enc:(fun (e : entry) -> e.reason) 116 112 |> opt_mem "origin" origin_codec ~enc:(fun (e : entry) -> e.origin) 117 - |> mem "mono" bool ~dec_absent:false 118 - ~enc:(fun (e : entry) -> e.mono) 119 - ~enc_omit:(fun b -> not b) 120 113 |> opt_mem "ref" string ~enc:(fun (e : entry) -> e.ref_) 121 114 |> opt_mem "path" string ~enc:(fun (e : entry) -> e.path) 122 115 |> finish)) 123 116 124 - (** Decode entry from a TOML table that uses old field names (backward compat). 125 - Reads [url] as [source] and ignores [mono]/[ref]/[path]. *) 126 - let entry_codec_legacy : entry Tomlt.t = 127 - Tomlt.( 128 - Table.( 129 - obj (fun source upstream branch reason entry_origin -> 130 - { 131 - source; 132 - upstream; 133 - branch; 134 - reason; 135 - origin = entry_origin; 136 - mono = false; 137 - ref_ = None; 138 - path = None; 139 - }) 140 - |> mem "url" string ~enc:(fun (e : entry) -> e.source) 141 - |> opt_mem "upstream" string ~enc:(fun (e : entry) -> e.upstream) 142 - |> opt_mem "branch" string ~enc:(fun (e : entry) -> e.branch) 143 - |> opt_mem "reason" string ~enc:(fun (e : entry) -> e.reason) 144 - |> opt_mem "origin" origin_codec ~enc:(fun (e : entry) -> e.origin) 145 - |> finish)) 146 - 147 117 let codec : t Tomlt.t = 148 118 Tomlt.( 149 119 Table.( ··· 152 122 |> keep_unknown ~enc:(fun t -> t.entries) (Mems.assoc entry_codec) 153 123 |> finish)) 154 124 155 - (** Legacy codec that reads [default_url_base] and [url] field names *) 156 - let codec_legacy : t Tomlt.t = 157 - Tomlt.( 158 - Table.( 159 - obj (fun origin entries -> { origin; entries }) 160 - |> opt_mem "default_url_base" string ~enc:(fun t -> t.origin) 161 - |> keep_unknown ~enc:(fun t -> t.entries) (Mems.assoc entry_codec_legacy) 162 - |> finish)) 163 - 164 - let is_legacy_format content = 165 - (* Detect legacy format by checking for old field names. 166 - The legacy format uses "url =" and "default_url_base" while 167 - the new format uses "source =" and "origin =". We check for 168 - lines starting with the old keys to avoid false matches. *) 169 - let lines = String.split_on_char '\n' content in 170 - List.exists 171 - (fun line -> 172 - let line = String.trim line in 173 - String.starts_with ~prefix:"url " line 174 - || String.starts_with ~prefix:"url=" line 175 - || String.starts_with ~prefix:"default_url_base" line) 176 - lines 177 - 178 125 let load ~fs path = 179 126 let path_str = Fpath.to_string path in 180 127 let eio_path = Eio.Path.(fs / path_str) in 181 - (* Check if file exists *) 182 128 match Eio.Path.kind ~follow:true eio_path with 183 129 | `Regular_file -> ( 184 - (* Read file content to detect format *) 185 - let content = try Some (Eio.Path.load eio_path) with Eio.Io _ -> None in 186 - let use_legacy = 187 - match content with Some c -> is_legacy_format c | None -> false 188 - in 189 - if use_legacy then 190 - try Ok (Tomlt_eio.decode_path_exn codec_legacy ~fs path_str) with 191 - | Failure msg -> err_invalid msg 192 - | exn -> err_load exn 193 - else 194 - try Ok (Tomlt_eio.decode_path_exn codec ~fs path_str) with 195 - | Failure msg -> err_invalid msg 196 - | exn -> err_load exn) 130 + try Ok (Tomlt_eio.decode_path_exn codec ~fs path_str) with 131 + | Failure msg -> err_invalid msg 132 + | exn -> err_load exn) 197 133 | _ -> Ok empty (* File doesn't exist, return empty registry *) 198 134 | exception _ -> Ok empty 199 135 ··· 214 150 Option.iter (fun b -> Fmt.pf ppf "@ branch: %s" b) e.branch; 215 151 Option.iter (fun r -> Fmt.pf ppf "@ reason: %s" r) e.reason; 216 152 Option.iter (fun o -> Fmt.pf ppf "@ origin: %a" pp_origin o) e.origin; 217 - if e.mono then Fmt.pf ppf "@ mono: true"; 218 153 Option.iter (fun r -> Fmt.pf ppf "@ ref: %s" r) e.ref_; 219 154 Option.iter (fun p -> Fmt.pf ppf "@ path: %s" p) e.path; 220 155 Fmt.pf ppf "@]"
+4 -7
lib/sources_registry.mli
··· 6 6 - Forked packages (our fork URL vs upstream) 7 7 - Vendored packages (local copy, custom URL) 8 8 - Packages without source in dune-project 9 - - Nested monorepos ([mono = true]) 9 + 10 + Nested monorepos are auto-detected: any subtree containing its own 11 + [sources.toml] file is treated as a nested monorepo by push and pull. No 12 + explicit flag is required. 10 13 11 14 The registry also supports an [origin] field (formerly [default_url_base]) 12 15 that is used to derive URLs for subtrees without explicit entries: ··· 28 31 branch : string option; (** Override branch (default: main) *) 29 32 reason : string option; (** Why we have a custom source *) 30 33 origin : origin option; (** How this entry was created *) 31 - mono : bool; 32 - (** If [true], this subtree is itself a monorepo with its own 33 - sources.toml. Push and pull recurse into it. *) 34 34 ref_ : string option; 35 35 (** Pinned commit SHA. Replaces mono.lock — records the exact commit that 36 36 was imported/last synced. *) ··· 71 71 72 72 val derive_url : t -> subtree:string -> string option 73 73 (** @deprecated Use {!derive_source} instead. *) 74 - 75 - val mono_entries : t -> (string * entry) list 76 - (** [mono_entries t] returns entries where [mono = true]. *) 77 74 78 75 val add : t -> subtree:string -> entry -> t 79 76 (** [add t ~subtree entry] adds or replaces an entry. *)
+42 -31
test/nested_mono.t/run.t
··· 1 - Nested monorepos: mono = true marker 2 - ====================================== 1 + Nested monorepos: auto-detection 2 + ================================== 3 3 4 - One outer monorepo (product) vendors another monorepo (open-mono) as 5 - a subtree marked `mono = true`. Open-mono itself vendors a library 6 - (lib) as a regular subtree. The test asserts that pushing from 7 - product recurses depth-first into open-mono's inner sources.toml and 8 - propagates the changes to the library's upstream. 9 - 10 - The README's full three-layer story (product → open-mono.git → 11 - lib.git) also wants an intermediate push to open-mono.git itself. 12 - That middle layer is not yet wired up; this test only covers the 13 - direct product → lib.git recursion, which is what the current 14 - `mono = true` implementation provides. See TODO.md for the gap. 4 + The README's three-layer story has an outer product monorepo, a 5 + middle open-mono monorepo (vendors lib/ as a regular subtree), and 6 + an inner lib.git as the library's own upstream. The outer monorepo 7 + treats open-mono as a nested monorepo because open-mono contains 8 + its own sources.toml — no flag required, no marker in the outer 9 + sources.toml. Push from the product workspace propagates a fix in 10 + open-mono/lib/ depth-first all the way out: first to lib.git, then 11 + to open-mono.git, then to the outer product remote. This test 12 + exercises all three layers and checks that lib.git AND open-mono.git 13 + both receive the change in a single `monopam push`. 15 14 16 15 Setup 17 16 ----- ··· 29 28 $ printf '[init]\n\tdefaultBranch = main\n[user]\n\tname = Alice\n\temail = alice@example.com\n' > "$GIT_CONFIG_GLOBAL" 30 29 $ TROOT=$(pwd) 31 30 32 - Stage 1: the empty library upstream 33 - ------------------------------------ 31 + Stage 1: the empty bare upstreams 32 + ---------------------------------- 34 33 35 - An empty bare repo, like a freshly created github or tangled repo. 36 - The first push from the product workspace will establish its initial 37 - history — same pattern as push.t. 34 + Two empty bare repos, like freshly created github or tangled repos. 35 + The first push from the product workspace will establish their 36 + initial histories — same pattern as push.t. 38 37 39 38 $ git init -q --bare lib.git 39 + $ git init -q --bare open-mono.git 40 40 $ cat > lib.opam << OPAM 41 41 > opam-version: "2.0" 42 42 > name: "lib" ··· 48 48 Stage 2: product workspace 49 49 --------------------------- 50 50 51 - Product has two layers. The outer layer is the product monorepo, which 52 - vendors open-mono/ as `mono = true`. The inner layer is open-mono, 53 - materialized inside product/open-mono/, with its own sources.toml and 54 - a regular subtree under product/open-mono/lib/ that tracks lib.git. 51 + Product has two layers. The outer layer is the product monorepo. The 52 + inner layer is open-mono, materialized inside product/open-mono/ with 53 + its own sources.toml and a regular subtree under 54 + product/open-mono/lib/ that tracks lib.git. The presence of 55 + open-mono/sources.toml is the only signal monopam needs to treat 56 + open-mono as a nested monorepo. 55 57 56 58 The outer opam-repo overlay registers lib so that `monopam push lib` 57 59 from product can discover it and drive the nested merge. ··· 84 86 $ cat > sources.toml << EOF 85 87 > [open-mono] 86 88 > source = "git+file://$TROOT/open-mono.git" 87 - > mono = true 88 89 > EOF 89 90 $ git add . && git commit -q -m "initial product with open-mono subtree" 90 91 ··· 96 97 $ echo 'let run () = Printf.printf "ok"' > open-mono/lib/src/main.ml 97 98 $ git add -A && git commit -q -m "lib: print ok" 98 99 99 - Pushing `lib` from the product workspace must detect the `open-mono` 100 - `mono = true` entry, load open-mono/sources.toml, split product's 101 - open-mono/lib subdirectory, and push that split all the way to lib.git 102 - (the library's own upstream). 100 + Pushing `lib` from the product workspace must detect open-mono as a 101 + nested monorepo (because open-mono/sources.toml exists), load that 102 + inner sources.toml, then push depth-first: lib.git first, then 103 + open-mono.git for the outer subtree. 103 104 104 105 $ monopam push lib 2>&1 \ 105 106 > | grep -E "✓|nested" \ 106 107 > | sed -e '/Changes pushed/ s/ ([0-9.]*s)//' 107 108 ✓ open-mono/lib (nested) → $TESTCASE_ROOT/src/lib 109 + ✓ open-mono (nested) → $TESTCASE_ROOT/src/open-mono 108 110 ✓ Changes pushed to your remotes. 109 111 110 - The lib.git upstream should contain the new commit in src/main.ml. 112 + Inner layer: lib.git received the new commit in src/main.ml. 111 113 112 - $ rm -rf "$TROOT/verify" 113 - $ git clone -q "$TROOT/lib.git" "$TROOT/verify" 2>/dev/null 114 - $ grep "ok" "$TROOT/verify/src/main.ml" 114 + $ rm -rf "$TROOT/verify-lib" 115 + $ git clone -q "$TROOT/lib.git" "$TROOT/verify-lib" 2>/dev/null 116 + $ grep "ok" "$TROOT/verify-lib/src/main.ml" 117 + let run () = Printf.printf "ok" 118 + 119 + Middle layer: open-mono.git received the same change inside its 120 + own lib/ subdirectory, so a downstream developer who pulls from 121 + open-mono.git (and not directly from lib.git) also sees the fix. 122 + 123 + $ rm -rf "$TROOT/verify-open" 124 + $ git clone -q "$TROOT/open-mono.git" "$TROOT/verify-open" 2>/dev/null 125 + $ grep "ok" "$TROOT/verify-open/lib/src/main.ml" 115 126 let run () = Printf.printf "ok"
-1
test/test_deps.ml
··· 21 21 branch; 22 22 reason = None; 23 23 origin = None; 24 - mono = false; 25 24 ref_; 26 25 path; 27 26 }
+2 -13
test/test_pkg.ml
··· 33 33 34 34 (* Helper to build a sources_registry entry *) 35 35 let sr_entry ?(upstream = None) ?(branch = None) ?(reason = None) 36 - ?(entry_origin = None) ?(mono = false) ?(ref_ = None) ?(path = None) source 37 - = 38 - SR. 39 - { 40 - source; 41 - upstream; 42 - branch; 43 - reason; 44 - origin = entry_origin; 45 - mono; 46 - ref_; 47 - path; 48 - } 36 + ?(entry_origin = None) ?(ref_ = None) ?(path = None) source = 37 + SR.{ source; upstream; branch; reason; origin = entry_origin; ref_; path } 49 38 50 39 let check_dev_repo msg expected result = 51 40 Alcotest.(check (option (pair string string))) msg expected result
+3 -35
test/test_sources_registry.ml
··· 6 6 Alcotest.testable SR.pp_entry (fun a b -> 7 7 a.SR.source = b.SR.source && a.upstream = b.upstream 8 8 && a.branch = b.branch && a.reason = b.reason && a.origin = b.origin 9 - && a.mono = b.mono && a.ref_ = b.ref_ && a.path = b.path) 9 + && a.ref_ = b.ref_ && a.path = b.path) 10 10 11 11 let entry ?(upstream = None) ?(branch = None) ?(reason = None) 12 - ?(entry_origin = None) ?(mono = false) ?(ref_ = None) ?(path = None) source 13 - = 14 - SR. 15 - { 16 - source; 17 - upstream; 18 - branch; 19 - reason; 20 - origin = entry_origin; 21 - mono; 22 - ref_; 23 - path; 24 - } 12 + ?(entry_origin = None) ?(ref_ = None) ?(path = None) source = 13 + SR.{ source; upstream; branch; reason; origin = entry_origin; ref_; path } 25 14 26 15 (* Test basic operations *) 27 16 ··· 53 42 54 43 (* Test new fields *) 55 44 56 - let test_mono_field () = 57 - let entry = entry ~mono:true "git+https://github.com/org/open-mono" in 58 - Alcotest.(check bool) "mono true" true entry.SR.mono; 59 - let t = SR.add SR.empty ~subtree:"open-mono" entry in 60 - match SR.find t ~subtree:"open-mono" with 61 - | Some e -> Alcotest.(check bool) "mono preserved" true e.SR.mono 62 - | None -> Alcotest.fail "entry not found" 63 - 64 45 let test_ref_field () = 65 46 let entry = 66 47 entry ~ref_:(Some "abc123def456") "git+https://github.com/org/foo" ··· 72 53 Alcotest.(check (option string)) 73 54 "ref preserved" (Some "abc123def456") e.SR.ref_ 74 55 | None -> Alcotest.fail "entry not found" 75 - 76 - let test_mono_entries () = 77 - let regular = entry "git+https://github.com/org/foo" in 78 - let mono_entry = entry ~mono:true "git+https://github.com/org/open-mono" in 79 - let regular2 = entry "git+https://github.com/org/bar" in 80 - let t = SR.add SR.empty ~subtree:"foo" regular in 81 - let t = SR.add t ~subtree:"open-mono" mono_entry in 82 - let t = SR.add t ~subtree:"bar" regular2 in 83 - let monos = SR.mono_entries t in 84 - Alcotest.(check int) "one mono entry" 1 (List.length monos); 85 - Alcotest.(check string) "mono name" "open-mono" (fst (List.hd monos)) 86 56 87 57 (* Test origin (renamed from default_url_base) *) 88 58 ··· 151 121 Alcotest.test_case "add and find" `Quick test_add_find; 152 122 Alcotest.test_case "add and remove" `Quick test_add_remove; 153 123 Alcotest.test_case "add replace" `Quick test_add_replace; 154 - Alcotest.test_case "mono field" `Quick test_mono_field; 155 124 Alcotest.test_case "ref field" `Quick test_ref_field; 156 - Alcotest.test_case "mono_entries filter" `Quick test_mono_entries; 157 125 Alcotest.test_case "origin" `Quick test_origin; 158 126 Alcotest.test_case "default_url_base compat" `Quick 159 127 test_default_url_base_compat;