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;
25#[cfg(feature = "mcp")]
26pub mod mcp;
27pub mod probers;
28pub mod proposal;
29pub mod publish;
30pub mod recipe;
31pub mod run;
32pub mod utils;
33pub mod vcs;
34pub mod workspace;
35pub use breezyshim::branch::{Branch, GenericBranch};
36pub use breezyshim::controldir::{ControlDir, Prober};
37pub use breezyshim::forge::{Forge, MergeProposal};
38pub use breezyshim::transport::Transport;
39pub use breezyshim::tree::WorkingTree;
40pub use breezyshim::RevisionId;
41use serde::{Deserialize, Deserializer, Serialize, Serializer};
42use std::path::Path;
43
44#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
45/// Publish mode
46pub enum Mode {
47    #[serde(rename = "push")]
48    /// Push to the target branch
49    Push,
50
51    #[serde(rename = "propose")]
52    /// Propose a merge
53    Propose,
54
55    #[serde(rename = "attempt-push")]
56    #[default]
57    /// Attempt to push to the target branch, falling back to propose if necessary
58    AttemptPush,
59
60    #[serde(rename = "push-derived")]
61    /// Push to a branch derived from the script name
62    PushDerived,
63
64    #[serde(rename = "bts")]
65    /// Bug tracking system
66    Bts,
67}
68
69impl std::fmt::Display for Mode {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        let s = match self {
72            Mode::Push => "push",
73            Mode::Propose => "propose",
74            Mode::AttemptPush => "attempt-push",
75            Mode::PushDerived => "push-derived",
76            Mode::Bts => "bts",
77        };
78        write!(f, "{}", s)
79    }
80}
81
82impl std::str::FromStr for Mode {
83    type Err = String;
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        match s {
87            "push" => Ok(Mode::Push),
88            "propose" => Ok(Mode::Propose),
89            "attempt" | "attempt-push" => Ok(Mode::AttemptPush),
90            "push-derived" => Ok(Mode::PushDerived),
91            "bts" => Ok(Mode::Bts),
92            _ => Err(format!("Unknown mode: {}", s)),
93        }
94    }
95}
96
97/// Returns the branch name derived from a script name
98pub fn derived_branch_name(script: &str) -> &str {
99    let first_word = script.split(' ').next().unwrap_or("");
100    let script_name = Path::new(first_word).file_stem().unwrap_or_default();
101    script_name.to_str().unwrap_or("")
102}
103
104/// Policy on whether to commit pending changes
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum CommitPending {
107    /// Automatically determine pending changes
108    #[default]
109    Auto,
110
111    /// Commit pending changes
112    Yes,
113
114    /// Don't commit pending changes
115    No,
116}
117
118impl std::str::FromStr for CommitPending {
119    type Err = String;
120
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        match s {
123            "auto" => Ok(CommitPending::Auto),
124            "yes" => Ok(CommitPending::Yes),
125            "no" => Ok(CommitPending::No),
126            _ => Err(format!("Unknown commit-pending value: {}", s)),
127        }
128    }
129}
130
131impl Serialize for CommitPending {
132    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
133    where
134        S: Serializer,
135    {
136        match *self {
137            CommitPending::Auto => serializer.serialize_none(),
138            CommitPending::Yes => serializer.serialize_bool(true),
139            CommitPending::No => serializer.serialize_bool(false),
140        }
141    }
142}
143
144impl<'de> Deserialize<'de> for CommitPending {
145    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
146    where
147        D: Deserializer<'de>,
148    {
149        let opt: Option<bool> = Option::deserialize(deserializer)?;
150        Ok(match opt {
151            None => CommitPending::Auto,
152            Some(true) => CommitPending::Yes,
153            Some(false) => CommitPending::No,
154        })
155    }
156}
157
158impl CommitPending {
159    /// Returns whether the policy is to commit pending changes
160    pub fn is_default(&self) -> bool {
161        *self == CommitPending::Auto
162    }
163}
164
165/// The result of a codemod
166pub trait CodemodResult {
167    /// Context
168    fn context(&self) -> serde_json::Value;
169
170    /// Returns the value of the result
171    fn value(&self) -> Option<u32>;
172
173    /// Returns the URL of the target branch
174    fn target_branch_url(&self) -> Option<url::Url>;
175
176    /// Returns the description of the result
177    fn description(&self) -> Option<String>;
178
179    /// Returns the tags of the result
180    fn tags(&self) -> Vec<(String, Option<RevisionId>)>;
181
182    /// Returns the context as a Tera context
183    fn tera_context(&self) -> tera::Context {
184        tera::Context::from_value(self.context()).unwrap()
185    }
186}
187
188/// The version of the library
189pub const VERSION: &str = env!("CARGO_PKG_VERSION");
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use std::str::FromStr;
195
196    #[test]
197    fn test_derived_branch_name() {
198        assert_eq!(derived_branch_name("script.py"), "script");
199        assert_eq!(derived_branch_name("path/to/script.py"), "script");
200        assert_eq!(derived_branch_name("/absolute/path/to/script.py"), "script");
201        assert_eq!(derived_branch_name("script.py arg1 arg2"), "script");
202        assert_eq!(derived_branch_name(""), "");
203        assert_eq!(derived_branch_name("script"), "script");
204        assert_eq!(derived_branch_name("no-extension."), "no-extension");
205    }
206
207    #[test]
208    fn test_commit_pending_from_str() {
209        assert_eq!(
210            CommitPending::from_str("auto").unwrap(),
211            CommitPending::Auto
212        );
213        assert_eq!(CommitPending::from_str("yes").unwrap(), CommitPending::Yes);
214        assert_eq!(CommitPending::from_str("no").unwrap(), CommitPending::No);
215
216        let err = CommitPending::from_str("invalid").unwrap_err();
217        assert_eq!(err, "Unknown commit-pending value: invalid");
218    }
219
220    #[test]
221    fn test_commit_pending_serialization() {
222        // Test serialization
223        let auto_json = serde_json::to_string(&CommitPending::Auto).unwrap();
224        let yes_json = serde_json::to_string(&CommitPending::Yes).unwrap();
225        let no_json = serde_json::to_string(&CommitPending::No).unwrap();
226
227        assert_eq!(auto_json, "null");
228        assert_eq!(yes_json, "true");
229        assert_eq!(no_json, "false");
230
231        // Test deserialization
232        let auto: CommitPending = serde_json::from_str("null").unwrap();
233        let yes: CommitPending = serde_json::from_str("true").unwrap();
234        let no: CommitPending = serde_json::from_str("false").unwrap();
235
236        assert_eq!(auto, CommitPending::Auto);
237        assert_eq!(yes, CommitPending::Yes);
238        assert_eq!(no, CommitPending::No);
239    }
240
241    #[test]
242    fn test_commit_pending_is_default() {
243        assert!(CommitPending::Auto.is_default());
244        assert!(!CommitPending::Yes.is_default());
245        assert!(!CommitPending::No.is_default());
246    }
247
248    #[test]
249    fn test_mode_from_str() {
250        assert_eq!(Mode::from_str("push").unwrap(), Mode::Push);
251        assert_eq!(Mode::from_str("propose").unwrap(), Mode::Propose);
252        assert_eq!(Mode::from_str("attempt").unwrap(), Mode::AttemptPush);
253        assert_eq!(Mode::from_str("attempt-push").unwrap(), Mode::AttemptPush);
254        assert_eq!(Mode::from_str("push-derived").unwrap(), Mode::PushDerived);
255        assert_eq!(Mode::from_str("bts").unwrap(), Mode::Bts);
256
257        let err = Mode::from_str("invalid").unwrap_err();
258        assert_eq!(err, "Unknown mode: invalid");
259    }
260
261    #[test]
262    fn test_mode_to_string() {
263        assert_eq!(Mode::Push.to_string(), "push");
264        assert_eq!(Mode::Propose.to_string(), "propose");
265        assert_eq!(Mode::AttemptPush.to_string(), "attempt-push");
266        assert_eq!(Mode::PushDerived.to_string(), "push-derived");
267        assert_eq!(Mode::Bts.to_string(), "bts");
268    }
269}