qtcloud-devops-cli 0.7.0-rc.2

量潮DevOps云命令行工具
Documentation
/// 契约模块 — 基于 `quanttide-devops` toolkit 的适配层。
///
/// 类型定义与 YAML 解析委托给 toolkit,本模块仅保留 CLI 特有的版本检测逻辑。
pub use quanttide_devops::contract::{
    detect_language_by_files, normalize_version, read_all_config_versions, validate_version,
    BuildTool, Contract, Language, Pipeline, Platform, Registry, Scope, SourceControl, SourceType,
    StageBuild, StageRelease, StageTest, VersionSource,
};

use std::path::Path;

// ═══════════════════════════════════════════════════════════════════════
// 加载(保留向后兼容的行为)
// ═══════════════════════════════════════════════════════════════════════

/// 从 `.quanttide/devops/contract.yaml` 加载完整契约。
///
/// 文件不存在或解析失败时降级为默认契约(兼容旧调用方)。
pub fn load(repo_path: &Path) -> Contract {
    match quanttide_devops::contract::load(repo_path) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("  ℹ contract.yaml: {},使用默认契约", e);
            Contract::default()
        }
    }
}

/// 快速加载 scope 列表。
pub fn load_scopes(repo_path: &Path) -> Vec<Scope> {
    load(repo_path).scopes
}

/// 根据目录下的标志文件推测编程语言(`detect_language_by_files` 的别名)。
pub fn detect_by_files(dir: &Path) -> Language {
    detect_language_by_files(dir)
}

// ═══════════════════════════════════════════════════════════════════════
// 版本状态(CLI 特有,toolkit 尚不支持)
// ═══════════════════════════════════════════════════════════════════════

/// 版本一致性检查结果。
#[derive(Debug)]
pub struct VersionStatus {
    pub tag_version: Option<String>,
    pub config_version: Option<String>,
    pub consistent: bool,
    /// 所有配置文件的版本号明细。(文件名, 版本号)
    pub config_files: Vec<(String, Option<String>)>,
}

/// 检查 scope 下所有已知配置文件的版本,判断与 tag 是否一致。
pub fn version_status(repo_path: &Path, scope: &Scope) -> VersionStatus {
    let tag_version = latest_tag_for_scope(repo_path, &scope.name);
    let scope_dir = repo_path.join(&scope.dir);
    let config_files = quanttide_devops::contract::read_all_config_versions(&scope_dir);
    let config_version = config_files
        .iter()
        .find(|(_, v)| v.is_some())
        .and_then(|(_, v)| v.clone());
    let consistent = match &tag_version {
        Some(t) => config_files.iter().all(|(_, v)| match v {
            Some(cv) => cv == t,
            None => true,
        }),
        None => config_version.is_none(),
    };
    VersionStatus {
        tag_version,
        config_version,
        consistent,
        config_files,
    }
}

fn latest_tag_for_scope(repo_path: &Path, scope_name: &str) -> Option<String> {
    let output = std::process::Command::new("git")
        .args(["tag", "--sort=-version:refname"])
        .current_dir(repo_path)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let prefix = format!("{}/", scope_name);
    let tags: Vec<&str> = std::str::from_utf8(&output.stdout)
        .ok()?
        .lines()
        .filter(|t| t.starts_with(&prefix) || !t.contains('/'))
        .collect();
    let scoped = tags.iter().find(|t| t.starts_with(&prefix));
    match scoped {
        Some(t) => Some(normalize_version(t)),
        None => tags.first().map(|t| normalize_version(t)),
    }
}