use crate::error::Result;
use crate::http::HttpClient;
use reqwest::Method;
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct WorktreeApi {
http: HttpClient,
}
impl WorktreeApi {
pub fn new(http: HttpClient) -> Self {
Self { http }
}
pub async fn create(&self, req: &CreateWorktreeRequest) -> Result<Worktree> {
let body = serde_json::to_value(req)?;
self.http
.request_json(Method::POST, "/experimental/worktree", Some(body))
.await
}
pub async fn list(&self) -> Result<Vec<Worktree>> {
self.http
.request_json(Method::GET, "/experimental/worktree", None)
.await
}
pub async fn delete(&self, req: &DeleteWorktreeRequest) -> Result<()> {
let body = serde_json::to_value(req)?;
self.http
.request_empty(Method::DELETE, "/experimental/worktree", Some(body))
.await
}
pub async fn reset(&self, req: &ResetWorktreeRequest) -> Result<WorktreeResetResponse> {
let body = serde_json::to_value(req)?;
self.http
.request_json(Method::POST, "/experimental/worktree/reset", Some(body))
.await
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateWorktreeRequest {
pub branch: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Worktree {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(default)]
pub is_main: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteWorktreeRequest {
pub path: String,
#[serde(default)]
pub force: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResetWorktreeRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorktreeResetResponse {
#[serde(default)]
pub success: bool,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::HttpConfig;
use std::time::Duration;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn test_worktree_delete() {
let mock_server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/experimental/worktree"))
.respond_with(ResponseTemplate::new(204))
.mount(&mock_server)
.await;
let client = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let api = WorktreeApi::new(client);
let req = DeleteWorktreeRequest {
path: "/path/to/worktree".to_string(),
force: false,
};
api.delete(&req).await.unwrap();
}
#[tokio::test]
async fn test_worktree_reset() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/experimental/worktree/reset"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"success": true
})))
.mount(&mock_server)
.await;
let client = HttpClient::new(HttpConfig {
base_url: mock_server.uri(),
directory: None,
timeout: Duration::from_secs(30),
})
.unwrap();
let api = WorktreeApi::new(client);
let req = ResetWorktreeRequest {
path: Some("/path/to/worktree".to_string()),
mode: Some("hard".to_string()),
target: Some("HEAD~1".to_string()),
};
let response = api.reset(&req).await.unwrap();
assert!(response.success);
}
#[test]
fn test_delete_worktree_request() {
let req = DeleteWorktreeRequest {
path: "/tmp/worktree".to_string(),
force: true,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("force"));
assert!(json.contains("true"));
}
#[test]
fn test_reset_worktree_request() {
let req = ResetWorktreeRequest {
path: None,
mode: Some("soft".to_string()),
target: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("soft"));
}
}