1use crate::debian::{add_changelog_entry, control_files_in_root, guess_update_changelog};
4use crate::CommitPending;
5use breezyshim::error::Error as BrzError;
6use breezyshim::tree::WorkingTree;
7use breezyshim::RevisionId;
8use debian_changelog::get_maintainer_from_env;
9use debian_changelog::ChangeLog;
10use std::collections::HashMap;
11use url::Url;
12
13#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
14pub struct CommandResult {
16 pub source_name: String,
18
19 pub value: Option<u32>,
21
22 pub context: Option<serde_json::Value>,
24
25 pub description: String,
27
28 pub serialized_context: Option<String>,
30
31 pub tags: Vec<(String, Option<RevisionId>)>,
33
34 pub target_branch_url: Option<Url>,
36
37 pub old_revision: RevisionId,
39
40 pub new_revision: RevisionId,
42}
43
44impl crate::CodemodResult for CommandResult {
45 fn context(&self) -> serde_json::Value {
46 self.context.clone().unwrap_or_default()
47 }
48
49 fn value(&self) -> Option<u32> {
50 self.value
51 }
52
53 fn target_branch_url(&self) -> Option<Url> {
54 self.target_branch_url.clone()
55 }
56
57 fn description(&self) -> Option<String> {
58 Some(self.description.clone())
59 }
60
61 fn tags(&self) -> Vec<(String, Option<RevisionId>)> {
62 self.tags.clone()
63 }
64}
65
66impl From<&CommandResult> for DetailedSuccess {
67 fn from(r: &CommandResult) -> Self {
68 DetailedSuccess {
69 value: r.value,
70 context: r.context.clone(),
71 description: Some(r.description.clone()),
72 serialized_context: r.serialized_context.clone(),
73 tags: Some(
74 r.tags
75 .iter()
76 .map(|(k, v)| (k.clone(), v.as_ref().map(|v| v.to_string())))
77 .collect(),
78 ),
79 target_branch_url: r.target_branch_url.clone(),
80 }
81 }
82}
83
84#[derive(Debug, serde::Deserialize, serde::Serialize, Default)]
85struct DetailedSuccess {
86 value: Option<u32>,
87 context: Option<serde_json::Value>,
88 description: Option<String>,
89 serialized_context: Option<String>,
90 tags: Option<Vec<(String, Option<String>)>>,
91 #[serde(rename = "target-branch-url")]
92 target_branch_url: Option<Url>,
93}
94
95#[derive(Debug)]
96pub enum Error {
98 ScriptMadeNoChanges,
100
101 ScriptNotFound,
103
104 MissingChangelog(std::path::PathBuf),
106
107 ChangelogParse(debian_changelog::ParseError),
109
110 ExitCode(i32),
112
113 Detailed(DetailedFailure),
115
116 Io(std::io::Error),
118
119 Json(serde_json::Error),
121
122 Utf8(std::string::FromUtf8Error),
124
125 Other(String),
127}
128
129impl From<debian_changelog::Error> for Error {
130 fn from(e: debian_changelog::Error) -> Self {
131 match e {
132 debian_changelog::Error::Io(e) => Error::Io(e),
133 debian_changelog::Error::Parse(e) => Error::ChangelogParse(e),
134 }
135 }
136}
137
138impl std::fmt::Display for Error {
139 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
140 match self {
141 Error::ScriptMadeNoChanges => write!(f, "Script made no changes"),
142 Error::ScriptNotFound => write!(f, "Script not found"),
143 Error::ExitCode(code) => write!(f, "Script exited with code {}", code),
144 Error::Detailed(d) => write!(f, "Script failed: {:?}", d),
145 Error::Io(e) => write!(f, "Command failed: {}", e),
146 Error::Json(e) => write!(f, "JSON error: {}", e),
147 Error::Utf8(e) => write!(f, "UTF-8 error: {}", e),
148 Error::Other(s) => write!(f, "{}", s),
149 Error::ChangelogParse(e) => write!(f, "Changelog parse error: {}", e),
150 Error::MissingChangelog(p) => write!(f, "Missing changelog at {}", p.display()),
151 }
152 }
153}
154
155impl From<serde_json::Error> for Error {
156 fn from(e: serde_json::Error) -> Self {
157 Error::Json(e)
158 }
159}
160
161impl From<std::io::Error> for Error {
162 fn from(e: std::io::Error) -> Self {
163 Error::Io(e)
164 }
165}
166
167impl From<std::string::FromUtf8Error> for Error {
168 fn from(e: std::string::FromUtf8Error) -> Self {
169 Error::Utf8(e)
170 }
171}
172
173impl std::error::Error for Error {}
174
175#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
176pub struct DetailedFailure {
178 pub result_code: String,
180 pub description: Option<String>,
182 pub stage: Option<Vec<String>>,
184 pub details: Option<serde_json::Value>,
186}
187
188pub fn script_runner(
199 local_tree: &dyn WorkingTree,
200 script: &[&str],
201 subpath: &std::path::Path,
202 commit_pending: CommitPending,
203 resume_metadata: Option<&serde_json::Value>,
204 committer: Option<&str>,
205 extra_env: Option<HashMap<String, String>>,
206 stderr: std::process::Stdio,
207 update_changelog: Option<bool>,
208) -> Result<CommandResult, Error> {
209 let mut env = std::env::vars().collect::<HashMap<_, _>>();
210
211 if let Some(extra_env) = extra_env.as_ref() {
212 for (k, v) in extra_env {
213 env.insert(k.to_string(), v.to_string());
214 }
215 }
216
217 env.insert("SVP_API".to_string(), "1".to_string());
218
219 let debian_path = if control_files_in_root(local_tree, subpath) {
220 subpath.to_owned()
221 } else {
222 subpath.join("debian")
223 };
224
225 let update_changelog = update_changelog.unwrap_or_else(|| {
226 if let Some(dch_guess) = guess_update_changelog(local_tree, &debian_path) {
227 log::info!("{}", dch_guess.explanation);
228 dch_guess.update_changelog
229 } else {
230 true
232 }
233 });
234
235 let cl_path = debian_path.join("changelog");
236 let source_name = match local_tree.get_file_text(&cl_path) {
237 Ok(text) => debian_changelog::ChangeLog::read(text.as_slice())
238 .unwrap()
239 .iter()
240 .next()
241 .and_then(|e| e.package()),
242 Err(BrzError::NoSuchFile(_)) => None,
243 Err(e) => {
244 return Err(Error::Other(format!("Failed to read changelog: {}", e)));
245 }
246 };
247
248 let last_revision = local_tree.last_revision().unwrap();
249
250 let mut orig_tags = local_tree.get_tag_dict().unwrap();
251
252 let td = tempfile::tempdir()?;
253
254 let result_path = td.path().join("result.json");
255 env.insert(
256 "SVP_RESULT".to_string(),
257 result_path.to_string_lossy().to_string(),
258 );
259 if let Some(resume_metadata) = resume_metadata {
260 let resume_path = td.path().join("resume.json");
261 env.insert(
262 "SVP_RESUME".to_string(),
263 resume_path.to_string_lossy().to_string(),
264 );
265 let w = std::fs::File::create(&resume_path)?;
266 serde_json::to_writer(w, &resume_metadata)?;
267 }
268
269 let mut command = std::process::Command::new(script[0]);
270 command.args(&script[1..]);
271 command.envs(env);
272 command.stdin(std::process::Stdio::null());
273 command.stdout(std::process::Stdio::piped());
274 command.stderr(stderr);
275 command.current_dir(local_tree.abspath(subpath).unwrap());
276
277 let ret = match command.output() {
278 Ok(ret) => ret,
279 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
280 return Err(Error::ScriptNotFound);
281 }
282 Err(e) => {
283 return Err(Error::Io(e));
284 }
285 };
286
287 if !ret.status.success() {
288 return Err(match std::fs::read_to_string(&result_path) {
289 Ok(result) => {
290 let result: DetailedFailure = serde_json::from_str(&result)?;
291 Error::Detailed(result)
292 }
293 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
294 Error::ExitCode(ret.status.code().unwrap_or(1))
295 }
296 Err(_) => Error::ExitCode(ret.status.code().unwrap_or(1)),
297 });
298 }
299
300 let source_name: String = if let Some(source_name) = source_name {
303 source_name
304 } else {
305 match local_tree.get_file_text(&cl_path) {
306 Ok(text) => match ChangeLog::read(text.as_slice())?
307 .iter()
308 .next()
309 .and_then(|e| e.package())
310 {
311 Some(source_name) => source_name,
312 None => {
313 return Err(Error::Other(format!(
314 "Failed to read changelog: {}",
315 cl_path.display()
316 )));
317 }
318 },
319 Err(BrzError::NoSuchFile(_)) => {
320 return Err(Error::MissingChangelog(cl_path));
321 }
322 Err(e) => {
323 return Err(Error::Other(format!("Failed to read changelog: {}", e)));
324 }
325 }
326 };
327
328 let mut result: DetailedSuccess = match std::fs::read_to_string(&result_path) {
330 Ok(result) => serde_json::from_str(&result)?,
331 Err(e) if e.kind() == std::io::ErrorKind::NotFound => DetailedSuccess::default(),
332 Err(e) => return Err(e.into()),
333 };
334
335 if result.description.is_none() {
336 result.description = Some(String::from_utf8(ret.stdout)?);
337 }
338
339 let mut new_revision = local_tree.last_revision().unwrap();
340 let tags: Vec<(String, Option<RevisionId>)> = if let Some(tags) = result.tags {
341 tags.into_iter()
342 .map(|(n, v)| (n, v.map(|v| RevisionId::from(v.as_bytes().to_vec()))))
343 .collect()
344 } else {
345 let mut tags = local_tree
346 .get_tag_dict()
347 .unwrap()
348 .into_iter()
349 .filter_map(|(n, v)| {
350 if orig_tags.remove(n.as_str()).as_ref() != Some(&v) {
351 Some((n, Some(v)))
352 } else {
353 None
354 }
355 })
356 .collect::<Vec<_>>();
357 tags.extend(orig_tags.into_keys().map(|n| (n, None)));
358 tags
359 };
360
361 let commit_pending = match commit_pending {
362 CommitPending::Yes => true,
363 CommitPending::No => false,
364 CommitPending::Auto => {
365 last_revision == new_revision
368 }
369 };
370
371 if commit_pending {
372 if update_changelog && result.description.is_some() && local_tree.has_changes().unwrap() {
373 let maintainer = match extra_env.map(|e| get_maintainer_from_env(|k| e.get(k).cloned()))
374 {
375 Some(Some((name, email))) => Some((name, email)),
376 _ => None,
377 };
378
379 add_changelog_entry(
380 local_tree,
381 &debian_path.join("changelog"),
382 vec![result.description.as_ref().unwrap().as_str()].as_slice(),
383 maintainer.as_ref(),
384 None,
385 None,
386 );
387 }
388 local_tree
389 .smart_add(&[local_tree.abspath(subpath).unwrap().as_path()])
390 .unwrap();
391 let mut builder = local_tree
392 .build_commit()
393 .message(result.description.as_ref().unwrap())
394 .allow_pointless(false);
395 if let Some(committer) = committer {
396 builder = builder.committer(committer);
397 }
398 new_revision = match builder.commit() {
399 Ok(rev) => rev,
400 Err(BrzError::PointlessCommit) => {
401 last_revision.clone()
403 }
404 Err(e) => return Err(Error::Other(format!("Failed to commit changes: {}", e))),
405 };
406 }
407
408 if new_revision == last_revision {
409 return Err(Error::ScriptMadeNoChanges);
410 }
411
412 let old_revision = last_revision;
413 let new_revision = local_tree.last_revision().unwrap();
414
415 Ok(CommandResult {
416 source_name,
417 old_revision,
418 new_revision,
419 tags,
420 description: result.description.unwrap(),
421 value: result.value,
422 context: result.context,
423 serialized_context: result.serialized_context,
424 target_branch_url: result.target_branch_url,
425 })
426}