···318318 let doc = "Path to vendor cache (overrides config and UNPAC_VENDOR_CACHE env var)." in
319319 Arg.(value & opt (some string) None & info ["cache"] ~docv:"PATH" ~doc)
320320 in
321321- let run () pkg name_opt version_opt branch_opt solve cli_cache =
321321+ let fast_arg =
322322+ let doc = "Fast mode: skip history rewriting (useful for large repos like dune)." in
323323+ Arg.(value & flag & info ["fast"] ~doc)
324324+ in
325325+ let run () pkg name_opt version_opt branch_opt solve cli_cache fast =
326326+ let preserve_history = not fast in
322327 with_root @@ fun ~env:_ ~fs ~proc_mgr ~root ->
323328 let config = load_config root in
324329 let cache = resolve_cache ~proc_mgr ~fs ~config ~cli_cache in
···383388 url;
384389 branch = None;
385390 } in
386386- match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with
391391+ match Unpac_opam.Opam.add_package ~preserve_history ~proc_mgr ~root ?cache info with
387392 | Unpac.Backend.Added { name = pkg_name; sha } ->
388393 Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7);
389394 if List.length g.packages > 1 then
···442447 url;
443448 branch = branch_opt;
444449 } in
445445- match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with
450450+ match Unpac_opam.Opam.add_package ~preserve_history ~proc_mgr ~root ?cache info with
446451 | Unpac.Backend.Added { name = pkg_name; sha } ->
447452 Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7);
448453 Format.printf "@.Next steps:@.";
···456461 end
457462 in
458463 let info = Cmd.info "add" ~doc in
459459- Cmd.v info Term.(const run $ logging_term $ pkg_arg $ name_arg $ version_arg $ branch_arg $ solve_arg $ cache_arg)
464464+ Cmd.v info Term.(const run $ logging_term $ pkg_arg $ name_arg $ version_arg $ branch_arg $ solve_arg $ cache_arg $ fast_arg)
460465461466(* Opam list command *)
462467let opam_list_cmd =
···475480476481(* Opam edit command *)
477482let opam_edit_cmd =
478478- let doc = "Open a package's patches worktree for editing." in
483483+ let doc = "Open a package's patches worktree for editing. \
484484+ Also creates a vendor worktree for reference." in
479485 let pkg_arg =
480486 let doc = "Package name to edit." in
481487 Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
···488494 Format.eprintf "Package '%s' is not vendored@." pkg;
489495 exit 1
490496 end;
491491- (* Ensure patches worktree exists *)
497497+ (* Ensure both patches and vendor worktrees exist *)
492498 Unpac.Worktree.ensure ~proc_mgr root (Unpac.Worktree.Opam_patches pkg);
493493- let wt_path = Unpac.Worktree.path root (Unpac.Worktree.Opam_patches pkg) in
494494- let path_str = snd wt_path in
499499+ Unpac.Worktree.ensure ~proc_mgr root (Unpac.Worktree.Opam_vendor pkg);
500500+ let patches_path = snd (Unpac.Worktree.path root (Unpac.Worktree.Opam_patches pkg)) in
501501+ let vendor_path = snd (Unpac.Worktree.path root (Unpac.Worktree.Opam_vendor pkg)) in
495502 Format.printf "Editing %s@." pkg;
496496- Format.printf "Worktree: %s@." path_str;
497503 Format.printf "@.";
498498- Format.printf "Make your changes, then:@.";
499499- Format.printf " cd %s@." path_str;
504504+ Format.printf "Worktrees created:@.";
505505+ Format.printf " patches: %s (make changes here)@." patches_path;
506506+ Format.printf " vendor: %s (original for reference)@." vendor_path;
507507+ Format.printf "@.";
508508+ Format.printf "Make your changes in the patches worktree, then:@.";
509509+ Format.printf " cd %s@." patches_path;
500510 Format.printf " git add -A && git commit -m 'your message'@.";
501511 Format.printf "@.";
502512 Format.printf "When done: unpac opam done %s@." pkg
···506516507517(* Opam done command *)
508518let opam_done_cmd =
509509- let doc = "Close a package's patches worktree after editing." in
519519+ let doc = "Close a package's patches and vendor worktrees after editing." in
510520 let pkg_arg =
511521 let doc = "Package name." in
512522 Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
513523 in
514524 let run () pkg =
515525 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
516516- let kind = Unpac.Worktree.Opam_patches pkg in
517517- if not (Unpac.Worktree.exists root kind) then begin
526526+ let patches_kind = Unpac.Worktree.Opam_patches pkg in
527527+ let vendor_kind = Unpac.Worktree.Opam_vendor pkg in
528528+ if not (Unpac.Worktree.exists root patches_kind) then begin
518529 Format.eprintf "No editing session for '%s'@." pkg;
519530 exit 1
520531 end;
521521- (* Check for uncommitted changes *)
522522- let wt_path = Unpac.Worktree.path root kind in
532532+ (* Check for uncommitted changes in patches worktree *)
533533+ let wt_path = Unpac.Worktree.path root patches_kind in
523534 let status = Unpac.Git.run_exn ~proc_mgr ~cwd:wt_path ["status"; "--porcelain"] in
524535 if String.trim status <> "" then begin
525536 Format.eprintf "Warning: uncommitted changes in %s@." pkg;
526537 Format.eprintf "Commit or discard them before closing.@.";
527538 exit 1
528539 end;
529529- (* Remove worktree *)
530530- Unpac.Worktree.remove ~proc_mgr root kind;
540540+ (* Remove both worktrees *)
541541+ Unpac.Worktree.remove ~proc_mgr root patches_kind;
542542+ if Unpac.Worktree.exists root vendor_kind then
543543+ Unpac.Worktree.remove ~proc_mgr root vendor_kind;
531544 Format.printf "Closed editing session for %s@." pkg;
532545 Format.printf "@.Next steps:@.";
533546 Format.printf " unpac opam diff %s # view your changes@." pkg;
···563576564577(* Opam merge command *)
565578let opam_merge_cmd =
566566- let doc = "Merge a vendored opam package into a project." in
567567- let pkg_arg =
568568- let doc = "Package name to merge." in
569569- Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
579579+ let doc = "Merge vendored opam packages into a project. \
580580+ Use --all to merge all vendored packages." in
581581+ let args =
582582+ let doc = "With --all: PROJECT. Without --all: PACKAGE PROJECT." in
583583+ Arg.(value & pos_all string [] & info [] ~docv:"ARGS" ~doc)
570584 in
571571- let project_arg =
572572- let doc = "Target project name." in
573573- Arg.(required & pos 1 (some string) None & info [] ~docv:"PROJECT" ~doc)
585585+ let all_flag =
586586+ let doc = "Merge all vendored packages into the project." in
587587+ Arg.(value & flag & info ["all"] ~doc)
574588 in
575575- let run () pkg project =
589589+ let run () args all =
576590 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
577577- let patches_branch = Unpac_opam.Opam.patches_branch pkg in
578578- match Unpac.Backend.merge_to_project ~proc_mgr ~root ~project ~patches_branch with
579579- | Ok () ->
580580- Format.printf "Merged %s into project %s@." pkg project;
581581- Format.printf "@.Next: Build your project in project/%s@." project
582582- | Error (`Conflict files) ->
583583- Format.eprintf "Merge conflict in %s:@." pkg;
584584- List.iter (Format.eprintf " %s@.") files;
591591+ (* Parse arguments based on --all flag *)
592592+ let pkg, project = if all then
593593+ match args with
594594+ | [project] -> None, project
595595+ | _ ->
596596+ Format.eprintf "Usage: unpac opam merge --all PROJECT@.";
597597+ exit 1
598598+ else
599599+ match args with
600600+ | [pkg; project] -> Some pkg, project
601601+ | _ ->
602602+ Format.eprintf "Usage: unpac opam merge PACKAGE PROJECT@.";
603603+ exit 1
604604+ in
605605+ let merge_one pkg =
606606+ let patches_branch = Unpac_opam.Opam.patches_branch pkg in
607607+ match Unpac.Backend.merge_to_project ~proc_mgr ~root ~project ~patches_branch with
608608+ | Ok () ->
609609+ Format.printf "Merged %s into project %s@." pkg project;
610610+ true
611611+ | Error (`Conflict files) ->
612612+ Format.eprintf "Merge conflict in %s:@." pkg;
613613+ List.iter (Format.eprintf " %s@.") files;
614614+ false
615615+ in
616616+ if all then begin
617617+ (* Merge all vendored packages *)
618618+ let packages = Unpac_opam.Opam.list_packages ~proc_mgr ~root in
619619+ if packages = [] then begin
620620+ Format.eprintf "No vendored packages to merge.@.";
621621+ exit 1
622622+ end;
623623+ Format.printf "Merging %d packages into project %s...@." (List.length packages) project;
624624+ let (successes, failures) = List.fold_left (fun (s, f) pkg ->
625625+ if merge_one pkg then (s + 1, f) else (s, f + 1)
626626+ ) (0, 0) packages in
627627+ Format.printf "@.Done: %d merged" successes;
628628+ if failures > 0 then Format.printf ", %d had conflicts" failures;
629629+ Format.printf "@.";
630630+ if failures > 0 then begin
585631 Format.eprintf "Resolve conflicts in project/%s and commit.@." project;
586632 exit 1
633633+ end else
634634+ Format.printf "Next: Build your project in project/%s@." project
635635+ end else begin
636636+ match pkg with
637637+ | Some pkg ->
638638+ if merge_one pkg then
639639+ Format.printf "@.Next: Build your project in project/%s@." project
640640+ else begin
641641+ Format.eprintf "Resolve conflicts in project/%s and commit.@." project;
642642+ exit 1
643643+ end
644644+ | None ->
645645+ Format.eprintf "Error: Either provide a package name or use --all@.";
646646+ exit 1
647647+ end
587648 in
588649 let info = Cmd.info "merge" ~doc in
589589- Cmd.v info Term.(const run $ logging_term $ pkg_arg $ project_arg)
650650+ Cmd.v info Term.(const run $ logging_term $ args $ all_flag)
590651591652(* Opam info command *)
592653let opam_info_cmd =
+61
lib/git.ml
···395395let clean_fd ~proc_mgr ~cwd =
396396 Log.debug (fun m -> m "Cleaning untracked files");
397397 run_exn ~proc_mgr ~cwd ["clean"; "-fd"] |> ignore
398398+399399+let filter_branch_to_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory =
400400+ Log.info (fun m -> m "Rewriting history of %s into subdirectory %s..." branch subdirectory);
401401+ (* Use filter-branch with index-filter to rewrite all paths into subdirectory.
402402+ This preserves full history with paths prefixed.
403403+404404+ For bare repositories, we need to create a temporary worktree, run filter-branch
405405+ there, and then update the branch in the bare repo. *)
406406+407407+ (* Create a unique temporary worktree name using the branch name *)
408408+ let safe_branch = String.map (fun c -> if c = '/' then '-' else c) branch in
409409+ let temp_wt_name = ".filter-tmp-" ^ safe_branch in
410410+ let temp_wt_relpath = "../" ^ temp_wt_name in
411411+412412+ (* Construct the worktree path - cwd is (fs, path_string), so we go up one level *)
413413+ let fs = fst cwd in
414414+ let git_path = snd cwd in
415415+ let parent_path = Filename.dirname git_path in
416416+ let temp_wt_path = Filename.concat parent_path temp_wt_name in
417417+ let temp_wt : path = (fs, temp_wt_path) in
418418+419419+ (* Remove any existing temp worktree *)
420420+ ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
421421+422422+ (* Create worktree for the branch *)
423423+ run_exn ~proc_mgr ~cwd ["worktree"; "add"; temp_wt_relpath; branch] |> ignore;
424424+425425+ (* Run filter-branch in the worktree *)
426426+ let script = Printf.sprintf
427427+ {|git ls-files -s | sed "s,\t,&%s/," | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"|}
428428+ subdirectory
429429+ in
430430+431431+ (* Set environment to suppress the warning *)
432432+ let old_env = try Some (Unix.getenv "FILTER_BRANCH_SQUELCH_WARNING") with Not_found -> None in
433433+ Unix.putenv "FILTER_BRANCH_SQUELCH_WARNING" "1";
434434+435435+ let result = run ~proc_mgr ~cwd:temp_wt [
436436+ "filter-branch"; "-f";
437437+ "--index-filter"; script;
438438+ "--"; "HEAD"
439439+ ] in
440440+441441+ (* Restore environment *)
442442+ (match old_env with
443443+ | Some v -> Unix.putenv "FILTER_BRANCH_SQUELCH_WARNING" v
444444+ | None -> (try Unix.putenv "FILTER_BRANCH_SQUELCH_WARNING" "" with _ -> ()));
445445+446446+ (* Handle result: get the new SHA, cleanup worktree, then update branch *)
447447+ (match result with
448448+ | Ok _ ->
449449+ (* Get the new HEAD SHA from the worktree BEFORE removing it *)
450450+ let new_sha = run_exn ~proc_mgr ~cwd:temp_wt ["rev-parse"; "HEAD"] |> string_trim in
451451+ (* Cleanup temporary worktree first (must do this before updating branch) *)
452452+ ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
453453+ (* Now update the branch in the bare repo *)
454454+ run_exn ~proc_mgr ~cwd ["branch"; "-f"; branch; new_sha] |> ignore
455455+ | Error e ->
456456+ (* Cleanup and re-raise *)
457457+ ignore (run ~proc_mgr ~cwd ["worktree"; "remove"; "-f"; temp_wt_relpath]);
458458+ raise (err e))
+10
lib/git.mli
···344344 cwd:path ->
345345 unit
346346(** [clean_fd] removes untracked files and directories. *)
347347+348348+val filter_branch_to_subdirectory :
349349+ proc_mgr:proc_mgr ->
350350+ cwd:path ->
351351+ branch:string ->
352352+ subdirectory:string ->
353353+ unit
354354+(** [filter_branch_to_subdirectory ~proc_mgr ~cwd ~branch ~subdirectory]
355355+ rewrites the history of [branch] so all files are moved into [subdirectory].
356356+ This preserves full commit history with paths prefixed. *)
···2233 Implements vendoring of opam packages using the three-tier branch model:
44 - opam/upstream/<pkg> - pristine upstream code
55- - opam/vendor/<pkg> - orphan branch with vendor/opam/<pkg>/ prefix
66- - opam/patches/<pkg> - local modifications *)
55+ - opam/vendor/<pkg> - upstream history rewritten with vendor/opam/<pkg>/ prefix
66+ - opam/patches/<pkg> - local modifications
77+88+ The vendor branch preserves full git history from upstream, with all paths
99+ rewritten to be under vendor/opam/<pkg>/. This allows git blame/log to work
1010+ correctly on vendored files. *)
711812module Worktree = Unpac.Worktree
913module Git = Unpac.Git
···6266 end
6367 )
64686565-let add_package ~proc_mgr ~root ?cache (info : Backend.package_info) =
6969+let add_package ?(preserve_history=true) ~proc_mgr ~root ?cache (info : Backend.package_info) =
6670 let pkg = info.name in
6771 let git = Worktree.git_dir root in
6872···7175 if Worktree.branch_exists ~proc_mgr root (patches_kind pkg) then
7276 Backend.Already_exists pkg
7377 else begin
7474- (* Step 1: Create upstream branch and fetch *)
7575- let upstream_wt = Worktree.path root (upstream_kind pkg) in
7676-7778 (* Rewrite URL for known mirrors *)
7879 let url = Git_repo_lookup.rewrite_url info.url in
7980···100101 remote ^ "/" ^ branch
101102 in
102103103103- (* Create upstream branch *)
104104+ (* Step 1: Create upstream branch from fetched ref *)
104105 Git.branch_force ~proc_mgr ~cwd:git
105106 ~name:(upstream_branch pkg) ~point:ref_point;
106107107107- (* Create upstream worktree temporarily *)
108108- Worktree.ensure ~proc_mgr root (upstream_kind pkg);
108108+ let vendor_sha =
109109+ if preserve_history then begin
110110+ (* Step 2a: Create vendor branch from upstream (preserves history) *)
111111+ Git.branch_force ~proc_mgr ~cwd:git
112112+ ~name:(vendor_branch pkg) ~point:(upstream_branch pkg);
109113110110- (* Step 2: Create vendor branch (orphan) with prefix *)
111111- Worktree.ensure_orphan ~proc_mgr root (vendor_kind pkg);
112112- let vendor_wt = Worktree.path root (vendor_kind pkg) in
114114+ (* Rewrite vendor branch history to move all files into vendor/opam/<pkg>/ *)
115115+ Git.filter_branch_to_subdirectory ~proc_mgr ~cwd:git
116116+ ~branch:(vendor_branch pkg)
117117+ ~subdirectory:(vendor_path pkg);
113118114114- (* Copy files with vendor/opam/<pkg>/ prefix *)
115115- copy_with_prefix
116116- ~src_dir:upstream_wt
117117- ~dst_dir:vendor_wt
118118- ~prefix:(vendor_path pkg);
119119+ (* Get the vendor SHA after rewriting *)
120120+ match Git.rev_parse ~proc_mgr ~cwd:git (vendor_branch pkg) with
121121+ | Some sha -> sha
122122+ | None -> failwith "Vendor branch not found after filter-branch"
123123+ end else begin
124124+ (* Step 2b: Fast mode - create orphan vendor branch without history *)
125125+ Worktree.ensure ~proc_mgr root (upstream_kind pkg);
126126+ let upstream_wt = Worktree.path root (upstream_kind pkg) in
119127120120- (* Commit vendor branch *)
121121- Git.add_all ~proc_mgr ~cwd:vendor_wt;
122122- Git.commit ~proc_mgr ~cwd:vendor_wt
123123- ~message:(Printf.sprintf "Vendor %s" pkg);
128128+ Worktree.ensure_orphan ~proc_mgr root (vendor_kind pkg);
129129+ let vendor_wt = Worktree.path root (vendor_kind pkg) in
124130125125- let vendor_sha = Git.current_head ~proc_mgr ~cwd:vendor_wt in
131131+ (* Copy files with vendor/opam/<pkg>/ prefix *)
132132+ copy_with_prefix
133133+ ~src_dir:upstream_wt
134134+ ~dst_dir:vendor_wt
135135+ ~prefix:(vendor_path pkg);
136136+137137+ (* Commit vendor branch *)
138138+ Git.add_all ~proc_mgr ~cwd:vendor_wt;
139139+ Git.commit ~proc_mgr ~cwd:vendor_wt
140140+ ~message:(Printf.sprintf "Vendor %s" pkg);
141141+142142+ let sha = Git.current_head ~proc_mgr ~cwd:vendor_wt in
143143+144144+ (* Cleanup worktrees *)
145145+ Worktree.remove ~proc_mgr root (upstream_kind pkg);
146146+ Worktree.remove ~proc_mgr root (vendor_kind pkg);
147147+ sha
148148+ end
149149+ in
126150127151 (* Step 3: Create patches branch from vendor *)
128152 Git.branch_create ~proc_mgr ~cwd:git
129153 ~name:(patches_branch pkg)
130154 ~start_point:(vendor_branch pkg);
131131-132132- (* Cleanup worktrees *)
133133- Worktree.remove ~proc_mgr root (upstream_kind pkg);
134134- Worktree.remove ~proc_mgr root (vendor_kind pkg);
135155136156 Backend.Added { name = pkg; sha = vendor_sha }
137157 end
+5-1
lib/opam/opam.mli
···3131(** {1 Package Operations} *)
32323333val add_package :
3434+ ?preserve_history:bool ->
3435 proc_mgr:Unpac.Git.proc_mgr ->
3536 root:Unpac.Worktree.root ->
3637 ?cache:Unpac.Vendor_cache.t ->
···3940(** [add_package ~proc_mgr ~root ?cache info] vendors a single package.
40414142 1. Fetches upstream into opam/upstream/<pkg> (via cache if provided)
4242- 2. Creates opam/vendor/<pkg> orphan with vendor/opam/<pkg>/ prefix
4343+ 2. Creates opam/vendor/<pkg> with vendor/opam/<pkg>/ prefix
4344 3. Creates opam/patches/<pkg> from vendor
44454646+ @param preserve_history If true (default), rewrites git history to preserve
4747+ full commit history in the vendor branch. Set to false for faster
4848+ vendoring without history (useful for large repositories).
4549 @param cache Optional vendor cache for shared fetches across projects. *)
46504751val update_package :