Skip to main content

ralph_workflow/git_helpers/repo/
diff.rs

1// Git diff operations.
2//
3// Thin I/O primitives live in `diff/io.rs` (boundary module).
4// This module contains orchestration logic that calls those primitives.
5
6mod io;
7
8use std::path::Path;
9
10use crate::git_helpers::git_oid_to_git2_oid;
11use crate::workspace::Workspace;
12
13/// Get the diff of all changes (unstaged and staged).
14///
15/// Returns a formatted diff string suitable for LLM analysis.
16/// This is similar to `git diff HEAD`.
17///
18/// Handles the case of an empty repository (no commits yet) by
19/// diffing against an empty tree using a read-only approach.
20///
21/// # Errors
22///
23/// Returns error if the operation fails.
24pub fn git_diff() -> std::io::Result<String> {
25    let repo = io::discover_repo(Path::new("."))?;
26    diff_against_head(&repo)
27}
28
29/// Get the diff of all changes (unstaged and staged) by discovering from an explicit path.
30///
31/// This avoids coupling diff generation to the process current working directory.
32///
33/// # Errors
34///
35/// Returns error if the operation fails.
36pub fn git_diff_in_repo(repo_root: &Path) -> std::io::Result<String> {
37    let repo = io::discover_repo(repo_root)?;
38    diff_against_head(&repo)
39}
40
41/// Diff the current working directory against HEAD, handling the unborn-branch case.
42fn diff_against_head(repo: &git2::Repository) -> std::io::Result<String> {
43    match io::resolve_head_tree_oid(repo)? {
44        io::HeadTreeOid::Tree(tree_oid) => io::diff_from_tree_oid_impl(repo, tree_oid),
45        io::HeadTreeOid::UnbornBranch => io::diff_from_empty_tree_impl(repo),
46    }
47}
48
49/// Generate a diff from a specific starting commit.
50///
51/// Takes a starting commit OID and generates a diff between that commit
52/// and the current working tree. Returns a formatted diff string suitable
53/// for LLM analysis.
54///
55/// # Errors
56///
57/// Returns error if the operation fails.
58pub fn git_diff_from(start_oid: &str) -> std::io::Result<String> {
59    let repo = io::discover_repo(Path::new("."))?;
60    let oid = git2::Oid::from_str(start_oid).map_err(|_| {
61        std::io::Error::new(
62            std::io::ErrorKind::InvalidInput,
63            format!("Invalid commit OID: {start_oid}"),
64        )
65    })?;
66    io::diff_from_oid_impl(&repo, oid)
67}
68
69/// Get the git diff from the starting commit.
70///
71/// Uses the saved starting commit from `.agent/start_commit` to generate
72/// an incremental diff. Falls back to diffing from HEAD if no start commit
73/// file exists.
74///
75/// # Errors
76///
77/// Returns error if the operation fails.
78pub fn get_git_diff_from_start() -> std::io::Result<String> {
79    use crate::git_helpers::start_commit::{load_start_point, save_start_commit, StartPoint};
80
81    // Ensure a valid starting point exists. This is expected to persist across runs,
82    // but we also repair missing/corrupt files opportunistically for robustness.
83    save_start_commit()?;
84
85    let repo = io::discover_repo(Path::new("."))?;
86
87    match load_start_point()? {
88        StartPoint::Commit(oid) => {
89            let git2_oid = git_oid_to_git2_oid(&oid)?;
90            io::diff_from_oid_impl(&repo, git2_oid)
91        }
92        StartPoint::EmptyRepo => io::diff_from_empty_tree_impl(&repo),
93    }
94}
95
96/// Get the git diff from the starting commit (workspace-aware).
97///
98/// This uses `.agent/start_commit` as the baseline and generates a diff between that baseline
99/// and the current state on disk, including staged + unstaged changes and untracked files.
100///
101/// Unlike [`get_git_diff_from_start`], this does not rely on the process CWD.
102///
103/// # Errors
104///
105/// Returns error if the operation fails.
106pub fn get_git_diff_from_start_with_workspace(
107    workspace: &dyn Workspace,
108) -> std::io::Result<String> {
109    use crate::git_helpers::start_commit::{
110        load_start_point_with_workspace, save_start_commit_with_workspace, StartPoint,
111    };
112
113    // Fast path: if the workspace has no on-disk .git, refuse to emit a diff.
114    // This ensures MemoryWorkspace and other in-memory workspaces never accidentally
115    // leak into the process CWD's git repository.
116    if !workspace.exists(std::path::Path::new(".git")) {
117        return Err(std::io::Error::new(
118            std::io::ErrorKind::NotFound,
119            "Workspace has no on-disk git repository",
120        ));
121    }
122
123    let repo = io::discover_repo(Path::new("."))?;
124
125    // Ensure a valid start point exists. This is expected to persist across runs, but we also
126    // repair missing/corrupt files opportunistically for robustness.
127    save_start_commit_with_workspace(workspace, &repo)?;
128
129    match load_start_point_with_workspace(workspace, &repo)? {
130        StartPoint::Commit(oid) => {
131            let git2_oid = git_oid_to_git2_oid(&oid).map_err(|err| {
132                std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string())
133            })?;
134            io::diff_from_oid_impl(&repo, git2_oid)
135        }
136        StartPoint::EmptyRepo => io::diff_from_empty_tree_impl(&repo),
137    }
138}
139
140/// Get the diff content that should be shown to reviewers.
141///
142/// Baseline selection:
143/// - If `.agent/review_baseline.txt` is set, diff from that commit.
144/// - Otherwise, diff from `.agent/start_commit` (the initial pipeline baseline).
145///
146/// The diff is always generated against the current state on disk (staged + unstaged + untracked).
147///
148/// Returns `(diff, baseline_oid_for_prompts)` where `baseline_oid_for_prompts` is the commit hash
149/// to mention in fallback instructions (or empty for empty repo baseline).
150///
151/// # Errors
152///
153/// Returns error if the operation fails.
154pub fn get_git_diff_for_review_with_workspace(
155    workspace: &dyn Workspace,
156) -> std::io::Result<(String, String)> {
157    use crate::git_helpers::review_baseline::{
158        load_review_baseline_with_workspace, ReviewBaseline,
159    };
160    use crate::git_helpers::start_commit::{
161        load_start_point_with_workspace, save_start_commit_with_workspace, StartPoint,
162    };
163
164    // NOTE: We discover the repo from CWD here because the `ReviewBaseline` and `start_commit`
165    // files live in the injected Workspace, but the diff itself must be generated from the real
166    // on-disk git repository.
167    let repo = io::discover_repo(Path::new("."))?;
168
169    let baseline = load_review_baseline_with_workspace(workspace).unwrap_or(ReviewBaseline::NotSet);
170    match baseline {
171        ReviewBaseline::Commit(oid) => {
172            let diff = io::diff_from_oid_impl(&repo, oid)?;
173            Ok((diff, oid.to_string()))
174        }
175        ReviewBaseline::NotSet => {
176            // Ensure a valid start point exists.
177            save_start_commit_with_workspace(workspace, &repo)?;
178
179            match load_start_point_with_workspace(workspace, &repo)? {
180                StartPoint::Commit(oid) => {
181                    let git2_oid = git_oid_to_git2_oid(&oid)?;
182                    let diff = io::diff_from_oid_impl(&repo, git2_oid)?;
183                    Ok((diff, oid.to_string()))
184                }
185                StartPoint::EmptyRepo => Ok((io::diff_from_empty_tree_impl(&repo)?, String::new())),
186            }
187        }
188    }
189}