(** Git operations for monopam. This module provides git operations needed for managing individual checkouts and git subtree operations in the monorepo. All operations use Eio for process spawning. *) (** {1 Types} *) type cmd_result = { exit_code : int; stdout : string; stderr : string } (** Result of a git command execution. *) (** Errors from git operations. *) type error = | Command_failed of string * cmd_result (** Git command failed: (command, result) *) | Not_a_repo of Fpath.t (** Path is not a git repository *) | Dirty_worktree of Fpath.t (** Repository has uncommitted changes *) | Remote_not_found of string (** Named remote does not exist *) | Branch_not_found of string (** Named branch does not exist *) | Subtree_prefix_exists of string (** Subtree prefix already exists in repo *) | Subtree_prefix_missing of string (** Subtree prefix does not exist *) | Io_error of string (** Filesystem or process error *) val pp_error : error Fmt.t (** [pp_error] is a formatter for errors. *) (** {1 Repository Queries} *) val is_repo : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> bool (** [is_repo ~proc ~fs path] returns true if path is a git repository. *) val is_dirty : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> bool (** [is_dirty ~proc ~fs path] returns true if the repository has uncommitted changes (staged or unstaged). *) val current_branch : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> string option (** [current_branch ~proc ~fs path] returns the current branch name, or [None] if in detached HEAD state. *) val head_commit : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> (string, error) result (** [head_commit ~proc ~fs path] returns the current HEAD commit hash. *) val rev_parse : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> rev:string -> Fpath.t -> (string, error) result (** [rev_parse ~proc ~fs ~rev path] resolves a revision to a commit hash. @param rev The revision to resolve (e.g., "HEAD", "main", "abc123") *) (** {1 Basic Operations} *) val clone : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> url:Uri.t -> branch:string -> Fpath.t -> (unit, error) result (** [clone ~proc ~fs ~url ~branch target] clones a repository. @param proc Eio process manager @param fs Eio filesystem @param url Git remote URL @param branch Branch to checkout @param target Destination directory *) val fetch : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?remote:string -> Fpath.t -> (unit, error) result (** [fetch ~proc ~fs ?remote path] fetches from the remote. @param remote Remote name (default: "origin") *) val fetch_all : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> (unit, error) result (** [fetch_all ~proc ~fs path] fetches from all remotes. Runs [git fetch --all] to update all remote tracking branches. *) val merge_ff : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?remote:string -> ?branch:string -> Fpath.t -> (unit, error) result (** [merge_ff ~proc ~fs ?remote ?branch path] performs a fast-forward only merge from the remote tracking branch. @param remote Remote name (default: "origin") @param branch Branch to merge from (default: current branch) *) val pull : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?remote:string -> ?branch:string -> Fpath.t -> (unit, error) result (** [pull ~proc ~fs ?remote ?branch path] pulls from the remote. @param remote Remote name (default: "origin") @param branch Branch to pull (default: current branch) *) val fetch_and_reset : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?remote:string -> branch:string -> Fpath.t -> (unit, error) result (** [fetch_and_reset ~proc ~fs ?remote ~branch path] fetches from the remote and resets the local branch to match the remote. This is useful for repositories that should not have local changes, as it discards any local modifications and sets the working tree to exactly match the remote branch. @param remote Remote name (default: "origin") @param branch Branch to reset to *) val checkout : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> branch:string -> Fpath.t -> (unit, error) result (** [checkout ~proc ~fs ~branch path] checks out the specified branch. *) (** {1 Comparison} *) type ahead_behind = { ahead : int; (** Commits ahead of upstream *) behind : int; (** Commits behind upstream *) } (** Describes how a local branch relates to its upstream. *) val ahead_behind : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?remote:string -> ?branch:string -> Fpath.t -> (ahead_behind, error) result (** [ahead_behind ~proc ~fs ?remote ?branch path] computes how many commits the local branch is ahead/behind the remote. @param remote Remote name (default: "origin") @param branch Branch to compare (default: current branch) *) (** {1 Subtree Operations} *) (** Operations for git subtree management in the monorepo. *) module Subtree : sig val add : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> url:Uri.t -> branch:string -> unit -> (unit, error) result (** [add ~proc ~fs ~repo ~prefix ~url ~branch ()] adds a new subtree to the repository. @param repo Path to the monorepo @param prefix Subdirectory for the subtree @param url Git remote URL for the subtree source @param branch Branch to add *) val pull : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> url:Uri.t -> branch:string -> unit -> (unit, error) result (** [pull ~proc ~fs ~repo ~prefix ~url ~branch ()] pulls updates from the remote into the subtree. @param repo Path to the monorepo @param prefix Subdirectory of the subtree @param url Git remote URL @param branch Branch to pull *) val push : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> url:Uri.t -> branch:string -> unit -> (unit, error) result (** [push ~proc ~fs ~repo ~prefix ~url ~branch ()] pushes subtree changes to the remote. This extracts commits that affected the subtree and pushes them to the specified remote/branch. @param repo Path to the monorepo @param prefix Subdirectory of the subtree @param url Git remote URL @param branch Branch to push to *) val split : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> unit -> (string, error) result (** [split ~proc ~fs ~repo ~prefix ()] extracts commits for a subtree into a standalone branch. Returns the commit hash of the split branch head. *) val exists : fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> bool (** [exists ~fs ~repo ~prefix] returns true if the subtree prefix directory exists in the repository. *) end (** {1 Initialization} *) val init : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> (unit, error) result (** [init ~proc ~fs path] initializes a new git repository. *) val commit_allow_empty : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> message:string -> Fpath.t -> (unit, error) result (** [commit_allow_empty ~proc ~fs ~message path] creates a commit, even if there are no changes. Useful for initializing a repository. *) val push_remote : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?remote:string -> ?branch:string -> Fpath.t -> (unit, error) result (** [push_remote ~proc ~fs ?remote ?branch path] pushes the current branch to the remote. @param remote Remote name (default: "origin") @param branch Branch to push (default: current branch) *) val push_ref : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> target:string -> ref_spec:string -> unit -> (unit, error) result (** [push_ref ~proc ~fs ~repo ~target ~ref_spec ()] pushes a specific ref to a target repository or path. @param repo Path to the git repository to push from @param target Target repository path or remote name @param ref_spec The refspec to push (e.g., "abc123:refs/heads/main") *) val set_push_url : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?remote:string -> url:string -> Fpath.t -> (unit, error) result (** [set_push_url ~proc ~fs ?remote ~url path] sets the push URL for a remote. This allows the fetch and push URLs to be different. @param remote Remote name (default: "origin") @param url The URL to use for pushing *) val get_push_url : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?remote:string -> Fpath.t -> string option (** [get_push_url ~proc ~fs ?remote path] returns the push URL for a remote, or [None] if not set or the remote doesn't exist. @param remote Remote name (default: "origin") *) (** {1 Remote Management} *) val list_remotes : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> string list (** [list_remotes ~proc ~fs path] returns a list of all remote names. *) val get_remote_url : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> remote:string -> Fpath.t -> string option (** [get_remote_url ~proc ~fs ~remote path] returns the URL for a remote. *) val add_remote : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> name:string -> url:string -> Fpath.t -> (unit, error) result (** [add_remote ~proc ~fs ~name ~url path] adds a new remote. *) val remove_remote : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> name:string -> Fpath.t -> (unit, error) result (** [remove_remote ~proc ~fs ~name path] removes a remote. *) val set_remote_url : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> name:string -> url:string -> Fpath.t -> (unit, error) result (** [set_remote_url ~proc ~fs ~name ~url path] updates the URL for an existing remote. *) val ensure_remote : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> name:string -> url:string -> Fpath.t -> (unit, error) result (** [ensure_remote ~proc ~fs ~name ~url path] ensures a remote exists with the given URL. If the remote exists with a different URL, it is updated. If the remote doesn't exist, it is added. *) (** {1 Commit History} *) type log_entry = { hash : string; (** Full commit hash *) author : string; (** Author name *) date : string; (** ISO 8601 date *) subject : string; (** Commit subject line *) body : string; (** Commit body *) } (** A single commit log entry. *) val log : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ?since:string -> ?until:string -> ?path:string -> Fpath.t -> (log_entry list, error) result (** [log ~proc ~fs ?since ?until ?path repo] retrieves commit history. @param since Include commits more recent than this date (e.g., "1 week ago") @param until Include commits older than this date @param path Filter to commits affecting this path (relative to repo) @param repo Path to the git repository *) val log_range : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> base:string -> tip:string -> ?max_count:int -> Fpath.t -> (log_entry list, error) result (** [log_range ~proc ~fs ~base ~tip ?max_count repo] retrieves commits between refs. Gets commits reachable from [tip] but not from [base] (i.e., [base..tip]). @param base Base ref (commits reachable from here are excluded) @param tip Tip ref (commits reachable from here are included) @param max_count Maximum number of commits to return @param repo Path to the git repository *) val show_patch : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> commit:string -> Fpath.t -> (string, error) result (** [show_patch ~proc ~fs ~commit repo] returns the patch content for a commit. Runs [git show --patch --stat commit] to get the full diff with stats. *) (** {1 Subtree Commit Analysis} *) val parse_subtree_message : string -> string option (** [parse_subtree_message subject] extracts the upstream commit SHA from a subtree merge/squash commit message. Handles messages like: - "Squashed 'prefix/' changes from abc123..def456" -> Some "def456" - "Squashed 'prefix/' content from commit abc123" -> Some "abc123" - "Add 'prefix/' from commit abc123" -> Some "abc123" Returns [None] if the message doesn't match any known pattern. *) val subtree_last_upstream_commit : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> unit -> string option (** [subtree_last_upstream_commit ~proc ~fs ~repo ~prefix ()] finds the upstream commit SHA that the subtree was last synced from. Searches git log for the most recent subtree merge/squash commit for the given prefix and extracts the upstream commit reference. @param repo Path to the monorepo @param prefix Subtree directory name (e.g., "ocaml-bytesrw") *) val is_ancestor : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> commit1:string -> commit2:string -> unit -> bool (** [is_ancestor ~proc ~fs ~repo ~commit1 ~commit2 ()] returns true if commit1 is an ancestor of commit2. Uses [git merge-base --is-ancestor]. *) val merge_base : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> commit1:string -> commit2:string -> unit -> (string, error) result (** [merge_base ~proc ~fs ~repo ~commit1 ~commit2 ()] finds the common ancestor of two commits. *) val count_commits_between : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> base:string -> head:string -> unit -> int (** [count_commits_between ~proc ~fs ~repo ~base ~head ()] counts the number of commits between base and head (exclusive of base, inclusive of head). *) (** {1 Worktree Operations} *) (** Operations for git worktree management. *) module Worktree : sig (** A git worktree entry. *) type entry = { path : Fpath.t; (** Absolute path to the worktree *) head : string; (** HEAD commit hash *) branch : string option; (** Branch name if not detached *) } val add : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> path:Fpath.t -> branch:string -> unit -> (unit, error) result (** [add ~proc ~fs ~repo ~path ~branch ()] creates a new worktree at [path] with a new branch [branch]. @param repo Path to the main repository @param path Path where the worktree will be created @param branch Name of the new branch to create *) val remove : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> path:Fpath.t -> force:bool -> unit -> (unit, error) result (** [remove ~proc ~fs ~repo ~path ~force ()] removes a worktree. @param repo Path to the main repository @param path Path to the worktree to remove @param force If true, remove even if there are uncommitted changes *) val list : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> entry list (** [list ~proc ~fs repo] returns all worktrees for the repository. *) val exists : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> path:Fpath.t -> bool (** [exists ~proc ~fs ~repo ~path] returns true if a worktree exists at [path]. *) end (** {1 Cherry-pick Operations} *) val cherry_pick : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> commit:string -> Fpath.t -> (unit, error) result (** [cherry_pick ~proc ~fs ~commit path] applies a single commit to the current branch. @param commit The commit hash to cherry-pick @param path Path to the repository *) val merge : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> ref_name:string -> ?ff_only:bool -> Fpath.t -> (unit, error) result (** [merge ~proc ~fs ~ref_name ?ff_only path] merges a ref into the current branch. @param ref_name The ref to merge (e.g., "verse/handle/main") @param ff_only If true, only allow fast-forward merges (default: false) @param path Path to the repository *) (** {1 Diff Operations} *) val diff_trees : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> source:Fpath.t -> target:Fpath.t -> (string, error) result (** [diff_trees ~proc ~fs ~source ~target] generates a diff between two directory trees using [git diff --no-index]. Returns [Ok ""] if the trees are identical, [Ok diff] with the diff content if they differ, or [Error] if the diff command fails. @param source The source directory (typically the monorepo subtree) @param target The target directory (typically the checkout) *) val apply_diff : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> cwd:Fpath.t -> diff:string -> (unit, error) result (** [apply_diff ~proc ~fs ~cwd ~diff] applies a diff to the directory at [cwd]. Uses [git apply] to apply the diff. Returns [Ok ()] if the diff was applied successfully or was empty, [Error] if the apply failed. *) val add_all : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> Fpath.t -> (unit, error) result (** [add_all ~proc ~fs path] stages all changes (git add -A) in the repository at [path]. *) val commit : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> message:string -> Fpath.t -> (unit, error) result (** [commit ~proc ~fs ~message path] creates a commit with the given message in the repository at [path]. *) val rm : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> recursive:bool -> Fpath.t -> string -> (unit, error) result (** [rm ~proc ~fs ~recursive path target] removes [target] from the git index in the repository at [path]. If [recursive] is true, removes directories recursively (git rm -r). *) val config : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> key:string -> value:string -> Fpath.t -> (unit, error) result (** [config ~proc ~fs ~key ~value path] sets a git config value in the repository at [path]. *) val has_subtree_history : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> repo:Fpath.t -> prefix:string -> unit -> bool (** [has_subtree_history ~proc ~fs ~repo ~prefix ()] returns true if the prefix has subtree commit history (i.e., was added via git subtree add). Returns false for fresh local packages that were never part of a subtree. *) val branch_rename : proc:_ Eio.Process.mgr -> fs:Eio.Fs.dir_ty Eio.Path.t -> new_name:string -> Fpath.t -> (unit, error) result (** [branch_rename ~proc ~fs ~new_name path] renames the current branch to [new_name] in the repository at [path]. Uses [git branch -M]. *)