Skip to content

Commit 995358e

Browse files
committed
Add subrepo utils
1 parent 13e08fe commit 995358e

7 files changed

Lines changed: 217 additions & 0 deletions

File tree

src/central/subrepo.ml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,27 @@ let all ~repo_root =
3939
|> List.sort ~cmp:String.compare
4040
|> List.map ~f:v
4141
;;
42+
43+
let find_on_disk ~repo_root ~name =
44+
List.find_opt (all ~repo_root) ~f:(fun t -> String.equal (to_string t) name)
45+
;;
46+
47+
let central_path t ~subrepo_path =
48+
Vcs.Path_in_repo.v
49+
(Printf.sprintf
50+
"%s/%s"
51+
(Vcs.Path_in_repo.to_string (root t))
52+
(Vcs.Path_in_repo.to_string subrepo_path))
53+
;;
54+
55+
let subrepo_path t ~central_path =
56+
let prefix = Vcs.Path_in_repo.to_string (root t) ^ "/" in
57+
let central_path = Vcs.Path_in_repo.to_string central_path in
58+
if
59+
String.is_prefix central_path ~prefix
60+
&& String.length central_path > String.length prefix
61+
then (
62+
let len = String.length central_path - String.length prefix in
63+
Some (Vcs.Path_in_repo.v (String.sub central_path ~pos:(String.length prefix) ~len)))
64+
else None
65+
;;

src/central/subrepo.mli

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,24 @@ val gitrepo_file_path : t -> Vcs.Path_in_repo.t
3131
its direct children and keeps the ones that contain a [.gitrepo] file.
3232
The result is sorted by name. *)
3333
val all : repo_root:Vcs.Repo_root.t -> t list
34+
35+
(** Look [name] (a sub-repo name, e.g. what {!to_string} returns - not a
36+
path) up against {!all}. Unlike {!of_string}, this validates that the
37+
name actually names a vendored sub-repo (not merely that it has the
38+
right shape). This reads the filesystem (via {!all}). *)
39+
val find_on_disk : repo_root:Vcs.Repo_root.t -> name:string -> t option
40+
41+
(** {1 Manipulating paths}
42+
43+
Pure path manipulation: unlike {!find_on_disk}, neither of these reads
44+
the filesystem, and neither confirms that the sub-repo or the path it is
45+
given actually exist on disk. *)
46+
47+
(** [central_path t ~subrepo_path] is [subrepo_path], expressed as a path in
48+
the enclosing monorepo (i.e. prefixed with {!root}). *)
49+
val central_path : t -> subrepo_path:Vcs.Path_in_repo.t -> Vcs.Path_in_repo.t
50+
51+
(** [subrepo_path t ~central_path] is [central_path], expressed relative to
52+
[t]'s own root, if [central_path] is strictly under {!root} (not equal to
53+
it - a path in the subrepo's own repo is never empty). *)
54+
val subrepo_path : t -> central_path:Vcs.Path_in_repo.t -> Vcs.Path_in_repo.t option

test/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
- [Advance Main, Advance Subrepo](expect/advance.md)
1111
- [Todo](expect/todo.md)
1212
- [Config](expect/config.md)
13+
- [Subrepo](expect/subrepo.md)
1314
- [Deterministic Revisions](expect/redact.md)

test/expect/dune

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@
140140
(action
141141
(diff config.md config.md.gen)))
142142

143+
(rule
144+
(enabled_if %{bin-available:mdexp})
145+
(target subrepo.md.gen)
146+
(deps subrepo.ml)
147+
(action
148+
(with-stdout-to
149+
%{target}
150+
(run mdexp pp %{deps}))))
151+
152+
(rule
153+
(enabled_if %{bin-available:mdexp})
154+
(alias runtest)
155+
(action
156+
(diff subrepo.md subrepo.md.gen)))
157+
143158
(rule
144159
(enabled_if %{bin-available:mdexp})
145160
(target todo.md.gen)

test/expect/subrepo.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Subrepo
2+
3+
`Central.Subrepo.t` identifies one of the sub-repos vendored under `repo/`
4+
in the enclosing monorepo. It isn't a fixed, hand-maintained enum: it's just
5+
a validated string (the directory name under `repo/`), and the set of known
6+
sub-repos is discovered dynamically by walking the filesystem.
7+
8+
## Discovery
9+
10+
`all` walks `repo/`'s direct children and keeps the ones that contain a
11+
`.gitrepo` file, sorted by name:
12+
13+
`find_on_disk` looks a sub-repo name up against `all` - unlike `of_string`,
14+
it validates that the name actually names a vendored sub-repo, not merely
15+
that it has the right shape:
16+
17+
## Manipulating paths
18+
19+
Unlike `all` and `find_on_disk` above, everything in this section is pure
20+
path manipulation: none of it reads the filesystem, or confirms that
21+
anything it is given actually exists on disk.
22+
23+
`root` and `gitrepo_file_path` locate a sub-repo's own directory, and its
24+
`.gitrepo` file, as paths in the enclosing monorepo:
25+
26+
`central_path` and `subrepo_path` convert a path back and forth between the
27+
two frames of reference a path can be expressed in: relative to the
28+
sub-repo's own root (what the sub-repo's standalone checkout sees), or
29+
relative to the enclosing monorepo (prefixed with `root`, what the monorepo
30+
checkout sees):
31+
32+
```text
33+
repo/widget/src/dune
34+
```
35+
36+
`subrepo_path` returns `None` for a path that doesn't belong to the
37+
sub-repo at all - and, since a path in the sub-repo's own repo is never
38+
empty, for the sub-repo's root itself:

test/expect/subrepo.ml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
(*********************************************************************************)
2+
(* central - Manage history between sub-repos and their monorepo *)
3+
(* SPDX-FileCopyrightText: 2024-2026 Mathieu Barbin <mathieu.barbin@gmail.com> *)
4+
(* SPDX-License-Identifier: MIT *)
5+
(*********************************************************************************)
6+
7+
open! Central
8+
9+
(* @mdexp
10+
11+
# Subrepo
12+
13+
`Central.Subrepo.t` identifies one of the sub-repos vendored under `repo/`
14+
in the enclosing monorepo. It isn't a fixed, hand-maintained enum: it's just
15+
a validated string (the directory name under `repo/`), and the set of known
16+
sub-repos is discovered dynamically by walking the filesystem.
17+
18+
## Discovery
19+
20+
`all` walks `repo/`'s direct children and keeps the ones that contain a
21+
`.gitrepo` file, sorted by name: *)
22+
23+
let%expect_test "Subrepo.all" =
24+
let vcs = Volgo_git_unix.create () in
25+
let widget = Subrepo.v "widget" in
26+
let gadget = Subrepo.v "gadget" in
27+
let fake_central = Central_test_helpers.create ~vcs ~subrepos:[ widget; gadget ] in
28+
let { Central_test_helpers.Fake_central.central_root; _ } = fake_central in
29+
List.iter (Subrepo.all ~repo_root:central_root) ~f:(fun t ->
30+
print_endline (Subrepo.to_string t));
31+
[%expect
32+
{|
33+
gadget
34+
widget
35+
|}]
36+
;;
37+
38+
(* @mdexp
39+
40+
`find_on_disk` looks a sub-repo name up against `all` - unlike `of_string`,
41+
it validates that the name actually names a vendored sub-repo, not merely
42+
that it has the right shape: *)
43+
44+
let%expect_test "Subrepo.find_on_disk" =
45+
let vcs = Volgo_git_unix.create () in
46+
let widget = Subrepo.v "widget" in
47+
let fake_central = Central_test_helpers.create ~vcs ~subrepos:[ widget ] in
48+
let { Central_test_helpers.Fake_central.central_root; _ } = fake_central in
49+
let print_find name =
50+
print_dyn
51+
(Subrepo.find_on_disk ~repo_root:central_root ~name |> Dyn.option Subrepo.to_dyn)
52+
in
53+
print_find "widget";
54+
[%expect {| Some "widget" |}];
55+
print_find "does-not-exist";
56+
[%expect {| None |}]
57+
;;
58+
59+
(* @mdexp
60+
61+
## Manipulating paths
62+
63+
Unlike `all` and `find_on_disk` above, everything in this section is pure
64+
path manipulation: none of it reads the filesystem, or confirms that
65+
anything it is given actually exists on disk.
66+
67+
`root` and `gitrepo_file_path` locate a sub-repo's own directory, and its
68+
`.gitrepo` file, as paths in the enclosing monorepo: *)
69+
70+
let%expect_test "Subrepo.root, Subrepo.gitrepo_file_path" =
71+
let widget = Subrepo.v "widget" in
72+
print_endline (Vcs.Path_in_repo.to_string (Subrepo.root widget));
73+
[%expect {| repo/widget |}];
74+
print_endline (Vcs.Path_in_repo.to_string (Subrepo.gitrepo_file_path widget));
75+
[%expect {| repo/widget/.gitrepo |}]
76+
;;
77+
78+
(* @mdexp
79+
80+
`central_path` and `subrepo_path` convert a path back and forth between the
81+
two frames of reference a path can be expressed in: relative to the
82+
sub-repo's own root (what the sub-repo's standalone checkout sees), or
83+
relative to the enclosing monorepo (prefixed with `root`, what the monorepo
84+
checkout sees): *)
85+
86+
let print_subrepo_path t ~central_path =
87+
print_dyn (Subrepo.subrepo_path t ~central_path |> Dyn.option Vcs.Path_in_repo.to_dyn)
88+
;;
89+
90+
let%expect_test "Subrepo.central_path, Subrepo.subrepo_path" =
91+
let widget = Subrepo.v "widget" in
92+
let subrepo_path = Vcs.Path_in_repo.v "src/dune" in
93+
let central_path = Subrepo.central_path widget ~subrepo_path in
94+
print_endline (Vcs.Path_in_repo.to_string central_path);
95+
(* @mdexp.snapshot { lang: "text" } *)
96+
[%expect {| repo/widget/src/dune |}];
97+
print_subrepo_path widget ~central_path;
98+
[%expect {| Some "src/dune" |}]
99+
;;
100+
101+
(* @mdexp
102+
103+
`subrepo_path` returns `None` for a path that doesn't belong to the
104+
sub-repo at all - and, since a path in the sub-repo's own repo is never
105+
empty, for the sub-repo's root itself: *)
106+
107+
let%expect_test "Subrepo.subrepo_path, not under root" =
108+
let widget = Subrepo.v "widget" in
109+
print_subrepo_path widget ~central_path:(Vcs.Path_in_repo.v "README.md");
110+
[%expect {| None |}];
111+
print_subrepo_path widget ~central_path:(Vcs.Path_in_repo.v "repo/widget");
112+
[%expect {| None |}]
113+
;;

test/expect/subrepo.mli

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(*_********************************************************************************)
2+
(*_ central - Manage history between sub-repos and their monorepo *)
3+
(*_ SPDX-FileCopyrightText: 2024-2026 Mathieu Barbin <mathieu.barbin@gmail.com> *)
4+
(*_ SPDX-License-Identifier: MIT *)
5+
(*_********************************************************************************)

0 commit comments

Comments
 (0)