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;
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)]
45pub enum Mode {
47 #[serde(rename = "push")]
48 Push,
50
51 #[serde(rename = "propose")]
52 Propose,
54
55 #[serde(rename = "attempt-push")]
56 #[default]
57 AttemptPush,
59
60 #[serde(rename = "push-derived")]
61 PushDerived,
63
64 #[serde(rename = "bts")]
65 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
97pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum CommitPending {
107 #[default]
109 Auto,
110
111 Yes,
113
114 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 pub fn is_default(&self) -> bool {
161 *self == CommitPending::Auto
162 }
163}
164
165pub trait CodemodResult {
167 fn context(&self) -> serde_json::Value;
169
170 fn value(&self) -> Option<u32>;
172
173 fn target_branch_url(&self) -> Option<url::Url>;
175
176 fn description(&self) -> Option<String>;
178
179 fn tags(&self) -> Vec<(String, Option<RevisionId>)>;
181
182 fn tera_context(&self) -> tera::Context {
184 tera::Context::from_value(self.context()).unwrap()
185 }
186}
187
188pub 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 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 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}