From 608dc5f624ba7f75cae97218676d7ac62d499bd1 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Tue, 25 Aug 2026 11:08:11 +0500 Subject: [PATCH] feat: add deploy from current work directory command --- README.md | 5 ++++ src/cli.rs | 5 +++- src/config.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 24 +++++++++++++++-- 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cb5d5f8..da33f05 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ xboctploy --list-servers # показать серверы из конфига xboctploy --pick # интерактивно выбрать проект (нечёткий поиск) xboctploy --dry-run demo # напечатать план деплоя без выполнения xboctploy demo # задеплоить проект demo +xboctploy --this # задеплоить проект текущей папки (глобальный конфиг) ``` Вывод: @@ -44,6 +45,10 @@ Deploy successful! (total 2.3s) 2. `./deploy.toml` в текущей папке — удобно держать рядом с проектом и в тестах; 3. глобальный `~/.config/xboctploy/deploy.toml` — запуск «из любой папки». +`--this` деплоит проект, чей `workdir` совпадает с текущей папкой; конфиг +ищется по обычной цепочке, так что временный локальный `./deploy.toml` +переопределяет глобальный — создали файл, задеплоились, удалили. + Схема: ```toml diff --git a/src/cli.rs b/src/cli.rs index c802605..8fc06e6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser}; #[derive(Debug, Parser)] #[command(name = "xboctploy", version, about)] #[command(arg_required_else_help = true)] -#[command(group = ArgGroup::new("target").required(true).args(["project", "list", "list_servers", "pick"]))] +#[command(group = ArgGroup::new("target").required(true).args(["project", "list", "list_servers", "pick", "this"]))] pub struct Cli { /// Project name from deploy.toml pub project: Option, @@ -25,4 +25,7 @@ pub struct Cli { /// Interactively pick a project to deploy (fuzzy search) #[arg(long)] pub pick: bool, + /// Deploy the project whose workdir matches the current folder (global config) + #[arg(long)] + pub this: bool, } diff --git a/src/config.rs b/src/config.rs index 29f945a..358cc82 100644 --- a/src/config.rs +++ b/src/config.rs @@ -168,16 +168,42 @@ pub fn expand_tilde(path: &Path) -> Result { Ok(path.to_path_buf()) } +pub fn display(path: &Path) -> String { + let Some(home) = home::home_dir() else { + return path.display().to_string(); + }; + let Ok(rest) = path.strip_prefix(&home) else { + return path.display().to_string(); + }; + format!("~/{}", rest.to_string_lossy().replace('\\', "/")) +} + +pub fn global_path(home: Option<&Path>) -> Option { + let path = home? + .join(".config") + .join(GLOBAL_DIR) + .join(CONFIG_FILE_NAME); + path.is_file().then_some(path) +} + fn pick_config(cwd: &Path, home: Option<&Path>) -> Option { let local = cwd.join(CONFIG_FILE_NAME); if local.is_file() { return Some(local); } - let global = home? - .join(".config") - .join(GLOBAL_DIR) - .join(CONFIG_FILE_NAME); - global.is_file().then_some(global) + global_path(home) +} + +pub fn find_by_cwd<'a>(cfg: &'a Config, cwd: &Path) -> Option<&'a str> { + let current = fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf()); + cfg.projects.iter().find_map(|(name, project)| { + let wd = canonicalized(&project.workdir); + (wd == current).then_some(name.as_str()) + }) +} + +fn canonicalized(path: &Path) -> PathBuf { + fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } pub fn resolve(explicit: Option<&Path>) -> Result { @@ -383,6 +409,18 @@ commands = [] ); } + #[test] + fn display_abbreviates_home_with_tilde() { + let home = home::home_dir().unwrap(); + let inside = home.join(".config").join(GLOBAL_DIR).join(CONFIG_FILE_NAME); + assert_eq!( + display(&inside), + format!("~/.config/{GLOBAL_DIR}/{CONFIG_FILE_NAME}") + ); + let outside = home.ancestors().nth(1).unwrap(); + assert_eq!(display(outside), outside.display().to_string()); + } + fn tmpdir(tag: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("xboctploy-tests-{}-{tag}", std::process::id())); @@ -424,4 +462,28 @@ commands = [] assert_eq!(pick_config(&cwd, Some(&root.join("nope"))), None); let _ = fs::remove_dir_all(root); } + + #[test] + fn finds_project_whose_workdir_matches_cwd() { + let root = tmpdir("find-cwd"); + let proj = root.join("proj"); + fs::create_dir_all(&proj).unwrap(); + let s = format!( + r#" +[projects.web] +workdir = '{}' +host = "vps" + +[projects.api] +workdir = '~/code/api' +host = "vps" +"#, + proj.display() + ); + let cfg = toml::from_str::(&s).unwrap().validate().unwrap(); + + assert_eq!(find_by_cwd(&cfg, &proj), Some("web")); + assert_eq!(find_by_cwd(&cfg, &root.join("other")), None); + let _ = fs::remove_dir_all(root); + } } diff --git a/src/main.rs b/src/main.rs index 79fc6a2..6ec51ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,11 +28,15 @@ fn main() -> ExitCode { } fn run(cli: &cli::Cli) -> Result<()> { + if cli.this { + return deploy_this(cli.dry_run); + } + let path = config::resolve(cli.config.as_deref())?; let cfg = config::load(&path)?; if cli.list_servers { - println!("config: {}", path.display()); + println!("config: {}", config::display(&path)); println!("configured servers:"); for (name, server) in &cfg.servers { let mut line = format!(" - {name} {}", server.host); @@ -48,7 +52,7 @@ fn run(cli: &cli::Cli) -> Result<()> { } if cli.list { - println!("config: {}", path.display()); + println!("config: {}", config::display(&path)); println!("configured projects:"); for name in cfg.projects.keys() { println!(" - {name}"); @@ -74,6 +78,22 @@ fn run(cli: &cli::Cli) -> Result<()> { deploy::deploy(&name, project, cli.dry_run) } +fn deploy_this(dry_run: bool) -> Result<()> { + let path = config::resolve(None)?; + let cfg = config::load(&path)?; + + let cwd = std::env::current_dir().context("cannot determine current directory")?; + let Some(name) = config::find_by_cwd(&cfg, &cwd) else { + bail!( + "no project in {} has workdir matching {}", + config::display(&path), + cwd.display() + ); + }; + println!("config: {}", config::display(&path)); + deploy::deploy(name, &cfg.projects[name], dry_run) +} + fn pick_project(cfg: &config::Config) -> Result> { use dialoguer::FuzzySelect;