Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions src/commands/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,7 @@ impl CommitOptions<'_> {
}

if self.stage_mode == StageMode::All {
let status = wt
.run_command(&["status", "--porcelain", "-z", "-uall"])
.context("Failed to get status")?;
warn_about_untracked_files(&status)?;
warn_about_untracked_files(&wt)?;
}

// Stage changes based on mode. Re-gated inside `stage`: the refusal
Expand Down
49 changes: 30 additions & 19 deletions src/commands/repository_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ use super::worktree::{RemovalPlan, SharedBranchCheckout};
use anyhow::{Context, bail};
use color_print::cformat;
use worktrunk::git::{
BranchDeletionMode, GitError, IntegrationReason, RefSnapshot, Repository, WorktreeInfo,
parse_porcelain_z, parse_untracked_files,
BranchDeletionMode, GitError, IntegrationReason, RefSnapshot, Repository, WorkingTree,
WorktreeInfo, parse_porcelain_z, parse_untracked_files,
};
use worktrunk::path::format_path_for_display;
use worktrunk::styling::{eprintln, format_with_gutter, suggest_command, warning_message};
use worktrunk::styling::{
eprintln, format_with_gutter, hint_message, suggest_command, warning_message,
};

/// Target for worktree removal.
#[derive(Debug)]
Expand All @@ -31,9 +33,6 @@ pub enum RemoveTarget {
/// CLI-only helpers implemented on [`Repository`] via an extension trait so we can keep orphan
/// implementations inside the binary crate.
pub trait RepositoryCliExt {
/// Warn about untracked files being auto-staged.
fn warn_if_auto_staging_untracked(&self) -> anyhow::Result<()>;

/// Prepare the removal of whichever worktree or branch [`RemoveTarget`]
/// names.
///
Expand Down Expand Up @@ -99,15 +98,6 @@ pub trait RepositoryCliExt {
}

impl RepositoryCliExt for Repository {
fn warn_if_auto_staging_untracked(&self) -> anyhow::Result<()> {
// `-uall` overrides the user's display preference and expands untracked
// directories so the warning names every path `git add -A` will stage.
let status = self
.run_command(&["status", "--porcelain", "-z", "-uall"])
.context("Failed to get status")?;
warn_about_untracked_files(&status)
}

#[allow(clippy::too_many_arguments)]
fn prepare_worktree_removal(
&self,
Expand Down Expand Up @@ -620,8 +610,21 @@ pub(crate) fn check_not_default_branch(
}

/// Warn about untracked files that will be auto-staged.
pub(crate) fn warn_about_untracked_files(status_output: &str) -> anyhow::Result<()> {
let files = parse_untracked_files(status_output);
///
/// Paths come from git's `normal` untracked mode, which overrides a
/// `status.showUntrackedFiles=no` that would hide them and names a wholly
/// untracked directory once as `dir/`. A directory of generated files then
/// takes one row instead of pushing the paths beside it past the cap.
///
/// The listing has at most `MAX_ROWS` rows. Past that many paths, the last row
/// is a hint counting the rest, which is always at least two paths.
pub(crate) fn warn_about_untracked_files(wt: &WorkingTree) -> anyhow::Result<()> {
const MAX_ROWS: usize = 10;

let status = wt
.run_command(&["status", "--porcelain", "-z", "-unormal"])
.context("Failed to get status")?;
let files = parse_untracked_files(&status);
if files.is_empty() {
return Ok(());
}
Expand All @@ -633,8 +636,16 @@ pub(crate) fn warn_about_untracked_files(status_output: &str) -> anyhow::Result<
warning_message(format!("Auto-staging {count} untracked {path_word}:"))
);

let joined_files = files.join("\n");
eprintln!("{}", format_with_gutter(&joined_files, None));
let listed = if count > MAX_ROWS {
MAX_ROWS - 1
} else {
count
};
eprintln!("{}", format_with_gutter(&files[..listed].join("\n"), None));
if listed < count {
let omitted = count - listed;
eprintln!("{}", hint_message(format!("… and {omitted} other paths")));
}

Ok(())
}
Expand Down
4 changes: 2 additions & 2 deletions src/commands/step/squash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use super::super::command_executor::FailureStrategy;
use super::super::commit::{CommitGenerator, CommitOutcome, HookGate, StageMode};
use super::super::context::CommandEnv;
use super::super::hooks::{HookAnnouncer, execute_hook};
use super::super::repository_ext::RepositoryCliExt;
use super::super::repository_ext::warn_about_untracked_files;
use super::super::template_vars::TemplateVars;
use super::shared::print_dry_run;

Expand Down Expand Up @@ -160,7 +160,7 @@ pub fn handle_squash(

// Auto-stage changes before running pre-commit hooks so both beta and merge paths behave identically
if stage_mode == StageMode::All {
repo.warn_if_auto_staging_untracked()?;
warn_about_untracked_files(&wt)?;
}
wt.stage(stage_mode)?;

Expand Down
36 changes: 32 additions & 4 deletions tests/integration_tests/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,8 @@ fn assert_hidden_untracked_auto_staging_warning(output: &std::process::Output, c
"{command} should succeed; stderr:\n{stderr}"
);
assert!(
stderr.contains("Auto-staging 2 untracked paths:")
&& stderr.contains("nested/first.txt")
&& stderr.contains("nested/second.txt"),
"the warning must enumerate every hidden file that git add -A will stage; stderr:\n{stderr}"
stderr.contains("Auto-staging 1 untracked path:") && stderr.contains("nested/\n"),
"the warning must name the hidden directory git add -A will stage; stderr:\n{stderr}"
);
}

Expand Down Expand Up @@ -2769,6 +2767,36 @@ fn test_step_commit_auto_staging_warns_about_untracked_files_hidden_by_user_conf
assert_hidden_untracked_auto_staging_warning(&output, "step commit");
}

/// Ten paths fill the listing's ten rows. One more lists nine and counts the
/// other two, so the hint never takes the row a single remaining path would
/// have used. `0-generated/` sorts first and holds 300 files, but as a wholly
/// untracked directory it takes one row and leaves the rest of the listing to
/// the paths beside it.
#[rstest]
fn test_step_commit_auto_staging_caps_untracked_listing(
repo: TestRepo,
#[values(10, 11)] count: usize,
) {
let generated = repo.root_path().join("0-generated");
fs::create_dir(&generated).unwrap();
for i in 1..=300 {
fs::write(generated.join(format!("{i:03}.txt")), "").unwrap();
}
for i in 1..count {
fs::write(repo.root_path().join(format!("{i:02}.txt")), "").unwrap();
}

let mut cmd = make_snapshot_cmd(&repo, "step", &["commit"], None);
cmd.env(
"WORKTRUNK_COMMIT__GENERATION__COMMAND",
"cat >/dev/null && echo 'feat: add generated files'",
);
assert_cmd_snapshot!(
format!("step_commit_auto_staging_caps_untracked_listing_{count}"),
cmd
);
}

#[rstest]
fn test_step_commit_with_stage_tracked_flag(repo: TestRepo) {
fs::write(repo.root_path().join("tracked.txt"), "initial").expect("Failed to write file");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
source: tests/integration_tests/merge.rs
info:
program: wt
args:
- step
- commit
env:
APPDATA: "[TEST_CONFIG_HOME]"
CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]"
CLICOLOR_FORCE: "1"
COLUMNS: "500"
GIT_ALLOW_PROTOCOL: file
GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
GIT_AUTHOR_EMAIL: test@example.com
GIT_AUTHOR_NAME: Test User
GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
GIT_COMMITTER_EMAIL: test@example.com
GIT_COMMITTER_NAME: Test User
GIT_CONFIG_COUNT: "2"
GIT_CONFIG_GLOBAL: /nonexistent/wt/gitconfig
GIT_CONFIG_KEY_0: user.useConfigOnly
GIT_CONFIG_KEY_1: rerere.enabled
GIT_CONFIG_SYSTEM: /nonexistent/wt/gitconfig
GIT_CONFIG_VALUE_0: "true"
GIT_CONFIG_VALUE_1: "false"
GIT_TERMINAL_PROMPT: "0"
HOME: "[TEST_HOME]"
LANG: C
LC_ALL: C
LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]"
OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]"
PATH: "[PATH]"
TERM: alacritty
USERPROFILE: "[TEST_HOME]"
WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
WORKTRUNK_COMMIT__GENERATION__COMMAND: "cat >/dev/null && echo 'feat: add generated files'"
WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
WORKTRUNK_TEST_BASH_INSTALLED: "0"
WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
WORKTRUNK_TEST_CODEX_INSTALLED: "0"
WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
WORKTRUNK_TEST_EPOCH: "1735776000"
WORKTRUNK_TEST_FISH_INSTALLED: "0"
WORKTRUNK_TEST_GEMINI_INSTALLED: "0"
WORKTRUNK_TEST_MOCK_CONFIG_DIR: "[TEST_MOCK_CONFIG]"
WORKTRUNK_TEST_NUSHELL_ENV: "0"
WORKTRUNK_TEST_OPENCODE_INSTALLED: "0"
WORKTRUNK_TEST_PARENT_SHELL: ""
WORKTRUNK_TEST_POWERSHELL_ENV: "0"
WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0"
WORKTRUNK_TEST_PROBE_TIMEOUT_MS: "60000"
WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
WORKTRUNK_TEST_ZSH_INSTALLED: "0"
XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
---
success: true
exit_code: 0
----- stdout -----

----- stderr -----
▲ Auto-staging 10 untracked paths:
  0-generated/
  01.txt
  02.txt
  03.txt
  04.txt
  05.txt
  06.txt
  07.txt
  08.txt
  09.txt
◎ Generating commit message and committing changes... (309 files)
  feat: add generated files
✓ Committed changes @ [HASH]
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
source: tests/integration_tests/merge.rs
info:
program: wt
args:
- step
- commit
env:
APPDATA: "[TEST_CONFIG_HOME]"
CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]"
CLICOLOR_FORCE: "1"
COLUMNS: "500"
GIT_ALLOW_PROTOCOL: file
GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
GIT_AUTHOR_EMAIL: test@example.com
GIT_AUTHOR_NAME: Test User
GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
GIT_COMMITTER_EMAIL: test@example.com
GIT_COMMITTER_NAME: Test User
GIT_CONFIG_COUNT: "2"
GIT_CONFIG_GLOBAL: /nonexistent/wt/gitconfig
GIT_CONFIG_KEY_0: user.useConfigOnly
GIT_CONFIG_KEY_1: rerere.enabled
GIT_CONFIG_SYSTEM: /nonexistent/wt/gitconfig
GIT_CONFIG_VALUE_0: "true"
GIT_CONFIG_VALUE_1: "false"
GIT_TERMINAL_PROMPT: "0"
HOME: "[TEST_HOME]"
LANG: C
LC_ALL: C
LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]"
OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]"
PATH: "[PATH]"
TERM: alacritty
USERPROFILE: "[TEST_HOME]"
WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
WORKTRUNK_COMMIT__GENERATION__COMMAND: "cat >/dev/null && echo 'feat: add generated files'"
WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
WORKTRUNK_TEST_BASH_INSTALLED: "0"
WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
WORKTRUNK_TEST_CODEX_INSTALLED: "0"
WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
WORKTRUNK_TEST_EPOCH: "1735776000"
WORKTRUNK_TEST_FISH_INSTALLED: "0"
WORKTRUNK_TEST_GEMINI_INSTALLED: "0"
WORKTRUNK_TEST_MOCK_CONFIG_DIR: "[TEST_MOCK_CONFIG]"
WORKTRUNK_TEST_NUSHELL_ENV: "0"
WORKTRUNK_TEST_OPENCODE_INSTALLED: "0"
WORKTRUNK_TEST_PARENT_SHELL: ""
WORKTRUNK_TEST_POWERSHELL_ENV: "0"
WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0"
WORKTRUNK_TEST_PROBE_TIMEOUT_MS: "60000"
WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
WORKTRUNK_TEST_ZSH_INSTALLED: "0"
XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
---
success: true
exit_code: 0
----- stdout -----

----- stderr -----
▲ Auto-staging 11 untracked paths:
  0-generated/
  01.txt
  02.txt
  03.txt
  04.txt
  05.txt
  06.txt
  07.txt
  08.txt
↳ … and 2 other paths
◎ Generating commit message and committing changes... (310 files)
  feat: add generated files
✓ Committed changes @ [HASH]
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ exit_code: 0
----- stderr -----
○ Skipping pre-commit hooks (--no-hooks)
▲ Auto-staging 2 untracked paths:
  .config/wt.toml
  .config/
  file1.txt
◎ Generating commit message and committing changes... (2 files, +2)
  feat: add file
Expand Down
Loading