use crate::{Id, MoonContext, VirtualPath};
use bitflags::bitflags;
use std::collections::BTreeMap;
use std::path::PathBuf;
use warpgate_api::{api_enum, api_struct, api_unit_enum};
pub const VCS_PLUGIN_PROTOCOL_VERSION: u16 = 6;
api_struct!(
pub struct RegisterVcsInput {
pub id: Id,
pub host_protocol_version: u16,
}
);
api_struct!(
#[serde(default)]
pub struct RegisterVcsOutput {
pub name: String,
pub description: Option<String>,
pub plugin_version: String,
pub protocol_version: u16,
}
);
api_struct!(
pub struct VcsRoots {
pub repository_root: VirtualPath,
pub working_root: VirtualPath,
}
);
api_struct!(
pub struct InitializeVcsInput {
pub baseline: Option<String>,
#[serde(default)]
pub remote_candidates: Vec<String>,
pub context: MoonContext,
}
);
api_struct!(
pub struct VcsState {
pub id: Option<String>,
pub label: Option<String>,
}
);
api_unit_enum!(
pub enum VcsHistoryCompleteness {
Complete,
Incomplete,
#[default]
Unknown,
}
);
api_struct!(
pub struct VcsInitialization {
pub client: Id,
pub client_version: Option<String>,
pub roots: VcsRoots,
pub current: VcsState,
pub recorded: VcsState,
pub baseline: Option<VcsState>,
pub repository_slug: Option<String>,
pub history: VcsHistoryCompleteness,
}
);
api_enum!(
#[serde(tag = "status", rename_all = "kebab-case")]
pub enum InitializeVcsOutput {
NotDetected {
reason: String,
},
Initialized {
initialization: Box<VcsInitialization>,
},
}
);
api_enum!(
#[derive(Default)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum VcsImpactIntent {
#[default]
Working,
Submission {
base: Option<String>,
head: Option<String>,
include_working: bool,
},
}
);
api_struct!(
pub struct GetVcsImpactsInput {
pub context: MoonContext,
pub intent: VcsImpactIntent,
}
);
#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "schematic", derive(schematic::Schematic))]
#[serde(transparent)]
pub struct VcsChangeMask(u8);
bitflags! {
impl VcsChangeMask: u8 {
const ADDED = 1;
const DELETED = 2;
const MODIFIED = 4;
const RECORDED = 8;
const STAGED = 16;
const WORKING = 32;
const UNTRACKED = 64;
const CHANGE_BITS = Self::ADDED.bits() | Self::DELETED.bits() | Self::MODIFIED.bits();
const LOCATION_BITS = Self::RECORDED.bits() | Self::STAGED.bits() | Self::WORKING.bits() | Self::UNTRACKED.bits();
const KNOWN_BITS = Self::CHANGE_BITS.bits() | Self::LOCATION_BITS.bits();
}
}
api_unit_enum!(
pub enum VcsImpactCompleteness {
Exact,
Conservative,
#[default]
Unavailable,
}
);
api_struct!(
#[serde(default)]
pub struct GetVcsImpactsOutput {
pub changes: BTreeMap<PathBuf, VcsChangeMask>,
pub completeness: VcsImpactCompleteness,
pub diagnostics: Vec<String>,
}
);
api_struct!(
pub struct SetupVcsHookEnvironmentInput {
pub context: MoonContext,
pub hooks_dir: VirtualPath,
pub hooks: Vec<String>,
}
);
api_struct!(
#[serde(default)]
pub struct SetupVcsHookEnvironmentOutput {
pub working_dir: Option<VirtualPath>,
}
);
api_struct!(
pub struct TeardownVcsHookEnvironmentInput {
pub context: MoonContext,
pub hooks_dir: VirtualPath,
pub hooks: Vec<String>,
}
);
api_struct!(
pub struct TeardownVcsHookEnvironmentOutput {}
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_working_intents() {
assert_eq!(
serde_json::to_value(VcsImpactIntent::Working).unwrap(),
serde_json::json!({"type": "working"})
);
assert_eq!(
serde_json::to_value(VcsImpactIntent::Submission {
base: None,
head: None,
include_working: true,
})
.unwrap(),
serde_json::json!({
"type": "submission",
"base": null,
"head": null,
"include_working": true,
})
);
}
#[test]
fn serializes_vcs_identifiers_as_strings() {
let initialization = VcsInitialization {
client: Id::raw("git"),
client_version: Some("2.0.0".into()),
roots: VcsRoots {
repository_root: VirtualPath::new("/repo/.git"),
working_root: VirtualPath::new("/repo"),
},
current: VcsState {
id: Some("abc123".into()),
label: Some("main".into()),
},
recorded: VcsState {
id: None,
label: None,
},
baseline: None,
repository_slug: None,
history: VcsHistoryCompleteness::Complete,
};
let output = InitializeVcsOutput::Initialized {
initialization: Box::new(initialization),
};
let value = serde_json::to_value(output).unwrap();
assert_eq!(value["status"], serde_json::json!("initialized"));
assert_eq!(value["initialization"]["client"], serde_json::json!("git"));
assert_eq!(
value["initialization"]["current"]["id"],
serde_json::json!("abc123")
);
assert_eq!(
value["initialization"]["recorded"]["id"],
serde_json::Value::Null
);
assert_eq!(
serde_json::to_value("main").unwrap(),
serde_json::json!("main")
);
}
#[test]
fn serializes_change_masks_as_numbers() {
assert_eq!(
serde_json::to_value(VcsChangeMask::ADDED).unwrap(),
serde_json::json!(1)
);
assert_eq!(
serde_json::to_value(VcsChangeMask::DELETED).unwrap(),
serde_json::json!(2)
);
assert_eq!(
serde_json::to_value(VcsChangeMask::MODIFIED).unwrap(),
serde_json::json!(4)
);
assert_eq!(
serde_json::to_value(VcsChangeMask::RECORDED).unwrap(),
serde_json::json!(8)
);
assert_eq!(
serde_json::to_value(VcsChangeMask::STAGED).unwrap(),
serde_json::json!(16)
);
assert_eq!(
serde_json::to_value(VcsChangeMask::WORKING).unwrap(),
serde_json::json!(32)
);
assert_eq!(
serde_json::to_value(VcsChangeMask::UNTRACKED).unwrap(),
serde_json::json!(64)
);
let output = GetVcsImpactsOutput {
changes: BTreeMap::from([
(
PathBuf::from("a.txt"),
VcsChangeMask::ADDED | VcsChangeMask::WORKING,
),
(
PathBuf::from("z.txt"),
VcsChangeMask::MODIFIED | VcsChangeMask::RECORDED,
),
]),
completeness: VcsImpactCompleteness::Exact,
..Default::default()
};
assert_eq!(
serde_json::to_value(output).unwrap(),
serde_json::json!({
"changes": {
"a.txt": 33,
"z.txt": 12,
},
"completeness": "exact",
"diagnostics": [],
})
);
}
#[test]
fn defaults_missing_impact_completeness_to_unavailable() {
let output: GetVcsImpactsOutput = serde_json::from_value(serde_json::json!({
"changes": {},
"diagnostics": [],
}))
.unwrap();
assert_eq!(output.completeness, VcsImpactCompleteness::Unavailable);
}
}