@@ -1,7 +1,6 @@
11//! j — a functional-programming-centered CLI for jj repositories (§1).
22
33use j::config::{self, Config};
4−use j::eval::Interp;
54use j::jj::JjBackend;
65use j::parse::parse_expr;
76use j::value::{Crash, Value};
@@ -17,6 +16,13 @@ fn err(status: u8, msg: impl std::fmt::Display) -> ExitCode {
1716 ExitCode::from(status)
1817}
1918
19+/// Report a failure that ends the process, keeping the status as a number so
20+/// callers do not have to recover it from an `ExitCode`.
21+fn fail(status: u8, msg: impl std::fmt::Display) -> u8 {
22+ eprintln!("j: {}", msg);
23+ status
24+}
25+
2026fn crash_err(c: &Crash, expr_text: Option<&str>) -> ExitCode {
2127 eprintln!("j: crash: {}", c.msg);
2228 match (&c.def, expr_text) {
@@ -42,7 +48,7 @@ fn config_path() -> String {
4248/// installed (the share/j/config.j file may not exist next to the binary).
4349const DEFAULT_CONFIG: &str = include_str!("../config.j");
4450
45−fn load_config_file() -> Result<(Config, String), ExitCode> {
51+fn load_config_file() -> Result<(Config, String), u8> {
4652 let path = config_path();
4753 let src = match std::fs::read_to_string(&path) {
4854 Ok(s) => s,
@@ -50,18 +56,18 @@ fn load_config_file() -> Result<(Config, String), ExitCode> {
5056 // No user config yet: materialise the default there, editable by
5157 // the current user, so they have a starting point to customise.
5258 if let Err(e) = write_default_config(&path) {
53− return Err(err(3, format!("cannot create config at {}: {}", path, e)));
59+ return Err(fail(3, format!("cannot create config at {}: {}", path, e)));
5460 }
5561 eprintln!("j: created an editable default config at {}", path);
5662 match std::fs::read_to_string(&path) {
5763 Ok(s) => s,
58− Err(e) => return Err(err(3, format!("cannot read config at {}: {}", path, e))),
64+ Err(e) => return Err(fail(3, format!("cannot read config at {}: {}", path, e))),
5965 }
6066 }
6167 };
6268 match config::load_config(&src) {
6369 Ok(c) => Ok((c, src)),
64− Err(e) => Err(err(3, e.message())),
70+ Err(e) => Err(fail(3, e.message())),
6571 }
6672}
6773
@@ -95,18 +101,36 @@ fn main() -> ExitCode {
95101 .expect("worker thread panicked")
96102}
97103
104+/// True when stdin has input (or end-of-input) waiting within `ms`
105+/// milliseconds. A closed or exhausted stdin reports ready at once, so the
106+/// wait is only paid for a pipe that is open but idle.
107+fn stdin_is_ready(ms: i32) -> bool {
108+ let mut fds = libc::pollfd {
109+ fd: libc::STDIN_FILENO,
110+ events: libc::POLLIN,
111+ revents: 0,
112+ };
113+ unsafe { libc::poll(&mut fds, 1, ms) > 0 }
114+}
115+
98116fn run() -> ExitCode {
99117 let args: Vec<String> = std::env::args().skip(1).collect();
100− // §1: stdin vs arguments
118+ // §1: stdin vs arguments. With no arguments the expression comes from
119+ // stdin, so reading to end-of-input is the whole point. With arguments,
120+ // stdin is only read to report the "both" error — and reading it blindly
121+ // hangs whenever the process inherited a pipe nobody is writing to (a
122+ // shell loop, a CI step), so wait only briefly for input to appear.
101123 let mut stdin_content = String::new();
102124 let stdin_has_data = {
103125 if std::io::stdin().is_terminal() {
104126 false
105− } else {
127+ } else if args.is_empty() || stdin_is_ready(50) {
106128 match std::io::stdin().read_to_string(&mut stdin_content) {
107129 Ok(n) => n > 0,
108130 Err(_) => false,
109131 }
132+ } else {
133+ false
110134 }
111135 };
112136 if stdin_has_data && !args.is_empty() {
@@ -185,10 +209,10 @@ fn run_reserved(cmd: &str, text: &str) -> ExitCode {
185209 if rest.is_empty() {
186210 return err(2, "usage: j push EXPR");
187211 }
188− let (cfg, _) = or_exit(load_config_file());
212+ let (mut cfg, _) = or_exit(load_config_file());
189213 let backend = or_exit(open_repo_locked(&cfg));
190214 // evaluate EXPR against the recorded repository (no snapshot)
191− match backend.cmd_push(&cfg, rest) {
215+ match backend.cmd_push(&mut cfg, rest) {
192216 Ok(()) => ExitCode::SUCCESS,
193217 Err(e) => err(e.0, e.1),
194218 }
@@ -232,58 +256,44 @@ fn default_clone_dir(url: &str) -> String {
232256 last.strip_suffix(".git").unwrap_or(last).to_string()
233257}
234258
235−fn or_exit<T>(r: Result<T, ExitCode>) -> T {
259+fn or_exit<T>(r: Result<T, u8>) -> T {
236260 match r {
237261 Ok(v) => v,
238− Err(c) => std::process::exit(exit_code_number(c)),
239− }
240−}
241−
242−/// ExitCode doesn't expose its value; track it alongside.
243−fn exit_code_number(c: ExitCode) -> i32 {
244− // ExitCode's Debug is "ExitCode(unix_exit_status(N))" on unix
245− let s = format!("{:?}", c);
246− for tok in s.split(|ch: char| !ch.is_ascii_digit()) {
247− if !tok.is_empty() {
248− if let Ok(n) = tok.parse() {
249− return n;
250− }
251− }
262+ Err(code) => std::process::exit(code as i32),
252263 }
253− 1
254264}
255265
256−fn open_repo() -> Result<Rc<JjBackend>, ExitCode> {
266+fn open_repo() -> Result<Rc<JjBackend>, u8> {
257267 match JjBackend::open(None) {
258268 Ok(b) => Ok(Rc::new(b)),
259− Err(e) => Err(err(e.0, e.1)),
269+ Err(e) => Err(fail(e.0, e.1)),
260270 }
261271}
262272
263−fn open_repo_noconfig() -> Result<Rc<JjBackend>, ExitCode> {
273+fn open_repo_noconfig() -> Result<Rc<JjBackend>, u8> {
264274 // undo/redo/ops do not read the config (§6.1), but take the lock
265275 match JjBackend::open(None) {
266276 Ok(b) => {
267277 b.take_lock();
268278 Ok(Rc::new(b))
269279 }
270− Err(e) => Err(err(e.0, e.1)),
280+ Err(e) => Err(fail(e.0, e.1)),
271281 }
272282}
273283
274−fn open_repo_locked(cfg: &Config) -> Result<Rc<JjBackend>, ExitCode> {
284+fn open_repo_locked(cfg: &Config) -> Result<Rc<JjBackend>, u8> {
275285 match JjBackend::open(Some(cfg)) {
276286 Ok(b) => {
277287 b.take_lock();
278288 Ok(Rc::new(b))
279289 }
280− Err(e) => Err(err(e.0, e.1)),
290+ Err(e) => Err(fail(e.0, e.1)),
281291 }
282292}
283293
284294fn run_expression(text: &str, snapshot: bool) -> ExitCode {
285295 // §1.2.1: locate the repository
286− let (cfg, _cfg_src) = or_exit(load_config_file());
296+ let (mut cfg, _cfg_src) = or_exit(load_config_file());
287297 let backend = or_exit(open_repo_locked(&cfg));
288298
289299 // §1.2.2: parse the expression
@@ -295,23 +305,15 @@ fn run_expression(text: &str, snapshot: bool) -> ExitCode {
295305
296306 // §1.2.4: build the current repository value (with snapshot)
297307 let (mut interp, loaded_repo, current_repo) =
298− match backend.build_interp(&cfg, text, snapshot) {
308+ match backend.build_interp(&mut cfg, text, snapshot) {
299309 Ok(t) => t,
300310 Err((2, m)) => return err(2, m),
311+ Err((3, m)) => return err(3, m),
301312 Err((_, m)) => return err(1, m),
302313 };
303314
304− // §1.2.5: resolve Id literals in config and expression
305− for (def_name, prefix, candidates) in config::config_id_failures(&cfg, &interp) {
306− let _ = candidates;
307− return err(
308− 3,
309− format!(
310− "config.j: `@{}` in `{}` does not resolve to a unique commit",
311− prefix, def_name
312− ),
313− );
314− }
315+ // §1.2.5: the config's Id literals were resolved by build_interp, as soon
316+ // as the visible ids were known; the expression's are resolved here
315317 expr = match config::resolve_ids(&expr, &interp) {
316318 Ok(e) => e,
317319 Err(failures) => {
@@ -349,7 +351,7 @@ fn run_expression(text: &str, snapshot: bool) -> ExitCode {
349351 }
350352
351353 // §1.2.8: a Repo is persisted (unless unchanged)
352− if is_repo_value(&interp, &v) {
354+ if is_repo_value(&v) {
353355 match backend.persist(&mut interp, &cfg, &loaded_repo, ¤t_repo, &v, text) {
354356 Ok(()) => ExitCode::SUCCESS,
355357 Err(c) => crash_err(&c, Some(text)),
@@ -367,7 +369,7 @@ fn run_expression(text: &str, snapshot: bool) -> ExitCode {
367369 }
368370}
369371
370−fn is_repo_value(interp: &Interp, v: &Value) -> bool {
372+fn is_repo_value(v: &Value) -> bool {
371373 // §4.4: shape Repo and root's shape Commit
372374 let fields = match v {
373375 Value::Record(m) => m.keys().cloned().collect::<BTreeSet<String>>(),
@@ -392,6 +394,5 @@ fn is_repo_value(interp: &Interp, v: &Value) -> bool {
392394 .iter()
393395 .map(|s| s.to_string())
394396 .collect();
395− let _ = interp;
396397 root_fields == want_root
397398}