use clap::{Parser, Subcommand};
use qtcloud_devops_cli::commands::code::GitSubmoduleEditor;
use qtcloud_devops_cli::commands::release::Registry;
use qtcloud_devops_cli::commands::{HealthIssue, SubmoduleEditor};
use qtcloud_devops_cli::model::code;
use std::path::PathBuf;
use std::process;
#[derive(Parser)]
#[command(
name = "qtcloud-devops",
about = "量潮DevOps实验室 — Git 子模块管理 & 发布管理",
version
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Code {
#[command(subcommand)]
action: CodeAction,
},
Release {
#[command(subcommand)]
action: ReleaseAction,
},
}
#[derive(Subcommand)]
enum ReleaseAction {
Stage {
#[arg(short = 'v', long)]
version: String,
},
Publish {
#[arg(short = 'v', long)]
version: String,
#[arg(long, short = 'y')]
yes: bool,
#[arg(long, value_enum)]
registry: Option<Registry>,
},
Retire {
#[arg(short = 'v', long)]
version: String,
},
Status,
}
#[derive(Subcommand)]
enum CodeAction {
Status {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long)]
offline: bool,
},
Sync {
name: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(default_value = ".")]
repo: PathBuf,
},
Retire {
name: String,
#[arg(long)]
dry_run: bool,
#[arg(default_value = ".")]
repo: PathBuf,
},
}
fn resolve_path(path: &PathBuf) -> Result<PathBuf, String> {
std::fs::canonicalize(path)
.map_err(|e| format!("无法解析路径 '{}': {}", path.display(), e))
}
fn print_issues(issues: &[HealthIssue]) {
if !issues.is_empty() {
println!("\n需要关注的子模块:");
for issue in issues {
println!(" [{}] {}", issue.submodule_name, issue.description);
println!(" 建议: {}", issue.suggested_action);
}
}
}
fn print_aggregate(state: &code::RepoState) {
if let Ok((_, agg)) = code::RepoState::scan_all(&state.root_path) {
println!("\n聚合统计:");
println!(" 总数: {}", agg.total);
println!(" ✅ Clean: {}", agg.clean);
if agg.ahead_of_parent > 0 {
println!(" ⬆ AheadOfParent: {}", agg.ahead_of_parent);
}
if agg.behind_remote > 0 {
println!(" ⬇ BehindRemote: {}", agg.behind_remote);
}
if agg.detached > 0 {
println!(" ⚠ Detached: {}", agg.detached);
}
if agg.dirty > 0 {
println!(" 🔴 Dirty: {}", agg.dirty);
}
if agg.orphaned > 0 {
println!(" 💀 Orphaned: {}", agg.orphaned);
}
if agg.uninitialized > 0 {
println!(" ⚪ Uninitialized: {}", agg.uninitialized);
}
}
}
fn repo_path() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
fn main() {
let cli = Cli::parse();
let result = match cli.command {
Commands::Code { action } => run_code(action),
Commands::Release { action } => match action {
ReleaseAction::Stage { version } => {
qtcloud_devops_cli::commands::release::stage(&version, &repo_path())
.map(|_| ()).map_err(|e| format!("{}", e))
}
ReleaseAction::Publish { version, yes, registry } => {
qtcloud_devops_cli::commands::release::publish(&version, &repo_path(), yes, registry)
.map(|_| ()).map_err(|e| format!("{}", e))
}
ReleaseAction::Retire { version } => {
qtcloud_devops_cli::commands::release::retire(&version, &repo_path())
.map(|_| ()).map_err(|e| format!("{}", e))
}
ReleaseAction::Status => {
qtcloud_devops_cli::commands::release::release_status(&repo_path())
.map(|_| ()).map_err(|e| format!("{}", e))
}
}
};
if let Err(e) = result {
eprintln!("错误: {}", e);
process::exit(1);
}
}
fn run_code(action: CodeAction) -> Result<(), String> {
match action {
CodeAction::Status { path, offline } => {
let root = resolve_path(&path)?;
let mut editor = GitSubmoduleEditor::new(root.clone());
editor.set_offline(offline);
let state = code::RepoState::scan(&root)
.map_err(|e| format!("{}", e))?;
let issues = editor.status()
.map_err(|e| format!("{}", e))?;
println!("仓库: {}", state.root_path.display());
println!("子模块总数: {}", state.total);
println!("干净: {}", state.clean_count);
if !state.needs_attention.is_empty() {
println!("需要关注: {}", state.needs_attention.join(", "));
}
print_aggregate(&state);
println!();
if state.submodules.is_empty() && state.total == 0 {
println!(" 没有子模块");
} else {
println!(
" {:<20} {:<15} {:<10} {:<8}",
"名称", "状态", "分支", "差异"
);
for sm in &state.submodules {
let diff = if sm.ahead_count > 0 && sm.behind_count > 0 {
format!("+{}/-{}", sm.ahead_count, sm.behind_count)
} else if sm.ahead_count > 0 {
format!("+{}", sm.ahead_count)
} else if sm.behind_count > 0 {
format!("-{}", sm.behind_count)
} else {
String::new()
};
println!(
" {:<20} {:<15} {:<10} {:<8}",
sm.name,
format!("{:?}", sm.status),
sm.tracked_branch,
diff,
);
}
}
print_issues(&issues);
Ok(())
}
CodeAction::Sync {
name: Some(n),
dry_run,
repo,
} => {
let root = resolve_path(&repo)?;
if dry_run {
println!("[预览] 同步子模块 '{}' 到父仓库", n);
return Ok(());
}
let editor = GitSubmoduleEditor::new(root);
editor.sync_to_parent(&n)
.map_err(|e| format!("同步子模块 '{}' 失败: {}", n, e))
}
CodeAction::Sync {
name: None,
dry_run,
repo,
} => {
let root = resolve_path(&repo)?;
if dry_run {
println!("[预览] 同步所有子模块到父仓库");
return Ok(());
}
let editor = GitSubmoduleEditor::new(root);
editor.sync_all_to_parent()
.map_err(|e| format!("同步所有子模块失败: {}", e))
}
CodeAction::Retire {
name,
dry_run,
repo,
} => {
let root = resolve_path(&repo)?;
if dry_run {
println!("[预览] 退役子模块 '{}'", name);
return Ok(());
}
let editor = GitSubmoduleEditor::new(root);
editor.retire_submodule(&name)
.map_err(|e| format!("退役子模块 '{}' 失败: {}", name, e))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_resolve_path_valid() {
let p = resolve_path(&std::env::current_dir().unwrap().join("Cargo.toml").into());
assert!(p.is_ok());
assert!(p.unwrap().is_absolute());
}
#[test]
fn test_resolve_path_invalid() {
let result = resolve_path(&PathBuf::from("/__nonexistent_path_12345__"));
assert!(result.is_err());
}
#[test]
fn test_print_issues_empty() {
let issues = vec![];
print_issues(&issues);
}
#[test]
fn test_print_issues_non_empty() {
use qtcloud_devops_cli::commands::HealthIssue;
use qtcloud_devops_cli::model::code::SubmoduleStatus;
let issues = vec![HealthIssue {
submodule_name: "libs/foo".into(),
status: SubmoduleStatus::Dirty,
description: "有修改".into(),
suggested_action: "提交".into(),
}];
print_issues(&issues);
}
#[test]
fn test_print_aggregate_all_zeros() {
let state = code::RepoState {
root_path: PathBuf::from("/tmp"),
submodules: vec![],
total: 0,
clean_count: 0,
needs_attention: vec![],
parent_dirty: false,
};
print_aggregate(&state);
}
#[test]
fn test_print_aggregate_with_variants() {
use qtcloud_devops_cli::model::code::{CommitHash, Submodule, SubmoduleStatus};
let submodules = vec![
Submodule {
name: "a".into(),
path: PathBuf::new(),
url: String::new(),
tracked_branch: "main".into(),
parent_pointer: CommitHash::default(),
local_head: CommitHash::default(),
remote_head: CommitHash::default(),
status: SubmoduleStatus::AheadOfParent,
ahead_count: 0,
behind_count: 0,
remote_unreachable: false,
},
Submodule {
name: "b".into(),
path: PathBuf::new(),
url: String::new(),
tracked_branch: "main".into(),
parent_pointer: CommitHash::default(),
local_head: CommitHash::default(),
remote_head: CommitHash::default(),
status: SubmoduleStatus::BehindRemote,
ahead_count: 0,
behind_count: 0,
remote_unreachable: false,
},
Submodule {
name: "c".into(),
path: PathBuf::new(),
url: String::new(),
tracked_branch: "main".into(),
parent_pointer: CommitHash::default(),
local_head: CommitHash::default(),
remote_head: CommitHash::default(),
status: SubmoduleStatus::Detached,
ahead_count: 0,
behind_count: 0,
remote_unreachable: false,
},
Submodule {
name: "d".into(),
path: PathBuf::new(),
url: String::new(),
tracked_branch: "main".into(),
parent_pointer: CommitHash::default(),
local_head: CommitHash::default(),
remote_head: CommitHash::default(),
status: SubmoduleStatus::Dirty,
ahead_count: 0,
behind_count: 0,
remote_unreachable: false,
},
Submodule {
name: "e".into(),
path: PathBuf::new(),
url: String::new(),
tracked_branch: "main".into(),
parent_pointer: CommitHash::default(),
local_head: CommitHash::default(),
remote_head: CommitHash::default(),
status: SubmoduleStatus::Orphaned,
ahead_count: 0,
behind_count: 0,
remote_unreachable: false,
},
Submodule {
name: "f".into(),
path: PathBuf::new(),
url: String::new(),
tracked_branch: "main".into(),
parent_pointer: CommitHash::default(),
local_head: CommitHash::default(),
remote_head: CommitHash::default(),
status: SubmoduleStatus::Uninitialized,
ahead_count: 0,
behind_count: 0,
remote_unreachable: false,
},
Submodule {
name: "g".into(),
path: PathBuf::new(),
url: String::new(),
tracked_branch: "main".into(),
parent_pointer: CommitHash::default(),
local_head: CommitHash::default(),
remote_head: CommitHash::default(),
status: SubmoduleStatus::Clean,
ahead_count: 0,
behind_count: 0,
remote_unreachable: false,
},
];
let state = code::RepoState {
root_path: PathBuf::from("/tmp"),
submodules,
total: 7,
clean_count: 1,
needs_attention: vec![
"a".into(),
"b".into(),
"c".into(),
"d".into(),
"e".into(),
"f".into(),
],
parent_dirty: false,
};
print_aggregate(&state);
}
}