From 2014b61e1280176eecd25f6aab03a2fcd80cf31a Mon Sep 17 00:00:00 2001 From: x71c9 <108585118+x71c9@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:21:50 +0200 Subject: [PATCH 1/2] fix: suggest on all new files --- src/commands/deploy.rs | 2 +- src/commands/infra.rs | 2 +- src/git/mod.rs | 29 ++++++++--------------------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index b1cf138..8c4ad04 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -97,7 +97,7 @@ pub fn run(repo_root: &Path, opts: DeployOptions) -> Result<()> { if matches!(answer.trim().to_lowercase().as_str(), "n" | "no") { bail!("aborted — commit or stash your changes and re-run"); } - let suggested = git::suggest_commit_msg(&repo); + let suggested = git::suggest_commit_msg(&dirty); let prompt_msg = if let Some(ref s) = suggested { log_info!("deploy suggested message: {}", crate::log::cyan(s)); eprint!( diff --git a/src/commands/infra.rs b/src/commands/infra.rs index dcfbd31..bd607a1 100644 --- a/src/commands/infra.rs +++ b/src/commands/infra.rs @@ -197,7 +197,7 @@ fn maybe_commit_dirty( if matches!(answer.trim().to_lowercase().as_str(), "n" | "no") { bail!("aborted — commit or stash your changes and re-run"); } - let suggested = git::suggest_commit_msg(repo); + let suggested = git::suggest_commit_msg(&dirty); let prompt_msg = if let Some(ref s) = suggested { log_info!("infra suggested message: {}", crate::log::cyan(s)); eprint!( diff --git a/src/git/mod.rs b/src/git/mod.rs index c47be66..c59fa90 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -313,31 +313,18 @@ fn push_refspec(repo: &Repository, refspec: &str) -> Result<()> { // Suggest commit message (heuristic, mirrors suggest-commit-msg.sh) // --------------------------------------------------------------------------- -pub fn suggest_commit_msg(repo: &Repository) -> Option { - let mut opts = git2::DiffOptions::new(); - opts.include_untracked(true); - let diff = repo.diff_index_to_workdir(None, Some(&mut opts)).ok()?; - - let mut files: Vec = Vec::new(); - diff - .foreach( - &mut |delta, _| { - if let Some(p) = delta.new_file().path() { - files.push(p.to_string_lossy().to_string()); - } - true - }, - None, - None, - None, - ) - .ok()?; +pub fn suggest_commit_msg(dirty: &DirtyFiles) -> Option { + let files: Vec = dirty.files.iter().map(|f| f.path.clone()).collect(); if files.is_empty() { return None; } - let commit_type = infer_type(&files); + let all_new = dirty + .files + .iter() + .all(|f| f.status == 'A' || f.status == '?'); + let commit_type = if all_new { "feat" } else { infer_type(&files) }; let scope = infer_scope(&files); let desc = files .iter() @@ -393,7 +380,7 @@ fn infer_type(files: &[String]) -> &'static str { return "chore"; } } - "fix" + "chore" } fn infer_scope(files: &[String]) -> Option { From 2598a0eeb0b42e4bb85ff7d44fd4063b3f8cf276 Mon Sep 17 00:00:00 2001 From: x71c9 <108585118+x71c9@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:12:39 +0200 Subject: [PATCH 2/2] feat: use interactive version selection in dogma deploy --- Cargo.lock | 49 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/commands/deploy.rs | 55 ++++++++++++++++++++++++------------------ src/git/mod.rs | 51 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8370868..cdc8309 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,12 +162,36 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "dirs" version = "6.0.0" @@ -207,6 +231,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "dialoguer", "git2", "indexmap", "pathdiff", @@ -224,6 +249,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.2" @@ -772,6 +803,12 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shellexpand" version = "3.1.2" @@ -882,6 +919,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -1193,6 +1236,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index b8c6e9d..98460b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" anyhow = "1" chrono = "0.4" clap = { version = "4", features = ["derive"] } +dialoguer = "0.12.0" git2 = { version = "0.20", default-features = false } indexmap = { version = "2", features = ["serde"] } pathdiff = "0.2" diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 8c4ad04..9b54e14 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -169,36 +169,43 @@ pub fn run(repo_root: &Path, opts: DeployOptions) -> Result<()> { tag } Mode::Interactive => { - let tags = git::list_deploy_tags(&repo)?; - if tags.is_empty() { + let tags_with_date = git::list_deploy_tags_with_date(&repo)?; + if tags_with_date.is_empty() { bail!( "no deploy/* tags found — run 'dogma deploy {} --new' first", opts.env ); } - log_info!("deploy available versions:"); - for (i, tag) in tags.iter().enumerate() { - let marker = if i == 0 { " (latest)" } else { "" }; - eprintln!(" [{}] {tag}{marker}", i + 1); - } - eprint!( - "{}select version to deploy to '{}' [1]: ", - crate::log::prompt_prefix(), - opts.env - ); - io::stderr().flush()?; - let mut sel = String::new(); - io::stdin().read_line(&mut sel)?; - let sel = sel.trim(); - let idx: usize = if sel.is_empty() { - 1 - } else { - sel.parse().context("invalid selection")? + let items: Vec = tags_with_date + .iter() + .enumerate() + .map(|(i, (t, date))| { + let date_part = if date.is_empty() { + String::new() + } else { + format!(" {date}") + }; + if i == 0 { + format!("{t}{date_part} (latest)") + } else { + format!("{t}{date_part}") + } + }) + .collect(); + let theme = dialoguer::theme::ColorfulTheme { + active_item_style: dialoguer::console::Style::new().for_stderr().red(), + active_item_prefix: dialoguer::console::style(">".to_string()) + .for_stderr() + .red(), + ..dialoguer::theme::ColorfulTheme::default() }; - if idx < 1 || idx > tags.len() { - bail!("selection out of range: {idx}"); - } - let tag = tags[idx - 1].clone(); + let idx = dialoguer::Select::with_theme(&theme) + .with_prompt(format!("select version to deploy to '{}'", opts.env)) + .items(&items) + .default(0) + .max_length(10) + .interact_on(&dialoguer::console::Term::stderr())?; + let tag = tags_with_date[idx].0.clone(); log_info!("deploy selected: {tag}"); log_info!("deploy checking out {tag} (detached HEAD) ..."); git::checkout_tag(&repo, &tag)?; diff --git a/src/git/mod.rs b/src/git/mod.rs index c59fa90..be3cb3e 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -151,6 +151,57 @@ pub fn list_deploy_tags(repo: &Repository) -> Result> { Ok(tags) } +/// Returns deploy tags sorted newest-first, each paired with a formatted date +/// string (`YYYY-MM-DD`). Falls back to the commit date when the tag is +/// lightweight (no tag object), and to an empty string on any error. +pub fn list_deploy_tags_with_date( + repo: &Repository, +) -> Result> { + let tags = list_deploy_tags(repo)?; + let pairs = tags + .into_iter() + .map(|name| { + let date = tag_date(repo, &name).unwrap_or_default(); + (name, date) + }) + .collect(); + Ok(pairs) +} + +fn tag_date(repo: &Repository, name: &str) -> Option { + let obj = repo.revparse_single(name).ok()?; + let time = if let Ok(tag) = obj.clone().into_tag() { + tag.tagger()?.when() + } else { + obj.peel_to_commit().ok()?.time() + }; + let secs = time.seconds(); + let dt = chrono::DateTime::::from_timestamp(secs, 0)? + .with_timezone(&chrono::Local); + let now = chrono::Local::now(); + let diff = now.signed_duration_since(dt); + let (n, unit) = if diff.num_seconds() < 60 { + (0u64, "just now") + } else if diff.num_minutes() < 60 { + (diff.num_minutes() as u64, "m ago") + } else if diff.num_hours() < 24 { + (diff.num_hours() as u64, "h ago") + } else if diff.num_days() < 30 { + (diff.num_days() as u64, "d ago") + } else if diff.num_days() < 365 { + ((diff.num_days() / 30) as u64, "mo ago") + } else { + ((diff.num_days() / 365) as u64, "y ago") + }; + let rel = if unit == "just now" { + "just now".to_string() + } else { + format!("{n:>2}{unit}") + }; + let relative = format!("{rel:<9}"); + Some(format!("{} {}", dt.format("%Y-%m-%d %H:%M"), relative)) +} + pub fn tag_exists(repo: &Repository, name: &str) -> Result { Ok( repo