use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use wsb::peer::model::{
Continuity, Desired, PeerId, RepositoryId, Service, Switch, SyncMode, WorkspaceId,
};
use crate::machine::MachineState;
type Failure = (StatusCode, Json<serde_json::Value>);
fn bad(status: StatusCode, message: impl Into<String>) -> Failure {
(
status,
Json(serde_json::json!({ "error": message.into() })),
)
}
pub fn routes() -> Router<Arc<MachineState>> {
Router::new()
.route(wsb::peer::route::PEERS, get(list))
.route("/api/v1/peers/pair", post(pair_open))
.route("/api/v1/peers/pair/join", post(pair_join))
.route("/api/v1/peers/:peer", axum::routing::delete(remove))
.route("/api/v1/peers/:peer/workspaces", get(peer_workspaces))
.route("/api/v1/peers/:peer/sync", post(enable))
.route("/api/v1/peers/:peer/pause", post(pause))
.route("/api/v1/peers/:peer/resume", post(resume))
.route("/api/v1/peers/:peer/status", get(status))
.route("/api/v1/peers/:peer/conflicts", get(conflicts))
.route("/api/v1/peers/:peer/diff", get(diff))
.route("/api/v1/peers/:peer/conflicts/versions", get(versions))
.route("/api/v1/peers/:peer/conflicts/resolve", post(resolve))
.route("/api/v1/peers/:peer/services", get(services))
.route("/api/v1/peers/:peer/services/:service", post(set_service))
.route("/api/v1/peers/:peer/mode", post(set_mode))
.route("/api/v1/peers/:peer/align", post(align))
.route("/api/v1/peers/:peer/deltas", get(deltas))
.route("/api/v1/peers/:peer/deltas/decide", post(decide))
.route("/api/v1/peers/:peer/deltas/dismiss", post(dismiss))
.route("/api/v1/peers/:peer/deltas/reconsider", post(reconsider))
}
#[derive(Debug, Serialize)]
pub struct PeerView {
pub id: String,
pub name: String,
pub fingerprint: String,
pub address: String,
pub state: String,
pub state_for_seconds: u64,
pub detail: Option<String>,
pub last_seen: Option<u64>,
pub shared_workspaces: usize,
}
async fn list(State(machine): State<Arc<MachineState>>) -> Result<Json<Vec<PeerView>>, Failure> {
let peers = machine
.store
.peers()
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?;
let relationships = machine.store.relationships().unwrap_or_default();
Ok(Json(
peers
.into_iter()
.map(|peer| {
let liveness = machine.peers.liveness(&peer.id);
PeerView {
shared_workspaces: relationships
.iter()
.filter(|(id, _, desired)| *id == peer.id && *desired != Desired::Disabled)
.count(),
state: liveness.state.as_str().to_string(),
state_for_seconds: liveness.held_for(),
detail: liveness.detail,
last_seen: machine.store.last_seen(&peer.id).ok().flatten(),
id: peer.id.to_string(),
name: peer.name,
fingerprint: peer.fingerprint.to_string(),
address: peer.address,
}
})
.collect(),
))
}
#[derive(Debug, Serialize)]
pub struct PairingView {
pub code: String,
pub expires_in_seconds: u64,
pub machine_name: String,
pub fingerprint: String,
pub peer_port: u16,
}
async fn pair_open(State(machine): State<Arc<MachineState>>) -> Result<Json<PairingView>, Failure> {
let peer_port = machine.peers.peer_port();
if peer_port == 0 {
let why = machine
.peers
.refusal()
.unwrap_or_else(|| "peer listening is off (--peer-port 0)".to_string());
return Err(bad(StatusCode::CONFLICT, format!("peers cannot reach this machine, so it cannot pair: {why}")));
}
let code = machine
.peers
.open_pairing()
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?;
Ok(Json(PairingView {
code: code.to_string(),
expires_in_seconds: wsb::peer::identity::PAIRING_WINDOW.as_secs(),
machine_name: machine.identity.name().to_string(),
fingerprint: machine.identity.fingerprint().to_string(),
peer_port,
}))
}
#[derive(Debug, Deserialize)]
pub struct JoinRequest {
pub code: String,
pub address: String,
}
async fn pair_join(
State(machine): State<Arc<MachineState>>,
Json(body): Json<JoinRequest>,
) -> Result<Json<PeerView>, Failure> {
let code = wsb::peer::identity::PairingCode::parse(&body.code)
.map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let address = body.address.clone();
let peers = machine.peers.clone();
let paired = tokio::task::spawn_blocking(move || peers.pair_with(&address, &code))
.await
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, format!("join error: {e}")))?
.map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
Ok(Json(PeerView {
id: paired.id.to_string(),
name: paired.name,
fingerprint: paired.fingerprint.to_string(),
address: paired.address,
state: wsb::peer::model::PeerState::Connected.as_str().to_string(),
state_for_seconds: 0,
detail: None,
last_seen: Some(paired.paired_at),
shared_workspaces: 0,
}))
}
async fn remove(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
) -> Result<Json<serde_json::Value>, Failure> {
let id = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let removed = machine
.store
.remove_peer(&id)
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?;
machine.forget_peer(&id);
Ok(Json(serde_json::json!({ "removed": removed })))
}
async fn peer_workspaces(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
) -> Result<Json<serde_json::Value>, Failure> {
let id = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let mine = machine.summaries().await;
let theirs = machine
.peer_inventory(&id)
.await
.map_err(|e| bad(StatusCode::SERVICE_UNAVAILABLE, e))?;
let shared = super::inventory::compare(&mine, &theirs);
let relationships = machine.store.relationships().unwrap_or_default();
let files = machine
.store
.service(&id, Service::Files)
.unwrap_or_else(|_| Switch::default_for(&Service::Files));
Ok(Json(serde_json::json!({
"workspaces": shared
.into_iter()
.map(|workspace| {
let desired = relationships
.iter()
.find(|(p, w, _)| *p == id && *w == workspace.id)
.map(|(_, _, desired)| *desired)
.unwrap_or(Desired::Disabled);
let mode = machine.store.mode(&id, &workspace.id).unwrap_or_default();
serde_json::json!({
"id": workspace.id.to_string(),
"display_name": workspace.display_name,
"presence": workspace.presence.as_str(),
"compatible": workspace.compatible,
"desired": desired.as_str(),
"mode": mode.as_str(),
"continuity": Continuity::of(files, desired, mode).as_str(),
})
})
.collect::<Vec<_>>()
})))
}
#[derive(Debug, Deserialize)]
pub struct WorkspaceBody {
pub workspace: String,
}
async fn enable(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<WorkspaceBody>,
) -> Result<Json<serde_json::Value>, Failure> {
set_desired(machine, peer, body.workspace, Desired::Enabled).await
}
async fn pause(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<WorkspaceBody>,
) -> Result<Json<serde_json::Value>, Failure> {
set_desired(machine, peer, body.workspace, Desired::Paused).await
}
async fn resume(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<WorkspaceBody>,
) -> Result<Json<serde_json::Value>, Failure> {
set_desired(machine, peer, body.workspace, Desired::Enabled).await
}
async fn set_desired(
machine: Arc<MachineState>,
peer: String,
workspace: String,
desired: Desired,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace = WorkspaceId::parse(&workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
if machine.store.peer(&peer).map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?.is_none() {
return Err(bad(StatusCode::NOT_FOUND, format!("{peer} is not a paired peer")));
}
machine
.store
.set_desired(&peer, &workspace, desired, super::service::now())
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?;
Ok(Json(serde_json::json!({ "desired": desired.as_str() })))
}
#[derive(Debug, Deserialize)]
pub struct StatusQuery {
pub workspace: String,
}
async fn status(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
axum::extract::Query(query): axum::extract::Query<StatusQuery>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace =
WorkspaceId::parse(&query.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let statuses = machine.statuses(&peer, &workspace);
let liveness = machine.peers.liveness(&peer);
Ok(Json(serde_json::json!({
"peer": peer.to_string(),
"workspace": workspace.to_string(),
"peer_state": liveness.state.as_str(),
"desired": machine.store.desired(&peer, &workspace).unwrap_or(Desired::Disabled).as_str(),
"mode": machine.store.mode(&peer, &workspace).unwrap_or_default().as_str(),
"repositories": statuses,
})))
}
async fn conflicts(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
axum::extract::Query(query): axum::extract::Query<StatusQuery>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace =
WorkspaceId::parse(&query.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let rows = machine
.store
.conflicts(&peer, &workspace)
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?;
Ok(Json(serde_json::json!({
"conflicts": rows
.into_iter()
.map(|row| serde_json::json!({
"repository": row.repository,
"path": row.path,
"here": if row.here_hash.is_some() { "modified" } else { "deleted" },
"there": if row.there_hash.is_some() { "modified" } else { "deleted" },
"here_hash": row.here_hash,
"there_hash": row.there_hash,
"base_hash": row.base_hash,
"detected_at": row.detected_at,
}))
.collect::<Vec<_>>()
})))
}
async fn diff(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
axum::extract::Query(query): axum::extract::Query<StatusQuery>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace =
WorkspaceId::parse(&query.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let plans = machine.plans(&peer, &workspace);
let statuses = machine.statuses(&peer, &workspace);
Ok(Json(serde_json::json!({
"peer": peer.to_string(),
"workspace": workspace.to_string(),
"repositories": statuses
.iter()
.map(|status| {
let repository = status.repository.as_str().to_string();
serde_json::json!({
"repository": repository,
"label": status.repository.label(),
"observed": status.observed.as_str(),
"planned": status.blocked.is_none(),
"moves": plans.get(&repository).cloned().unwrap_or_default(),
})
})
.collect::<Vec<_>>()
})))
}
#[derive(Debug, Deserialize)]
pub struct VersionsQuery {
pub workspace: String,
pub repository: String,
pub path: String,
}
async fn versions(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
axum::extract::Query(query): axum::extract::Query<VersionsQuery>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace =
WorkspaceId::parse(&query.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let repository =
RepositoryId::parse(&query.repository).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let path = query.path.clone();
let state = machine.clone();
let versions = tokio::task::spawn_blocking(move || {
super::versions::versions(&state, &peer, &workspace, &repository, &path)
})
.await
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, format!("read error: {e}")))?
.map_err(|e| bad(StatusCode::SERVICE_UNAVAILABLE, e))?;
Ok(Json(serde_json::to_value(versions).map_err(|e| {
bad(StatusCode::INTERNAL_SERVER_ERROR, format!("cannot encode versions: {e}"))
})?))
}
#[derive(Debug, Deserialize)]
pub struct ResolveRequest {
pub workspace: String,
pub repository: String,
pub path: String,
pub keep: String,
}
async fn resolve(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<ResolveRequest>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace =
WorkspaceId::parse(&body.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let repository =
RepositoryId::parse(&body.repository).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let keep = match body.keep.as_str() {
"here" => super::conflict::Keep::Here,
"there" => super::conflict::Keep::There,
other => {
return Err(bad(
StatusCode::BAD_REQUEST,
format!("'{other}' is not a side to keep — 'here' or 'there'"),
))
}
};
super::conflict::resolve(&machine, &peer, &workspace, &repository, &body.path, keep)
.await
.map_err(|e| bad(StatusCode::CONFLICT, e))?;
Ok(Json(serde_json::json!({ "resolved": body.path })))
}
async fn services(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
) -> Result<Json<wsb::peer::model::PeerServices>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let switches = machine
.store
.services(&peer)
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?;
let mut listed: std::collections::BTreeSet<Service> = Service::BUILT_IN.into_iter().collect();
listed.extend(switches.keys().cloned());
for registration in machine
.plugins
.running()
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?
{
listed.extend(registration.services.into_iter().map(Service::Plugin));
}
Ok(Json(wsb::peer::model::PeerServices {
peer: peer.to_string(),
services: listed
.iter()
.map(|service| wsb::peer::model::ServiceSwitch {
service: service.as_str().to_string(),
switch: switches
.get(service)
.copied()
.unwrap_or_else(|| Switch::default_for(service)),
describes: service.describe(),
})
.collect(),
}))
}
#[derive(Debug, Deserialize)]
pub struct SwitchBody {
pub switch: String,
}
async fn set_service(
State(machine): State<Arc<MachineState>>,
Path((peer, service)): Path<(String, String)>,
Json(body): Json<SwitchBody>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let service = Service::parse(&service).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let switch = Switch::parse(&body.switch).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
if machine
.store
.peer(&peer)
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?
.is_none()
{
return Err(bad(StatusCode::NOT_FOUND, format!("{peer} is not a paired peer")));
}
machine
.store
.set_service(&peer, service.clone(), switch, super::service::now())
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?;
Ok(Json(serde_json::json!({
"service": service.as_str(),
"switch": switch.as_str(),
})))
}
#[derive(Debug, Deserialize)]
pub struct ModeBody {
pub workspace: String,
pub mode: String,
}
async fn set_mode(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<ModeBody>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace =
WorkspaceId::parse(&body.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let mode = SyncMode::parse(&body.mode).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
if machine
.store
.peer(&peer)
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?
.is_none()
{
return Err(bad(StatusCode::NOT_FOUND, format!("{peer} is not a paired peer")));
}
machine
.store
.set_mode(&peer, &workspace, mode, super::service::now())
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, e))?;
Ok(Json(serde_json::json!({ "mode": mode.as_str() })))
}
#[derive(Debug, Deserialize)]
pub struct AlignBody {
pub workspace: String,
pub repository: String,
pub commit: String,
#[serde(default)]
pub discard_changes: bool,
#[serde(default)]
pub discard_commits: bool,
}
async fn align(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<AlignBody>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace =
WorkspaceId::parse(&body.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let repository =
RepositoryId::parse(&body.repository).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let observed = machine.statuses(&peer, &workspace);
let theirs = observed
.iter()
.find(|status| status.repository == repository)
.and_then(|status| status.head.clone())
.ok_or_else(|| {
bad(
StatusCode::CONFLICT,
format!(
"nothing has been observed about {repository} with this machine yet, so \
there is no commit of theirs to move to"
),
)
})?;
if theirs != body.commit {
return Err(bad(
StatusCode::CONFLICT,
format!(
"that machine is on {theirs}, not {}. It moved since the page was drawn — \
look again before deciding.",
body.commit
),
));
}
let state = machine.clone();
let outcome = tokio::task::spawn_blocking(move || {
align_repository(&state, &workspace, &repository, &theirs, body.discard_changes, body.discard_commits)
})
.await
.map_err(|e| bad(StatusCode::INTERNAL_SERVER_ERROR, format!("align error: {e}")))?;
match outcome {
Ok(value) => Ok(Json(value)),
Err((code, message)) => Err(bad(code, message)),
}
}
fn align_repository(
machine: &Arc<MachineState>,
workspace: &WorkspaceId,
repository: &RepositoryId,
commit: &str,
discard_changes: bool,
discard_commits: bool,
) -> Result<serde_json::Value, (StatusCode, String)> {
let (_, local) = futures::executor::block_on(machine.continuity(workspace))
.map_err(|e| (StatusCode::CONFLICT, e))?
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("no worktree open here is {workspace}")))?;
let repo = local
.repository(repository)
.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
format!("{repository} is not a repository of this workspace"),
)
})?;
let opened = wsb::git_backend::open_repo(&repo.workdir)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
let present = wsb::git_backend::has_commit(&opened, commit)
.map_err(|e| (StatusCode::BAD_REQUEST, format!("{e}")))?;
if !present {
let _ = wsb::git_backend::fetch(&opened, Some("origin"));
let present = wsb::git_backend::has_commit(&opened, commit)
.map_err(|e| (StatusCode::BAD_REQUEST, format!("{e}")))?;
if !present {
return Err((
StatusCode::CONFLICT,
format!(
"{commit} is not in this repository and origin does not have it either. \
That machine has not pushed it, so there is nothing here to move to."
),
));
}
}
let cost = wsb::git_backend::move_cost(&opened, commit)
.map_err(|e| (StatusCode::CONFLICT, format!("{e}")))?;
use wsb::git_backend::MoveCost;
if matches!(cost, MoveCost::Already) {
return Ok(serde_json::json!({ "moved": false, "reason": "already on that commit" }));
}
if let MoveCost::Discards { commits } = cost {
if !discard_commits {
return Err((
StatusCode::CONFLICT,
format!(
"moving to {commit} takes {commits} commit(s) off this branch. They would \
survive only in the reflog. Say so explicitly to proceed."
),
));
}
}
let dirty = wsb::fleet::git::GitOps::get_uncommitted_own_files(&opened)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
if !dirty.is_empty() && !discard_changes {
return Err((
StatusCode::CONFLICT,
format!(
"there are uncommitted changes here that moving would overwrite: {}. \
Say so explicitly to proceed.",
wsb::fleet::git::GitOps::describe_uncommitted(&dirty)
),
));
}
wsb::git_backend::move_branch_to(&opened, commit)
.map_err(|e| (StatusCode::CONFLICT, format!("{e}")))?;
Ok(serde_json::json!({
"moved": true,
"commit": commit,
"discarded_changes": dirty,
"discarded_commits": match cost {
MoveCost::Discards { commits } => commits,
_ => 0,
},
}))
}
async fn deltas(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
axum::extract::Query(query): axum::extract::Query<StatusQuery>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let workspace =
WorkspaceId::parse(&query.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let plans = machine.plans(&peer, &workspace);
let statuses = machine.statuses(&peer, &workspace);
let desired = machine
.store
.desired(&peer, &workspace)
.unwrap_or(Desired::Disabled);
let files = machine
.store
.service(&peer, Service::Files)
.unwrap_or_else(|_| Switch::default_for(&Service::Files));
let mode = machine.store.mode(&peer, &workspace).unwrap_or_default();
let liveness = machine.peers.liveness(&peer);
Ok(Json(serde_json::json!({
"peer": peer.to_string(),
"workspace": workspace.to_string(),
"peer_state": liveness.state.as_str(),
"desired": desired.as_str(),
"mode": mode.as_str(),
"files": files.as_str(),
"continuity": Continuity::of(files, desired, mode).as_str(),
"repositories": statuses
.iter()
.map(|status| {
let repository = status.repository.as_str().to_string();
serde_json::json!({
"repository": repository,
"label": status.repository.label(),
"observed": status.observed.as_str(),
"blocked": status.blocked,
"undecided": status.undecided,
"conflicts": status.conflicts,
"compared": status.blocked.is_none(),
"deltas": plans.get(&repository).cloned().unwrap_or_default(),
})
})
.collect::<Vec<_>>(),
})))
}
#[derive(Debug, Deserialize)]
pub struct DeltaBody {
pub workspace: String,
pub repository: String,
pub path: String,
#[serde(default)]
pub keep: Option<String>,
}
fn delta_target(body: &DeltaBody) -> Result<(WorkspaceId, RepositoryId), Failure> {
let workspace =
WorkspaceId::parse(&body.workspace).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let repository =
RepositoryId::parse(&body.repository).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
Ok((workspace, repository))
}
async fn decide(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<DeltaBody>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let (workspace, repository) = delta_target(&body)?;
let keep = match body.keep.as_deref() {
Some("here") => super::conflict::Keep::Here,
Some("there") => super::conflict::Keep::There,
other => {
return Err(bad(
StatusCode::BAD_REQUEST,
format!(
"'{}' is not a side to keep — 'here' or 'there'",
other.unwrap_or("")
),
))
}
};
super::conflict::decide(&machine, &peer, &workspace, &repository, &body.path, keep)
.await
.map_err(|e| bad(StatusCode::CONFLICT, e))?;
Ok(Json(serde_json::json!({
"decided": body.path,
"keep": body.keep,
})))
}
async fn dismiss(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<DeltaBody>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let (workspace, repository) = delta_target(&body)?;
super::conflict::dismiss(&machine, &peer, &workspace, &repository, &body.path)
.await
.map_err(|e| bad(StatusCode::CONFLICT, e))?;
Ok(Json(serde_json::json!({ "dismissed": body.path })))
}
async fn reconsider(
State(machine): State<Arc<MachineState>>,
Path(peer): Path<String>,
Json(body): Json<DeltaBody>,
) -> Result<Json<serde_json::Value>, Failure> {
let peer = PeerId::parse(&peer).map_err(|e| bad(StatusCode::BAD_REQUEST, e))?;
let (workspace, repository) = delta_target(&body)?;
super::conflict::reconsider(&machine, &peer, &workspace, &repository, &body.path)
.await
.map_err(|e| bad(StatusCode::CONFLICT, e))?;
Ok(Json(serde_json::json!({ "reconsidered": body.path })))
}