feat: add deploy from current work directory command
This commit is contained in:
@@ -24,6 +24,7 @@ xboctploy --list-servers # показать серверы из конфига
|
|||||||
xboctploy --pick # интерактивно выбрать проект (нечёткий поиск)
|
xboctploy --pick # интерактивно выбрать проект (нечёткий поиск)
|
||||||
xboctploy --dry-run demo # напечатать план деплоя без выполнения
|
xboctploy --dry-run demo # напечатать план деплоя без выполнения
|
||||||
xboctploy demo # задеплоить проект demo
|
xboctploy demo # задеплоить проект demo
|
||||||
|
xboctploy --this # задеплоить проект текущей папки (глобальный конфиг)
|
||||||
```
|
```
|
||||||
|
|
||||||
Вывод:
|
Вывод:
|
||||||
@@ -44,6 +45,10 @@ Deploy successful! (total 2.3s)
|
|||||||
2. `./deploy.toml` в текущей папке — удобно держать рядом с проектом и в тестах;
|
2. `./deploy.toml` в текущей папке — удобно держать рядом с проектом и в тестах;
|
||||||
3. глобальный `~/.config/xboctploy/deploy.toml` — запуск «из любой папки».
|
3. глобальный `~/.config/xboctploy/deploy.toml` — запуск «из любой папки».
|
||||||
|
|
||||||
|
`--this` деплоит проект, чей `workdir` совпадает с текущей папкой; конфиг
|
||||||
|
ищется по обычной цепочке, так что временный локальный `./deploy.toml`
|
||||||
|
переопределяет глобальный — создали файл, задеплоились, удалили.
|
||||||
|
|
||||||
Схема:
|
Схема:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
|||||||
+4
-1
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
|
|||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(name = "xboctploy", version, about)]
|
#[command(name = "xboctploy", version, about)]
|
||||||
#[command(arg_required_else_help = true)]
|
#[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 {
|
pub struct Cli {
|
||||||
/// Project name from deploy.toml
|
/// Project name from deploy.toml
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
@@ -25,4 +25,7 @@ pub struct Cli {
|
|||||||
/// Interactively pick a project to deploy (fuzzy search)
|
/// Interactively pick a project to deploy (fuzzy search)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub pick: bool,
|
pub pick: bool,
|
||||||
|
/// Deploy the project whose workdir matches the current folder (global config)
|
||||||
|
#[arg(long)]
|
||||||
|
pub this: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-5
@@ -168,16 +168,42 @@ pub fn expand_tilde(path: &Path) -> Result<PathBuf> {
|
|||||||
Ok(path.to_path_buf())
|
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<PathBuf> {
|
||||||
|
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<PathBuf> {
|
fn pick_config(cwd: &Path, home: Option<&Path>) -> Option<PathBuf> {
|
||||||
let local = cwd.join(CONFIG_FILE_NAME);
|
let local = cwd.join(CONFIG_FILE_NAME);
|
||||||
if local.is_file() {
|
if local.is_file() {
|
||||||
return Some(local);
|
return Some(local);
|
||||||
}
|
}
|
||||||
let global = home?
|
global_path(home)
|
||||||
.join(".config")
|
}
|
||||||
.join(GLOBAL_DIR)
|
|
||||||
.join(CONFIG_FILE_NAME);
|
pub fn find_by_cwd<'a>(cfg: &'a Config, cwd: &Path) -> Option<&'a str> {
|
||||||
global.is_file().then_some(global)
|
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<PathBuf> {
|
pub fn resolve(explicit: Option<&Path>) -> Result<PathBuf> {
|
||||||
@@ -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 {
|
fn tmpdir(tag: &str) -> PathBuf {
|
||||||
let dir =
|
let dir =
|
||||||
std::env::temp_dir().join(format!("xboctploy-tests-{}-{tag}", std::process::id()));
|
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);
|
assert_eq!(pick_config(&cwd, Some(&root.join("nope"))), None);
|
||||||
let _ = fs::remove_dir_all(root);
|
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::<Config>(&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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-2
@@ -28,11 +28,15 @@ fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run(cli: &cli::Cli) -> Result<()> {
|
fn run(cli: &cli::Cli) -> Result<()> {
|
||||||
|
if cli.this {
|
||||||
|
return deploy_this(cli.dry_run);
|
||||||
|
}
|
||||||
|
|
||||||
let path = config::resolve(cli.config.as_deref())?;
|
let path = config::resolve(cli.config.as_deref())?;
|
||||||
let cfg = config::load(&path)?;
|
let cfg = config::load(&path)?;
|
||||||
|
|
||||||
if cli.list_servers {
|
if cli.list_servers {
|
||||||
println!("config: {}", path.display());
|
println!("config: {}", config::display(&path));
|
||||||
println!("configured servers:");
|
println!("configured servers:");
|
||||||
for (name, server) in &cfg.servers {
|
for (name, server) in &cfg.servers {
|
||||||
let mut line = format!(" - {name} {}", server.host);
|
let mut line = format!(" - {name} {}", server.host);
|
||||||
@@ -48,7 +52,7 @@ fn run(cli: &cli::Cli) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cli.list {
|
if cli.list {
|
||||||
println!("config: {}", path.display());
|
println!("config: {}", config::display(&path));
|
||||||
println!("configured projects:");
|
println!("configured projects:");
|
||||||
for name in cfg.projects.keys() {
|
for name in cfg.projects.keys() {
|
||||||
println!(" - {name}");
|
println!(" - {name}");
|
||||||
@@ -74,6 +78,22 @@ fn run(cli: &cli::Cli) -> Result<()> {
|
|||||||
deploy::deploy(&name, project, cli.dry_run)
|
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<Option<String>> {
|
fn pick_project(cfg: &config::Config) -> Result<Option<String>> {
|
||||||
use dialoguer::FuzzySelect;
|
use dialoguer::FuzzySelect;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user