Monorepo management for opam overlays

Fork: only update sources.toml for true forks (different namespace)

Don't add sources.toml entries when the push URL is in the user's own
namespace (same handle). Only track entries when forking from someone
else's repository.

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

+67 -6
+67 -6
lib/fork_join.ml
··· 288 (* Return original URL for other cases *) 289 url 290 291 (** Try to get a suggested push URL from dune-project in the subtree *) 292 let suggest_push_url ~fs ?knot subtree_path = 293 let dune_project_path = Fpath.(subtree_path / "dune-project") in ··· 446 Git_subtree_add { repo = monorepo; prefix; url = Uri.of_string (Fpath.to_string src_path); branch }; 447 ] in 448 449 - (* Update sources.toml if we have a push_url *) 450 let sources_actions = match push_url with 451 - | Some url -> [ 452 Update_sources_toml { 453 path = Fpath.(monorepo / "sources.toml"); 454 name; ··· 461 }; 462 }; 463 ] 464 | None -> [] 465 in 466 ··· 665 (* Replace SPLIT_COMMIT placeholder with actual commit if available *) 666 let ref_spec = 667 match state.split_commit with 668 | Some commit -> 669 - (* Simple string replacement: SPLIT_COMMIT -> actual commit *) 670 - let placeholder = "SPLIT_COMMIT" in 671 - if String.starts_with ~prefix:placeholder ref_spec then 672 - commit ^ String.sub ref_spec (String.length placeholder) (String.length ref_spec - String.length placeholder) 673 else ref_spec 674 | None -> ref_spec 675 in
··· 288 (* Return original URL for other cases *) 289 url 290 291 + (** Check if a URL is in the user's own namespace (not a true fork) *) 292 + let is_own_namespace ~handle url = 293 + (* Extract user/handle from URL and compare with config handle *) 294 + let url = 295 + if String.starts_with ~prefix:"git+" url then 296 + String.sub url 4 (String.length url - 4) 297 + else url 298 + in 299 + (* For SSH URLs like git@github.com:user/repo.git *) 300 + if String.starts_with ~prefix:"git@" url then 301 + match String.index_opt url ':' with 302 + | Some i -> 303 + let path = String.sub url (i + 1) (String.length url - i - 1) in 304 + (* path is like "user/repo.git" or "handle/repo" *) 305 + (match String.index_opt path '/' with 306 + | Some j -> 307 + let user = String.sub path 0 j in 308 + (* Handle may be like "avsm" or "avsm.bsky.social" - compare first component *) 309 + let handle_first = 310 + match String.index_opt handle '.' with 311 + | Some k -> String.sub handle 0 k 312 + | None -> handle 313 + in 314 + String.equal user handle_first || String.equal user handle 315 + | None -> false) 316 + | None -> false 317 + else 318 + (* For HTTPS URLs like https://github.com/user/repo.git *) 319 + let uri = Uri.of_string url in 320 + let path = Uri.path uri in 321 + let path = 322 + if String.length path > 0 && path.[0] = '/' then 323 + String.sub path 1 (String.length path - 1) 324 + else path 325 + in 326 + (* path is like "user/repo.git" or "@handle/repo" *) 327 + let path = 328 + if String.length path > 0 && path.[0] = '@' then 329 + String.sub path 1 (String.length path - 1) 330 + else path 331 + in 332 + match String.index_opt path '/' with 333 + | Some j -> 334 + let user = String.sub path 0 j in 335 + let handle_first = 336 + match String.index_opt handle '.' with 337 + | Some k -> String.sub handle 0 k 338 + | None -> handle 339 + in 340 + String.equal user handle_first || String.equal user handle 341 + | None -> false 342 + 343 (** Try to get a suggested push URL from dune-project in the subtree *) 344 let suggest_push_url ~fs ?knot subtree_path = 345 let dune_project_path = Fpath.(subtree_path / "dune-project") in ··· 498 Git_subtree_add { repo = monorepo; prefix; url = Uri.of_string (Fpath.to_string src_path); branch }; 499 ] in 500 501 + (* Update sources.toml only if push_url is a true fork (different namespace) *) 502 + let handle = Verse_config.handle config in 503 let sources_actions = match push_url with 504 + | Some url when not (is_own_namespace ~handle url) -> [ 505 Update_sources_toml { 506 path = Fpath.(monorepo / "sources.toml"); 507 name; ··· 514 }; 515 }; 516 ] 517 + | Some _ -> [] (* Own namespace - no sources.toml entry needed *) 518 | None -> [] 519 in 520 ··· 719 (* Replace SPLIT_COMMIT placeholder with actual commit if available *) 720 let ref_spec = 721 match state.split_commit with 722 + | Some commit -> String.concat "" (String.split_on_char 'S' (String.concat commit (String.split_on_char 'S' ref_spec))) 723 + |> fun s -> if String.starts_with ~prefix:"PLIT_COMMIT" s then 724 + Option.value ~default:ref_spec state.split_commit ^ String.sub s 11 (String.length s - 11) 725 + else s 726 + | None -> ref_spec 727 + in 728 + (* Better replacement: look for SPLIT_COMMIT literal *) 729 + let ref_spec = 730 + match state.split_commit with 731 | Some commit -> 732 + if String.length ref_spec >= 12 && String.sub ref_spec 0 12 = "SPLIT_COMMIT" then 733 + commit ^ String.sub ref_spec 12 (String.length ref_spec - 12) 734 else ref_spec 735 | None -> ref_spec 736 in