Monorepo management for opam overlays
0
fork

Configure Feed

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

Remove monopam extract command

The extract functionality was breaking monopam push for normal repos.
A different strategy will be used for the "develop first, extract later"
workflow.

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

+1 -201
+1 -54
bin/main.ml
··· 164 164 Term.( 165 165 ret (const run $ package_arg $ upstream_arg $ logging_term)) 166 166 167 - (* Extract command *) 168 - 169 - let extract_cmd = 170 - let doc = "Extract a subdirectory as a standalone repository" in 171 - let man = [ 172 - `S Manpage.s_description; 173 - `P "Extracts a subdirectory from the monorepo as a standalone git \ 174 - repository with full history. Enables 'develop first, extract later'."; 175 - `P "The extraction process:"; 176 - `I ("1.", "Runs git subtree split to extract commits"); 177 - `I ("2.", "Creates a new git repository in checkouts"); 178 - `I ("3.", "Configures the remote URL"); 179 - `S "EXAMPLES"; 180 - `Pre " monopam extract my-lib --repo git@github.com:user/my-lib.git"; 181 - `Pre " monopam extract my-lib --repo git@github.com:user/my-lib.git --push"; 182 - ] in 183 - let info = Cmd.info "extract" ~doc ~man in 184 - let subdir_arg = 185 - let doc = "Subdirectory in monorepo to extract" in 186 - Arg.(required & pos 0 (some string) None & info [] ~docv:"SUBDIR" ~doc) 187 - in 188 - let repo_arg = 189 - let doc = "Git URL for the new repository" in 190 - Arg.(required & opt (some string) None & info [ "repo"; "r" ] ~docv:"URL" ~doc) 191 - in 192 - let branch_arg = 193 - let doc = "Branch name (default: from config)" in 194 - Arg.(value & opt (some string) None & info [ "branch"; "b" ] ~docv:"BRANCH" ~doc) 195 - in 196 - let push_arg = 197 - let doc = "Push to remote after extraction" in 198 - Arg.(value & flag & info [ "push" ] ~doc) 199 - in 200 - let create_opam_arg = 201 - let doc = "Create opam package metadata in overlay" in 202 - Arg.(value & flag & info [ "create-opam" ] ~doc) 203 - in 204 - let run subdir repo branch push create_opam () = 205 - Eio_main.run @@ fun env -> 206 - with_config env @@ fun config -> 207 - let fs = Eio.Stdenv.fs env in 208 - let proc = Eio.Stdenv.process_mgr env in 209 - match Monopam.extract ~proc ~fs ~config ~subdir ~repo_url:repo 210 - ?branch ~push ~create_opam () with 211 - | Ok () -> `Ok () 212 - | Error e -> 213 - Fmt.epr "Error: %a@." Monopam.pp_error e; 214 - `Error (false, "extract failed") 215 - in 216 - Cmd.v info 217 - Term.(ret (const run $ subdir_arg $ repo_arg 218 - $ branch_arg $ push_arg $ create_opam_arg $ logging_term)) 219 - 220 167 (* Add command *) 221 168 222 169 let add_cmd = ··· 934 881 in 935 882 let info = Cmd.info "monopam" ~version:"%%VERSION%%" ~doc ~man in 936 883 Cmd.group info 937 - [ status_cmd; pull_cmd; push_cmd; extract_cmd; add_cmd; remove_cmd; changes_cmd; verse_cmd ] 884 + [ status_cmd; pull_cmd; push_cmd; add_cmd; remove_cmd; changes_cmd; verse_cmd ] 938 885 939 886 let () = exit (Cmd.eval main_cmd)
-113
lib/monopam.ml
··· 19 19 | Dirty_state of Package.t list 20 20 | Package_not_found of string 21 21 | Claude_error of string 22 - | Subdir_not_found of string 23 - | Checkout_exists of Fpath.t 24 22 25 23 let pp_error ppf = function 26 24 | Config_error msg -> Fmt.pf ppf "Configuration error: %s" msg ··· 32 30 pkgs 33 31 | Package_not_found name -> Fmt.pf ppf "Package not found: %s" name 34 32 | Claude_error msg -> Fmt.pf ppf "Claude error: %s" msg 35 - | Subdir_not_found name -> Fmt.pf ppf "Subdirectory not found: %s" name 36 - | Checkout_exists path -> Fmt.pf ppf "Checkout already exists: %a" Fpath.pp path 37 33 38 34 let fs_typed (fs : _ Eio.Path.t) : Eio.Fs.dir_ty Eio.Path.t = 39 35 let dir, _ = fs in ··· 838 834 end 839 835 else Ok () 840 836 end 841 - end 842 - 843 - let create_opam_package ~fs ~config ~name ~repo_url = 844 - let opam_repo = Config.Paths.opam_repo config in 845 - let pkg_dir = Fpath.(opam_repo / "packages" / name / (name ^ ".dev")) in 846 - let opam_file = Fpath.(pkg_dir / "opam") in 847 - let content = Printf.sprintf {|opam-version: "2.0" 848 - name: "%s" 849 - version: "dev" 850 - synopsis: "TODO: Add synopsis" 851 - dev-repo: "git+%s" 852 - depends: [ 853 - "dune" {>= "3.0"} 854 - "ocaml" {>= "4.14"} 855 - ] 856 - build: [ 857 - ["dune" "build" "-p" name "-j" jobs] 858 - ] 859 - |} name repo_url in 860 - let pkg_dir_eio = Eio.Path.(fs / Fpath.to_string pkg_dir) in 861 - mkdirs pkg_dir_eio; 862 - let opam_eio = Eio.Path.(fs / Fpath.to_string opam_file) in 863 - Eio.Path.save ~create:(`Or_truncate 0o644) opam_eio content; 864 - Log.app (fun m -> m "Created opam package at %a" Fpath.pp opam_file); 865 - Ok () 866 - 867 - let extract ~proc ~fs ~config ~subdir ~repo_url ?branch ?(push = false) 868 - ?(create_opam = false) () = 869 - let ( let* ) r f = Result.bind (Result.map_error (fun e -> Git_error e) r) f in 870 - let fs = fs_typed fs in 871 - let monorepo = Config.Paths.monorepo config in 872 - let checkouts_root = Config.Paths.checkouts config in 873 - let checkout_dir = Fpath.(checkouts_root / subdir) in 874 - let branch = Option.value branch ~default:(Config.default_branch config) in 875 - 876 - (* Validate: subdir exists in monorepo *) 877 - if not (Git.Subtree.exists ~fs ~repo:monorepo ~prefix:subdir) then 878 - Error (Subdir_not_found subdir) 879 - else 880 - (* Validate: checkout doesn't already exist *) 881 - let checkout_eio = Eio.Path.(fs / Fpath.to_string checkout_dir) in 882 - let checkout_exists = 883 - match Eio.Path.kind ~follow:true checkout_eio with 884 - | `Directory -> true | _ -> false | exception _ -> false 885 - in 886 - if checkout_exists then Error (Checkout_exists checkout_dir) 887 - else 888 - (* Validate: monorepo is clean *) 889 - if Git.is_dirty ~proc ~fs monorepo then 890 - Error (Git_error (Git.Dirty_worktree monorepo)) 891 - else begin 892 - ensure_checkouts_dir ~fs ~config; 893 - 894 - (* Step 1: Split the subtree history *) 895 - Log.info (fun m -> m "Splitting subtree history for %s" subdir); 896 - let* split_commit = Git.Subtree.split ~proc ~fs ~repo:monorepo ~prefix:subdir () in 897 - Log.info (fun m -> m "Split commit: %s" split_commit); 898 - 899 - (* Step 2: Create new repo from split *) 900 - Log.info (fun m -> m "Creating checkout at %a" Fpath.pp checkout_dir); 901 - let* () = Git.init ~proc ~fs checkout_dir in 902 - let checkout_eio = Eio.Path.(fs / Fpath.to_string checkout_dir) in 903 - let monorepo_path = Fpath.to_string monorepo in 904 - 905 - (* Fetch split commit from monorepo *) 906 - let* _ = run_git_in ~proc ~cwd:checkout_eio 907 - [ "fetch"; monorepo_path; split_commit ] in 908 - let* _ = run_git_in ~proc ~cwd:checkout_eio 909 - [ "checkout"; "-b"; branch; "FETCH_HEAD" ] in 910 - 911 - (* Step 3: Add origin remote *) 912 - Log.info (fun m -> m "Adding remote origin: %s" repo_url); 913 - let* _ = run_git_in ~proc ~cwd:checkout_eio 914 - [ "remote"; "add"; "origin"; repo_url ] in 915 - 916 - (* Step 4: Optionally push *) 917 - let push_result = 918 - if push then begin 919 - Log.info (fun m -> m "Pushing to %s" repo_url); 920 - Git.push_remote ~proc ~fs ~branch checkout_dir 921 - |> Result.map_error (fun e -> Git_error e) 922 - end else Ok () 923 - in 924 - match push_result with 925 - | Error e -> Error e 926 - | Ok () -> 927 - 928 - (* Step 5: Optionally create opam metadata *) 929 - let create_opam_result = 930 - if create_opam then 931 - create_opam_package ~fs ~config ~name:subdir ~repo_url 932 - else Ok () 933 - in 934 - match create_opam_result with 935 - | Error e -> Error e 936 - | Ok () -> 937 - 938 - (* Print summary *) 939 - Log.app (fun m -> m "Extracted %s to %a" subdir Fpath.pp checkout_dir); 940 - Log.app (fun m -> m ""); 941 - Log.app (fun m -> m "Next steps:"); 942 - if not push then begin 943 - Log.app (fun m -> m " 1. Create the remote repository"); 944 - Log.app (fun m -> m " 2. Push: cd %a && git push -u origin %s" 945 - Fpath.pp checkout_dir branch) 946 - end; 947 - if not create_opam then 948 - Log.app (fun m -> m " 3. Add opam package metadata to enable push/pull"); 949 - Ok () 950 837 end 951 838 952 839 let add ~proc ~fs ~config ~package () =
-34
lib/monopam.mli
··· 43 43 (** Operation blocked due to dirty packages *) 44 44 | Package_not_found of string (** Named package not found in opam repo *) 45 45 | Claude_error of string (** Claude API or response parsing error *) 46 - | Subdir_not_found of string (** Subdirectory not found in monorepo *) 47 - | Checkout_exists of Fpath.t (** Checkout already exists at path *) 48 46 49 47 val pp_error : error Fmt.t 50 48 (** [pp_error] formats errors. *) ··· 112 110 @param config Monopam configuration 113 111 @param package Optional specific package to push 114 112 @param upstream If true, also push checkouts to their git remotes *) 115 - 116 - (** {2 Extract} *) 117 - 118 - val extract : 119 - proc:_ Eio.Process.mgr -> 120 - fs:Eio.Fs.dir_ty Eio.Path.t -> 121 - config:Config.t -> 122 - subdir:string -> 123 - repo_url:string -> 124 - ?branch:string -> 125 - ?push:bool -> 126 - ?create_opam:bool -> 127 - unit -> 128 - (unit, error) result 129 - (** [extract ~proc ~fs ~config ~subdir ~repo_url ()] extracts a subdirectory 130 - from the monorepo as a standalone git repository with full history. 131 - 132 - Enables the "develop in monorepo first, extract later" workflow. 133 - 134 - The extraction process: 135 - 1. Runs git subtree split to extract commits affecting the subdirectory 136 - 2. Creates a new git repository in the checkouts directory 137 - 3. Configures the remote URL 138 - 139 - @param proc Eio process manager 140 - @param fs Eio filesystem 141 - @param config Monopam configuration 142 - @param subdir Subdirectory in monorepo to extract 143 - @param repo_url Git URL for the new repository 144 - @param branch Branch name (default: from config) 145 - @param push If true, push to remote after extraction 146 - @param create_opam If true, create opam package metadata in overlay *) 147 113 148 114 (** {2 Package Management} *) 149 115