Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace

coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.85.0"
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate coverage
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: lcov.info
fail_ci_if_error: false

doc:
name: Rustdoc
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ A fast, manifest-driven development toolkit written in Rust.

## Status

**Phase 2.6** — complete.
**Phase 2.7** — complete.

- **Phase 1:** Multi-repo management — `east init`, `east update`, `east list`, `east status`, `east manifest --resolve`
- **Phase 2:** Configuration & extension commands — `east config`, manifest-declared commands, PATH-based discovery, template engine
- **Phase 2.6:** Topology correction — manifest lives in a real git repo inside the workspace
- **Phase 2.7:** Quality improvements — doc-tests, unified miette diagnostics, CI coverage, configurable concurrency

## Quick Start

Expand Down
5 changes: 3 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@

## 状态

**Phase 2.6** — 已完成。
**Phase 2.7** — 已完成。

- **Phase 1:** 多仓库管理 — `east init`、`east update`、`east list`、`east status`、`east manifest --resolve`
- **Phase 2:** 配置与扩展命令 — `east config`、manifest 声明命令、PATH 发现、模板引擎
- **Phase 2.6:** 拓扑修正 — manifest 住在 workspace 内的真实 git 仓库中
- **Phase 2.6:** 拓扑修正 — manifest 存放于 workspace 内的真实 git 仓库中
- **Phase 2.7:** 质量改进 — doc-tests、统一 miette 诊断、CI 覆盖率、可配置并发数

## 快速开始

Expand Down
17 changes: 14 additions & 3 deletions crates/east-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use miette::{IntoDiagnostic, WrapErr, bail};
use tokio::sync::Semaphore;
use tracing::info;

/// Maximum concurrent git operations.
const MAX_CONCURRENT_GIT: usize = 8;
/// Default maximum concurrent git operations.
const DEFAULT_CONCURRENT_GIT: usize = 8;

/// A fast, manifest-driven development toolkit.
#[derive(Parser)]
Expand Down Expand Up @@ -471,7 +471,18 @@ async fn do_update(
.template(" {spinner:.green} {msg}")
.expect("valid template");

let semaphore = std::sync::Arc::new(Semaphore::new(MAX_CONCURRENT_GIT));
// Read update.jobs from config, falling back to DEFAULT_CONCURRENT_GIT.
let max_jobs = {
let provider = DefaultPathProvider::new(Some(workspace_root.to_path_buf()));
let cfg = Config::load_with_provider(&provider).into_diagnostic()?;
cfg.get("update.jobs")
.and_then(ConfigValue::as_i64)
.map_or(DEFAULT_CONCURRENT_GIT, |n| {
let n = n.max(1);
usize::try_from(n).unwrap_or(usize::MAX)
})
};
let semaphore = std::sync::Arc::new(Semaphore::new(max_jobs));
let overall = std::sync::Arc::new(overall);
let force_set: std::sync::Arc<std::collections::HashSet<String>> =
std::sync::Arc::new(force_projects.iter().cloned().collect());
Expand Down
19 changes: 19 additions & 0 deletions crates/east-command/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ pub struct ResolvedCommand {
///
/// Holds manifest-declared and PATH-discovered commands with collision
/// resolution: manifest commands always win over PATH commands.
///
/// # Example
///
/// ```
/// use east_manifest::Manifest;
/// use east_command::registry::CommandRegistry;
///
/// let yaml = r#"
/// version: 1
/// commands:
/// - name: hello
/// help: "Say hello"
/// exec: "echo hello"
/// "#;
/// let manifest = Manifest::from_yaml_str(yaml).unwrap();
/// let registry = CommandRegistry::from_manifest(&manifest);
/// assert!(registry.get("hello").is_some());
/// assert!(registry.get("unknown").is_none());
/// ```
#[derive(Debug)]
#[allow(clippy::module_name_repetitions)]
pub struct CommandRegistry {
Expand Down
25 changes: 25 additions & 0 deletions crates/east-config/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ use crate::value::ConfigValue;
///
/// Keys are dotted paths (e.g. `"user.name"`). Internally the store
/// is a tree of `Node`s — branches hold child nodes, leaves hold values.
///
/// # Example
///
/// ```
/// use east_config::{ConfigStore, ConfigValue};
///
/// let mut store = ConfigStore::new();
/// store.set("user.name", ConfigValue::String("alice".into()));
/// store.set("update.jobs", ConfigValue::Integer(4));
///
/// assert_eq!(store.get("user.name").and_then(|v| v.as_str()), Some("alice"));
/// assert_eq!(store.get("update.jobs").and_then(|v| v.as_i64()), Some(4));
/// ```
#[derive(Debug, Clone)]
#[allow(clippy::module_name_repetitions)]
pub struct ConfigStore {
Expand Down Expand Up @@ -67,6 +80,18 @@ impl ConfigStore {
/// # Errors
///
/// Returns [`ConfigError::TomlParse`] if the TOML is invalid.
///
/// # Example
///
/// ```
/// use east_config::ConfigStore;
///
/// let store = ConfigStore::from_toml_str(r#"
/// [user]
/// name = "alice"
/// "#).unwrap();
/// assert_eq!(store.get("user.name").and_then(|v| v.as_str()), Some("alice"));
/// ```
pub fn from_toml_str(toml_str: &str) -> Result<Self, ConfigError> {
let table: toml::Table = toml::from_str(toml_str)?;
let mut store = Self::new();
Expand Down
13 changes: 13 additions & 0 deletions crates/east-config/src/value.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
use std::fmt;

/// A configuration value that can be stored in a config layer.
///
/// # Example
///
/// ```
/// use east_config::ConfigValue;
///
/// let s = ConfigValue::String("hello".into());
/// assert_eq!(s.as_str(), Some("hello"));
/// assert_eq!(format!("{s}"), "hello");
///
/// let n = ConfigValue::Integer(42);
/// assert_eq!(n.as_i64(), Some(42));
/// ```
#[derive(Debug, Clone, PartialEq)]
#[allow(clippy::module_name_repetitions)]
pub enum ConfigValue {
Expand Down
58 changes: 58 additions & 0 deletions crates/east-manifest/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ use crate::error::ManifestError;
///
/// Combined with a project name to form the full clone URL:
/// `{url_base}/{project_name}`.
///
/// # Example
///
/// ```
/// use east_manifest::Remote;
///
/// let remote = Remote {
/// name: "origin".into(),
/// url_base: "https://github.com/my-org".into(),
/// };
/// assert_eq!(remote.name, "origin");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Remote {
/// Unique identifier for this remote (e.g. `"origin"`).
Expand All @@ -36,6 +48,30 @@ pub struct Defaults {
///
/// Each project maps to one git repository that will be cloned into
/// the workspace.
///
/// # Example
///
/// ```
/// use east_manifest::Project;
///
/// let p = Project {
/// name: "hal".into(),
/// path: Some("modules/hal".into()),
/// remote: None,
/// revision: Some("v2.0".into()),
/// groups: vec!["required".into()],
/// };
/// assert_eq!(p.effective_path(), "modules/hal");
///
/// let p2 = Project {
/// name: "app".into(),
/// path: None,
/// remote: None,
/// revision: None,
/// groups: vec![],
/// };
/// assert_eq!(p2.effective_path(), "app");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Project {
/// Unique project name (also used to construct the clone URL).
Expand Down Expand Up @@ -196,6 +232,28 @@ impl Manifest {
/// # Errors
///
/// Returns [`ManifestError`] if parsing or validation fails.
///
/// # Example
///
/// ```
/// use east_manifest::Manifest;
///
/// let yaml = r#"
/// version: 1
/// remotes:
/// - name: origin
/// url-base: https://github.com/my-org
/// defaults:
/// remote: origin
/// revision: main
/// projects:
/// - name: hal
/// path: modules/hal
/// "#;
/// let manifest = Manifest::from_yaml_str(yaml).unwrap();
/// assert_eq!(manifest.projects.len(), 1);
/// assert_eq!(manifest.projects[0].name, "hal");
/// ```
pub fn from_yaml_str(yaml: &str) -> Result<Self, ManifestError> {
let manifest: Self = serde_yaml::from_str(yaml)?;
manifest.validate()?;
Expand Down
1 change: 1 addition & 0 deletions crates/east-vcs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ repository.workspace = true
description = "Git operations via shell-out to system git for east"

[dependencies]
miette = { workspace = true }
thiserror.workspace = true
tokio = { workspace = true, features = ["process"] }

Expand Down
5 changes: 4 additions & 1 deletion crates/east-vcs/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use std::path::PathBuf;

use miette::Diagnostic;
use thiserror::Error;

/// Errors from git shell-out operations.
#[derive(Debug, Error)]
#[derive(Debug, Error, Diagnostic)]
#[allow(clippy::module_name_repetitions)]
pub enum VcsError {
/// A git command failed with a non-zero exit code.
#[error("git command failed in {path}: {stderr}")]
#[diagnostic(help("check that the repository exists and the revision is valid"))]
GitFailed {
/// Working directory or target path.
path: PathBuf,
Expand All @@ -17,5 +19,6 @@ pub enum VcsError {

/// Failed to spawn the git process.
#[error("failed to execute git: {0}")]
#[diagnostic(help("ensure git is installed and available on PATH"))]
Io(#[from] std::io::Error),
}
14 changes: 14 additions & 0 deletions crates/east-vcs/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ use crate::error::VcsError;
///
/// All methods are async and call the `git` binary as a child process.
/// No `libgit2` or `git2-rs` binding is used.
///
/// # Example
///
/// ```no_run
/// use std::path::Path;
/// use east_vcs::Git;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// Git::clone("https://github.com/org/repo", Path::new("./repo"), Some("main")).await?;
/// let sha = Git::head(Path::new("./repo")).await?;
/// let dirty = Git::is_dirty(Path::new("./repo")).await?;
/// # Ok(())
/// # }

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc example starts a fenced Rust code block (no_run) but never closes it with a terminating line. This can break rustdoc rendering and may prevent the snippet from being recognized as a doctest; add the closing fence before pub struct Git;.

Suggested change
/// # }
/// # }
/// ```

Copilot uses AI. Check for mistakes.
/// ```
pub struct Git;

impl Git {
Expand Down
14 changes: 14 additions & 0 deletions crates/east-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ const LEGACY_MANIFEST_FILE: &str = "east.yml";
/// A workspace is rooted at the directory that contains `.east/`.
/// The manifest location is determined by the `[manifest]` section
/// in `.east/config.toml`.
///
/// # Example
///
/// ```
/// # use tempfile::TempDir;
/// use east_workspace::Workspace;
///
/// let dir = TempDir::new().unwrap();
/// let ws = Workspace::init(dir.path()).unwrap();
/// assert!(ws.east_dir().exists());
///
/// let found = Workspace::discover(dir.path()).unwrap();
/// assert_eq!(found.root(), ws.root());
/// ```
#[derive(Debug, Clone)]
pub struct Workspace {
root: PathBuf,
Expand Down
2 changes: 1 addition & 1 deletion docs/design/phase-2.6.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Phase 2.6 设计文档 — 拓扑修正

**状态:** 生效中
**范围:** 修正 workspace 拓扑,让 manifest 住在真实 git 仓库中,而非 workspace 根的裸文件。对已有 workspace 是破坏性变更。
**范围:** 修正 workspace 拓扑,让 manifest 存放于真实 git 仓库中,而非 workspace 根的裸文件。对已有 workspace 是破坏性变更。

## 1. 本 Phase 存在的原因

Expand Down
2 changes: 1 addition & 1 deletion docs/dev/phase-2.6.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 交付内容

修正 workspace 拓扑:manifest 现在住在真实 git 仓库中,与 `.east/` 平级。
修正 workspace 拓扑:manifest 现在存放于真实 git 仓库中,与 `.east/` 平级。

### 新的 `east init` 模式

Expand Down
Loading
Loading