1use anyhow::{Context, Result};
2use std::process::Command;
3
4#[derive(Debug, Clone)]
5pub struct DiffHunk {
6 pub file: String,
9 pub old_file: String,
11 pub new_file: String,
13 pub file_header: String,
15 pub header: String,
17 pub lines: Vec<String>,
19 pub unsupported_metadata: Option<String>,
21}
22
23const DIFF_FORMAT_ARGS: &[&str] = &[
24 "--no-color",
25 "--no-ext-diff",
26 "--src-prefix=a/",
27 "--dst-prefix=b/",
28];
29
30pub fn run_git_diff(staged: bool, file: Option<&str>) -> Result<String> {
31 let mut cmd = Command::new("git");
32 cmd.arg("diff");
33 cmd.args(DIFF_FORMAT_ARGS);
34 if staged {
35 cmd.arg("--cached");
36 }
37 if let Some(f) = file {
38 cmd.arg("--").arg(f);
39 }
40 run_git_cmd(&mut cmd)
41}
42
43pub fn run_git_diff_commit(commit: &str, file: Option<&str>) -> Result<String> {
44 let mut cmd = Command::new("git");
45 cmd.args(["show", "--pretty="]);
46 cmd.args(DIFF_FORMAT_ARGS);
47 cmd.arg(commit);
48 if let Some(f) = file {
49 cmd.arg("--").arg(f);
50 }
51 run_git_cmd(&mut cmd)
52}
53
54pub fn run_git_cmd(cmd: &mut Command) -> Result<String> {
55 let output = cmd.output().context("failed to run git command")?;
56 if !output.status.success() {
57 anyhow::bail!(
58 "git command failed: {}",
59 String::from_utf8_lossy(&output.stderr)
60 );
61 }
62 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
63}
64
65fn strip_diff_prefix(line: &str) -> &str {
67 line.strip_prefix("--- a/")
68 .or_else(|| line.strip_prefix("+++ b/"))
69 .or_else(|| line.strip_prefix("--- /"))
70 .or_else(|| line.strip_prefix("+++ /"))
71 .or_else(|| line.strip_prefix("+++ a/"))
72 .or_else(|| line.strip_prefix("--- "))
73 .or_else(|| line.strip_prefix("+++ "))
74 .unwrap_or(line)
75}
76
77const UNSUPPORTED_PREAMBLE_PREFIXES: &[&str] = &[
81 "rename from ",
82 "rename to ",
83 "copy from ",
84 "copy to ",
85 "old mode ",
86 "new mode ",
87 "similarity index ",
88 "dissimilarity index ",
89];
90
91pub fn parse_diff(input: &str) -> Vec<DiffHunk> {
92 let mut hunks = Vec::new();
93 let mut current_old_file = String::new();
94 let mut current_new_file = String::new();
95 let mut current_file_header = String::new();
96 let mut current_header: Option<String> = None;
97 let mut current_lines: Vec<String> = Vec::new();
98 let mut current_unsupported: Option<String> = None;
99
100 for line in input.lines() {
101 if line.starts_with("diff --git") {
102 if let Some(header) = current_header.take() {
104 hunks.push(DiffHunk {
105 file: display_file(¤t_old_file, ¤t_new_file),
106 old_file: current_old_file.clone(),
107 new_file: current_new_file.clone(),
108 file_header: current_file_header.clone(),
109 header,
110 lines: std::mem::take(&mut current_lines),
111 unsupported_metadata: current_unsupported.clone(),
112 });
113 }
114 current_file_header.clear();
115 current_old_file.clear();
116 current_new_file.clear();
117 current_unsupported = None;
118 } else if current_unsupported.is_none() {
119 if let Some(prefix) = UNSUPPORTED_PREAMBLE_PREFIXES
121 .iter()
122 .find(|p| line.starts_with(*p))
123 {
124 current_unsupported = Some(prefix.trim().to_string());
125 }
126 }
127
128 if line.starts_with("--- ") {
129 current_file_header = line.to_string();
130 current_old_file = strip_diff_prefix(line).to_string();
131 } else if line.starts_with("+++ ") {
132 current_file_header.push('\n');
133 current_file_header.push_str(line);
134 current_new_file = strip_diff_prefix(line).to_string();
135 } else if line.starts_with("@@ ") {
136 if let Some(header) = current_header.take() {
138 hunks.push(DiffHunk {
139 file: display_file(¤t_old_file, ¤t_new_file),
140 old_file: current_old_file.clone(),
141 new_file: current_new_file.clone(),
142 file_header: current_file_header.clone(),
143 header,
144 lines: std::mem::take(&mut current_lines),
145 unsupported_metadata: current_unsupported.clone(),
146 });
147 }
148 current_header = Some(line.to_string());
149 } else if current_header.is_some() {
150 current_lines.push(line.to_string());
151 }
152 }
153
154 if let Some(header) = current_header.take() {
156 hunks.push(DiffHunk {
157 file: display_file(¤t_old_file, ¤t_new_file),
158 old_file: current_old_file,
159 new_file: current_new_file,
160 file_header: current_file_header,
161 header,
162 lines: current_lines,
163 unsupported_metadata: current_unsupported,
164 });
165 }
166
167 hunks
168}
169
170fn display_file(old: &str, new: &str) -> String {
173 if new == "dev/null" || new.is_empty() {
174 old.to_string()
175 } else {
176 new.to_string()
177 }
178}
179
180pub fn check_supported(hunk: &DiffHunk, id: &str) -> Result<()> {
182 if let Some(ref metadata) = hunk.unsupported_metadata {
183 anyhow::bail!(
184 "hunk {} involves '{}' which is not supported for hunk-level operations",
185 id,
186 metadata
187 );
188 }
189 Ok(())
190}