git_policy_cli/
replay_capture_history.rs1use super::replay_capture_order::{CaptureOrderDecision, CaptureView, LandedChange, capture_view, decide_capture_order, parse_commit_changes};
16use super::capture_records::{landed_record_directory, parse_landed_capture_record, read_captured_record};
18use super::capture_store::read_store_file;
19use super::replay_shadow::run_shadow_checked;
20use super::transaction_git::{GitContext, GitFailure};
21use std::collections::BTreeMap;
23use std::path::Path;
25
26#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum CaptureHistoryError {
38 Git(GitFailure),
40 Record(String),
42}
43
44pub fn list_landed_changes(
55 context: &GitContext,
56 shadow: &Path,
57 cwd: &Path,
58 base: Option<&str>,
59 current: &str,
60) -> Result<Vec<LandedChange>, CaptureHistoryError> {
61 let exclusion: String = base.map(|oid| { return format!("^{oid}"); }).unwrap_or_default();
62 let mut arguments: Vec<&str> = vec!["rev-list", "--first-parent", current];
64 if base.is_some() {
65 arguments.push(exclusion.as_str());
66 }
67 arguments.push("--");
68 let listed = run_shadow_checked(context, shadow, cwd, &arguments, None, None, &[]).map_err(CaptureHistoryError::Git)?;
69 if listed.stdout.is_empty() {
70 return Ok(Vec::new());
71 }
72 let changes = run_shadow_checked(
73 context,
74 shadow,
75 cwd,
76 &[
77 "diff-tree",
78 "--stdin",
79 "--root",
80 "-r",
81 "-z",
82 "--raw",
83 "--no-renames",
84 "--no-abbrev",
85 "-m",
86 "--first-parent",
87 ],
88 Some(listed.stdout.as_slice()),
89 None,
90 &[],
91 )
92 .map_err(CaptureHistoryError::Git)?;
93 let text: String = changes.stdout.iter().map(|byte| { return char::from(*byte); }).collect();
94 return parse_commit_changes(text.as_str()).map_err(|error| { return CaptureHistoryError::Record(error.0); });
95}
96
97fn read_landed_captures(git_dir: &Path, commits: &[LandedChange]) -> Result<BTreeMap<String, CaptureView>, CaptureHistoryError> {
108 let directory = landed_record_directory(git_dir);
109 let mut views: BTreeMap<String, CaptureView> = BTreeMap::new();
111 for change in commits {
112 let name: String = format!("{}.json", change.commit);
113 let text: Option<String> = read_store_file(directory.join(&name).as_path())
114 .map_err(|error| { return CaptureHistoryError::Record(error.0); })?;
115 let Some(found) = text else {
116 continue;
117 };
118 let record = parse_landed_capture_record(found.as_str(), &format!("landed/{name}"))
119 .map_err(|error| { return CaptureHistoryError::Record(error.0); })?;
120 views.insert(
121 change.commit.clone(),
122 capture_view(record.worktree_id.as_str(), record.sequence, &record.worktree_paths),
123 );
124 }
125 return Ok(views);
126}
127
128pub fn decide_shared_paths(
140 context: &GitContext,
141 shadow: &Path,
142 cwd: &Path,
143 git_dir: &Path,
144 transaction_directory: &Path,
145 base: Option<&str>,
146 current: &str,
147 paths: &[String],
148) -> Result<BTreeMap<String, CaptureOrderDecision>, CaptureHistoryError> {
149 let captured = match read_captured_record(transaction_directory).map_err(|error| { return CaptureHistoryError::Record(error.0); })? {
150 Some(found) => found,
151 None => return Ok(BTreeMap::new()),
152 };
153 let own: CaptureView = capture_view(captured.worktree_id.as_str(), captured.sequence, &captured.worktree_paths);
154 let candidates: Vec<&String> = paths.iter().filter(|path| { return own.paths.contains(path.as_str()); }).collect();
155 if candidates.is_empty() {
156 return Ok(BTreeMap::new());
157 }
158 let history: Vec<LandedChange> = list_landed_changes(context, shadow, cwd, base, current)?;
159 let landed: BTreeMap<String, CaptureView> = read_landed_captures(git_dir, &history)?;
160 return Ok(candidates
161 .into_iter()
162 .map(|path| {
163 let decision: CaptureOrderDecision = decide_capture_order(path.as_str(), &own, &history, &landed);
164 return (path.to_string(), decision);
165 })
166 .collect());
167}
168
169#[cfg(test)]
170#[path = "replay_capture_history_tests.rs"]
171mod tests;