A CLI tool that generates an opam repository and monorepo from a list of git repos
0
fork

Configure Feed

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

Rename to monopam and prepare for opam release

- Rename project from repo-tool to monopam
- Add ISC license headers to source files
- Add LICENSE.md, .ocamlformat, updated .gitignore
- Add Tangled CI workflow (.tangled/workflows/build.yml)
- Update dune-project with proper metadata for Tangled hosting
- Regenerate opam file with correct maintainers and URLs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

+212 -96
+17 -1
.gitignore
··· 1 + # OCaml build artifacts 1 2 _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Third-party sources (fetch locally with opam source) 7 + third_party/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 2 17 _opam/ 18 + 19 + # Project-specific 3 20 monorepo/ 4 - *.swp
+1
.ocamlformat
··· 1 + version=0.28.1
+53
.tangled/workflows/build.yml
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + branch: ["main"] 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - shell 10 + - stdenv 11 + - findutils 12 + - binutils 13 + - libunwind 14 + - ncurses 15 + - opam 16 + - git 17 + - gawk 18 + - gnupatch 19 + - gnum4 20 + - gnumake 21 + - gnutar 22 + - gnused 23 + - gnugrep 24 + - diffutils 25 + - gzip 26 + - bzip2 27 + - gcc 28 + - ocaml 29 + - pkg-config 30 + 31 + steps: 32 + - name: opam 33 + command: | 34 + opam init --disable-sandboxing -a -y 35 + - name: repo 36 + command: | 37 + opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git 38 + - name: switch 39 + command: | 40 + opam install . --confirm-level=unsafe-yes --deps-only 41 + - name: build 42 + command: | 43 + opam exec -- dune build 44 + - name: switch-test 45 + command: | 46 + opam install . --confirm-level=unsafe-yes --deps-only --with-test 47 + - name: test 48 + command: | 49 + opam exec -- dune runtest --verbose 50 + - name: doc 51 + command: | 52 + opam install -y odoc 53 + opam exec -- dune build @doc
+15
LICENSE.md
··· 1 + ISC License 2 + 3 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org> 4 + 5 + Permission to use, copy, modify, and distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+7 -7
README.md
··· 1 - # repo-tool 1 + # monopam 2 2 3 3 A CLI tool that generates an opam repository and monorepo from a list of git repositories. 4 4 5 5 ## Overview 6 6 7 - `repo-tool` reads a text file containing git repository URLs and: 7 + `monopam` reads a text file containing git repository URLs and: 8 8 9 9 1. Clones each repository into a `vendor/` directory 10 10 2. Generates an opam repository structure in `opam-repository/` ··· 24 24 Or run directly: 25 25 26 26 ```bash 27 - dune exec repo-tool -- <args> 27 + dune exec monopam -- <args> 28 28 ``` 29 29 30 30 ## Usage 31 31 32 32 ```bash 33 - repo-tool INPUT_FILE [-o OUTPUT_DIR] [-v] 33 + monopam INPUT_FILE [-o OUTPUT_DIR] [-v] 34 34 ``` 35 35 36 36 ### Arguments ··· 72 72 73 73 ## Setting Up the Monorepo 74 74 75 - After running `repo-tool`, set up the development environment: 75 + After running `monopam`, set up the development environment: 76 76 77 77 ```bash 78 78 cd output-dir ··· 109 109 110 110 ## Incremental Updates 111 111 112 - Running `repo-tool` again on an existing output directory will: 112 + Running `monopam` again on an existing output directory will: 113 113 - Update existing repositories with `git pull` 114 114 - Clone any new repositories 115 115 - Regenerate the opam repository and setup script ··· 125 125 EOF 126 126 127 127 # Generate the monorepo 128 - repo-tool repos.txt -o my-monorepo -v 128 + monopam repos.txt -o my-monorepo -v 129 129 130 130 # Set up and build 131 131 cd my-monorepo
+2 -2
bin/dune
··· 1 1 (executable 2 2 (name main) 3 - (public_name repo-tool) 4 - (libraries repo_tool cmdliner unix)) 3 + (public_name monopam) 4 + (libraries monopam cmdliner unix))
+29 -8
bin/main.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 1 6 open Cmdliner 2 7 3 8 let input_file = 4 - let doc = "Path to the input file containing git repository URLs (one per line)." in 9 + let doc = 10 + "Path to the input file containing git repository URLs (one per line)." 11 + in 5 12 Arg.(value & pos 0 (some file) None & info [] ~docv:"INPUT_FILE" ~doc) 6 13 7 14 let opam_overlay = 8 - let doc = "Path to an opam overlay repository. Git URLs will be extracted from dev-repo fields in package opam files." in 15 + let doc = 16 + "Path to an opam overlay repository. Git URLs will be extracted from \ 17 + dev-repo fields in package opam files." 18 + in 9 19 Arg.(value & opt (some dir) None & info [ "opam-overlay" ] ~docv:"DIR" ~doc) 10 20 11 21 let output_dir = 12 22 let doc = "Output directory for the generated opam repository." in 13 - Arg.(value & opt string "opam-repository" & info [ "o"; "output" ] ~docv:"DIR" ~doc) 23 + Arg.( 24 + value 25 + & opt string "opam-repository" 26 + & info [ "o"; "output" ] ~docv:"DIR" ~doc) 14 27 15 28 let verbose = 16 29 let doc = "Enable verbose output." in 17 30 Arg.(value & flag & info [ "v"; "verbose" ] ~doc) 18 31 19 32 let use_submodules = 20 - let doc = "Add vendored repositories as git submodules instead of cloning them. This initializes the output directory as a git repository if needed." in 33 + let doc = 34 + "Add vendored repositories as git submodules instead of cloning them. This \ 35 + initializes the output directory as a git repository if needed." 36 + in 21 37 Arg.(value & flag & info [ "submodules" ] ~doc) 22 38 23 39 let run input_file opam_overlay output_dir use_submodules verbose = 24 - let exit_code = Repo_tool.run ~input_file ~opam_overlay ~output_dir ~use_submodules ~verbose in 40 + let exit_code = 41 + Monopam.run ~input_file ~opam_overlay ~output_dir ~use_submodules ~verbose 42 + in 25 43 exit exit_code 26 44 27 - let run_t = Term.(const run $ input_file $ opam_overlay $ output_dir $ use_submodules $ verbose) 45 + let run_t = 46 + Term.( 47 + const run $ input_file $ opam_overlay $ output_dir $ use_submodules 48 + $ verbose) 28 49 29 50 let cmd = 30 51 let doc = "Generate an opam repository from git repositories" in ··· 50 71 \ https://github.com/user/repo2.git main\n\ 51 72 \ # This is a comment"; 52 73 `S Manpage.s_bugs; 53 - `P "Report bugs at https://github.com/mtelvers/repo-tool/issues"; 74 + `P "Report bugs at https://github.com/mtelvers/monopam/issues"; 54 75 ] 55 76 in 56 - let info = Cmd.info "repo-tool" ~version:"0.1.0" ~doc ~man in 77 + let info = Cmd.info "monopam" ~version:"0.1.0" ~doc ~man in 57 78 Cmd.v info run_t 58 79 59 80 let () = exit (Cmd.eval cmd)
+15 -15
dune-project
··· 1 - (lang dune 3.0) 1 + (lang dune 3.18) 2 2 3 - (name repo_tool) 3 + (name monopam) 4 4 5 5 (generate_opam_files true) 6 6 7 - (source 8 - (github mtelvers/repo-tool)) 9 - 10 - (authors "Mark Elvers") 11 - 12 - (maintainers "Mark Elvers") 13 - 14 - (license MIT) 7 + (license ISC) 8 + (authors "Mark Elvers" "Anil Madhavapeddy") 9 + (homepage "https://tangled.org/@mtelvers.tunbury.org/repo-tool") 10 + (maintainers "Mark Elvers" "Anil Madhavapeddy <anil@recoil.org>") 11 + (bug_reports "https://tangled.org/@mtelvers.tunbury.org/repo-tool/issues") 15 12 16 13 (package 17 - (name repo_tool) 18 - (synopsis "Generate opam repository from git repositories") 14 + (name monopam) 15 + (synopsis "Turn an opam repository into a bunch of git submodules and sources") 19 16 (description 20 - "A CLI tool that reads a text file containing git repository URLs and generates an opam repository structure that can be overlaid on the official opam repository.") 17 + "monopam reads git repository URLs and generates an opam repository structure 18 + that can be overlaid on the official opam repository. It clones repositories 19 + into a vendor directory, creates an opam repository with dev packages, and 20 + sets up dune to build everything as a monorepo.") 21 21 (depends 22 22 (ocaml (>= 4.14)) 23 - dune 24 - cmdliner)) 23 + (cmdliner (>= 1.1.0)) 24 + (odoc :with-doc)))
+1 -1
lib/dune
··· 1 1 (library 2 - (name repo_tool) 2 + (name monopam) 3 3 (libraries unix))
+37 -31
lib/repo_tool.ml lib/monopam.ml
··· 1 - type repo_entry = { 2 - url : string; 3 - branch : string option; 4 - } 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type repo_entry = { url : string; branch : string option } 5 7 6 8 let verbose = ref false 7 9 ··· 53 55 let run_command cmd = 54 56 let exit_code = Sys.command cmd in 55 57 if exit_code = 0 then Ok () 56 - else Error (Printf.sprintf "Command failed with exit code %d: %s" exit_code cmd) 58 + else 59 + Error (Printf.sprintf "Command failed with exit code %d: %s" exit_code cmd) 57 60 58 61 let rec mkdir_p path = 59 62 if Sys.file_exists path then () ··· 79 82 let rel_path = "vendor/" ^ name in 80 83 if Sys.file_exists target then begin 81 84 log "Updating submodule %s" rel_path; 82 - let cmd = Printf.sprintf "git -C %s submodule update --remote %s 2>/dev/null" output_dir rel_path in 85 + let cmd = 86 + Printf.sprintf "git -C %s submodule update --remote %s 2>/dev/null" 87 + output_dir rel_path 88 + in 83 89 match run_command cmd with 84 90 | Ok () -> Ok target 85 - | Error _ -> 91 + | Error _ -> ( 86 92 (* Try to re-add the submodule *) 87 93 log "Submodule update failed, trying to re-add %s" entry.url; 88 - let rm_cmd = Printf.sprintf "git -C %s rm -f %s 2>/dev/null" output_dir rel_path in 94 + let rm_cmd = 95 + Printf.sprintf "git -C %s rm -f %s 2>/dev/null" output_dir rel_path 96 + in 89 97 ignore (run_command rm_cmd); 90 98 let rm_dir_cmd = Printf.sprintf "rm -rf %s" target in 91 99 ignore (run_command rm_dir_cmd); ··· 95 103 | None -> "" 96 104 in 97 105 let add_cmd = 98 - Printf.sprintf "git -C %s submodule add --depth 1 %s %s %s" 99 - output_dir branch_args entry.url rel_path 106 + Printf.sprintf "git -C %s submodule add --depth 1 %s %s %s" output_dir 107 + branch_args entry.url rel_path 100 108 in 101 109 match run_command add_cmd with 102 110 | Ok () -> Ok target 103 111 | Error msg -> 104 112 Printf.eprintf "Failed to add submodule %s: %s\n%!" entry.url msg; 105 - Error msg 113 + Error msg) 106 114 end 107 115 else begin 108 116 log "Adding submodule %s to %s" entry.url rel_path; ··· 112 120 | None -> "" 113 121 in 114 122 let cmd = 115 - Printf.sprintf "git -C %s submodule add --depth 1 %s %s %s" 116 - output_dir branch_args entry.url rel_path 123 + Printf.sprintf "git -C %s submodule add --depth 1 %s %s %s" output_dir 124 + branch_args entry.url rel_path 117 125 in 118 126 match run_command cmd with 119 127 | Ok () -> Ok target ··· 130 138 let cmd = Printf.sprintf "git -C %s pull --ff-only 2>/dev/null" target in 131 139 match run_command cmd with 132 140 | Ok () -> Ok target 133 - | Error _ -> 141 + | Error _ -> ( 134 142 (* If pull fails, try a fresh clone *) 135 143 log "Pull failed, re-cloning %s" entry.url; 136 144 let rm_cmd = Printf.sprintf "rm -rf %s" target in ··· 141 149 | None -> "" 142 150 in 143 151 let clone_cmd = 144 - Printf.sprintf "git clone --depth 1 %s %s %s 2>/dev/null" 145 - branch_args entry.url target 152 + Printf.sprintf "git clone --depth 1 %s %s %s 2>/dev/null" branch_args 153 + entry.url target 146 154 in 147 155 match run_command clone_cmd with 148 156 | Ok () -> Ok target 149 157 | Error msg -> 150 158 Printf.eprintf "Failed to clone %s: %s\n%!" entry.url msg; 151 - Error msg 159 + Error msg) 152 160 end 153 161 else begin 154 162 log "Cloning %s to %s" entry.url target; ··· 158 166 | None -> "" 159 167 in 160 168 let cmd = 161 - Printf.sprintf "git clone --depth 1 %s %s %s 2>/dev/null" 162 - branch_args entry.url target 169 + Printf.sprintf "git clone --depth 1 %s %s %s 2>/dev/null" branch_args 170 + entry.url target 163 171 in 164 172 match run_command cmd with 165 173 | Ok () -> Ok target ··· 243 251 | [] -> None 244 252 | version_dir :: _ -> 245 253 let opam_path = 246 - Filename.concat 247 - (Filename.concat pkg_dir version_dir) 248 - "opam" 254 + Filename.concat (Filename.concat pkg_dir version_dir) "opam" 249 255 in 250 256 if Sys.file_exists opam_path then begin 251 257 let content = read_file opam_path in ··· 297 303 (fun line -> 298 304 let trimmed = String.trim line in 299 305 not 300 - (String.length trimmed > 8 301 - && String.sub trimmed 0 8 = "version:")) 306 + (String.length trimmed > 8 && String.sub trimmed 0 8 = "version:")) 302 307 lines 303 308 in 304 309 let final_content = String.concat "\n" filtered ^ url_section in ··· 337 342 packages; 338 343 Buffer.add_string buf "\necho \"Installing dependencies...\"\n"; 339 344 let pkg_names = List.map fst packages |> String.concat " " in 340 - Buffer.add_string buf (Printf.sprintf "opam install -y --deps-only --with-test %s\n" pkg_names); 345 + Buffer.add_string buf 346 + (Printf.sprintf "opam install -y --deps-only --with-test %s\n" pkg_names); 341 347 Buffer.add_string buf "\necho \"Building...\"\n"; 342 348 Buffer.add_string buf "opam exec -- dune build --root .\n"; 343 349 Buffer.add_string buf "\necho \"Done!\"\n"; ··· 356 362 let vendor_dir = Filename.concat output_dir "vendor" in 357 363 let vendor_dune_path = Filename.concat vendor_dir "dune" in 358 364 let subdirs = 359 - List.map (fun d -> Filename.basename d) vendor_dirs 360 - |> String.concat " " 365 + List.map (fun d -> Filename.basename d) vendor_dirs |> String.concat " " 361 366 in 362 367 let vendor_dune_content = Printf.sprintf "(dirs %s)\n" subdirs in 363 368 write_file vendor_dune_path vendor_dune_content; ··· 386 391 List.iter (fun e -> Hashtbl.replace seen e.url e) from_overlay; 387 392 Hashtbl.fold (fun _ v acc -> v :: acc) seen [] 388 393 | None, None -> 389 - Printf.eprintf "Error: must specify either INPUT_FILE or --opam-overlay\n%!"; 394 + Printf.eprintf 395 + "Error: must specify either INPUT_FILE or --opam-overlay\n%!"; 390 396 [] 391 397 in 392 398 Printf.printf "Found %d repositories to process\n%!" (List.length entries); ··· 408 414 let result = 409 415 if use_submodules then 410 416 add_or_update_submodule ~output_dir ~vendor_dir entry 411 - else 412 - clone_or_update_repo ~vendor_dir entry 417 + else clone_or_update_repo ~vendor_dir entry 413 418 in 414 419 match result with 415 420 | Ok repo_path -> 416 - generate_repo_structure ~output_dir:opam_repo_dir ~repo_path ~git_url:entry.url; 421 + generate_repo_structure ~output_dir:opam_repo_dir ~repo_path 422 + ~git_url:entry.url; 417 423 Ok repo_path 418 424 | Error msg -> Error msg) 419 425 entries
+35
monopam.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: 4 + "Turn an opam repository into a bunch of git submodules and sources" 5 + description: """ 6 + monopam reads git repository URLs and generates an opam repository structure 7 + that can be overlaid on the official opam repository. It clones repositories 8 + into a vendor directory, creates an opam repository with dev packages, and 9 + sets up dune to build everything as a monorepo.""" 10 + maintainer: ["Mark Elvers" "Anil Madhavapeddy <anil@recoil.org>"] 11 + authors: ["Mark Elvers" "Anil Madhavapeddy"] 12 + license: "ISC" 13 + homepage: "https://tangled.org/@mtelvers.tunbury.org/repo-tool" 14 + bug-reports: "https://tangled.org/@mtelvers.tunbury.org/repo-tool/issues" 15 + depends: [ 16 + "dune" {>= "3.18"} 17 + "ocaml" {>= "4.14"} 18 + "cmdliner" {>= "1.1.0"} 19 + "odoc" {with-doc} 20 + ] 21 + build: [ 22 + ["dune" "subst"] {dev} 23 + [ 24 + "dune" 25 + "build" 26 + "-p" 27 + name 28 + "-j" 29 + jobs 30 + "@install" 31 + "@runtest" {with-test} 32 + "@doc" {with-doc} 33 + ] 34 + ] 35 + x-maintenance-intent: ["(latest)"]
-31
repo_tool.opam
··· 1 - # This file is generated by dune, edit dune-project instead 2 - opam-version: "2.0" 3 - synopsis: "Generate opam repository from git repositories" 4 - description: 5 - "A CLI tool that reads a text file containing git repository URLs and generates an opam repository structure that can be overlaid on the official opam repository." 6 - maintainer: ["Mark Elvers"] 7 - authors: ["Mark Elvers"] 8 - license: "MIT" 9 - homepage: "https://github.com/mtelvers/repo-tool" 10 - bug-reports: "https://github.com/mtelvers/repo-tool/issues" 11 - depends: [ 12 - "ocaml" {>= "4.14"} 13 - "dune" {>= "3.0"} 14 - "cmdliner" 15 - "odoc" {with-doc} 16 - ] 17 - build: [ 18 - ["dune" "subst"] {dev} 19 - [ 20 - "dune" 21 - "build" 22 - "-p" 23 - name 24 - "-j" 25 - jobs 26 - "@install" 27 - "@runtest" {with-test} 28 - "@doc" {with-doc} 29 - ] 30 - ] 31 - dev-repo: "git+https://github.com/mtelvers/repo-tool.git"