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:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

View File

@ -0,0 +1,275 @@
//! [`Git2Repository`] — local Git adapter implementing the [`GitPort`] port via
//! libgit2 (`git2`), ARCHITECTURE §5, L8.
//!
//! Scope is **local** operations (status, stage/unstage, commit, branches,
//! checkout, log, init). Network operations (`pull`/`push`) need remote +
//! credential handling and are deferred to L9 (`RemoteGitRepository` over SSH/WSL
//! and credential callbacks); here they return a clear [`GitError::Operation`].
//!
//! # Async & `Send`
//!
//! [`GitPort`] is `#[async_trait]`, but libgit2 is synchronous and its handles
//! ([`git2::Repository`]) are `!Send`. Each method opens the repository, does all
//! its work, and drops it **within a single poll** — there is no `.await` while a
//! repo/handle is alive — so the returned futures are `Send` and the adapter is
//! safe behind `Arc<dyn GitPort>` on the multi-threaded runtime.
use async_trait::async_trait;
use git2::{BranchType, ErrorCode, Repository, Status, StatusOptions};
use std::collections::HashMap;
use domain::ports::{GitCommitInfo, GitError, GitFileStatus, GitPort, GraphCommit};
use domain::project::ProjectPath;
/// Local Git adapter backed by libgit2.
#[derive(Clone, Default)]
pub struct Git2Repository;
impl Git2Repository {
/// Builds the adapter (stateless; a repository is opened per call from the
/// project root, so one instance serves every project).
#[must_use]
pub fn new() -> Self {
Self
}
}
/// Maps a libgit2 error to a domain [`GitError`] (its message, not the raw type).
fn op(e: git2::Error) -> GitError {
GitError::Operation(e.message().to_owned())
}
/// Opens the repository at `root`, distinguishing "not a repo" from other errors.
fn open(root: &ProjectPath) -> Result<Repository, GitError> {
Repository::open(root.as_str()).map_err(|e| {
if e.code() == ErrorCode::NotFound {
GitError::NotFound
} else {
op(e)
}
})
}
/// Index-side status flags (a path with any of these has staged changes).
const STAGED: Status = Status::INDEX_NEW
.union(Status::INDEX_MODIFIED)
.union(Status::INDEX_DELETED)
.union(Status::INDEX_RENAMED)
.union(Status::INDEX_TYPECHANGE);
#[async_trait]
impl GitPort for Git2Repository {
async fn init(&self, root: &ProjectPath) -> Result<(), GitError> {
Repository::init(root.as_str()).map_err(op)?;
Ok(())
}
async fn status(&self, root: &ProjectPath) -> Result<Vec<GitFileStatus>, GitError> {
let repo = open(root)?;
let mut opts = StatusOptions::new();
opts.include_untracked(true).recurse_untracked_dirs(true);
let statuses = repo.statuses(Some(&mut opts)).map_err(op)?;
let mut out = Vec::new();
for entry in statuses.iter() {
let s = entry.status();
if s.is_ignored() {
continue;
}
if let Some(path) = entry.path() {
out.push(GitFileStatus {
path: path.to_owned(),
staged: s.intersects(STAGED),
});
}
}
Ok(out)
}
async fn stage(&self, root: &ProjectPath, path: &str) -> Result<(), GitError> {
let repo = open(root)?;
let mut index = repo.index().map_err(op)?;
index.add_path(std::path::Path::new(path)).map_err(op)?;
index.write().map_err(op)?;
Ok(())
}
async fn unstage(&self, root: &ProjectPath, path: &str) -> Result<(), GitError> {
let repo = open(root)?;
match repo.head() {
// Reset the path in the index back to its HEAD state.
Ok(head) => {
let obj = head.peel(git2::ObjectType::Commit).map_err(op)?;
repo.reset_default(Some(&obj), [path]).map_err(op)?;
}
// Unborn HEAD (no commit yet): "unstage" means drop it from the index.
Err(_) => {
let mut index = repo.index().map_err(op)?;
index.remove_path(std::path::Path::new(path)).map_err(op)?;
index.write().map_err(op)?;
}
}
Ok(())
}
async fn commit(&self, root: &ProjectPath, message: &str) -> Result<GitCommitInfo, GitError> {
let repo = open(root)?;
let mut index = repo.index().map_err(op)?;
let tree_oid = index.write_tree().map_err(op)?;
let tree = repo.find_tree(tree_oid).map_err(op)?;
// Prefer the configured identity; fall back to a stable local one so a
// fresh repo with no user.name/email can still commit.
let sig = repo
.signature()
.or_else(|_| git2::Signature::now("IdeA", "idea@localhost"))
.map_err(op)?;
let parent = match repo.head() {
Ok(head) => Some(head.peel_to_commit().map_err(op)?),
Err(_) => None,
};
let parents: Vec<&git2::Commit> = parent.iter().collect();
let oid = repo
.commit(Some("HEAD"), &sig, &sig, message, &tree, &parents)
.map_err(op)?;
Ok(GitCommitInfo {
hash: oid.to_string(),
summary: message.lines().next().unwrap_or("").to_owned(),
})
}
async fn branches(&self, root: &ProjectPath) -> Result<Vec<String>, GitError> {
let repo = open(root)?;
let mut names = Vec::new();
for branch in repo.branches(Some(BranchType::Local)).map_err(op)? {
let (branch, _) = branch.map_err(op)?;
if let Some(name) = branch.name().map_err(op)? {
names.push(name.to_owned());
}
}
Ok(names)
}
async fn current_branch(&self, root: &ProjectPath) -> Result<Option<String>, GitError> {
let repo = open(root)?;
let head = match repo.head() {
Ok(head) => head,
// Unborn branch (no commits yet): no current branch to report.
Err(e) if e.code() == ErrorCode::UnbornBranch => return Ok(None),
Err(e) => return Err(op(e)),
};
// A detached HEAD (or a non-branch ref) has no branch name.
Ok(if head.is_branch() {
head.shorthand().map(ToOwned::to_owned)
} else {
None
})
}
async fn checkout(&self, root: &ProjectPath, branch: &str) -> Result<(), GitError> {
let repo = open(root)?;
let obj = repo.revparse_single(branch).map_err(op)?;
repo.checkout_tree(&obj, None).map_err(op)?;
repo.set_head(&format!("refs/heads/{branch}")).map_err(op)?;
Ok(())
}
async fn log(
&self,
root: &ProjectPath,
limit: usize,
) -> Result<Vec<GitCommitInfo>, GitError> {
let repo = open(root)?;
let mut revwalk = repo.revwalk().map_err(op)?;
// No commits yet ⇒ nothing to walk.
if revwalk.push_head().is_err() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for oid in revwalk.take(limit) {
let oid = oid.map_err(op)?;
let commit = repo.find_commit(oid).map_err(op)?;
out.push(GitCommitInfo {
hash: oid.to_string(),
summary: commit.summary().unwrap_or("").to_owned(),
});
}
Ok(out)
}
async fn log_graph(
&self,
root: &ProjectPath,
limit: usize,
) -> Result<Vec<GraphCommit>, GitError> {
let repo = open(root)?;
// Build a map from OID → ref short-names for label attachment.
let mut ref_map: HashMap<git2::Oid, Vec<String>> = HashMap::new();
if let Ok(references) = repo.references() {
for reference in references.flatten() {
// Only handle symbolic and direct refs; skip errors.
let target_oid = reference.resolve().ok().and_then(|r| r.target());
if let Some(oid) = target_oid {
let label = match reference.shorthand() {
Some(name) => {
// Distinguish tags from branches by prefix.
if reference.is_tag() {
format!("tag: {name}")
} else {
name.to_owned()
}
}
None => continue,
};
ref_map.entry(oid).or_default().push(label);
}
}
}
let mut revwalk = repo.revwalk().map_err(op)?;
revwalk
.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
.map_err(op)?;
// Push all local branches; if the repo is empty this produces no OIDs.
let _ = revwalk.push_glob("refs/heads/*");
let mut out = Vec::new();
for oid_result in revwalk.take(limit) {
let oid = oid_result.map_err(op)?;
let commit = repo.find_commit(oid).map_err(op)?;
let parents = commit
.parent_ids()
.map(|p| p.to_string())
.collect::<Vec<_>>();
let author = commit.author().name().unwrap_or("").to_owned();
let timestamp = commit.time().seconds();
let refs = ref_map.get(&oid).cloned().unwrap_or_default();
out.push(GraphCommit {
hash: oid.to_string(),
summary: commit.summary().unwrap_or("").to_owned(),
parents,
refs,
author,
timestamp,
});
}
Ok(out)
}
async fn pull(&self, _root: &ProjectPath) -> Result<(), GitError> {
Err(GitError::Operation(
"pull requires remote/credential configuration (L9)".to_owned(),
))
}
async fn push(&self, _root: &ProjectPath) -> Result<(), GitError> {
Err(GitError::Operation(
"push requires remote/credential configuration (L9)".to_owned(),
))
}
}