1#![deny(missing_docs)]
16#![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)]
43pub enum Mode {
45 #[serde(rename = "push")]
46 Push,
48
49 #[serde(rename = "propose")]
50 Propose,
52
53 #[serde(rename = "attempt-push")]
54 #[default]
55 AttemptPush,
57
58 #[serde(rename = "push-derived")]
59 PushDerived,
61
62 #[serde(rename = "bts")]
63 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
115pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum CommitPending {
125 #[default]
127 Auto,
128
129 Yes,
131
132 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 pub fn is_default(&self) -> bool {
179 *self == CommitPending::Auto
180 }
181}
182
183pub trait CodemodResult {
185 fn context(&self) -> serde_json::Value;
187
188 fn value(&self) -> Option<u32>;
190
191 fn target_branch_url(&self) -> Option<url::Url>;
193
194 fn description(&self) -> Option<String>;
196
197 fn tags(&self) -> Vec<(String, Option<RevisionId>)>;
199
200 fn tera_context(&self) -> tera::Context {
202 tera::Context::from_value(self.context()).unwrap()
203 }
204}
205
206pub 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 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 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}