From bd1285657dbc832b4756c9a309d849d2ed7507f3 Mon Sep 17 00:00:00 2001 From: flamestro Date: Thu, 16 Apr 2026 19:33:57 +0200 Subject: [PATCH] allow --only-uncommitted to only see current non committed changes --- README.md | 13 ++++++--- src/cli.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/git.rs | 29 ++++++++++++++++++++ src/model.rs | 2 ++ 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 02b4984..8bed92d 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Bundled syntax grammars are compiled into the binary, so removing the checkout d If you have local edits (including untracked files) and want to review them before committing, run: ```bash -deff --include-uncommitted +deff --only-uncommitted ``` This opens the side-by-side review so you can check exactly what changed in your working tree. @@ -38,6 +38,7 @@ This opens the side-by-side review so you can check exactly what changed in your - `upstream-ahead` strategy (default) to compare local branch changes against its upstream - `range` strategy for explicit `--base` / `--head` comparison - Optional `--include-uncommitted` mode to include working tree and untracked files +- `--only-uncommitted` mode to compare working tree and untracked files against `HEAD` - Side-by-side panes with independent horizontal scroll offsets - Keyboard and mouse navigation (including wheel + shift-wheel) - Vim-like motion navigation (`h`/`j`/`k`/`l`, `g`/`G`, `Ctrl+u`/`Ctrl+d`) @@ -62,6 +63,7 @@ deff deff --strategy upstream-ahead deff --strategy range --base origin/main --head HEAD deff --strategy range --base origin/main --include-uncommitted +deff --only-uncommitted deff --theme dark ``` @@ -104,9 +106,12 @@ Prerequisites: # explicit range deff --base origin/main --head HEAD - # include uncommitted + untracked files - deff --base origin/main --include-uncommitted - ``` + # include uncommitted + untracked files + deff --base origin/main --include-uncommitted + + # compare only working tree + untracked files against HEAD + deff --only-uncommitted + ``` If your branch has no upstream configured, use the explicit `--base` flow. diff --git a/src/cli.rs b/src/cli.rs index e6e3d03..15bb501 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,6 +13,7 @@ const DEFAULT_HEAD_REF: &str = "HEAD"; deff deff --strategy upstream-ahead deff --include-uncommitted + deff --only-uncommitted deff --strategy range --base [--head ] deff --strategy range --base --include-uncommitted deff --theme dark @@ -43,6 +44,8 @@ struct Cli { head: String, #[arg(long)] include_uncommitted: bool, + #[arg(long)] + only_uncommitted: bool, #[arg(long, value_enum, default_value_t = ThemeMode::Auto)] theme: ThemeMode, } @@ -53,6 +56,7 @@ pub(crate) struct CliOptions { pub(crate) base_ref: Option, pub(crate) head_ref: String, pub(crate) include_uncommitted: bool, + pub(crate) only_uncommitted: bool, pub(crate) theme_mode: ThemeMode, } @@ -83,6 +87,21 @@ impl TryFrom for CliOptions { bail!("--base can only be used with --strategy range"); } + if value.only_uncommitted { + if strategy_explicitly_set { + bail!("--only-uncommitted cannot be combined with --strategy"); + } + if value.base.is_some() { + bail!("--only-uncommitted cannot be combined with --base"); + } + if value.head != DEFAULT_HEAD_REF { + bail!("--only-uncommitted cannot be combined with --head"); + } + if value.include_uncommitted { + bail!("--only-uncommitted cannot be combined with --include-uncommitted"); + } + } + if value.include_uncommitted && value.head != DEFAULT_HEAD_REF { bail!("--include-uncommitted currently requires --head HEAD"); } @@ -92,6 +111,7 @@ impl TryFrom for CliOptions { base_ref: value.base, head_ref: value.head, include_uncommitted: value.include_uncommitted, + only_uncommitted: value.only_uncommitted, theme_mode: value.theme, }) } @@ -101,3 +121,59 @@ pub(crate) fn parse_cli_options() -> Result { let cli = Cli::parse(); CliOptions::try_from(cli) } + +#[cfg(test)] +mod tests { + use super::*; + + fn base_cli() -> Cli { + Cli { + strategy: None, + base: None, + head: DEFAULT_HEAD_REF.to_string(), + include_uncommitted: false, + only_uncommitted: false, + theme: ThemeMode::Auto, + } + } + + #[test] + fn only_uncommitted_sets_flag_on_options() { + let mut cli = base_cli(); + cli.only_uncommitted = true; + + let options = CliOptions::try_from(cli).expect("cli options should parse"); + + assert!(options.only_uncommitted); + assert!(!options.include_uncommitted); + } + + #[test] + fn only_uncommitted_rejects_strategy() { + let mut cli = base_cli(); + cli.only_uncommitted = true; + cli.strategy = Some(StrategyArg::Range); + cli.base = Some("origin/main".to_string()); + + let error = CliOptions::try_from(cli).expect_err("strategy should be rejected"); + assert!( + error + .to_string() + .contains("--only-uncommitted cannot be combined with --strategy") + ); + } + + #[test] + fn only_uncommitted_rejects_head_override() { + let mut cli = base_cli(); + cli.only_uncommitted = true; + cli.head = "HEAD~1".to_string(); + + let error = CliOptions::try_from(cli).expect_err("head override should be rejected"); + assert!( + error + .to_string() + .contains("--only-uncommitted cannot be combined with --head") + ); + } +} diff --git a/src/git.rs b/src/git.rs index 7b2f926..1852a89 100644 --- a/src/git.rs +++ b/src/git.rs @@ -171,10 +171,38 @@ fn resolve_range_comparison( }) } +fn resolve_only_uncommitted_comparison(repo_root: &Path) -> Result { + let current_branch = run_git_text(["rev-parse", "--abbrev-ref", "HEAD"], repo_root)? + .trim() + .to_string(); + let head_commit = run_git_text(["rev-parse", "HEAD^{commit}"], repo_root)? + .trim() + .to_string(); + + Ok(ResolvedComparison { + strategy_id: StrategyId::OnlyUncommitted, + base_ref: current_branch.clone(), + head_ref: current_branch.clone(), + base_commit: head_commit.clone(), + head_commit, + summary: format!("{current_branch}..WORKTREE"), + details: vec![ + format!("branch: {current_branch}"), + "mode: only-uncommitted".to_string(), + ], + ahead_count: None, + includes_uncommitted: true, + }) +} + pub(crate) fn resolve_comparison( repo_root: &Path, options: &CliOptions, ) -> Result { + if options.only_uncommitted { + return resolve_only_uncommitted_comparison(repo_root); + } + match options.strategy_id { StrategyId::Range => { let base_ref = options @@ -186,5 +214,6 @@ pub(crate) fn resolve_comparison( StrategyId::UpstreamAhead => { resolve_upstream_ahead_comparison(repo_root, &options.head_ref) } + StrategyId::OnlyUncommitted => resolve_only_uncommitted_comparison(repo_root), } } diff --git a/src/model.rs b/src/model.rs index dd2bc4f..c40b8b1 100644 --- a/src/model.rs +++ b/src/model.rs @@ -27,6 +27,7 @@ pub(crate) enum StrategyArg { pub(crate) enum StrategyId { UpstreamAhead, Range, + OnlyUncommitted, } impl Display for StrategyId { @@ -34,6 +35,7 @@ impl Display for StrategyId { match self { StrategyId::UpstreamAhead => write!(f, "upstream-ahead"), StrategyId::Range => write!(f, "range"), + StrategyId::OnlyUncommitted => write!(f, "only-uncommitted"), } } }