···1+# This is a basic workflow to help you get started with Actions
2+3+name: CI
4+5+# Controls when the workflow will run
6+on:
7+ # Triggers the workflow on push or pull request events but only for the "main" branch
8+ push:
9+ branches: [ "project/unpac" ]
10+ pull_request:
11+ branches: [ "project/unpac" ]
12+13+ # Allows you to run this workflow manually from the Actions tab
14+ workflow_dispatch:
15+16+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
17+jobs:
18+ # This workflow contains a single job called "build"
19+ build:
20+ # The type of runner that the job will run on
21+ runs-on: ubuntu-latest
22+23+ # Steps represent a sequence of tasks that will be executed as part of the job
24+ steps:
25+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26+ - uses: actions/checkout@v4
27+28+ - name: Set-up OCaml
29+ uses: ocaml/setup-ocaml@v3
30+ with:
31+ ocaml-compiler: 5
32+33+ - name: Install dune
34+ run: opam install -y dune
35+36+ - name: Build unpac
37+ run: opam exec -- dune build
···33 val upstream_branch : string -> string
34 (** [upstream_branch pkg] returns branch name, e.g. "opam/upstream/astring". *)
3536- val vendor_branch : string -> string
37- (** [vendor_branch pkg] returns branch name, e.g. "opam/vendor/astring". *)
38-39 val patches_branch : string -> string
40 (** [patches_branch pkg] returns branch name, e.g. "opam/patches/astring". *)
41···4445 (** {2 Worktree Kinds} *)
4647- val upstream_kind : string -> Worktree.kind
48- val vendor_kind : string -> Worktree.kind
49 val patches_kind : string -> Worktree.kind
5051 (** {2 Package Operations} *)
···57 add_result
58 (** [add_package ~proc_mgr ~root info] vendors a single package.
5960- 1. Creates/updates opam/upstream/<pkg> from URL
61- 2. Creates opam/vendor/<pkg> orphan with vendor/ prefix
62- 3. Creates opam/patches/<pkg> from vendor *)
6364 val update_package :
65 proc_mgr:Git.proc_mgr ->
···68 update_result
69 (** [update_package ~proc_mgr ~root name] updates a package from upstream.
7071- 1. Fetches latest into opam/upstream/<pkg>
72- 2. Updates opam/vendor/<pkg> with new content
73- Does NOT rebase patches - that's a separate operation. *)
7475 val list_packages :
76 proc_mgr:Git.proc_mgr ->
···8384(** These operations are backend-agnostic and work on any patches branch. *)
8586-let merge_to_project ~proc_mgr ~root ~project ~patches_branch =
87 let project_wt = Worktree.path root (Worktree.Project project) in
88- Git.merge_allow_unrelated ~proc_mgr ~cwd:project_wt
89- ~branch:patches_branch
90- ~message:(Printf.sprintf "Merge %s" patches_branch)
0000009192let rebase_patches ~proc_mgr ~root ~patches_kind ~onto =
93 Worktree.ensure ~proc_mgr root patches_kind;
···33 val upstream_branch : string -> string
34 (** [upstream_branch pkg] returns branch name, e.g. "opam/upstream/astring". *)
3500036 val patches_branch : string -> string
37 (** [patches_branch pkg] returns branch name, e.g. "opam/patches/astring". *)
38···4142 (** {2 Worktree Kinds} *)
430044 val patches_kind : string -> Worktree.kind
4546 (** {2 Package Operations} *)
···52 add_result
53 (** [add_package ~proc_mgr ~root info] vendors a single package.
5455+ 1. Creates/updates upstream branch from URL
56+ 2. Creates patches branch from upstream *)
05758 val update_package :
59 proc_mgr:Git.proc_mgr ->
···62 update_result
63 (** [update_package ~proc_mgr ~root name] updates a package from upstream.
6465+ 1. Fetches latest into upstream branch
66+ 2. Rebases patches branch onto new upstream *)
06768 val list_packages :
69 proc_mgr:Git.proc_mgr ->
···7677(** These operations are backend-agnostic and work on any patches branch. *)
7879+let merge_to_project ~proc_mgr ~root ~project ~patches_branch ~prefix =
80 let project_wt = Worktree.path root (Worktree.Project project) in
81+ (* Check if this is the first merge (prefix doesn't exist) or an update *)
82+ let prefix_path = Eio.Path.(project_wt / prefix) in
83+ let exists = Eio.Path.is_directory prefix_path in
84+ if exists then
85+ (* Update existing subtree *)
86+ Git.subtree_pull ~proc_mgr ~cwd:project_wt ~prefix ~branch:patches_branch ~squash:true
87+ else
88+ (* First time merge *)
89+ Git.subtree_add ~proc_mgr ~cwd:project_wt ~prefix ~branch:patches_branch ~squash:true
9091let rebase_patches ~proc_mgr ~root ~patches_kind ~onto =
92 Worktree.ensure ~proc_mgr root patches_kind;
+8-14
lib/claude/tools.ml
···34 err (Printf.sprintf "Failed to list git repos: %s" (Printexc.to_string exn))
3536(* Git add tool *)
37-let git_add ~proc_mgr ~fs ~root ~url ?name ?branch ?subdir () =
38 try
39 let repo_name = match name with
40 | Some n -> n
···49 name = repo_name;
50 url;
51 branch;
52- subdir;
53 } in
5455 let config_path = Filename.concat (snd (Unpac.Worktree.path root Unpac.Worktree.Main))
···100 | None -> ());
101102 let upstream = Unpac.Git_backend.upstream_branch name in
103- let vendor = Unpac.Git_backend.vendor_branch name in
104 let patches = Unpac.Git_backend.patches_branch name in
105106 (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git upstream with
107 | Some sha -> add (Printf.sprintf "Upstream: %s\n" (String.sub sha 0 7))
108- | None -> ());
109- (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git vendor with
110- | Some sha -> add (Printf.sprintf "Vendor: %s\n" (String.sub sha 0 7))
111 | None -> ());
112 (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git patches with
113 | Some sha -> add (Printf.sprintf "Patches: %s\n" (String.sub sha 0 7))
114 | None -> ());
1150116 let log_output = Unpac.Git.run_exn ~proc_mgr ~cwd:git
117- ["log"; "--oneline"; vendor ^ ".." ^ patches] in
118 let commits = List.length (String.split_on_char '\n' log_output |>
119 List.filter (fun s -> String.trim s <> "")) in
120 add (Printf.sprintf "Local commits: %d\n" commits);
···132 if not (List.mem name repos) then
133 err (Printf.sprintf "Repository '%s' is not vendored" name)
134 else begin
135- let vendor = Unpac.Git_backend.vendor_branch name in
136 let patches = Unpac.Git_backend.patches_branch name in
137- let diff = Unpac.Git.run_exn ~proc_mgr ~cwd:git ["diff"; vendor; patches] in
138 if String.trim diff = "" then
139 ok (Printf.sprintf "No local changes in '%s'." name)
140 else
···547548 create
549 ~name:"unpac_git_add"
550- ~description:"Vendor a new git repository. Clones the repo and creates the three-tier \
551- branch structure for conflict-free vendoring with full history preservation."
552 ~input_schema:(schema_object [
553 ("url", schema_string);
554 ("name", schema_string);
555 ("branch", schema_string);
556- ("subdir", schema_string);
557 ] ~required:["url"])
558 ~handler:(fun args ->
559 match Claude.Tool_input.get_string args "url" with
···561 | Some url ->
562 let name = Claude.Tool_input.get_string args "name" in
563 let branch = Claude.Tool_input.get_string args "branch" in
564- let subdir = Claude.Tool_input.get_string args "subdir" in
565- git_add ~proc_mgr ~fs ~root ~url ?name ?branch ?subdir ());
566567 create
568 ~name:"unpac_git_info"
···34 err (Printf.sprintf "Failed to list git repos: %s" (Printexc.to_string exn))
3536(* Git add tool *)
37+let git_add ~proc_mgr ~fs ~root ~url ?name ?branch () =
38 try
39 let repo_name = match name with
40 | Some n -> n
···49 name = repo_name;
50 url;
51 branch;
052 } in
5354 let config_path = Filename.concat (snd (Unpac.Worktree.path root Unpac.Worktree.Main))
···99 | None -> ());
100101 let upstream = Unpac.Git_backend.upstream_branch name in
0102 let patches = Unpac.Git_backend.patches_branch name in
103104 (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git upstream with
105 | Some sha -> add (Printf.sprintf "Upstream: %s\n" (String.sub sha 0 7))
000106 | None -> ());
107 (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git patches with
108 | Some sha -> add (Printf.sprintf "Patches: %s\n" (String.sub sha 0 7))
109 | None -> ());
110111+ (* Count local commits: patches ahead of upstream *)
112 let log_output = Unpac.Git.run_exn ~proc_mgr ~cwd:git
113+ ["log"; "--oneline"; upstream ^ ".." ^ patches] in
114 let commits = List.length (String.split_on_char '\n' log_output |>
115 List.filter (fun s -> String.trim s <> "")) in
116 add (Printf.sprintf "Local commits: %d\n" commits);
···128 if not (List.mem name repos) then
129 err (Printf.sprintf "Repository '%s' is not vendored" name)
130 else begin
131+ let upstream = Unpac.Git_backend.upstream_branch name in
132 let patches = Unpac.Git_backend.patches_branch name in
133+ let diff = Unpac.Git.run_exn ~proc_mgr ~cwd:git ["diff"; upstream; patches] in
134 if String.trim diff = "" then
135 ok (Printf.sprintf "No local changes in '%s'." name)
136 else
···543544 create
545 ~name:"unpac_git_add"
546+ ~description:"Vendor a new git repository. Clones the repo and creates the two-tier \
547+ branch structure (upstream/patches) for conflict-free vendoring."
548 ~input_schema:(schema_object [
549 ("url", schema_string);
550 ("name", schema_string);
551 ("branch", schema_string);
0552 ] ~required:["url"])
553 ~handler:(fun args ->
554 match Claude.Tool_input.get_string args "url" with
···556 | Some url ->
557 let name = Claude.Tool_input.get_string args "name" in
558 let branch = Claude.Tool_input.get_string args "branch" in
559+ git_add ~proc_mgr ~fs ~root ~url ?name ?branch ());
0560561 create
562 ~name:"unpac_git_info"
+2-4
lib/config.ml
···30 git_name : string; (** User-specified name for the repo *)
31 git_url : string; (** Git URL to clone from *)
32 git_branch : string option; (** Optional branch/tag to track *)
33- git_subdir : string option; (** Optional subdirectory to extract *)
34}
3536type git_config = {
···101let git_repo_config_codec : git_repo_config Tomlt.t =
102 let open Tomlt in
103 let open Table in
104- obj (fun git_name git_url git_branch git_subdir : git_repo_config ->
105- { git_name; git_url; git_branch; git_subdir })
106 |> mem "name" string ~enc:(fun (r : git_repo_config) -> r.git_name)
107 |> mem "url" string ~enc:(fun (r : git_repo_config) -> r.git_url)
108 |> opt_mem "branch" string ~enc:(fun (r : git_repo_config) -> r.git_branch)
109- |> opt_mem "subdir" string ~enc:(fun (r : git_repo_config) -> r.git_subdir)
110 |> finish
111112let git_config_codec : git_config Tomlt.t =
···30 git_name : string; (** User-specified name for the repo *)
31 git_url : string; (** Git URL to clone from *)
32 git_branch : string option; (** Optional branch/tag to track *)
033}
3435type git_config = {
···100let git_repo_config_codec : git_repo_config Tomlt.t =
101 let open Tomlt in
102 let open Table in
103+ obj (fun git_name git_url git_branch : git_repo_config ->
104+ { git_name; git_url; git_branch })
105 |> mem "name" string ~enc:(fun (r : git_repo_config) -> r.git_name)
106 |> mem "url" string ~enc:(fun (r : git_repo_config) -> r.git_url)
107 |> opt_mem "branch" string ~enc:(fun (r : git_repo_config) -> r.git_branch)
0108 |> finish
109110let git_config_codec : git_config Tomlt.t =
-1
lib/config.mli
···30 git_name : string; (** User-specified name for the repo *)
31 git_url : string; (** Git URL to clone from *)
32 git_branch : string option; (** Optional branch/tag to track *)
33- git_subdir : string option; (** Optional subdirectory to extract *)
34}
3536type git_config = {
···30 git_name : string; (** User-specified name for the repo *)
31 git_url : string; (** Git URL to clone from *)
32 git_branch : string option; (** Optional branch/tag to track *)
033}
3435type git_config = {
+49-91
lib/git.ml
···437 Log.debug (fun m -> m "Cleaning untracked files");
438 run_exn ~proc_mgr ~cwd ["clean"; "-fd"] |> ignore
439440-let filter_repo_to_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory =
441- Log.info (fun m -> m "Rewriting history of %s into subdirectory %s..." branch subdirectory);
442- (* Use git-filter-repo with --to-subdirectory-filter to rewrite all paths into subdirectory.
443- This preserves full history with paths prefixed. Much faster than filter-branch.
444-445- For bare repositories, we need to create a temporary worktree, run filter-repo
446- there, and then update the branch in the bare repo. *)
447-448- (* Create a unique temporary worktree name using the branch name *)
449- let safe_branch = String.map (fun c -> if c = '/' then '-' else c) branch in
450- let temp_wt_name = ".filter-tmp-" ^ safe_branch in
451- let temp_wt_relpath = "../" ^ temp_wt_name in
452-453- (* Construct the worktree path - cwd is (fs, path_string), so we go up one level *)
454- let fs = fst cwd in
455- let git_path = snd cwd in
456- let parent_path = Filename.dirname git_path in
457- let temp_wt_path = Filename.concat parent_path temp_wt_name in
458- let temp_wt : path = (fs, temp_wt_path) in
459-460- (* Remove any existing temp worktree *)
461- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
462-463- (* Create worktree for the branch *)
464- run_exn ~proc_mgr ~cwd ["worktree"; "add"; temp_wt_relpath; branch] |> ignore;
465-466- (* Run git-filter-repo in the worktree *)
467- let result = run ~proc_mgr ~cwd:temp_wt [
468- "filter-repo";
469- "--to-subdirectory-filter"; subdirectory;
470- "--force";
471- "--refs"; "HEAD"
472- ] in
473-474- (* Handle result: get the new SHA, cleanup worktree, then update branch *)
475- (match result with
476- | Ok _ ->
477- (* Get the new HEAD SHA from the worktree BEFORE removing it *)
478- let new_sha = run_exn ~proc_mgr ~cwd:temp_wt ["rev-parse"; "HEAD"] |> string_trim in
479- (* Cleanup temporary worktree first (must do this before updating branch) *)
480- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
481- (* Now update the branch in the bare repo *)
482- run_exn ~proc_mgr ~cwd ["branch"; "-f"; branch; new_sha] |> ignore
483- | Error e ->
484- (* Cleanup and re-raise *)
485- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
486- raise (err e))
487488-let filter_repo_from_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory =
489- Log.info (fun m -> m "Extracting %s from subdirectory %s to root..." branch subdirectory);
490- (* Use git-filter-repo with --subdirectory-filter to extract files from subdirectory
491- to root. This is the inverse of --to-subdirectory-filter.
492- Preserves history for files that were in the subdirectory.
493-494- For bare repositories, we need to create a temporary worktree, run filter-repo
495- there, and then update the branch in the bare repo. *)
496-497- (* Create a unique temporary worktree name using the branch name *)
498- let safe_branch = String.map (fun c -> if c = '/' then '-' else c) branch in
499- let temp_wt_name = ".filter-tmp-" ^ safe_branch in
500- let temp_wt_relpath = "../" ^ temp_wt_name in
501-502- (* Construct the worktree path - cwd is (fs, path_string), so we go up one level *)
503- let fs = fst cwd in
504- let git_path = snd cwd in
505- let parent_path = Filename.dirname git_path in
506- let temp_wt_path = Filename.concat parent_path temp_wt_name in
507- let temp_wt : path = (fs, temp_wt_path) in
508509- (* Remove any existing temp worktree *)
510- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
00000000511512- (* Create worktree for the branch *)
513- run_exn ~proc_mgr ~cwd ["worktree"; "add"; temp_wt_relpath; branch] |> ignore;
00000000000000514515- (* Run git-filter-repo in the worktree with --subdirectory-filter *)
516- let result = run ~proc_mgr ~cwd:temp_wt [
517- "filter-repo";
518- "--subdirectory-filter"; subdirectory;
519- "--force";
520- "--refs"; "HEAD"
521- ] in
000000000522523- (* Handle result: get the new SHA, cleanup worktree, then update branch *)
524- (match result with
525- | Ok _ ->
526- (* Get the new HEAD SHA from the worktree BEFORE removing it *)
527- let new_sha = run_exn ~proc_mgr ~cwd:temp_wt ["rev-parse"; "HEAD"] |> string_trim in
528- (* Cleanup temporary worktree first (must do this before updating branch) *)
529- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
530- (* Now update the branch in the bare repo *)
531- run_exn ~proc_mgr ~cwd ["branch"; "-f"; branch; new_sha] |> ignore
532- | Error e ->
533- (* Cleanup and re-raise *)
534- ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
535- raise (err e))
···437 Log.debug (fun m -> m "Cleaning untracked files");
438 run_exn ~proc_mgr ~cwd ["clean"; "-fd"] |> ignore
439440+(* Git subtree operations *)
0000000000000000000000000000000000000000000000441442+type subtree_result =
443+ | Subtree_ok
444+ | Subtree_conflict of string list
00000000000000000445446+let has_conflict_marker stderr =
447+ String.starts_with ~prefix:"CONFLICT" stderr ||
448+ String.starts_with ~prefix:"conflict" stderr ||
449+ (* Check if "Merge conflict" appears anywhere *)
450+ let rec find_substring s sub i =
451+ if i + String.length sub > String.length s then false
452+ else if String.sub s i (String.length sub) = sub then true
453+ else find_substring s sub (i + 1)
454+ in
455+ find_substring stderr "Merge conflict" 0
456457+let subtree_add ~proc_mgr ~cwd ~prefix ~branch ~squash =
458+ Log.info (fun m -> m "Subtree add: %s from %s (squash=%b)" prefix branch squash);
459+ let args = ["subtree"; "add"; "--prefix"; prefix] @
460+ (if squash then ["--squash"] else []) @
461+ [branch] in
462+ match run ~proc_mgr ~cwd args with
463+ | Ok _ -> Subtree_ok
464+ | Error (Command_failed { exit_code = 1; stderr; _ }) when
465+ String.length stderr > 0 && has_conflict_marker stderr ->
466+ (* Parse conflicting files *)
467+ let conflict_output = run_exn ~proc_mgr ~cwd ["diff"; "--name-only"; "--diff-filter=U"] in
468+ let files = lines conflict_output in
469+ Log.warn (fun m -> m "Subtree add conflict: %a" Fmt.(list ~sep:comma string) files);
470+ Subtree_conflict files
471+ | Error e ->
472+ raise (err e)
473474+let subtree_pull ~proc_mgr ~cwd ~prefix ~branch ~squash =
475+ Log.info (fun m -> m "Subtree pull: %s from %s (squash=%b)" prefix branch squash);
476+ let args = ["subtree"; "pull"; "--prefix"; prefix] @
477+ (if squash then ["--squash"] else []) @
478+ ["." (* local repo *); branch] in
479+ match run ~proc_mgr ~cwd args with
480+ | Ok _ -> Subtree_ok
481+ | Error (Command_failed { exit_code = 1; stderr; _ }) when
482+ String.length stderr > 0 && has_conflict_marker stderr ->
483+ (* Parse conflicting files *)
484+ let conflict_output = run_exn ~proc_mgr ~cwd ["diff"; "--name-only"; "--diff-filter=U"] in
485+ let files = lines conflict_output in
486+ Log.warn (fun m -> m "Subtree pull conflict: %a" Fmt.(list ~sep:comma string) files);
487+ Subtree_conflict files
488+ | Error e ->
489+ raise (err e)
490491+let subtree_split ~proc_mgr ~cwd ~prefix ~branch =
492+ Log.info (fun m -> m "Subtree split: %s to branch %s" prefix branch);
493+ run_exn ~proc_mgr ~cwd ["subtree"; "split"; "--prefix"; prefix; "-b"; branch] |> ignore
0000000000
+29-13
lib/git.mli
···376 unit
377(** [clean_fd] removes untracked files and directories. *)
378379-val filter_repo_to_subdirectory :
0000000380 proc_mgr:proc_mgr ->
381 cwd:path ->
0382 branch:string ->
383- subdirectory:string ->
384- unit
385-(** [filter_repo_to_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory]
386- rewrites the history of [branch] so all files are moved into [subdirectory].
387- Uses git-filter-repo for fast history rewriting. Preserves full commit history. *)
0388389-val filter_repo_from_subdirectory :
0000000000390 proc_mgr:proc_mgr ->
391 cwd:path ->
0392 branch:string ->
393- subdirectory:string ->
394 unit
395-(** [filter_repo_from_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory]
396- rewrites the history of [branch] extracting only files from [subdirectory]
397- and placing them at the repository root. This is the inverse of
398- [filter_repo_to_subdirectory]. Uses git-filter-repo --subdirectory-filter.
399- Preserves full commit history for files that were in the subdirectory. *)
···376 unit
377(** [clean_fd] removes untracked files and directories. *)
378379+(** {1 Git Subtree Operations} *)
380+381+type subtree_result =
382+ | Subtree_ok
383+ | Subtree_conflict of string list
384+(** Result of a subtree operation. *)
385+386+val subtree_add :
387 proc_mgr:proc_mgr ->
388 cwd:path ->
389+ prefix:string ->
390 branch:string ->
391+ squash:bool ->
392+ subtree_result
393+(** [subtree_add ~proc_mgr ~cwd ~prefix ~branch ~squash] adds a branch as a subtree.
394+ Use this for the first merge of a package into a project.
395+ Files from [branch] will be placed under [prefix].
396+ If [squash] is true, the subtree's history is squashed into a single commit. *)
397398+val subtree_pull :
399+ proc_mgr:proc_mgr ->
400+ cwd:path ->
401+ prefix:string ->
402+ branch:string ->
403+ squash:bool ->
404+ subtree_result
405+(** [subtree_pull ~proc_mgr ~cwd ~prefix ~branch ~squash] updates an existing subtree.
406+ Use this to update a package that was previously merged via subtree_add. *)
407+408+val subtree_split :
409 proc_mgr:proc_mgr ->
410 cwd:path ->
411+ prefix:string ->
412 branch:string ->
0413 unit
414+(** [subtree_split ~proc_mgr ~cwd ~prefix ~branch] extracts a subdirectory's history
415+ into a new branch. This is used for promoting project code to a vendored library. *)
000
+53-103
lib/git_backend.ml
···1(** Git backend for direct repository vendoring.
23- Implements vendoring of arbitrary git repositories using the three-tier branch model:
4 - git/upstream/<name> - pristine upstream code
5- - git/vendor/<name> - upstream history rewritten with vendor/git/<name>/ prefix
6- - git/patches/<name> - local modifications *)
0078(** {1 Branch Naming} *)
910let upstream_branch name = "git/upstream/" ^ name
11-let vendor_branch name = "git/vendor/" ^ name
12let patches_branch name = "git/patches/" ^ name
13let vendor_path name = "vendor/git/" ^ name
1415(** {1 Worktree Kinds} *)
1617-let upstream_kind name = Worktree.Git_upstream name
18-let vendor_kind name = Worktree.Git_vendor name
19let patches_kind name = Worktree.Git_patches name
2021(** {1 Repository Info} *)
···24 name : string;
25 url : string;
26 branch : string option;
27- subdir : string option;
28}
2930(** {1 Repository Operations} *)
···68 Git.branch_force ~proc_mgr ~cwd:git
69 ~name:(upstream_branch repo_name) ~point:ref_point;
7071- (* Step 2: Create vendor branch from upstream and rewrite history *)
72- Git.branch_force ~proc_mgr ~cwd:git
73- ~name:(vendor_branch repo_name) ~point:(upstream_branch repo_name);
007475- (* If subdir is specified, we first filter to that subdirectory,
76- then move to vendor path. Otherwise, just move to vendor path. *)
77- (match info.subdir with
78- | Some subdir ->
79- (* First filter to extract only the subdirectory *)
80- Git.filter_repo_to_subdirectory ~proc_mgr ~cwd:git
81- ~branch:(vendor_branch repo_name)
82- ~subdirectory:subdir;
83- (* Now the subdir is at root, rewrite to vendor path *)
84- Git.filter_repo_to_subdirectory ~proc_mgr ~cwd:git
85- ~branch:(vendor_branch repo_name)
86- ~subdirectory:(vendor_path repo_name)
87- | None ->
88- (* Rewrite vendor branch history to move all files into vendor/git/<name>/ *)
89- Git.filter_repo_to_subdirectory ~proc_mgr ~cwd:git
90- ~branch:(vendor_branch repo_name)
91- ~subdirectory:(vendor_path repo_name));
92-93- (* Get the vendor SHA after rewriting *)
94- let vendor_sha = match Git.rev_parse ~proc_mgr ~cwd:git (vendor_branch repo_name) with
95 | Some sha -> sha
96- | None -> failwith "Vendor branch not found after filter-repo"
97 in
9899- (* Step 3: Create patches branch from vendor *)
100- Git.branch_create ~proc_mgr ~cwd:git
101- ~name:(patches_branch repo_name)
102- ~start_point:(vendor_branch repo_name);
103-104- Backend.Added { name = repo_name; sha = vendor_sha }
105 end
106 with exn ->
107- (* Cleanup on failure *)
108- (try Worktree.remove_force ~proc_mgr root (upstream_kind repo_name) with _ -> ());
109- (try Worktree.remove_force ~proc_mgr root (vendor_kind repo_name) with _ -> ());
110 Backend.Failed { name = repo_name; error = Printexc.to_string exn }
111112-let copy_with_prefix ~src_dir ~dst_dir ~prefix =
113- (* Recursively copy files from src_dir to dst_dir/prefix/ *)
114- let prefix_dir = Eio.Path.(dst_dir / prefix) in
115- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 prefix_dir;
116-117- let rec copy_dir src dst =
118- Eio.Path.read_dir src |> List.iter (fun name ->
119- let src_path = Eio.Path.(src / name) in
120- let dst_path = Eio.Path.(dst / name) in
121- if Eio.Path.is_directory src_path then begin
122- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 dst_path;
123- copy_dir src_path dst_path
124- end else begin
125- let content = Eio.Path.load src_path in
126- Eio.Path.save ~create:(`Or_truncate 0o644) dst_path content
127- end
128- )
129- in
130-131- (* Copy everything except .git *)
132- Eio.Path.read_dir src_dir |> List.iter (fun name ->
133- if name <> ".git" then begin
134- let src_path = Eio.Path.(src_dir / name) in
135- let dst_path = Eio.Path.(prefix_dir / name) in
136- if Eio.Path.is_directory src_path then begin
137- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 dst_path;
138- copy_dir src_path dst_path
139- end else begin
140- let content = Eio.Path.load src_path in
141- Eio.Path.save ~create:(`Or_truncate 0o644) dst_path content
142- end
143- end
144- )
145-146let update_repo ~proc_mgr ~root ?cache repo_name =
147 let git = Worktree.git_dir root in
148···188 if old_sha = new_sha then
189 Backend.No_changes repo_name
190 else begin
191- (* Create worktrees *)
192- Worktree.ensure ~proc_mgr root (upstream_kind repo_name);
193- Worktree.ensure ~proc_mgr root (vendor_kind repo_name);
00000000000194195- let upstream_wt = Worktree.path root (upstream_kind repo_name) in
196- let vendor_wt = Worktree.path root (vendor_kind repo_name) in
000197198- (* Clear vendor content and copy new *)
199- let vendor_pkg_path = Eio.Path.(vendor_wt / "vendor" / "git" / repo_name) in
200- (try Eio.Path.rmtree vendor_pkg_path with _ -> ());
201202- copy_with_prefix
203- ~src_dir:upstream_wt
204- ~dst_dir:vendor_wt
205- ~prefix:(vendor_path repo_name);
206207- (* Commit *)
208- Git.add_all ~proc_mgr ~cwd:vendor_wt;
209- Git.commit ~proc_mgr ~cwd:vendor_wt
210- ~message:(Printf.sprintf "Update %s to %s" repo_name (String.sub new_sha 0 7));
211212- (* Cleanup *)
213- Worktree.remove ~proc_mgr root (upstream_kind repo_name);
214- Worktree.remove ~proc_mgr root (vendor_kind repo_name);
215216- Backend.Updated { name = repo_name; old_sha; new_sha }
0000000217 end
218 end
219 with exn ->
220- (try Worktree.remove_force ~proc_mgr root (upstream_kind repo_name) with _ -> ());
221- (try Worktree.remove_force ~proc_mgr root (vendor_kind repo_name) with _ -> ());
222 Backend.Update_failed { name = repo_name; error = Printexc.to_string exn }
223224let list_repos ~proc_mgr ~root =
···227let remove_repo ~proc_mgr ~root repo_name =
228 let git = Worktree.git_dir root in
229230- (* Remove worktrees if exist *)
231- (try Worktree.remove_force ~proc_mgr root (upstream_kind repo_name) with _ -> ());
232- (try Worktree.remove_force ~proc_mgr root (vendor_kind repo_name) with _ -> ());
233- (try Worktree.remove_force ~proc_mgr root (patches_kind repo_name) with _ -> ());
234-235- (* Delete branches *)
236 (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; upstream_branch repo_name] |> ignore with _ -> ());
237- (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; vendor_branch repo_name] |> ignore with _ -> ());
238 (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; patches_branch repo_name] |> ignore with _ -> ());
239240 (* Remove remote *)
···1(** Git backend for direct repository vendoring.
23+ Implements vendoring of arbitrary git repositories using a two-tier branch model:
4 - git/upstream/<name> - pristine upstream code
5+ - git/patches/<name> - local modifications on top of upstream
6+7+ Repositories are merged into projects using git subtree, which places files
8+ under vendor/git/<name>/ without rewriting upstream history. *)
910(** {1 Branch Naming} *)
1112let upstream_branch name = "git/upstream/" ^ name
013let patches_branch name = "git/patches/" ^ name
14let vendor_path name = "vendor/git/" ^ name
1516(** {1 Worktree Kinds} *)
170018let patches_kind name = Worktree.Git_patches name
1920(** {1 Repository Info} *)
···23 name : string;
24 url : string;
25 branch : string option;
026}
2728(** {1 Repository Operations} *)
···66 Git.branch_force ~proc_mgr ~cwd:git
67 ~name:(upstream_branch repo_name) ~point:ref_point;
6869+ (* Step 2: Create patches branch from upstream (initially identical) *)
70+ (* Patches will be merged into projects via git subtree *)
71+ Git.branch_create ~proc_mgr ~cwd:git
72+ ~name:(patches_branch repo_name)
73+ ~start_point:(upstream_branch repo_name);
7475+ (* Get the SHA for reporting *)
76+ let sha = match Git.rev_parse ~proc_mgr ~cwd:git (patches_branch repo_name) with
00000000000000000077 | Some sha -> sha
78+ | None -> failwith "Patches branch not found"
79 in
8081+ Backend.Added { name = repo_name; sha }
0000082 end
83 with exn ->
84+ (* Cleanup on failure - just delete branches, no worktrees to remove *)
85+ (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; upstream_branch repo_name] |> ignore with _ -> ());
86+ (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; patches_branch repo_name] |> ignore with _ -> ());
87 Backend.Failed { name = repo_name; error = Printexc.to_string exn }
88000000000000000000000000000000000089let update_repo ~proc_mgr ~root ?cache repo_name =
90 let git = Worktree.git_dir root in
91···131 if old_sha = new_sha then
132 Backend.No_changes repo_name
133 else begin
134+ (* Rebase patches branch onto new upstream *)
135+ (* First check if patches has diverged from upstream *)
136+ let patches_sha = Git.rev_parse_exn ~proc_mgr ~cwd:git (patches_branch repo_name) in
137+ if patches_sha = old_sha then begin
138+ (* No local patches - just fast-forward patches branch *)
139+ Git.branch_force ~proc_mgr ~cwd:git
140+ ~name:(patches_branch repo_name) ~point:(upstream_branch repo_name);
141+ Backend.Updated { name = repo_name; old_sha; new_sha }
142+ end else begin
143+ (* Has local patches - need to rebase *)
144+ (* Create a temporary worktree for rebasing *)
145+ let safe_name = String.map (fun c -> if c = '/' then '-' else c) repo_name in
146+ let temp_wt_name = ".rebase-tmp-" ^ safe_name in
147+ let temp_wt_relpath = "../" ^ temp_wt_name in
148149+ let fs = fst git in
150+ let git_path = snd git in
151+ let parent_path = Filename.dirname git_path in
152+ let temp_wt_path = Filename.concat parent_path temp_wt_name in
153+ let temp_wt : Git.path = (fs, temp_wt_path) in
154155+ (* Remove any existing temp worktree *)
156+ ignore (Git.run ~proc_mgr ~cwd:git ["worktree"; "remove"; "-f"; temp_wt_relpath]);
0157158+ (* Create worktree for the patches branch *)
159+ Git.run_exn ~proc_mgr ~cwd:git ["worktree"; "add"; temp_wt_relpath; patches_branch repo_name] |> ignore;
00160161+ (* Try to rebase *)
162+ let result = Git.rebase ~proc_mgr ~cwd:temp_wt ~onto:(upstream_branch repo_name) in
00163164+ (* Cleanup temp worktree *)
165+ ignore (Git.run ~proc_mgr ~cwd:git ["worktree"; "remove"; "-f"; temp_wt_relpath]);
0166167+ match result with
168+ | Ok () ->
169+ Backend.Updated { name = repo_name; old_sha; new_sha }
170+ | Error (`Conflict hint) ->
171+ (* Abort the rebase and report conflict *)
172+ Git.rebase_abort ~proc_mgr ~cwd:git;
173+ Backend.Update_failed { name = repo_name; error = "Rebase conflict: " ^ hint }
174+ end
175 end
176 end
177 with exn ->
00178 Backend.Update_failed { name = repo_name; error = Printexc.to_string exn }
179180let list_repos ~proc_mgr ~root =
···183let remove_repo ~proc_mgr ~root repo_name =
184 let git = Worktree.git_dir root in
185186+ (* Delete branches - no worktrees in new architecture *)
00000187 (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; upstream_branch repo_name] |> ignore with _ -> ());
0188 (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; patches_branch repo_name] |> ignore with _ -> ());
189190 (* Remove remote *)
+10-12
lib/git_backend.mli
···1(** Git backend for direct repository vendoring.
23- Implements vendoring of arbitrary git repositories using the three-tier branch model:
4 - git/upstream/<name> - pristine upstream code
5- - git/vendor/<name> - upstream history rewritten with vendor/git/<name>/ prefix
6- - git/patches/<name> - local modifications
0078 Unlike the opam backend which discovers packages via opam repositories,
9 this backend allows cloning any git repository directly. *)
···1213val upstream_branch : string -> string
14(** [upstream_branch name] returns the upstream branch name "git/upstream/<name>". *)
15-16-val vendor_branch : string -> string
17-(** [vendor_branch name] returns the vendor branch name "git/vendor/<name>". *)
1819val patches_branch : string -> string
20(** [patches_branch name] returns the patches branch name "git/patches/<name>". *)
···28 name : string; (** User-specified name *)
29 url : string; (** Git URL to clone from *)
30 branch : string option; (** Optional branch/tag to track *)
31- subdir : string option; (** Optional subdirectory to extract *)
32}
3334(** {1 Repository Operations} *)
···41 Backend.add_result
42(** [add_repo ~proc_mgr ~root ?cache info] vendors a git repository.
4344- Creates the three-tier branch structure:
45 1. Fetches from url into git/upstream/<name>
46- 2. Rewrites history into git/vendor/<name> with vendor/git/<name>/ prefix
47- 3. Creates git/patches/<name> for local modifications
4849- If [subdir] is specified, only that subdirectory is extracted from the repo. *)
5051val update_repo :
52 proc_mgr:Git.proc_mgr ->
···54 ?cache:Vendor_cache.t ->
55 string ->
56 Backend.update_result
57-(** [update_repo ~proc_mgr ~root ?cache name] updates a vendored repository from upstream. *)
05859val list_repos :
60 proc_mgr:Git.proc_mgr ->
···1(** Git backend for direct repository vendoring.
23+ Implements vendoring of arbitrary git repositories using a two-tier branch model:
4 - git/upstream/<name> - pristine upstream code
5+ - git/patches/<name> - local modifications on top of upstream
6+7+ Repositories are merged into projects using git subtree, which places files
8+ under vendor/git/<name>/ without rewriting upstream history.
910 Unlike the opam backend which discovers packages via opam repositories,
11 this backend allows cloning any git repository directly. *)
···1415val upstream_branch : string -> string
16(** [upstream_branch name] returns the upstream branch name "git/upstream/<name>". *)
0001718val patches_branch : string -> string
19(** [patches_branch name] returns the patches branch name "git/patches/<name>". *)
···27 name : string; (** User-specified name *)
28 url : string; (** Git URL to clone from *)
29 branch : string option; (** Optional branch/tag to track *)
030}
3132(** {1 Repository Operations} *)
···39 Backend.add_result
40(** [add_repo ~proc_mgr ~root ?cache info] vendors a git repository.
4142+ Creates the two-tier branch structure:
43 1. Fetches from url into git/upstream/<name>
44+ 2. Creates git/patches/<name> for local modifications (initially identical to upstream)
04546+ Use [git subtree add] to merge into a project. *)
4748val update_repo :
49 proc_mgr:Git.proc_mgr ->
···51 ?cache:Vendor_cache.t ->
52 string ->
53 Backend.update_result
54+(** [update_repo ~proc_mgr ~root ?cache name] updates a vendored repository from upstream.
55+ Rebases the patches branch onto the new upstream. *)
5657val list_repos :
58 proc_mgr:Git.proc_mgr ->
+51-84
lib/opam/opam.ml
···1(** Opam backend for unpac.
23- Implements vendoring of opam packages using the three-tier branch model:
4 - opam/upstream/<pkg> - pristine upstream code
5- - opam/vendor/<pkg> - upstream history rewritten with vendor/opam/<pkg>/ prefix
6- - opam/patches/<pkg> - local modifications
78- The vendor branch preserves full git history from upstream, with all paths
9- rewritten to be under vendor/opam/<pkg>/. This allows git blame/log to work
10- correctly on vendored files. *)
1112module Worktree = Unpac.Worktree
13module Git = Unpac.Git
···20(** {1 Branch Naming} *)
2122let upstream_branch pkg = "opam/upstream/" ^ pkg
23-let vendor_branch pkg = "opam/vendor/" ^ pkg
24let patches_branch pkg = "opam/patches/" ^ pkg
25let vendor_path pkg = "vendor/opam/" ^ pkg
2627(** {1 Worktree Kinds} *)
2829let upstream_kind pkg = Worktree.Opam_upstream pkg
30-let vendor_kind pkg = Worktree.Opam_vendor pkg
31let patches_kind pkg = Worktree.Opam_patches pkg
3233(** {1 Package Operations} *)
3435-let copy_with_prefix ~src_dir ~dst_dir ~prefix =
36- (* Recursively copy files from src_dir to dst_dir/prefix/ *)
37- let prefix_dir = Eio.Path.(dst_dir / prefix) in
38- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 prefix_dir;
39-40- let rec copy_dir src dst =
41- Eio.Path.read_dir src |> List.iter (fun name ->
42- let src_path = Eio.Path.(src / name) in
43- let dst_path = Eio.Path.(dst / name) in
44- if Eio.Path.is_directory src_path then begin
45- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 dst_path;
46- copy_dir src_path dst_path
47- end else begin
48- let content = Eio.Path.load src_path in
49- Eio.Path.save ~create:(`Or_truncate 0o644) dst_path content
50- end
51- )
52- in
53-54- (* Copy everything except .git *)
55- Eio.Path.read_dir src_dir |> List.iter (fun name ->
56- if name <> ".git" then begin
57- let src_path = Eio.Path.(src_dir / name) in
58- let dst_path = Eio.Path.(prefix_dir / name) in
59- if Eio.Path.is_directory src_path then begin
60- Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 dst_path;
61- copy_dir src_path dst_path
62- end else begin
63- let content = Eio.Path.load src_path in
64- Eio.Path.save ~create:(`Or_truncate 0o644) dst_path content
65- end
66- end
67- )
68-69let add_package ~proc_mgr ~root ?cache (info : Backend.package_info) =
70 let pkg = info.name in
71 let git = Worktree.git_dir root in
···105 Git.branch_force ~proc_mgr ~cwd:git
106 ~name:(upstream_branch pkg) ~point:ref_point;
107108- (* Step 2: Create vendor branch from upstream and rewrite history *)
109- Git.branch_force ~proc_mgr ~cwd:git
110- ~name:(vendor_branch pkg) ~point:(upstream_branch pkg);
111-112- (* Rewrite vendor branch history to move all files into vendor/opam/<pkg>/ *)
113- Git.filter_repo_to_subdirectory ~proc_mgr ~cwd:git
114- ~branch:(vendor_branch pkg)
115- ~subdirectory:(vendor_path pkg);
116117- (* Get the vendor SHA after rewriting *)
118- let vendor_sha = match Git.rev_parse ~proc_mgr ~cwd:git (vendor_branch pkg) with
119 | Some sha -> sha
120- | None -> failwith "Vendor branch not found after filter-repo"
121 in
122123- (* Step 3: Create patches branch from vendor *)
124- Git.branch_create ~proc_mgr ~cwd:git
125- ~name:(patches_branch pkg)
126- ~start_point:(vendor_branch pkg);
127-128- Backend.Added { name = pkg; sha = vendor_sha }
129 end
130 with exn ->
131- (* Cleanup on failure *)
132- (try Worktree.remove_force ~proc_mgr root (upstream_kind pkg) with _ -> ());
133- (try Worktree.remove_force ~proc_mgr root (vendor_kind pkg) with _ -> ());
134 Backend.Failed { name = pkg; error = Printexc.to_string exn }
135136let update_package ~proc_mgr ~root ?cache pkg =
···183 if old_sha = new_sha then
184 Backend.No_changes pkg
185 else begin
186- (* Create worktrees *)
187- Worktree.ensure ~proc_mgr root (upstream_kind pkg);
188- Worktree.ensure ~proc_mgr root (vendor_kind pkg);
00000000000189190- let upstream_wt = Worktree.path root (upstream_kind pkg) in
191- let vendor_wt = Worktree.path root (vendor_kind pkg) in
000192193- (* Clear vendor content and copy new *)
194- let vendor_pkg_path = Eio.Path.(vendor_wt / "vendor" / "opam" / pkg) in
195- (try Eio.Path.rmtree vendor_pkg_path with _ -> ());
196197- copy_with_prefix
198- ~src_dir:upstream_wt
199- ~dst_dir:vendor_wt
200- ~prefix:(vendor_path pkg);
201202- (* Commit *)
203- Git.add_all ~proc_mgr ~cwd:vendor_wt;
204- Git.commit ~proc_mgr ~cwd:vendor_wt
205- ~message:(Printf.sprintf "Update %s to %s" pkg (String.sub new_sha 0 7));
206207- (* Cleanup *)
208- Worktree.remove ~proc_mgr root (upstream_kind pkg);
209- Worktree.remove ~proc_mgr root (vendor_kind pkg);
210211- Backend.Updated { name = pkg; old_sha; new_sha }
0000000212 end
213 end
214 with exn ->
215- (try Worktree.remove_force ~proc_mgr root (upstream_kind pkg) with _ -> ());
216- (try Worktree.remove_force ~proc_mgr root (vendor_kind pkg) with _ -> ());
217 Backend.Update_failed { name = pkg; error = Printexc.to_string exn }
218219let list_packages ~proc_mgr ~root =
···1(** Opam backend for unpac.
23+ Implements vendoring of opam packages using a two-tier branch model:
4 - opam/upstream/<pkg> - pristine upstream code
5+ - opam/patches/<pkg> - local modifications on top of upstream
067+ Packages are merged into projects using git subtree, which places files
8+ under vendor/opam/<pkg>/ without rewriting upstream history. *)
0910module Worktree = Unpac.Worktree
11module Git = Unpac.Git
···18(** {1 Branch Naming} *)
1920let upstream_branch pkg = "opam/upstream/" ^ pkg
021let patches_branch pkg = "opam/patches/" ^ pkg
22let vendor_path pkg = "vendor/opam/" ^ pkg
2324(** {1 Worktree Kinds} *)
2526let upstream_kind pkg = Worktree.Opam_upstream pkg
027let patches_kind pkg = Worktree.Opam_patches pkg
2829(** {1 Package Operations} *)
30000000000000000000000000000000000031let add_package ~proc_mgr ~root ?cache (info : Backend.package_info) =
32 let pkg = info.name in
33 let git = Worktree.git_dir root in
···67 Git.branch_force ~proc_mgr ~cwd:git
68 ~name:(upstream_branch pkg) ~point:ref_point;
6970+ (* Step 2: Create patches branch from upstream (initially identical) *)
71+ (* Patches will be merged into projects via git subtree *)
72+ Git.branch_create ~proc_mgr ~cwd:git
73+ ~name:(patches_branch pkg)
74+ ~start_point:(upstream_branch pkg);
0007576+ (* Get the SHA for reporting *)
77+ let sha = match Git.rev_parse ~proc_mgr ~cwd:git (patches_branch pkg) with
78 | Some sha -> sha
79+ | None -> failwith "Patches branch not found"
80 in
8182+ Backend.Added { name = pkg; sha }
0000083 end
84 with exn ->
85+ (* Cleanup on failure - just delete branches, no worktrees to remove *)
86+ (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; upstream_branch pkg] |> ignore with _ -> ());
87+ (try Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; patches_branch pkg] |> ignore with _ -> ());
88 Backend.Failed { name = pkg; error = Printexc.to_string exn }
8990let update_package ~proc_mgr ~root ?cache pkg =
···137 if old_sha = new_sha then
138 Backend.No_changes pkg
139 else begin
140+ (* Rebase patches branch onto new upstream *)
141+ (* First check if patches has diverged from upstream *)
142+ let patches_sha = Git.rev_parse_exn ~proc_mgr ~cwd:git (patches_branch pkg) in
143+ if patches_sha = old_sha then begin
144+ (* No local patches - just fast-forward patches branch *)
145+ Git.branch_force ~proc_mgr ~cwd:git
146+ ~name:(patches_branch pkg) ~point:(upstream_branch pkg);
147+ Backend.Updated { name = pkg; old_sha; new_sha }
148+ end else begin
149+ (* Has local patches - need to rebase *)
150+ (* Create a temporary worktree for rebasing *)
151+ let safe_name = String.map (fun c -> if c = '/' then '-' else c) pkg in
152+ let temp_wt_name = ".rebase-tmp-" ^ safe_name in
153+ let temp_wt_relpath = "../" ^ temp_wt_name in
154155+ let fs = fst git in
156+ let git_path = snd git in
157+ let parent_path = Filename.dirname git_path in
158+ let temp_wt_path = Filename.concat parent_path temp_wt_name in
159+ let temp_wt : Git.path = (fs, temp_wt_path) in
160161+ (* Remove any existing temp worktree *)
162+ ignore (Git.run ~proc_mgr ~cwd:git ["worktree"; "remove"; "-f"; temp_wt_relpath]);
0163164+ (* Create worktree for the patches branch *)
165+ Git.run_exn ~proc_mgr ~cwd:git ["worktree"; "add"; temp_wt_relpath; patches_branch pkg] |> ignore;
00166167+ (* Try to rebase *)
168+ let result = Git.rebase ~proc_mgr ~cwd:temp_wt ~onto:(upstream_branch pkg) in
00169170+ (* Cleanup temp worktree *)
171+ ignore (Git.run ~proc_mgr ~cwd:git ["worktree"; "remove"; "-f"; temp_wt_relpath]);
0172173+ match result with
174+ | Ok () ->
175+ Backend.Updated { name = pkg; old_sha; new_sha }
176+ | Error (`Conflict hint) ->
177+ (* Abort the rebase and report conflict *)
178+ Git.rebase_abort ~proc_mgr ~cwd:git;
179+ Backend.Update_failed { name = pkg; error = "Rebase conflict: " ^ hint }
180+ end
181 end
182 end
183 with exn ->
00184 Backend.Update_failed { name = pkg; error = Printexc.to_string exn }
185186let list_packages ~proc_mgr ~root =
+8-13
lib/opam/opam.mli
···1(** Opam backend for unpac.
23- Implements vendoring of opam packages using the three-tier branch model:
4 - opam/upstream/<pkg> - pristine upstream code
5- - opam/vendor/<pkg> - orphan branch with vendor/opam/<pkg>/ prefix
6- - opam/patches/<pkg> - local modifications *)
0078val name : string
9(** Backend name: "opam" *)
···1213val upstream_branch : string -> string
14(** [upstream_branch pkg] returns "opam/upstream/<pkg>". *)
15-16-val vendor_branch : string -> string
17-(** [vendor_branch pkg] returns "opam/vendor/<pkg>". *)
1819val patches_branch : string -> string
20(** [patches_branch pkg] returns "opam/patches/<pkg>". *)
···25(** {1 Worktree Kinds} *)
2627val upstream_kind : string -> Unpac.Worktree.kind
28-val vendor_kind : string -> Unpac.Worktree.kind
29val patches_kind : string -> Unpac.Worktree.kind
3031(** {1 Package Operations} *)
···39(** [add_package ~proc_mgr ~root ?cache info] vendors a single package.
4041 1. Fetches upstream into opam/upstream/<pkg> (via cache if provided)
42- 2. Creates opam/vendor/<pkg> with vendor/opam/<pkg>/ prefix (preserving history)
43- 3. Creates opam/patches/<pkg> from vendor
4445- Uses git-filter-repo for fast history rewriting.
46 @param cache Optional vendor cache for shared fetches across projects. *)
4748val update_package :
···54(** [update_package ~proc_mgr ~root ?cache name] updates a package from upstream.
5556 1. Fetches latest into opam/upstream/<pkg> (via cache if provided)
57- 2. Updates opam/vendor/<pkg> with new content
58-59- Does NOT rebase patches - call [Backend.rebase_patches] separately.
6061 @param cache Optional vendor cache for shared fetches across projects. *)
62
···1(** Opam backend for unpac.
23+ Implements vendoring of opam packages using a two-tier branch model:
4 - opam/upstream/<pkg> - pristine upstream code
5+ - opam/patches/<pkg> - local modifications on top of upstream
6+7+ Packages are merged into projects using git subtree, which places files
8+ under vendor/opam/<pkg>/ without rewriting upstream history. *)
910val name : string
11(** Backend name: "opam" *)
···1415val upstream_branch : string -> string
16(** [upstream_branch pkg] returns "opam/upstream/<pkg>". *)
0001718val patches_branch : string -> string
19(** [patches_branch pkg] returns "opam/patches/<pkg>". *)
···24(** {1 Worktree Kinds} *)
2526val upstream_kind : string -> Unpac.Worktree.kind
027val patches_kind : string -> Unpac.Worktree.kind
2829(** {1 Package Operations} *)
···37(** [add_package ~proc_mgr ~root ?cache info] vendors a single package.
3839 1. Fetches upstream into opam/upstream/<pkg> (via cache if provided)
40+ 2. Creates opam/patches/<pkg> from upstream (initially identical)
04142+ Use [git subtree add] to merge into a project.
43 @param cache Optional vendor cache for shared fetches across projects. *)
4445val update_package :
···51(** [update_package ~proc_mgr ~root ?cache name] updates a package from upstream.
5253 1. Fetches latest into opam/upstream/<pkg> (via cache if provided)
54+ 2. Rebases opam/patches/<pkg> onto the new upstream
005556 @param cache Optional vendor cache for shared fetches across projects. *)
57
+54-119
lib/promote.ml
···1(** Project promotion to vendor library.
23 Promotes a locally-developed project to a vendored library by:
4- 1. Filtering out the vendor/ directory from the project history
5- 2. Creating vendor branches (upstream/vendor/patches) for the specified backend
6- 3. Recording the promotion in the audit log
78- This allows the project to be merged into other projects as a dependency. *)
0910let src = Logs.Src.create "unpac.promote" ~doc:"Project promotion"
11module Log = (val Logs.src_log src : Logs.LOG)
···29 | Opam -> "opam/upstream/" ^ name
30 | Git -> "git/upstream/" ^ name
3132-let vendor_branch backend name = match backend with
33- | Opam -> "opam/vendor/" ^ name
34- | Git -> "git/vendor/" ^ name
35-36let patches_branch backend name = match backend with
37 | Opam -> "opam/patches/" ^ name
38 | Git -> "git/patches/" ^ name
···46 | Promoted of {
47 name : string;
48 backend : backend;
49- original_commits : int;
50- filtered_commits : int;
51 }
52 | Already_promoted of string
53 | Project_not_found of string
54 | Failed of { name : string; error : string }
5556-(** Filter a branch to exclude vendor/ directory.
57- Uses git-filter-repo to rewrite history. *)
58-let filter_vendor_directory ~proc_mgr ~cwd ~branch =
59- Log.info (fun m -> m "Filtering vendor/ directory from branch %s..." branch);
60-61- (* Use git-filter-repo with path filtering to exclude vendor/ *)
62- let fs = fst cwd in
63- let git_path = snd cwd in
64- let parent_path = Filename.dirname git_path in
65-66- (* Create a unique temporary worktree *)
67- let safe_branch = String.map (fun c -> if c = '/' then '-' else c) branch in
68- let temp_wt_name = ".filter-vendor-" ^ safe_branch in
69- let temp_wt_relpath = "../" ^ temp_wt_name in
70- let temp_wt_path = Filename.concat parent_path temp_wt_name in
71- let temp_wt : Git.path = (fs, temp_wt_path) in
72-73- (* Remove any existing temp worktree *)
74- ignore (Git.run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
7576- (* Create worktree for the branch *)
77- Git.run_exn ~proc_mgr ~cwd ["worktree"; "add"; temp_wt_relpath; branch] |> ignore;
7879- (* Count commits before filtering *)
80- let commits_before =
81- int_of_string (String.trim (Git.run_exn ~proc_mgr ~cwd:temp_wt ["rev-list"; "--count"; "HEAD"]))
82- in
83-84- (* Run git-filter-repo to exclude vendor/ *)
85- let result = Git.run ~proc_mgr ~cwd:temp_wt [
86- "filter-repo";
87- "--invert-paths";
88- "--path"; "vendor/";
89- "--force";
90- "--refs"; "HEAD"
91- ] in
92-93- match result with
94- | Ok _ ->
95- (* Count commits after filtering *)
96- let commits_after =
97- int_of_string (String.trim (Git.run_exn ~proc_mgr ~cwd:temp_wt ["rev-list"; "--count"; "HEAD"]))
98- in
99- (* Get the new HEAD SHA *)
100- let new_sha = Git.run_exn ~proc_mgr ~cwd:temp_wt ["rev-parse"; "HEAD"] |> String.trim in
101- (* Cleanup temporary worktree *)
102- ignore (Git.run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
103- (* Update the branch in the bare repo *)
104- Git.run_exn ~proc_mgr ~cwd ["branch"; "-f"; branch; new_sha] |> ignore;
105- Ok (commits_before, commits_after)
106- | Error e ->
107- (* Cleanup and return error *)
108- ignore (Git.run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
109- Error (Fmt.str "%a" Git.pp_error e)
110-111-(** Promote a project to a vendored library *)
112-let promote ~proc_mgr ~root ~project ~backend ~vendor_name =
113 let git = Worktree.git_dir root in
114 let name = Option.value ~default:project vendor_name in
115···123 Already_promoted name
124 else begin
125 try
126- Log.info (fun m -> m "Promoting project %s as %s vendor %s..." project (backend_to_string backend) name);
0127128 let project_branch = Worktree.branch (Worktree.Project project) in
129130- (* Step 1: Create a temporary branch from the project for filtering *)
131- let temp_branch = "promote-temp-" ^ name in
132- Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-f"; temp_branch; project_branch] |> ignore;
0133134- (* Step 2: Filter out vendor/ directory from the temp branch *)
135- let (commits_before, commits_after) =
136- match filter_vendor_directory ~proc_mgr ~cwd:git ~branch:temp_branch with
137- | Ok counts -> counts
138- | Error msg ->
139- (* Cleanup temp branch *)
140- ignore (Git.run ~proc_mgr ~cwd:git ["branch"; "-D"; temp_branch]);
141- failwith msg
142- in
143144- Log.info (fun m -> m "Filtered %d -> %d commits" commits_before commits_after);
145-146- (* Step 3: Create upstream branch (filtered, files at root) *)
147- (* For local projects, upstream is the same as filtered temp - no external upstream *)
148- Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-f"; upstream_branch backend name; temp_branch] |> ignore;
149150- (* Step 4: Create vendor branch from upstream and rewrite to vendor path *)
151- Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-f"; vendor_branch backend name; upstream_branch backend name] |> ignore;
152153- (* Rewrite vendor branch to move files into vendor/<backend>/<name>/ *)
154- Git.filter_repo_to_subdirectory ~proc_mgr ~cwd:git
155- ~branch:(vendor_branch backend name)
156- ~subdirectory:(vendor_path backend name);
0157158- (* Step 5: Create patches branch from vendor *)
159- Git.run_exn ~proc_mgr ~cwd:git ["branch"; patches_branch backend name; vendor_branch backend name] |> ignore;
160161- (* Step 6: Cleanup temp branch *)
162- ignore (Git.run ~proc_mgr ~cwd:git ["branch"; "-D"; temp_branch]);
000000163164- Promoted {
165- name;
166- backend;
167- original_commits = commits_before;
168- filtered_commits = commits_after
169- }
170 with exn ->
171 (* Cleanup on failure *)
172- let temp_branch = "promote-temp-" ^ name in
173- ignore (Git.run ~proc_mgr ~cwd:git ["branch"; "-D"; temp_branch]);
174 ignore (Git.run ~proc_mgr ~cwd:git ["branch"; "-D"; upstream_branch backend name]);
175- ignore (Git.run ~proc_mgr ~cwd:git ["branch"; "-D"; vendor_branch backend name]);
176 Failed { name = project; error = Printexc.to_string exn }
177 end
178 end
···301 | Already_exported of string
302 | Export_failed of { name : string; error : string }
303304-(** Export a vendored package back to root-level files.
305- This is the inverse of vendoring - takes a vendor branch and creates
306- an export branch with files moved from vendor/<backend>/<name>/ to root.
307308- Can export from either vendor/* or patches/* branch. *)
309-let export ~proc_mgr ~root ~name ~backend ~from_patches =
0310 let git = Worktree.git_dir root in
311312- (* Determine source branch *)
313- let source_br = if from_patches then patches_branch backend name
314- else vendor_branch backend name in
315 let export_br = export_branch backend name in
316- let subdir = vendor_path backend name in
317318 (* Check if source branch exists *)
319 if not (Git.branch_exists ~proc_mgr ~cwd:git source_br) then
···324 try
325 Log.info (fun m -> m "Exporting %s from %s to %s..." name source_br export_br);
326327- (* Step 1: Create export branch from source *)
328- Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-f"; export_br; source_br] |> ignore;
329330- (* Step 2: Count commits before transformation *)
331 let commits =
332 int_of_string (String.trim (
333 Git.run_exn ~proc_mgr ~cwd:git ["rev-list"; "--count"; export_br]))
334 in
335-336- (* Step 3: Rewrite export branch to move files from subdirectory to root *)
337- Git.filter_repo_from_subdirectory ~proc_mgr ~cwd:git
338- ~branch:export_br
339- ~subdirectory:subdir;
340341 Exported {
342 name;
···1(** Project promotion to vendor library.
23 Promotes a locally-developed project to a vendored library by:
4+ 1. Using git subtree split to extract a subdirectory into a branch
5+ 2. Creating upstream/patches branches for the specified backend
067+ This allows the project to be merged into other projects as a dependency
8+ using git subtree. *)
910let src = Logs.Src.create "unpac.promote" ~doc:"Project promotion"
11module Log = (val Logs.src_log src : Logs.LOG)
···29 | Opam -> "opam/upstream/" ^ name
30 | Git -> "git/upstream/" ^ name
31000032let patches_branch backend name = match backend with
33 | Opam -> "opam/patches/" ^ name
34 | Git -> "git/patches/" ^ name
···42 | Promoted of {
43 name : string;
44 backend : backend;
45+ source_prefix : string;
046 }
47 | Already_promoted of string
48 | Project_not_found of string
49 | Failed of { name : string; error : string }
5051+(** Promote a project subdirectory to a vendored library using subtree split.
0000000000000000005253+ Unlike the old filter-repo approach, this extracts a specific subdirectory
54+ from the project into standalone branches that can be subtree'd elsewhere.
5556+ @param prefix The subdirectory path within the project to extract (e.g., "src/mylib") *)
57+let promote ~proc_mgr ~root ~project ~backend ~vendor_name ~prefix =
0000000000000000000000000000000058 let git = Worktree.git_dir root in
59 let name = Option.value ~default:project vendor_name in
60···68 Already_promoted name
69 else begin
70 try
71+ Log.info (fun m -> m "Promoting project %s prefix %s as %s vendor %s..."
72+ project prefix (backend_to_string backend) name);
7374 let project_branch = Worktree.branch (Worktree.Project project) in
7576+ (* Create a temporary worktree for the project to run subtree split *)
77+ let safe_name = String.map (fun c -> if c = '/' then '-' else c) project in
78+ let temp_wt_name = ".promote-tmp-" ^ safe_name in
79+ let temp_wt_relpath = "../" ^ temp_wt_name in
8081+ let fs = fst git in
82+ let git_path = snd git in
83+ let parent_path = Filename.dirname git_path in
84+ let temp_wt_path = Filename.concat parent_path temp_wt_name in
85+ let temp_wt : Git.path = (fs, temp_wt_path) in
00008687+ (* Remove any existing temp worktree *)
88+ ignore (Git.run ~proc_mgr ~cwd:git ["worktree"; "remove"; "-f"; temp_wt_relpath]);
0008990+ (* Create worktree for the project branch *)
91+ Git.run_exn ~proc_mgr ~cwd:git ["worktree"; "add"; temp_wt_relpath; project_branch] |> ignore;
9293+ (* Use git subtree split to extract the prefix into a new branch *)
94+ let upstream_br = upstream_branch backend name in
95+ let result = Git.run ~proc_mgr ~cwd:temp_wt [
96+ "subtree"; "split"; "--prefix"; prefix; "-b"; upstream_br
97+ ] in
9899+ (* Cleanup temp worktree *)
100+ ignore (Git.run ~proc_mgr ~cwd:git ["worktree"; "remove"; "-f"; temp_wt_relpath]);
101102+ (match result with
103+ | Error e ->
104+ raise (Eio.Exn.create (Git.E e))
105+ | Ok _ ->
106+ (* Create patches branch from upstream (initially identical) *)
107+ Git.branch_create ~proc_mgr ~cwd:git
108+ ~name:(patches_branch backend name)
109+ ~start_point:upstream_br;
110111+ Promoted {
112+ name;
113+ backend;
114+ source_prefix = prefix;
115+ })
0116 with exn ->
117 (* Cleanup on failure *)
00118 ignore (Git.run ~proc_mgr ~cwd:git ["branch"; "-D"; upstream_branch backend name]);
0119 Failed { name = project; error = Printexc.to_string exn }
120 end
121 end
···244 | Already_exported of string
245 | Export_failed of { name : string; error : string }
246247+(** Export a vendored package to an export branch for pushing.
00248249+ In the new subtree architecture, upstream and patches branches already have
250+ files at root, so export is just creating a copy of the patches branch. *)
251+let export ~proc_mgr ~root ~name ~backend =
252 let git = Worktree.git_dir root in
253254+ (* Source is always the patches branch (which has files at root) *)
255+ let source_br = patches_branch backend name in
0256 let export_br = export_branch backend name in
0257258 (* Check if source branch exists *)
259 if not (Git.branch_exists ~proc_mgr ~cwd:git source_br) then
···264 try
265 Log.info (fun m -> m "Exporting %s from %s to %s..." name source_br export_br);
266267+ (* Create export branch as a copy of patches branch *)
268+ Git.run_exn ~proc_mgr ~cwd:git ["branch"; export_br; source_br] |> ignore;
269270+ (* Count commits *)
271 let commits =
272 int_of_string (String.trim (
273 Git.run_exn ~proc_mgr ~cwd:git ["rev-list"; "--count"; export_br]))
274 in
00000275276 Exported {
277 name;
+24-32
lib/promote.mli
···1(** Project promotion to vendor library.
23 Promotes a locally-developed project to a vendored library by:
4- 1. Filtering out the vendor/ directory from the project history
5- 2. Creating vendor branches (upstream/vendor/patches) for the specified backend
6- 3. Recording the promotion in the audit log
78- This allows the project to be merged into other projects as a dependency. *)
0910(** {1 Backend Types} *)
11···26(** [upstream_branch backend name] returns the upstream branch name,
27 e.g., "opam/upstream/brotli" or "git/upstream/brotli" *)
2829-val vendor_branch : backend -> string -> string
30-(** [vendor_branch backend name] returns the vendor branch name *)
31-32val patches_branch : backend -> string -> string
33(** [patches_branch backend name] returns the patches branch name *)
34···43 | Promoted of {
44 name : string; (** Vendor library name *)
45 backend : backend; (** Backend used *)
46- original_commits : int; (** Commits in project before filtering *)
47- filtered_commits : int; (** Commits after removing vendor/ *)
48 }
49 | Already_promoted of string
50 (** Library already exists with this name *)
···59 project:string ->
60 backend:backend ->
61 vendor_name:string option ->
062 promote_result
63-(** [promote ~proc_mgr ~root ~project ~backend ~vendor_name] promotes
64- a local project to a vendored library.
6566 The operation:
67 1. Checks that the project exists and hasn't been promoted yet
68- 2. Creates a filtered copy of project history (excluding vendor/)
69- 3. Creates upstream/vendor/patches branches for the backend
70 4. The original project branch is preserved unchanged
7172- @param project Name of the project to promote (e.g., "brotli")
73 @param backend Backend type (Opam or Git)
74 @param vendor_name Optional override for the vendor library name
07576 After promotion, the library can be merged into other projects using:
77 - [unpac opam merge <name> <project>] for Opam backend
···142(** [get_info ~proc_mgr ~root ~project] returns information about a project,
143 or None if the project doesn't exist. *)
144145-(** {1 Export (Unvendor)}
146-147- Export reverses the vendoring process, creating a branch with files
148- at the repository root suitable for pushing to an external git repo.
149150- This is the inverse of vendoring:
151- - Vendoring: files at root โ files in vendor/<backend>/<name>/
152- - Exporting: files in vendor/<backend>/<name>/ โ files at root *)
153154val export_branch : backend -> string -> string
155(** [export_branch backend name] returns the export branch name,
···160 | Exported of {
161 name : string; (** Package name *)
162 backend : backend; (** Backend used *)
163- source_branch : string; (** Branch exported from (vendor or patches) *)
164 export_branch : string; (** Created export branch *)
165 commits : int; (** Number of commits in export *)
166 }
167 | Not_vendored of string
168- (** No vendor branch exists for this package *)
169 | Already_exported of string
170 (** Export branch already exists *)
171 | Export_failed of { name : string; error : string }
···176 root:Worktree.root ->
177 name:string ->
178 backend:backend ->
179- from_patches:bool ->
180 export_result
181-(** [export ~proc_mgr ~root ~name ~backend ~from_patches] exports a vendored
182- package back to root-level files.
183184- Creates an export branch where files are moved from [vendor/<backend>/<name>/]
185- to the repository root. This branch can then be pushed to an upstream repo.
186187 @param name The vendored package name
188 @param backend The backend (Opam or Git)
189- @param from_patches If true, exports from patches/* branch (includes local mods);
190- if false, exports from vendor/* branch (pristine upstream)
191192 The export branch is named [<backend>/export/<name>], e.g., "git/export/brotli".
193194 Example workflow:
195 {[
196- (* Export with local patches *)
197- export ~from_patches:true ...
198199 (* Set remote and push *)
200 set_export_remote ~url:"git@github.com:me/brotli.git" ...
···1(** Project promotion to vendor library.
23 Promotes a locally-developed project to a vendored library by:
4+ 1. Using git subtree split to extract a subdirectory into a branch
5+ 2. Creating upstream/patches branches for the specified backend
067+ This allows the project to be merged into other projects as a dependency
8+ using git subtree. *)
910(** {1 Backend Types} *)
11···26(** [upstream_branch backend name] returns the upstream branch name,
27 e.g., "opam/upstream/brotli" or "git/upstream/brotli" *)
2800029val patches_branch : backend -> string -> string
30(** [patches_branch backend name] returns the patches branch name *)
31···40 | Promoted of {
41 name : string; (** Vendor library name *)
42 backend : backend; (** Backend used *)
43+ source_prefix : string; (** Prefix extracted from project *)
044 }
45 | Already_promoted of string
46 (** Library already exists with this name *)
···55 project:string ->
56 backend:backend ->
57 vendor_name:string option ->
58+ prefix:string ->
59 promote_result
60+(** [promote ~proc_mgr ~root ~project ~backend ~vendor_name ~prefix] promotes
61+ a project subdirectory to a vendored library using subtree split.
6263 The operation:
64 1. Checks that the project exists and hasn't been promoted yet
65+ 2. Uses [git subtree split] to extract the prefix into a branch
66+ 3. Creates upstream/patches branches for the backend
67 4. The original project branch is preserved unchanged
6869+ @param project Name of the project to promote (e.g., "myapp")
70 @param backend Backend type (Opam or Git)
71 @param vendor_name Optional override for the vendor library name
72+ @param prefix The subdirectory path within the project to extract (e.g., "src/mylib")
7374 After promotion, the library can be merged into other projects using:
75 - [unpac opam merge <name> <project>] for Opam backend
···140(** [get_info ~proc_mgr ~root ~project] returns information about a project,
141 or None if the project doesn't exist. *)
142143+(** {1 Export}
000144145+ Export creates a branch suitable for pushing to an external git repo.
146+ In the subtree architecture, patches branches already have files at root,
147+ so export is simply a copy of the patches branch. *)
148149val export_branch : backend -> string -> string
150(** [export_branch backend name] returns the export branch name,
···155 | Exported of {
156 name : string; (** Package name *)
157 backend : backend; (** Backend used *)
158+ source_branch : string; (** Branch exported from (patches) *)
159 export_branch : string; (** Created export branch *)
160 commits : int; (** Number of commits in export *)
161 }
162 | Not_vendored of string
163+ (** No patches branch exists for this package *)
164 | Already_exported of string
165 (** Export branch already exists *)
166 | Export_failed of { name : string; error : string }
···171 root:Worktree.root ->
172 name:string ->
173 backend:backend ->
0174 export_result
175+(** [export ~proc_mgr ~root ~name ~backend] exports a vendored
176+ package to an export branch for pushing.
177178+ Creates an export branch as a copy of the patches branch.
179+ This branch can then be pushed to an upstream repo.
180181 @param name The vendored package name
182 @param backend The backend (Opam or Git)
00183184 The export branch is named [<backend>/export/<name>], e.g., "git/export/brotli".
185186 Example workflow:
187 {[
188+ (* Export *)
189+ export ...
190191 (* Set remote and push *)
192 set_export_remote ~url:"git@github.com:me/brotli.git" ...
+4-9
lib/worktree.ml
···12 | Main
13 | Project of string
14 | Opam_upstream of string
15- | Opam_vendor of string
16 | Opam_patches of string
17 | Git_upstream of string
18- | Git_vendor of string
19 | Git_patches of string
20(** Worktree kinds with their associated names.
21 Opam_* variants are for opam package vendoring.
22- Git_* variants are for direct git repository vendoring. *)
0002324(** {1 Path and Branch Helpers} *)
25···30 | Main -> Eio.Path.(root / "main")
31 | Project name -> Eio.Path.(root / "project" / name)
32 | Opam_upstream name -> Eio.Path.(root / "opam" / "upstream" / name)
33- | Opam_vendor name -> Eio.Path.(root / "opam" / "vendor" / name)
34 | Opam_patches name -> Eio.Path.(root / "opam" / "patches" / name)
35 | Git_upstream name -> Eio.Path.(root / "git-repos" / "upstream" / name)
36- | Git_vendor name -> Eio.Path.(root / "git-repos" / "vendor" / name)
37 | Git_patches name -> Eio.Path.(root / "git-repos" / "patches" / name)
3839let branch = function
40 | Main -> "main"
41 | Project name -> "project/" ^ name
42 | Opam_upstream name -> "opam/upstream/" ^ name
43- | Opam_vendor name -> "opam/vendor/" ^ name
44 | Opam_patches name -> "opam/patches/" ^ name
45 | Git_upstream name -> "git/upstream/" ^ name
46- | Git_vendor name -> "git/vendor/" ^ name
47 | Git_patches name -> "git/patches/" ^ name
4849let relative_path = function
50 | Main -> "main"
51 | Project name -> "project/" ^ name
52 | Opam_upstream name -> "opam/upstream/" ^ name
53- | Opam_vendor name -> "opam/vendor/" ^ name
54 | Opam_patches name -> "opam/patches/" ^ name
55 | Git_upstream name -> "git-repos/upstream/" ^ name
56- | Git_vendor name -> "git-repos/vendor/" ^ name
57 | Git_patches name -> "git-repos/patches/" ^ name
5859(** {1 Queries} *)
···12 | Main
13 | Project of string
14 | Opam_upstream of string
015 | Opam_patches of string
16 | Git_upstream of string
017 | Git_patches of string
18(** Worktree kinds with their associated names.
19 Opam_* variants are for opam package vendoring.
20+ Git_* variants are for direct git repository vendoring.
21+22+ Note: In the subtree architecture, upstream and patches are branches only
23+ (no worktrees). These variants are kept for branch name computation. *)
2425(** {1 Path and Branch Helpers} *)
26···31 | Main -> Eio.Path.(root / "main")
32 | Project name -> Eio.Path.(root / "project" / name)
33 | Opam_upstream name -> Eio.Path.(root / "opam" / "upstream" / name)
034 | Opam_patches name -> Eio.Path.(root / "opam" / "patches" / name)
35 | Git_upstream name -> Eio.Path.(root / "git-repos" / "upstream" / name)
036 | Git_patches name -> Eio.Path.(root / "git-repos" / "patches" / name)
3738let branch = function
39 | Main -> "main"
40 | Project name -> "project/" ^ name
41 | Opam_upstream name -> "opam/upstream/" ^ name
042 | Opam_patches name -> "opam/patches/" ^ name
43 | Git_upstream name -> "git/upstream/" ^ name
044 | Git_patches name -> "git/patches/" ^ name
4546let relative_path = function
47 | Main -> "main"
48 | Project name -> "project/" ^ name
49 | Opam_upstream name -> "opam/upstream/" ^ name
050 | Opam_patches name -> "opam/patches/" ^ name
51 | Git_upstream name -> "git-repos/upstream/" ^ name
052 | Git_patches name -> "git-repos/patches/" ^ name
5354(** {1 Queries} *)
+21-22
lib/worktree.mli
···1(** Git worktree lifecycle management for unpac.
23 Manages creation, cleanup, and paths of worktrees within the unpac
4- directory structure. All branch operations happen in isolated worktrees.
56 {2 Directory Structure}
78 An unpac project has this layout:
9 {v
10 my-project/
11- โโโ git/ # Bare repository
12 โโโ main/ # Worktree โ main branch
13- โโโ project/
14- โ โโโ myapp/ # Worktree โ project/myapp
15- โโโ opam/
16- โ โโโ upstream/
17- โ โ โโโ pkg/ # Worktree โ opam/upstream/pkg
18- โ โโโ vendor/
19- โ โ โโโ pkg/ # Worktree โ opam/vendor/pkg
20- โ โโโ patches/
21- โ โโโ pkg/ # Worktree โ opam/patches/pkg
22- โโโ git-repos/
23- โโโ upstream/
24- โ โโโ repo/ # Worktree โ git/upstream/repo
25- โโโ vendor/
26- โ โโโ repo/ # Worktree โ git/vendor/repo
27- โโโ patches/
28- โโโ repo/ # Worktree โ git/patches/repo
29- v} *)
3031(** {1 Types} *)
32···37 | Main
38 | Project of string
39 | Opam_upstream of string
40- | Opam_vendor of string
41 | Opam_patches of string
42 | Git_upstream of string
43- | Git_vendor of string
44 | Git_patches of string
45(** Worktree kinds with their associated names.
46 Opam_* variants are for opam package vendoring.
47- Git_* variants are for direct git repository vendoring. *)
0004849(** {1 Path and Branch Helpers} *)
50
···1(** Git worktree lifecycle management for unpac.
23 Manages creation, cleanup, and paths of worktrees within the unpac
4+ directory structure. Project branches get isolated worktrees.
56 {2 Directory Structure}
78 An unpac project has this layout:
9 {v
10 my-project/
11+ โโโ git/ # Bare repository (stores all branches)
12 โโโ main/ # Worktree โ main branch
13+ โโโ project/
14+ โโโ myapp/ # Worktree โ project/myapp
15+ v}
16+17+ {2 Branch Structure}
18+19+ Branches are organized as:
20+ - [main] - main branch with unpac.toml configuration
21+ - [project/<name>] - project branches (have worktrees)
22+ - [opam/upstream/<pkg>] - pristine upstream for opam packages (branch only)
23+ - [opam/patches/<pkg>] - local modifications for opam packages (branch only)
24+ - [git/upstream/<name>] - pristine upstream for git repos (branch only)
25+ - [git/patches/<name>] - local modifications for git repos (branch only)
26+27+ Upstream and patches branches are merged into projects using git subtree. *)
002829(** {1 Types} *)
30···35 | Main
36 | Project of string
37 | Opam_upstream of string
038 | Opam_patches of string
39 | Git_upstream of string
040 | Git_patches of string
41(** Worktree kinds with their associated names.
42 Opam_* variants are for opam package vendoring.
43+ Git_* variants are for direct git repository vendoring.
44+45+ Note: In the subtree architecture, upstream and patches are branches only
46+ (no worktrees). These variants are kept for branch name computation. *)
4748(** {1 Path and Branch Helpers} *)
49
+5-5
test/cram/full-workflow.t
···28Initialize unpac project
2930 $ unpac init myproj
31- Initialized unpac project at myproj
3233 Next steps:
34 cd myproj
···5354Create a project
5556- $ unpac project new myapp
57 Created project myapp
5859 Next steps:
···88 $ unpac opam edit testlib 2>&1 | head -1
89 Editing testlib
9091-Make a change in the patches worktree
9293- $ echo 'let goodbye () = "Goodbye!"' >> opam/patches/testlib/vendor/opam/testlib/lib.ml
94 $ (cd opam/patches/testlib && git add -A && git commit -q -m "Add goodbye function")
9596Now we have local changes
···113Merge into project
114115 $ unpac opam merge testlib myapp 2>&1 | grep -E "^(Merged|Merge)"
116- Merged testlib into project myapp
117118Verify files in project
119
···28Initialize unpac project
2930 $ unpac init myproj
31+ Initialized unpac workspace at myproj
3233 Next steps:
34 cd myproj
···5354Create a project
5556+ $ unpac project new myapp 2>&1 | grep -v "^\[INFO\]\|unpac: \[INFO\]"
57 Created project myapp
5859 Next steps:
···88 $ unpac opam edit testlib 2>&1 | head -1
89 Editing testlib
9091+Make a change in the patches worktree (files are at root, not under vendor/)
9293+ $ echo 'let goodbye () = "Goodbye!"' >> opam/patches/testlib/lib.ml
94 $ (cd opam/patches/testlib && git add -A && git commit -q -m "Add goodbye function")
9596Now we have local changes
···113Merge into project
114115 $ unpac opam merge testlib myapp 2>&1 | grep -E "^(Merged|Merge)"
116+ Merged testlib to vendor/opam/testlib
117118Verify files in project
119
+2-2
test/cram/init.t
···1Initialize a new unpac project
23 $ unpac init myproject
4- Initialized unpac project at myproject
56 Next steps:
7 cd myproject
···29Check git branches (main is in worktree so shows +)
3031 $ git -C myproject/git branch
32- + main
3334Init should fail if directory exists
35
···1Initialize a new unpac project
23 $ unpac init myproject
4+ Initialized unpac workspace at myproject
56 Next steps:
7 cd myproject
···29Check git branches (main is in worktree so shows +)
3031 $ git -C myproject/git branch
32+ * main
3334Init should fail if directory exists
35
+8-9
test/cram/opam.t
···21Create unpac project
2223 $ unpac init myproj
24- Initialized unpac project at myproj
2526 Next steps:
27 cd myproj
···51 $ git -C git branch | grep opam | sort
52 opam/patches/testpkg
53 opam/upstream/testpkg
54- opam/vendor/testpkg
5556-Check vendor branch has prefixed content
5758- $ git -C git show opam/vendor/testpkg --name-only | grep "^vendor/"
59- vendor/opam/testpkg/dune-project
60- vendor/opam/testpkg/lib.ml
6162Create a project and merge
6364- $ unpac project new myapp
65 Created project myapp
6667 Next steps:
···69 unpac opam merge <package> myapp # merge package into project
7071 $ unpac opam merge testpkg myapp 2>&1 | grep -E "^(Merged|Merge conflict)"
72- Merged testpkg into project myapp
7374Check files appear in project
75···8283Check git log shows merge
8485- $ git -C project/myapp log --oneline | wc -l
86 3
···21Create unpac project
2223 $ unpac init myproj
24+ Initialized unpac workspace at myproj
2526 Next steps:
27 cd myproj
···51 $ git -C git branch | grep opam | sort
52 opam/patches/testpkg
53 opam/upstream/testpkg
05455+Check patches branch has content at root (no vendor/ prefix in branch)
5657+ $ git -C git show opam/patches/testpkg --name-only | tail -2
58+ dune-project
59+ lib.ml
6061Create a project and merge
6263+ $ unpac project new myapp 2>&1 | grep -v "^\[INFO\]\|unpac: \[INFO\]"
64 Created project myapp
6566 Next steps:
···68 unpac opam merge <package> myapp # merge package into project
6970 $ unpac opam merge testpkg myapp 2>&1 | grep -E "^(Merged|Merge conflict)"
71+ Merged testpkg to vendor/opam/testpkg
7273Check files appear in project
74···8182Check git log shows merge
8384+ $ git -C project/myapp log --oneline | wc -l | tr -d ' '
85 3
+4-3
test/cram/project.t
···3Setup: create an unpac project first
45 $ unpac init testproj
6- Initialized unpac project at testproj
78 Next steps:
9 cd testproj
···1314Create a new project
1516- $ unpac project new myapp
17 Created project myapp
1819 Next steps:
···39Check vendor directory structure
4041 $ ls project/myapp/vendor
042 opam
4344List projects
···5354Create another project
5556- $ unpac project new otherapp
57 Created project otherapp
5859 Next steps:
···3Setup: create an unpac project first
45 $ unpac init testproj
6+ Initialized unpac workspace at testproj
78 Next steps:
9 cd testproj
···1314Create a new project
1516+ $ unpac project new myapp 2>&1 | grep -v "^\[INFO\]\|unpac: \[INFO\]"
17 Created project myapp
1819 Next steps:
···39Check vendor directory structure
4041 $ ls project/myapp/vendor
42+ dune
43 opam
4445List projects
···5455Create another project
5657+ $ unpac project new otherapp 2>&1 | grep -v "^\[INFO\]\|unpac: \[INFO\]"
58 Created project otherapp
5960 Next steps: