merge: cli modernization
This commit is contained in:
11
Cargo.toml
11
Cargo.toml
@@ -13,11 +13,18 @@ build = "build.rs"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.6.6", features = ["derive"] }
|
clap = { version = "4.6.6", features = ["derive", "env"] }
|
||||||
clap_complete = "4.6.9"
|
clap_complete = "4.6.9"
|
||||||
anyhow = "1.0.104"
|
anyhow = "1.0.104"
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
clap = { version = "4.6.6", features = ["derive"] }
|
clap = { version = "4.6.6", features = ["derive", "env"] }
|
||||||
clap_complete = "4.6.9"
|
clap_complete = "4.6.9"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
dbg_macro = "warn"
|
||||||
|
todo = "warn"
|
||||||
|
|||||||
@@ -134,4 +134,5 @@ from the git `origin` remote.
|
|||||||
|
|
||||||
Currently `ahab` respects the following environment variables.
|
Currently `ahab` respects the following environment variables.
|
||||||
|
|
||||||
- `AHAB_LINK_ROOT`: root of the out-of-repo store `ahab link` moves paths into - defaults to `${XDG_DATA_HOME:-$HOME/.local/share}/ahab`
|
- `AHAB_LINK_ROOT`: where `ahab link` keeps its store - the same as `--store`,
|
||||||
|
which takes precedence, and defaults to `${XDG_DATA_HOME:-$HOME/.local/share}/ahab`
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use super::{Django, Link, Postgres};
|
use super::{Django, Link, Postgres};
|
||||||
|
use clap::builder::styling::{AnsiColor, Effects, Styles};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_complete::Shell;
|
use clap_complete::Shell;
|
||||||
|
|
||||||
/// A program for interacting with various dockerized applications
|
/// A program for interacting with various dockerized applications
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about=None)]
|
#[command(version, about, long_about = None)]
|
||||||
|
#[command(styles = help_styles())]
|
||||||
pub struct Ahab {
|
pub struct Ahab {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
@@ -43,3 +45,23 @@ pub enum Commands {
|
|||||||
shell: Shell,
|
shell: Shell,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clap styles headers bold by default but adds no colour
|
||||||
|
fn help_styles() -> Styles {
|
||||||
|
Styles::styled()
|
||||||
|
.header(AnsiColor::Green.on_default() | Effects::BOLD)
|
||||||
|
.usage(AnsiColor::Green.on_default() | Effects::BOLD)
|
||||||
|
.literal(AnsiColor::Cyan.on_default() | Effects::BOLD)
|
||||||
|
.placeholder(AnsiColor::Cyan.on_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Ahab;
|
||||||
|
use clap::CommandFactory;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_command_is_well_formed() {
|
||||||
|
Ahab::command().debug_assert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum Link {
|
pub enum Link {
|
||||||
@@ -13,6 +13,9 @@ pub enum Link {
|
|||||||
/// Link to a store path that already exists
|
/// Link to a store path that already exists
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
force: bool,
|
force: bool,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
store: Store,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Move paths in the store back into the repository
|
/// Move paths in the store back into the repository
|
||||||
@@ -22,6 +25,9 @@ pub enum Link {
|
|||||||
/// Restore every path this repository has in the store
|
/// Restore every path this repository has in the store
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
all: bool,
|
all: bool,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
store: Store,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// List untracked paths a sandbox would still see
|
/// List untracked paths a sandbox would still see
|
||||||
@@ -40,5 +46,17 @@ pub enum Link {
|
|||||||
/// Terminate porcelain entries with NUL
|
/// Terminate porcelain entries with NUL
|
||||||
#[arg(short = 'z')]
|
#[arg(short = 'z')]
|
||||||
null: bool,
|
null: bool,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
store: Store,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct Store {
|
||||||
|
/// Where the out-of-repo store lives
|
||||||
|
///
|
||||||
|
/// Defaults to `${XDG_DATA_HOME:-$HOME/.local/share}/ahab`.
|
||||||
|
#[arg(long = "store", env = "AHAB_LINK_ROOT", value_name = "DIR")]
|
||||||
|
pub root: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ mod postgres;
|
|||||||
|
|
||||||
pub use ahab::{Ahab, Commands};
|
pub use ahab::{Ahab, Commands};
|
||||||
pub use django::Django;
|
pub use django::Django;
|
||||||
pub use link::Link;
|
pub use link::{Link, Store};
|
||||||
pub use postgres::{Format, Postgres};
|
pub use postgres::{Format, Postgres};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
|
os::unix::process::CommandExt,
|
||||||
process::{Child, Command, ExitStatus, Stdio},
|
process::{Child, Command, ExitStatus, Stdio},
|
||||||
sync::OnceLock,
|
sync::OnceLock,
|
||||||
};
|
};
|
||||||
@@ -83,6 +84,19 @@ impl CommandBuilder {
|
|||||||
check(&shown, status)
|
check(&shown, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replaces this process, so the command's exit code and signals become ours
|
||||||
|
pub fn exec_replace(self) -> Result<()> {
|
||||||
|
if options().dry_run {
|
||||||
|
eprintln!("would run `{self}`");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let shown = self.to_string();
|
||||||
|
let error = self.build()?.exec();
|
||||||
|
|
||||||
|
Err(error).with_context(|| format!("running `{shown}`"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn spawn(self) -> Result<Child> {
|
pub fn spawn(self) -> Result<Child> {
|
||||||
Ok(self.build()?.spawn()?)
|
Ok(self.build()?.spawn()?)
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/main.rs
58
src/main.rs
@@ -1,9 +1,11 @@
|
|||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use ahab::{cli, command_builder, scripts};
|
use ahab::{cli, command_builder, scripts};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> ExitCode {
|
||||||
let args = cli::Ahab::parse();
|
let args = cli::Ahab::parse();
|
||||||
|
|
||||||
command_builder::set_options(command_builder::Options {
|
command_builder::set_options(command_builder::Options {
|
||||||
@@ -11,33 +13,67 @@ fn main() -> Result<()> {
|
|||||||
dry_run: args.dry_run,
|
dry_run: args.dry_run,
|
||||||
});
|
});
|
||||||
|
|
||||||
match args.command {
|
match run(args.command) {
|
||||||
cli::Commands::Django { command } => match command {
|
Ok(code) => code,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {e:#}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(command: cli::Commands) -> Result<ExitCode> {
|
||||||
|
let done = ExitCode::SUCCESS;
|
||||||
|
|
||||||
|
match command {
|
||||||
|
cli::Commands::Django { command } => {
|
||||||
|
match command {
|
||||||
cli::Django::Bash => scripts::django::bash(),
|
cli::Django::Bash => scripts::django::bash(),
|
||||||
cli::Django::Run { rest } => scripts::django::run(&rest),
|
cli::Django::Run { rest } => scripts::django::run(&rest),
|
||||||
cli::Django::MakeCommand { app, name } => scripts::django::make_command(&app, &name),
|
cli::Django::MakeCommand { app, name } => {
|
||||||
|
scripts::django::make_command(&app, &name)
|
||||||
|
}
|
||||||
cli::Django::Makemigrations => scripts::django::makemigrations(),
|
cli::Django::Makemigrations => scripts::django::makemigrations(),
|
||||||
cli::Django::Manage { rest } => scripts::django::manage(&rest),
|
cli::Django::Manage { rest } => scripts::django::manage(&rest),
|
||||||
cli::Django::Migrate { rest } => scripts::django::migrate(&rest),
|
cli::Django::Migrate { rest } => scripts::django::migrate(&rest),
|
||||||
cli::Django::Shell => scripts::django::shell(),
|
cli::Django::Shell => scripts::django::shell(),
|
||||||
cli::Django::Test => scripts::django::test(),
|
cli::Django::Test => scripts::django::test(),
|
||||||
},
|
}?;
|
||||||
cli::Commands::Postgres { command } => match command {
|
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
|
cli::Commands::Postgres { command } => {
|
||||||
|
match command {
|
||||||
cli::Postgres::Import { path } => scripts::postgres::import(&path),
|
cli::Postgres::Import { path } => scripts::postgres::import(&path),
|
||||||
cli::Postgres::Dump { path, format, gzip } => {
|
cli::Postgres::Dump { path, format, gzip } => {
|
||||||
scripts::postgres::dump(&path, format, gzip)
|
scripts::postgres::dump(&path, format, gzip)
|
||||||
}
|
}
|
||||||
},
|
}?;
|
||||||
|
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
cli::Commands::Link { command } => match command {
|
cli::Commands::Link { command } => match command {
|
||||||
cli::Link::Add { paths, force } => scripts::link::add(&paths, force),
|
cli::Link::Add {
|
||||||
cli::Link::Restore { paths, all } => scripts::link::restore(&paths, all),
|
paths,
|
||||||
|
force,
|
||||||
|
store,
|
||||||
|
} => scripts::link::add(&paths, force, store.root.as_deref()).map(|()| done),
|
||||||
|
cli::Link::Restore { paths, all, store } => {
|
||||||
|
scripts::link::restore(&paths, all, store.root.as_deref()).map(|()| done)
|
||||||
|
}
|
||||||
|
// the one command with something to say through its exit code
|
||||||
cli::Link::Check {
|
cli::Link::Check {
|
||||||
paths,
|
paths,
|
||||||
porcelain,
|
porcelain,
|
||||||
null,
|
null,
|
||||||
exit_code,
|
exit_code,
|
||||||
} => scripts::link::check(&paths, porcelain, null, exit_code),
|
store,
|
||||||
|
} => scripts::link::check(&paths, porcelain, null, exit_code, store.root.as_deref()),
|
||||||
},
|
},
|
||||||
cli::Commands::Completions { shell } => scripts::completions::completions(shell),
|
cli::Commands::Completions { shell } => {
|
||||||
|
scripts::completions::completions(shell)?;
|
||||||
|
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub fn run(service: &str, rest: &[String]) -> Result<()> {
|
|||||||
.args("run --rm")
|
.args("run --rm")
|
||||||
.arg(service)
|
.arg(service)
|
||||||
.args(rest)
|
.args(rest)
|
||||||
.exec()
|
.exec_replace()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec(service: &str, rest: &[String]) -> Result<()> {
|
pub fn exec(service: &str, rest: &[String]) -> Result<()> {
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ use std::cell::Cell;
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
|
||||||
@@ -14,8 +13,8 @@ const BACKUP_SUFFIX: &str = ".ahab-bak";
|
|||||||
const LOCAL_NAMESPACE: &str = "_local";
|
const LOCAL_NAMESPACE: &str = "_local";
|
||||||
|
|
||||||
// move untracked paths out of the repo and symlink them back
|
// move untracked paths out of the repo and symlink them back
|
||||||
pub fn add(paths: &[PathBuf], force: bool) -> Result<()> {
|
pub fn add(paths: &[PathBuf], force: bool, store: Option<&Path>) -> Result<()> {
|
||||||
let repo = Repo::discover()?;
|
let repo = Repo::discover(store)?;
|
||||||
let report = Report::new(&repo);
|
let report = Report::new(&repo);
|
||||||
|
|
||||||
if let [path] = paths {
|
if let [path] = paths {
|
||||||
@@ -37,8 +36,8 @@ pub fn add(paths: &[PathBuf], force: bool) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// move paths in the store back into the repo, the inverse of add
|
// move paths in the store back into the repo, the inverse of add
|
||||||
pub fn restore(paths: &[PathBuf], all: bool) -> Result<()> {
|
pub fn restore(paths: &[PathBuf], all: bool, store: Option<&Path>) -> Result<()> {
|
||||||
let repo = Repo::discover()?;
|
let repo = Repo::discover(store)?;
|
||||||
let report = Report::new(&repo);
|
let report = Report::new(&repo);
|
||||||
|
|
||||||
let paths = match (all, paths) {
|
let paths = match (all, paths) {
|
||||||
@@ -179,8 +178,14 @@ fn warn(msg: impl std::fmt::Display) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// list untracked paths not in the store, i.e. what a sandbox can still read
|
// list untracked paths not in the store, i.e. what a sandbox can still read
|
||||||
pub fn check(paths: &[PathBuf], porcelain: bool, null: bool, exit_code: bool) -> Result<()> {
|
pub fn check(
|
||||||
let repo = Repo::discover()?;
|
paths: &[PathBuf],
|
||||||
|
porcelain: bool,
|
||||||
|
null: bool,
|
||||||
|
exit_code: bool,
|
||||||
|
store: Option<&Path>,
|
||||||
|
) -> Result<ExitCode> {
|
||||||
|
let repo = Repo::discover(store)?;
|
||||||
let pathspecs = relative_pathspecs(&repo, paths)?;
|
let pathspecs = relative_pathspecs(&repo, paths)?;
|
||||||
|
|
||||||
let mut exposed = Vec::new();
|
let mut exposed = Vec::new();
|
||||||
@@ -208,11 +213,10 @@ pub fn check(paths: &[PathBuf], porcelain: bool, null: bool, exit_code: bool) ->
|
|||||||
|
|
||||||
// git's --exit-code convention: nothing to report is 0, anything is 1
|
// git's --exit-code convention: nothing to report is 0, anything is 1
|
||||||
if exit_code && !exposed.is_empty() {
|
if exit_code && !exposed.is_empty() {
|
||||||
io::stdout().flush().context("writing the listing")?;
|
return Ok(ExitCode::FAILURE);
|
||||||
process::exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_porcelain(exposed: &[Exposed], null: bool) {
|
fn print_porcelain(exposed: &[Exposed], null: bool) {
|
||||||
@@ -424,10 +428,17 @@ struct Repo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Repo {
|
impl Repo {
|
||||||
fn discover() -> Result<Self> {
|
fn discover(store: Option<&Path>) -> Result<Self> {
|
||||||
let root = git_root()?;
|
let root = git_root()?;
|
||||||
let store = store_root()?.join(repo_components(&root)?);
|
let store = match store {
|
||||||
Ok(Self { root, store })
|
Some(store) => store.to_path_buf(),
|
||||||
|
None => store_root()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
root: root.clone(),
|
||||||
|
store: store.join(repo_components(&root)?),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relative(&self, src: &Path) -> Result<PathBuf> {
|
fn relative(&self, src: &Path) -> Result<PathBuf> {
|
||||||
@@ -679,18 +690,16 @@ fn sanitize(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn store_root() -> Result<PathBuf> {
|
fn store_root() -> Result<PathBuf> {
|
||||||
if let Some(root) = non_empty_var("AHAB_LINK_ROOT") {
|
|
||||||
return Ok(PathBuf::from(root));
|
|
||||||
}
|
|
||||||
if let Some(xdg) = non_empty_var("XDG_DATA_HOME") {
|
if let Some(xdg) = non_empty_var("XDG_DATA_HOME") {
|
||||||
return Ok(PathBuf::from(xdg).join("ahab"));
|
return Ok(PathBuf::from(xdg).join("ahab"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let home = non_empty_var("HOME")
|
let home = non_empty_var("HOME")
|
||||||
.ok_or_else(|| anyhow!("none of AHAB_LINK_ROOT, XDG_DATA_HOME or HOME is set"))?;
|
.filter(|v| !v.is_empty())
|
||||||
|
.ok_or_else(|| anyhow!("neither XDG_DATA_HOME nor HOME is set"))?;
|
||||||
|
|
||||||
Ok(PathBuf::from(home).join(".local/share/ahab"))
|
Ok(PathBuf::from(home).join(".local/share/ahab"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn non_empty_var(name: &str) -> Option<OsString> {
|
fn non_empty_var(name: &str) -> Option<OsString> {
|
||||||
env::var_os(name).filter(|v| !v.is_empty())
|
env::var_os(name).filter(|v| !v.is_empty())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user