use std::collections::BTreeMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::landed::Landed;
use crate::workspaces::Bound;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionRequest {
pub repo: String,
pub branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch_prefix: Option<String>,
pub base: Option<String>,
pub execution_checkout: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pool: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overflow: Option<Bound>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub labels: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub refuse_conflicts: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SessionToken(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Session {
pub token: SessionToken,
pub worktree: PathBuf,
pub branch: String,
pub base: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conflict: Option<OpenConflict>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OpenConflict {
pub paths: Vec<String>,
pub base_commit: String,
pub branch_tip: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Lifecycle {
Open,
Closed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionRecord {
pub session: Session,
pub identity: String,
pub lifecycle: Lifecycle,
pub provenance: Provenance,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retried_by: Option<SessionToken>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionHolder {
pub token: SessionToken,
pub identity: String,
pub branch: String,
pub worktree: PathBuf,
pub owner_pid: u32,
pub state: Lifecycle,
pub liveness: Liveness,
#[serde(default)]
pub labels: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Liveness {
Live,
Stale,
}
impl Liveness {
pub fn as_str(&self) -> &'static str {
match self {
Self::Live => "live",
Self::Stale => "stale",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Provenance {
Complete,
IncompleteStep,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PreservedBranch {
pub branch: String,
pub base: String,
pub provenance: Provenance,
pub change_url: Option<Url>,
pub change_base: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Scope {
All,
Repo(String),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum Detail {
#[default]
Full,
Decision,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Selection {
#[serde(default)]
pub detail: Detail,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sessions: Vec<SessionToken>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub labels: BTreeMap<String, String>,
}
impl Selection {
pub fn is_empty(&self) -> bool {
self.sessions.is_empty() && self.labels.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "AnyRecoverable")]
pub struct Recoverable {
pub identity: String,
pub branch: PreservedBranch,
pub checkout: PathBuf,
#[serde(default)]
pub tip: Option<String>,
pub stopped_because: String,
#[serde(default)]
pub landed: Landed,
pub recover_command: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub held_by: Option<HeldBy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub net_negative: Option<NetNegative>,
#[serde(default)]
pub session: Option<SessionToken>,
#[serde(default)]
pub labels: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub on_origin: Option<OnOrigin>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retirement: Option<crate::retire::Retirement>,
}
#[derive(Deserialize)]
struct AnyRecoverable {
#[serde(default)]
tip: Option<String>,
identity: String,
branch: PreservedBranch,
checkout: PathBuf,
#[serde(default)]
landed: Landed,
stopped_because: String,
recover_command: Vec<String>,
#[serde(default)]
held_by: Option<HeldBy>,
#[serde(default)]
net_negative: Option<NetNegative>,
#[serde(default)]
session: Option<SessionToken>,
#[serde(default)]
labels: BTreeMap<String, String>,
on_origin: Option<OnOrigin>,
#[serde(default)]
retirement: Option<crate::retire::Retirement>,
}
impl TryFrom<AnyRecoverable> for Recoverable {
type Error = String;
fn try_from(value: AnyRecoverable) -> std::result::Result<Self, Self::Error> {
if value
.tip
.as_ref()
.is_some_and(|tip| crate::git::ObjectId::parse(tip).is_none())
{
return Err(format!(
"the row for branch {:?} carries tip {:?}, which is not a full commit object name",
value.branch.branch, value.tip
));
}
if value.landed.is_landed() && !value.recover_command.is_empty() {
return Err(format!(
"the row for branch {branch:?} says its work reached {base} and carries \
{command:?} to publish it again; a row that landed carries no command",
branch = value.branch.branch,
base = value.branch.base,
command = value.recover_command.join(" "),
));
}
if let Some(held) = &value.held_by {
if held.token.is_none() != (held.holding == Holding::PublicationRunning) {
return Err(format!(
"the row for branch {branch:?} carries a {holding:?} hold that {names} a \
session; a running publication is the one hold that names none",
branch = value.branch.branch,
holding = held.holding,
names = if held.token.is_some() {
"names"
} else {
"names no"
},
));
}
}
Ok(Recoverable {
identity: value.identity,
branch: value.branch,
checkout: value.checkout,
tip: value.tip,
landed: value.landed,
stopped_because: value.stopped_because,
recover_command: value.recover_command,
held_by: value.held_by,
net_negative: value.net_negative,
session: value.session,
labels: value.labels,
on_origin: value.on_origin,
retirement: value.retirement,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OnOrigin {
pub remote: String,
pub commit: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HeldBy {
pub token: Option<SessionToken>,
pub worktree: PathBuf,
pub holding: Holding,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Holding {
OwnerRunning,
RunRootOccupied,
PublicationRunning,
}
impl Holding {
pub fn because(&self) -> &'static str {
match self {
Self::OwnerRunning => "the process that opened it is still running",
Self::RunRootOccupied => "a command is working in its run root right now",
Self::PublicationRunning => "a publication is running on this branch right now",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LineChange {
pub added: u64,
pub removed: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "LineChange", into = "LineChange")]
pub struct NetNegative(LineChange);
impl NetNegative {
pub fn new(lines: LineChange) -> Option<Self> {
(lines.removed > lines.added).then_some(Self(lines))
}
pub fn added(&self) -> u64 {
self.0.added
}
pub fn removed(&self) -> u64 {
self.0.removed
}
}
impl TryFrom<LineChange> for NetNegative {
type Error = String;
fn try_from(lines: LineChange) -> std::result::Result<Self, Self::Error> {
Self::new(lines).ok_or_else(|| {
format!(
"{} line(s) added and {} removed is not a net-negative change",
lines.added, lines.removed
)
})
}
}
impl From<NetNegative> for LineChange {
fn from(value: NetNegative) -> Self {
value.0
}
}