From 26708cebf7b0c3ac45b42ae213971e4b99f6f25c Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Mon, 15 Jun 2026 00:07:26 +0200 Subject: [PATCH 01/14] Add `--commit` flag to restart on git HEAD changes --- src/lib.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 142 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 52f2fab..1b122e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,55 @@ pub fn xtask_command() -> Command { Command::new(env::args_os().next().unwrap()) } +/// Resolve the actual git directory path via `git rev-parse --git-dir`. +/// +/// Git handles regular repos, worktrees, and submodules transparently. +fn resolve_git_dir(repo_root: &Path) -> Result { + let output = Command::new("git") + .args(["rev-parse", "--git-dir"]) + .current_dir(repo_root) + .output() + .context("failed to run `git rev-parse --git-dir`")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("not a git repository: {stderr}"); + } + + let dir = String::from_utf8(output.stdout).context("git output is not valid UTF-8")?; + let dir = dir.trim(); + + let path = if Path::new(dir).is_absolute() { + PathBuf::from(dir) + } else { + repo_root.join(dir) + }; + + path.canonicalize() + .with_context(|| format!("canonicalize git dir `{dir}`")) +} + +/// Get the current HEAD commit hash via `git rev-parse HEAD`. +fn get_current_head() -> Result { + let output = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(metadata().workspace_root.as_std_path()) + .output() + .context("failed to run `git rev-parse HEAD`")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("git rev-parse HEAD failed: {stderr}"); + } + + let hash = String::from_utf8(output.stdout) + .context("git output is not valid UTF-8")? + .trim() + .to_string(); + + Ok(hash) +} + /// Watches over your project's source code, relaunching a given command when /// changes are detected. /// @@ -71,6 +120,14 @@ pub struct Watch { /// Paths or glob patterns, relative to the workspace root, that will be excluded. #[clap(skip)] pub workspace_exclude_paths: Vec, + /// Watch for commit changes in addition to file changes. + /// + /// Monitors the git directory (resolved via `git rev-parse --git-dir`) + /// for when the current git commit (HEAD) changes. + /// For worktrees, the watched directory is the git directory resolved + /// via `git rev-parse --git-dir`, not the workspace-local `.git` file. + #[clap(long = "commit")] + pub commit: bool, /// Throttle events to prevent the command to be re-executed too early /// right after an execution already occurred. /// @@ -153,6 +210,12 @@ impl Watch { self.watch_lock.clone() } + /// Enable commit mode: also restart the command when the git HEAD changes. + pub fn commit(mut self) -> Self { + self.commit = true; + self + } + /// Set the debounce duration after relaunching the command. pub fn debounce(mut self, duration: Duration) -> Self { self.debounce = duration; @@ -194,6 +257,15 @@ impl Watch { self.prepare_excludes()?; + let git_dirs: Vec = if self.commit { + let git_dir = resolve_git_dir(metadata.workspace_root.as_std_path()) + .context("--commit requires a git repository")?; + self.watch_paths.push(git_dir.clone()); + vec![git_dir] + } else { + Vec::new() + }; + if self.watch_paths.is_empty() { self.watch_paths .push(metadata.workspace_root.clone().into_std_path_buf()); @@ -210,10 +282,24 @@ impl Watch { let (tx, rx) = mpsc::channel(); + let current_commit = if self.commit { + match get_current_head() { + Ok(hash) => Some(hash), + Err(err) => { + log::warn!("failed to read initial git HEAD: {err:?}"); + None + } + } + } else { + None + }; + let handler = WatchEventHandler { watch: self.clone(), tx: tx.clone(), command_start: Instant::now(), + current_commit, + git_dirs, }; let mut watcher = @@ -407,31 +493,69 @@ struct WatchEventHandler { watch: Watch, tx: mpsc::Sender, command_start: Instant, + current_commit: Option, + git_dirs: Vec, } impl notify::EventHandler for WatchEventHandler { fn handle_event(&mut self, event: Result) { - match event { - Ok(event) => { - if (event.kind.is_modify() || event.kind.is_create()) - && event.paths.iter().any(|x| { - !self.watch.is_excluded_path(x) - && x.exists() - && !self.watch.is_hidden_path(x) - && !self.watch.is_backup_file(x) - && self.command_start.elapsed() >= self.watch.debounce - }) - { - log::trace!("Changes detected in {event:?}"); - self.command_start = Instant::now(); - - self.tx.send(Event::ChangeDetected).expect("can send"); - } else { - log::trace!("Ignoring changes in {event:?}"); + let event = match event { + Ok(event) => event, + Err(err) => { + log::error!("watch error: {err}"); + return; + } + }; + + if self.command_start.elapsed() < self.watch.debounce { + log::trace!("Ignoring changes (debounce): {event:?}"); + return; + } + + let valid: Vec<_> = event + .paths + .iter() + .filter(|x| { + (event.kind.is_modify() || event.kind.is_create()) + && !self.watch.is_excluded_path(x) + && x.exists() + && !self.watch.is_hidden_path(x) + && !self.watch.is_backup_file(x) + }) + .collect(); + + if valid.is_empty() { + log::trace!("Ignoring changes in {event:?}"); + return; + } + + if self.watch.commit { + let all_under_git = valid + .iter() + .all(|p| self.git_dirs.iter().any(|g| p.starts_with(g))); + + if all_under_git { + match get_current_head() { + Ok(hash) if Some(hash.as_str()) != self.current_commit.as_deref() => { + log::trace!("HEAD changed: {:?} -> {hash}", self.current_commit); + self.current_commit = Some(hash); + self.command_start = Instant::now(); + self.tx.send(Event::ChangeDetected).expect("can send"); + } + Ok(_) => { + log::trace!("HEAD unchanged, ignoring event"); + } + Err(err) => { + log::error!("failed to read git HEAD: {err}"); + } } + return; } - Err(err) => log::error!("watch error: {err}"), } + + log::trace!("Changes detected in {event:?}"); + self.command_start = Instant::now(); + self.tx.send(Event::ChangeDetected).expect("can send"); } } From 7d36544867dda8837683ce55c00ea4fd9563ff91 Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Mon, 15 Jun 2026 00:12:20 +0200 Subject: [PATCH 02/14] Update `CHANGELOG.md` --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10f20b..27af5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `--commit` flag to also restart the command when git HEAD changes. (#36) + ## [0.3.4] - 2026-05-17 ### Fixed From 44e20ffe6c0d1586a29c241d2a6e3e6ebceca30b Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Sun, 21 Jun 2026 00:39:43 +0200 Subject: [PATCH 03/14] refactor: simplify handle_event with early returns and filtered paths --- CHANGELOG.md | 6 ++++++ src/lib.rs | 29 +++++++++++++++++++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ceb5d..f9687de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `--commit` flag to also restart the command when git HEAD changes. (#36) +### Changed + +- Commit-change detection now considers only valid event paths (existing, non-excluded, + non-hidden, non-backup files), ignoring stale or deleted paths that could previously + cause a `ChangeDetected` event to be emitted instead. (#36) + ## [0.3.5] - 2026-06-02 ### Changed diff --git a/src/lib.rs b/src/lib.rs index 7f31786..82cdd18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -534,18 +534,27 @@ impl notify::EventHandler for WatchEventHandler { fn handle_event(&mut self, event: Result) { match event { Ok(event) => { - if (event.kind.is_modify() || event.kind.is_create()) - && event.paths.iter().any(|x| { - !self.watch.is_excluded_path(x) - && x.exists() - && !self.watch.is_hidden_path(x) - && !self.watch.is_backup_file(x) - }) - { - if event + if event.kind.is_modify() || event.kind.is_create() { + let valids = event .paths .iter() - .all(|p| self.git_dirs.iter().any(|g| p.starts_with(g))) + .filter(|x| { + x.exists() + && !self.watch.is_excluded_path(x) + && !self.watch.is_hidden_path(x) + && !self.watch.is_backup_file(x) + }) + .collect::>(); + + if valids.is_empty() { + log::trace!("Ignoring changes in {event:?}"); + return; + } + + if self.watch.commit + && valids + .iter() + .all(|p| self.git_dirs.iter().any(|g| p.starts_with(g))) { match get_current_head() { Ok(hash) if Some(hash.as_str()) != self.current_commit.as_deref() => { From 79bbb1d1b4eb58fce3d56a08bdcf2af3fbff1bd8 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Sun, 21 Jun 2026 17:32:35 +0200 Subject: [PATCH 04/14] refactor: address review feedback in handle_event - Add Watch::is_valid_path helper to consolidate path validity checks. - Use a peekable iterator instead of collecting valid paths into a Vec. - Rename valids to valid_paths. - Differentiate trace messages for ignored events. --- src/lib.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 82cdd18..d747166 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -457,6 +457,13 @@ impl Watch { }) } + fn is_valid_path(&self, path: &Path) -> bool { + path.exists() + && !self.is_excluded_path(path) + && !self.is_hidden_path(path) + && !self.is_backup_file(path) + } + fn is_glob_pattern(path: &Path) -> bool { let s = path.as_os_str().to_string_lossy(); s.contains('*') || s.contains('?') || (!cfg!(windows) && s.contains('[')) @@ -535,26 +542,19 @@ impl notify::EventHandler for WatchEventHandler { match event { Ok(event) => { if event.kind.is_modify() || event.kind.is_create() { - let valids = event + let mut valid_paths = event .paths .iter() - .filter(|x| { - x.exists() - && !self.watch.is_excluded_path(x) - && !self.watch.is_hidden_path(x) - && !self.watch.is_backup_file(x) - }) - .collect::>(); - - if valids.is_empty() { - log::trace!("Ignoring changes in {event:?}"); + .filter(|p| self.watch.is_valid_path(p)) + .peekable(); + + if valid_paths.peek().is_none() { + log::trace!("No valid paths in {event:?}, ignoring"); return; } if self.watch.commit - && valids - .iter() - .all(|p| self.git_dirs.iter().any(|g| p.starts_with(g))) + && valid_paths.all(|p| self.git_dirs.iter().any(|g| p.starts_with(g))) { match get_current_head() { Ok(hash) if Some(hash.as_str()) != self.current_commit.as_deref() => { @@ -575,7 +575,7 @@ impl notify::EventHandler for WatchEventHandler { log::trace!("Changes detected in {event:?}"); self.tx.send(Event::ChangeDetected).expect("can send"); } else { - log::trace!("Ignoring changes in {event:?}"); + log::trace!("Ignoring non-create/modify event: {event:?}"); } } Err(err) => log::error!("watch error: {err}"), From 2be8b14bc5bcad3d64435b0fcc6b532077718ded Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 08:28:32 +0200 Subject: [PATCH 05/14] refactor: deduplicate ChangeDetected send in handle_event Move the early returns into the HEAD-unchanged and HEAD-read-error branches so that Event::ChangeDetected is sent from a single place. --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d747166..e6640d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -560,16 +560,16 @@ impl notify::EventHandler for WatchEventHandler { Ok(hash) if Some(hash.as_str()) != self.current_commit.as_deref() => { log::trace!("HEAD changed: {:?} -> {hash}", self.current_commit); self.current_commit = Some(hash); - self.tx.send(Event::ChangeDetected).expect("can send"); } Ok(_) => { log::trace!("HEAD unchanged, ignoring event"); + return; } Err(err) => { log::error!("failed to read git HEAD: {err}"); + return; } } - return; } log::trace!("Changes detected in {event:?}"); From d09d1a0e0c0ca385ed6d1b9f9397ef6bcdf3fc8c Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 10:04:18 +0200 Subject: [PATCH 06/14] perf: defer git HEAD check to after debounce period Move the get_current_head() call (git rev-parse HEAD) from the file event handler into the main event loop, so it runs at most once per debounce cycle instead of on every individual filesystem event. Previously, every notify event targeting the git directory spawned a git subprocess to check whether HEAD had changed. During a single git operation (rebase, commit, fetch) this could trigger dozens of redundant git invocations before the debounce timer even started. Now the event handler just records the type of change detected (file change vs git-dir change) via two flags. After the debounce settles, if only git-dir changes are pending and --commit is on, a single get_current_head() call confirms whether HEAD actually changed before spawning the build. Real file changes still bypass the HEAD check and always trigger a build. --- src/lib.rs | 78 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e6640d8..27602e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -292,22 +292,9 @@ impl Watch { let (tx, rx) = mpsc::channel(); - let current_commit = if self.commit { - match get_current_head() { - Ok(hash) => Some(hash), - Err(err) => { - log::warn!("failed to read initial git HEAD: {err:?}"); - None - } - } - } else { - None - }; - let handler = WatchEventHandler { watch: self.clone(), tx: tx.clone(), - current_commit, git_dirs, }; @@ -329,10 +316,23 @@ impl Watch { // been translated into a spawned command. It starts as `true` so the // first build fires immediately without waiting for a file-change event. let mut pending_build = true; + let mut current_commit = if self.commit { + match get_current_head() { + Ok(hash) => Some(hash), + Err(err) => { + log::warn!("failed to read initial git HEAD: {err:?}"); + None + } + } + } else { + None + }; + let mut has_file_changes = false; loop { if pending_build { pending_build = false; + has_file_changes = false; log::info!("Running command"); let mut current_child = current_child.clone(); let mut list = list.clone(); @@ -373,6 +373,7 @@ impl Watch { match rx.recv_timeout(self.debounce) { Ok(Event::ChangeDetected) => { log::trace!("Change detected, resetting debounce timer"); + has_file_changes = true; if !pending_build { // Cancel any in-progress build immediately so we // build the latest version, not an intermediate one. @@ -385,6 +386,17 @@ impl Watch { } // Loop back to reset the recv_timeout. } + Ok(Event::GitDirChangeDetected) => { + log::trace!("Git directory change detected, resetting debounce timer"); + if !pending_build { + current_child.terminate(); + generation += 1; + if lock_guard.is_none() { + lock_guard = Some(self.watch_lock.write()); + } + pending_build = true; + } + } Ok(Event::CommandSucceeded(build_id)) if build_id == generation => { log::trace!("Command succeeded, releasing lock"); lock_guard.take(); @@ -399,6 +411,27 @@ impl Watch { // Quiet for `debounce` — time to build if there is a // pending change. if pending_build { + // Only check HEAD if commit mode and no real file changes. + if self.commit && !has_file_changes { + match get_current_head() { + Ok(hash) + if Some(hash.as_str()) != current_commit.as_deref() => + { + log::trace!("HEAD changed: {:?} -> {hash}", current_commit); + current_commit = Some(hash); + } + Ok(_) => { + log::trace!("HEAD unchanged, skipping build"); + pending_build = false; + continue; + } + Err(err) => { + log::error!("failed to read git HEAD: {err}"); + pending_build = false; + continue; + } + } + } break; } } @@ -533,7 +566,6 @@ impl Watch { struct WatchEventHandler { watch: Watch, tx: mpsc::Sender, - current_commit: Option, git_dirs: Vec, } @@ -556,20 +588,9 @@ impl notify::EventHandler for WatchEventHandler { if self.watch.commit && valid_paths.all(|p| self.git_dirs.iter().any(|g| p.starts_with(g))) { - match get_current_head() { - Ok(hash) if Some(hash.as_str()) != self.current_commit.as_deref() => { - log::trace!("HEAD changed: {:?} -> {hash}", self.current_commit); - self.current_commit = Some(hash); - } - Ok(_) => { - log::trace!("HEAD unchanged, ignoring event"); - return; - } - Err(err) => { - log::error!("failed to read git HEAD: {err}"); - return; - } - } + log::trace!("Git directory change detected in {event:?}"); + self.tx.send(Event::GitDirChangeDetected).expect("can send"); + return; } log::trace!("Changes detected in {event:?}"); @@ -776,6 +797,7 @@ impl WatchLock { enum Event { CommandSucceeded(u64), ChangeDetected, + GitDirChangeDetected, } #[cfg(test)] From a3a7f16cd13558c9df56fcf861cc4b95569f1ac7 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 11:49:13 +0200 Subject: [PATCH 07/14] fix: allow paths under explicitly-watched hidden directories (e.g. .git/ with --commit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is_hidden_path was filtering out all paths whose first component (relative to a watch path) starts with '.'. When --commit adds .git/ to watch_paths, and a parent of .git/ is also watched, git directory changes like .git/HEAD were silently dropped. The fix: if the path lives under a watch path whose own last component starts with '.', the user explicitly opted into watching it — don't treat it as hidden. --- CHANGELOG.md | 6 ++++++ src/lib.rs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9687de..5bbdc4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 non-hidden, non-backup files), ignoring stale or deleted paths that could previously cause a `ChangeDetected` event to be emitted instead. (#36) +### Fixed + +- When `--commit` is used alongside a watch path that is a parent of `.git`, + git directory changes were incorrectly filtered out by the hidden-path check. + `is_hidden_path` now allows paths under explicitly-watched hidden directories. (#36) + ## [0.3.5] - 2026-06-02 ### Changed diff --git a/src/lib.rs b/src/lib.rs index 27602e6..2ec5002 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -475,6 +475,18 @@ impl Watch { } fn is_hidden_path(&self, path: &Path) -> bool { + // If the path is under a watch path whose last component starts + // with '.', the user explicitly opted into watching it — don't + // treat it as hidden (e.g. --commit adds .git/ to watch_paths). + if self.watch_paths.iter().any(|x| { + x.file_name() + .and_then(|s| s.to_str()) + .is_some_and(|s| s.starts_with('.')) + && path.starts_with(x) + }) { + return false; + } + self.watch_paths.iter().any(|x| { path.strip_prefix(x) .iter() From d776f891169bb6d55fe0a372c715935f59067762 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 12:17:49 +0200 Subject: [PATCH 08/14] refactor: consolidate ChangeDetected and GitDirChangeDetected into single variant Merge two near-identical Event variants (ChangeDetected, GitDirChangeDetected) into one with a field. means real file change (sets has_file_changes), means git directory metadata change only (triggers HEAD hash check before build). Eliminates duplicated cancel-in-progress logic across the two match arms while preserving the semantic distinction that controls when the git HEAD staleness check runs. --- src/lib.rs | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2ec5002..0e8be8f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -371,23 +371,13 @@ impl Watch { // been quiet for `debounce`. loop { match rx.recv_timeout(self.debounce) { - Ok(Event::ChangeDetected) => { - log::trace!("Change detected, resetting debounce timer"); - has_file_changes = true; - if !pending_build { - // Cancel any in-progress build immediately so we - // build the latest version, not an intermediate one. - current_child.terminate(); - generation += 1; - if lock_guard.is_none() { - lock_guard = Some(self.watch_lock.write()); - } - pending_build = true; + Ok(Event::ChangeDetected { git }) => { + if git { + log::trace!("Git directory change detected, resetting debounce timer"); + } else { + log::trace!("Change detected, resetting debounce timer"); + has_file_changes = true; } - // Loop back to reset the recv_timeout. - } - Ok(Event::GitDirChangeDetected) => { - log::trace!("Git directory change detected, resetting debounce timer"); if !pending_build { current_child.terminate(); generation += 1; @@ -601,12 +591,16 @@ impl notify::EventHandler for WatchEventHandler { && valid_paths.all(|p| self.git_dirs.iter().any(|g| p.starts_with(g))) { log::trace!("Git directory change detected in {event:?}"); - self.tx.send(Event::GitDirChangeDetected).expect("can send"); + self.tx + .send(Event::ChangeDetected { git: true }) + .expect("can send"); return; } log::trace!("Changes detected in {event:?}"); - self.tx.send(Event::ChangeDetected).expect("can send"); + self.tx + .send(Event::ChangeDetected { git: false }) + .expect("can send"); } else { log::trace!("Ignoring non-create/modify event: {event:?}"); } @@ -808,8 +802,7 @@ impl WatchLock { #[derive(Debug)] enum Event { CommandSucceeded(u64), - ChangeDetected, - GitDirChangeDetected, + ChangeDetected { git: bool }, } #[cfg(test)] From e037fa2551a5357330449c5241657268b36b7d3b Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 12:30:45 +0200 Subject: [PATCH 09/14] refactor: remove repo_root parameter from resolve_git_dir Hardcodes metadata().workspace_root inside function, consistent with get_current_head. Single call site already passed same value. --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0e8be8f..6dc1399 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,8 @@ pub fn xtask_command() -> Command { /// Resolve the actual git directory path via `git rev-parse --git-dir`. /// /// Git handles regular repos, worktrees, and submodules transparently. -fn resolve_git_dir(repo_root: &Path) -> Result { +fn resolve_git_dir() -> Result { + let repo_root = metadata().workspace_root.as_std_path(); let output = Command::new("git") .args(["rev-parse", "--git-dir"]) .current_dir(repo_root) @@ -268,7 +269,7 @@ impl Watch { self.prepare_excludes()?; let git_dirs: Vec = if self.commit { - let git_dir = resolve_git_dir(metadata.workspace_root.as_std_path()) + let git_dir = resolve_git_dir() .context("--commit requires a git repository")?; self.watch_paths.push(git_dir.clone()); vec![git_dir] From 1777381af374396f4fcd16ad87a31e55c37bd994 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 12:31:21 +0200 Subject: [PATCH 10/14] fix: propagate initial get_current_head error instead of logging Failing to read HEAD at startup means --commit cannot work. Error now propagates via ? like resolve_git_dir, rather than silently setting current_commit to None which causes a spurious rebuild on first successful HEAD read. --- src/lib.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6dc1399..1227819 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -318,13 +318,7 @@ impl Watch { // first build fires immediately without waiting for a file-change event. let mut pending_build = true; let mut current_commit = if self.commit { - match get_current_head() { - Ok(hash) => Some(hash), - Err(err) => { - log::warn!("failed to read initial git HEAD: {err:?}"); - None - } - } + Some(get_current_head()?) } else { None }; From 987c2e95216364e07e795b24c68852bab8f2c8db Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 12:33:36 +0200 Subject: [PATCH 11/14] refactor: replace self.commit guard with if let on current_commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit None means !self.commit — single source of truth eliminates redundant field check. --- src/lib.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1227819..f410ac7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -396,24 +396,23 @@ impl Watch { // Quiet for `debounce` — time to build if there is a // pending change. if pending_build { - // Only check HEAD if commit mode and no real file changes. - if self.commit && !has_file_changes { - match get_current_head() { - Ok(hash) - if Some(hash.as_str()) != current_commit.as_deref() => - { - log::trace!("HEAD changed: {:?} -> {hash}", current_commit); - current_commit = Some(hash); - } - Ok(_) => { - log::trace!("HEAD unchanged, skipping build"); - pending_build = false; - continue; - } - Err(err) => { - log::error!("failed to read git HEAD: {err}"); - pending_build = false; - continue; + if !has_file_changes { + if let Some(current_hash) = ¤t_commit { + match get_current_head() { + Ok(hash) if &hash != current_hash => { + log::trace!("HEAD changed: {:?} -> {hash}", current_commit); + current_commit = Some(hash); + } + Ok(_) => { + log::trace!("HEAD unchanged, skipping build"); + pending_build = false; + continue; + } + Err(err) => { + log::error!("failed to read git HEAD: {err}"); + pending_build = false; + continue; + } } } } From b696a970278f2496e5d21b2ed7c68a232d55b76d Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 12:38:01 +0200 Subject: [PATCH 12/14] docs: reclassify changelog entry from Changed to Fixed Previous entry described a bug fix, not a behavior change. Entries now more accurately reflect their category. --- CHANGELOG.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bbdc4f..4bee471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,19 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `--commit` flag to also restart the command when git HEAD changes. (#36) - -### Changed - -- Commit-change detection now considers only valid event paths (existing, non-excluded, - non-hidden, non-backup files), ignoring stale or deleted paths that could previously - cause a `ChangeDetected` event to be emitted instead. (#36) +- `--commit` flag to restart the command when git HEAD changes. (#36) ### Fixed -- When `--commit` is used alongside a watch path that is a parent of `.git`, - git directory changes were incorrectly filtered out by the hidden-path check. - `is_hidden_path` now allows paths under explicitly-watched hidden directories. (#36) +- `is_hidden_path` no longer filters out paths under explicitly-watched hidden + directories, fixing an issue where `--commit` could not detect git changes when + the watch path was a parent of `.git`. (#36) + +- Commit-change detection now ignores stale or deleted paths that could previously + trigger a spurious `ChangeDetected` event. (#36) ## [0.3.5] - 2026-06-02 From 622588181f39cbb834998d65213f19a3c321a78f Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 12:39:56 +0200 Subject: [PATCH 13/14] docs: remove misleading changelog entry Stale/deleted path filtering was existing behavior, not a new fix. --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bee471..4f0babb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 directories, fixing an issue where `--commit` could not detect git changes when the watch path was a parent of `.git`. (#36) -- Commit-change detection now ignores stale or deleted paths that could previously - trigger a spurious `ChangeDetected` event. (#36) - ## [0.3.5] - 2026-06-02 ### Changed From 0576011b4661d74af0abcf6f5a88449009d5e531 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Mon, 22 Jun 2026 12:43:56 +0200 Subject: [PATCH 14/14] cargo fmt --- src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f410ac7..99cc399 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -269,8 +269,7 @@ impl Watch { self.prepare_excludes()?; let git_dirs: Vec = if self.commit { - let git_dir = resolve_git_dir() - .context("--commit requires a git repository")?; + let git_dir = resolve_git_dir().context("--commit requires a git repository")?; self.watch_paths.push(git_dir.clone()); vec![git_dir] } else { @@ -400,7 +399,10 @@ impl Watch { if let Some(current_hash) = ¤t_commit { match get_current_head() { Ok(hash) if &hash != current_hash => { - log::trace!("HEAD changed: {:?} -> {hash}", current_commit); + log::trace!( + "HEAD changed: {:?} -> {hash}", + current_commit + ); current_commit = Some(hash); } Ok(_) => {