Skip to main content

silver_platter/
proposal.rs

1//! Merge proposal related functions
2use crate::vcs::{full_branch_url, open_branch};
3use breezyshim::branch::{Branch, GenericBranch};
4use breezyshim::error::Error as BrzError;
5pub use breezyshim::forge::MergeProposal;
6pub use breezyshim::forge::MergeProposalStatus;
7use breezyshim::forge::{iter_forge_instances, Forge};
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10use url::Url;
11
12fn instance_iter_mps(
13    instance: Forge,
14    statuses: Option<Vec<MergeProposalStatus>>,
15) -> impl Iterator<Item = MergeProposal> {
16    let statuses = statuses.unwrap_or_else(|| vec![MergeProposalStatus::All]);
17    statuses
18        .into_iter()
19        .flat_map(
20            move |status| match instance.iter_my_proposals(Some(status), None) {
21                Ok(mps) => Some(mps),
22                Err(BrzError::ForgeLoginRequired) => {
23                    log::warn!("Skipping forge {:?} because login is required", instance);
24                    None
25                }
26                Err(e) => {
27                    log::error!("Error listing merge proposals: {:?}", e);
28                    None
29                }
30            },
31        )
32        .flatten()
33}
34
35/// Iterate over all merge proposals
36pub fn iter_all_mps(
37    statuses: Option<Vec<MergeProposalStatus>>,
38) -> impl Iterator<Item = (Forge, MergeProposal)> {
39    let statuses = statuses.unwrap_or_else(|| vec![MergeProposalStatus::All]);
40    iter_forge_instances().flat_map(move |instance| {
41        instance_iter_mps(instance.clone(), Some(statuses.clone()))
42            .map(move |mp| (instance.clone(), mp))
43    })
44}
45
46/// Find conflicted branches owned by the current user.
47///
48/// # Arguments
49/// * `branch_name`: Branch name to search for
50pub fn iter_conflicted(
51    branch_name: &str,
52) -> impl Iterator<
53    Item = (
54        Url,
55        GenericBranch,
56        String,
57        GenericBranch,
58        Forge,
59        MergeProposal,
60        bool,
61    ),
62> + '_ {
63    let mut possible_transports = vec![];
64
65    iter_all_mps(Some(vec![MergeProposalStatus::Open])).filter_map(move |(forge, mp)| {
66        if mp.can_be_merged().unwrap() {
67            None
68        } else {
69            let main_branch = open_branch(
70                &mp.get_target_branch_url().unwrap().unwrap(),
71                Some(&mut possible_transports),
72                None,
73                None,
74            )
75            .unwrap();
76            let resume_branch = open_branch(
77                &mp.get_source_branch_url().unwrap().unwrap(),
78                Some(&mut possible_transports),
79                None,
80                None,
81            )
82            .unwrap();
83            if resume_branch.name().as_deref() != Some(branch_name)
84                && !(resume_branch.name().is_none()
85                    && resume_branch.get_user_url().as_str().ends_with(branch_name))
86            {
87                None
88            } else {
89                // TODO(jelmer): Find out somehow whether we need to modify a subpath?
90                let subpath = "";
91
92                Some((
93                    full_branch_url(&resume_branch),
94                    main_branch,
95                    subpath.to_string(),
96                    resume_branch,
97                    forge,
98                    mp,
99                    true,
100                ))
101            }
102        }
103    })
104}
105
106#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)]
107#[serde(rename_all = "kebab-case")]
108/// Description format for merge proposals descriptions
109pub enum DescriptionFormat {
110    /// Markdown format
111    Markdown,
112
113    /// HTML format
114    Html,
115
116    /// Plain text format
117    Plain,
118}
119
120impl FromStr for DescriptionFormat {
121    type Err = String;
122
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        match s {
125            "markdown" => Ok(DescriptionFormat::Markdown),
126            "html" => Ok(DescriptionFormat::Html),
127            "plain" => Ok(DescriptionFormat::Plain),
128            _ => Err(format!("Unknown description format: {}", s)),
129        }
130    }
131}
132
133impl ToString for DescriptionFormat {
134    fn to_string(&self) -> String {
135        match self {
136            DescriptionFormat::Markdown => "markdown".to_string(),
137            DescriptionFormat::Html => "html".to_string(),
138            DescriptionFormat::Plain => "plain".to_string(),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_description_format_from_str() {
149        assert_eq!(
150            DescriptionFormat::from_str("markdown").unwrap(),
151            DescriptionFormat::Markdown
152        );
153        assert_eq!(
154            DescriptionFormat::from_str("html").unwrap(),
155            DescriptionFormat::Html
156        );
157        assert_eq!(
158            DescriptionFormat::from_str("plain").unwrap(),
159            DescriptionFormat::Plain
160        );
161
162        // Test invalid format
163        assert!(DescriptionFormat::from_str("invalid").is_err());
164        let err = DescriptionFormat::from_str("invalid").unwrap_err();
165        assert_eq!(err, "Unknown description format: invalid");
166    }
167
168    #[test]
169    fn test_description_format_to_string() {
170        assert_eq!(DescriptionFormat::Markdown.to_string(), "markdown");
171        assert_eq!(DescriptionFormat::Html.to_string(), "html");
172        assert_eq!(DescriptionFormat::Plain.to_string(), "plain");
173    }
174
175    #[test]
176    fn test_description_format_serialization() {
177        // Test serialization
178        let format = DescriptionFormat::Markdown;
179        let serialized = serde_json::to_string(&format).unwrap();
180        assert_eq!(serialized, "\"markdown\"");
181
182        // Test deserialization
183        let deserialized: DescriptionFormat = serde_json::from_str("\"markdown\"").unwrap();
184        assert_eq!(deserialized, DescriptionFormat::Markdown);
185
186        let deserialized: DescriptionFormat = serde_json::from_str("\"html\"").unwrap();
187        assert_eq!(deserialized, DescriptionFormat::Html);
188
189        let deserialized: DescriptionFormat = serde_json::from_str("\"plain\"").unwrap();
190        assert_eq!(deserialized, DescriptionFormat::Plain);
191    }
192}