Monorepo management for opam overlays
0
fork

Configure Feed

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

Add -p/--patch option to diff command

- Add -p flag to show full patch content for each commit
- Support `monopam diff <sha>` to show patch for specific commit from diff output
- Add Git.show_patch for fetching commit patches
- Detect commit SHA vs repo name (7+ hex characters)

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

+174 -20
+35 -8
bin/main.ml
··· 834 834 `Pre "monopam diff"; 835 835 `P "Show diff for a specific repository:"; 836 836 `Pre "monopam diff ocaml-eio"; 837 + `P "Show patches for all commits:"; 838 + `Pre "monopam diff -p"; 839 + `P "Show patch for a specific commit (from diff output):"; 840 + `Pre "monopam diff abc1234"; 837 841 `P "Force fresh fetches from all remotes:"; 838 842 `Pre "monopam diff --refresh"; 839 843 ] 840 844 in 841 845 let info = Cmd.info "diff" ~doc ~man in 842 - let repo_arg = 843 - let doc = "Repository name. If not specified, shows diffs for all repos needing attention." in 844 - Arg.(value & pos 0 (some string) None & info [] ~docv:"REPO" ~doc) 846 + let arg = 847 + let doc = "Repository name or commit SHA. If a 7+ character hex string, shows \ 848 + the patch for that commit. Otherwise filters to that repository. \ 849 + If not specified, shows diffs for all repos needing attention." in 850 + Arg.(value & pos 0 (some string) None & info [] ~docv:"REPO|SHA" ~doc) 845 851 in 846 852 let refresh_arg = 847 853 let doc = "Force fresh fetches from all remotes, ignoring the 5-minute cache." in 848 854 Arg.(value & flag & info [ "refresh"; "r" ] ~doc) 849 855 in 850 - let run repo refresh () = 856 + let patch_arg = 857 + let doc = "Show full patch content for each commit." in 858 + Arg.(value & flag & info [ "patch"; "p" ] ~doc) 859 + in 860 + let run arg refresh patch () = 851 861 Eio_main.run @@ fun env -> 852 862 with_config env @@ fun config -> 853 863 with_verse_config env @@ fun verse_config -> 854 864 let fs = Eio.Stdenv.fs env in 855 865 let proc = Eio.Stdenv.process_mgr env in 856 - let result = Monopam.diff ~proc ~fs ~config ~verse_config ?repo ~refresh () in 857 - Fmt.pr "%a" Monopam.pp_diff_result result; 858 - `Ok () 866 + (* Check if arg looks like a commit SHA *) 867 + match arg with 868 + | Some sha when Monopam.is_commit_sha sha -> 869 + (* Show patch for specific commit *) 870 + (match Monopam.diff_show_commit ~proc ~fs ~config ~verse_config ~sha ~refresh () with 871 + | Some info -> 872 + let short_hash = String.sub info.commit_hash 0 (min 7 (String.length info.commit_hash)) in 873 + Fmt.pr "%a %s (%s/%s)@.@.%s@." 874 + Fmt.(styled `Yellow string) short_hash 875 + info.commit_subject 876 + info.commit_repo info.commit_handle 877 + info.commit_patch; 878 + `Ok () 879 + | None -> 880 + Fmt.epr "Commit %s not found in any verse diff@." sha; 881 + `Error (false, "commit not found")) 882 + | repo -> 883 + let result = Monopam.diff ~proc ~fs ~config ~verse_config ?repo ~refresh ~patch () in 884 + Fmt.pr "%a" (Monopam.pp_diff_result ~show_patch:patch) result; 885 + `Ok () 859 886 in 860 - Cmd.v info Term.(ret (const run $ repo_arg $ refresh_arg $ logging_term)) 887 + Cmd.v info Term.(ret (const run $ arg $ refresh_arg $ patch_arg $ logging_term)) 861 888 862 889 (* Doctor command *) 863 890
+4
lib/git.ml
··· 322 322 | Ok output -> Ok (parse_log_entries output) 323 323 | Error e -> Error e 324 324 325 + let show_patch ~proc ~fs ~commit repo_path = 326 + let cwd = path_to_eio ~fs repo_path in 327 + run_git_ok ~proc ~cwd [ "show"; "--patch"; "--stat"; commit ] 328 + 325 329 (** Parse a subtree merge/squash commit message to extract the upstream commit range. 326 330 Messages look like: "Squashed 'prefix/' changes from abc123..def456" 327 331 or "Squashed 'prefix/' content from commit abc123"
+10
lib/git.mli
··· 395 395 @param max_count Maximum number of commits to return 396 396 @param repo Path to the git repository *) 397 397 398 + val show_patch : 399 + proc:_ Eio.Process.mgr -> 400 + fs:Eio.Fs.dir_ty Eio.Path.t -> 401 + commit:string -> 402 + Fpath.t -> 403 + (string, error) result 404 + (** [show_patch ~proc ~fs ~commit repo] returns the patch content for a commit. 405 + 406 + Runs [git show --patch --stat commit] to get the full diff with stats. *) 407 + 398 408 (** {1 Subtree Commit Analysis} *) 399 409 400 410 val parse_subtree_message : string -> string option
+84 -6
lib/monopam.ml
··· 1948 1948 handle : string; 1949 1949 relationship : Forks.relationship; 1950 1950 commits : Git.log_entry list; 1951 + patches : (string * string) list; (* hash -> patch content *) 1951 1952 } 1952 1953 1953 1954 type diff_result = { ··· 1955 1956 forks : Forks.t; 1956 1957 } 1957 1958 1958 - let pp_diff_entry ppf entry = 1959 + let pp_diff_entry ~show_patch ppf entry = 1959 1960 let n_commits = List.length entry.commits in 1960 1961 Fmt.pf ppf "@[<v 2>%a %s (%a, %d commit%s):@," 1961 1962 Fmt.(styled `Bold string) entry.repo_name ··· 1967 1968 Fmt.pf ppf " %a %s %a@," 1968 1969 Fmt.(styled `Yellow string) short_hash 1969 1970 c.subject 1970 - Fmt.(styled `Faint string) c.author) 1971 + Fmt.(styled `Faint string) c.author; 1972 + if show_patch then 1973 + match List.assoc_opt c.hash entry.patches with 1974 + | Some patch -> Fmt.pf ppf "@,%s@," patch 1975 + | None -> ()) 1971 1976 entry.commits; 1972 1977 Fmt.pf ppf "@]" 1973 1978 1974 - let pp_diff_result ppf result = 1979 + let pp_diff_result ~show_patch ppf result = 1975 1980 (* First show the summary *) 1976 1981 Fmt.pf ppf "%a@." (Forks.pp_summary' ~show_all:false) result.forks; 1977 1982 (* Then show diffs for each entry *) 1978 1983 if result.entries <> [] then begin 1979 1984 Fmt.pf ppf "@[<v>%a@]@." 1980 - Fmt.(list ~sep:(any "@,@,") pp_diff_entry) result.entries 1985 + Fmt.(list ~sep:(any "@,@,") (pp_diff_entry ~show_patch)) result.entries 1981 1986 end 1982 1987 1983 - let diff ~proc ~fs ~config ~verse_config ?repo ?(refresh=false) () = 1988 + (** Check if a string looks like a git commit hash (7+ hex chars) *) 1989 + let is_commit_sha s = 1990 + String.length s >= 7 && 1991 + String.for_all (function '0'..'9' | 'a'..'f' | 'A'..'F' -> true | _ -> false) s 1992 + 1993 + let diff ~proc ~fs ~config ~verse_config ?repo ?(refresh=false) ?(patch=false) () = 1984 1994 let checkouts_path = Config.Paths.checkouts config in 1985 1995 1986 1996 (* Compute fork analysis *) ··· 2019 2029 | Error _ -> None 2020 2030 | Ok commits when commits = [] -> None 2021 2031 | Ok commits -> 2022 - Some { repo_name = r.repo_name; handle; relationship = rel; commits } 2032 + (* Fetch patches if requested *) 2033 + let patches = 2034 + if patch then 2035 + List.filter_map (fun (c : Git.log_entry) -> 2036 + match Git.show_patch ~proc ~fs ~commit:c.hash checkout_path with 2037 + | Ok p -> Some (c.hash, p) 2038 + | Error _ -> None) 2039 + commits 2040 + else [] 2041 + in 2042 + Some { repo_name = r.repo_name; handle; relationship = rel; commits; patches } 2023 2043 end) 2024 2044 sources 2025 2045 in ··· 2030 2050 |> List.flatten 2031 2051 in 2032 2052 { entries; forks } 2053 + 2054 + (** Result of looking up a specific commit *) 2055 + type commit_info = { 2056 + commit_repo : string; 2057 + commit_handle : string; 2058 + commit_hash : string; 2059 + commit_subject : string; 2060 + commit_author : string; 2061 + commit_patch : string; 2062 + } 2063 + 2064 + (** Show patch for a specific commit SHA from diff output *) 2065 + let diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?(refresh=false) () = 2066 + let checkouts_path = Config.Paths.checkouts config in 2067 + 2068 + (* Compute fork analysis to find which repo has this commit *) 2069 + let forks = Forks.compute ~proc ~fs ~verse_config ~monopam_config:config ~refresh () in 2070 + 2071 + (* Search through repos for this commit *) 2072 + let result = List.find_map (fun (r : Forks.repo_analysis) -> 2073 + let checkout_path = Fpath.(checkouts_path / r.repo_name) in 2074 + if not (Git.is_repo ~proc ~fs checkout_path) then None 2075 + else 2076 + (* Check each verse source *) 2077 + List.find_map (fun (handle, _src, rel) -> 2078 + match rel with 2079 + | Forks.I_am_behind _ | Forks.Diverged _ -> 2080 + let remote_name = "verse/" ^ handle in 2081 + let my_ref = "origin/main" in 2082 + let their_ref = remote_name ^ "/main" in 2083 + (* Get commits they have that I don't *) 2084 + (match Git.log_range ~proc ~fs ~base:my_ref ~tip:their_ref ~max_count:50 checkout_path with 2085 + | Error _ -> None 2086 + | Ok commits -> 2087 + (* Check if our sha matches any commit *) 2088 + let matching = List.find_opt (fun (c : Git.log_entry) -> 2089 + String.starts_with ~prefix:sha c.hash || 2090 + String.starts_with ~prefix:(String.lowercase_ascii sha) (String.lowercase_ascii c.hash)) 2091 + commits 2092 + in 2093 + match matching with 2094 + | None -> None 2095 + | Some c -> 2096 + match Git.show_patch ~proc ~fs ~commit:c.hash checkout_path with 2097 + | Ok patch -> Some { 2098 + commit_repo = r.repo_name; 2099 + commit_handle = handle; 2100 + commit_hash = c.hash; 2101 + commit_subject = c.subject; 2102 + commit_author = c.author; 2103 + commit_patch = patch; 2104 + } 2105 + | Error _ -> None) 2106 + | _ -> None) 2107 + r.verse_sources) 2108 + forks.repos 2109 + in 2110 + result
+41 -6
lib/monopam.mli
··· 380 380 handle : string; 381 381 relationship : Forks.relationship; 382 382 commits : Git.log_entry list; 383 + patches : (string * string) list; (** hash -> patch content *) 383 384 } 384 385 385 386 (** Result of computing diffs for repos needing attention. *) ··· 388 389 forks : Forks.t; 389 390 } 390 391 391 - val pp_diff_entry : diff_entry Fmt.t 392 - (** [pp_diff_entry] formats a single diff entry. *) 392 + val pp_diff_entry : show_patch:bool -> diff_entry Fmt.t 393 + (** [pp_diff_entry ~show_patch] formats a single diff entry. 394 + If [show_patch] is true, includes the patch content for each commit. *) 393 395 394 - val pp_diff_result : diff_result Fmt.t 395 - (** [pp_diff_result] formats the full diff result. *) 396 + val pp_diff_result : show_patch:bool -> diff_result Fmt.t 397 + (** [pp_diff_result ~show_patch] formats the full diff result. *) 398 + 399 + val is_commit_sha : string -> bool 400 + (** [is_commit_sha s] returns true if [s] looks like a git commit hash 401 + (7+ hexadecimal characters). *) 396 402 397 403 val diff : 398 404 proc:_ Eio.Process.mgr -> ··· 401 407 verse_config:Verse_config.t -> 402 408 ?repo:string -> 403 409 ?refresh:bool -> 410 + ?patch:bool -> 404 411 unit -> 405 412 diff_result 406 - (** [diff ~proc ~fs ~config ~verse_config ?repo ?refresh ()] computes and displays diffs 413 + (** [diff ~proc ~fs ~config ~verse_config ?repo ?refresh ?patch ()] computes and displays diffs 407 414 for repositories that need attention from verse members. 408 415 409 416 For each repository where a verse member is ahead (I_am_behind or Diverged), ··· 417 424 @param config Monopam configuration 418 425 @param verse_config Verse configuration 419 426 @param repo Optional specific repository to show diff for 420 - @param refresh If true, force fresh fetches ignoring cache (default: false) *) 427 + @param refresh If true, force fresh fetches ignoring cache (default: false) 428 + @param patch If true, fetch and include patch content for each commit (default: false) *) 429 + 430 + (** Result of looking up a specific commit *) 431 + type commit_info = { 432 + commit_repo : string; 433 + commit_handle : string; 434 + commit_hash : string; 435 + commit_subject : string; 436 + commit_author : string; 437 + commit_patch : string; 438 + } 439 + 440 + val diff_show_commit : 441 + proc:_ Eio.Process.mgr -> 442 + fs:Eio.Fs.dir_ty Eio.Path.t -> 443 + config:Config.t -> 444 + verse_config:Verse_config.t -> 445 + sha:string -> 446 + ?refresh:bool -> 447 + unit -> 448 + commit_info option 449 + (** [diff_show_commit ~proc ~fs ~config ~verse_config ~sha ?refresh ()] finds and shows 450 + the patch for a specific commit SHA from the diff output. 451 + 452 + Searches through all repos with actionable verse sources to find a commit 453 + matching the given SHA prefix. Returns [Some commit_info] if found, [None] otherwise. 454 + 455 + @param sha Commit SHA prefix (7+ characters) to look up *)