ralph_workflow/git_helpers/review_baseline/
baseline_persistence.rs1pub const REVIEW_BASELINE_FILE: &str = ".agent/review_baseline.txt";
8
9pub const BASELINE_NOT_SET: &str = "__BASELINE_NOT_SET__";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ReviewBaseline {
17 Commit(git2::Oid),
19 NotSet,
21}
22
23pub fn load_review_baseline() -> io::Result<ReviewBaseline> {
25 let repo = git2::Repository::discover(".").map_err(|e| to_io_error(&e))?;
26 let repo_root = repo
27 .workdir()
28 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No workdir for repository"))?;
29 let workspace = WorkspaceFs::new(repo_root.to_path_buf());
30 load_review_baseline_with_workspace(&workspace)
31}
32
33pub fn load_review_baseline_with_workspace(
35 workspace: &dyn Workspace,
36) -> io::Result<ReviewBaseline> {
37 let path = Path::new(REVIEW_BASELINE_FILE);
38 if !workspace.exists(path) {
39 return Ok(ReviewBaseline::NotSet);
40 }
41
42 let content = workspace.read(path)?;
43 let raw = content.trim();
44
45 if raw.is_empty() || raw == BASELINE_NOT_SET {
46 return Ok(ReviewBaseline::NotSet);
47 }
48
49 let oid = git2::Oid::from_str(raw).map_err(|_| {
50 io::Error::new(
51 io::ErrorKind::InvalidData,
52 format!(
53 "Invalid baseline OID in {}: '{}'",
54 REVIEW_BASELINE_FILE, raw
55 ),
56 )
57 })?;
58
59 Ok(ReviewBaseline::Commit(oid))
60}
61
62pub fn update_review_baseline() -> io::Result<()> {
64 let repo = git2::Repository::discover(".").map_err(|e| to_io_error(&e))?;
65 let repo_root = repo
66 .workdir()
67 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No workdir for repository"))?;
68 let workspace = WorkspaceFs::new(repo_root.to_path_buf());
69 update_review_baseline_with_workspace(&workspace)
70}
71
72pub fn update_review_baseline_with_workspace(workspace: &dyn Workspace) -> io::Result<()> {
74 let path = Path::new(REVIEW_BASELINE_FILE);
75 match get_current_head_oid() {
76 Ok(oid) => workspace.write(path, oid.trim()),
77 Err(e) if e.kind() == io::ErrorKind::NotFound => workspace.write(path, BASELINE_NOT_SET),
78 Err(e) => Err(e),
79 }
80}
81
82pub fn get_review_baseline_info() -> io::Result<(Option<String>, usize, bool)> {
86 let repo = git2::Repository::discover(".").map_err(|e| to_io_error(&e))?;
87 match load_review_baseline()? {
88 ReviewBaseline::Commit(oid) => {
89 let oid_str = oid.to_string();
90 let commits_since = count_commits_since(&repo, &oid_str)?;
91 let is_stale = commits_since > 10;
92 Ok((Some(oid_str), commits_since, is_stale))
93 }
94 ReviewBaseline::NotSet => Ok((None, 0, false)),
95 }
96}
97
98fn count_commits_since(repo: &git2::Repository, baseline_oid: &str) -> io::Result<usize> {
99 let baseline = git2::Oid::from_str(baseline_oid).map_err(|_| {
100 io::Error::new(
101 io::ErrorKind::InvalidInput,
102 format!("Invalid baseline OID: {}", baseline_oid),
103 )
104 })?;
105
106 let head_oid = match repo.head() {
107 Ok(head) => head.peel_to_commit().map_err(|e| to_io_error(&e))?.id(),
108 Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch => return Ok(0),
109 Err(e) => return Err(to_io_error(&e)),
110 };
111
112 if let Ok((ahead, _behind)) = repo.graph_ahead_behind(head_oid, baseline) {
114 return Ok(ahead);
115 }
116
117 let mut walk = repo.revwalk().map_err(|e| to_io_error(&e))?;
119 walk.push(head_oid).map_err(|e| to_io_error(&e))?;
120 walk.hide(baseline).map_err(|e| to_io_error(&e))?;
121 Ok(walk.count())
122}
123
124fn to_io_error(err: &git2::Error) -> io::Error {
125 io::Error::other(err.to_string())
126}