silver_platter/
lib.rs

1//! # Silver-Platter
2//!
3//! Silver-Platter makes it possible to contribute automatable changes to source
4//! code in a version control system
5//! ([codemods](https://github.com/jelmer/awesome-codemods)).
6//!
7//! It automatically creates a local checkout of a remote repository,
8//! makes user-specified changes, publishes those changes on the remote hosting
9//! site and then creates a pull request.
10//!
11//! In addition to that, it can also perform basic maintenance on branches
12//! that have been proposed for merging - such as restarting them if they
13//! have conflicts due to upstream changes.
14
15#![deny(missing_docs)]
16// Allow unknown cfgs for now, since import_exception_bound
17// expects a gil-refs feature that is not defined
18#![allow(unexpected_cfgs)]
19pub mod batch;
20pub mod candidates;
21pub mod checks;
22pub mod codemod;
23#[cfg(feature = "debian")]
24pub mod debian;
25pub mod probers;
26pub mod proposal;
27pub mod publish;
28pub mod recipe;
29pub mod run;
30pub mod utils;
31pub mod vcs;
32pub mod workspace;
33pub use breezyshim::branch::{Branch, GenericBranch};
34pub use breezyshim::controldir::{ControlDir, Prober};
35pub use breezyshim::forge::{Forge, MergeProposal};
36pub use breezyshim::transport::Transport;
37pub use breezyshim::tree::WorkingTree;
38pub use breezyshim::RevisionId;
39use serde::{Deserialize, Deserializer, Serialize, Serializer};
40use std::path::Path;
41
42#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
43/// Publish mode
44pub enum Mode {
45    #[serde(rename = "push")]
46    /// Push to the target branch
47    Push,
48
49    #[serde(rename = "propose")]
50    /// Propose a merge
51    Propose,
52
53    #[serde(rename = "attempt-push")]
54    #[default]
55    /// Attempt to push to the target branch, falling back to propose if necessary
56    AttemptPush,
57
58    #[serde(rename = "push-derived")]
59    /// Push to a branch derived from the script name
60    PushDerived,
61
62    #[serde(rename = "bts")]
63    /// Bug tracking system
64    Bts,
65}
66
67impl std::fmt::Display for Mode {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        let s = match self {
70            Mode::Push => "push",
71            Mode::Propose => "propose",
72            Mode::AttemptPush => "attempt-push",
73            Mode::PushDerived => "push-derived",
74            Mode::Bts => "bts",
75        };
76        write!(f, "{}", s)
77    }
78}
79
80impl std::str::FromStr for Mode {
81    type Err = String;
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        match s {
85            "push" => Ok(Mode::Push),
86            "propose" => Ok(Mode::Propose),
87            "attempt" | "attempt-push" => Ok(Mode::AttemptPush),
88            "push-derived" => Ok(Mode::PushDerived),
89            "bts" => Ok(Mode::Bts),
90            _ => Err(format!("Unknown mode: {}", s)),
91        }
92    }
93}
94
95#[cfg(feature = "pyo3")]
96impl pyo3::FromPyObject<'_, '_> for Mode {
97    type Error = pyo3::PyErr;
98
99    fn extract(ob: pyo3::Borrowed<'_, '_, pyo3::PyAny>) -> Result<Self, Self::Error> {
100        let s: std::borrow::Cow<str> = ob.extract()?;
101        match s.as_ref() {
102            "push" => Ok(Mode::Push),
103            "propose" => Ok(Mode::Propose),
104            "attempt-push" => Ok(Mode::AttemptPush),
105            "push-derived" => Ok(Mode::PushDerived),
106            "bts" => Ok(Mode::Bts),
107            _ => Err(pyo3::exceptions::PyValueError::new_err((format!(
108                "Unknown mode: {}",
109                s
110            ),))),
111        }
112    }
113}
114
115/// Returns the branch name derived from a script name
116pub fn derived_branch_name(script: &str) -> &str {
117    let first_word = script.split(' ').next().unwrap_or("");
118    let script_name = Path::new(first_word).file_stem().unwrap_or_default();
119    script_name.to_str().unwrap_or("")
120}
121
122/// Policy on whether to commit pending changes
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum CommitPending {
125    /// Automatically determine pending changes
126    #[default]
127    Auto,
128
129    /// Commit pending changes
130    Yes,
131
132    /// Don't commit pending changes
133    No,
134}
135
136impl std::str::FromStr for CommitPending {
137    type Err = String;
138
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        match s {
141            "auto" => Ok(CommitPending::Auto),
142            "yes" => Ok(CommitPending::Yes),
143            "no" => Ok(CommitPending::No),
144            _ => Err(format!("Unknown commit-pending value: {}", s)),
145        }
146    }
147}
148
149impl Serialize for CommitPending {
150    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
151    where
152        S: Serializer,
153    {
154        match *self {
155            CommitPending::Auto => serializer.serialize_none(),
156            CommitPending::Yes => serializer.serialize_bool(true),
157            CommitPending::No => serializer.serialize_bool(false),
158        }
159    }
160}
161
162impl<'de> Deserialize<'de> for CommitPending {
163    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164    where
165        D: Deserializer<'de>,
166    {
167        let opt: Option<bool> = Option::deserialize(deserializer)?;
168        Ok(match opt {
169            None => CommitPending::Auto,
170            Some(true) => CommitPending::Yes,
171            Some(false) => CommitPending::No,
172        })
173    }
174}
175
176impl CommitPending {
177    /// Returns whether the policy is to commit pending changes
178    pub fn is_default(&self) -> bool {
179        *self == CommitPending::Auto
180    }
181}
182
183/// The result of a codemod
184pub trait CodemodResult {
185    /// Context
186    fn context(&self) -> serde_json::Value;
187
188    /// Returns the value of the result
189    fn value(&self) -> Option<u32>;
190
191    /// Returns the URL of the target branch
192    fn target_branch_url(&self) -> Option<url::Url>;
193
194    /// Returns the description of the result
195    fn description(&self) -> Option<String>;
196
197    /// Returns the tags of the result
198    fn tags(&self) -> Vec<(String, Option<RevisionId>)>;
199
200    /// Returns the context as a Tera context
201    fn tera_context(&self) -> tera::Context {
202        tera::Context::from_value(self.context()).unwrap()
203    }
204}
205
206/// The version of the library
207pub const VERSION: &str = env!("CARGO_PKG_VERSION");
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use std::str::FromStr;
213
214    #[test]
215    fn test_derived_branch_name() {
216        assert_eq!(derived_branch_name("script.py"), "script");
217        assert_eq!(derived_branch_name("path/to/script.py"), "script");
218        assert_eq!(derived_branch_name("/absolute/path/to/script.py"), "script");
219        assert_eq!(derived_branch_name("script.py arg1 arg2"), "script");
220        assert_eq!(derived_branch_name(""), "");
221        assert_eq!(derived_branch_name("script"), "script");
222        assert_eq!(derived_branch_name("no-extension."), "no-extension");
223    }
224
225    #[test]
226    fn test_commit_pending_from_str() {
227        assert_eq!(
228            CommitPending::from_str("auto").unwrap(),
229            CommitPending::Auto
230        );
231        assert_eq!(CommitPending::from_str("yes").unwrap(), CommitPending::Yes);
232        assert_eq!(CommitPending::from_str("no").unwrap(), CommitPending::No);
233
234        let err = CommitPending::from_str("invalid").unwrap_err();
235        assert_eq!(err, "Unknown commit-pending value: invalid");
236    }
237
238    #[test]
239    fn test_commit_pending_serialization() {
240        // Test serialization
241        let auto_json = serde_json::to_string(&CommitPending::Auto).unwrap();
242        let yes_json = serde_json::to_string(&CommitPending::Yes).unwrap();
243        let no_json = serde_json::to_string(&CommitPending::No).unwrap();
244
245        assert_eq!(auto_json, "null");
246        assert_eq!(yes_json, "true");
247        assert_eq!(no_json, "false");
248
249        // Test deserialization
250        let auto: CommitPending = serde_json::from_str("null").unwrap();
251        let yes: CommitPending = serde_json::from_str("true").unwrap();
252        let no: CommitPending = serde_json::from_str("false").unwrap();
253
254        assert_eq!(auto, CommitPending::Auto);
255        assert_eq!(yes, CommitPending::Yes);
256        assert_eq!(no, CommitPending::No);
257    }
258
259    #[test]
260    fn test_commit_pending_is_default() {
261        assert!(CommitPending::Auto.is_default());
262        assert!(!CommitPending::Yes.is_default());
263        assert!(!CommitPending::No.is_default());
264    }
265
266    #[test]
267    fn test_mode_from_str() {
268        assert_eq!(Mode::from_str("push").unwrap(), Mode::Push);
269        assert_eq!(Mode::from_str("propose").unwrap(), Mode::Propose);
270        assert_eq!(Mode::from_str("attempt").unwrap(), Mode::AttemptPush);
271        assert_eq!(Mode::from_str("attempt-push").unwrap(), Mode::AttemptPush);
272        assert_eq!(Mode::from_str("push-derived").unwrap(), Mode::PushDerived);
273        assert_eq!(Mode::from_str("bts").unwrap(), Mode::Bts);
274
275        let err = Mode::from_str("invalid").unwrap_err();
276        assert_eq!(err, "Unknown mode: invalid");
277    }
278
279    #[test]
280    fn test_mode_to_string() {
281        assert_eq!(Mode::Push.to_string(), "push");
282        assert_eq!(Mode::Propose.to_string(), "propose");
283        assert_eq!(Mode::AttemptPush.to_string(), "attempt-push");
284        assert_eq!(Mode::PushDerived.to_string(), "push-derived");
285        assert_eq!(Mode::Bts.to_string(), "bts");
286    }
287}