Add first-class Actions management (#63)
This commit is contained in:
@@ -1,7 +1,82 @@
|
||||
use gotcha_gitea::{
|
||||
Client, DEFAULT_PAGE_SIZE as PAGE_SIZE, IssueQuery, RepositoryId, api_date, diff, models,
|
||||
ActionJobDetails, ActionRunDetails, ActionRunQuery, Client, DEFAULT_PAGE_SIZE as PAGE_SIZE,
|
||||
IssueQuery, Page as GiteaPage, RepositoryId, api_date, diff, models, parse_action_inputs,
|
||||
};
|
||||
|
||||
pub async fn load_action_workflows(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
repository: &str,
|
||||
) -> Result<Vec<models::ActionWorkflow>, String> {
|
||||
client(server)?
|
||||
.action_workflows(&scope(owner, repository)?)
|
||||
.await
|
||||
.map_err(message)
|
||||
}
|
||||
|
||||
pub async fn dispatch_action_workflow(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
repository: &str,
|
||||
workflow: &str,
|
||||
reference: &str,
|
||||
inputs: &[String],
|
||||
) -> Result<(), String> {
|
||||
client(server)?
|
||||
.dispatch_action_workflow(
|
||||
&scope(owner, repository)?,
|
||||
workflow,
|
||||
reference,
|
||||
&parse_action_inputs(inputs).map_err(message)?,
|
||||
)
|
||||
.await
|
||||
.map_err(message)
|
||||
}
|
||||
|
||||
pub async fn load_action_runs(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
repository: &str,
|
||||
page: i32,
|
||||
) -> Result<GiteaPage<models::ActionWorkflowRun>, String> {
|
||||
client(server)?
|
||||
.action_runs(
|
||||
&scope(owner, repository)?,
|
||||
&ActionRunQuery {
|
||||
page,
|
||||
limit: PAGE_SIZE,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(message)
|
||||
}
|
||||
|
||||
pub async fn load_action_run(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
repository: &str,
|
||||
run: i64,
|
||||
) -> Result<ActionRunDetails, String> {
|
||||
client(server)?
|
||||
.action_run_details(&scope(owner, repository)?, run)
|
||||
.await
|
||||
.map_err(message)
|
||||
}
|
||||
|
||||
pub async fn load_action_job_details(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
repository: &str,
|
||||
run: i64,
|
||||
job: i64,
|
||||
) -> Result<ActionJobDetails, String> {
|
||||
client(server)?
|
||||
.action_job_details(&scope(owner, repository)?, run, job)
|
||||
.await
|
||||
.map_err(message)
|
||||
}
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, IssueFilter,
|
||||
|
||||
75
crates/app/src/core/actions.rs
Normal file
75
crates/app/src/core/actions.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use crate::*;
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl GotchaCore {
|
||||
pub async fn action_workflows(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
) -> Result<Vec<ActionWorkflowRow>, GotchaError> {
|
||||
let (owner, repository) = validate_repository(&owner, &repository)?;
|
||||
Ok(action_workflow_rows(
|
||||
load_action_workflows(&self.server()?, owner, repository).await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn dispatch_action_workflow(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
workflow: String,
|
||||
reference: String,
|
||||
inputs: Vec<String>,
|
||||
) -> Result<(), GotchaError> {
|
||||
let (owner, repository) = validate_repository(&owner, &repository)?;
|
||||
dispatch_action_workflow(
|
||||
&self.server()?,
|
||||
owner,
|
||||
repository,
|
||||
&workflow,
|
||||
&reference,
|
||||
&inputs,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn action_runs(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
page: u32,
|
||||
) -> Result<ActionRunListPage, GotchaError> {
|
||||
let (owner, repository) = validate_repository(&owner, &repository)?;
|
||||
let page = load_action_runs(&self.server()?, owner, repository, valid_page(page)?).await?;
|
||||
Ok(ActionRunListPage {
|
||||
rows: action_run_rows(&page.items),
|
||||
has_more: page.has_more,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn action_run(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
run: i64,
|
||||
) -> Result<ActionRunPage, GotchaError> {
|
||||
let (owner, repository) = validate_repository(&owner, &repository)?;
|
||||
Ok(action_run_page(
|
||||
load_action_run(&self.server()?, owner, repository, run).await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn action_job_log(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
run: i64,
|
||||
job: i64,
|
||||
) -> Result<ActionJobLogPage, GotchaError> {
|
||||
let (owner, repository) = validate_repository(&owner, &repository)?;
|
||||
Ok(action_job_log_page(
|
||||
load_action_job_details(&self.server()?, owner, repository, run, job).await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod actions;
|
||||
mod content;
|
||||
mod issues;
|
||||
mod milestones;
|
||||
|
||||
@@ -229,9 +229,25 @@ impl GotchaCore {
|
||||
pull_status: state.preferences.pull_status.clone(),
|
||||
appearance: state.preferences.appearance.index() as u32,
|
||||
notifications_enabled: state.preferences.notifications_enabled,
|
||||
primary_destinations: state.preferences.primary_destinations.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_primary_destinations(
|
||||
&self,
|
||||
destinations: Vec<PrimaryDestination>,
|
||||
) -> Result<(), GotchaError> {
|
||||
let mut normalized = destinations;
|
||||
if normalize_primary_destinations(&mut normalized) {
|
||||
return Err(
|
||||
"Choose each navigation destination once, with no more than four selected.".into(),
|
||||
);
|
||||
}
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.preferences.primary_destinations = normalized;
|
||||
save_preferences(&state.preferences).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn set_issue_status(&self, status: String) -> Result<(), GotchaError> {
|
||||
if !matches!(status.as_str(), "open" | "closed") {
|
||||
return Err("Unsupported issue status.".into());
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
pub use gotcha_gitea::{
|
||||
HistoryCommit, HomeData, IssueDetails, IssueEditorData, MilestoneDetails, Page, PullDetails,
|
||||
civil_from_days, days_from_civil, parse_api_date,
|
||||
civil_from_days, days_from_civil, parse_api_date, parse_api_timestamp,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -74,6 +74,8 @@ pub struct Preferences {
|
||||
pub notifications_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub notification_cursors: BTreeMap<String, String>,
|
||||
#[serde(default = "default_primary_destinations")]
|
||||
pub primary_destinations: Vec<crate::PrimaryDestination>,
|
||||
}
|
||||
|
||||
impl Default for Preferences {
|
||||
@@ -90,10 +92,32 @@ impl Default for Preferences {
|
||||
appearance: AppearanceMode::default(),
|
||||
notifications_enabled: false,
|
||||
notification_cursors: BTreeMap::new(),
|
||||
primary_destinations: default_primary_destinations(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_primary_destinations() -> Vec<crate::PrimaryDestination> {
|
||||
vec![
|
||||
crate::PrimaryDestination::Issues,
|
||||
crate::PrimaryDestination::Repositories,
|
||||
crate::PrimaryDestination::PullRequests,
|
||||
crate::PrimaryDestination::Milestones,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn normalize_primary_destinations(destinations: &mut Vec<crate::PrimaryDestination>) -> bool {
|
||||
let original = destinations.clone();
|
||||
let mut unique = Vec::with_capacity(destinations.len().min(4));
|
||||
for destination in destinations.drain(..) {
|
||||
if !unique.contains(&destination) && unique.len() < 4 {
|
||||
unique.push(destination);
|
||||
}
|
||||
}
|
||||
*destinations = unique;
|
||||
*destinations != original
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct IssueFilter {
|
||||
#[serde(default)]
|
||||
@@ -222,4 +246,32 @@ mod tests {
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn primary_destinations_are_ordered_unique_and_limited() {
|
||||
let migrated: Preferences = serde_json::from_str("{}").unwrap();
|
||||
assert_eq!(
|
||||
migrated.primary_destinations,
|
||||
default_primary_destinations()
|
||||
);
|
||||
|
||||
let mut destinations = vec![
|
||||
crate::PrimaryDestination::Actions,
|
||||
crate::PrimaryDestination::Actions,
|
||||
crate::PrimaryDestination::Issues,
|
||||
crate::PrimaryDestination::Repositories,
|
||||
crate::PrimaryDestination::PullRequests,
|
||||
crate::PrimaryDestination::Milestones,
|
||||
];
|
||||
assert!(normalize_primary_destinations(&mut destinations));
|
||||
assert_eq!(
|
||||
destinations,
|
||||
[
|
||||
crate::PrimaryDestination::Actions,
|
||||
crate::PrimaryDestination::Issues,
|
||||
crate::PrimaryDestination::Repositories,
|
||||
crate::PrimaryDestination::PullRequests,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Mutex;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -14,11 +15,23 @@ use storage::*;
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, uniffi::Enum)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PrimaryDestination {
|
||||
Issues,
|
||||
Repositories,
|
||||
PullRequests,
|
||||
Milestones,
|
||||
Actions,
|
||||
ServerActivity,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, uniffi::Enum)]
|
||||
pub enum RepositoryPane {
|
||||
Issues,
|
||||
Commits,
|
||||
Milestones,
|
||||
Actions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, uniffi::Enum)]
|
||||
@@ -46,13 +59,14 @@ impl From<gotcha_gitea::Provider> for ServerProvider {
|
||||
}
|
||||
|
||||
impl RepositoryPane {
|
||||
const ALL: [Self; 3] = [Self::Issues, Self::Commits, Self::Milestones];
|
||||
const ALL: [Self; 4] = [Self::Issues, Self::Commits, Self::Milestones, Self::Actions];
|
||||
|
||||
const fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Issues => "issues",
|
||||
Self::Commits => "commits",
|
||||
Self::Milestones => "milestones",
|
||||
Self::Actions => "actions",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,6 +95,7 @@ pub struct Settings {
|
||||
pub pull_status: String,
|
||||
pub appearance: u32,
|
||||
pub notifications_enabled: bool,
|
||||
pub primary_destinations: Vec<PrimaryDestination>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
|
||||
@@ -5,6 +5,7 @@ use gotcha_gitea::{
|
||||
use crate::domain::{
|
||||
HistoryCommit, HomeData, IssueDetails, IssueEditorData, IssueFilter, MilestoneDetails,
|
||||
PullDetails, PullFilter, RepositoryData, civil_from_days, days_from_civil, parse_api_date,
|
||||
parse_api_timestamp,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||
@@ -35,6 +36,84 @@ pub struct RepositoryRow {
|
||||
pub description: String,
|
||||
pub meta: String,
|
||||
pub favorite: bool,
|
||||
pub default_branch: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||
pub enum ActionState {
|
||||
Queued,
|
||||
Waiting,
|
||||
InProgress,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
Skipped,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ActionWorkflowRow {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ActionRunRow {
|
||||
pub id: i64,
|
||||
pub number: i64,
|
||||
pub title: String,
|
||||
pub state: ActionState,
|
||||
pub status: String,
|
||||
pub conclusion: String,
|
||||
pub branch: String,
|
||||
pub event: String,
|
||||
pub meta: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ActionStepRow {
|
||||
pub name: String,
|
||||
pub state: ActionState,
|
||||
pub meta: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ActionJobRow {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub state: ActionState,
|
||||
pub meta: String,
|
||||
pub steps: Vec<ActionStepRow>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ActionRunListPage {
|
||||
pub rows: Vec<ActionRunRow>,
|
||||
pub has_more: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ActionRunPage {
|
||||
pub run: ActionRunRow,
|
||||
pub jobs: Vec<ActionJobRow>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ActionJobLogPage {
|
||||
pub job: ActionJobRow,
|
||||
pub text: String,
|
||||
pub groups: Vec<ActionLogGroupRow>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ActionLogGroupRow {
|
||||
pub name: String,
|
||||
pub text: String,
|
||||
pub line_count: u64,
|
||||
pub state: ActionState,
|
||||
pub duration: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -404,6 +483,7 @@ pub struct WidgetPullPage {
|
||||
pub rows: Vec<PullRow>,
|
||||
}
|
||||
|
||||
mod actions;
|
||||
mod details;
|
||||
mod files;
|
||||
mod helpers;
|
||||
@@ -411,6 +491,7 @@ mod home;
|
||||
mod lists;
|
||||
mod notifications;
|
||||
|
||||
pub use actions::*;
|
||||
pub use details::*;
|
||||
pub use files::*;
|
||||
pub use helpers::compact_date;
|
||||
|
||||
240
crates/app/src/presentation/actions.rs
Normal file
240
crates/app/src/presentation/actions.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use gotcha_gitea::{ActionJobDetails, ActionRunDetails, models};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn action_workflow_rows(workflows: Vec<models::ActionWorkflow>) -> Vec<ActionWorkflowRow> {
|
||||
workflows
|
||||
.into_iter()
|
||||
.map(|workflow| {
|
||||
let path = workflow.path.unwrap_or_default();
|
||||
ActionWorkflowRow {
|
||||
id: workflow.id.unwrap_or_else(|| path.clone()),
|
||||
name: workflow.name.unwrap_or_else(|| "Unnamed workflow".into()),
|
||||
path,
|
||||
state: workflow.state.unwrap_or_else(|| "unknown".into()),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn action_run_rows(runs: &[models::ActionWorkflowRun]) -> Vec<ActionRunRow> {
|
||||
runs.iter().map(action_run_row).collect()
|
||||
}
|
||||
|
||||
pub fn action_run_page(details: ActionRunDetails) -> ActionRunPage {
|
||||
ActionRunPage {
|
||||
run: action_run_row(&details.run),
|
||||
jobs: details.jobs.iter().map(action_job_row).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_job_log_page(details: ActionJobDetails) -> ActionJobLogPage {
|
||||
let job = action_job_row(&details.job);
|
||||
let groups = details
|
||||
.groups
|
||||
.into_iter()
|
||||
.map(|group| ActionLogGroupRow {
|
||||
state: action_log_group_state(&job, &group.name, &group.text),
|
||||
duration: group
|
||||
.duration_seconds
|
||||
.map(format_duration)
|
||||
.unwrap_or_default(),
|
||||
line_count: group.text.lines().count() as u64,
|
||||
name: group.name,
|
||||
text: group.text,
|
||||
})
|
||||
.collect();
|
||||
ActionJobLogPage {
|
||||
job,
|
||||
text: details.text,
|
||||
groups,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_duration(seconds: u64) -> String {
|
||||
if seconds >= 3_600 {
|
||||
format!("{}h {}m", seconds / 3_600, seconds % 3_600 / 60)
|
||||
} else if seconds >= 60 {
|
||||
format!("{}m {}s", seconds / 60, seconds % 60)
|
||||
} else {
|
||||
format!("{seconds}s")
|
||||
}
|
||||
}
|
||||
|
||||
fn action_log_group_state(job: &ActionJobRow, name: &str, text: &str) -> ActionState {
|
||||
if let Some(step) = job
|
||||
.steps
|
||||
.iter()
|
||||
.find(|step| step.name.eq_ignore_ascii_case(name))
|
||||
{
|
||||
return step.state;
|
||||
}
|
||||
if text.contains("::error::")
|
||||
|| text.contains("##[error]")
|
||||
|| text.contains("Process completed with exit code")
|
||||
|| text.contains("Job failed")
|
||||
{
|
||||
ActionState::Failed
|
||||
} else if name == "Complete job" {
|
||||
job.state
|
||||
} else if matches!(job.state, ActionState::Succeeded | ActionState::Failed) {
|
||||
ActionState::Succeeded
|
||||
} else {
|
||||
ActionState::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn action_run_row(run: &models::ActionWorkflowRun) -> ActionRunRow {
|
||||
let status = run.status.clone().unwrap_or_else(|| "unknown".into());
|
||||
let conclusion = run.conclusion.clone().unwrap_or_default();
|
||||
let duration = duration_between(run.started_at.as_deref(), run.completed_at.as_deref());
|
||||
ActionRunRow {
|
||||
id: run.id.unwrap_or_default(),
|
||||
number: run.run_number.unwrap_or_default(),
|
||||
title: run
|
||||
.display_title
|
||||
.clone()
|
||||
.or_else(|| run.path.clone())
|
||||
.unwrap_or_else(|| "Workflow run".into()),
|
||||
state: action_state(&status, &conclusion),
|
||||
status,
|
||||
conclusion,
|
||||
branch: run.head_branch.clone().unwrap_or_default(),
|
||||
event: run.event.clone().unwrap_or_default(),
|
||||
meta: [
|
||||
duration.as_deref().unwrap_or_default(),
|
||||
run.started_at.as_deref().unwrap_or_default(),
|
||||
run.actor
|
||||
.as_ref()
|
||||
.and_then(|actor| actor.login.as_deref())
|
||||
.unwrap_or_default(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · "),
|
||||
}
|
||||
}
|
||||
|
||||
fn action_job_row(job: &models::ActionWorkflowJob) -> ActionJobRow {
|
||||
let status = job.status.as_deref().unwrap_or("unknown");
|
||||
let conclusion = job.conclusion.as_deref().unwrap_or_default();
|
||||
let duration = duration_between(job.started_at.as_deref(), job.completed_at.as_deref());
|
||||
ActionJobRow {
|
||||
id: job.id.unwrap_or_default(),
|
||||
name: job.name.clone().unwrap_or_else(|| "Job".into()),
|
||||
state: action_state(status, conclusion),
|
||||
meta: [
|
||||
status,
|
||||
conclusion,
|
||||
duration.as_deref().unwrap_or_default(),
|
||||
job.runner_name.as_deref().unwrap_or_default(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · "),
|
||||
steps: job
|
||||
.steps
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|step| {
|
||||
let status = step.status.as_deref().unwrap_or("unknown");
|
||||
let conclusion = step.conclusion.as_deref().unwrap_or_default();
|
||||
let duration =
|
||||
duration_between(step.started_at.as_deref(), step.completed_at.as_deref());
|
||||
ActionStepRow {
|
||||
name: step.name.clone().unwrap_or_else(|| "Step".into()),
|
||||
state: action_state(status, conclusion),
|
||||
meta: [status, conclusion, duration.as_deref().unwrap_or_default()]
|
||||
.into_iter()
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · "),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_between(start: Option<&str>, end: Option<&str>) -> Option<String> {
|
||||
let seconds = parse_api_timestamp(end?)?.checked_sub(parse_api_timestamp(start?)?)?;
|
||||
u64::try_from(seconds).ok().map(format_duration)
|
||||
}
|
||||
|
||||
fn action_state(status: &str, conclusion: &str) -> ActionState {
|
||||
match conclusion {
|
||||
"success" => ActionState::Succeeded,
|
||||
"failure" | "timed_out" | "startup_failure" | "stale" => ActionState::Failed,
|
||||
"cancelled" => ActionState::Cancelled,
|
||||
"skipped" | "neutral" => ActionState::Skipped,
|
||||
_ => match status {
|
||||
"queued" | "requested" => ActionState::Queued,
|
||||
"waiting" | "pending" | "blocked" => ActionState::Waiting,
|
||||
"in_progress" | "running" => ActionState::InProgress,
|
||||
"success" => ActionState::Succeeded,
|
||||
"failure" | "timed_out" | "startup_failure" | "stale" => ActionState::Failed,
|
||||
"cancelled" => ActionState::Cancelled,
|
||||
"skipped" | "neutral" => ActionState::Skipped,
|
||||
_ => ActionState::Unknown,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn classifies_progress_and_terminal_conclusions() {
|
||||
assert_eq!(action_state("queued", ""), ActionState::Queued);
|
||||
assert_eq!(action_state("waiting", ""), ActionState::Waiting);
|
||||
assert_eq!(action_state("in_progress", ""), ActionState::InProgress);
|
||||
assert_eq!(action_state("completed", "success"), ActionState::Succeeded);
|
||||
assert_eq!(action_state("completed", "failure"), ActionState::Failed);
|
||||
assert_eq!(
|
||||
action_state("completed", "cancelled"),
|
||||
ActionState::Cancelled
|
||||
);
|
||||
|
||||
let mut workflow = models::ActionWorkflow::new();
|
||||
workflow.path = Some(".gitea/workflows/ci.yml".into());
|
||||
assert_eq!(
|
||||
action_workflow_rows(vec![workflow])[0].id,
|
||||
".gitea/workflows/ci.yml"
|
||||
);
|
||||
|
||||
let mut job = models::ActionWorkflowJob::new();
|
||||
job.id = Some(1);
|
||||
job.name = Some("build".into());
|
||||
job.status = Some("completed".into());
|
||||
job.conclusion = Some("failure".into());
|
||||
let mut step = models::ActionWorkflowStep::new();
|
||||
step.name = Some("Test".into());
|
||||
step.status = Some("completed".into());
|
||||
step.conclusion = Some("failure".into());
|
||||
job.steps = Some(vec![step]);
|
||||
let log = action_job_log_page(ActionJobDetails {
|
||||
job,
|
||||
text: "one\ntwo".into(),
|
||||
groups: vec![
|
||||
gotcha_gitea::ActionLogGroup {
|
||||
name: "Test".into(),
|
||||
text: "one\ntwo".into(),
|
||||
duration_seconds: Some(65),
|
||||
},
|
||||
gotcha_gitea::ActionLogGroup {
|
||||
name: "Set up job".into(),
|
||||
text: "ready".into(),
|
||||
duration_seconds: Some(1),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert_eq!(log.groups[0].name, "Test");
|
||||
assert_eq!(log.groups[0].line_count, 2);
|
||||
assert_eq!(log.groups[0].state, ActionState::Failed);
|
||||
assert_eq!(log.groups[0].duration, "1m 5s");
|
||||
assert_eq!(log.groups[1].state, ActionState::Succeeded);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ pub fn repository_rows(
|
||||
name: repository.name,
|
||||
owner: repository.owner,
|
||||
description: repository.description,
|
||||
default_branch: repository.default_branch,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use security_framework::passwords::{
|
||||
|
||||
use crate::{
|
||||
RepositoryPane,
|
||||
domain::{Preferences, Server, open_status},
|
||||
domain::{Preferences, Server, normalize_primary_destinations, open_status},
|
||||
};
|
||||
|
||||
pub fn favorite_key(pane: RepositoryPane, server: &str, owner: &str, repository: &str) -> String {
|
||||
@@ -81,6 +81,7 @@ pub fn load_preferences(storage_directory: Option<&str>) -> Result<Preferences,
|
||||
if !matches!(preferences.pull_status.as_str(), "open" | "closed") {
|
||||
preferences.pull_status = open_status();
|
||||
}
|
||||
let navigation_migrated = normalize_primary_destinations(&mut preferences.primary_destinations);
|
||||
let mut credentials_migrated = false;
|
||||
for server in &mut preferences.servers {
|
||||
if server.credential_account.is_empty() {
|
||||
@@ -90,7 +91,7 @@ pub fn load_preferences(storage_directory: Option<&str>) -> Result<Preferences,
|
||||
server.token = String::from_utf8(load_server_token(server)?)
|
||||
.map_err(|_| format!("The token for {} is not valid text.", server.name))?;
|
||||
}
|
||||
if storage_migrated || favorites_migrated || credentials_migrated {
|
||||
if storage_migrated || favorites_migrated || navigation_migrated || credentials_migrated {
|
||||
save_preferences(&preferences)?;
|
||||
}
|
||||
Ok(preferences)
|
||||
@@ -304,6 +305,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
favorites,
|
||||
[
|
||||
"actions|https://gitea.example.com|octo/demo".to_string(),
|
||||
"commits|https://gitea.example.com|octo/demo".to_string(),
|
||||
"issues|https://gitea.example.com|octo/demo".to_string(),
|
||||
"milestones|https://gitea.example.com|octo/demo".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user