Skip to main content

silver_platter/debian/
codemod.rs

1//! Codemod runner
2
3use 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)]
14/// A codemod command result
15pub struct CommandResult {
16    /// Source package name
17    pub source_name: String,
18
19    /// Result value
20    pub value: Option<u32>,
21
22    /// Unique representation of the context
23    pub context: Option<serde_json::Value>,
24
25    /// Description of the command
26    pub description: String,
27
28    /// Serialized context
29    pub serialized_context: Option<String>,
30
31    /// Tags
32    pub tags: Vec<(String, Option<RevisionId>)>,
33
34    /// Target branch URL
35    pub target_branch_url: Option<Url>,
36
37    /// Old revision
38    pub old_revision: RevisionId,
39
40    /// New revision
41    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)]
96/// Code mod error
97pub enum Error {
98    /// The script made no changes
99    ScriptMadeNoChanges,
100
101    /// The script was not found
102    ScriptNotFound,
103
104    /// No changelog found
105    MissingChangelog(std::path::PathBuf),
106
107    /// Changelog parse error
108    ChangelogParse(debian_changelog::ParseError),
109
110    /// Script exited with a non-zero exit code
111    ExitCode(i32),
112
113    /// Detailed failure
114    Detailed(DetailedFailure),
115
116    /// I/O error
117    Io(std::io::Error),
118
119    /// JSON error
120    Json(serde_json::Error),
121
122    /// UTF-8 error
123    Utf8(std::string::FromUtf8Error),
124
125    /// Other error
126    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)]
176/// A detailed failure
177pub struct DetailedFailure {
178    /// Result code
179    pub result_code: String,
180    /// Description of the failure
181    pub description: Option<String>,
182    /// Stage at with the failure occurred
183    pub stage: Option<Vec<String>>,
184    /// Details about the failure
185    pub details: Option<serde_json::Value>,
186}
187
188/// Run a script in a tree and commit the result.
189///
190/// This ignores newly added files.
191///
192/// # Arguments
193///
194/// - `local_tree`: Local tree to run script in
195/// - `subpath`: Subpath to run script in
196/// - `script`: Script to run
197/// - `commit_pending`: Whether to commit pending changes
198pub 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            // Assume yes.
231            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    // If the changelog didn't exist earlier, then hopefully it was created
301    // now.
302    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    // Open result_path, read metadata
329    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            // Automatically commit pending changes if the script did not
366            // touch the branch
367            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                // No changes
402                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}