feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
12
crates/application/src/git/mod.rs
Normal file
12
crates/application/src/git/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
//! Git use cases (ARCHITECTURE §6, L8). Local Git operations orchestrated over
|
||||
//! the [`domain::ports::GitPort`]: status, staging, commit, branches, checkout
|
||||
//! and log. State-changing operations announce [`domain::DomainEvent::GitStateChanged`].
|
||||
|
||||
mod usecases;
|
||||
|
||||
pub use usecases::{
|
||||
GitBranches, GitBranchesInput, GitBranchesOutput, GitCheckout, GitCheckoutInput, GitCommit,
|
||||
GitCommitInput, GitCommitOutput, GitGraph, GitGraphInput, GitGraphOutput, GitInit, GitInitInput,
|
||||
GitLog, GitLogInput, GitLogOutput, GitStage, GitStagePathInput, GitStatus, GitStatusInput,
|
||||
GitStatusOutput, GitUnstage,
|
||||
};
|
||||
382
crates/application/src/git/usecases.rs
Normal file
382
crates/application/src/git/usecases.rs
Normal file
@ -0,0 +1,382 @@
|
||||
//! Git use cases (ARCHITECTURE §6, L8). Thin orchestration over the [`GitPort`]:
|
||||
//! validate the root, call the port, and (for state-changing operations) announce
|
||||
//! [`DomainEvent::GitStateChanged`] so the UI can refresh.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{EventBus, GitCommitInfo, GitFileStatus, GitPort, GraphCommit};
|
||||
use domain::{DomainEvent, ProjectId, ProjectPath};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
/// Parses a raw root string into a validated [`ProjectPath`].
|
||||
fn parse_root(root: &str) -> Result<ProjectPath, AppError> {
|
||||
ProjectPath::new(root).map_err(|e| AppError::Invalid(e.to_string()))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitStatus
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`GitStatus::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitStatusInput {
|
||||
/// Absolute repository root.
|
||||
pub root: String,
|
||||
}
|
||||
|
||||
/// Output of [`GitStatus::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitStatusOutput {
|
||||
/// Changed paths (staged flag per path).
|
||||
pub entries: Vec<GitFileStatus>,
|
||||
}
|
||||
|
||||
/// Reports the working-tree status of a repository.
|
||||
pub struct GitStatus {
|
||||
git: Arc<dyn GitPort>,
|
||||
}
|
||||
|
||||
impl GitStatus {
|
||||
/// Builds the use case from the [`GitPort`].
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>) -> Self {
|
||||
Self { git }
|
||||
}
|
||||
|
||||
/// Returns the status.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::Invalid`] for a non-absolute root,
|
||||
/// - [`AppError::Git`] if the repo is missing or the operation fails.
|
||||
pub async fn execute(&self, input: GitStatusInput) -> Result<GitStatusOutput, AppError> {
|
||||
let root = parse_root(&input.root)?;
|
||||
let entries = self.git.status(&root).await?;
|
||||
Ok(GitStatusOutput { entries })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitStage / GitUnstage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`GitStage::execute`] / [`GitUnstage::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitStagePathInput {
|
||||
/// Absolute repository root.
|
||||
pub root: String,
|
||||
/// Repo-relative path to (un)stage.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Stages a path (adds it to the index).
|
||||
pub struct GitStage {
|
||||
git: Arc<dyn GitPort>,
|
||||
}
|
||||
|
||||
impl GitStage {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>) -> Self {
|
||||
Self { git }
|
||||
}
|
||||
|
||||
/// Stages the path.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] for a bad root, [`AppError::Git`] on failure.
|
||||
pub async fn execute(&self, input: GitStagePathInput) -> Result<(), AppError> {
|
||||
let root = parse_root(&input.root)?;
|
||||
self.git.stage(&root, &input.path).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Unstages a path (resets it to its HEAD state, or removes it when unborn).
|
||||
pub struct GitUnstage {
|
||||
git: Arc<dyn GitPort>,
|
||||
}
|
||||
|
||||
impl GitUnstage {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>) -> Self {
|
||||
Self { git }
|
||||
}
|
||||
|
||||
/// Unstages the path.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] for a bad root, [`AppError::Git`] on failure.
|
||||
pub async fn execute(&self, input: GitStagePathInput) -> Result<(), AppError> {
|
||||
let root = parse_root(&input.root)?;
|
||||
self.git.unstage(&root, &input.path).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitCommit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`GitCommit::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitCommitInput {
|
||||
/// The project (for the emitted event).
|
||||
pub project_id: ProjectId,
|
||||
/// Absolute repository root.
|
||||
pub root: String,
|
||||
/// Commit message.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Output of [`GitCommit::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitCommitOutput {
|
||||
/// The created commit.
|
||||
pub commit: GitCommitInfo,
|
||||
}
|
||||
|
||||
/// Commits the staged index, announcing [`DomainEvent::GitStateChanged`].
|
||||
pub struct GitCommit {
|
||||
git: Arc<dyn GitPort>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl GitCommit {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>, events: Arc<dyn EventBus>) -> Self {
|
||||
Self { git, events }
|
||||
}
|
||||
|
||||
/// Creates the commit.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::Invalid`] for a bad root or an empty message,
|
||||
/// - [`AppError::Git`] on failure.
|
||||
pub async fn execute(&self, input: GitCommitInput) -> Result<GitCommitOutput, AppError> {
|
||||
if input.message.trim().is_empty() {
|
||||
return Err(AppError::Invalid("commit message is empty".to_owned()));
|
||||
}
|
||||
let root = parse_root(&input.root)?;
|
||||
let commit = self.git.commit(&root, &input.message).await?;
|
||||
self.events.publish(DomainEvent::GitStateChanged {
|
||||
project_id: input.project_id,
|
||||
});
|
||||
Ok(GitCommitOutput { commit })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitBranches
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`GitBranches::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitBranchesInput {
|
||||
/// Absolute repository root.
|
||||
pub root: String,
|
||||
}
|
||||
|
||||
/// Output of [`GitBranches::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitBranchesOutput {
|
||||
/// All local branches.
|
||||
pub branches: Vec<String>,
|
||||
/// The current branch (`None` when detached or unborn).
|
||||
pub current: Option<String>,
|
||||
}
|
||||
|
||||
/// Lists local branches and the current one.
|
||||
pub struct GitBranches {
|
||||
git: Arc<dyn GitPort>,
|
||||
}
|
||||
|
||||
impl GitBranches {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>) -> Self {
|
||||
Self { git }
|
||||
}
|
||||
|
||||
/// Lists branches + current.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] for a bad root, [`AppError::Git`] on failure.
|
||||
pub async fn execute(&self, input: GitBranchesInput) -> Result<GitBranchesOutput, AppError> {
|
||||
let root = parse_root(&input.root)?;
|
||||
let branches = self.git.branches(&root).await?;
|
||||
let current = self.git.current_branch(&root).await?;
|
||||
Ok(GitBranchesOutput { branches, current })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitCheckout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`GitCheckout::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitCheckoutInput {
|
||||
/// The project (for the emitted event).
|
||||
pub project_id: ProjectId,
|
||||
/// Absolute repository root.
|
||||
pub root: String,
|
||||
/// Branch to check out.
|
||||
pub branch: String,
|
||||
}
|
||||
|
||||
/// Checks out a branch, announcing [`DomainEvent::GitStateChanged`].
|
||||
pub struct GitCheckout {
|
||||
git: Arc<dyn GitPort>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl GitCheckout {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>, events: Arc<dyn EventBus>) -> Self {
|
||||
Self { git, events }
|
||||
}
|
||||
|
||||
/// Checks out the branch.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] for a bad root, [`AppError::Git`] on failure.
|
||||
pub async fn execute(&self, input: GitCheckoutInput) -> Result<(), AppError> {
|
||||
let root = parse_root(&input.root)?;
|
||||
self.git.checkout(&root, &input.branch).await?;
|
||||
self.events.publish(DomainEvent::GitStateChanged {
|
||||
project_id: input.project_id,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitLog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`GitLog::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitLogInput {
|
||||
/// Absolute repository root.
|
||||
pub root: String,
|
||||
/// Maximum number of commits to return.
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
/// Output of [`GitLog::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitLogOutput {
|
||||
/// Recent commits, newest first.
|
||||
pub commits: Vec<GitCommitInfo>,
|
||||
}
|
||||
|
||||
/// Returns the recent commit log.
|
||||
pub struct GitLog {
|
||||
git: Arc<dyn GitPort>,
|
||||
}
|
||||
|
||||
impl GitLog {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>) -> Self {
|
||||
Self { git }
|
||||
}
|
||||
|
||||
/// Returns the log.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] for a bad root, [`AppError::Git`] on failure.
|
||||
pub async fn execute(&self, input: GitLogInput) -> Result<GitLogOutput, AppError> {
|
||||
let root = parse_root(&input.root)?;
|
||||
let commits = self.git.log(&root, input.limit).await?;
|
||||
Ok(GitLogOutput { commits })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitInit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`GitInit::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitInitInput {
|
||||
/// The project (for the emitted event).
|
||||
pub project_id: ProjectId,
|
||||
/// Absolute repository root.
|
||||
pub root: String,
|
||||
}
|
||||
|
||||
/// Initialises a repository at the project root, announcing
|
||||
/// [`DomainEvent::GitStateChanged`].
|
||||
pub struct GitInit {
|
||||
git: Arc<dyn GitPort>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl GitInit {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>, events: Arc<dyn EventBus>) -> Self {
|
||||
Self { git, events }
|
||||
}
|
||||
|
||||
/// Initialises the repository.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] for a bad root, [`AppError::Git`] on failure.
|
||||
pub async fn execute(&self, input: GitInitInput) -> Result<(), AppError> {
|
||||
let root = parse_root(&input.root)?;
|
||||
self.git.init(&root).await?;
|
||||
self.events.publish(DomainEvent::GitStateChanged {
|
||||
project_id: input.project_id,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitGraph
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`GitGraph::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitGraphInput {
|
||||
/// Absolute repository root.
|
||||
pub root: String,
|
||||
/// Maximum number of commits to return.
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
/// Output of [`GitGraph::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitGraphOutput {
|
||||
/// Graph commits (topological + time order), newest first.
|
||||
pub commits: Vec<GraphCommit>,
|
||||
}
|
||||
|
||||
/// Returns the full commit graph for all local branches.
|
||||
pub struct GitGraph {
|
||||
git: Arc<dyn GitPort>,
|
||||
}
|
||||
|
||||
impl GitGraph {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(git: Arc<dyn GitPort>) -> Self {
|
||||
Self { git }
|
||||
}
|
||||
|
||||
/// Returns the graph.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Invalid`] for a bad root, [`AppError::Git`] on failure.
|
||||
pub async fn execute(&self, input: GitGraphInput) -> Result<GitGraphOutput, AppError> {
|
||||
let root = parse_root(&input.root)?;
|
||||
let commits = self.git.log_graph(&root, input.limit).await?;
|
||||
Ok(GitGraphOutput { commits })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user