Story 42: Deterministic worktree management via REST/MCP API
Add REST and MCP endpoints for creating, listing, and removing worktrees. Includes worktree lifecycle management and cleanup operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Tags)]
|
||||
@@ -117,6 +118,8 @@ struct CreateStoryPayload {
|
||||
pub name: String,
|
||||
pub user_story: Option<String>,
|
||||
pub acceptance_criteria: Option<Vec<String>>,
|
||||
/// If true, git-add and git-commit the new story file to the current branch.
|
||||
pub commit: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
@@ -556,11 +559,13 @@ impl WorkflowApi {
|
||||
payload: Json<CreateStoryPayload>,
|
||||
) -> OpenApiResult<Json<CreateStoryResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let commit = payload.0.commit.unwrap_or(false);
|
||||
let story_id = create_story_file(
|
||||
&root,
|
||||
&payload.0.name,
|
||||
payload.0.user_story.as_deref(),
|
||||
payload.0.acceptance_criteria.as_deref(),
|
||||
commit,
|
||||
)
|
||||
.map_err(bad_request)?;
|
||||
|
||||
@@ -597,11 +602,15 @@ impl WorkflowApi {
|
||||
}
|
||||
|
||||
/// Shared create-story logic used by both the OpenApi and MCP handlers.
|
||||
///
|
||||
/// When `commit` is `true`, the new story file is git-added and committed to
|
||||
/// the current branch immediately after creation.
|
||||
pub fn create_story_file(
|
||||
root: &std::path::Path,
|
||||
name: &str,
|
||||
user_story: Option<&str>,
|
||||
acceptance_criteria: Option<&[String]>,
|
||||
commit: bool,
|
||||
) -> Result<String, String> {
|
||||
let story_number = next_story_number(root)?;
|
||||
let slug = slugify_name(name);
|
||||
@@ -658,9 +667,43 @@ pub fn create_story_file(
|
||||
fs::write(&filepath, &content)
|
||||
.map_err(|e| format!("Failed to write story file: {e}"))?;
|
||||
|
||||
if commit {
|
||||
git_commit_story_file(root, &filepath, name)?;
|
||||
}
|
||||
|
||||
Ok(story_id)
|
||||
}
|
||||
|
||||
/// Git-add and git-commit a newly created story file to the current branch.
|
||||
fn git_commit_story_file(
|
||||
root: &std::path::Path,
|
||||
filepath: &std::path::Path,
|
||||
name: &str,
|
||||
) -> Result<(), String> {
|
||||
let output = Command::new("git")
|
||||
.args(["add", &filepath.to_string_lossy()])
|
||||
.current_dir(root)
|
||||
.output()
|
||||
.map_err(|e| format!("git add: {e}"))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("git add failed: {stderr}"));
|
||||
}
|
||||
|
||||
let msg = format!("Add story: {name}");
|
||||
let output = Command::new("git")
|
||||
.args(["commit", "-m", &msg])
|
||||
.current_dir(root)
|
||||
.output()
|
||||
.map_err(|e| format!("git commit: {e}"))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("git commit failed: {stderr}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn slugify_name(name: &str) -> String {
|
||||
let slug: String = name
|
||||
.chars()
|
||||
|
||||
Reference in New Issue
Block a user