on master help sign in
Index the history before printing a revset; resolve config id literals
33cb7e3iko
src/config.rs+19−7
@@ -464,17 +464,29 @@ fn resolve_ids_rec(
464 }
464 }
465}
465}
466
466
467−/// find Id literals per definition for config validation (§6.2.5)
467+/// Resolve every `@prefix` literal in the config's definitions against the
468−pub fn config_id_failures(
468+/// visible ids, in place (§1.2.5: "in `config.j`, every literal is resolved at
469+/// load"). Returns the ones that did not resolve to exactly one commit, as
470+/// (definition, prefix, candidates); those definitions are left untouched.
471+///
472+/// Checking without rewriting is not enough: an unresolved literal reaching
473+/// the evaluator crashes with `internal: unresolved id literal`, and because a
474+/// lambda body is not evaluated at load, it does so only when the definition
475+/// is eventually used.
476+pub fn resolve_config_ids(
469− cfg: &Config,
477+ cfg: &mut Config,
470 interp: &Interp,
478 interp: &Interp,
471) -> Vec<(String, String, Vec<String>)> {
479) -> Vec<(String, String, Vec<String>)> {
472 let mut out = Vec::new();
480 let mut out = Vec::new();
473− for (name, expr) in &cfg.defs {
481+ for (name, expr) in cfg.defs.iter_mut() {
474 let mut failures = Vec::new();
482 let mut failures = Vec::new();
475− resolve_ids_rec(expr, interp, &mut failures);
483+ let resolved = resolve_ids_rec(expr, interp, &mut failures);
484+ if failures.is_empty() {
485+ *expr = Rc::new(resolved);
486+ } else {
476− for (prefix, matches) in failures {
487+ for (prefix, matches) in failures {
477− out.push((name.clone(), prefix, matches));
488+ out.push((name.clone(), prefix, matches));
489+ }
478 }
490 }
479 }
491 }
480 out
492 out
src/domain.rs+42−3
@@ -237,17 +237,42 @@ pub struct MemBackend {
237 pub metas: HashMap<String, MetaInfo>,
237 pub metas: HashMap<String, MetaInfo>,
238 /// id -> parent change ids (first parent first)
238 /// id -> parent change ids (first parent first)
239 pub parents: HashMap<String, Vec<String>>,
239 pub parents: HashMap<String, Vec<String>>,
240+ /// Answer `has_conflict` and `is_empty` from the two maps below instead of
241+ /// returning None, the way the jj backend answers them from tree ids. A
242+ /// backend that answers them takes different paths through rendering and
243+ /// never forces a commit's file list, so tests that only ever used the
244+ /// None-answering default could not reach those paths at all.
245+ pub answers_tree_queries: bool,
246+ /// id -> the commit's tree holds a conflict
247+ pub conflicts: HashMap<String, bool>,
248+ /// id -> the commit's tree equals its first parent's
249+ pub empties: HashMap<String, bool>,
240}
250}
241
251
242impl MemBackend {
252impl MemBackend {
243 pub fn new() -> Self {
253 pub fn new() -> Self {
254+ MemBackend::default()
255+ }
256+
257+ /// A backend that answers the tree queries, like the jj backend does.
258+ pub fn answering() -> Self {
244 MemBackend {
259 MemBackend {
245− metas: HashMap::new(),
260+ answers_tree_queries: true,
246− parents: HashMap::new(),
261+ ..MemBackend::default()
247 }
262 }
248 }
263 }
249}
264}
250
265
266+/// A snapshot that is only materialised on first use, the way the jj backend
267+/// builds a commit's `files` (§7.2). The laziness must stay invisible to the
268+/// language, which is exactly what is easy to get wrong, so tests need to be
269+/// able to build repos this way.
270+pub fn lazy_files(entries: Vec<Value>) -> Value {
271+ Value::Thunk(Rc::new(crate::value::ThunkVal::new(move || {
272+ Ok(Value::list(entries))
273+ })))
274+}
275+
251impl Backend for MemBackend {
276impl Backend for MemBackend {
252 fn visible_ids(&self) -> Vec<String> {
277 fn visible_ids(&self) -> Vec<String> {
253 let mut ids: Vec<String> = self.metas.keys().cloned().collect();
278 let mut ids: Vec<String> = self.metas.keys().cloned().collect();
@@ -258,11 +283,25 @@ impl Backend for MemBackend {
258 self.metas
283 self.metas
259 .get(id)
284 .get(id)
260 .cloned()
285 .cloned()
261− .ok_or_else(|| Crash::new(format!("meta: no stored commit for id")))
286+ .ok_or_else(|| Crash::new(format!("meta: no stored commit for `{}`", id)))
262 }
287 }
263 fn is_merge(&self, id: &str) -> bool {
288 fn is_merge(&self, id: &str) -> bool {
264 self.parents.get(id).map(|p| p.len() >= 2).unwrap_or(false)
289 self.parents.get(id).map(|p| p.len() >= 2).unwrap_or(false)
265 }
290 }
291+ fn has_conflict(&self, id: &str) -> Option<bool> {
292+ if !self.answers_tree_queries {
293+ return None;
294+ }
295+ Some(self.conflicts.get(id).copied().unwrap_or(false))
296+ }
297+ fn is_empty(&self, id: &str) -> Option<bool> {
298+ if !self.answers_tree_queries {
299+ return None;
300+ }
301+ // like jj: only answerable against a recorded first parent
302+ self.parents.get(id)?.first()?;
303+ Some(self.empties.get(id).copied().unwrap_or(false))
304+ }
266 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
305 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
267 let mut seen = BTreeSet::new();
306 let mut seen = BTreeSet::new();
268 let mut stack: Vec<String> = ids.iter().cloned().collect();
307 let mut stack: Vec<String> = ids.iter().cloned().collect();
src/eval.rs+61−13
@@ -30,6 +30,43 @@ pub struct Interp {
30 /// history. The `Rc` is kept so the address cannot be reused while it is
30 /// history. The `Rc` is kept so the address cannot be reused while it is
31 /// a key (§4.1: definitions are pure, so the result cannot change).
31 /// a key (§4.1: definitions are pure, so the result cannot change).
32 revsets: RefCell<Vec<(&'static str, Rc<crate::value::RecordMap>, Value)>>,
32 revsets: RefCell<Vec<(&'static str, Rc<crate::value::RecordMap>, Value)>>,
33+ /// visible ids in sorted order, built on first use. The backend's
34+ /// `visible_ids` allocates every id afresh, and a shortest-unique-prefix
35+ /// needs the whole set, so computing one per displayed commit was
36+ /// quadratic in both allocations and comparisons.
37+ sorted_ids: RefCell<Option<Rc<Vec<String>>>>,
38+}
39+
40+/// length of the common leading run of two ids (ids are ASCII, so bytes)
41+fn common_prefix_len(a: &str, b: &str) -> usize {
42+ a.bytes()
43+ .zip(b.bytes())
44+ .take_while(|(x, y)| x == y)
45+ .count()
46+}
47+
48+/// The shortest prefix of `id` that no other id in `sorted` shares, at least
49+/// four characters long. Only the neighbours of `id` in sorted order can share
50+/// its longest prefix, so two lookups settle it.
51+pub fn unique_prefix_in(sorted: &[String], id: &str) -> String {
52+ let pos = sorted.partition_point(|o| o.as_str() < id);
53+ let mut shared = 0usize;
54+ if pos > 0 {
55+ shared = shared.max(common_prefix_len(&sorted[pos - 1], id));
56+ }
57+ let mut next = pos;
58+ while next < sorted.len() && sorted[next] == id {
59+ next += 1;
60+ }
61+ if next < sorted.len() {
62+ shared = shared.max(common_prefix_len(&sorted[next], id));
63+ }
64+ let n = 4.max(shared + 1);
65+ if n >= id.len() {
66+ id.to_string()
67+ } else {
68+ id[..n].to_string()
69+ }
33}
70}
34
71
35/// machine state
72/// machine state
@@ -179,9 +216,27 @@ impl Interp {
179 current_def: RefCell::new(None),
216 current_def: RefCell::new(None),
180 old_repo: RefCell::new(None),
217 old_repo: RefCell::new(None),
181 revsets: RefCell::new(Vec::new()),
218 revsets: RefCell::new(Vec::new()),
219+ sorted_ids: RefCell::new(None),
182 }
220 }
183 }
221 }
184
222
223+ /// Shortest prefix of `id` unique among the visible ids, minimum four
224+ /// (§5.1). The sorted id list is built once per run.
225+ pub fn unique_prefix(&self, id: &str) -> String {
226+ let cached = self.sorted_ids.borrow().clone();
227+ let sorted = match cached {
228+ Some(s) => s,
229+ None => {
230+ let mut ids = self.backend.visible_ids();
231+ ids.sort();
232+ let rc = Rc::new(ids);
233+ *self.sorted_ids.borrow_mut() = Some(rc.clone());
234+ rc
235+ }
236+ };
237+ unique_prefix_in(&sorted, id)
238+ }
239+
185 /// Apply one of the config revsets the binary calls itself, reusing the
240 /// Apply one of the config revsets the binary calls itself, reusing the
186 /// result when the same Repo value has already been asked (§7.5).
241 /// result when the same Repo value has already been asked (§7.5).
187 pub fn apply_cached_revset(&mut self, name: &'static str, repo: &Value) -> EResult {
242 pub fn apply_cached_revset(&mut self, name: &'static str, repo: &Value) -> EResult {
@@ -219,6 +274,7 @@ impl Interp {
219 current_def: RefCell::new(None),
274 current_def: RefCell::new(None),
220 old_repo: RefCell::new(None),
275 old_repo: RefCell::new(None),
221 revsets: RefCell::new(Vec::new()),
276 revsets: RefCell::new(Vec::new()),
277+ sorted_ids: RefCell::new(None),
222 }
278 }
223 }
279 }
224
280
@@ -296,26 +352,18 @@ impl Interp {
296 Run::Step(next) => st = next,
352 Run::Step(next) => st = next,
297 Run::Done(v) => return Ok(v),
353 Run::Done(v) => return Ok(v),
298 Run::Crash(c) => {
354 Run::Crash(c) => {
299− // unwind to the nearest Try continuation, restoring the
355+ // Unwind to the nearest Try continuation, restoring the
300 // fresh-id snapshot so a caught crash leaves no trace.
356 // fresh-id snapshot so a caught crash leaves no trace.
301− // A `Shared` continuation links back to a context already
357+ // A Try cannot catch a crash raised by its own handler:
302− // containing a Try, so track visited Try nodes and skip
358+ // every `on_crash` state continues through `Cont::Shared`
303− // ones already unwound past (a caught crash must not be
359+ // holding the continuation *outside* the Try, so the Try
304− // re-caught by the same Try, which would loop forever).
360+ // is no longer on the chain once the handler runs.
305 let mut k = Some(active.clone());
361 let mut k = Some(active.clone());
306− let mut visited: Vec<*const Cont> = Vec::new();
307 let caught = loop {
362 let caught = loop {
308 match k {
363 match k {
309 Some(Cont::Try {
364 Some(Cont::Try {
310 snapshot, on_crash, ..
365 snapshot, on_crash, ..
311 }) => {
366 }) => {
312− let ptr = &*on_crash as *const State as *const Cont;
313− let already = visited.contains(&ptr);
314− visited.push(ptr);
315− if already {
316− k = None;
317− continue;
318− }
319 *self.fresh.borrow_mut() = snapshot;
367 *self.fresh.borrow_mut() = snapshot;
320 break Some(*on_crash);
368 break Some(*on_crash);
321 }
369 }
src/jj.rs+23−8
@@ -350,7 +350,7 @@ impl JjBackend {
350
350
351 pub fn build_interp(
351 pub fn build_interp(
352 &self,
352 &self,
353− cfg: &Config,
353+ cfg: &mut Config,
354 _text: &str,
354 _text: &str,
355 snapshot: bool,
355 snapshot: bool,
356 ) -> Result<(Interp, Value, Value), OpenError> {
356 ) -> Result<(Interp, Value, Value), OpenError> {
@@ -392,6 +392,19 @@ impl JjBackend {
392 cfg.shapes.clone(),
392 cfg.shapes.clone(),
393 crate::value::Env::empty(),
393 crate::value::Env::empty(),
394 );
394 );
395+ // §1.2.5: resolve the config's `@prefix` literals now that the visible
396+ // ids are known, and before anything evaluates a definition
397+ if let Some((def, prefix, _)) =
398+ crate::config::resolve_config_ids(cfg, &interp).into_iter().next()
399+ {
400+ return Err((
401+ 3,
402+ format!(
403+ "config.j: `@{}` in `{}` does not resolve to a unique commit",
404+ prefix, def
405+ ),
406+ ));
407+ }
395 // §7.2: if the focused commit is in the immutable set, the snapshot
408 // §7.2: if the focused commit is in the immutable set, the snapshot
396 // cannot go into it; the focus is a new empty child of it holding the
409 // cannot go into it; the focus is a new empty child of it holding the
397 // snapshot, and persisting records that child
410 // snapshot, and persisting records that child
@@ -648,12 +661,14 @@ impl JjBackend {
648 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
661 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
649
662
650 // walk `new` top-down (§7.5 step 4), parents before children
663 // walk `new` top-down (§7.5 step 4), parents before children
664+ // a deque, not a Vec: `remove(0)` shifts everything after it, which
665+ // is quadratic in the number of commits for a wide history
666+ let mut queue: std::collections::VecDeque<(Value, String)> =
651− let mut queue: Vec<(Value, String)> = Vec::new(); // (subtree value, parent change id)
667+ std::collections::VecDeque::new(); // (subtree value, parent change id)
652 for kid in top.field("children")?.as_list()?.iter() {
668 for kid in top.field("children")?.as_list()?.iter() {
653− queue.push((kid.clone(), ROOT_ID.to_string()));
669+ queue.push_back((kid.clone(), ROOT_ID.to_string()));
654 }
670 }
655− while !queue.is_empty() {
656− let (subtree, parent_change) = queue.remove(0);
671+ while let Some((subtree, parent_change)) = queue.pop_front() {
657 let commit_v = subtree.field("root")?;
672 let commit_v = subtree.field("root")?;
658 let id = commit_id_of(&commit_v)?;
673 let id = commit_id_of(&commit_v)?;
659 let parent_jj = written
674 let parent_jj = written
@@ -712,7 +727,7 @@ impl JjBackend {
712 };
727 };
713 written.insert(id.clone(), new_id);
728 written.insert(id.clone(), new_id);
714 for kid in subtree.field("children")?.as_list()?.iter() {
729 for kid in subtree.field("children")?.as_list()?.iter() {
715− queue.push((kid.clone(), id.clone()));
730+ queue.push_back((kid.clone(), id.clone()));
716 }
731 }
717 }
732 }
718
733
@@ -909,7 +924,7 @@ impl JjBackend {
909 let _ = block_on(self.publish_tx(tx, "fetch"))?;
924 let _ = block_on(self.publish_tx(tx, "fetch"))?;
910 Ok(())
925 Ok(())
911 }
926 }
912− pub fn cmd_push(&self, cfg: &Config, expr_text: &str) -> Result<(), OpenError> {
927+ pub fn cmd_push(&self, cfg: &mut Config, expr_text: &str) -> Result<(), OpenError> {
913 let origin = RemoteName::new("origin");
928 let origin = RemoteName::new("origin");
914 let base = self.head_repo()?;
929 let base = self.head_repo()?;
915 let (value, vis) = self.eval_push_expr(cfg, expr_text, &base)?;
930 let (value, vis) = self.eval_push_expr(cfg, expr_text, &base)?;
@@ -923,7 +938,7 @@ impl JjBackend {
923 /// (§1.1), apply a function result to the repo value once (§7.6)
938 /// (§1.1), apply a function result to the repo value once (§7.6)
924 fn eval_push_expr(
939 fn eval_push_expr(
925 &self,
940 &self,
926− cfg: &Config,
941+ cfg: &mut Config,
927 expr_text: &str,
942 expr_text: &str,
928 base: &Arc<ReadonlyRepo>,
943 base: &Arc<ReadonlyRepo>,
929 ) -> Result<(Value, Arc<VisibleRepo>), OpenError> {
944 ) -> Result<(Value, Arc<VisibleRepo>), OpenError> {
src/main.rs+48−47
@@ -1,7 +1,6 @@
1//! j — a functional-programming-centered CLI for jj repositories (§1).
1//! j — a functional-programming-centered CLI for jj repositories (§1).
2
2
3use j::config::{self, Config};
3use j::config::{self, Config};
4−use j::eval::Interp;
5use j::jj::JjBackend;
4use j::jj::JjBackend;
6use j::parse::parse_expr;
5use j::parse::parse_expr;
7use j::value::{Crash, Value};
6use j::value::{Crash, Value};
@@ -17,6 +16,13 @@ fn err(status: u8, msg: impl std::fmt::Display) -> ExitCode {
17 ExitCode::from(status)
16 ExitCode::from(status)
18}
17}
19
18
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+
20fn crash_err(c: &Crash, expr_text: Option<&str>) -> ExitCode {
26fn crash_err(c: &Crash, expr_text: Option<&str>) -> ExitCode {
21 eprintln!("j: crash: {}", c.msg);
27 eprintln!("j: crash: {}", c.msg);
22 match (&c.def, expr_text) {
28 match (&c.def, expr_text) {
@@ -42,7 +48,7 @@ fn config_path() -> String {
42/// installed (the share/j/config.j file may not exist next to the binary).
48/// installed (the share/j/config.j file may not exist next to the binary).
43const DEFAULT_CONFIG: &str = include_str!("../config.j");
49const DEFAULT_CONFIG: &str = include_str!("../config.j");
44
50
45−fn load_config_file() -> Result<(Config, String), ExitCode> {
51+fn load_config_file() -> Result<(Config, String), u8> {
46 let path = config_path();
52 let path = config_path();
47 let src = match std::fs::read_to_string(&path) {
53 let src = match std::fs::read_to_string(&path) {
48 Ok(s) => s,
54 Ok(s) => s,
@@ -50,18 +56,18 @@ fn load_config_file() -> Result<(Config, String), ExitCode> {
50 // No user config yet: materialise the default there, editable by
56 // No user config yet: materialise the default there, editable by
51 // the current user, so they have a starting point to customise.
57 // the current user, so they have a starting point to customise.
52 if let Err(e) = write_default_config(&path) {
58 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)));
54 }
60 }
55 eprintln!("j: created an editable default config at {}", path);
61 eprintln!("j: created an editable default config at {}", path);
56 match std::fs::read_to_string(&path) {
62 match std::fs::read_to_string(&path) {
57 Ok(s) => s,
63 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))),
59 }
65 }
60 }
66 }
61 };
67 };
62 match config::load_config(&src) {
68 match config::load_config(&src) {
63 Ok(c) => Ok((c, src)),
69 Ok(c) => Ok((c, src)),
64− Err(e) => Err(err(3, e.message())),
70+ Err(e) => Err(fail(3, e.message())),
65 }
71 }
66}
72}
67
73
@@ -95,18 +101,36 @@ fn main() -> ExitCode {
95 .expect("worker thread panicked")
101 .expect("worker thread panicked")
96}
102}
97
103
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+
98fn run() -> ExitCode {
116fn run() -> ExitCode {
99 let args: Vec<String> = std::env::args().skip(1).collect();
117 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.
101 let mut stdin_content = String::new();
123 let mut stdin_content = String::new();
102 let stdin_has_data = {
124 let stdin_has_data = {
103 if std::io::stdin().is_terminal() {
125 if std::io::stdin().is_terminal() {
104 false
126 false
105− } else {
127+ } else if args.is_empty() || stdin_is_ready(50) {
106 match std::io::stdin().read_to_string(&mut stdin_content) {
128 match std::io::stdin().read_to_string(&mut stdin_content) {
107 Ok(n) => n > 0,
129 Ok(n) => n > 0,
108 Err(_) => false,
130 Err(_) => false,
109 }
131 }
132+ } else {
133+ false
110 }
134 }
111 };
135 };
112 if stdin_has_data && !args.is_empty() {
136 if stdin_has_data && !args.is_empty() {
@@ -185,10 +209,10 @@ fn run_reserved(cmd: &str, text: &str) -> ExitCode {
185 if rest.is_empty() {
209 if rest.is_empty() {
186 return err(2, "usage: j push EXPR");
210 return err(2, "usage: j push EXPR");
187 }
211 }
188− let (cfg, _) = or_exit(load_config_file());
212+ let (mut cfg, _) = or_exit(load_config_file());
189 let backend = or_exit(open_repo_locked(&cfg));
213 let backend = or_exit(open_repo_locked(&cfg));
190 // evaluate EXPR against the recorded repository (no snapshot)
214 // evaluate EXPR against the recorded repository (no snapshot)
191− match backend.cmd_push(&cfg, rest) {
215+ match backend.cmd_push(&mut cfg, rest) {
192 Ok(()) => ExitCode::SUCCESS,
216 Ok(()) => ExitCode::SUCCESS,
193 Err(e) => err(e.0, e.1),
217 Err(e) => err(e.0, e.1),
194 }
218 }
@@ -232,58 +256,44 @@ fn default_clone_dir(url: &str) -> String {
232 last.strip_suffix(".git").unwrap_or(last).to_string()
256 last.strip_suffix(".git").unwrap_or(last).to_string()
233}
257}
234
258
235−fn or_exit<T>(r: Result<T, ExitCode>) -> T {
259+fn or_exit<T>(r: Result<T, u8>) -> T {
236 match r {
260 match r {
237 Ok(v) => v,
261 Ok(v) => v,
238− Err(c) => std::process::exit(exit_code_number(c)),
262+ Err(code) => std::process::exit(code as i32),
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− }
252 }
263 }
253− 1
254}
264}
255
265
256−fn open_repo() -> Result<Rc<JjBackend>, ExitCode> {
266+fn open_repo() -> Result<Rc<JjBackend>, u8> {
257 match JjBackend::open(None) {
267 match JjBackend::open(None) {
258 Ok(b) => Ok(Rc::new(b)),
268 Ok(b) => Ok(Rc::new(b)),
259− Err(e) => Err(err(e.0, e.1)),
269+ Err(e) => Err(fail(e.0, e.1)),
260 }
270 }
261}
271}
262
272
263−fn open_repo_noconfig() -> Result<Rc<JjBackend>, ExitCode> {
273+fn open_repo_noconfig() -> Result<Rc<JjBackend>, u8> {
264 // undo/redo/ops do not read the config (§6.1), but take the lock
274 // undo/redo/ops do not read the config (§6.1), but take the lock
265 match JjBackend::open(None) {
275 match JjBackend::open(None) {
266 Ok(b) => {
276 Ok(b) => {
267 b.take_lock();
277 b.take_lock();
268 Ok(Rc::new(b))
278 Ok(Rc::new(b))
269 }
279 }
270− Err(e) => Err(err(e.0, e.1)),
280+ Err(e) => Err(fail(e.0, e.1)),
271 }
281 }
272}
282}
273
283
274−fn open_repo_locked(cfg: &Config) -> Result<Rc<JjBackend>, ExitCode> {
284+fn open_repo_locked(cfg: &Config) -> Result<Rc<JjBackend>, u8> {
275 match JjBackend::open(Some(cfg)) {
285 match JjBackend::open(Some(cfg)) {
276 Ok(b) => {
286 Ok(b) => {
277 b.take_lock();
287 b.take_lock();
278 Ok(Rc::new(b))
288 Ok(Rc::new(b))
279 }
289 }
280− Err(e) => Err(err(e.0, e.1)),
290+ Err(e) => Err(fail(e.0, e.1)),
281 }
291 }
282}
292}
283
293
284fn run_expression(text: &str, snapshot: bool) -> ExitCode {
294fn run_expression(text: &str, snapshot: bool) -> ExitCode {
285 // §1.2.1: locate the repository
295 // §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());
287 let backend = or_exit(open_repo_locked(&cfg));
297 let backend = or_exit(open_repo_locked(&cfg));
288
298
289 // §1.2.2: parse the expression
299 // §1.2.2: parse the expression
@@ -295,23 +305,15 @@ fn run_expression(text: &str, snapshot: bool) -> ExitCode {
295
305
296 // §1.2.4: build the current repository value (with snapshot)
306 // §1.2.4: build the current repository value (with snapshot)
297 let (mut interp, loaded_repo, current_repo) =
307 let (mut interp, loaded_repo, current_repo) =
298− match backend.build_interp(&cfg, text, snapshot) {
308+ match backend.build_interp(&mut cfg, text, snapshot) {
299 Ok(t) => t,
309 Ok(t) => t,
300 Err((2, m)) => return err(2, m),
310 Err((2, m)) => return err(2, m),
311+ Err((3, m)) => return err(3, m),
301 Err((_, m)) => return err(1, m),
312 Err((_, m)) => return err(1, m),
302 };
313 };
303
314
304− // §1.2.5: resolve Id literals in config and expression
315+ // §1.2.5: the config's Id literals were resolved by build_interp, as soon
305− for (def_name, prefix, candidates) in config::config_id_failures(&cfg, &interp) {
316+ // as the visible ids were known; the expression's are resolved here
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 expr = match config::resolve_ids(&expr, &interp) {
317 expr = match config::resolve_ids(&expr, &interp) {
316 Ok(e) => e,
318 Ok(e) => e,
317 Err(failures) => {
319 Err(failures) => {
@@ -349,7 +351,7 @@ fn run_expression(text: &str, snapshot: bool) -> ExitCode {
349 }
351 }
350
352
351 // §1.2.8: a Repo is persisted (unless unchanged)
353 // §1.2.8: a Repo is persisted (unless unchanged)
352− if is_repo_value(&interp, &v) {
354+ if is_repo_value(&v) {
353 match backend.persist(&mut interp, &cfg, &loaded_repo, &current_repo, &v, text) {
355 match backend.persist(&mut interp, &cfg, &loaded_repo, &current_repo, &v, text) {
354 Ok(()) => ExitCode::SUCCESS,
356 Ok(()) => ExitCode::SUCCESS,
355 Err(c) => crash_err(&c, Some(text)),
357 Err(c) => crash_err(&c, Some(text)),
@@ -367,7 +369,7 @@ fn run_expression(text: &str, snapshot: bool) -> ExitCode {
367 }
369 }
368}
370}
369
371
370−fn is_repo_value(interp: &Interp, v: &Value) -> bool {
372+fn is_repo_value(v: &Value) -> bool {
371 // §4.4: shape Repo and root's shape Commit
373 // §4.4: shape Repo and root's shape Commit
372 let fields = match v {
374 let fields = match v {
373 Value::Record(m) => m.keys().cloned().collect::<BTreeSet<String>>(),
375 Value::Record(m) => m.keys().cloned().collect::<BTreeSet<String>>(),
@@ -392,6 +394,5 @@ fn is_repo_value(interp: &Interp, v: &Value) -> bool {
392 .iter()
394 .iter()
393 .map(|s| s.to_string())
395 .map(|s| s.to_string())
394 .collect();
396 .collect();
395− let _ = interp;
396 root_fields == want_root
397 root_fields == want_root
397}
398}
src/render.rs+19−13
@@ -185,7 +185,7 @@ pub fn display(interp: &mut Interp, v: &Value, color: bool) -> Result<String, Cr
185}
185}
186
186
187fn display_id(interp: &Interp, id: &str, pal: &Palette) -> String {
187fn display_id(interp: &Interp, id: &str, pal: &Palette) -> String {
188− let p = interp.backend.unique_prefix(id);
188+ let p = interp.unique_prefix(id);
189 let n = p.len();
189 let n = p.len();
190 format!("{}{}", pal.bold(p.as_str()), pal.dim(&id[n..]))
190 format!("{}{}", pal.bold(p.as_str()), pal.dim(&id[n..]))
191}
191}
@@ -712,16 +712,24 @@ fn display_list_block(
712 }
712 }
713 // list of Id: commit table (one commit line per id, in list order)
713 // list of Id: commit table (one commit line per id, in list order)
714 if xs.iter().all(|x| matches!(x, Value::Id(_))) {
714 if xs.iter().all(|x| matches!(x, Value::Id(_))) {
715+ // index the history once: a `by_id` walk per id made printing a
716+ // revset quadratic in the size of the history
717+ let repo = top_of(interp, &xs[0])?;
718+ let mut commits: BTreeMap<String, Value> = BTreeMap::new();
719+ for c in crate::repo::all_commits(&repo)? {
720+ if let Ok(Value::Id(i)) = c.field("id") {
721+ commits.insert(i.to_string(), c);
722+ }
723+ }
715 for x in xs {
724 for x in xs {
716 let id = match x {
725 let id = match x {
717 Value::Id(i) => i.to_string(),
726 Value::Id(i) => i.to_string(),
718 _ => unreachable!(),
727 _ => unreachable!(),
719 };
728 };
720 out.push_str(&pad);
729 out.push_str(&pad);
721− let repo = top_of(interp, x)?;
730+ match commits.get(&id) {
722− match crate::repo::by_id(&repo, &id)? {
723− Some(loc) => {
731+ Some(c) => {
724− let c = loc.field("root")?;
732+ let c = c.clone();
725 let conflict = has_conflict(&c)?;
733 let conflict = has_conflict(&c)?;
726 let glyph = if conflict {
734 let glyph = if conflict {
727 pal.red("⊗")
735 pal.red("⊗")
@@ -737,7 +745,7 @@ fn display_list_block(
737 .map(|l| l.as_text().map(|s| s.to_string()))
745 .map(|l| l.as_text().map(|s| s.to_string()))
738 .collect::<Result<_, _>>()?;
746 .collect::<Result<_, _>>()?;
739 let id_s = if conflict {
747 let id_s = if conflict {
740− pal.red(&interp.backend.unique_prefix(&id))
748+ pal.red(&interp.unique_prefix(&id))
741 } else {
749 } else {
742 display_id(interp, &id, pal)
750 display_id(interp, &id, pal)
743 };
751 };
@@ -1263,16 +1271,14 @@ fn tree_render(
1263 // Step 3 — lanes
1271 // Step 3 — lanes
1264 let placements = assign_lanes(&rows, &trunk, lanes_n);
1272 let placements = assign_lanes(&rows, &trunk, lanes_n);
1265 // id column: shortest unique prefix among the display tree's commits, min 4
1273 // id column: shortest unique prefix among the display tree's commits, min 4
1274+ // (sorted once, so each row costs a lookup rather than a scan of them all)
1266− let ids: Vec<&str> = rows
1275+ let mut ids: Vec<String> = rows
1267 .iter()
1276 .iter()
1268− .filter_map(|n| n.commit().map(|c| c.id.as_str()))
1277+ .filter_map(|n| n.commit().map(|c| c.id.clone()))
1269 .collect();
1278 .collect();
1279+ ids.sort();
1270 let prefix_len = |id: &str| -> usize {
1280 let prefix_len = |id: &str| -> usize {
1271− let mut n = 4.min(id.len());
1281+ crate::eval::unique_prefix_in(&ids, id).len()
1272− while n < id.len() && ids.iter().any(|o| *o != id && o.starts_with(&id[..n])) {
1273− n += 1;
1274− }
1275− n
1276 };
1282 };
1277 // Step 4 — draw
1283 // Step 4 — draw
1278 draw_rows(&rows, &placements, opts, pal, lanes_n, with_focus, &prefix_len)
1284 draw_rows(&rows, &placements, opts, pal, lanes_n, with_focus, &prefix_len)
src/value.rs−5
@@ -270,16 +270,11 @@ pub fn render_conflict(sides: &[Rc<Vec<u8>>]) -> Vec<u8> {
270 let mut out = Vec::new();
270 let mut out = Vec::new();
271 out.extend_from_slice(b"<<<<<<<\n");
271 out.extend_from_slice(b"<<<<<<<\n");
272 let mut i = 0;
272 let mut i = 0;
273− let mut first_add = true;
274 while i < sides.len() {
273 while i < sides.len() {
275 if i % 2 == 0 {
274 if i % 2 == 0 {
276− if !first_add {
277− // another add after removes: separate
278− }
279 out.extend_from_slice(b"+++++++\n");
275 out.extend_from_slice(b"+++++++\n");
280 out.extend_from_slice(&sides[i]);
276 out.extend_from_slice(&sides[i]);
281 ensure_newline(&mut out);
277 ensure_newline(&mut out);
282− first_add = false;
283 } else {
278 } else {
284 out.extend_from_slice(b"%%%%%%%\n");
279 out.extend_from_slice(b"%%%%%%%\n");
285 out.extend_from_slice(&sides[i]);
280 out.extend_from_slice(&sides[i]);
tests/display.rs+112
@@ -328,3 +328,115 @@ fn entry_list_aligns_by_display_width() {
328 out
328 out
329 );
329 );
330}
330}
331+
332+// ----------------------------------------------------------------------
333+// The jj backend differs from the default in-memory one in two ways that
334+// changed behaviour and were invisible to the suite: a commit's `files` is a
335+// thunk until asked for, and `has_conflict`/`is_empty` are answered from tree
336+// ids rather than by walking files. `MemBackend::answering` plus `lazy_files`
337+// reproduce both.
338+// ----------------------------------------------------------------------
339+
340+fn lazy_commit(id: &str, msg: &str, files: Vec<(&str, &str)>) -> Value {
341+ let entries: Vec<Value> = files
342+ .into_iter()
343+ .map(|(p, c)| {
344+ Value::record(&[
345+ ("content", BlobVal::text_blob(c)),
346+ ("path", Value::list(p.split('/').map(Value::text).collect())),
347+ ])
348+ })
349+ .collect();
350+ Value::record(&[
351+ ("files", j::domain::lazy_files(entries)),
352+ ("id", Value::Id(Rc::new(id.to_string()))),
353+ ("labels", Value::list(vec![])),
354+ ("message", Value::text(msg)),
355+ ])
356+}
357+
358+/// A linear repo of `n` commits above the root whose file lists are lazy,
359+/// focused on the last. It has to be long enough that a commit in the middle
360+/// has a parent nothing asked the files of — that is the case where emptiness
361+/// had nothing to fall back on.
362+fn lazy_repo_chain(n: usize) -> (Value, MemBackend) {
363+ let ids: Vec<String> = (0..n).map(|k| format!("kpqx{:04}", k)).collect();
364+ let mut be = MemBackend::answering();
365+ be.metas.insert(ROOT_ID.to_string(), meta(ROOT_ID, "Root", 1_700_000_000).1);
366+ be.parents.insert(ROOT_ID.to_string(), vec![]);
367+ let mut frames: Vec<Value> = vec![Value::record(&[
368+ ("left", Value::list(vec![])),
369+ ("parent", lazy_commit(ROOT_ID, "", vec![])),
370+ ("right", Value::list(vec![])),
371+ ])];
372+ let mut files: Vec<(String, String)> = Vec::new();
373+ let mut focus = None;
374+ for (k, id) in ids.iter().enumerate() {
375+ files.push((format!("src/f{}.rs", k), format!("fn f{}() {{}}\n", k)));
376+ let as_refs: Vec<(&str, &str)> =
377+ files.iter().map(|(p, c)| (p.as_str(), c.as_str())).collect();
378+ let c = lazy_commit(id, &format!("commit {}", k), as_refs);
379+ be.metas.insert(id.clone(), meta(id, "M", 1_700_000_100 + k as i64).1);
380+ let parent = if k == 0 { ROOT_ID.to_string() } else { ids[k - 1].clone() };
381+ be.parents.insert(id.clone(), vec![parent]);
382+ be.empties.insert(id.clone(), false);
383+ if k + 1 == n {
384+ focus = Some(c);
385+ } else {
386+ frames.push(Value::record(&[
387+ ("left", Value::list(vec![])),
388+ ("parent", c),
389+ ("right", Value::list(vec![])),
390+ ]));
391+ }
392+ }
393+ frames.reverse();
394+ let repo = Value::record(&[
395+ ("children", Value::list(vec![])),
396+ ("context", Value::list(frames)),
397+ ("root", focus.unwrap()),
398+ ]);
399+ (repo, be)
400+}
401+
402+fn lazy_repo() -> (Value, MemBackend) {
403+ lazy_repo_chain(2)
404+}
405+
406+#[test]
407+fn lazy_file_lists_stay_invisible_to_the_language() {
408+ // `extract` walked past an unforced `files` thunk, so it found nothing on
409+ // a real repository while finding everything in memory
410+ let (repo, be) = lazy_repo();
411+ let (mut i, cfg) = make_interp(be);
412+ for (src, want) in [
413+ ("length (extract Entry repo)", "3"),
414+ ("length (extract Blob repo)", "3"),
415+ ("length (extract Commit repo)", "3"),
416+ ("length (files repo)", "2"),
417+ ] {
418+ let out = eval_and_display(
419+ &mut i,
420+ &cfg,
421+ &format!("(\\repo -> {})", src),
422+ repo.clone(),
423+ );
424+ assert_eq!(out.trim(), want, "{}", src);
425+ }
426+}
427+
428+#[test]
429+fn tree_emptiness_uses_the_backend_when_files_are_lazy() {
430+ // `empty` fell back to a file count that is 0 whenever the list was never
431+ // materialised, so a backend that answers `is_empty` (and therefore never
432+ // forces the list) marked every commit empty
433+ // six commits: the middle ones have a parent whose files nothing needs
434+ let (repo, be) = lazy_repo_chain(6);
435+ let (mut i, cfg) = make_interp(be);
436+ let out = eval_and_display(&mut i, &cfg, "tree", repo);
437+ let rows: Vec<&str> = out.lines().filter(|l| l.contains("commit ")).collect();
438+ assert_eq!(rows.len(), 6, "{}", out);
439+ for row in rows {
440+ assert!(!row.contains('◌'), "wrongly marked empty: {:?}\n{}", row, out);
441+ }
442+}
tests/show.rs+43
@@ -335,3 +335,46 @@ fn render_date_matches_utc_calendar() {
335 assert_eq!(j::render::render_date(t), want, "t={}", t);
335 assert_eq!(j::render::render_date(t), want, "t={}", t);
336 }
336 }
337}
337}
338+
339+#[test]
340+fn unique_prefix_is_shortest_and_unambiguous() {
341+ // computed from sorted neighbours rather than a scan of every id; the
342+ // answers must match the definition: shortest prefix no other id shares,
343+ // minimum four characters
344+ let ids: Vec<String> = [
345+ "kkkkllll", "kkkkmmmm", "kkkmnnnn", "lllloooo", "zzzzzzzz",
346+ ]
347+ .iter()
348+ .map(|s| s.to_string())
349+ .collect();
350+ let p = |id: &str| j::eval::unique_prefix_in(&ids, id);
351+ // shares "kkkk" with kkkkmmmm, so needs five
352+ assert_eq!(p("kkkkllll"), "kkkkl");
353+ assert_eq!(p("kkkkmmmm"), "kkkkm");
354+ // differs from the others by the fourth character: the minimum applies
355+ assert_eq!(p("kkkmnnnn"), "kkkm");
356+ assert_eq!(p("lllloooo"), "llll");
357+ assert_eq!(p("zzzzzzzz"), "zzzz");
358+ // an id not in the set is measured against the set all the same:
359+ // "kkkkll" would still be a prefix of kkkkllll, so it needs seven
360+ assert_eq!(p("kkkkllmm"), "kkkkllm");
361+ // and every answer really is unique among the others
362+ for id in &ids {
363+ let pre = p(id);
364+ assert!(pre.len() >= 4, "{} -> {}", id, pre);
365+ let sharers = ids.iter().filter(|o| *o != id && o.starts_with(&pre)).count();
366+ assert_eq!(sharers, 0, "{} -> {} is shared", id, pre);
367+ // and it is the shortest such prefix
368+ if pre.len() > 4 {
369+ let shorter = &id[..pre.len() - 1];
370+ assert!(
371+ ids.iter().any(|o| o != id && o.starts_with(shorter)),
372+ "{} -> {} is longer than needed",
373+ id,
374+ pre
375+ );
376+ }
377+ }
378+ // an empty set still respects the minimum
379+ assert_eq!(j::eval::unique_prefix_in(&[], "kkkkllll"), "kkkk");
380+}
tests/validation.rs+18−3
@@ -330,11 +330,26 @@ fn id_literals_in_config_resolve() {
330 },
330 },
331 );
331 );
332 let i = Interp::new(Rc::new(b), cfg.shapes.clone(), Env::empty());
332 let i = Interp::new(Rc::new(b), cfg.shapes.clone(), Env::empty());
333− let failures = config::config_id_failures(&cfg, &i);
333+ let mut resolved = config::load_config(&src).unwrap();
334+ let failures = config::resolve_config_ids(&mut resolved, &i);
334 assert!(failures.is_empty());
335 assert!(failures.is_empty());
336+ // resolving rewrites the definition: evaluating it must not hit the
337+ // literal again (it used to crash with `internal: unresolved id literal`,
338+ // and only when the definition was finally used)
339+ let mut i3 = Interp::new(
340+ Rc::new(MemBackend::new()),
341+ resolved.shapes.clone(),
342+ Env::empty(),
343+ );
344+ config::eval_config(&mut i3, &resolved).expect("resolved config evaluates");
345+ match i3.globals.lookup("target") {
346+ Some(Value::Id(id)) => assert_eq!(&*id, "kqqqqqqq"),
347+ other => panic!("target is {:?}", other),
348+ }
335− // unknown id fails
349+ // unknown id fails, naming the definition
336 let i2 = Interp::new(Rc::new(MemBackend::new()), cfg.shapes.clone(), Env::empty());
350 let i2 = Interp::new(Rc::new(MemBackend::new()), cfg.shapes.clone(), Env::empty());
337− let failures = config::config_id_failures(&cfg, &i2);
351+ let mut unresolved = config::load_config(&src).unwrap();
352+ let failures = config::resolve_config_ids(&mut unresolved, &i2);
338 assert_eq!(failures.len(), 1);
353 assert_eq!(failures.len(), 1);
339 assert_eq!(failures[0].0, "target");
354 assert_eq!(failures[0].0, "target");
340}
355}