···11+ISC License
22+33+Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
44+55+Permission to use, copy, modify, and distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
1010+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1111+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1212+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1313+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1414+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1515+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+50
README.md
···11+# unpac - OCaml Monorepo Management Tool
22+33+A tool for managing OCaml monorepos with opam repository integration. Unpac handles vendoring dependencies, managing worktrees, and synchronizing packages across a monorepo.
44+55+## Key Features
66+77+- **Dependency Vendoring**: Vendor opam packages directly into your monorepo
88+- **Worktree Management**: Manage git worktrees for isolated development
99+- **Repository Integration**: Seamless integration with opam repositories
1010+- **Eio-based**: Built on the Eio direct-style effects library for modern OCaml
1111+1212+## Installation
1313+1414+```
1515+opam install unpac
1616+```
1717+1818+## Usage
1919+2020+Configure your monorepo with an `unpac.toml` file:
2121+2222+```toml
2323+# Example configuration - see unpac.toml.example for details
2424+```
2525+2626+Then run:
2727+2828+```bash
2929+unpac init # Initialize a new monorepo
3030+unpac vendor # Vendor dependencies
3131+unpac sync # Synchronize packages
3232+```
3333+3434+## Packages
3535+3636+- **unpac**: Core library for monorepo management
3737+- **unpac-opam**: Opam backend for dependency resolution and vendoring
3838+3939+## Documentation
4040+4141+API documentation is available via:
4242+4343+```
4444+opam install unpac
4545+odig doc unpac
4646+```
4747+4848+## License
4949+5050+ISC
+439-282
bin/main.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16open Cmdliner
2738(* Logging setup *)
···611 Logs.set_level (Some Logs.Info);
712 Logs.set_reporter (Logs_fmt.reporter ())
81399-let logging_term =
1010- Term.(const setup_logging $ const ())
1414+let logging_term = Term.(const setup_logging $ const ())
11151216(* Helper to find project root *)
1317let with_root f =
···1923 | None ->
2024 Format.eprintf "Error: Not in an unpac project.@.";
2125 exit 1
2222- | Some root ->
2323- f ~env ~fs ~proc_mgr ~root
2626+ | Some root -> f ~env ~fs ~proc_mgr ~root
24272528(* Helper to get config path *)
2629let config_path root =
···3942 let path = config_path root in
4043 Unpac.Config.save_exn path config;
4144 let main_wt = Unpac.Worktree.path root Unpac.Worktree.Main in
4242- Unpac.Git.run_exn ~proc_mgr ~cwd:main_wt ["add"; "unpac.toml"] |> ignore;
4343- Unpac.Git.run_exn ~proc_mgr ~cwd:main_wt ["commit"; "-m"; msg] |> ignore
4545+ Unpac.Git.run_exn ~proc_mgr ~cwd:main_wt [ "add"; "unpac.toml" ] |> ignore;
4646+ Unpac.Git.run_exn ~proc_mgr ~cwd:main_wt [ "commit"; "-m"; msg ] |> ignore
44474548(* Check if string looks like a URL or path (vs a package name) *)
4649let is_url_or_path s =
4747- String.starts_with ~prefix:"http://" s ||
4848- String.starts_with ~prefix:"https://" s ||
4949- String.starts_with ~prefix:"git@" s ||
5050- String.starts_with ~prefix:"git://" s ||
5151- String.starts_with ~prefix:"ssh://" s ||
5252- String.starts_with ~prefix:"file://" s ||
5353- String.starts_with ~prefix:"/" s || (* Absolute path *)
5454- String.starts_with ~prefix:"./" s || (* Relative path *)
5555- String.starts_with ~prefix:"../" s || (* Relative path *)
5656- String.contains s ':' (* URL with scheme *)
5050+ String.starts_with ~prefix:"http://" s
5151+ || String.starts_with ~prefix:"https://" s
5252+ || String.starts_with ~prefix:"git@" s
5353+ || String.starts_with ~prefix:"git://" s
5454+ || String.starts_with ~prefix:"ssh://" s
5555+ || String.starts_with ~prefix:"file://" s
5656+ || String.starts_with ~prefix:"/" s
5757+ (* Absolute path *)
5858+ || String.starts_with ~prefix:"./" s
5959+ (* Relative path *)
6060+ || String.starts_with ~prefix:"../" s
6161+ ||
6262+ (* Relative path *)
6363+ String.contains s ':' (* URL with scheme *)
57645865(* Normalize a dev-repo URL for grouping comparison *)
5966let normalize_dev_repo url =
6067 let s = url in
6168 (* Strip git+ prefix *)
6262- let s = if String.starts_with ~prefix:"git+" s then
6363- String.sub s 4 (String.length s - 4) else s in
6969+ let s =
7070+ if String.starts_with ~prefix:"git+" s then
7171+ String.sub s 4 (String.length s - 4)
7272+ else s
7373+ in
6474 (* Strip trailing .git *)
6565- let s = if String.ends_with ~suffix:".git" s then
6666- String.sub s 0 (String.length s - 4) else s in
7575+ let s =
7676+ if String.ends_with ~suffix:".git" s then
7777+ String.sub s 0 (String.length s - 4)
7878+ else s
7979+ in
6780 (* Strip trailing slash *)
6868- let s = if String.ends_with ~suffix:"/" s then
6969- String.sub s 0 (String.length s - 1) else s in
8181+ let s =
8282+ if String.ends_with ~suffix:"/" s then String.sub s 0 (String.length s - 1)
8383+ else s
8484+ in
7085 (* Normalize github URLs: git@github.com:x/y -> https://github.com/x/y *)
7171- let s = if String.starts_with ~prefix:"git@github.com:" s then
7272- "https://github.com/" ^ String.sub s 15 (String.length s - 15) else s in
8686+ let s =
8787+ if String.starts_with ~prefix:"git@github.com:" s then
8888+ "https://github.com/" ^ String.sub s 15 (String.length s - 15)
8989+ else s
9090+ in
7391 String.lowercase_ascii s
74927593(* Group solved packages by their dev-repo *)
7694type package_group = {
7777- canonical_name : string; (* First package name, used as vendor name *)
7878- dev_repo : string; (* Original dev-repo URL *)
7979- packages : string list; (* All package names in this group *)
9595+ canonical_name : string; (* First package name, used as vendor name *)
9696+ dev_repo : string; (* Original dev-repo URL *)
9797+ packages : string list; (* All package names in this group *)
8098}
81998282-let group_packages_by_dev_repo ~config (pkgs : OpamPackage.t list) : package_group list =
100100+let group_packages_by_dev_repo ~config (pkgs : OpamPackage.t list) :
101101+ package_group list =
83102 let repos = config.Unpac.Config.opam.repositories in
84103 (* Build a map from normalized dev-repo to package info *)
85104 let groups = Hashtbl.create 16 in
8686- List.iter (fun pkg ->
8787- let name = OpamPackage.Name.to_string (OpamPackage.name pkg) in
8888- let version = OpamPackage.Version.to_string (OpamPackage.version pkg) in
8989- match Unpac_opam.Repo.find_package ~repos ~name ~version () with
9090- | None -> () (* Skip packages not found *)
9191- | Some result ->
9292- match result.metadata.dev_repo with
9393- | None -> () (* Skip packages without dev-repo *)
9494- | Some dev_repo ->
9595- let key = normalize_dev_repo dev_repo in
9696- match Hashtbl.find_opt groups key with
9797- | None ->
9898- Hashtbl.add groups key (dev_repo, [name])
9999- | Some (orig_url, names) ->
100100- Hashtbl.replace groups key (orig_url, name :: names)
101101- ) pkgs;
105105+ List.iter
106106+ (fun pkg ->
107107+ let name = OpamPackage.Name.to_string (OpamPackage.name pkg) in
108108+ let version = OpamPackage.Version.to_string (OpamPackage.version pkg) in
109109+ match Unpac_opam.Repo.find_package ~repos ~name ~version () with
110110+ | None -> () (* Skip packages not found *)
111111+ | Some result -> (
112112+ match result.metadata.dev_repo with
113113+ | None -> () (* Skip packages without dev-repo *)
114114+ | Some dev_repo -> (
115115+ let key = normalize_dev_repo dev_repo in
116116+ match Hashtbl.find_opt groups key with
117117+ | None -> Hashtbl.add groups key (dev_repo, [ name ])
118118+ | Some (orig_url, names) ->
119119+ Hashtbl.replace groups key (orig_url, name :: names))))
120120+ pkgs;
102121 (* Convert to list of groups *)
103103- Hashtbl.fold (fun _key (dev_repo, names) acc ->
104104- let names = List.rev names in (* Preserve order *)
105105- let canonical_name = List.hd names in
106106- { canonical_name; dev_repo; packages = names } :: acc
107107- ) groups []
122122+ Hashtbl.fold
123123+ (fun _key (dev_repo, names) acc ->
124124+ let names = List.rev names in
125125+ (* Preserve order *)
126126+ let canonical_name = List.hd names in
127127+ { canonical_name; dev_repo; packages = names } :: acc)
128128+ groups []
108129 |> List.sort (fun a b -> String.compare a.canonical_name b.canonical_name)
109130110131(* Helper to resolve vendor cache *)
111132let resolve_cache ~proc_mgr ~fs ~config ~cli_cache =
112133 match Unpac.Config.resolve_vendor_cache ?cli_override:cli_cache config with
113134 | None -> None
114114- | Some path ->
115115- Some (Unpac.Vendor_cache.init ~proc_mgr ~fs ~path ())
135135+ | Some path -> Some (Unpac.Vendor_cache.init ~proc_mgr ~fs ~path ())
116136117137(* Init command *)
118138let init_cmd =
···129149 Format.printf "Initialized unpac project at %s@." path;
130150 Format.printf "@.Next steps:@.";
131151 Format.printf " cd %s@." path;
132132- Format.printf " unpac opam repo add <name> <path> # configure opam repository@.";
152152+ Format.printf
153153+ " unpac opam repo add <name> <path> # configure opam repository@.";
133154 Format.printf " unpac project new <name> # create a project@."
134155 in
135156 let info = Cmd.info "init" ~doc in
···148169 Format.printf "Created project %s@." name;
149170 Format.printf "@.Next steps:@.";
150171 Format.printf " unpac opam add <package> # vendor a package@.";
151151- Format.printf " unpac opam merge <package> %s # merge package into project@." name
172172+ Format.printf
173173+ " unpac opam merge <package> %s # merge package into project@." name
152174 in
153175 let info = Cmd.info "new" ~doc in
154176 Cmd.v info Term.(const run $ logging_term $ name_arg)
···168190let project_cmd =
169191 let doc = "Project management commands." in
170192 let info = Cmd.info "project" ~doc in
171171- Cmd.group info [project_new_cmd; project_list_cmd]
193193+ Cmd.group info [ project_new_cmd; project_list_cmd ]
172194173195(* Opam repo add command *)
174196let opam_repo_add_cmd =
···191213 end;
192214 (* Resolve to absolute path *)
193215 let abs_path =
194194- if Filename.is_relative path then
195195- Filename.concat (Sys.getcwd ()) path
216216+ if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path
196217 else path
197218 in
198219 (* Check path exists *)
···200221 Format.eprintf "Error: '%s' is not a valid directory@." abs_path;
201222 exit 1
202223 end;
203203- let repo : Unpac.Config.repo_config = {
204204- repo_name = name;
205205- source = Local abs_path;
206206- } in
224224+ let repo : Unpac.Config.repo_config =
225225+ { repo_name = name; source = Local abs_path }
226226+ in
207227 let config' = Unpac.Config.add_repo config repo in
208228 save_config ~proc_mgr root config' (Printf.sprintf "Add repository %s" name);
209229 Format.printf "Added repository %s at %s@." name abs_path;
210210- Format.printf "@.Next: unpac opam add <package> # vendor a package by name@."
230230+ Format.printf
231231+ "@.Next: unpac opam add <package> # vendor a package by name@."
211232 in
212233 let info = Cmd.info "add" ~doc in
213234 Cmd.v info Term.(const run $ logging_term $ name_arg $ path_arg)
···221242 if config.opam.repositories = [] then begin
222243 Format.printf "No repositories configured@.";
223244 Format.printf "@.Hint: unpac opam repo add <name> <path>@."
224224- end else
225225- List.iter (fun (r : Unpac.Config.repo_config) ->
226226- let path = match r.source with
227227- | Local p -> p
228228- | Remote u -> u
229229- in
230230- Format.printf "%s: %s@." r.repo_name path
231231- ) config.opam.repositories
245245+ end
246246+ else
247247+ List.iter
248248+ (fun (r : Unpac.Config.repo_config) ->
249249+ let path = match r.source with Local p -> p | Remote u -> u in
250250+ Format.printf "%s: %s@." r.repo_name path)
251251+ config.opam.repositories
232252 in
233253 let info = Cmd.info "list" ~doc in
234254 Cmd.v info Term.(const run $ logging_term)
···248268 exit 1
249269 end;
250270 let config' = Unpac.Config.remove_repo config name in
251251- save_config ~proc_mgr root config' (Printf.sprintf "Remove repository %s" name);
271271+ save_config ~proc_mgr root config'
272272+ (Printf.sprintf "Remove repository %s" name);
252273 Format.printf "Removed repository %s@." name
253274 in
254275 let info = Cmd.info "remove" ~doc in
···258279let opam_repo_cmd =
259280 let doc = "Manage opam repositories." in
260281 let info = Cmd.info "repo" ~doc in
261261- Cmd.group info [opam_repo_add_cmd; opam_repo_list_cmd; opam_repo_remove_cmd]
282282+ Cmd.group info [ opam_repo_add_cmd; opam_repo_list_cmd; opam_repo_remove_cmd ]
262283263284(* Opam config compiler command *)
264285let opam_config_compiler_cmd =
···271292 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
272293 let config = load_config root in
273294 match version_opt with
274274- | None ->
295295+ | None -> (
275296 (* Show current compiler *)
276276- (match Unpac.Config.get_compiler config with
277277- | Some v -> Format.printf "Compiler: %s@." v
278278- | None -> Format.printf "No compiler configured@.@.Hint: unpac opam config compiler 5.2.0@.")
297297+ match Unpac.Config.get_compiler config with
298298+ | Some v -> Format.printf "Compiler: %s@." v
299299+ | None ->
300300+ Format.printf
301301+ "No compiler configured@.@.Hint: unpac opam config compiler \
302302+ 5.2.0@.")
279303 | Some version ->
280304 (* Set compiler *)
281305 let config' = Unpac.Config.set_compiler config version in
282282- save_config ~proc_mgr root config' (Printf.sprintf "Set compiler to %s" version);
306306+ save_config ~proc_mgr root config'
307307+ (Printf.sprintf "Set compiler to %s" version);
283308 Format.printf "Compiler set to %s@." version
284309 in
285310 let info = Cmd.info "compiler" ~doc in
···289314let opam_config_cmd =
290315 let doc = "Configure opam settings." in
291316 let info = Cmd.info "config" ~doc in
292292- Cmd.group info [opam_config_compiler_cmd]
317317+ Cmd.group info [ opam_config_compiler_cmd ]
293318294319(* Opam add command - enhanced to support package names and dependency solving *)
295320let opam_add_cmd =
···300325 in
301326 let name_arg =
302327 let doc = "Override package name." in
303303- Arg.(value & opt (some string) None & info ["n"; "name"] ~docv:"NAME" ~doc)
328328+ Arg.(
329329+ value & opt (some string) None & info [ "n"; "name" ] ~docv:"NAME" ~doc)
304330 in
305331 let version_arg =
306332 let doc = "Package version (when adding by name)." in
307307- Arg.(value & opt (some string) None & info ["V"; "pkg-version"] ~docv:"VERSION" ~doc)
333333+ Arg.(
334334+ value
335335+ & opt (some string) None
336336+ & info [ "V"; "pkg-version" ] ~docv:"VERSION" ~doc)
308337 in
309338 let branch_arg =
310339 let doc = "Git branch to vendor (defaults to remote default)." in
311311- Arg.(value & opt (some string) None & info ["b"; "branch"] ~docv:"BRANCH" ~doc)
340340+ Arg.(
341341+ value
342342+ & opt (some string) None
343343+ & info [ "b"; "branch" ] ~docv:"BRANCH" ~doc)
312344 in
313345 let solve_arg =
314346 let doc = "Solve dependencies and vendor all required packages." in
315315- Arg.(value & flag & info ["solve"] ~doc)
347347+ Arg.(value & flag & info [ "solve" ] ~doc)
316348 in
317349 let cache_arg =
318318- let doc = "Path to vendor cache (overrides config and UNPAC_VENDOR_CACHE env var)." in
319319- Arg.(value & opt (some string) None & info ["cache"] ~docv:"PATH" ~doc)
350350+ let doc =
351351+ "Path to vendor cache (overrides config and UNPAC_VENDOR_CACHE env var)."
352352+ in
353353+ Arg.(value & opt (some string) None & info [ "cache" ] ~docv:"PATH" ~doc)
320354 in
321355 let run () pkg name_opt version_opt branch_opt solve cli_cache =
322356 with_root @@ fun ~env:_ ~fs ~proc_mgr ~root ->
···327361 (* Solve dependencies and add all packages *)
328362 let repos = config.opam.repositories in
329363 if repos = [] then begin
330330- Format.eprintf "No repositories configured. Add one with: unpac opam repo add <name> <path>@.";
364364+ Format.eprintf
365365+ "No repositories configured. Add one with: unpac opam repo add \
366366+ <name> <path>@.";
331367 exit 1
332368 end;
333333- let ocaml_version = match Unpac.Config.get_compiler config with
369369+ let ocaml_version =
370370+ match Unpac.Config.get_compiler config with
334371 | Some v -> v
335372 | None ->
336373 Format.eprintf "No compiler version configured.@.";
···338375 exit 1
339376 in
340377 (* Get repo paths *)
341341- let repo_paths = List.map (fun (r : Unpac.Config.repo_config) ->
342342- match r.source with
343343- | Unpac.Config.Local p -> p
344344- | Unpac.Config.Remote u -> u (* TODO: handle remote repos *)
345345- ) repos in
378378+ let repo_paths =
379379+ List.map
380380+ (fun (r : Unpac.Config.repo_config) ->
381381+ match r.source with
382382+ | Unpac.Config.Local p -> p
383383+ | Unpac.Config.Remote u -> u (* TODO: handle remote repos *))
384384+ repos
385385+ in
346386 Format.printf "Solving dependencies for %s...@." pkg;
347347- match Unpac_opam.Solver.solve ~repos:repo_paths ~ocaml_version ~packages:[pkg] with
387387+ match
388388+ Unpac_opam.Solver.solve ~repos:repo_paths ~ocaml_version
389389+ ~packages:[ pkg ]
390390+ with
348391 | Error msg ->
349392 Format.eprintf "Dependency solving failed:@.%s@." msg;
350393 exit 1
351394 | Ok result ->
352395 let pkgs = result.packages in
353396 Format.printf "Solution found: %d packages@." (List.length pkgs);
354354- List.iter (fun p ->
355355- Format.printf " %s.%s@."
356356- (OpamPackage.Name.to_string (OpamPackage.name p))
357357- (OpamPackage.Version.to_string (OpamPackage.version p))
358358- ) pkgs;
397397+ List.iter
398398+ (fun p ->
399399+ Format.printf " %s.%s@."
400400+ (OpamPackage.Name.to_string (OpamPackage.name p))
401401+ (OpamPackage.Version.to_string (OpamPackage.version p)))
402402+ pkgs;
359403360404 (* Group packages by dev-repo to avoid duplicating sources *)
361405 let groups = group_packages_by_dev_repo ~config pkgs in
362362- Format.printf "@.Grouped into %d unique repositories:@." (List.length groups);
363363- List.iter (fun (g : package_group) ->
364364- if List.length g.packages > 1 then
365365- Format.printf " %s (%d packages: %s)@."
366366- g.canonical_name
367367- (List.length g.packages)
368368- (String.concat ", " g.packages)
369369- else
370370- Format.printf " %s@." g.canonical_name
371371- ) groups;
406406+ Format.printf "@.Grouped into %d unique repositories:@."
407407+ (List.length groups);
408408+ List.iter
409409+ (fun (g : package_group) ->
410410+ if List.length g.packages > 1 then
411411+ Format.printf " %s (%d packages: %s)@." g.canonical_name
412412+ (List.length g.packages)
413413+ (String.concat ", " g.packages)
414414+ else Format.printf " %s@." g.canonical_name)
415415+ groups;
372416373417 Format.printf "@.Vendoring repositories...@.";
374418 let added = ref 0 in
375419 let failed = ref 0 in
376376- List.iter (fun (g : package_group) ->
377377- (* Use canonical name as vendor name, dev-repo as URL *)
378378- let url = if String.starts_with ~prefix:"git+" g.dev_repo then
379379- String.sub g.dev_repo 4 (String.length g.dev_repo - 4)
380380- else g.dev_repo in
381381- let info : Unpac.Backend.package_info = {
382382- name = g.canonical_name;
383383- url;
384384- branch = None;
385385- } in
386386- match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with
387387- | Unpac.Backend.Added { name = pkg_name; sha } ->
388388- Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7);
389389- if List.length g.packages > 1 then
390390- Format.printf " Contains: %s@." (String.concat ", " g.packages);
391391- incr added
392392- | Unpac.Backend.Already_exists pkg_name ->
393393- Format.printf "Package %s already vendored@." pkg_name
394394- | Unpac.Backend.Failed { name = pkg_name; error } ->
395395- Format.eprintf "Error adding %s: %s@." pkg_name error;
396396- incr failed
397397- ) groups;
398398- Format.printf "@.Done: %d repositories added, %d failed@." !added !failed;
420420+ List.iter
421421+ (fun (g : package_group) ->
422422+ (* Use canonical name as vendor name, dev-repo as URL *)
423423+ let url =
424424+ if String.starts_with ~prefix:"git+" g.dev_repo then
425425+ String.sub g.dev_repo 4 (String.length g.dev_repo - 4)
426426+ else g.dev_repo
427427+ in
428428+ let info : Unpac.Backend.package_info =
429429+ { name = g.canonical_name; url; branch = None }
430430+ in
431431+ match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with
432432+ | Unpac.Backend.Added { name = pkg_name; sha } ->
433433+ Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7);
434434+ if List.length g.packages > 1 then
435435+ Format.printf " Contains: %s@."
436436+ (String.concat ", " g.packages);
437437+ incr added
438438+ | Unpac.Backend.Already_exists pkg_name ->
439439+ Format.printf "Package %s already vendored@." pkg_name
440440+ | Unpac.Backend.Failed { name = pkg_name; error } ->
441441+ Format.eprintf "Error adding %s: %s@." pkg_name error;
442442+ incr failed)
443443+ groups;
444444+ Format.printf "@.Done: %d repositories added, %d failed@." !added
445445+ !failed;
399446 if !failed > 0 then exit 1
400400- end else begin
447447+ end
448448+ else begin
401449 (* Single package mode *)
402450 let url, name =
403451 if is_url_or_path pkg then begin
404452 (* It's a URL *)
405405- let n = match name_opt with
453453+ let n =
454454+ match name_opt with
406455 | Some n -> n
407456 | None ->
408457 let base = Filename.basename pkg in
···411460 else base
412461 in
413462 (pkg, n)
414414- end else begin
463463+ end
464464+ else begin
415465 (* It's a package name - look up in repositories *)
416466 let repos = config.opam.repositories in
417467 if repos = [] then begin
418418- Format.eprintf "No repositories configured. Add one with: unpac opam repo add <name> <path>@.";
468468+ Format.eprintf
469469+ "No repositories configured. Add one with: unpac opam repo add \
470470+ <name> <path>@.";
419471 exit 1
420472 end;
421421- match Unpac_opam.Repo.find_package ~repos ~name:pkg ?version:version_opt () with
473473+ match
474474+ Unpac_opam.Repo.find_package ~repos ~name:pkg ?version:version_opt
475475+ ()
476476+ with
422477 | None ->
423423- Format.eprintf "Package '%s' not found in configured repositories@." pkg;
478478+ Format.eprintf
479479+ "Package '%s' not found in configured repositories@." pkg;
424480 exit 1
425425- | Some result ->
481481+ | Some result -> (
426482 match result.metadata.dev_repo with
427483 | None ->
428484 Format.eprintf "Package '%s' has no dev-repo field@." pkg;
429485 exit 1
430486 | Some dev_repo ->
431487 (* Strip git+ prefix if present (opam dev-repo format) *)
432432- let url = if String.starts_with ~prefix:"git+" dev_repo then
433433- String.sub dev_repo 4 (String.length dev_repo - 4)
434434- else dev_repo in
488488+ let url =
489489+ if String.starts_with ~prefix:"git+" dev_repo then
490490+ String.sub dev_repo 4 (String.length dev_repo - 4)
491491+ else dev_repo
492492+ in
435493 let n = match name_opt with Some n -> n | None -> pkg in
436436- (url, n)
494494+ (url, n))
437495 end
438496 in
439497440440- let info : Unpac.Backend.package_info = {
441441- name;
442442- url;
443443- branch = branch_opt;
444444- } in
498498+ let info : Unpac.Backend.package_info =
499499+ { name; url; branch = branch_opt }
500500+ in
445501 match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with
446502 | Unpac.Backend.Added { name = pkg_name; sha } ->
447503 Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7);
448504 Format.printf "@.Next steps:@.";
449449- Format.printf " unpac opam edit %s # make local changes@." pkg_name;
450450- Format.printf " unpac opam merge %s <project> # merge into a project@." pkg_name
505505+ Format.printf
506506+ " unpac opam edit %s # make local changes@." pkg_name;
507507+ Format.printf
508508+ " unpac opam merge %s <project> # merge into a project@."
509509+ pkg_name
451510 | Unpac.Backend.Already_exists name ->
452511 Format.printf "Package %s already vendored@." name
453512 | Unpac.Backend.Failed { name; error } ->
···456515 end
457516 in
458517 let info = Cmd.info "add" ~doc in
459459- Cmd.v info Term.(const run $ logging_term $ pkg_arg $ name_arg $ version_arg $ branch_arg $ solve_arg $ cache_arg)
518518+ Cmd.v info
519519+ Term.(
520520+ const run $ logging_term $ pkg_arg $ name_arg $ version_arg $ branch_arg
521521+ $ solve_arg $ cache_arg)
460522461523(* Opam list command *)
462524let opam_list_cmd =
···467529 if packages = [] then begin
468530 Format.printf "No packages vendored@.";
469531 Format.printf "@.Hint: unpac opam add <package>@."
470470- end else
471471- List.iter (Format.printf "%s@.") packages
532532+ end
533533+ else List.iter (Format.printf "%s@.") packages
472534 in
473535 let info = Cmd.info "list" ~doc in
474536 Cmd.v info Term.(const run $ logging_term)
475537476538(* Opam edit command *)
477539let opam_edit_cmd =
478478- let doc = "Open a package's patches worktree for editing. \
479479- Also creates a vendor worktree for reference." in
540540+ let doc =
541541+ "Open a package's patches worktree for editing. Also creates a vendor \
542542+ worktree for reference."
543543+ in
480544 let pkg_arg =
481545 let doc = "Package name to edit." in
482546 Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
···492556 (* Ensure both patches and vendor worktrees exist *)
493557 Unpac.Worktree.ensure ~proc_mgr root (Unpac.Worktree.Opam_patches pkg);
494558 Unpac.Worktree.ensure ~proc_mgr root (Unpac.Worktree.Opam_vendor pkg);
495495- let patches_path = snd (Unpac.Worktree.path root (Unpac.Worktree.Opam_patches pkg)) in
496496- let vendor_path = snd (Unpac.Worktree.path root (Unpac.Worktree.Opam_vendor pkg)) in
559559+ let patches_path =
560560+ snd (Unpac.Worktree.path root (Unpac.Worktree.Opam_patches pkg))
561561+ in
562562+ let vendor_path =
563563+ snd (Unpac.Worktree.path root (Unpac.Worktree.Opam_vendor pkg))
564564+ in
497565 Format.printf "Editing %s@." pkg;
498566 Format.printf "@.";
499567 Format.printf "Worktrees created:@.";
···526594 end;
527595 (* Check for uncommitted changes in patches worktree *)
528596 let wt_path = Unpac.Worktree.path root patches_kind in
529529- let status = Unpac.Git.run_exn ~proc_mgr ~cwd:wt_path ["status"; "--porcelain"] in
597597+ let status =
598598+ Unpac.Git.run_exn ~proc_mgr ~cwd:wt_path [ "status"; "--porcelain" ]
599599+ in
530600 if String.trim status <> "" then begin
531601 Format.eprintf "Warning: uncommitted changes in %s@." pkg;
532602 Format.eprintf "Commit or discard them before closing.@.";
···539609 Format.printf "Closed editing session for %s@." pkg;
540610 Format.printf "@.Next steps:@.";
541611 Format.printf " unpac opam diff %s # view your changes@." pkg;
542542- Format.printf " unpac opam merge %s <project> # merge into a project@." pkg
612612+ Format.printf " unpac opam merge %s <project> # merge into a project@."
613613+ pkg
543614 in
544615 let info = Cmd.info "done" ~doc in
545616 Cmd.v info Term.(const run $ logging_term $ pkg_arg)
···555626 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
556627 match Unpac_opam.Opam.update_package ~proc_mgr ~root name with
557628 | Unpac.Backend.Updated { name = pkg_name; old_sha; new_sha } ->
558558- Format.printf "Updated %s: %s -> %s@." pkg_name
559559- (String.sub old_sha 0 7) (String.sub new_sha 0 7);
629629+ Format.printf "Updated %s: %s -> %s@." pkg_name (String.sub old_sha 0 7)
630630+ (String.sub new_sha 0 7);
560631 Format.printf "@.Next steps:@.";
561561- Format.printf " unpac opam diff %s # view changes@." pkg_name;
562562- Format.printf " unpac opam merge %s <project> # merge into a project@." pkg_name
563563- | Unpac.Backend.No_changes name ->
564564- Format.printf "%s is up to date@." name
632632+ Format.printf " unpac opam diff %s # view changes@."
633633+ pkg_name;
634634+ Format.printf
635635+ " unpac opam merge %s <project> # merge into a project@." pkg_name
636636+ | Unpac.Backend.No_changes name -> Format.printf "%s is up to date@." name
565637 | Unpac.Backend.Update_failed { name; error } ->
566638 Format.eprintf "Error updating %s: %s@." name error;
567639 exit 1
···571643572644(* Opam merge command *)
573645let opam_merge_cmd =
574574- let doc = "Merge vendored opam packages into a project. \
575575- Use --solve to merge a package and its dependencies, \
576576- or --all to merge all vendored packages." in
646646+ let doc =
647647+ "Merge vendored opam packages into a project. Use --solve to merge a \
648648+ package and its dependencies, or --all to merge all vendored packages."
649649+ in
577650 let args =
578651 let doc = "PACKAGE PROJECT (or just PROJECT with --all)." in
579652 Arg.(value & pos_all string [] & info [] ~docv:"ARGS" ~doc)
580653 in
581654 let all_flag =
582655 let doc = "Merge all vendored packages into the project." in
583583- Arg.(value & flag & info ["all"] ~doc)
656656+ Arg.(value & flag & info [ "all" ] ~doc)
584657 in
585658 let solve_flag =
586586- let doc = "Solve dependencies for PACKAGE and merge all solved packages into the project." in
587587- Arg.(value & flag & info ["solve"] ~doc)
659659+ let doc =
660660+ "Solve dependencies for PACKAGE and merge all solved packages into the \
661661+ project."
662662+ in
663663+ Arg.(value & flag & info [ "solve" ] ~doc)
588664 in
589665 let run () args all solve =
590666 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
···592668593669 let merge_one ~project pkg =
594670 let patches_branch = Unpac_opam.Opam.patches_branch pkg in
595595- match Unpac.Backend.merge_to_project ~proc_mgr ~root ~project ~patches_branch with
671671+ match
672672+ Unpac.Backend.merge_to_project ~proc_mgr ~root ~project ~patches_branch
673673+ with
596674 | Ok () ->
597675 Format.printf "Merged %s@." pkg;
598676 true
···603681 in
604682605683 let merge_packages packages project =
606606- Format.printf "Merging %d packages into project %s...@." (List.length packages) project;
607607- let (successes, failures) = List.fold_left (fun (s, f) pkg ->
608608- if merge_one ~project pkg then (s + 1, f) else (s, f + 1)
609609- ) (0, 0) packages in
684684+ Format.printf "Merging %d packages into project %s...@."
685685+ (List.length packages) project;
686686+ let successes, failures =
687687+ List.fold_left
688688+ (fun (s, f) pkg ->
689689+ if merge_one ~project pkg then (s + 1, f) else (s, f + 1))
690690+ (0, 0) packages
691691+ in
610692 Format.printf "@.Done: %d merged" successes;
611693 if failures > 0 then Format.printf ", %d had conflicts" failures;
612694 Format.printf "@.";
613695 if failures > 0 then begin
614696 Format.eprintf "Resolve conflicts in project/%s and commit.@." project;
615697 exit 1
616616- end else
617617- Format.printf "Next: Build your project in project/%s@." project
698698+ end
699699+ else Format.printf "Next: Build your project in project/%s@." project
618700 in
619701620702 if solve then begin
621703 (* Solve dependencies and merge all solved packages that are vendored *)
622622- let pkg, project = match args with
623623- | [pkg; project] -> pkg, project
704704+ let pkg, project =
705705+ match args with
706706+ | [ pkg; project ] -> (pkg, project)
624707 | _ ->
625708 Format.eprintf "Usage: unpac opam merge --solve PACKAGE PROJECT@.";
626709 exit 1
627710 in
628711 let repos = config.opam.repositories in
629712 if repos = [] then begin
630630- Format.eprintf "No repositories configured. Add one with: unpac opam repo add <name> <path>@.";
713713+ Format.eprintf
714714+ "No repositories configured. Add one with: unpac opam repo add \
715715+ <name> <path>@.";
631716 exit 1
632717 end;
633633- let ocaml_version = match Unpac.Config.get_compiler config with
718718+ let ocaml_version =
719719+ match Unpac.Config.get_compiler config with
634720 | Some v -> v
635721 | None ->
636722 Format.eprintf "No compiler version configured.@.";
637723 Format.eprintf "Set one with: unpac opam config compiler 5.2.0@.";
638724 exit 1
639725 in
640640- let repo_paths = List.map (fun (r : Unpac.Config.repo_config) ->
641641- match r.source with
642642- | Unpac.Config.Local p -> p
643643- | Unpac.Config.Remote u -> u
644644- ) repos in
726726+ let repo_paths =
727727+ List.map
728728+ (fun (r : Unpac.Config.repo_config) ->
729729+ match r.source with
730730+ | Unpac.Config.Local p -> p
731731+ | Unpac.Config.Remote u -> u)
732732+ repos
733733+ in
645734 Format.printf "Solving dependencies for %s...@." pkg;
646646- match Unpac_opam.Solver.solve ~repos:repo_paths ~ocaml_version ~packages:[pkg] with
735735+ match
736736+ Unpac_opam.Solver.solve ~repos:repo_paths ~ocaml_version
737737+ ~packages:[ pkg ]
738738+ with
647739 | Error msg ->
648740 Format.eprintf "Dependency solving failed:@.%s@." msg;
649741 exit 1
650742 | Ok result ->
651743 (* Group by dev-repo to get canonical names *)
652744 let groups = group_packages_by_dev_repo ~config result.packages in
653653- let canonical_names = List.map (fun (g : package_group) -> g.canonical_name) groups in
745745+ let canonical_names =
746746+ List.map (fun (g : package_group) -> g.canonical_name) groups
747747+ in
654748 (* Filter to only vendored packages *)
655749 let vendored = Unpac_opam.Opam.list_packages ~proc_mgr ~root in
656656- let to_merge = List.filter (fun name -> List.mem name vendored) canonical_names in
750750+ let to_merge =
751751+ List.filter (fun name -> List.mem name vendored) canonical_names
752752+ in
657753 if to_merge = [] then begin
658658- Format.eprintf "No vendored packages match the solved dependencies.@.";
659659- Format.eprintf "Run 'unpac opam add %s --solve' first to vendor them.@." pkg;
754754+ Format.eprintf
755755+ "No vendored packages match the solved dependencies.@.";
756756+ Format.eprintf
757757+ "Run 'unpac opam add %s --solve' first to vendor them.@." pkg;
660758 exit 1
661759 end;
662662- Format.printf "Found %d vendored packages to merge.@.@." (List.length to_merge);
760760+ Format.printf "Found %d vendored packages to merge.@.@."
761761+ (List.length to_merge);
663762 merge_packages to_merge project
664664- end else if all then begin
763763+ end
764764+ else if all then begin
665765 (* Merge all vendored packages *)
666666- let project = match args with
667667- | [project] -> project
766766+ let project =
767767+ match args with
768768+ | [ project ] -> project
668769 | _ ->
669770 Format.eprintf "Usage: unpac opam merge --all PROJECT@.";
670771 exit 1
···675776 exit 1
676777 end;
677778 merge_packages packages project
678678- end else begin
779779+ end
780780+ else begin
679781 (* Single package mode *)
680680- let pkg, project = match args with
681681- | [pkg; project] -> pkg, project
782782+ let pkg, project =
783783+ match args with
784784+ | [ pkg; project ] -> (pkg, project)
682785 | _ ->
683786 Format.eprintf "Usage: unpac opam merge PACKAGE PROJECT@.";
684787 exit 1
···714817 let remote = "origin-" ^ pkg in
715818 let url = Unpac.Git.remote_url ~proc_mgr ~cwd:git remote in
716819 Format.printf "Package: %s@." pkg;
717717- (match url with
718718- | Some u -> Format.printf "URL: %s@." u
719719- | None -> ());
820820+ (match url with Some u -> Format.printf "URL: %s@." u | None -> ());
720821 (* Get branch SHAs *)
721822 let upstream = Unpac_opam.Opam.upstream_branch pkg in
722823 let vendor = Unpac_opam.Opam.vendor_branch pkg in
723824 let patches = Unpac_opam.Opam.patches_branch pkg in
724825 (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git upstream with
725725- | Some sha -> Format.printf "Upstream: %s@." (String.sub sha 0 7)
726726- | None -> ());
826826+ | Some sha -> Format.printf "Upstream: %s@." (String.sub sha 0 7)
827827+ | None -> ());
727828 (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git vendor with
728728- | Some sha -> Format.printf "Vendor: %s@." (String.sub sha 0 7)
729729- | None -> ());
829829+ | Some sha -> Format.printf "Vendor: %s@." (String.sub sha 0 7)
830830+ | None -> ());
730831 (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git patches with
731731- | Some sha -> Format.printf "Patches: %s@." (String.sub sha 0 7)
732732- | None -> ());
832832+ | Some sha -> Format.printf "Patches: %s@." (String.sub sha 0 7)
833833+ | None -> ());
733834 (* Count commits ahead *)
734734- let log_output = Unpac.Git.run_exn ~proc_mgr ~cwd:git
735735- ["log"; "--oneline"; vendor ^ ".." ^ patches] in
736736- let commits = List.length (String.split_on_char '\n' log_output |>
737737- List.filter (fun s -> String.trim s <> "")) in
835835+ let log_output =
836836+ Unpac.Git.run_exn ~proc_mgr ~cwd:git
837837+ [ "log"; "--oneline"; vendor ^ ".." ^ patches ]
838838+ in
839839+ let commits =
840840+ List.length
841841+ (String.split_on_char '\n' log_output
842842+ |> List.filter (fun s -> String.trim s <> ""))
843843+ in
738844 Format.printf "Local commits: %d@." commits;
739845 Format.printf "@.Commands:@.";
740846 Format.printf " unpac opam diff %s # view local changes@." pkg;
···762868 end;
763869 let vendor = Unpac_opam.Opam.vendor_branch pkg in
764870 let patches = Unpac_opam.Opam.patches_branch pkg in
765765- let diff = Unpac.Git.run_exn ~proc_mgr ~cwd:git
766766- ["diff"; vendor; patches] in
871871+ let diff =
872872+ Unpac.Git.run_exn ~proc_mgr ~cwd:git [ "diff"; vendor; patches ]
873873+ in
767874 if String.trim diff = "" then begin
768875 Format.printf "No local changes@.";
769876 Format.printf "@.Hint: unpac opam edit %s # to make changes@." pkg
770770- end else begin
877877+ end
878878+ else begin
771879 print_string diff;
772880 Format.printf "@.Next: unpac opam merge %s <project>@." pkg
773881 end
···792900 exit 1
793901 end;
794902 (* Remove worktrees if exist *)
795795- (try Unpac.Worktree.remove_force ~proc_mgr root (Unpac.Worktree.Opam_upstream pkg) with _ -> ());
796796- (try Unpac.Worktree.remove_force ~proc_mgr root (Unpac.Worktree.Opam_vendor pkg) with _ -> ());
797797- (try Unpac.Worktree.remove_force ~proc_mgr root (Unpac.Worktree.Opam_patches pkg) with _ -> ());
903903+ (try
904904+ Unpac.Worktree.remove_force ~proc_mgr root
905905+ (Unpac.Worktree.Opam_upstream pkg)
906906+ with _ -> ());
907907+ (try
908908+ Unpac.Worktree.remove_force ~proc_mgr root
909909+ (Unpac.Worktree.Opam_vendor pkg)
910910+ with _ -> ());
911911+ (try
912912+ Unpac.Worktree.remove_force ~proc_mgr root
913913+ (Unpac.Worktree.Opam_patches pkg)
914914+ with _ -> ());
798915 (* Delete branches *)
799916 let upstream = Unpac_opam.Opam.upstream_branch pkg in
800917 let vendor = Unpac_opam.Opam.vendor_branch pkg in
801918 let patches = Unpac_opam.Opam.patches_branch pkg in
802802- (try Unpac.Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; upstream] |> ignore with _ -> ());
803803- (try Unpac.Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; vendor] |> ignore with _ -> ());
804804- (try Unpac.Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; patches] |> ignore with _ -> ());
919919+ (try
920920+ Unpac.Git.run_exn ~proc_mgr ~cwd:git [ "branch"; "-D"; upstream ]
921921+ |> ignore
922922+ with _ -> ());
923923+ (try
924924+ Unpac.Git.run_exn ~proc_mgr ~cwd:git [ "branch"; "-D"; vendor ] |> ignore
925925+ with _ -> ());
926926+ (try
927927+ Unpac.Git.run_exn ~proc_mgr ~cwd:git [ "branch"; "-D"; patches ]
928928+ |> ignore
929929+ with _ -> ());
805930 (* Remove remote *)
806931 let remote = "origin-" ^ pkg in
807807- (try Unpac.Git.run_exn ~proc_mgr ~cwd:git ["remote"; "remove"; remote] |> ignore with _ -> ());
932932+ (try
933933+ Unpac.Git.run_exn ~proc_mgr ~cwd:git [ "remote"; "remove"; remote ]
934934+ |> ignore
935935+ with _ -> ());
808936 Format.printf "Removed %s@." pkg;
809809- Format.printf "@.Hint: unpac opam add <package> # to add another package@."
937937+ Format.printf
938938+ "@.Hint: unpac opam add <package> # to add another package@."
810939 in
811940 let info = Cmd.info "remove" ~doc in
812941 Cmd.v info Term.(const run $ logging_term $ pkg_arg)
···815944let opam_cmd =
816945 let doc = "Opam package vendoring commands." in
817946 let info = Cmd.info "opam" ~doc in
818818- Cmd.group info [
819819- opam_repo_cmd;
820820- opam_config_cmd;
821821- opam_add_cmd;
822822- opam_list_cmd;
823823- opam_edit_cmd;
824824- opam_done_cmd;
825825- opam_update_cmd;
826826- opam_merge_cmd;
827827- opam_info_cmd;
828828- opam_diff_cmd;
829829- opam_remove_cmd;
830830- ]
947947+ Cmd.group info
948948+ [
949949+ opam_repo_cmd;
950950+ opam_config_cmd;
951951+ opam_add_cmd;
952952+ opam_list_cmd;
953953+ opam_edit_cmd;
954954+ opam_done_cmd;
955955+ opam_update_cmd;
956956+ opam_merge_cmd;
957957+ opam_info_cmd;
958958+ opam_diff_cmd;
959959+ opam_remove_cmd;
960960+ ]
831961832962(* Push command - push all unpac branches to a remote *)
833963let push_cmd =
···838968 in
839969 let force_arg =
840970 let doc = "Force push (use with caution)." in
841841- Arg.(value & flag & info ["f"; "force"] ~doc)
971971+ Arg.(value & flag & info [ "f"; "force" ] ~doc)
842972 in
843973 let dry_run_arg =
844974 let doc = "Show what would be pushed without actually pushing." in
845845- Arg.(value & flag & info ["n"; "dry-run"] ~doc)
975975+ Arg.(value & flag & info [ "n"; "dry-run" ] ~doc)
846976 in
847977 let run () remote force dry_run =
848978 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
···850980851981 (* Check if remote exists *)
852982 (match Unpac.Git.remote_url ~proc_mgr ~cwd:git remote with
853853- | None ->
854854- Format.eprintf "Remote '%s' not configured.@." remote;
855855- Format.eprintf "Add it with: git -C %s remote add %s <url>@." (snd git) remote;
856856- exit 1
857857- | Some _ -> ());
983983+ | None ->
984984+ Format.eprintf "Remote '%s' not configured.@." remote;
985985+ Format.eprintf "Add it with: git -C %s remote add %s <url>@." (snd git)
986986+ remote;
987987+ exit 1
988988+ | Some _ -> ());
858989859990 (* Get all branches *)
860860- let all_branches = Unpac.Git.run_lines ~proc_mgr ~cwd:git ["branch"; "--format=%(refname:short)"] in
991991+ let all_branches =
992992+ Unpac.Git.run_lines ~proc_mgr ~cwd:git
993993+ [ "branch"; "--format=%(refname:short)" ]
994994+ in
861995862996 (* Filter to only unpac-managed branches *)
863863- let unpac_branches = List.filter (fun b ->
864864- b = "main" ||
865865- String.starts_with ~prefix:"opam/" b ||
866866- String.starts_with ~prefix:"project/" b
867867- ) all_branches in
997997+ let unpac_branches =
998998+ List.filter
999999+ (fun b ->
10001000+ b = "main"
10011001+ || String.starts_with ~prefix:"opam/" b
10021002+ || String.starts_with ~prefix:"project/" b)
10031003+ all_branches
10041004+ in
86810058691006 if unpac_branches = [] then begin
8701007 Format.printf "No branches to push@.";
···87710148781015 if dry_run then begin
8791016 Format.printf "(dry run - no changes made)@."
880880- end else begin
10171017+ end
10181018+ else begin
8811019 (* Build push command *)
882882- let force_flag = if force then ["--force"] else [] in
883883- let push_args = ["push"] @ force_flag @ [remote; "--"] @ unpac_branches in
10201020+ let force_flag = if force then [ "--force" ] else [] in
10211021+ let push_args =
10221022+ [ "push" ] @ force_flag @ [ remote; "--" ] @ unpac_branches
10231023+ in
88410248851025 Format.printf "Pushing %d branches...@." (List.length unpac_branches);
8861026 try
···8921032 end
8931033 in
8941034 let info = Cmd.info "push" ~doc in
895895- Cmd.v info Term.(const run $ logging_term $ remote_arg $ force_arg $ dry_run_arg)
10351035+ Cmd.v info
10361036+ Term.(const run $ logging_term $ remote_arg $ force_arg $ dry_run_arg)
89610378971038(* Vendor status command *)
8981039let vendor_status_cmd =
···9091050 end;
91010519111052 (* Get all project branches *)
912912- let all_branches = Unpac.Git.run_lines ~proc_mgr ~cwd:git
913913- ["branch"; "--format=%(refname:short)"] in
914914- let project_branches = List.filter (fun b ->
915915- String.starts_with ~prefix:"project/" b
916916- ) all_branches in
917917- let project_names = List.map (fun b ->
918918- String.sub b 8 (String.length b - 8) (* Remove "project/" prefix *)
919919- ) project_branches in
10531053+ let all_branches =
10541054+ Unpac.Git.run_lines ~proc_mgr ~cwd:git
10551055+ [ "branch"; "--format=%(refname:short)" ]
10561056+ in
10571057+ let project_branches =
10581058+ List.filter
10591059+ (fun b -> String.starts_with ~prefix:"project/" b)
10601060+ all_branches
10611061+ in
10621062+ let project_names =
10631063+ List.map
10641064+ (fun b ->
10651065+ String.sub b 8 (String.length b - 8) (* Remove "project/" prefix *))
10661066+ project_branches
10671067+ in
92010689211069 (* Print header *)
9221070 Format.printf "%-25s %8s %s@." "Package" "Patches" "Merged into";
9231071 Format.printf "%s@." (String.make 70 '-');
92410729251073 (* For each package, get patch count and merge status *)
926926- List.iter (fun pkg ->
927927- let vendor_branch = Unpac_opam.Opam.vendor_branch pkg in
928928- let patches_branch = Unpac_opam.Opam.patches_branch pkg in
10741074+ List.iter
10751075+ (fun pkg ->
10761076+ let vendor_branch = Unpac_opam.Opam.vendor_branch pkg in
10771077+ let patches_branch = Unpac_opam.Opam.patches_branch pkg in
9291078930930- (* Count commits on patches that aren't on vendor *)
931931- let patch_count =
932932- let output = Unpac.Git.run_exn ~proc_mgr ~cwd:git
933933- ["rev-list"; "--count"; vendor_branch ^ ".." ^ patches_branch] in
934934- int_of_string (String.trim output)
935935- in
10791079+ (* Count commits on patches that aren't on vendor *)
10801080+ let patch_count =
10811081+ let output =
10821082+ Unpac.Git.run_exn ~proc_mgr ~cwd:git
10831083+ [ "rev-list"; "--count"; vendor_branch ^ ".." ^ patches_branch ]
10841084+ in
10851085+ int_of_string (String.trim output)
10861086+ in
9361087937937- (* Check which projects contain this package's patches *)
938938- let merged_into = List.filter (fun proj_name ->
939939- let proj_branch = "project/" ^ proj_name in
940940- (* Check if patches branch is an ancestor of project branch *)
941941- match Unpac.Git.run ~proc_mgr ~cwd:git
942942- ["merge-base"; "--is-ancestor"; patches_branch; proj_branch] with
943943- | Ok _ -> true
944944- | Error _ -> false
945945- ) project_names in
10881088+ (* Check which projects contain this package's patches *)
10891089+ let merged_into =
10901090+ List.filter
10911091+ (fun proj_name ->
10921092+ let proj_branch = "project/" ^ proj_name in
10931093+ (* Check if patches branch is an ancestor of project branch *)
10941094+ match
10951095+ Unpac.Git.run ~proc_mgr ~cwd:git
10961096+ [ "merge-base"; "--is-ancestor"; patches_branch; proj_branch ]
10971097+ with
10981098+ | Ok _ -> true
10991099+ | Error _ -> false)
11001100+ project_names
11011101+ in
9461102947947- let merged_str = if merged_into = [] then "-"
948948- else String.concat ", " merged_into in
11031103+ let merged_str =
11041104+ if merged_into = [] then "-" else String.concat ", " merged_into
11051105+ in
9491106950950- Format.printf "%-25s %8d %s@." pkg patch_count merged_str
951951- ) packages;
11071107+ Format.printf "%-25s %8d %s@." pkg patch_count merged_str)
11081108+ packages;
95211099531110 Format.printf "@.Total: %d packages@." (List.length packages)
9541111 in
···9591116let vendor_cmd =
9601117 let doc = "Vendor status and management commands." in
9611118 let info = Cmd.info "vendor" ~doc in
962962- Cmd.group info [vendor_status_cmd]
11191119+ Cmd.group info [ vendor_status_cmd ]
96311209641121(* Main command *)
9651122let main_cmd =
9661123 let doc = "Multi-backend vendoring tool using git worktrees." in
9671124 let info = Cmd.info "unpac" ~version:"0.1.0" ~doc in
968968- Cmd.group info [init_cmd; project_cmd; opam_cmd; vendor_cmd; push_cmd]
11251125+ Cmd.group info [ init_cmd; project_cmd; opam_cmd; vendor_cmd; push_cmd ]
96911269701127let () = exit (Cmd.eval main_cmd)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Git operations wrapped with Eio and robust error handling. *)
2738let src = Logs.Src.create "unpac.git" ~doc:"Git operations"
99+410module Log = (val Logs.src_log src : Logs.LOG)
511612(* Error types *)
···2632let pp_error fmt = function
2733 | Command_failed { cmd; exit_code; stderr; _ } ->
2834 Format.fprintf fmt "git %a failed (exit %d): %s"
2929- Fmt.(list ~sep:sp string) cmd exit_code
3030- (String.trim stderr)
3131- | Not_a_repository ->
3232- Format.fprintf fmt "not a git repository"
3333- | Remote_exists name ->
3434- Format.fprintf fmt "remote '%s' already exists" name
3535- | Remote_not_found name ->
3636- Format.fprintf fmt "remote '%s' not found" name
3737- | Branch_exists name ->
3838- Format.fprintf fmt "branch '%s' already exists" name
3939- | Branch_not_found name ->
4040- Format.fprintf fmt "branch '%s' not found" name
3535+ Fmt.(list ~sep:sp string)
3636+ cmd exit_code (String.trim stderr)
3737+ | Not_a_repository -> Format.fprintf fmt "not a git repository"
3838+ | Remote_exists name -> Format.fprintf fmt "remote '%s' already exists" name
3939+ | Remote_not_found name -> Format.fprintf fmt "remote '%s' not found" name
4040+ | Branch_exists name -> Format.fprintf fmt "branch '%s' already exists" name
4141+ | Branch_not_found name -> Format.fprintf fmt "branch '%s' not found" name
4142 | Merge_conflict { branch; conflicting_files } ->
4243 Format.fprintf fmt "merge conflict in '%s': %a" branch
4343- Fmt.(list ~sep:comma string) conflicting_files
4444+ Fmt.(list ~sep:comma string)
4545+ conflicting_files
4446 | Rebase_conflict { onto; hint } ->
4547 Format.fprintf fmt "rebase conflict onto '%s': %s" onto hint
4648 | Uncommitted_changes ->
4749 Format.fprintf fmt "uncommitted changes in working directory"
4848- | Not_on_branch ->
4949- Format.fprintf fmt "not on any branch"
5050- | Detached_head ->
5151- Format.fprintf fmt "HEAD is detached"
5050+ | Not_on_branch -> Format.fprintf fmt "not on any branch"
5151+ | Detached_head -> Format.fprintf fmt "HEAD is detached"
52525353type Eio.Exn.err += E of error
54545555let () =
5656 Eio.Exn.register_pp (fun fmt -> function
5757- | E e -> Format.fprintf fmt "Git %a" pp_error e; true
5757+ | E e ->
5858+ Format.fprintf fmt "Git %a" pp_error e;
5959+ true
5860 | _ -> false)
59616062let err e = Eio.Exn.create (E e)
···6971let string_trim s = String.trim s
70727173let lines s =
7272- String.split_on_char '\n' s
7373- |> List.filter (fun s -> String.trim s <> "")
7474+ String.split_on_char '\n' s |> List.filter (fun s -> String.trim s <> "")
74757576(* Low-level execution *)
7677···8384 Eio.Switch.run @@ fun sw ->
8485 let stdout_r, stdout_w = Eio.Process.pipe proc_mgr ~sw in
8586 let stderr_r, stderr_w = Eio.Process.pipe proc_mgr ~sw in
8686- let child = Eio.Process.spawn proc_mgr ~sw
8787+ let child =
8888+ Eio.Process.spawn proc_mgr ~sw
8789 ?cwd:(Option.map (fun p -> (p :> Eio.Fs.dir_ty Eio.Path.t)) cwd)
8888- ~stdout:stdout_w ~stderr:stderr_w
8989- full_cmd
9090+ ~stdout:stdout_w ~stderr:stderr_w full_cmd
9091 in
9192 Eio.Flow.close stdout_w;
9293 Eio.Flow.close stderr_w;
9394 (* Read stdout and stderr concurrently *)
9495 Eio.Fiber.both
9596 (fun () ->
9696- let chunk = Cstruct.create 4096 in
9797- let rec loop () =
9898- match Eio.Flow.single_read stdout_r chunk with
9999- | n ->
100100- Buffer.add_string stdout_buf (Cstruct.to_string (Cstruct.sub chunk 0 n));
101101- loop ()
102102- | exception End_of_file -> ()
103103- in
104104- loop ())
9797+ let chunk = Cstruct.create 4096 in
9898+ let rec loop () =
9999+ match Eio.Flow.single_read stdout_r chunk with
100100+ | n ->
101101+ Buffer.add_string stdout_buf
102102+ (Cstruct.to_string (Cstruct.sub chunk 0 n));
103103+ loop ()
104104+ | exception End_of_file -> ()
105105+ in
106106+ loop ())
105107 (fun () ->
106106- let chunk = Cstruct.create 4096 in
107107- let rec loop () =
108108- match Eio.Flow.single_read stderr_r chunk with
109109- | n ->
110110- Buffer.add_string stderr_buf (Cstruct.to_string (Cstruct.sub chunk 0 n));
111111- loop ()
112112- | exception End_of_file -> ()
113113- in
114114- loop ());
108108+ let chunk = Cstruct.create 4096 in
109109+ let rec loop () =
110110+ match Eio.Flow.single_read stderr_r chunk with
111111+ | n ->
112112+ Buffer.add_string stderr_buf
113113+ (Cstruct.to_string (Cstruct.sub chunk 0 n));
114114+ loop ()
115115+ | exception End_of_file -> ()
116116+ in
117117+ loop ());
115118 let status = Eio.Process.await child in
116119 let stdout = Buffer.contents stdout_buf in
117120 let stderr = Buffer.contents stderr_buf in
118121 match status with
119119- | `Exited 0 ->
120120- Log.debug (fun m -> m "Output: %s" (string_trim stdout));
121121- Ok stdout
122122- | `Exited code ->
123123- Log.debug (fun m -> m "Failed (exit %d): %s" code (string_trim stderr));
124124- Error (Command_failed { cmd = args; exit_code = code; stdout; stderr })
125125- | `Signaled signal ->
126126- Log.debug (fun m -> m "Killed by signal %d" signal);
127127- let code = 128 + signal in
128128- Error (Command_failed { cmd = args; exit_code = code; stdout; stderr })
122122+ | `Exited 0 ->
123123+ Log.debug (fun m -> m "Output: %s" (string_trim stdout));
124124+ Ok stdout
125125+ | `Exited code ->
126126+ Log.debug (fun m -> m "Failed (exit %d): %s" code (string_trim stderr));
127127+ Error (Command_failed { cmd = args; exit_code = code; stdout; stderr })
128128+ | `Signaled signal ->
129129+ Log.debug (fun m -> m "Killed by signal %d" signal);
130130+ let code = 128 + signal in
131131+ Error (Command_failed { cmd = args; exit_code = code; stdout; stderr })
129132 with exn ->
130133 Log.err (fun m -> m "Exception running git: %a" Fmt.exn exn);
131134 raise exn
···135138 | Ok output -> output
136139 | Error e ->
137140 let ex = err e in
138138- raise (Eio.Exn.add_context ex "running git %a" Fmt.(list ~sep:sp string) args)
141141+ raise
142142+ (Eio.Exn.add_context ex "running git %a" Fmt.(list ~sep:sp string) args)
139143140144let run_lines ~proc_mgr ?cwd args =
141145 run_exn ~proc_mgr ?cwd args |> string_trim |> lines
···145149let is_repository path =
146150 let git_dir = Eio.Path.(path / ".git") in
147151 match Eio.Path.kind ~follow:false git_dir with
148148- | `Directory | `Regular_file -> true (* .git can be a file for worktrees *)
152152+ | `Directory | `Regular_file -> true (* .git can be a file for worktrees *)
149153 | _ -> false
150154 | exception _ -> false
151155152156let current_branch ~proc_mgr ~cwd =
153153- match run ~proc_mgr ~cwd ["symbolic-ref"; "--short"; "HEAD"] with
157157+ match run ~proc_mgr ~cwd [ "symbolic-ref"; "--short"; "HEAD" ] with
154158 | Ok output -> Some (string_trim output)
155159 | Error _ -> None
156160···160164 | None -> raise (err Not_on_branch)
161165162166let current_head ~proc_mgr ~cwd =
163163- run_exn ~proc_mgr ~cwd ["rev-parse"; "HEAD"] |> string_trim
167167+ run_exn ~proc_mgr ~cwd [ "rev-parse"; "HEAD" ] |> string_trim
164168165169let has_uncommitted_changes ~proc_mgr ~cwd =
166166- let status = run_exn ~proc_mgr ~cwd ["status"; "--porcelain"] in
170170+ let status = run_exn ~proc_mgr ~cwd [ "status"; "--porcelain" ] in
167171 String.trim status <> ""
168172169173let remote_exists ~proc_mgr ~cwd name =
170170- match run ~proc_mgr ~cwd ["remote"; "get-url"; name] with
174174+ match run ~proc_mgr ~cwd [ "remote"; "get-url"; name ] with
171175 | Ok _ -> true
172176 | Error _ -> false
173177174178let branch_exists ~proc_mgr ~cwd name =
175175- match run ~proc_mgr ~cwd ["show-ref"; "--verify"; "--quiet"; "refs/heads/" ^ name] with
179179+ match
180180+ run ~proc_mgr ~cwd
181181+ [ "show-ref"; "--verify"; "--quiet"; "refs/heads/" ^ name ]
182182+ with
176183 | Ok _ -> true
177184 | Error _ -> false
178185179186let rev_parse ~proc_mgr ~cwd ref_ =
180180- match run ~proc_mgr ~cwd ["rev-parse"; "--verify"; "--quiet"; ref_] with
187187+ match run ~proc_mgr ~cwd [ "rev-parse"; "--verify"; "--quiet"; ref_ ] with
181188 | Ok output -> Some (string_trim output)
182189 | Error _ -> None
183190···187194 | None -> raise (err (Branch_not_found ref_))
188195189196let rev_parse_short ~proc_mgr ~cwd ref_ =
190190- run_exn ~proc_mgr ~cwd ["rev-parse"; "--short"; ref_] |> string_trim
197197+ run_exn ~proc_mgr ~cwd [ "rev-parse"; "--short"; ref_ ] |> string_trim
191198192199let ls_remote_default_branch ~proc_mgr ~cwd ~url =
193200 Log.info (fun m -> m "Detecting default branch for %s..." url);
194201 (* Try to get the default branch from the remote *)
195195- let output = run_exn ~proc_mgr ~cwd ["ls-remote"; "--symref"; url; "HEAD"] in
202202+ let output =
203203+ run_exn ~proc_mgr ~cwd [ "ls-remote"; "--symref"; url; "HEAD" ]
204204+ in
196205 (* Parse output like: ref: refs/heads/main\tHEAD *)
197206 let default =
198207 let lines = String.split_on_char '\n' output in
199199- List.find_map (fun line ->
200200- if String.starts_with ~prefix:"ref:" line then
201201- let parts = String.split_on_char '\t' line in
202202- match parts with
203203- | ref_part :: _ ->
204204- let ref_part = String.trim ref_part in
205205- if String.starts_with ~prefix:"ref: refs/heads/" ref_part then
206206- Some (String.sub ref_part 16 (String.length ref_part - 16))
207207- else None
208208- | _ -> None
209209- else None
210210- ) lines
208208+ List.find_map
209209+ (fun line ->
210210+ if String.starts_with ~prefix:"ref:" line then
211211+ let parts = String.split_on_char '\t' line in
212212+ match parts with
213213+ | ref_part :: _ ->
214214+ let ref_part = String.trim ref_part in
215215+ if String.starts_with ~prefix:"ref: refs/heads/" ref_part then
216216+ Some (String.sub ref_part 16 (String.length ref_part - 16))
217217+ else None
218218+ | _ -> None
219219+ else None)
220220+ lines
211221 in
212222 match default with
213223 | Some branch ->
···215225 branch
216226 | None ->
217227 (* Fallback: try common branch names *)
218218- Log.debug (fun m -> m "Could not detect default branch, trying common names...");
228228+ Log.debug (fun m ->
229229+ m "Could not detect default branch, trying common names...");
219230 let try_branch name =
220220- match run ~proc_mgr ~cwd ["ls-remote"; "--heads"; url; name] with
231231+ match run ~proc_mgr ~cwd [ "ls-remote"; "--heads"; url; name ] with
221232 | Ok output when String.trim output <> "" -> true
222233 | _ -> false
223234 in
···228239 "main"
229240 end
230241231231-let list_remotes ~proc_mgr ~cwd =
232232- run_lines ~proc_mgr ~cwd ["remote"]
242242+let list_remotes ~proc_mgr ~cwd = run_lines ~proc_mgr ~cwd [ "remote" ]
233243234244let remote_url ~proc_mgr ~cwd name =
235235- match run ~proc_mgr ~cwd ["remote"; "get-url"; name] with
245245+ match run ~proc_mgr ~cwd [ "remote"; "get-url"; name ] with
236246 | Ok output -> Some (string_trim output)
237247 | Error _ -> None
238248239249let log_oneline ~proc_mgr ~cwd ?max_count from_ref to_ref =
240250 let range = from_ref ^ ".." ^ to_ref in
241241- let args = ["log"; "--oneline"; range] in
242242- let args = match max_count with
243243- | Some n -> args @ ["--max-count"; string_of_int n]
251251+ let args = [ "log"; "--oneline"; range ] in
252252+ let args =
253253+ match max_count with
254254+ | Some n -> args @ [ "--max-count"; string_of_int n ]
244255 | None -> args
245256 in
246257 run_lines ~proc_mgr ~cwd args
247258248259let diff_stat ~proc_mgr ~cwd from_ref to_ref =
249260 let range = from_ref ^ ".." ^ to_ref in
250250- run_exn ~proc_mgr ~cwd ["diff"; "--stat"; range]
261261+ run_exn ~proc_mgr ~cwd [ "diff"; "--stat"; range ]
251262252263let ls_tree ~proc_mgr ~cwd ~tree ~path =
253253- match run ~proc_mgr ~cwd ["ls-tree"; tree; path] with
264264+ match run ~proc_mgr ~cwd [ "ls-tree"; tree; path ] with
254265 | Ok output -> String.trim output <> ""
255266 | Error _ -> false
256267257268let rev_list_count ~proc_mgr ~cwd from_ref to_ref =
258269 let range = from_ref ^ ".." ^ to_ref in
259259- let output = run_exn ~proc_mgr ~cwd ["rev-list"; "--count"; range] in
270270+ let output = run_exn ~proc_mgr ~cwd [ "rev-list"; "--count"; range ] in
260271 int_of_string (string_trim output)
261272262273(* Idempotent mutations *)
···265276 match remote_url ~proc_mgr ~cwd name with
266277 | None ->
267278 Log.info (fun m -> m "Adding remote %s -> %s" name url);
268268- run_exn ~proc_mgr ~cwd ["remote"; "add"; name; url] |> ignore;
279279+ run_exn ~proc_mgr ~cwd [ "remote"; "add"; name; url ] |> ignore;
269280 `Created
270281 | Some existing_url ->
271282 if existing_url = url then begin
272283 Log.debug (fun m -> m "Remote %s already exists with correct URL" name);
273284 `Existed
274274- end else begin
275275- Log.info (fun m -> m "Updating remote %s URL: %s -> %s" name existing_url url);
276276- run_exn ~proc_mgr ~cwd ["remote"; "set-url"; name; url] |> ignore;
285285+ end
286286+ else begin
287287+ Log.info (fun m ->
288288+ m "Updating remote %s URL: %s -> %s" name existing_url url);
289289+ run_exn ~proc_mgr ~cwd [ "remote"; "set-url"; name; url ] |> ignore;
277290 `Updated
278291 end
279292···281294 if branch_exists ~proc_mgr ~cwd name then begin
282295 Log.debug (fun m -> m "Branch %s already exists" name);
283296 `Existed
284284- end else begin
297297+ end
298298+ else begin
285299 Log.info (fun m -> m "Creating branch %s at %s" name start_point);
286286- run_exn ~proc_mgr ~cwd ["branch"; name; start_point] |> ignore;
300300+ run_exn ~proc_mgr ~cwd [ "branch"; name; start_point ] |> ignore;
287301 `Created
288302 end
289303···291305292306let init ~proc_mgr ~cwd =
293307 Log.info (fun m -> m "Initializing git repository...");
294294- run_exn ~proc_mgr ~cwd ["init"] |> ignore
308308+ run_exn ~proc_mgr ~cwd [ "init" ] |> ignore
295309296310let fetch ~proc_mgr ~cwd ~remote =
297311 Log.info (fun m -> m "Fetching from %s..." remote);
298298- run_exn ~proc_mgr ~cwd ["fetch"; remote] |> ignore
312312+ run_exn ~proc_mgr ~cwd [ "fetch"; remote ] |> ignore
299313300314let checkout ~proc_mgr ~cwd ref_ =
301315 Log.debug (fun m -> m "Checking out %s" ref_);
302302- run_exn ~proc_mgr ~cwd ["checkout"; ref_] |> ignore
316316+ run_exn ~proc_mgr ~cwd [ "checkout"; ref_ ] |> ignore
303317304318let checkout_orphan ~proc_mgr ~cwd name =
305319 Log.info (fun m -> m "Creating orphan branch %s" name);
306306- run_exn ~proc_mgr ~cwd ["checkout"; "--orphan"; name] |> ignore
320320+ run_exn ~proc_mgr ~cwd [ "checkout"; "--orphan"; name ] |> ignore
307321308322let read_tree_prefix ~proc_mgr ~cwd ~prefix ~tree =
309323 Log.debug (fun m -> m "Reading tree %s with prefix %s" tree prefix);
310310- run_exn ~proc_mgr ~cwd ["read-tree"; "--prefix=" ^ prefix; tree] |> ignore
324324+ run_exn ~proc_mgr ~cwd [ "read-tree"; "--prefix=" ^ prefix; tree ] |> ignore
311325312326let checkout_index ~proc_mgr ~cwd =
313327 Log.debug (fun m -> m "Checking out index to working directory");
314314- run_exn ~proc_mgr ~cwd ["checkout-index"; "-a"; "-f"] |> ignore
328328+ run_exn ~proc_mgr ~cwd [ "checkout-index"; "-a"; "-f" ] |> ignore
315329316330let rm_rf ~proc_mgr ~cwd ~target =
317331 Log.debug (fun m -> m "Removing %s from git" target);
318332 (* Ignore errors - target might not exist *)
319319- ignore (run ~proc_mgr ~cwd ["rm"; "-rf"; target])
333333+ ignore (run ~proc_mgr ~cwd [ "rm"; "-rf"; target ])
320334321335let rm_cached_rf ~proc_mgr ~cwd =
322336 Log.debug (fun m -> m "Removing all files from index");
323337 (* Ignore errors - index might be empty *)
324324- ignore (run ~proc_mgr ~cwd ["rm"; "-rf"; "--cached"; "."])
338338+ ignore (run ~proc_mgr ~cwd [ "rm"; "-rf"; "--cached"; "." ])
325339326340let add_all ~proc_mgr ~cwd =
327341 Log.debug (fun m -> m "Staging all changes");
328328- run_exn ~proc_mgr ~cwd ["add"; "-A"] |> ignore
342342+ run_exn ~proc_mgr ~cwd [ "add"; "-A" ] |> ignore
329343330344let commit ~proc_mgr ~cwd ~message =
331331- Log.debug (fun m -> m "Committing: %s" (String.sub message 0 (min 50 (String.length message))));
332332- run_exn ~proc_mgr ~cwd ["commit"; "-m"; message] |> ignore
345345+ Log.debug (fun m ->
346346+ m "Committing: %s" (String.sub message 0 (min 50 (String.length message))));
347347+ run_exn ~proc_mgr ~cwd [ "commit"; "-m"; message ] |> ignore
333348334349let commit_allow_empty ~proc_mgr ~cwd ~message =
335335- Log.debug (fun m -> m "Committing (allow empty): %s" (String.sub message 0 (min 50 (String.length message))));
336336- run_exn ~proc_mgr ~cwd ["commit"; "--allow-empty"; "-m"; message] |> ignore
350350+ Log.debug (fun m ->
351351+ m "Committing (allow empty): %s"
352352+ (String.sub message 0 (min 50 (String.length message))));
353353+ run_exn ~proc_mgr ~cwd [ "commit"; "--allow-empty"; "-m"; message ] |> ignore
337354338355let branch_create ~proc_mgr ~cwd ~name ~start_point =
339356 Log.info (fun m -> m "Creating branch %s at %s" name start_point);
340340- run_exn ~proc_mgr ~cwd ["branch"; name; start_point] |> ignore
357357+ run_exn ~proc_mgr ~cwd [ "branch"; name; start_point ] |> ignore
341358342359let branch_force ~proc_mgr ~cwd ~name ~point =
343360 Log.info (fun m -> m "Force-moving branch %s to %s" name point);
344344- run_exn ~proc_mgr ~cwd ["branch"; "-f"; name; point] |> ignore
361361+ run_exn ~proc_mgr ~cwd [ "branch"; "-f"; name; point ] |> ignore
345362346363let remote_add ~proc_mgr ~cwd ~name ~url =
347364 Log.info (fun m -> m "Adding remote %s -> %s" name url);
348348- run_exn ~proc_mgr ~cwd ["remote"; "add"; name; url] |> ignore
365365+ run_exn ~proc_mgr ~cwd [ "remote"; "add"; name; url ] |> ignore
349366350367let remote_set_url ~proc_mgr ~cwd ~name ~url =
351368 Log.info (fun m -> m "Setting remote %s URL to %s" name url);
352352- run_exn ~proc_mgr ~cwd ["remote"; "set-url"; name; url] |> ignore
369369+ run_exn ~proc_mgr ~cwd [ "remote"; "set-url"; name; url ] |> ignore
353370354371let merge_allow_unrelated ~proc_mgr ~cwd ~branch ~message =
355372 Log.info (fun m -> m "Merging %s (allow unrelated histories)..." branch);
356356- match run ~proc_mgr ~cwd ["merge"; "--allow-unrelated-histories"; "-m"; message; branch] with
373373+ match
374374+ run ~proc_mgr ~cwd
375375+ [ "merge"; "--allow-unrelated-histories"; "-m"; message; branch ]
376376+ with
357377 | Ok _ -> Ok ()
358378 | Error (Command_failed { exit_code = 1; _ }) ->
359379 (* Merge conflict - get list of conflicting files *)
360360- let output = run_exn ~proc_mgr ~cwd ["diff"; "--name-only"; "--diff-filter=U"] in
380380+ let output =
381381+ run_exn ~proc_mgr ~cwd [ "diff"; "--name-only"; "--diff-filter=U" ]
382382+ in
361383 let files = lines output in
362362- Log.warn (fun m -> m "Merge conflict: %a" Fmt.(list ~sep:comma string) files);
384384+ Log.warn (fun m ->
385385+ m "Merge conflict: %a" Fmt.(list ~sep:comma string) files);
363386 Error (`Conflict files)
364364- | Error e ->
365365- raise (err e)
387387+ | Error e -> raise (err e)
366388367389let rebase ~proc_mgr ~cwd ~onto =
368390 Log.info (fun m -> m "Rebasing onto %s..." onto);
369369- match run ~proc_mgr ~cwd ["rebase"; onto] with
391391+ match run ~proc_mgr ~cwd [ "rebase"; onto ] with
370392 | Ok _ -> Ok ()
371393 | Error (Command_failed { stderr; _ }) ->
372394 let hint =
373373- if String.length stderr > 200 then
374374- String.sub stderr 0 200 ^ "..."
375375- else
376376- stderr
395395+ if String.length stderr > 200 then String.sub stderr 0 200 ^ "..."
396396+ else stderr
377397 in
378398 Log.warn (fun m -> m "Rebase conflict onto %s" onto);
379399 Error (`Conflict hint)
380380- | Error e ->
381381- raise (err e)
400400+ | Error e -> raise (err e)
382401383402let rebase_abort ~proc_mgr ~cwd =
384403 Log.info (fun m -> m "Aborting rebase...");
385385- ignore (run ~proc_mgr ~cwd ["rebase"; "--abort"])
404404+ ignore (run ~proc_mgr ~cwd [ "rebase"; "--abort" ])
386405387406let merge_abort ~proc_mgr ~cwd =
388407 Log.info (fun m -> m "Aborting merge...");
389389- ignore (run ~proc_mgr ~cwd ["merge"; "--abort"])
408408+ ignore (run ~proc_mgr ~cwd [ "merge"; "--abort" ])
390409391410let reset_hard ~proc_mgr ~cwd ref_ =
392411 Log.info (fun m -> m "Hard reset to %s" ref_);
393393- run_exn ~proc_mgr ~cwd ["reset"; "--hard"; ref_] |> ignore
412412+ run_exn ~proc_mgr ~cwd [ "reset"; "--hard"; ref_ ] |> ignore
394413395414let clean_fd ~proc_mgr ~cwd =
396415 Log.debug (fun m -> m "Cleaning untracked files");
397397- run_exn ~proc_mgr ~cwd ["clean"; "-fd"] |> ignore
416416+ run_exn ~proc_mgr ~cwd [ "clean"; "-fd" ] |> ignore
398417399418let filter_repo_to_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory =
400400- Log.info (fun m -> m "Rewriting history of %s into subdirectory %s..." branch subdirectory);
419419+ Log.info (fun m ->
420420+ m "Rewriting history of %s into subdirectory %s..." branch subdirectory);
421421+401422 (* Use git-filter-repo with --to-subdirectory-filter to rewrite all paths into subdirectory.
402423 This preserves full history with paths prefixed. Much faster than filter-branch.
403424···417438 let temp_wt : path = (fs, temp_wt_path) in
418439419440 (* Remove any existing temp worktree *)
420420- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
441441+ ignore (run ~proc_mgr ~cwd [ "worktree"; "remove"; "-f"; temp_wt_relpath ]);
421442422443 (* Create worktree for the branch *)
423423- run_exn ~proc_mgr ~cwd ["worktree"; "add"; temp_wt_relpath; branch] |> ignore;
444444+ run_exn ~proc_mgr ~cwd [ "worktree"; "add"; temp_wt_relpath; branch ]
445445+ |> ignore;
424446425447 (* Run git-filter-repo in the worktree *)
426426- let result = run ~proc_mgr ~cwd:temp_wt [
427427- "filter-repo";
428428- "--to-subdirectory-filter"; subdirectory;
429429- "--force";
430430- "--refs"; "HEAD"
431431- ] in
448448+ let result =
449449+ run ~proc_mgr ~cwd:temp_wt
450450+ [
451451+ "filter-repo";
452452+ "--to-subdirectory-filter";
453453+ subdirectory;
454454+ "--force";
455455+ "--refs";
456456+ "HEAD";
457457+ ]
458458+ in
432459433460 (* Handle result: get the new SHA, cleanup worktree, then update branch *)
434434- (match result with
435435- | Ok _ ->
436436- (* Get the new HEAD SHA from the worktree BEFORE removing it *)
437437- let new_sha = run_exn ~proc_mgr ~cwd:temp_wt ["rev-parse"; "HEAD"] |> string_trim in
438438- (* Cleanup temporary worktree first (must do this before updating branch) *)
439439- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
440440- (* Now update the branch in the bare repo *)
441441- run_exn ~proc_mgr ~cwd ["branch"; "-f"; branch; new_sha] |> ignore
442442- | Error e ->
443443- (* Cleanup and re-raise *)
444444- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
445445- raise (err e))
461461+ match result with
462462+ | Ok _ ->
463463+ (* Get the new HEAD SHA from the worktree BEFORE removing it *)
464464+ let new_sha =
465465+ run_exn ~proc_mgr ~cwd:temp_wt [ "rev-parse"; "HEAD" ] |> string_trim
466466+ in
467467+ (* Cleanup temporary worktree first (must do this before updating branch) *)
468468+ ignore
469469+ (run ~proc_mgr ~cwd [ "worktree"; "remove"; "-f"; temp_wt_relpath ]);
470470+ (* Now update the branch in the bare repo *)
471471+ run_exn ~proc_mgr ~cwd [ "branch"; "-f"; branch; new_sha ] |> ignore
472472+ | Error e ->
473473+ (* Cleanup and re-raise *)
474474+ ignore
475475+ (run ~proc_mgr ~cwd [ "worktree"; "remove"; "-f"; temp_wt_relpath ]);
476476+ raise (err e)
+64-194
lib/git.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Git operations wrapped with Eio and robust error handling.
2733- All git commands are executed via [Eio.Process] with proper logging
44- and error context. Errors are wrapped in [Eio.Exn.Io] with context
55- chains for debugging. *)
88+ All git commands are executed via [Eio.Process] with proper logging and
99+ error context. Errors are wrapped in [Eio.Exn.Io] with context chains for
1010+ debugging. *)
611712(** {1 Error Types} *)
813···3944(** {1 Low-level execution} *)
40454146val run :
4242- proc_mgr:proc_mgr ->
4343- ?cwd:path ->
4444- string list ->
4545- (string, error) result
4747+ proc_mgr:proc_mgr -> ?cwd:path -> string list -> (string, error) result
4648(** [run ~proc_mgr args] executes [git args] and returns stdout on success. *)
47494848-val run_exn :
4949- proc_mgr:proc_mgr ->
5050- ?cwd:path ->
5151- string list ->
5252- string
5353-(** [run_exn ~proc_mgr args] executes [git args] and returns stdout.
5454- Raises on failure with context. *)
5050+val run_exn : proc_mgr:proc_mgr -> ?cwd:path -> string list -> string
5151+(** [run_exn ~proc_mgr args] executes [git args] and returns stdout. Raises on
5252+ failure with context. *)
55535656-val run_lines :
5757- proc_mgr:proc_mgr ->
5858- ?cwd:path ->
5959- string list ->
6060- string list
5454+val run_lines : proc_mgr:proc_mgr -> ?cwd:path -> string list -> string list
6155(** [run_lines ~proc_mgr args] executes and splits output by newlines. *)
62566357(** {1 Queries - Safe read-only operations} *)
···6559val is_repository : path -> bool
6660(** [is_repository path] checks if [path] contains a [.git] directory. *)
67616868-val current_branch :
6969- proc_mgr:proc_mgr ->
7070- cwd:path ->
7171- string option
7272-(** [current_branch] returns [Some branch] if on a branch, [None] if detached. *)
6262+val current_branch : proc_mgr:proc_mgr -> cwd:path -> string option
6363+(** [current_branch] returns [Some branch] if on a branch, [None] if detached.
6464+*)
73657474-val current_branch_exn :
7575- proc_mgr:proc_mgr ->
7676- cwd:path ->
7777- string
6666+val current_branch_exn : proc_mgr:proc_mgr -> cwd:path -> string
7867(** [current_branch_exn] returns current branch or raises [Not_on_branch]. *)
79688080-val current_head :
8181- proc_mgr:proc_mgr ->
8282- cwd:path ->
8383- string
6969+val current_head : proc_mgr:proc_mgr -> cwd:path -> string
8470(** [current_head] returns the current HEAD SHA. *)
85718686-val has_uncommitted_changes :
8787- proc_mgr:proc_mgr ->
8888- cwd:path ->
8989- bool
9090-(** [has_uncommitted_changes] returns true if there are staged or unstaged changes. *)
7272+val has_uncommitted_changes : proc_mgr:proc_mgr -> cwd:path -> bool
7373+(** [has_uncommitted_changes] returns true if there are staged or unstaged
7474+ changes. *)
91759292-val remote_exists :
9393- proc_mgr:proc_mgr ->
9494- cwd:path ->
9595- string ->
9696- bool
7676+val remote_exists : proc_mgr:proc_mgr -> cwd:path -> string -> bool
9777(** [remote_exists ~proc_mgr ~cwd name] checks if remote [name] exists. *)
98789999-val branch_exists :
100100- proc_mgr:proc_mgr ->
101101- cwd:path ->
102102- string ->
103103- bool
7979+val branch_exists : proc_mgr:proc_mgr -> cwd:path -> string -> bool
10480(** [branch_exists ~proc_mgr ~cwd name] checks if branch [name] exists. *)
10581106106-val rev_parse :
107107- proc_mgr:proc_mgr ->
108108- cwd:path ->
109109- string ->
110110- string option
8282+val rev_parse : proc_mgr:proc_mgr -> cwd:path -> string -> string option
11183(** [rev_parse ~proc_mgr ~cwd ref] returns the SHA for [ref], or [None]. *)
11284113113-val rev_parse_exn :
114114- proc_mgr:proc_mgr ->
115115- cwd:path ->
116116- string ->
117117- string
8585+val rev_parse_exn : proc_mgr:proc_mgr -> cwd:path -> string -> string
11886(** [rev_parse_exn] returns SHA or raises. *)
11987120120-val rev_parse_short :
121121- proc_mgr:proc_mgr ->
122122- cwd:path ->
123123- string ->
124124- string
8888+val rev_parse_short : proc_mgr:proc_mgr -> cwd:path -> string -> string
12589(** [rev_parse_short] returns abbreviated SHA. *)
1269012791val ls_remote_default_branch :
128128- proc_mgr:proc_mgr ->
129129- cwd:path ->
130130- url:string ->
131131- string
132132-(** [ls_remote_default_branch ~proc_mgr ~cwd ~url] detects the default branch of remote. *)
9292+ proc_mgr:proc_mgr -> cwd:path -> url:string -> string
9393+(** [ls_remote_default_branch ~proc_mgr ~cwd ~url] detects the default branch of
9494+ remote. *)
13395134134-val list_remotes :
135135- proc_mgr:proc_mgr ->
136136- cwd:path ->
137137- string list
9696+val list_remotes : proc_mgr:proc_mgr -> cwd:path -> string list
13897(** [list_remotes] returns all remote names. *)
13998140140-val remote_url :
141141- proc_mgr:proc_mgr ->
142142- cwd:path ->
143143- string ->
144144- string option
9999+val remote_url : proc_mgr:proc_mgr -> cwd:path -> string -> string option
145100(** [remote_url ~proc_mgr ~cwd name] returns the URL for remote [name]. *)
146101147102val log_oneline :
···153108 string list
154109(** [log_oneline ~proc_mgr ~cwd from_ref to_ref] returns commit summaries. *)
155110156156-val diff_stat :
157157- proc_mgr:proc_mgr ->
158158- cwd:path ->
159159- string ->
160160- string ->
161161- string
111111+val diff_stat : proc_mgr:proc_mgr -> cwd:path -> string -> string -> string
162112(** [diff_stat ~proc_mgr ~cwd from_ref to_ref] returns diff statistics. *)
163113164114val ls_tree :
165165- proc_mgr:proc_mgr ->
166166- cwd:path ->
167167- tree:string ->
168168- path:string ->
169169- bool
115115+ proc_mgr:proc_mgr -> cwd:path -> tree:string -> path:string -> bool
170116(** [ls_tree ~proc_mgr ~cwd ~tree ~path] checks if [path] exists in [tree]. *)
171117172172-val rev_list_count :
173173- proc_mgr:proc_mgr ->
174174- cwd:path ->
175175- string ->
176176- string ->
177177- int
178178-(** [rev_list_count ~proc_mgr ~cwd from_ref to_ref] counts commits between refs. *)
118118+val rev_list_count : proc_mgr:proc_mgr -> cwd:path -> string -> string -> int
119119+(** [rev_list_count ~proc_mgr ~cwd from_ref to_ref] counts commits between refs.
120120+*)
179121180122(** {1 Idempotent mutations - Safe to re-run} *)
181123···197139198140(** {1 State-changing operations} *)
199141200200-val init :
201201- proc_mgr:proc_mgr ->
202202- cwd:path ->
203203- unit
142142+val init : proc_mgr:proc_mgr -> cwd:path -> unit
204143(** [init] initializes a new git repository. *)
205144206206-val fetch :
207207- proc_mgr:proc_mgr ->
208208- cwd:path ->
209209- remote:string ->
210210- unit
145145+val fetch : proc_mgr:proc_mgr -> cwd:path -> remote:string -> unit
211146(** [fetch] fetches from a remote. *)
212147213213-val checkout :
214214- proc_mgr:proc_mgr ->
215215- cwd:path ->
216216- string ->
217217- unit
148148+val checkout : proc_mgr:proc_mgr -> cwd:path -> string -> unit
218149(** [checkout] switches to a branch or commit. *)
219150220220-val checkout_orphan :
221221- proc_mgr:proc_mgr ->
222222- cwd:path ->
223223- string ->
224224- unit
151151+val checkout_orphan : proc_mgr:proc_mgr -> cwd:path -> string -> unit
225152(** [checkout_orphan] creates and switches to a new orphan branch. *)
226153227154val read_tree_prefix :
228228- proc_mgr:proc_mgr ->
229229- cwd:path ->
230230- prefix:string ->
231231- tree:string ->
232232- unit
155155+ proc_mgr:proc_mgr -> cwd:path -> prefix:string -> tree:string -> unit
233156(** [read_tree_prefix] reads a tree into the index with a path prefix. *)
234157235235-val checkout_index :
236236- proc_mgr:proc_mgr ->
237237- cwd:path ->
238238- unit
158158+val checkout_index : proc_mgr:proc_mgr -> cwd:path -> unit
239159(** [checkout_index] checks out files from the index to working directory. *)
240160241241-val rm_rf :
242242- proc_mgr:proc_mgr ->
243243- cwd:path ->
244244- target:string ->
245245- unit
161161+val rm_rf : proc_mgr:proc_mgr -> cwd:path -> target:string -> unit
246162(** [rm_rf] removes files/directories from git tracking. *)
247163248248-val rm_cached_rf :
249249- proc_mgr:proc_mgr ->
250250- cwd:path ->
251251- unit
164164+val rm_cached_rf : proc_mgr:proc_mgr -> cwd:path -> unit
252165(** [rm_cached_rf] removes all files from index (for orphan branch setup). *)
253166254254-val add_all :
255255- proc_mgr:proc_mgr ->
256256- cwd:path ->
257257- unit
167167+val add_all : proc_mgr:proc_mgr -> cwd:path -> unit
258168(** [add_all] stages all changes. *)
259169260260-val commit :
261261- proc_mgr:proc_mgr ->
262262- cwd:path ->
263263- message:string ->
264264- unit
170170+val commit : proc_mgr:proc_mgr -> cwd:path -> message:string -> unit
265171(** [commit] creates a commit with the given message. *)
266172267267-val commit_allow_empty :
268268- proc_mgr:proc_mgr ->
269269- cwd:path ->
270270- message:string ->
271271- unit
173173+val commit_allow_empty : proc_mgr:proc_mgr -> cwd:path -> message:string -> unit
272174(** [commit_allow_empty] creates a commit even if there are no changes. *)
273175274176val branch_create :
275275- proc_mgr:proc_mgr ->
276276- cwd:path ->
277277- name:string ->
278278- start_point:string ->
279279- unit
177177+ proc_mgr:proc_mgr -> cwd:path -> name:string -> start_point:string -> unit
280178(** [branch_create] creates a new branch at [start_point]. *)
281179282180val branch_force :
283283- proc_mgr:proc_mgr ->
284284- cwd:path ->
285285- name:string ->
286286- point:string ->
287287- unit
181181+ proc_mgr:proc_mgr -> cwd:path -> name:string -> point:string -> unit
288182(** [branch_force] moves branch to point (creates if needed). *)
289183290184val remote_add :
291291- proc_mgr:proc_mgr ->
292292- cwd:path ->
293293- name:string ->
294294- url:string ->
295295- unit
185185+ proc_mgr:proc_mgr -> cwd:path -> name:string -> url:string -> unit
296186(** [remote_add] adds a new remote. *)
297187298188val remote_set_url :
299299- proc_mgr:proc_mgr ->
300300- cwd:path ->
301301- name:string ->
302302- url:string ->
303303- unit
189189+ proc_mgr:proc_mgr -> cwd:path -> name:string -> url:string -> unit
304190(** [remote_set_url] updates the URL of an existing remote. *)
305191306192val merge_allow_unrelated :
···309195 branch:string ->
310196 message:string ->
311197 (unit, [ `Conflict of string list ]) result
312312-(** [merge_allow_unrelated] merges with [--allow-unrelated-histories].
313313- Returns [Error (`Conflict files)] if there are conflicts. *)
198198+(** [merge_allow_unrelated] merges with [--allow-unrelated-histories]. Returns
199199+ [Error (`Conflict files)] if there are conflicts. *)
314200315201val rebase :
316202 proc_mgr:proc_mgr ->
317203 cwd:path ->
318204 onto:string ->
319205 (unit, [ `Conflict of string ]) result
320320-(** [rebase] rebases current branch onto [onto].
321321- Returns [Error (`Conflict hint)] if there are conflicts. *)
206206+(** [rebase] rebases current branch onto [onto]. Returns
207207+ [Error (`Conflict hint)] if there are conflicts. *)
322208323323-val rebase_abort :
324324- proc_mgr:proc_mgr ->
325325- cwd:path ->
326326- unit
209209+val rebase_abort : proc_mgr:proc_mgr -> cwd:path -> unit
327210(** [rebase_abort] aborts an in-progress rebase. *)
328211329329-val merge_abort :
330330- proc_mgr:proc_mgr ->
331331- cwd:path ->
332332- unit
212212+val merge_abort : proc_mgr:proc_mgr -> cwd:path -> unit
333213(** [merge_abort] aborts an in-progress merge. *)
334214335335-val reset_hard :
336336- proc_mgr:proc_mgr ->
337337- cwd:path ->
338338- string ->
339339- unit
215215+val reset_hard : proc_mgr:proc_mgr -> cwd:path -> string -> unit
340216(** [reset_hard] does a hard reset to the given ref. *)
341217342342-val clean_fd :
343343- proc_mgr:proc_mgr ->
344344- cwd:path ->
345345- unit
218218+val clean_fd : proc_mgr:proc_mgr -> cwd:path -> unit
346219(** [clean_fd] removes untracked files and directories. *)
347220348221val filter_repo_to_subdirectory :
349349- proc_mgr:proc_mgr ->
350350- cwd:path ->
351351- branch:string ->
352352- subdirectory:string ->
353353- unit
354354-(** [filter_repo_to_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory]
355355- rewrites the history of [branch] so all files are moved into [subdirectory].
356356- Uses git-filter-repo for fast history rewriting. Preserves full commit history. *)
222222+ proc_mgr:proc_mgr -> cwd:path -> branch:string -> subdirectory:string -> unit
223223+(** [filter_repo_to_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory] rewrites
224224+ the history of [branch] so all files are moved into [subdirectory]. Uses
225225+ git-filter-repo for fast history rewriting. Preserves full commit history.
226226+*)
+45-33
lib/git_repo_lookup.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Git repository URL lookup and rewriting.
2733- This module handles URL rewriting for git repositories, mapping known
44- slow upstream URLs to faster mirrors, and branch/tag overrides for
55- specific packages. *)
88+ This module handles URL rewriting for git repositories, mapping known slow
99+ upstream URLs to faster mirrors, and branch/tag overrides for specific
1010+ packages. *)
611712(** Rewrite a git URL to use a faster mirror if available.
813914 Currently handles:
1015 - erratique.ch repos are mirrored on GitHub under dbuenzli
1111- - git.robur.coop repos are mirrored on GitHub under robur-coop
1212- (strips the org prefix: git.robur.coop/robur/X -> github.com/robur-coop/X) *)
1616+ - git.robur.coop repos are mirrored on GitHub under robur-coop (strips the
1717+ org prefix: git.robur.coop/robur/X -> github.com/robur-coop/X) *)
1318let rewrite_url url =
1419 (* Helper to check and rewrite prefix *)
1520 let try_rewrite ~prefix ~replacement url =
1616- if String.length url > String.length prefix
1717- && String.sub url 0 (String.length prefix) = prefix
2121+ if
2222+ String.length url > String.length prefix
2323+ && String.sub url 0 (String.length prefix) = prefix
1824 then
1919- let rest = String.sub url (String.length prefix)
2020- (String.length url - String.length prefix) in
2525+ let rest =
2626+ String.sub url (String.length prefix)
2727+ (String.length url - String.length prefix)
2828+ in
2129 Some (replacement ^ rest)
2230 else None
2331 in
2432 (* Helper to rewrite robur.coop URLs, stripping the org path component *)
2533 let try_rewrite_robur ~prefix url =
2626- if String.length url > String.length prefix
2727- && String.sub url 0 (String.length prefix) = prefix
3434+ if
3535+ String.length url > String.length prefix
3636+ && String.sub url 0 (String.length prefix) = prefix
2837 then
2938 (* rest is e.g. "robur/ohex.git" - strip org prefix *)
3030- let rest = String.sub url (String.length prefix)
3131- (String.length url - String.length prefix) in
3939+ let rest =
4040+ String.sub url (String.length prefix)
4141+ (String.length url - String.length prefix)
4242+ in
3243 (* Find the first slash to strip the org *)
3344 match String.index_opt rest '/' with
3445 | Some idx ->
···3849 else None
3950 in
4051 (* Try each rewrite rule in order *)
4141- match try_rewrite ~prefix:"https://erratique.ch/repos/"
4242- ~replacement:"https://github.com/dbuenzli/" url with
5252+ match
5353+ try_rewrite ~prefix:"https://erratique.ch/repos/"
5454+ ~replacement:"https://github.com/dbuenzli/" url
5555+ with
4356 | Some u -> u
4444- | None ->
4545- match try_rewrite ~prefix:"http://erratique.ch/repos/"
4646- ~replacement:"https://github.com/dbuenzli/" url with
4747- | Some u -> u
4848- | None ->
4949- match try_rewrite_robur ~prefix:"https://git.robur.coop/" url with
5050- | Some u -> u
5151- | None ->
5252- match try_rewrite_robur ~prefix:"git://git.robur.coop/" url with
5353- | Some u -> u
5454- | None -> url
5757+ | None -> (
5858+ match
5959+ try_rewrite ~prefix:"http://erratique.ch/repos/"
6060+ ~replacement:"https://github.com/dbuenzli/" url
6161+ with
6262+ | Some u -> u
6363+ | None -> (
6464+ match try_rewrite_robur ~prefix:"https://git.robur.coop/" url with
6565+ | Some u -> u
6666+ | None -> (
6767+ match try_rewrite_robur ~prefix:"git://git.robur.coop/" url with
6868+ | Some u -> u
6969+ | None -> url)))
55705671(** Override branch/tag for specific packages.
5772···6378let branch_override ~name ~url =
6479 (* Dune's main branch can be unstable; pin to release tag *)
6580 let is_dune_url =
6666- String.equal url "https://github.com/ocaml/dune.git" ||
6767- String.equal url "https://github.com/ocaml/dune" ||
6868- String.equal url "git://github.com/ocaml/dune.git"
8181+ String.equal url "https://github.com/ocaml/dune.git"
8282+ || String.equal url "https://github.com/ocaml/dune"
8383+ || String.equal url "git://github.com/ocaml/dune.git"
6984 in
7070- if name = "dune" || is_dune_url then
7171- Some "3.20.2"
7272- else
7373- None
8585+ if name = "dune" || is_dune_url then Some "3.20.2" else None
+36-28
lib/init.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Project initialization for unpac.
2738 Creates the bare repository structure and initial main worktree. *)
4955-let default_unpac_toml = {|[opam]
1010+let default_unpac_toml =
1111+ {|[opam]
612repositories = []
713# compiler = "5.4.0"
814···1319# Projects will be added here
1420|}
15211616-let project_dune_project name = Printf.sprintf {|(lang dune 3.20)
2222+let project_dune_project name =
2323+ Printf.sprintf {|(lang dune 3.20)
1724(name %s)
1825|} name
1926···3138let init ~proc_mgr ~fs path =
3239 (* Convert relative paths to absolute *)
3340 let abs_path =
3434- if Filename.is_relative path then
3535- Filename.concat (Sys.getcwd ()) path
4141+ if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path
3642 else path
3743 in
3844 let root = Eio.Path.(fs / abs_path) in
···4349 (* Initialize bare repository *)
4450 let git_path = Eio.Path.(root / "git") in
4551 Eio.Path.mkdirs ~exists_ok:false ~perm:0o755 git_path;
4646- Git.run_exn ~proc_mgr ~cwd:git_path ["init"; "--bare"] |> ignore;
5252+ Git.run_exn ~proc_mgr ~cwd:git_path [ "init"; "--bare" ] |> ignore;
47534854 (* Create initial main branch with unpac.toml *)
4955 (* First create a temporary worktree to make the initial commit *)
···5157 Eio.Path.mkdirs ~exists_ok:false ~perm:0o755 main_path;
52585359 (* Initialize as a regular repo temporarily to create first commit *)
5454- Git.run_exn ~proc_mgr ~cwd:main_path ["init"] |> ignore;
6060+ Git.run_exn ~proc_mgr ~cwd:main_path [ "init" ] |> ignore;
55615662 (* Write unpac.toml *)
5763 Eio.Path.save ~create:(`Or_truncate 0o644)
···5965 default_unpac_toml;
60666167 (* Create initial commit *)
6262- Git.run_exn ~proc_mgr ~cwd:main_path ["add"; "unpac.toml"] |> ignore;
6363- Git.run_exn ~proc_mgr ~cwd:main_path
6464- ["commit"; "-m"; "Initial commit"] |> ignore;
6868+ Git.run_exn ~proc_mgr ~cwd:main_path [ "add"; "unpac.toml" ] |> ignore;
6969+ Git.run_exn ~proc_mgr ~cwd:main_path [ "commit"; "-m"; "Initial commit" ]
7070+ |> ignore;
65716672 (* Rename branch to main if needed *)
6767- Git.run_exn ~proc_mgr ~cwd:main_path ["branch"; "-M"; "main"] |> ignore;
7373+ Git.run_exn ~proc_mgr ~cwd:main_path [ "branch"; "-M"; "main" ] |> ignore;
68746975 (* Push to bare repo and convert to worktree *)
7070- Git.run_exn ~proc_mgr ~cwd:main_path
7171- ["remote"; "add"; "origin"; "../git"] |> ignore;
7272- Git.run_exn ~proc_mgr ~cwd:main_path
7373- ["push"; "-u"; "origin"; "main"] |> ignore;
7676+ Git.run_exn ~proc_mgr ~cwd:main_path [ "remote"; "add"; "origin"; "../git" ]
7777+ |> ignore;
7878+ Git.run_exn ~proc_mgr ~cwd:main_path [ "push"; "-u"; "origin"; "main" ]
7979+ |> ignore;
74807581 (* Remove the temporary clone and add main as a worktree of the bare repo *)
7682 Eio.Path.rmtree main_path;
77837884 (* Add main as a worktree of the bare repo *)
7979- Git.run_exn ~proc_mgr ~cwd:git_path
8080- ["worktree"; "add"; "../main"; "main"] |> ignore;
8585+ Git.run_exn ~proc_mgr ~cwd:git_path [ "worktree"; "add"; "../main"; "main" ]
8686+ |> ignore;
81878288 root
83898490(** Check if a path is an unpac project root. *)
8591let is_unpac_root path =
8686- Eio.Path.is_directory Eio.Path.(path / "git") &&
8787- Eio.Path.is_directory Eio.Path.(path / "main") &&
8888- Eio.Path.is_file Eio.Path.(path / "main" / "unpac.toml")
9292+ Eio.Path.is_directory Eio.Path.(path / "git")
9393+ && Eio.Path.is_directory Eio.Path.(path / "main")
9494+ && Eio.Path.is_file Eio.Path.(path / "main" / "unpac.toml")
89959096(** Find the unpac root by walking up from current directory. *)
9197let find_root ~fs ~cwd =
9298 let rec go path =
9399 if is_unpac_root path then Some path
9494- else match Eio.Path.split path with
100100+ else
101101+ match Eio.Path.split path with
95102 | Some (parent, _) -> go parent
96103 | None -> None
97104 in
···130137 vendor_dune;
131138132139 (* Commit template *)
133133- Git.run_exn ~proc_mgr ~cwd:project_path ["add"; "-A"] |> ignore;
140140+ Git.run_exn ~proc_mgr ~cwd:project_path [ "add"; "-A" ] |> ignore;
134141 Git.run_exn ~proc_mgr ~cwd:project_path
135135- ["commit"; "-m"; "Initialize project " ^ name] |> ignore;
142142+ [ "commit"; "-m"; "Initialize project " ^ name ]
143143+ |> ignore;
136144137145 (* Update main/unpac.toml to register project *)
138146 let main_path = Worktree.path root Main in
···141149142150 (* Simple append to [projects] section - a proper implementation would parse TOML *)
143151 let updated =
144144- if content = "" || not (String.ends_with ~suffix:"\n" content)
145145- then content ^ "\n" ^ name ^ " = {}\n"
152152+ if content = "" || not (String.ends_with ~suffix:"\n" content) then
153153+ content ^ "\n" ^ name ^ " = {}\n"
146154 else content ^ name ^ " = {}\n"
147155 in
148156 Eio.Path.save ~create:(`Or_truncate 0o644) toml_path updated;
149157150150- Git.run_exn ~proc_mgr ~cwd:main_path ["add"; "unpac.toml"] |> ignore;
151151- Git.run_exn ~proc_mgr ~cwd:main_path
152152- ["commit"; "-m"; "Add project " ^ name] |> ignore;
158158+ Git.run_exn ~proc_mgr ~cwd:main_path [ "add"; "unpac.toml" ] |> ignore;
159159+ Git.run_exn ~proc_mgr ~cwd:main_path [ "commit"; "-m"; "Add project " ^ name ]
160160+ |> ignore;
153161154162 project_path
155163···161169 (* Delete the branch *)
162170 let git = Worktree.git_dir root in
163171 let branch = Worktree.branch (Project name) in
164164- Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; branch] |> ignore
172172+ Git.run_exn ~proc_mgr ~cwd:git [ "branch"; "-D"; branch ] |> ignore
+10-13
lib/init.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Project initialization for unpac.
2738 Creates the bare repository structure and initial main worktree. *)
···1722(** [is_unpac_root path] checks if [path] is an unpac project root. *)
18231924val find_root :
2020- fs:Eio.Fs.dir_ty Eio.Path.t ->
2121- cwd:string ->
2222- Worktree.root option
2525+ fs:Eio.Fs.dir_ty Eio.Path.t -> cwd:string -> Worktree.root option
2326(** [find_root ~fs ~cwd] walks up from [cwd] to find the unpac root. *)
24272528val create_project :
2626- proc_mgr:Git.proc_mgr ->
2727- Worktree.root ->
2828- string ->
2929- Eio.Fs.dir_ty Eio.Path.t
2929+ proc_mgr:Git.proc_mgr -> Worktree.root -> string -> Eio.Fs.dir_ty Eio.Path.t
3030(** [create_project ~proc_mgr root name] creates a new project branch.
31313232 Creates orphan branch [project/<name>] with template:
···36363737 Updates main/unpac.toml to register the project. *)
38383939-val remove_project :
4040- proc_mgr:Git.proc_mgr ->
4141- Worktree.root ->
4242- string ->
4343- unit
4444-(** [remove_project ~proc_mgr root name] removes a project branch and worktree. *)
3939+val remove_project : proc_mgr:Git.proc_mgr -> Worktree.root -> string -> unit
4040+(** [remove_project ~proc_mgr root name] removes a project branch and worktree.
4141+*)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Opam backend for unpac.
2738 Implements vendoring of opam packages using the three-tier branch model:
49 - opam/upstream/<pkg> - pristine upstream code
55- - opam/vendor/<pkg> - upstream history rewritten with vendor/opam/<pkg>/ prefix
1010+ - opam/vendor/<pkg> - upstream history rewritten with vendor/opam/<pkg>/
1111+ prefix
612 - opam/patches/<pkg> - local modifications
713814 The vendor branch preserves full git history from upstream, with all paths
···3844 Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 prefix_dir;
39454046 let rec copy_dir src dst =
4141- Eio.Path.read_dir src |> List.iter (fun name ->
4242- let src_path = Eio.Path.(src / name) in
4343- let dst_path = Eio.Path.(dst / name) in
4444- if Eio.Path.is_directory src_path then begin
4545- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 dst_path;
4646- copy_dir src_path dst_path
4747- end else begin
4848- let content = Eio.Path.load src_path in
4949- Eio.Path.save ~create:(`Or_truncate 0o644) dst_path content
5050- end
5151- )
4747+ Eio.Path.read_dir src
4848+ |> List.iter (fun name ->
4949+ let src_path = Eio.Path.(src / name) in
5050+ let dst_path = Eio.Path.(dst / name) in
5151+ if Eio.Path.is_directory src_path then begin
5252+ Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 dst_path;
5353+ copy_dir src_path dst_path
5454+ end
5555+ else begin
5656+ let content = Eio.Path.load src_path in
5757+ Eio.Path.save ~create:(`Or_truncate 0o644) dst_path content
5858+ end)
5259 in
53605461 (* Copy everything except .git *)
5555- Eio.Path.read_dir src_dir |> List.iter (fun name ->
5656- if name <> ".git" then begin
5757- let src_path = Eio.Path.(src_dir / name) in
5858- let dst_path = Eio.Path.(prefix_dir / name) in
5959- if Eio.Path.is_directory src_path then begin
6060- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 dst_path;
6161- copy_dir src_path dst_path
6262- end else begin
6363- let content = Eio.Path.load src_path in
6464- Eio.Path.save ~create:(`Or_truncate 0o644) dst_path content
6565- end
6666- end
6767- )
6262+ Eio.Path.read_dir src_dir
6363+ |> List.iter (fun name ->
6464+ if name <> ".git" then begin
6565+ let src_path = Eio.Path.(src_dir / name) in
6666+ let dst_path = Eio.Path.(prefix_dir / name) in
6767+ if Eio.Path.is_directory src_path then begin
6868+ Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 dst_path;
6969+ copy_dir src_path dst_path
7070+ end
7171+ else begin
7272+ let content = Eio.Path.load src_path in
7373+ Eio.Path.save ~create:(`Or_truncate 0o644) dst_path content
7474+ end
7575+ end)
68766977let add_package ~proc_mgr ~root ?cache (info : Backend.package_info) =
7078 let pkg = info.name in
···7987 let url = Git_repo_lookup.rewrite_url info.url in
80888189 (* Determine the ref to use: explicit > override > default *)
8282- let branch = match info.branch with
9090+ let branch =
9191+ match info.branch with
8392 | Some b -> b
8484- | None ->
9393+ | None -> (
8594 match Git_repo_lookup.branch_override ~name:pkg ~url with
8695 | Some b -> b
8787- | None -> Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url
9696+ | None -> Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url)
8897 in
89989099 (* Fetch - either via cache or directly *)
9191- let ref_point = match cache with
100100+ let ref_point =
101101+ match cache with
92102 | Some cache_path ->
93103 (* Fetch through vendor cache *)
9494- Vendor_cache.fetch_to_project ~proc_mgr
9595- ~cache:cache_path ~project_git:git ~url ~branch
104104+ Vendor_cache.fetch_to_project ~proc_mgr ~cache:cache_path
105105+ ~project_git:git ~url ~branch
96106 | None ->
97107 (* Direct fetch *)
98108 let remote = "origin-" ^ pkg in
···102112 in
103113104114 (* Step 1: Create upstream branch from fetched ref *)
105105- Git.branch_force ~proc_mgr ~cwd:git
106106- ~name:(upstream_branch pkg) ~point:ref_point;
115115+ Git.branch_force ~proc_mgr ~cwd:git ~name:(upstream_branch pkg)
116116+ ~point:ref_point;
107117108118 (* Step 2: Create vendor branch from upstream and rewrite history *)
109109- Git.branch_force ~proc_mgr ~cwd:git
110110- ~name:(vendor_branch pkg) ~point:(upstream_branch pkg);
119119+ Git.branch_force ~proc_mgr ~cwd:git ~name:(vendor_branch pkg)
120120+ ~point:(upstream_branch pkg);
111121112122 (* Rewrite vendor branch history to move all files into vendor/opam/<pkg>/ *)
113123 Git.filter_repo_to_subdirectory ~proc_mgr ~cwd:git
114114- ~branch:(vendor_branch pkg)
115115- ~subdirectory:(vendor_path pkg);
124124+ ~branch:(vendor_branch pkg) ~subdirectory:(vendor_path pkg);
116125117126 (* Get the vendor SHA after rewriting *)
118118- let vendor_sha = match Git.rev_parse ~proc_mgr ~cwd:git (vendor_branch pkg) with
127127+ let vendor_sha =
128128+ match Git.rev_parse ~proc_mgr ~cwd:git (vendor_branch pkg) with
119129 | Some sha -> sha
120130 | None -> failwith "Vendor branch not found after filter-repo"
121131 in
122132123133 (* Step 3: Create patches branch from vendor *)
124124- Git.branch_create ~proc_mgr ~cwd:git
125125- ~name:(patches_branch pkg)
134134+ Git.branch_create ~proc_mgr ~cwd:git ~name:(patches_branch pkg)
126135 ~start_point:(vendor_branch pkg);
127136128137 Backend.Added { name = pkg; sha = vendor_sha }
···143152 else begin
144153 (* Get remote URL - check vendor-cache remote first, then origin-<pkg> *)
145154 let remote = "origin-" ^ pkg in
146146- let url = match Git.remote_url ~proc_mgr ~cwd:git remote with
155155+ let url =
156156+ match Git.remote_url ~proc_mgr ~cwd:git remote with
147157 | Some u -> u
148158 | None -> failwith ("Remote not found: " ^ remote)
149159 in
150160151161 (* Fetch latest - either via cache or directly *)
152162 (match cache with
153153- | Some cache_path ->
154154- let branch = Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url in
155155- ignore (Vendor_cache.fetch_to_project ~proc_mgr
156156- ~cache:cache_path ~project_git:git ~url ~branch)
157157- | None ->
158158- Git.fetch ~proc_mgr ~cwd:git ~remote);
163163+ | Some cache_path ->
164164+ let branch = Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url in
165165+ ignore
166166+ (Vendor_cache.fetch_to_project ~proc_mgr ~cache:cache_path
167167+ ~project_git:git ~url ~branch)
168168+ | None -> Git.fetch ~proc_mgr ~cwd:git ~remote);
159169160170 (* Get old SHA *)
161161- let old_sha = match Git.rev_parse ~proc_mgr ~cwd:git (upstream_branch pkg) with
171171+ let old_sha =
172172+ match Git.rev_parse ~proc_mgr ~cwd:git (upstream_branch pkg) with
162173 | Some sha -> sha
163174 | None -> failwith "Upstream branch not found"
164175 in
165176166177 (* Determine default branch and update upstream *)
167167- let default_branch = Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url in
178178+ let default_branch =
179179+ Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url
180180+ in
168181 let ref_point = remote ^ "/" ^ default_branch in
169169- Git.branch_force ~proc_mgr ~cwd:git
170170- ~name:(upstream_branch pkg) ~point:ref_point;
182182+ Git.branch_force ~proc_mgr ~cwd:git ~name:(upstream_branch pkg)
183183+ ~point:ref_point;
171184172185 (* Get new SHA *)
173173- let new_sha = match Git.rev_parse ~proc_mgr ~cwd:git (upstream_branch pkg) with
186186+ let new_sha =
187187+ match Git.rev_parse ~proc_mgr ~cwd:git (upstream_branch pkg) with
174188 | Some sha -> sha
175189 | None -> failwith "Upstream branch not found"
176190 in
177191178178- if old_sha = new_sha then
179179- Backend.No_changes pkg
192192+ if old_sha = new_sha then Backend.No_changes pkg
180193 else begin
181194 (* Create worktrees *)
182195 Worktree.ensure ~proc_mgr root (upstream_kind pkg);
···189202 let vendor_pkg_path = Eio.Path.(vendor_wt / "vendor" / "opam" / pkg) in
190203 (try Eio.Path.rmtree vendor_pkg_path with _ -> ());
191204192192- copy_with_prefix
193193- ~src_dir:upstream_wt
194194- ~dst_dir:vendor_wt
205205+ copy_with_prefix ~src_dir:upstream_wt ~dst_dir:vendor_wt
195206 ~prefix:(vendor_path pkg);
196207197208 (* Commit *)
198209 Git.add_all ~proc_mgr ~cwd:vendor_wt;
199210 Git.commit ~proc_mgr ~cwd:vendor_wt
200200- ~message:(Printf.sprintf "Update %s to %s" pkg (String.sub new_sha 0 7));
211211+ ~message:
212212+ (Printf.sprintf "Update %s to %s" pkg (String.sub new_sha 0 7));
201213202214 (* Cleanup *)
203215 Worktree.remove ~proc_mgr root (upstream_kind pkg);
···211223 (try Worktree.remove_force ~proc_mgr root (vendor_kind pkg) with _ -> ());
212224 Backend.Update_failed { name = pkg; error = Printexc.to_string exn }
213225214214-let list_packages ~proc_mgr ~root =
215215- Worktree.list_opam_packages ~proc_mgr root
226226+let list_packages ~proc_mgr ~root = Worktree.list_opam_packages ~proc_mgr root
+13-9
lib/opam/opam.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Opam backend for unpac.
2738 Implements vendoring of opam packages using the three-tier branch model:
···3843 Unpac.Backend.add_result
3944(** [add_package ~proc_mgr ~root ?cache info] vendors a single package.
40454141- 1. Fetches upstream into opam/upstream/<pkg> (via cache if provided)
4242- 2. Creates opam/vendor/<pkg> with vendor/opam/<pkg>/ prefix (preserving history)
4343- 3. Creates opam/patches/<pkg> from vendor
4646+ 1. Fetches upstream into opam/upstream/<pkg> (via cache if provided) 2.
4747+ Creates opam/vendor/<pkg> with vendor/opam/<pkg>/ prefix (preserving
4848+ history) 3. Creates opam/patches/<pkg> from vendor
44494550 Uses git-filter-repo for fast history rewriting.
4651 @param cache Optional vendor cache for shared fetches across projects. *)
···5156 ?cache:Unpac.Vendor_cache.t ->
5257 string ->
5358 Unpac.Backend.update_result
5454-(** [update_package ~proc_mgr ~root ?cache name] updates a package from upstream.
5959+(** [update_package ~proc_mgr ~root ?cache name] updates a package from
6060+ upstream.
55615656- 1. Fetches latest into opam/upstream/<pkg> (via cache if provided)
5757- 2. Updates opam/vendor/<pkg> with new content
6262+ 1. Fetches latest into opam/upstream/<pkg> (via cache if provided) 2.
6363+ Updates opam/vendor/<pkg> with new content
58645965 Does NOT rebase patches - call [Backend.rebase_patches] separately.
60666167 @param cache Optional vendor cache for shared fetches across projects. *)
62686369val list_packages :
6464- proc_mgr:Unpac.Git.proc_mgr ->
6565- root:Unpac.Worktree.root ->
6666- string list
7070+ proc_mgr:Unpac.Git.proc_mgr -> root:Unpac.Worktree.root -> string list
6771(** [list_packages ~proc_mgr root] returns all vendored opam package names. *)
+50-45
lib/opam/opam_file.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Opam file parsing for extracting package metadata. *)
2738type metadata = {
···712 synopsis : string option;
813}
9141010-let empty_metadata = {
1111- name = "";
1212- version = "";
1313- dev_repo = None;
1414- synopsis = None;
1515-}
1515+let empty_metadata =
1616+ { name = ""; version = ""; dev_repo = None; synopsis = None }
16171718(** Parse an opam file and extract metadata. *)
1819let parse ~name ~version content =
···2324 let dev_repo = ref None in
2425 let synopsis = ref None in
25262626- List.iter (fun item ->
2727- match item.OpamParserTypes.FullPos.pelem with
2828- | OpamParserTypes.FullPos.Variable (name_pos, value_pos) ->
2929- let var_name = name_pos.OpamParserTypes.FullPos.pelem in
3030- (match var_name, value_pos.OpamParserTypes.FullPos.pelem with
3131- | "dev-repo", OpamParserTypes.FullPos.String s ->
3232- dev_repo := Some s
3333- | "synopsis", OpamParserTypes.FullPos.String s ->
3434- synopsis := Some s
3535- | _ -> ())
3636- | _ -> ()
3737- ) items;
2727+ List.iter
2828+ (fun item ->
2929+ match item.OpamParserTypes.FullPos.pelem with
3030+ | OpamParserTypes.FullPos.Variable (name_pos, value_pos) -> (
3131+ let var_name = name_pos.OpamParserTypes.FullPos.pelem in
3232+ match (var_name, value_pos.OpamParserTypes.FullPos.pelem) with
3333+ | "dev-repo", OpamParserTypes.FullPos.String s -> dev_repo := Some s
3434+ | "synopsis", OpamParserTypes.FullPos.String s -> synopsis := Some s
3535+ | _ -> ())
3636+ | _ -> ())
3737+ items;
38383939 { name; version; dev_repo = !dev_repo; synopsis = !synopsis }
4040- with _ ->
4141- { empty_metadata with name; version }
4040+ with _ -> { empty_metadata with name; version }
42414342(** Parse an opam file from a path. *)
4443let parse_file ~name ~version path =
4544 let content = In_channel.with_open_text path In_channel.input_all in
4645 parse ~name ~version content
47464848-(** Find a package in an opam repository directory.
4949- Returns the path to the opam file if found. *)
4747+(** Find a package in an opam repository directory. Returns the path to the opam
4848+ file if found. *)
5049let find_in_repo ~repo_path ~name ?version () =
5150 let packages_dir = Filename.concat repo_path "packages" in
5251 let pkg_dir = Filename.concat packages_dir name in
53525454- if not (Sys.file_exists pkg_dir && Sys.is_directory pkg_dir) then
5555- None
5353+ if not (Sys.file_exists pkg_dir && Sys.is_directory pkg_dir) then None
5654 else
5755 (* List version directories *)
5856 let entries = Sys.readdir pkg_dir |> Array.to_list in
5959- let version_dirs = List.filter (fun entry ->
6060- let full = Filename.concat pkg_dir entry in
6161- Sys.is_directory full && String.starts_with ~prefix:(name ^ ".") entry
6262- ) entries in
5757+ let version_dirs =
5858+ List.filter
5959+ (fun entry ->
6060+ let full = Filename.concat pkg_dir entry in
6161+ Sys.is_directory full && String.starts_with ~prefix:(name ^ ".") entry)
6262+ entries
6363+ in
63646465 match version with
6566 | Some v ->
6667 (* Look for specific version *)
6768 let target = name ^ "." ^ v in
6869 if List.mem target version_dirs then
6969- let opam_path = Filename.concat (Filename.concat pkg_dir target) "opam" in
7070- if Sys.file_exists opam_path then Some (opam_path, v)
7171- else None
7070+ let opam_path =
7171+ Filename.concat (Filename.concat pkg_dir target) "opam"
7272+ in
7373+ if Sys.file_exists opam_path then Some (opam_path, v) else None
7274 else None
7373- | None ->
7575+ | None -> (
7476 (* Find latest version (simple string sort, works for semver) *)
7577 let sorted = List.sort (fun a b -> String.compare b a) version_dirs in
7678 match sorted with
7779 | [] -> None
7880 | latest :: _ ->
7979- let v = String.sub latest (String.length name + 1)
8080- (String.length latest - String.length name - 1) in
8181- let opam_path = Filename.concat (Filename.concat pkg_dir latest) "opam" in
8282- if Sys.file_exists opam_path then Some (opam_path, v)
8383- else None
8181+ let v =
8282+ String.sub latest
8383+ (String.length name + 1)
8484+ (String.length latest - String.length name - 1)
8585+ in
8686+ let opam_path =
8787+ Filename.concat (Filename.concat pkg_dir latest) "opam"
8888+ in
8989+ if Sys.file_exists opam_path then Some (opam_path, v) else None)
84908591(** Get metadata for a package from an opam repository. *)
8692let get_metadata ~repo_path ~name ?version () =
8793 match find_in_repo ~repo_path ~name ?version () with
8894 | None -> None
8989- | Some (opam_path, v) ->
9090- Some (parse_file ~name ~version:v opam_path)
9595+ | Some (opam_path, v) -> Some (parse_file ~name ~version:v opam_path)
91969297(** List all versions of a package in a repository. *)
9398let list_versions ~repo_path ~name =
9499 let packages_dir = Filename.concat repo_path "packages" in
95100 let pkg_dir = Filename.concat packages_dir name in
961019797- if not (Sys.file_exists pkg_dir && Sys.is_directory pkg_dir) then
9898- []
102102+ if not (Sys.file_exists pkg_dir && Sys.is_directory pkg_dir) then []
99103 else
100100- Sys.readdir pkg_dir
101101- |> Array.to_list
104104+ Sys.readdir pkg_dir |> Array.to_list
102105 |> List.filter_map (fun entry ->
103106 if String.starts_with ~prefix:(name ^ ".") entry then
104104- Some (String.sub entry (String.length name + 1)
105105- (String.length entry - String.length name - 1))
107107+ Some
108108+ (String.sub entry
109109+ (String.length name + 1)
110110+ (String.length entry - String.length name - 1))
106111 else None)
107112 |> List.sort String.compare
+19-6
lib/opam/opam_file.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Opam file parsing for extracting package metadata. *)
2738type metadata = {
···1318val parse_file : name:string -> version:string -> string -> metadata
1419(** [parse_file ~name ~version path] parses an opam file from disk. *)
15201616-val find_in_repo : repo_path:string -> name:string -> ?version:string -> unit -> (string * string) option
1717-(** [find_in_repo ~repo_path ~name ?version ()] finds a package in an opam repository.
1818- Returns [Some (opam_file_path, version)] if found. *)
2121+val find_in_repo :
2222+ repo_path:string ->
2323+ name:string ->
2424+ ?version:string ->
2525+ unit ->
2626+ (string * string) option
2727+(** [find_in_repo ~repo_path ~name ?version ()] finds a package in an opam
2828+ repository. Returns [Some (opam_file_path, version)] if found. *)
19292020-val get_metadata : repo_path:string -> name:string -> ?version:string -> unit -> metadata option
2121-(** [get_metadata ~repo_path ~name ?version ()] gets package metadata from a repository. *)
3030+val get_metadata :
3131+ repo_path:string -> name:string -> ?version:string -> unit -> metadata option
3232+(** [get_metadata ~repo_path ~name ?version ()] gets package metadata from a
3333+ repository. *)
22342335val list_versions : repo_path:string -> name:string -> string list
2424-(** [list_versions ~repo_path ~name] lists all available versions of a package. *)
3636+(** [list_versions ~repo_path ~name] lists all available versions of a package.
3737+*)
+48-42
lib/opam/repo.ml
···11-(** Opam repository operations. *)
11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
2533-type repo = {
44- name : string;
55- path : string;
66-}
66+(** Opam repository operations. *)
7788-type search_result = {
99- repo : repo;
1010- metadata : Opam_file.metadata;
1111-}
88+type repo = { name : string; path : string }
99+type search_result = { repo : repo; metadata : Opam_file.metadata }
12101311(** Resolve repository path from config. *)
1412let resolve_repo (cfg : Unpac.Config.repo_config) : repo option =
···2523let find_package ~repos ~name ?version () : search_result option =
2624 let rec search = function
2725 | [] -> None
2828- | cfg :: rest ->
2626+ | cfg :: rest -> (
2927 match resolve_repo cfg with
3028 | None -> search rest
3131- | Some repo ->
3232- match Opam_file.get_metadata ~repo_path:repo.path ~name ?version () with
2929+ | Some repo -> (
3030+ match
3131+ Opam_file.get_metadata ~repo_path:repo.path ~name ?version ()
3232+ with
3333 | None -> search rest
3434- | Some metadata -> Some { repo; metadata }
3434+ | Some metadata -> Some { repo; metadata }))
3535 in
3636 search repos
37373838(** List all versions of a package across repositories. *)
3939let list_versions ~repos ~name : (repo * string list) list =
4040- List.filter_map (fun cfg ->
4141- match resolve_repo cfg with
4242- | None -> None
4343- | Some repo ->
4444- let versions = Opam_file.list_versions ~repo_path:repo.path ~name in
4545- if versions = [] then None
4646- else Some (repo, versions)
4747- ) repos
4040+ List.filter_map
4141+ (fun cfg ->
4242+ match resolve_repo cfg with
4343+ | None -> None
4444+ | Some repo ->
4545+ let versions = Opam_file.list_versions ~repo_path:repo.path ~name in
4646+ if versions = [] then None else Some (repo, versions))
4747+ repos
48484949(** Search for packages matching a pattern. *)
5050let search_packages ~repos ~pattern : (repo * string) list =
5151- List.concat_map (fun cfg ->
5252- match resolve_repo cfg with
5353- | None -> []
5454- | Some repo ->
5555- let packages_dir = Filename.concat repo.path "packages" in
5656- if not (Sys.file_exists packages_dir) then []
5757- else
5858- Sys.readdir packages_dir
5959- |> Array.to_list
6060- |> List.filter (fun name ->
6161- (* Simple substring match *)
6262- let pattern_lower = String.lowercase_ascii pattern in
6363- let name_lower = String.lowercase_ascii name in
6464- String.length pattern_lower <= String.length name_lower &&
6565- (let rec check i =
6666- if i > String.length name_lower - String.length pattern_lower then false
6767- else if String.sub name_lower i (String.length pattern_lower) = pattern_lower then true
6868- else check (i + 1)
6969- in check 0))
7070- |> List.map (fun name -> (repo, name))
7171- ) repos
5151+ List.concat_map
5252+ (fun cfg ->
5353+ match resolve_repo cfg with
5454+ | None -> []
5555+ | Some repo ->
5656+ let packages_dir = Filename.concat repo.path "packages" in
5757+ if not (Sys.file_exists packages_dir) then []
5858+ else
5959+ Sys.readdir packages_dir |> Array.to_list
6060+ |> List.filter (fun name ->
6161+ (* Simple substring match *)
6262+ let pattern_lower = String.lowercase_ascii pattern in
6363+ let name_lower = String.lowercase_ascii name in
6464+ String.length pattern_lower <= String.length name_lower
6565+ &&
6666+ let rec check i =
6767+ if i > String.length name_lower - String.length pattern_lower
6868+ then false
6969+ else if
7070+ String.sub name_lower i (String.length pattern_lower)
7171+ = pattern_lower
7272+ then true
7373+ else check (i + 1)
7474+ in
7575+ check 0)
7676+ |> List.map (fun name -> (repo, name)))
7777+ repos
+12-15
lib/opam/repo.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Opam repository operations. *)
2733-type repo = {
44- name : string;
55- path : string;
66-}
77-88-type search_result = {
99- repo : repo;
1010- metadata : Opam_file.metadata;
1111-}
88+type repo = { name : string; path : string }
99+type search_result = { repo : repo; metadata : Opam_file.metadata }
12101311val find_package :
1412 repos:Unpac.Config.repo_config list ->
···1614 ?version:string ->
1715 unit ->
1816 search_result option
1919-(** [find_package ~repos ~name ?version ()] searches for a package in repositories.
2020- Returns the first match found. *)
1717+(** [find_package ~repos ~name ?version ()] searches for a package in
1818+ repositories. Returns the first match found. *)
21192220val list_versions :
2321 repos:Unpac.Config.repo_config list ->
···2624(** [list_versions ~repos ~name] lists all versions across repositories. *)
27252826val search_packages :
2929- repos:Unpac.Config.repo_config list ->
3030- pattern:string ->
3131- (repo * string) list
3232-(** [search_packages ~repos ~pattern] searches for packages matching a pattern. *)
2727+ repos:Unpac.Config.repo_config list -> pattern:string -> (repo * string) list
2828+(** [search_packages ~repos ~pattern] searches for packages matching a pattern.
2929+*)
+72-60
lib/opam/solver.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Dependency solver using 0install algorithm. *)
2738let ( / ) = Filename.concat
49510(** List directory entries, returns empty list if directory doesn't exist. *)
611let list_dir path =
77- try Sys.readdir path |> Array.to_list
88- with Sys_error _ -> []
1212+ try Sys.readdir path |> Array.to_list with Sys_error _ -> []
9131014(** Known compiler packages to filter out. *)
1115let is_compiler_package name =
1216 let s = OpamPackage.Name.to_string name in
1313- String.starts_with ~prefix:"ocaml-base-compiler" s ||
1414- String.starts_with ~prefix:"ocaml-variants" s ||
1515- String.starts_with ~prefix:"ocaml-system" s ||
1616- String.starts_with ~prefix:"ocaml-config" s ||
1717- s = "ocaml" ||
1818- s = "base-unix" ||
1919- s = "base-threads" ||
2020- s = "base-bigarray" ||
2121- s = "base-domains" ||
2222- s = "base-nnp"
1717+ String.starts_with ~prefix:"ocaml-base-compiler" s
1818+ || String.starts_with ~prefix:"ocaml-variants" s
1919+ || String.starts_with ~prefix:"ocaml-system" s
2020+ || String.starts_with ~prefix:"ocaml-config" s
2121+ || s = "ocaml" || s = "base-unix" || s = "base-threads" || s = "base-bigarray"
2222+ || s = "base-domains" || s = "base-nnp"
23232424(** Check if a package has the compiler flag. *)
2525let has_compiler_flag opam =
···3434 ?constraints:OpamFormula.version_constraint OpamTypes.name_map ->
3535 repos:string list ->
3636 ocaml_version:string ->
3737- unit -> t
3737+ unit ->
3838+ t
3839end = struct
3940 type rejection =
4041 | UserConstraint of OpamFormula.atom
···4243 | CompilerPackage
43444445 let pp_rejection f = function
4545- | UserConstraint x -> Fmt.pf f "Rejected by user-specified constraint %s" (OpamFormula.string_of_atom x)
4646+ | UserConstraint x ->
4747+ Fmt.pf f "Rejected by user-specified constraint %s"
4848+ (OpamFormula.string_of_atom x)
4649 | Unavailable -> Fmt.pf f "Availability condition not satisfied"
4750 | CompilerPackage -> Fmt.pf f "Compiler package (filtered out)"
48514952 type t = {
5050- repos : string list; (* List of packages/ directories *)
5353+ repos : string list; (* List of packages/ directories *)
5154 constraints : OpamFormula.version_constraint OpamTypes.name_map;
5255 ocaml_version : string;
5356 }
···6770 let filter_deps t pkg f =
6871 f
6972 |> OpamFilter.partial_filter_formula (env t pkg)
7070- |> OpamFilter.filter_deps ~build:true ~post:true ~test:false ~doc:false ~dev:false ~dev_setup:false ~default:false
7373+ |> OpamFilter.filter_deps ~build:true ~post:true ~test:false ~doc:false
7474+ ~dev:false ~dev_setup:false ~default:false
71757276 let user_restrictions t name =
7377 OpamPackage.Name.Map.find_opt name t.constraints
···7882 with _ -> None
79838084 (** Create a minimal virtual opam file for base packages. *)
8181- let virtual_opam () =
8282- OpamFile.OPAM.empty
8585+ let virtual_opam () = OpamFile.OPAM.empty
83868487 (** Find all versions of a package across all repos. *)
8588 let find_versions t name =
8689 let name_str = OpamPackage.Name.to_string name in
8790 (* Collect versions from all repos, first repo wins for duplicates *)
8891 let seen = Hashtbl.create 16 in
8989- List.iter (fun packages_dir ->
9090- let pkg_dir = packages_dir / name_str in
9191- list_dir pkg_dir |> List.iter (fun entry ->
9292- match OpamPackage.of_string_opt entry with
9393- | Some pkg when OpamPackage.name pkg = name ->
9494- let v = OpamPackage.version pkg in
9595- if not (Hashtbl.mem seen v) then begin
9696- let opam_path = pkg_dir / entry / "opam" in
9797- Hashtbl.add seen v opam_path
9898- end
9999- | _ -> ()
100100- )
101101- ) t.repos;
9292+ List.iter
9393+ (fun packages_dir ->
9494+ let pkg_dir = packages_dir / name_str in
9595+ list_dir pkg_dir
9696+ |> List.iter (fun entry ->
9797+ match OpamPackage.of_string_opt entry with
9898+ | Some pkg when OpamPackage.name pkg = name ->
9999+ let v = OpamPackage.version pkg in
100100+ if not (Hashtbl.mem seen v) then begin
101101+ let opam_path = pkg_dir / entry / "opam" in
102102+ Hashtbl.add seen v opam_path
103103+ end
104104+ | _ -> ()))
105105+ t.repos;
102106 Hashtbl.fold (fun v path acc -> (v, path) :: acc) seen []
103107104108 let candidates t name =
···106110 (* Provide virtual packages for compiler/base packages at the configured version *)
107111 if name_str = "ocaml" then
108112 let v = OpamPackage.Version.of_string t.ocaml_version in
109109- [v, Ok (virtual_opam ())]
110110- else if name_str = "base-unix" || name_str = "base-threads" ||
111111- name_str = "base-bigarray" || name_str = "base-domains" ||
112112- name_str = "base-nnp" then
113113+ [ (v, Ok (virtual_opam ())) ]
114114+ else if
115115+ name_str = "base-unix" || name_str = "base-threads"
116116+ || name_str = "base-bigarray" || name_str = "base-domains"
117117+ || name_str = "base-nnp"
118118+ then
113119 let v = OpamPackage.Version.of_string "base" in
114114- [v, Ok (virtual_opam ())]
120120+ [ (v, Ok (virtual_opam ())) ]
115121 else if is_compiler_package name then
116122 (* Other compiler packages - not available *)
117123 []
118124 else
119125 let user_constraints = user_restrictions t name in
120126 find_versions t name
121121- |> List.sort (fun (v1, _) (v2, _) -> OpamPackage.Version.compare v2 v1) (* Prefer newest *)
127127+ |> List.sort (fun (v1, _) (v2, _) -> OpamPackage.Version.compare v2 v1)
128128+ (* Prefer newest *)
122129 |> List.map (fun (v, opam_path) ->
123130 match user_constraints with
124124- | Some test when not (OpamFormula.check_version_formula (OpamFormula.Atom test) v) ->
125125- v, Error (UserConstraint (name, Some test))
126126- | _ ->
131131+ | Some test
132132+ when not
133133+ (OpamFormula.check_version_formula (OpamFormula.Atom test) v)
134134+ ->
135135+ (v, Error (UserConstraint (name, Some test)))
136136+ | _ -> (
127137 match load_opam opam_path with
128128- | None -> v, Error Unavailable
129129- | Some opam ->
130130- (* Check flags:compiler *)
131131- if has_compiler_flag opam then
132132- v, Error CompilerPackage
138138+ | None -> (v, Error Unavailable)
139139+ | Some opam -> (
140140+ if
141141+ (* Check flags:compiler *)
142142+ has_compiler_flag opam
143143+ then (v, Error CompilerPackage)
133144 else
134145 (* Check available filter *)
135146 let pkg = OpamPackage.create name v in
136147 let available = OpamFile.OPAM.available opam in
137137- match OpamFilter.eval ~default:(OpamTypes.B false) (env t pkg) available with
138138- | B true -> v, Ok opam
139139- | _ -> v, Error Unavailable
140140- )
148148+ match
149149+ OpamFilter.eval ~default:(OpamTypes.B false) (env t pkg)
150150+ available
151151+ with
152152+ | B true -> (v, Ok opam)
153153+ | _ -> (v, Error Unavailable))))
141154142142- let create ?(constraints=OpamPackage.Name.Map.empty) ~repos ~ocaml_version () =
155155+ let create ?(constraints = OpamPackage.Name.Map.empty) ~repos ~ocaml_version
156156+ () =
143157 (* Convert repo roots to packages/ directories *)
144158 let packages_dirs = List.map (fun r -> r / "packages") repos in
145159 { repos = packages_dirs; constraints; ocaml_version }
146160end
147161148148-module Solver = Opam_0install.Solver.Make(Multi_context)
162162+module Solver = Opam_0install.Solver.Make (Multi_context)
149163150150-type solve_result = {
151151- packages : OpamPackage.t list;
152152-}
153153-164164+type solve_result = { packages : OpamPackage.t list }
154165type solve_error = string
155166156167(** Solve dependencies for a list of package names. *)
···161172 | Ok selections ->
162173 let pkgs = Solver.packages_of_result selections in
163174 (* Filter out compiler packages from result *)
164164- let pkgs = List.filter (fun pkg ->
165165- not (is_compiler_package (OpamPackage.name pkg))
166166- ) pkgs in
175175+ let pkgs =
176176+ List.filter
177177+ (fun pkg -> not (is_compiler_package (OpamPackage.name pkg)))
178178+ pkgs
179179+ in
167180 Ok { packages = pkgs }
168168- | Error diagnostics ->
169169- Error (Solver.diagnostics diagnostics)
181181+ | Error diagnostics -> Error (Solver.diagnostics diagnostics)
+9-3
lib/opam/solver.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Dependency solver using 0install algorithm.
2738 Solves package dependencies across multiple configured opam repositories,
···510611type solve_result = {
712 packages : OpamPackage.t list;
88- (** List of packages that need to be installed, including transitive deps. *)
1313+ (** List of packages that need to be installed, including transitive deps.
1414+ *)
915}
10161117type solve_error = string
···2935 filtered out since they are assumed to be pre-installed. *)
30363137val is_compiler_package : OpamPackage.Name.t -> bool
3232-(** [is_compiler_package name] returns true if [name] is a known compiler
3333- or base package that should be filtered out. *)
3838+(** [is_compiler_package name] returns true if [name] is a known compiler or
3939+ base package that should be filtered out. *)