Skip to content

Commit 12cab8f

Browse files
committed
Improve affected stdin auto detection
1 parent 2f3491e commit 12cab8f

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

crates/app/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,8 @@ tiny_http = "0.12.0"
100100
serial_test = { workspace = true }
101101
starbase_sandbox = { workspace = true }
102102

103+
[target.'cfg(unix)'.dependencies]
104+
nix = { version = "0.31.3", features = ["poll"] }
105+
103106
[lints]
104107
workspace = true

crates/app/src/queries/changed_files.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@ use moon_common::is_ci;
44
use moon_common::path::{WorkspaceRelativePathBuf, standardize_separators};
55
use moon_env_var::GlobalEnvBag;
66
use moon_vcs::{BoxedVcs, ChangedFiles, ChangedStatus};
7+
#[cfg(unix)]
8+
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
79
use rustc_hash::FxHashSet;
810
use serde::{Deserialize, Serialize};
911
use starbase_styles::color;
1012
use starbase_utils::json;
13+
#[cfg(unix)]
14+
use std::os::fd::AsFd;
1115
use std::io::{IsTerminal, Read, stdin};
1216
use tracing::{debug, warn};
1317

18+
#[cfg(unix)]
19+
const STDIN_READY_TIMEOUT_MS: u8 = 100;
20+
1421
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1522
#[serde(rename_all = "camelCase")]
1623
pub struct QueryChangedFilesOptions {
@@ -225,7 +232,7 @@ pub async fn query_changed_files_for_affected(
225232
let mut options = QueryChangedFilesOptions {
226233
default_branch: ci,
227234
local: !ci,
228-
stdin: true,
235+
stdin: should_read_from_stdin()?,
229236
..Default::default()
230237
};
231238

@@ -237,3 +244,35 @@ pub async fn query_changed_files_for_affected(
237244
.await
238245
.map(|result| result.files)
239246
}
247+
248+
fn should_read_from_stdin() -> miette::Result<bool> {
249+
if stdin().is_terminal() {
250+
return Ok(false);
251+
}
252+
253+
// Non-TTY stdin can be an open CI handle with no data, so only read when ready.
254+
is_stdin_readable()
255+
}
256+
257+
#[cfg(unix)]
258+
fn is_stdin_readable() -> miette::Result<bool> {
259+
let stdin = stdin();
260+
let mut poll_fds = [PollFd::new(stdin.as_fd(), PollFlags::POLLIN)];
261+
let ready = poll(
262+
&mut poll_fds,
263+
PollTimeout::from(STDIN_READY_TIMEOUT_MS),
264+
)
265+
.into_diagnostic()?;
266+
267+
let readable_events = PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR;
268+
269+
Ok(ready > 0
270+
&& poll_fds[0]
271+
.revents()
272+
.is_some_and(|events| events.intersects(readable_events)))
273+
}
274+
275+
#[cfg(not(unix))]
276+
fn is_stdin_readable() -> miette::Result<bool> {
277+
Ok(true)
278+
}

crates/cli/tests/query_affected_test.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,29 @@ mod query_affected {
4646
);
4747
}
4848

49+
#[test]
50+
fn includes_project_via_stdin() {
51+
let sandbox = create_query_sandbox();
52+
53+
let assert = sandbox.run_bin(|cmd| {
54+
cmd.arg("query")
55+
.arg("affected")
56+
.write_stdin("basic/file.txt");
57+
});
58+
59+
let mut affected: Affected = serde_json::from_str(assert.stdout().trim()).unwrap();
60+
61+
assert!(!affected.projects.contains_key("advanced"));
62+
assert_eq!(
63+
affected.projects.remove("basic").unwrap(),
64+
AffectedProjectState {
65+
files: FxHashSet::from_iter(["basic/file.txt".into()]),
66+
tasks: FxHashSet::from_iter([Target::parse("basic:dev").unwrap()]),
67+
..Default::default()
68+
}
69+
);
70+
}
71+
4972
#[test]
5073
fn includes_task_for_input() {
5174
let sandbox = create_query_sandbox();

0 commit comments

Comments
 (0)