Add pull request milestone features

This commit is contained in:
Georg Bauer
2026-07-31 14:22:21 +02:00
parent c5ecb8f87d
commit 86200b4668
9 changed files with 474 additions and 52 deletions

View File

@@ -159,29 +159,48 @@ pub async fn load_milestone(
apis::issue_api::issue_get_milestone(&configuration, owner, repository, &id.to_string())
.await
.map_err(|error| error.to_string())?;
let issues = apis::issue_api::issue_list_issues(
&configuration,
owner,
repository,
Some("all"),
None,
None,
Some("issues"),
milestone.title.as_deref(),
None,
None,
None,
None,
None,
Some(1),
Some(100),
)
.await
.map_err(|error| error.to_string())?;
Ok(MilestoneDetails { milestone, issues })
let load = |kind| {
apis::issue_api::issue_list_issues(
&configuration,
owner,
repository,
Some("all"),
None,
None,
Some(kind),
milestone.title.as_deref(),
None,
None,
None,
None,
None,
Some(1),
Some(100),
)
};
let (issues, pulls) = tokio::join!(load("issues"), load("pulls"));
let mut pulls = pulls.map_err(|error| error.to_string())?;
for pull in &mut pulls {
pull.repository.get_or_insert_with(|| {
Box::new(models::RepositoryMeta {
name: Some(repository.into()),
owner: Some(owner.into()),
..Default::default()
})
});
}
Ok(MilestoneDetails {
milestone,
issues: issues.map_err(|error| error.to_string())?,
pulls,
})
}
pub async fn load_pulls(server: &Server, status: &str) -> Result<Vec<models::Issue>, String> {
pub async fn load_pulls(
server: &Server,
status: &str,
milestone: &str,
) -> Result<Vec<models::Issue>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
@@ -197,7 +216,7 @@ pub async fn load_pulls(server: &Server, status: &str) -> Result<Vec<models::Iss
&configuration,
Some(status),
None,
None,
(!milestone.is_empty()).then_some(milestone),
None,
None,
Some("pulls"),
@@ -223,6 +242,20 @@ pub async fn load_pulls(server: &Server, status: &str) -> Result<Vec<models::Iss
Ok(pulls)
}
pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String> {
let (open, closed) = tokio::try_join!(
load_pulls(server, "open", ""),
load_pulls(server, "closed", "")
)?;
Ok(open
.into_iter()
.chain(closed)
.filter_map(|pull| pull.milestone?.title)
.collect::<BTreeSet<_>>()
.into_iter()
.collect())
}
pub async fn load_branches(
server: &Server,
owner: &str,

View File

@@ -55,6 +55,8 @@ pub struct Preferences {
#[serde(default = "open_status")]
pub pull_status: String,
#[serde(default)]
pub pull_filters: BTreeMap<String, PullFilter>,
#[serde(default)]
pub appearance: AppearanceMode,
}
@@ -67,6 +69,7 @@ impl Default for Preferences {
issue_status: open_status(),
issue_filters: BTreeMap::new(),
pull_status: open_status(),
pull_filters: BTreeMap::new(),
appearance: AppearanceMode::default(),
}
}
@@ -86,6 +89,18 @@ impl IssueFilter {
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct PullFilter {
#[serde(default)]
pub milestone: String,
}
impl PullFilter {
pub fn is_active(&self, status: &str) -> bool {
status != "open" || self != &Self::default()
}
}
#[derive(Clone)]
pub struct RepositoryData {
pub owner: String,
@@ -127,6 +142,7 @@ pub struct IssueDetails {
pub struct MilestoneDetails {
pub milestone: models::Milestone,
pub issues: Vec<models::Issue>,
pub pulls: Vec<models::Issue>,
}
pub struct PullDetails {
@@ -154,5 +170,13 @@ mod tests {
}
.is_active("open")
);
assert!(!PullFilter::default().is_active("open"));
assert!(PullFilter::default().is_active("closed"));
assert!(
PullFilter {
milestone: "v1".into(),
}
.is_active("open")
);
}
}

View File

@@ -344,10 +344,68 @@ impl GotchaCore {
))
}
pub async fn pull_filters(&self) -> Result<PullFilterOptions, GotchaError> {
let server = self.server()?;
let filter = self
.state
.lock()
.unwrap()
.preferences
.pull_filters
.get(&server.url)
.cloned()
.unwrap_or_default();
Ok(pull_filter_options(
&load_pull_milestones(&server).await?,
&filter,
))
}
pub fn pull_filters_active(&self) -> Result<bool, GotchaError> {
let server = self.server()?;
let state = self.state.lock().unwrap();
let filter = state
.preferences
.pull_filters
.get(&server.url)
.cloned()
.unwrap_or_default();
Ok(filter.is_active(&state.preferences.pull_status))
}
pub fn set_pull_filters(&self, milestone: String) -> Result<(), GotchaError> {
let filter = PullFilter { milestone };
let mut state = self.state.lock().unwrap();
let server = state
.active_server
.and_then(|index| state.preferences.servers.get(index))
.ok_or("Select a server first.")?;
let key = server.url.clone();
if filter == PullFilter::default() {
state.preferences.pull_filters.remove(&key);
} else {
state.preferences.pull_filters.insert(key, filter);
}
save_preferences(&state.preferences).map_err(Into::into)
}
pub async fn pulls(&self) -> Result<Vec<PullRow>, GotchaError> {
let server = self.server()?;
let status = self.state.lock().unwrap().preferences.pull_status.clone();
Ok(pull_rows(&load_pulls(&server, &status).await?))
let (status, filter) = {
let state = self.state.lock().unwrap();
(
state.preferences.pull_status.clone(),
state
.preferences
.pull_filters
.get(&server.url)
.cloned()
.unwrap_or_default(),
)
};
Ok(pull_rows(
&load_pulls(&server, &status, &filter.milestone).await?,
))
}
pub async fn pull(

View File

@@ -4,7 +4,7 @@ use crate::{
activity,
domain::{
HistoryCommit, HomeData, IssueDetails, IssueFilter, MilestoneDetails, PullDetails,
RepositoryData,
PullFilter, RepositoryData,
},
};
@@ -91,6 +91,13 @@ pub struct MilestoneRow {
pub struct MilestonePage {
pub milestone: MilestoneRow,
pub issues: Vec<IssueRow>,
pub pulls: Vec<PullRow>,
}
#[derive(Clone, uniffi::Record)]
pub struct PullFilterOptions {
pub milestones: Vec<String>,
pub selected_milestone: String,
}
#[derive(Clone, uniffi::Record)]
@@ -292,6 +299,20 @@ pub fn milestone_page(details: MilestoneDetails) -> MilestonePage {
MilestonePage {
milestone: milestone_row(&details.milestone),
issues: issue_rows(&details.issues),
pulls: pull_rows(&details.pulls),
}
}
pub fn pull_filter_options(milestones: &[String], filter: &PullFilter) -> PullFilterOptions {
let mut milestones = milestones.to_vec();
if !filter.milestone.is_empty() {
milestones.push(filter.milestone.clone());
}
milestones.sort_by_key(|milestone| milestone.to_lowercase());
milestones.dedup();
PullFilterOptions {
milestones,
selected_milestone: filter.milestone.clone(),
}
}
@@ -1186,6 +1207,37 @@ mod tests {
assert_eq!(options.milestones, ["v1", "v2", "v3"]);
assert_eq!(options.selected_labels, ["bug", "retired"]);
assert_eq!(options.selected_milestone, "v3");
let pull_options = pull_filter_options(
&["v2".into(), "v1".into()],
&PullFilter {
milestone: "v3".into(),
},
);
assert_eq!(pull_options.milestones, ["v1", "v2", "v3"]);
assert_eq!(pull_options.selected_milestone, "v3");
}
#[test]
fn milestone_page_includes_linked_pull_requests() {
let page = milestone_page(MilestoneDetails {
milestone: models::Milestone::default(),
issues: Vec::new(),
pulls: vec![models::Issue {
number: Some(7),
title: Some("Linked pull".into()),
repository: Some(Box::new(models::RepositoryMeta {
name: Some("demo".into()),
owner: Some("octo".into()),
..Default::default()
})),
..Default::default()
}],
});
assert!(page.issues.is_empty());
assert_eq!(page.pulls.len(), 1);
assert_eq!(page.pulls[0].number, 7);
}
#[test]

View File

@@ -100,7 +100,7 @@ mod tests {
"https://gitea.example.com|octo/demo"
);
let preferences: Preferences = serde_json::from_str(
r#"{"issue_filters":{"server|octo/demo":{"milestone":"v1","labels":["bug"]}}}"#,
r#"{"issue_filters":{"server|octo/demo":{"milestone":"v1","labels":["bug"]}},"pull_filters":{"server":{"milestone":"v2"}}}"#,
)
.unwrap();
assert_eq!(
@@ -112,6 +112,7 @@ mod tests {
.labels
.contains("bug")
);
assert_eq!(preferences.pull_filters["server"].milestone, "v2");
assert_eq!(Preferences::default().pull_status, "open");
assert_eq!(
Preferences::default().appearance,