Skip to main content

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/// Returns the branch name derived from a script name
96pub fn derived_branch_name(script: &str) -> &str {
97    let first_word = script.split(' ').next().unwrap_or("");
98    let script_name = Path::new(first_word).file_stem().unwrap_or_default();
99    script_name.to_str().unwrap_or("")
100}
101
102/// Policy on whether to commit pending changes
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
104pub enum CommitPending {
105    /// Automatically determine pending changes
106    #[default]
107    Auto,
108
109    /// Commit pending changes
110    Yes,
111
112    /// Don't commit pending changes
113    No,
114}
115
116impl std::str::FromStr for CommitPending {
117    type Err = String;
118
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        match s {
121            "auto" => Ok(CommitPending::Auto),
122            "yes" => Ok(CommitPending::Yes),
123            "no" => Ok(CommitPending::No),
124            _ => Err(format!("Unknown commit-pending value: {}", s)),
125        }
126    }
127}
128
129impl Serialize for CommitPending {
130    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
131    where
132        S: Serializer,
133    {
134        match *self {
135            CommitPending::Auto => serializer.serialize_none(),
136            CommitPending::Yes => serializer.serialize_bool(true),
137            CommitPending::No => serializer.serialize_bool(false),
138        }
139    }
140}
141
142impl<'de> Deserialize<'de> for CommitPending {
143    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144    where
145        D: Deserializer<'de>,
146    {
147        let opt: Option<bool> = Option::deserialize(deserializer)?;
148        Ok(match opt {
149            None => CommitPending::Auto,
150            Some(true) => CommitPending::Yes,
151            Some(false) => CommitPending::No,
152        })
153    }
154}
155
156impl CommitPending {
157    /// Returns whether the policy is to commit pending changes
158    pub fn is_default(&self) -> bool {
159        *self == CommitPending::Auto
160    }
161}
162
163/// The result of a codemod
164pub trait CodemodResult {
165    /// Context
166    fn context(&self) -> serde_json::Value;
167
168    /// Returns the value of the result
169    fn value(&self) -> Option<u32>;
170
171    /// Returns the URL of the target branch
172    fn target_branch_url(&self) -> Option<url::Url>;
173
174    /// Returns the description of the result
175    fn description(&self) -> Option<String>;
176
177    /// Returns the tags of the result
178    fn tags(&self) -> Vec<(String, Option<RevisionId>)>;
179
180    /// Returns the context as a Tera context
181    fn tera_context(&self) -> tera::Context {
182        tera::Context::from_value(self.context()).unwrap()
183    }
184}
185
186/// The version of the library
187pub const VERSION: &str = env!("CARGO_PKG_VERSION");
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use std::str::FromStr;
193
194    #[test]
195    fn test_derived_branch_name() {
196        assert_eq!(derived_branch_name("script.py"), "script");
197        assert_eq!(derived_branch_name("path/to/script.py"), "script");
198        assert_eq!(derived_branch_name("/absolute/path/to/script.py"), "script");
199        assert_eq!(derived_branch_name("script.py arg1 arg2"), "script");
200        assert_eq!(derived_branch_name(""), "");
201        assert_eq!(derived_branch_name("script"), "script");
202        assert_eq!(derived_branch_name("no-extension."), "no-extension");
203    }
204
205    #[test]
206    fn test_commit_pending_from_str() {
207        assert_eq!(
208            CommitPending::from_str("auto").unwrap(),
209            CommitPending::Auto
210        );
211        assert_eq!(CommitPending::from_str("yes").unwrap(), CommitPending::Yes);
212        assert_eq!(CommitPending::from_str("no").unwrap(), CommitPending::No);
213
214        let err = CommitPending::from_str("invalid").unwrap_err();
215        assert_eq!(err, "Unknown commit-pending value: invalid");
216    }
217
218    #[test]
219    fn test_commit_pending_serialization() {
220        // Test serialization
221        let auto_json = serde_json::to_string(&CommitPending::Auto).unwrap();
222        let yes_json = serde_json::to_string(&CommitPending::Yes).unwrap();
223        let no_json = serde_json::to_string(&CommitPending::No).unwrap();
224
225        assert_eq!(auto_json, "null");
226        assert_eq!(yes_json, "true");
227        assert_eq!(no_json, "false");
228
229        // Test deserialization
230        let auto: CommitPending = serde_json::from_str("null").unwrap();
231        let yes: CommitPending = serde_json::from_str("true").unwrap();
232        let no: CommitPending = serde_json::from_str("false").unwrap();
233
234        assert_eq!(auto, CommitPending::Auto);
235        assert_eq!(yes, CommitPending::Yes);
236        assert_eq!(no, CommitPending::No);
237    }
238
239    #[test]
240    fn test_commit_pending_is_default() {
241        assert!(CommitPending::Auto.is_default());
242        assert!(!CommitPending::Yes.is_default());
243        assert!(!CommitPending::No.is_default());
244    }
245
246    #[test]
247    fn test_mode_from_str() {
248        assert_eq!(Mode::from_str("push").unwrap(), Mode::Push);
249        assert_eq!(Mode::from_str("propose").unwrap(), Mode::Propose);
250        assert_eq!(Mode::from_str("attempt").unwrap(), Mode::AttemptPush);
251        assert_eq!(Mode::from_str("attempt-push").unwrap(), Mode::AttemptPush);
252        assert_eq!(Mode::from_str("push-derived").unwrap(), Mode::PushDerived);
253        assert_eq!(Mode::from_str("bts").unwrap(), Mode::Bts);
254
255        let err = Mode::from_str("invalid").unwrap_err();
256        assert_eq!(err, "Unknown mode: invalid");
257    }
258
259    #[test]
260    fn test_mode_to_string() {
261        assert_eq!(Mode::Push.to_string(), "push");
262        assert_eq!(Mode::Propose.to_string(), "propose");
263        assert_eq!(Mode::AttemptPush.to_string(), "attempt-push");
264        assert_eq!(Mode::PushDerived.to_string(), "push-derived");
265        assert_eq!(Mode::Bts.to_string(), "bts");
266    }
267}